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