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