• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "gn/xcode_writer.h"
6 
7 #include <iomanip>
8 #include <iterator>
9 #include <map>
10 #include <memory>
11 #include <sstream>
12 #include <string>
13 #include <utility>
14 
15 #include "base/environment.h"
16 #include "base/logging.h"
17 #include "base/sha1.h"
18 #include "base/strings/string_number_conversions.h"
19 #include "base/strings/string_util.h"
20 #include "gn/args.h"
21 #include "gn/build_settings.h"
22 #include "gn/builder.h"
23 #include "gn/commands.h"
24 #include "gn/deps_iterator.h"
25 #include "gn/filesystem_utils.h"
26 #include "gn/settings.h"
27 #include "gn/source_file.h"
28 #include "gn/target.h"
29 #include "gn/value.h"
30 #include "gn/variables.h"
31 #include "gn/xcode_object.h"
32 
33 namespace {
34 
35 using TargetToFileList = std::unordered_map<const Target*, Target::FileList>;
36 using TargetToTarget = std::unordered_map<const Target*, const Target*>;
37 using TargetToPBXTarget = std::unordered_map<const Target*, PBXTarget*>;
38 
39 const char* kXCTestFileSuffixes[] = {
40     "egtest.m",
41     "egtest.mm",
42     "xctest.m",
43     "xctest.mm",
44 };
45 
46 const char kXCTestModuleTargetNamePostfix[] = "_module";
47 const char kXCUITestRunnerTargetNamePostfix[] = "_runner";
48 
49 struct SafeEnvironmentVariableInfo {
50   const char* name;
51   bool capture_at_generation;
52 };
53 
54 SafeEnvironmentVariableInfo kSafeEnvironmentVariables[] = {
55     {"HOME", true},
56     {"LANG", true},
57     {"PATH", true},
58     {"USER", true},
59     {"TMPDIR", false},
60     {"ICECC_VERSION", true},
61     {"ICECC_CLANG_REMOTE_CPP", true}};
62 
GetTargetOs(const Args & args)63 XcodeWriter::TargetOsType GetTargetOs(const Args& args) {
64   const Value* target_os_value = args.GetArgOverride(variables::kTargetOs);
65   if (target_os_value) {
66     if (target_os_value->type() == Value::STRING) {
67       if (target_os_value->string_value() == "ios")
68         return XcodeWriter::WRITER_TARGET_OS_IOS;
69     }
70   }
71   return XcodeWriter::WRITER_TARGET_OS_MACOS;
72 }
73 
GetNinjaExecutable(const std::string & ninja_executable)74 std::string GetNinjaExecutable(const std::string& ninja_executable) {
75   return ninja_executable.empty() ? "ninja" : ninja_executable;
76 }
77 
GetBuildScript(const std::string & target_name,const std::string & ninja_executable,const std::string & ninja_extra_args,base::Environment * environment)78 std::string GetBuildScript(const std::string& target_name,
79                            const std::string& ninja_executable,
80                            const std::string& ninja_extra_args,
81                            base::Environment* environment) {
82   std::stringstream script;
83   script << "echo note: \"Compile and copy " << target_name << " via ninja\"\n"
84          << "exec ";
85 
86   // Launch ninja with a sanitized environment (Xcode sets many environment
87   // variable overridding settings, including the SDK, thus breaking hermetic
88   // build).
89   script << "env -i ";
90   for (const auto& variable : kSafeEnvironmentVariables) {
91     script << variable.name << "=\"";
92 
93     std::string value;
94     if (variable.capture_at_generation)
95       environment->GetVar(variable.name, &value);
96 
97     if (!value.empty())
98       script << value;
99     else
100       script << "$" << variable.name;
101     script << "\" ";
102   }
103 
104   script << GetNinjaExecutable(ninja_executable) << " -C .";
105   if (!ninja_extra_args.empty())
106     script << " " << ninja_extra_args;
107   if (!target_name.empty())
108     script << " " << target_name;
109   script << "\nexit 1\n";
110   return script.str();
111 }
112 
IsApplicationTarget(const Target * target)113 bool IsApplicationTarget(const Target* target) {
114   return target->output_type() == Target::CREATE_BUNDLE &&
115          target->bundle_data().product_type() ==
116              "com.apple.product-type.application";
117 }
118 
IsXCUITestRunnerTarget(const Target * target)119 bool IsXCUITestRunnerTarget(const Target* target) {
120   return IsApplicationTarget(target) &&
121          base::EndsWith(target->label().name(),
122                         kXCUITestRunnerTargetNamePostfix,
123                         base::CompareCase::SENSITIVE);
124 }
125 
IsXCTestModuleTarget(const Target * target)126 bool IsXCTestModuleTarget(const Target* target) {
127   return target->output_type() == Target::CREATE_BUNDLE &&
128          target->bundle_data().product_type() ==
129              "com.apple.product-type.bundle.unit-test" &&
130          base::EndsWith(target->label().name(), kXCTestModuleTargetNamePostfix,
131                         base::CompareCase::SENSITIVE);
132 }
133 
IsXCUITestModuleTarget(const Target * target)134 bool IsXCUITestModuleTarget(const Target* target) {
135   return target->output_type() == Target::CREATE_BUNDLE &&
136          target->bundle_data().product_type() ==
137              "com.apple.product-type.bundle.ui-testing" &&
138          base::EndsWith(target->label().name(), kXCTestModuleTargetNamePostfix,
139                         base::CompareCase::SENSITIVE);
140 }
141 
IsXCTestFile(const SourceFile & file)142 bool IsXCTestFile(const SourceFile& file) {
143   std::string file_name = file.GetName();
144   for (size_t i = 0; i < std::size(kXCTestFileSuffixes); ++i) {
145     if (base::EndsWith(file_name, kXCTestFileSuffixes[i],
146                        base::CompareCase::SENSITIVE)) {
147       return true;
148     }
149   }
150 
151   return false;
152 }
153 
FindApplicationTargetByName(const std::string & target_name,const std::vector<const Target * > & targets)154 const Target* FindApplicationTargetByName(
155     const std::string& target_name,
156     const std::vector<const Target*>& targets) {
157   for (const Target* target : targets) {
158     if (target->label().name() == target_name) {
159       DCHECK(IsApplicationTarget(target));
160       return target;
161     }
162   }
163   NOTREACHED();
164   return nullptr;
165 }
166 
167 // Adds |base_pbxtarget| as a dependency of |dependent_pbxtarget| in the
168 // generated Xcode project.
AddPBXTargetDependency(const PBXTarget * base_pbxtarget,PBXTarget * dependent_pbxtarget,const PBXProject * project)169 void AddPBXTargetDependency(const PBXTarget* base_pbxtarget,
170                             PBXTarget* dependent_pbxtarget,
171                             const PBXProject* project) {
172   auto container_item_proxy =
173       std::make_unique<PBXContainerItemProxy>(project, base_pbxtarget);
174   auto dependency = std::make_unique<PBXTargetDependency>(
175       base_pbxtarget, std::move(container_item_proxy));
176 
177   dependent_pbxtarget->AddDependency(std::move(dependency));
178 }
179 
180 // Adds the corresponding test application target as dependency of xctest or
181 // xcuitest module target in the generated Xcode project.
AddDependencyTargetForTestModuleTargets(const std::vector<const Target * > & targets,const TargetToPBXTarget & bundle_target_to_pbxtarget,const PBXProject * project)182 void AddDependencyTargetForTestModuleTargets(
183     const std::vector<const Target*>& targets,
184     const TargetToPBXTarget& bundle_target_to_pbxtarget,
185     const PBXProject* project) {
186   for (const Target* target : targets) {
187     if (!IsXCTestModuleTarget(target) && !IsXCUITestModuleTarget(target))
188       continue;
189 
190     const Target* test_application_target = FindApplicationTargetByName(
191         target->bundle_data().xcode_test_application_name(), targets);
192     const PBXTarget* test_application_pbxtarget =
193         bundle_target_to_pbxtarget.at(test_application_target);
194     PBXTarget* module_pbxtarget = bundle_target_to_pbxtarget.at(target);
195     DCHECK(test_application_pbxtarget);
196     DCHECK(module_pbxtarget);
197 
198     AddPBXTargetDependency(test_application_pbxtarget, module_pbxtarget,
199                            project);
200   }
201 }
202 
203 // Searches the list of xctest files recursively under |target|.
SearchXCTestFilesForTarget(const Target * target,TargetToFileList * xctest_files_per_target)204 void SearchXCTestFilesForTarget(const Target* target,
205                                 TargetToFileList* xctest_files_per_target) {
206   // Early return if already visited and processed.
207   if (xctest_files_per_target->find(target) != xctest_files_per_target->end())
208     return;
209 
210   Target::FileList xctest_files;
211   for (const SourceFile& file : target->sources()) {
212     if (IsXCTestFile(file)) {
213       xctest_files.push_back(file);
214     }
215   }
216 
217   // Call recursively on public and private deps.
218   for (const auto& t : target->public_deps()) {
219     SearchXCTestFilesForTarget(t.ptr, xctest_files_per_target);
220     const Target::FileList& deps_xctest_files =
221         (*xctest_files_per_target)[t.ptr];
222     xctest_files.insert(xctest_files.end(), deps_xctest_files.begin(),
223                         deps_xctest_files.end());
224   }
225 
226   for (const auto& t : target->private_deps()) {
227     SearchXCTestFilesForTarget(t.ptr, xctest_files_per_target);
228     const Target::FileList& deps_xctest_files =
229         (*xctest_files_per_target)[t.ptr];
230     xctest_files.insert(xctest_files.end(), deps_xctest_files.begin(),
231                         deps_xctest_files.end());
232   }
233 
234   // Sort xctest_files to remove duplicates.
235   std::sort(xctest_files.begin(), xctest_files.end());
236   xctest_files.erase(std::unique(xctest_files.begin(), xctest_files.end()),
237                      xctest_files.end());
238 
239   xctest_files_per_target->insert(std::make_pair(target, xctest_files));
240 }
241 
242 // Add all source files for indexing, both private and public.
AddSourceFilesToProjectForIndexing(const std::vector<const Target * > & targets,PBXProject * project,SourceDir source_dir,const BuildSettings * build_settings)243 void AddSourceFilesToProjectForIndexing(
244     const std::vector<const Target*>& targets,
245     PBXProject* project,
246     SourceDir source_dir,
247     const BuildSettings* build_settings) {
248   std::vector<SourceFile> sources;
249   for (const Target* target : targets) {
250     for (const SourceFile& source : target->sources()) {
251       if (IsStringInOutputDir(build_settings->build_dir(), source.value()))
252         continue;
253 
254       sources.push_back(source);
255     }
256 
257     if (target->all_headers_public())
258       continue;
259 
260     for (const SourceFile& source : target->public_headers()) {
261       if (IsStringInOutputDir(build_settings->build_dir(), source.value()))
262         continue;
263 
264       sources.push_back(source);
265     }
266   }
267 
268   // Sort sources to ensure determinism of the project file generation and
269   // remove duplicate reference to the source files (can happen due to the
270   // bundle_data targets).
271   std::sort(sources.begin(), sources.end());
272   sources.erase(std::unique(sources.begin(), sources.end()), sources.end());
273 
274   for (const SourceFile& source : sources) {
275     std::string source_file = RebasePath(source.value(), source_dir,
276                                          build_settings->root_path_utf8());
277     project->AddSourceFileToIndexingTarget(source_file, source_file,
278                                            CompilerFlags::NONE);
279   }
280 }
281 
282 // Add xctest files to the "Compiler Sources" of corresponding test module
283 // native targets.
AddXCTestFilesToTestModuleTarget(const Target::FileList & xctest_file_list,PBXNativeTarget * native_target,PBXProject * project,SourceDir source_dir,const BuildSettings * build_settings)284 void AddXCTestFilesToTestModuleTarget(const Target::FileList& xctest_file_list,
285                                       PBXNativeTarget* native_target,
286                                       PBXProject* project,
287                                       SourceDir source_dir,
288                                       const BuildSettings* build_settings) {
289   for (const SourceFile& source : xctest_file_list) {
290     std::string source_path = RebasePath(source.value(), source_dir,
291                                          build_settings->root_path_utf8());
292 
293     // Test files need to be known to Xcode for proper indexing and for
294     // discovery of tests function for XCTest and XCUITest, but the compilation
295     // is done via ninja and thus must prevent Xcode from compiling the files by
296     // adding '-help' as per file compiler flag.
297     project->AddSourceFile(source_path, source_path, CompilerFlags::HELP,
298                            native_target);
299   }
300 }
301 
302 class CollectPBXObjectsPerClassHelper : public PBXObjectVisitor {
303  public:
304   CollectPBXObjectsPerClassHelper() = default;
305 
Visit(PBXObject * object)306   void Visit(PBXObject* object) override {
307     DCHECK(object);
308     objects_per_class_[object->Class()].push_back(object);
309   }
310 
311   const std::map<PBXObjectClass, std::vector<const PBXObject*>>&
objects_per_class() const312   objects_per_class() const {
313     return objects_per_class_;
314   }
315 
316  private:
317   std::map<PBXObjectClass, std::vector<const PBXObject*>> objects_per_class_;
318 
319   DISALLOW_COPY_AND_ASSIGN(CollectPBXObjectsPerClassHelper);
320 };
321 
322 std::map<PBXObjectClass, std::vector<const PBXObject*>>
CollectPBXObjectsPerClass(PBXProject * project)323 CollectPBXObjectsPerClass(PBXProject* project) {
324   CollectPBXObjectsPerClassHelper visitor;
325   project->Visit(visitor);
326   return visitor.objects_per_class();
327 }
328 
329 class RecursivelyAssignIdsHelper : public PBXObjectVisitor {
330  public:
RecursivelyAssignIdsHelper(const std::string & seed)331   RecursivelyAssignIdsHelper(const std::string& seed)
332       : seed_(seed), counter_(0) {}
333 
Visit(PBXObject * object)334   void Visit(PBXObject* object) override {
335     std::stringstream buffer;
336     buffer << seed_ << " " << object->Name() << " " << counter_;
337     std::string hash = base::SHA1HashString(buffer.str());
338     DCHECK_EQ(hash.size() % 4, 0u);
339 
340     uint32_t id[3] = {0, 0, 0};
341     const uint32_t* ptr = reinterpret_cast<const uint32_t*>(hash.data());
342     for (size_t i = 0; i < hash.size() / 4; i++)
343       id[i % 3] ^= ptr[i];
344 
345     object->SetId(base::HexEncode(id, sizeof(id)));
346     ++counter_;
347   }
348 
349  private:
350   std::string seed_;
351   int64_t counter_;
352 
353   DISALLOW_COPY_AND_ASSIGN(RecursivelyAssignIdsHelper);
354 };
355 
RecursivelyAssignIds(PBXProject * project)356 void RecursivelyAssignIds(PBXProject* project) {
357   RecursivelyAssignIdsHelper visitor(project->Name());
358   project->Visit(visitor);
359 }
360 
361 }  // namespace
362 
363 // static
RunAndWriteFiles(const std::string & workspace_name,const std::string & root_target_name,const std::string & ninja_executable,const std::string & ninja_extra_args,const std::string & dir_filters_string,const BuildSettings * build_settings,const Builder & builder,Err * err)364 bool XcodeWriter::RunAndWriteFiles(const std::string& workspace_name,
365                                    const std::string& root_target_name,
366                                    const std::string& ninja_executable,
367                                    const std::string& ninja_extra_args,
368                                    const std::string& dir_filters_string,
369                                    const BuildSettings* build_settings,
370                                    const Builder& builder,
371                                    Err* err) {
372   const XcodeWriter::TargetOsType target_os =
373       GetTargetOs(build_settings->build_args());
374 
375   PBXAttributes attributes;
376   switch (target_os) {
377     case XcodeWriter::WRITER_TARGET_OS_IOS:
378       attributes["SDKROOT"] = "iphoneos";
379       attributes["TARGETED_DEVICE_FAMILY"] = "1,2";
380       break;
381     case XcodeWriter::WRITER_TARGET_OS_MACOS:
382       attributes["SDKROOT"] = "macosx";
383       break;
384   }
385 
386   const std::string source_path = FilePathToUTF8(
387       UTF8ToFilePath(RebasePath("//", build_settings->build_dir()))
388           .StripTrailingSeparators());
389 
390   std::string config_name = FilePathToUTF8(build_settings->build_dir()
391                                                .Resolve(base::FilePath())
392                                                .StripTrailingSeparators()
393                                                .BaseName());
394   DCHECK(!config_name.empty());
395 
396   std::string::size_type separator = config_name.find('-');
397   if (separator != std::string::npos)
398     config_name = config_name.substr(0, separator);
399 
400   std::vector<const Target*> targets;
401   std::vector<const Target*> all_targets = builder.GetAllResolvedTargets();
402   if (!XcodeWriter::FilterTargets(build_settings, all_targets,
403                                   dir_filters_string, &targets, err)) {
404     return false;
405   }
406 
407   XcodeWriter workspace(workspace_name);
408   workspace.CreateProductsProject(targets, all_targets, attributes, source_path,
409                                   config_name, root_target_name,
410                                   ninja_executable, ninja_extra_args,
411                                   build_settings, target_os);
412 
413   return workspace.WriteFiles(build_settings, err);
414 }
415 
XcodeWriter(const std::string & name)416 XcodeWriter::XcodeWriter(const std::string& name) : name_(name) {
417   if (name_.empty())
418     name_.assign("all");
419 }
420 
421 XcodeWriter::~XcodeWriter() = default;
422 
423 // static
FilterTargets(const BuildSettings * build_settings,const std::vector<const Target * > & all_targets,const std::string & dir_filters_string,std::vector<const Target * > * targets,Err * err)424 bool XcodeWriter::FilterTargets(const BuildSettings* build_settings,
425                                 const std::vector<const Target*>& all_targets,
426                                 const std::string& dir_filters_string,
427                                 std::vector<const Target*>* targets,
428                                 Err* err) {
429   // Filter targets according to the semicolon-delimited list of label patterns,
430   // if defined, first.
431   targets->reserve(all_targets.size());
432   if (dir_filters_string.empty()) {
433     *targets = all_targets;
434   } else {
435     std::vector<LabelPattern> filters;
436     if (!commands::FilterPatternsFromString(build_settings, dir_filters_string,
437                                             &filters, err)) {
438       return false;
439     }
440 
441     commands::FilterTargetsByPatterns(all_targets, filters, targets);
442   }
443 
444   // Filter out all target of type EXECUTABLE that are direct dependency of
445   // a BUNDLE_DATA target (under the assumption that they will be part of a
446   // CREATE_BUNDLE target generating an application bundle). Sort the list
447   // of targets per pointer to use binary search for the removal.
448   std::sort(targets->begin(), targets->end());
449 
450   for (const Target* target : all_targets) {
451     if (!target->settings()->is_default())
452       continue;
453 
454     if (target->output_type() != Target::BUNDLE_DATA)
455       continue;
456 
457     for (const auto& pair : target->GetDeps(Target::DEPS_LINKED)) {
458       if (pair.ptr->output_type() != Target::EXECUTABLE)
459         continue;
460 
461       auto iter = std::lower_bound(targets->begin(), targets->end(), pair.ptr);
462       if (iter != targets->end() && *iter == pair.ptr)
463         targets->erase(iter);
464     }
465   }
466 
467   // Sort the list of targets per-label to get a consistent ordering of them
468   // in the generated Xcode project (and thus stability of the file generated).
469   std::sort(targets->begin(), targets->end(),
470             [](const Target* a, const Target* b) {
471               return a->label().name() < b->label().name();
472             });
473 
474   return true;
475 }
476 
CreateProductsProject(const std::vector<const Target * > & targets,const std::vector<const Target * > & all_targets,const PBXAttributes & attributes,const std::string & source_path,const std::string & config_name,const std::string & root_target,const std::string & ninja_executable,const std::string & ninja_extra_args,const BuildSettings * build_settings,TargetOsType target_os)477 void XcodeWriter::CreateProductsProject(
478     const std::vector<const Target*>& targets,
479     const std::vector<const Target*>& all_targets,
480     const PBXAttributes& attributes,
481     const std::string& source_path,
482     const std::string& config_name,
483     const std::string& root_target,
484     const std::string& ninja_executable,
485     const std::string& ninja_extra_args,
486     const BuildSettings* build_settings,
487     TargetOsType target_os) {
488   std::unique_ptr<PBXProject> main_project(
489       new PBXProject("products", config_name, source_path, attributes));
490 
491   std::vector<const Target*> bundle_targets;
492   TargetToPBXTarget bundle_target_to_pbxtarget;
493 
494   std::string build_path;
495   std::unique_ptr<base::Environment> env(base::Environment::Create());
496   SourceDir source_dir("//");
497   AddSourceFilesToProjectForIndexing(all_targets, main_project.get(),
498                                      source_dir, build_settings);
499   main_project->AddAggregateTarget(
500       "All", GetBuildScript(root_target, ninja_executable, ninja_extra_args,
501                             env.get()));
502 
503   // Needs to search for xctest files under the application targets, and this
504   // variable is used to store the results of visited targets, thus making the
505   // search more efficient.
506   TargetToFileList xctest_files_per_target;
507 
508   for (const Target* target : targets) {
509     switch (target->output_type()) {
510       case Target::EXECUTABLE:
511         if (target_os == XcodeWriter::WRITER_TARGET_OS_IOS)
512           continue;
513 
514         main_project->AddNativeTarget(
515             target->label().name(), "compiled.mach-o.executable",
516             target->output_name().empty() ? target->label().name()
517                                           : target->output_name(),
518             "com.apple.product-type.tool",
519             GetBuildScript(target->label().name(), ninja_executable,
520                            ninja_extra_args, env.get()));
521         break;
522 
523       case Target::CREATE_BUNDLE: {
524         if (target->bundle_data().product_type().empty())
525           continue;
526 
527         // For XCUITest, two CREATE_BUNDLE targets are generated:
528         // ${target_name}_runner and ${target_name}_module, however, Xcode
529         // requires only one target named ${target_name} to run tests.
530         if (IsXCUITestRunnerTarget(target))
531           continue;
532         std::string pbxtarget_name = target->label().name();
533         if (IsXCUITestModuleTarget(target)) {
534           std::string target_name = target->label().name();
535           pbxtarget_name = target_name.substr(
536               0, target_name.rfind(kXCTestModuleTargetNamePostfix));
537         }
538 
539         PBXAttributes xcode_extra_attributes =
540             target->bundle_data().xcode_extra_attributes();
541 
542         const std::string& target_output_name =
543             RebasePath(target->bundle_data()
544                            .GetBundleRootDirOutput(target->settings())
545                            .value(),
546                        build_settings->build_dir());
547         PBXNativeTarget* native_target = main_project->AddNativeTarget(
548             pbxtarget_name, std::string(), target_output_name,
549             target->bundle_data().product_type(),
550             GetBuildScript(pbxtarget_name, ninja_executable, ninja_extra_args,
551               env.get()),
552             xcode_extra_attributes);
553 
554         bundle_targets.push_back(target);
555         bundle_target_to_pbxtarget.insert(
556             std::make_pair(target, native_target));
557 
558         if (!IsXCTestModuleTarget(target) && !IsXCUITestModuleTarget(target))
559           continue;
560 
561         // For XCTest, test files are compiled into the application bundle.
562         // For XCUITest, test files are compiled into the test module bundle.
563         const Target* target_with_xctest_files = nullptr;
564         if (IsXCTestModuleTarget(target)) {
565           target_with_xctest_files = FindApplicationTargetByName(
566               target->bundle_data().xcode_test_application_name(), targets);
567         } else if (IsXCUITestModuleTarget(target)) {
568           target_with_xctest_files = target;
569         } else {
570           NOTREACHED();
571         }
572 
573         SearchXCTestFilesForTarget(target_with_xctest_files,
574                                    &xctest_files_per_target);
575         const Target::FileList& xctest_file_list =
576             xctest_files_per_target[target_with_xctest_files];
577 
578         // Add xctest files to the "Compiler Sources" of corresponding xctest
579         // and xcuitest native targets for proper indexing and for discovery of
580         // tests function.
581         AddXCTestFilesToTestModuleTarget(xctest_file_list, native_target,
582                                          main_project.get(), source_dir,
583                                          build_settings);
584         break;
585       }
586 
587       default:
588         break;
589     }
590   }
591 
592   // Adding the corresponding test application target as a dependency of xctest
593   // or xcuitest module target in the generated Xcode project so that the
594   // application target is re-compiled when compiling the test module target.
595   AddDependencyTargetForTestModuleTargets(
596       bundle_targets, bundle_target_to_pbxtarget, main_project.get());
597 
598   projects_.push_back(std::move(main_project));
599 }
600 
WriteFiles(const BuildSettings * build_settings,Err * err)601 bool XcodeWriter::WriteFiles(const BuildSettings* build_settings, Err* err) {
602   for (const auto& project : projects_) {
603     if (!WriteProjectFile(build_settings, project.get(), err))
604       return false;
605   }
606 
607   SourceFile xcworkspacedata_file =
608       build_settings->build_dir().ResolveRelativeFile(
609           Value(nullptr, name_ + ".xcworkspace/contents.xcworkspacedata"), err);
610   if (xcworkspacedata_file.is_null())
611     return false;
612 
613   std::stringstream xcworkspacedata_string_out;
614   WriteWorkspaceContent(xcworkspacedata_string_out);
615 
616   return WriteFileIfChanged(build_settings->GetFullPath(xcworkspacedata_file),
617                             xcworkspacedata_string_out.str(), err);
618 }
619 
WriteProjectFile(const BuildSettings * build_settings,PBXProject * project,Err * err)620 bool XcodeWriter::WriteProjectFile(const BuildSettings* build_settings,
621                                    PBXProject* project,
622                                    Err* err) {
623   SourceFile pbxproj_file = build_settings->build_dir().ResolveRelativeFile(
624       Value(nullptr, project->Name() + ".xcodeproj/project.pbxproj"), err);
625   if (pbxproj_file.is_null())
626     return false;
627 
628   std::stringstream pbxproj_string_out;
629   WriteProjectContent(pbxproj_string_out, project);
630 
631   if (!WriteFileIfChanged(build_settings->GetFullPath(pbxproj_file),
632                           pbxproj_string_out.str(), err))
633     return false;
634 
635   return true;
636 }
637 
WriteWorkspaceContent(std::ostream & out)638 void XcodeWriter::WriteWorkspaceContent(std::ostream& out) {
639   out << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
640       << "<Workspace version = \"1.0\">\n";
641   for (const auto& project : projects_) {
642     out << "  <FileRef location = \"group:" << project->Name()
643         << ".xcodeproj\"></FileRef>\n";
644   }
645   out << "</Workspace>\n";
646 }
647 
WriteProjectContent(std::ostream & out,PBXProject * project)648 void XcodeWriter::WriteProjectContent(std::ostream& out, PBXProject* project) {
649   RecursivelyAssignIds(project);
650 
651   out << "// !$*UTF8*$!\n"
652       << "{\n"
653       << "\tarchiveVersion = 1;\n"
654       << "\tclasses = {\n"
655       << "\t};\n"
656       << "\tobjectVersion = 46;\n"
657       << "\tobjects = {\n";
658 
659   for (auto& pair : CollectPBXObjectsPerClass(project)) {
660     out << "\n"
661         << "/* Begin " << ToString(pair.first) << " section */\n";
662     std::sort(pair.second.begin(), pair.second.end(),
663               [](const PBXObject* a, const PBXObject* b) {
664                 return a->id() < b->id();
665               });
666     for (auto* object : pair.second) {
667       object->Print(out, 2);
668     }
669     out << "/* End " << ToString(pair.first) << " section */\n";
670   }
671 
672   out << "\t};\n"
673       << "\trootObject = " << project->Reference() << ";\n"
674       << "}\n";
675 }
676