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 <optional>
12 #include <sstream>
13 #include <string>
14 #include <string_view>
15 #include <utility>
16
17 #include "base/environment.h"
18 #include "base/files/file_enumerator.h"
19 #include "base/logging.h"
20 #include "base/sha1.h"
21 #include "base/stl_util.h"
22 #include "base/strings/string_number_conversions.h"
23 #include "base/strings/string_split.h"
24 #include "base/strings/string_util.h"
25 #include "gn/args.h"
26 #include "gn/build_settings.h"
27 #include "gn/builder.h"
28 #include "gn/bundle_data.h"
29 #include "gn/commands.h"
30 #include "gn/deps_iterator.h"
31 #include "gn/filesystem_utils.h"
32 #include "gn/item.h"
33 #include "gn/loader.h"
34 #include "gn/scheduler.h"
35 #include "gn/settings.h"
36 #include "gn/source_file.h"
37 #include "gn/string_output_buffer.h"
38 #include "gn/substitution_writer.h"
39 #include "gn/target.h"
40 #include "gn/value.h"
41 #include "gn/variables.h"
42 #include "gn/xcode_object.h"
43
44 namespace {
45
46 enum TargetOsType {
47 WRITER_TARGET_OS_IOS,
48 WRITER_TARGET_OS_MACOS,
49 };
50
51 const char* kXCTestFileSuffixes[] = {
52 "egtest.m", "egtest.mm", "egtest.swift", "xctest.m", "xctest.mm",
53 "xctest.swift", "UITests.m", "UITests.mm", "UITests.swift",
54 };
55
56 const char kXCTestModuleTargetNamePostfix[] = "_module";
57 const char kXCUITestRunnerTargetNamePostfix[] = "_runner";
58
59 struct SafeEnvironmentVariableInfo {
60 const char* name;
61 bool capture_at_generation;
62 };
63
64 SafeEnvironmentVariableInfo kSafeEnvironmentVariables[] = {
65 {"HOME", true},
66 {"LANG", true},
67 {"PATH", true},
68 {"USER", true},
69 {"TMPDIR", false},
70 {"ICECC_VERSION", true},
71 {"ICECC_CLANG_REMOTE_CPP", true}};
72
GetTargetOs(const Args & args)73 TargetOsType GetTargetOs(const Args& args) {
74 const Value* target_os_value = args.GetArgOverride(variables::kTargetOs);
75 if (target_os_value) {
76 if (target_os_value->type() == Value::STRING) {
77 if (target_os_value->string_value() == "ios")
78 return WRITER_TARGET_OS_IOS;
79 }
80 }
81 return WRITER_TARGET_OS_MACOS;
82 }
83
GetBuildScript(const std::string & target_name,const std::string & ninja_executable,const std::string & build_dir,base::Environment * environment)84 std::string GetBuildScript(const std::string& target_name,
85 const std::string& ninja_executable,
86 const std::string& build_dir,
87 base::Environment* environment) {
88 // Launch ninja with a sanitized environment (Xcode sets many environment
89 // variables overridding settings, including the SDK, thus breaking hermetic
90 // build).
91 std::stringstream buffer;
92 buffer << "exec env -i ";
93
94 // Write environment.
95 for (const auto& variable : kSafeEnvironmentVariables) {
96 buffer << variable.name << "=";
97 if (variable.capture_at_generation) {
98 std::string value;
99 environment->GetVar(variable.name, &value);
100 buffer << "'" << value << "'";
101 } else {
102 buffer << "\"${" << variable.name << "}\"";
103 }
104 buffer << " ";
105 }
106
107 if (ninja_executable.empty()) {
108 buffer << "ninja";
109 } else {
110 buffer << ninja_executable;
111 }
112
113 buffer << " -C " << build_dir;
114
115 if (!target_name.empty()) {
116 buffer << " '" << target_name << "'";
117 }
118 return buffer.str();
119 }
120
GetBuildScript(const Label & target_label,const std::string & ninja_executable,const std::string & build_dir,base::Environment * environment)121 std::string GetBuildScript(const Label& target_label,
122 const std::string& ninja_executable,
123 const std::string& build_dir,
124 base::Environment* environment) {
125 std::string target_name = target_label.GetUserVisibleName(false);
126 base::TrimString(target_name, "/", &target_name);
127 return GetBuildScript(target_name, ninja_executable, build_dir, environment);
128 }
129
IsApplicationTarget(const Target * target)130 bool IsApplicationTarget(const Target* target) {
131 return target->output_type() == Target::CREATE_BUNDLE &&
132 target->bundle_data().product_type() ==
133 "com.apple.product-type.application";
134 }
135
IsXCUITestRunnerTarget(const Target * target)136 bool IsXCUITestRunnerTarget(const Target* target) {
137 return IsApplicationTarget(target) &&
138 base::ends_with(target->label().name(), kXCUITestRunnerTargetNamePostfix);
139 }
140
IsXCTestModuleTarget(const Target * target)141 bool IsXCTestModuleTarget(const Target* target) {
142 return target->output_type() == Target::CREATE_BUNDLE &&
143 target->bundle_data().product_type() ==
144 "com.apple.product-type.bundle.unit-test" &&
145 base::ends_with(target->label().name(), kXCTestModuleTargetNamePostfix);
146 }
147
IsXCUITestModuleTarget(const Target * target)148 bool IsXCUITestModuleTarget(const Target* target) {
149 return target->output_type() == Target::CREATE_BUNDLE &&
150 target->bundle_data().product_type() ==
151 "com.apple.product-type.bundle.ui-testing" &&
152 base::ends_with(target->label().name(), kXCTestModuleTargetNamePostfix);
153 }
154
IsXCTestFile(const SourceFile & file)155 bool IsXCTestFile(const SourceFile& file) {
156 std::string file_name = file.GetName();
157 for (size_t i = 0; i < std::size(kXCTestFileSuffixes); ++i) {
158 if (base::ends_with(file_name, kXCTestFileSuffixes[i])) {
159 return true;
160 }
161 }
162
163 return false;
164 }
165
166 // Finds the application target from its target name.
167 std::optional<std::pair<const Target*, PBXNativeTarget*>>
FindApplicationTargetByName(const ParseNode * node,const std::string & target_name,const std::map<const Target *,PBXNativeTarget * > & targets,Err * err)168 FindApplicationTargetByName(
169 const ParseNode* node,
170 const std::string& target_name,
171 const std::map<const Target*, PBXNativeTarget*>& targets,
172 Err* err) {
173 for (auto& pair : targets) {
174 const Target* target = pair.first;
175 if (target->label().name() == target_name) {
176 if (!IsApplicationTarget(target)) {
177 *err = Err(node, "host application target \"" + target_name +
178 "\" not an application bundle");
179 return std::nullopt;
180 }
181 DCHECK(pair.first);
182 DCHECK(pair.second);
183 return pair;
184 }
185 }
186 *err =
187 Err(node, "cannot find host application bundle \"" + target_name + "\"");
188 return std::nullopt;
189 }
190
191 // Adds |base_pbxtarget| as a dependency of |dependent_pbxtarget| in the
192 // generated Xcode project.
AddPBXTargetDependency(const PBXTarget * base_pbxtarget,PBXTarget * dependent_pbxtarget,const PBXProject * project)193 void AddPBXTargetDependency(const PBXTarget* base_pbxtarget,
194 PBXTarget* dependent_pbxtarget,
195 const PBXProject* project) {
196 auto container_item_proxy =
197 std::make_unique<PBXContainerItemProxy>(project, base_pbxtarget);
198 auto dependency = std::make_unique<PBXTargetDependency>(
199 base_pbxtarget, std::move(container_item_proxy));
200
201 dependent_pbxtarget->AddDependency(std::move(dependency));
202 }
203
204 // Returns a SourceFile for absolute path `file_path` below `//`.
FilePathToSourceFile(const BuildSettings * build_settings,const base::FilePath & file_path)205 SourceFile FilePathToSourceFile(const BuildSettings* build_settings,
206 const base::FilePath& file_path) {
207 const std::string file_path_utf8 = FilePathToUTF8(file_path);
208 return SourceFile("//" + file_path_utf8.substr(
209 build_settings->root_path_utf8().size() + 1));
210 }
211
212 // Returns the list of patterns to use when looking for additional files
213 // from `options`.
GetAdditionalFilesPatterns(const XcodeWriter::Options & options)214 std::vector<base::FilePath::StringType> GetAdditionalFilesPatterns(
215 const XcodeWriter::Options& options) {
216 return base::SplitString(options.additional_files_patterns,
217 FILE_PATH_LITERAL(";"), base::TRIM_WHITESPACE,
218 base::SPLIT_WANT_ALL);
219 }
220
221 // Returns the list of roots to use when looking for additional files
222 // from `options`.
GetAdditionalFilesRoots(const BuildSettings * build_settings,const XcodeWriter::Options & options)223 std::vector<base::FilePath> GetAdditionalFilesRoots(
224 const BuildSettings* build_settings,
225 const XcodeWriter::Options& options) {
226 if (options.additional_files_roots.empty()) {
227 return {build_settings->root_path()};
228 }
229
230 const std::vector<base::FilePath::StringType> roots =
231 base::SplitString(options.additional_files_roots, FILE_PATH_LITERAL(";"),
232 base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
233
234 std::vector<base::FilePath> root_paths;
235 for (const base::FilePath::StringType& root : roots) {
236 const std::string rebased_root =
237 RebasePath(FilePathToUTF8(root), SourceDir("//"),
238 build_settings->root_path_utf8());
239
240 root_paths.push_back(
241 build_settings->root_path().Append(UTF8ToFilePath(rebased_root)));
242 }
243
244 return root_paths;
245 }
246
247 // Helper class to resolve list of XCTest files per target.
248 //
249 // Uses a cache of file found per intermediate targets to reduce the need
250 // to shared targets multiple times. It is recommended to reuse the same
251 // object to resolve all the targets for a project.
252 class XCTestFilesResolver {
253 public:
254 XCTestFilesResolver();
255 ~XCTestFilesResolver();
256
257 // Returns a set of all XCTest files for |target|. The returned reference
258 // may be invalidated the next time this method is called.
259 const SourceFileSet& SearchFilesForTarget(const Target* target);
260
261 private:
262 std::map<const Target*, SourceFileSet> cache_;
263 };
264
265 XCTestFilesResolver::XCTestFilesResolver() = default;
266
267 XCTestFilesResolver::~XCTestFilesResolver() = default;
268
SearchFilesForTarget(const Target * target)269 const SourceFileSet& XCTestFilesResolver::SearchFilesForTarget(
270 const Target* target) {
271 // Early return if already visited and processed.
272 auto iter = cache_.find(target);
273 if (iter != cache_.end())
274 return iter->second;
275
276 SourceFileSet xctest_files;
277 for (const SourceFile& file : target->sources()) {
278 if (IsXCTestFile(file)) {
279 xctest_files.insert(file);
280 }
281 }
282
283 // Call recursively on public and private deps.
284 for (const auto& t : target->public_deps()) {
285 const SourceFileSet& deps_xctest_files = SearchFilesForTarget(t.ptr);
286 xctest_files.insert(deps_xctest_files.begin(), deps_xctest_files.end());
287 }
288
289 for (const auto& t : target->private_deps()) {
290 const SourceFileSet& deps_xctest_files = SearchFilesForTarget(t.ptr);
291 xctest_files.insert(deps_xctest_files.begin(), deps_xctest_files.end());
292 }
293
294 auto insert = cache_.insert(std::make_pair(target, xctest_files));
295 DCHECK(insert.second);
296 return insert.first->second;
297 }
298
299 // Add xctest files to the "Compiler Sources" of corresponding test module
300 // native targets.
AddXCTestFilesToTestModuleTarget(const std::vector<SourceFile> & sources,PBXNativeTarget * native_target,PBXProject * project,SourceDir source_dir,const BuildSettings * build_settings)301 void AddXCTestFilesToTestModuleTarget(const std::vector<SourceFile>& sources,
302 PBXNativeTarget* native_target,
303 PBXProject* project,
304 SourceDir source_dir,
305 const BuildSettings* build_settings) {
306 for (const SourceFile& source : sources) {
307 const std::string source_path = RebasePath(
308 source.value(), source_dir, build_settings->root_path_utf8());
309 project->AddSourceFile(source_path, source_path, native_target);
310 }
311 }
312
313 // Helper class to collect all PBXObject per class.
314 class CollectPBXObjectsPerClassHelper : public PBXObjectVisitorConst {
315 public:
316 CollectPBXObjectsPerClassHelper() = default;
317
Visit(const PBXObject * object)318 void Visit(const PBXObject* object) override {
319 DCHECK(object);
320 objects_per_class_[object->Class()].push_back(object);
321 }
322
323 const std::map<PBXObjectClass, std::vector<const PBXObject*>>&
objects_per_class() const324 objects_per_class() const {
325 return objects_per_class_;
326 }
327
328 private:
329 std::map<PBXObjectClass, std::vector<const PBXObject*>> objects_per_class_;
330
331 CollectPBXObjectsPerClassHelper(const CollectPBXObjectsPerClassHelper&) =
332 delete;
333 CollectPBXObjectsPerClassHelper& operator=(
334 const CollectPBXObjectsPerClassHelper&) = delete;
335 };
336
337 std::map<PBXObjectClass, std::vector<const PBXObject*>>
CollectPBXObjectsPerClass(const PBXProject * project)338 CollectPBXObjectsPerClass(const PBXProject* project) {
339 CollectPBXObjectsPerClassHelper visitor;
340 project->Visit(visitor);
341 return visitor.objects_per_class();
342 }
343
344 // Helper class to assign unique ids to all PBXObject.
345 class RecursivelyAssignIdsHelper : public PBXObjectVisitor {
346 public:
RecursivelyAssignIdsHelper(const std::string & seed)347 RecursivelyAssignIdsHelper(const std::string& seed)
348 : seed_(seed), counter_(0) {}
349
Visit(PBXObject * object)350 void Visit(PBXObject* object) override {
351 std::stringstream buffer;
352 buffer << seed_ << " " << object->Name() << " " << counter_;
353 std::string hash = base::SHA1HashString(buffer.str());
354 DCHECK_EQ(hash.size() % 4, 0u);
355
356 uint32_t id[3] = {0, 0, 0};
357 const uint32_t* ptr = reinterpret_cast<const uint32_t*>(hash.data());
358 for (size_t i = 0; i < hash.size() / 4; i++)
359 id[i % 3] ^= ptr[i];
360
361 object->SetId(base::HexEncode(id, sizeof(id)));
362 ++counter_;
363 }
364
365 private:
366 std::string seed_;
367 int64_t counter_;
368
369 RecursivelyAssignIdsHelper(const RecursivelyAssignIdsHelper&) = delete;
370 RecursivelyAssignIdsHelper& operator=(const RecursivelyAssignIdsHelper&) =
371 delete;
372 };
373
RecursivelyAssignIds(PBXProject * project)374 void RecursivelyAssignIds(PBXProject* project) {
375 RecursivelyAssignIdsHelper visitor(project->Name());
376 project->Visit(visitor);
377 }
378
379 // Returns a list of configuration names from the options passed to the
380 // generator. If no configuration names have been passed, return default
381 // value.
ConfigListFromOptions(const std::string & configs)382 std::vector<std::string> ConfigListFromOptions(const std::string& configs) {
383 std::vector<std::string> result = base::SplitString(
384 configs, ";", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
385
386 if (result.empty())
387 result.push_back(std::string("Release"));
388
389 return result;
390 }
391
392 // Returns the path to root_src_dir from settings.
SourcePathFromBuildSettings(const BuildSettings * build_settings)393 std::string SourcePathFromBuildSettings(const BuildSettings* build_settings) {
394 return RebasePath("//", build_settings->build_dir());
395 }
396
397 // Returns the default attributes for the project from settings.
ProjectAttributesFromBuildSettings(const BuildSettings * build_settings)398 PBXAttributes ProjectAttributesFromBuildSettings(
399 const BuildSettings* build_settings) {
400 const TargetOsType target_os = GetTargetOs(build_settings->build_args());
401
402 PBXAttributes attributes;
403 switch (target_os) {
404 case WRITER_TARGET_OS_IOS:
405 attributes["SDKROOT"] = "iphoneos";
406 attributes["TARGETED_DEVICE_FAMILY"] = "1,2";
407 break;
408 case WRITER_TARGET_OS_MACOS:
409 attributes["SDKROOT"] = "macosx";
410 break;
411 }
412
413 // Xcode complains that the project needs to be upgraded if those keys are
414 // not set. Since the generated Xcode project is only used for debugging
415 // and the source of truth for build settings is the .gn files themselves,
416 // we can safely set them in the project as they won't be used by "ninja".
417 attributes["ALWAYS_SEARCH_USER_PATHS"] = "NO";
418 attributes["CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED"] = "YES";
419 attributes["CLANG_WARN__DUPLICATE_METHOD_MATCH"] = "YES";
420 attributes["CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING"] = "YES";
421 attributes["CLANG_WARN_BOOL_CONVERSION"] = "YES";
422 attributes["CLANG_WARN_COMMA"] = "YES";
423 attributes["CLANG_WARN_CONSTANT_CONVERSION"] = "YES";
424 attributes["CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS"] = "YES";
425 attributes["CLANG_WARN_EMPTY_BODY"] = "YES";
426 attributes["CLANG_WARN_ENUM_CONVERSION"] = "YES";
427 attributes["CLANG_WARN_INFINITE_RECURSION"] = "YES";
428 attributes["CLANG_WARN_INT_CONVERSION"] = "YES";
429 attributes["CLANG_WARN_NON_LITERAL_NULL_CONVERSION"] = "YES";
430 attributes["CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF"] = "YES";
431 attributes["CLANG_WARN_OBJC_LITERAL_CONVERSION"] = "YES";
432 attributes["CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER"] = "YES";
433 attributes["CLANG_WARN_RANGE_LOOP_ANALYSIS"] = "YES";
434 attributes["CLANG_WARN_STRICT_PROTOTYPES"] = "YES";
435 attributes["CLANG_WARN_SUSPICIOUS_MOVE"] = "YES";
436 attributes["CLANG_WARN_UNREACHABLE_CODE"] = "YES";
437 attributes["ENABLE_STRICT_OBJC_MSGSEND"] = "YES";
438 attributes["ENABLE_TESTABILITY"] = "YES";
439 attributes["GCC_NO_COMMON_BLOCKS"] = "YES";
440 attributes["GCC_WARN_64_TO_32_BIT_CONVERSION"] = "YES";
441 attributes["GCC_WARN_ABOUT_RETURN_TYPE"] = "YES";
442 attributes["GCC_WARN_UNDECLARED_SELECTOR"] = "YES";
443 attributes["GCC_WARN_UNINITIALIZED_AUTOS"] = "YES";
444 attributes["GCC_WARN_UNUSED_FUNCTION"] = "YES";
445 attributes["GCC_WARN_UNUSED_VARIABLE"] = "YES";
446 attributes["ONLY_ACTIVE_ARCH"] = "YES";
447
448 return attributes;
449 }
450
451 // Helper class used to collect the source files that will be added to
452 // and PBXProject.
453 class WorkspaceSources {
454 public:
455 WorkspaceSources(const BuildSettings* build_settings);
456 ~WorkspaceSources();
457
458 // Records `source` as part of the project. The source may be dropped if
459 // it should not be listed in the project (e.g. a generated file). Also
460 // for files in an assets catalog, only the catalog itself will be added.
461 void AddSourceFile(const SourceFile& source);
462
463 // Insert all the recorded source into `project`.
464 void AddToProject(PBXProject& project) const;
465
466 private:
467 const SourceDir build_dir_;
468 const std::string root_dir_;
469 SourceFileSet source_files_;
470 };
471
WorkspaceSources(const BuildSettings * build_settings)472 WorkspaceSources::WorkspaceSources(const BuildSettings* build_settings)
473 : build_dir_(build_settings->build_dir()),
474 root_dir_(build_settings->root_path_utf8()) {}
475
476 WorkspaceSources::~WorkspaceSources() = default;
477
AddSourceFile(const SourceFile & source)478 void WorkspaceSources::AddSourceFile(const SourceFile& source) {
479 if (IsStringInOutputDir(build_dir_, source.value())) {
480 return;
481 }
482
483 if (IsPathAbsolute(source.value())) {
484 return;
485 }
486
487 SourceFile assets_catalog_dir = BundleData::GetAssetsCatalogDirectory(source);
488 if (!assets_catalog_dir.is_null()) {
489 source_files_.insert(assets_catalog_dir);
490 } else {
491 source_files_.insert(source);
492 }
493 }
494
AddToProject(PBXProject & project) const495 void WorkspaceSources::AddToProject(PBXProject& project) const {
496 // Sort the files to ensure a deterministic generation of the project file.
497 std::vector<SourceFile> sources(source_files_.begin(), source_files_.end());
498 std::sort(sources.begin(), sources.end());
499
500 const SourceDir source_dir("//");
501 for (const SourceFile& source : sources) {
502 const std::string source_path =
503 RebasePath(source.value(), source_dir, root_dir_);
504 project.AddSourceFileToIndexingTarget(source_path, source_path);
505 }
506 }
507
508 } // namespace
509
510 // Class representing the workspace embedded in an xcodeproj file used to
511 // configure the build settings shared by all targets in the project (used
512 // to configure the build system).
513 class XcodeWorkspace {
514 public:
515 XcodeWorkspace(const BuildSettings* build_settings,
516 XcodeWriter::Options options);
517 ~XcodeWorkspace();
518
519 XcodeWorkspace(const XcodeWorkspace&) = delete;
520 XcodeWorkspace& operator=(const XcodeWorkspace&) = delete;
521
522 // Generates the .xcworkspace files to disk.
523 bool WriteWorkspace(const std::string& name, Err* err) const;
524
525 private:
526 // Writes the workspace data file.
527 bool WriteWorkspaceDataFile(const std::string& name, Err* err) const;
528
529 // Writes the settings file.
530 bool WriteSettingsFile(const std::string& name, Err* err) const;
531
532 const BuildSettings* build_settings_ = nullptr;
533 XcodeWriter::Options options_;
534 };
535
XcodeWorkspace(const BuildSettings * build_settings,XcodeWriter::Options options)536 XcodeWorkspace::XcodeWorkspace(const BuildSettings* build_settings,
537 XcodeWriter::Options options)
538 : build_settings_(build_settings), options_(options) {}
539
540 XcodeWorkspace::~XcodeWorkspace() = default;
541
WriteWorkspace(const std::string & name,Err * err) const542 bool XcodeWorkspace::WriteWorkspace(const std::string& name, Err* err) const {
543 return WriteWorkspaceDataFile(name, err) && WriteSettingsFile(name, err);
544 }
545
WriteWorkspaceDataFile(const std::string & name,Err * err) const546 bool XcodeWorkspace::WriteWorkspaceDataFile(const std::string& name,
547 Err* err) const {
548 const SourceFile source_file =
549 build_settings_->build_dir().ResolveRelativeFile(
550 Value(nullptr, name + "/contents.xcworkspacedata"), err);
551 if (source_file.is_null())
552 return false;
553
554 StringOutputBuffer storage;
555 std::ostream out(&storage);
556 out << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
557 << "<Workspace\n"
558 << " version = \"1.0\">\n"
559 << " <FileRef\n"
560 << " location = \"self:\">\n"
561 << " </FileRef>\n"
562 << "</Workspace>\n";
563
564 return storage.WriteToFileIfChanged(build_settings_->GetFullPath(source_file),
565 err);
566 }
567
WriteSettingsFile(const std::string & name,Err * err) const568 bool XcodeWorkspace::WriteSettingsFile(const std::string& name,
569 Err* err) const {
570 const SourceFile source_file =
571 build_settings_->build_dir().ResolveRelativeFile(
572 Value(nullptr, name + "/xcshareddata/WorkspaceSettings.xcsettings"),
573 err);
574 if (source_file.is_null())
575 return false;
576
577 StringOutputBuffer storage;
578 std::ostream out(&storage);
579 out << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
580 << "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" "
581 << "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
582 << "<plist version=\"1.0\">\n"
583 << "<dict>\n";
584
585 switch (options_.build_system) {
586 case XcodeBuildSystem::kLegacy:
587 out << "\t<key>BuildSystemType</key>\n"
588 << "\t<string>Original</string>\n";
589 break;
590 case XcodeBuildSystem::kNew:
591 break;
592 }
593
594 out << "</dict>\n" << "</plist>\n";
595
596 return storage.WriteToFileIfChanged(build_settings_->GetFullPath(source_file),
597 err);
598 }
599
600 // Class responsible for constructing and writing the .xcodeproj from the
601 // targets known to gn. It currently requires using the "Legacy build system"
602 // so it will embed an .xcworkspace file to force the setting.
603 class XcodeProject {
604 public:
605 XcodeProject(const BuildSettings* build_settings,
606 XcodeWriter::Options options);
607 ~XcodeProject();
608
609 // Recursively finds "source" files from |builder| and adds them to the
610 // project (this includes more than just text source files, e.g. images
611 // in resources, ...).
612 bool AddSourcesFromBuilder(const Builder& builder, Err* err);
613
614 // Recursively finds targets from |builder| and adds them to the project.
615 // Only targets of type CREATE_BUNDLE or EXECUTABLE are kept since they
616 // are the only one that can be run and thus debugged from Xcode.
617 bool AddTargetsFromBuilder(const Builder& builder, Err* err);
618
619 // Assigns ids to all PBXObject that were added to the project. Must be
620 // called before calling WriteFile().
621 bool AssignIds(Err* err);
622
623 // Generates the project file and the .xcodeproj file to disk if updated
624 // (i.e. if the generated project is identical to the currently existing
625 // one, it is not overwritten).
626 bool WriteFile(Err* err) const;
627
628 private:
629 // Finds all targets that needs to be generated for the project (applies
630 // the filter passed via |options|).
631 std::optional<std::vector<const Target*>> GetTargetsFromBuilder(
632 const Builder& builder,
633 Err* err) const;
634
635 // Adds a target of type EXECUTABLE to the project.
636 PBXNativeTarget* AddBinaryTarget(const Target* target,
637 base::Environment* env,
638 Err* err);
639
640 // Adds a target of type CREATE_BUNDLE to the project.
641 PBXNativeTarget* AddBundleTarget(const Target* target,
642 base::Environment* env,
643 Err* err);
644
645 // Adds the XCTest source files for all test xctest or xcuitest module target
646 // to allow Xcode to index the list of tests (thus allowing to run individual
647 // tests from Xcode UI).
648 bool AddCXTestSourceFilesForTestModuleTargets(
649 const std::map<const Target*, PBXNativeTarget*>& bundle_targets,
650 Err* err);
651
652 // Adds the corresponding test application target as dependency of xctest or
653 // xcuitest module target in the generated Xcode project.
654 bool AddDependencyTargetsForTestModuleTargets(
655 const std::map<const Target*, PBXNativeTarget*>& bundle_targets,
656 Err* err);
657
658 // Tweak `output_dir` to be relative to the configuration specific output
659 // directory (see --xcode-config-build-dir=... flag).
660 std::string GetConfigOutputDir(std::string_view output_dir);
661
662 // Generates the content of the .xcodeproj file into |out|.
663 void WriteFileContent(std::ostream& out) const;
664
665 // Returns whether the file should be added to the project.
666 bool ShouldIncludeFileInProject(const SourceFile& source) const;
667
668 const BuildSettings* build_settings_;
669 XcodeWriter::Options options_;
670 PBXProject project_;
671 };
672
XcodeProject(const BuildSettings * build_settings,XcodeWriter::Options options)673 XcodeProject::XcodeProject(const BuildSettings* build_settings,
674 XcodeWriter::Options options)
675 : build_settings_(build_settings),
676 options_(options),
677 project_(options.project_name,
678 ConfigListFromOptions(options.configurations),
679 SourcePathFromBuildSettings(build_settings),
680 ProjectAttributesFromBuildSettings(build_settings)) {}
681
682 XcodeProject::~XcodeProject() = default;
683
ShouldIncludeFileInProject(const SourceFile & source) const684 bool XcodeProject::ShouldIncludeFileInProject(const SourceFile& source) const {
685 if (IsStringInOutputDir(build_settings_->build_dir(), source.value()))
686 return false;
687
688 if (IsPathAbsolute(source.value()))
689 return false;
690
691 return true;
692 }
693
AddSourcesFromBuilder(const Builder & builder,Err * err)694 bool XcodeProject::AddSourcesFromBuilder(const Builder& builder, Err* err) {
695 WorkspaceSources sources(build_settings_);
696
697 // Add sources from all targets.
698 for (const Target* target : builder.GetAllResolvedTargets()) {
699 for (const SourceFile& source : target->sources()) {
700 sources.AddSourceFile(source);
701 }
702
703 for (const SourceFile& source : target->config_values().inputs()) {
704 sources.AddSourceFile(source);
705 }
706
707 for (const SourceFile& source : target->public_headers()) {
708 sources.AddSourceFile(source);
709 }
710
711 const SourceFile& bridge_header = target->swift_values().bridge_header();
712 if (!bridge_header.is_null()) {
713 sources.AddSourceFile(bridge_header);
714 }
715
716 if (target->output_type() == Target::ACTION ||
717 target->output_type() == Target::ACTION_FOREACH) {
718 sources.AddSourceFile(target->action_values().script());
719 }
720 }
721
722 // Add BUILD.gn and *.gni for targets, configs and toolchains.
723 for (const Item* item : builder.GetAllResolvedItems()) {
724 if (!item->AsConfig() && !item->AsTarget() && !item->AsToolchain())
725 continue;
726
727 const SourceFile build = builder.loader()->BuildFileForLabel(item->label());
728 sources.AddSourceFile(build);
729
730 for (const SourceFile& source :
731 item->settings()->import_manager().GetImportedFiles()) {
732 sources.AddSourceFile(source);
733 }
734 }
735
736 // Add other files read by gn (the main dotfile, exec_script scripts, ...).
737 for (const auto& path : g_scheduler->GetGenDependencies()) {
738 if (!build_settings_->root_path().IsParent(path))
739 continue;
740
741 const SourceFile source = FilePathToSourceFile(build_settings_, path);
742 sources.AddSourceFile(source);
743 }
744
745 // Add any files from --xcode-additional-files-patterns, using the root
746 // listed in --xcode-additional-files-roots.
747 if (!options_.additional_files_patterns.empty()) {
748 const std::vector<base::FilePath::StringType> patterns =
749 GetAdditionalFilesPatterns(options_);
750 const std::vector<base::FilePath> roots =
751 GetAdditionalFilesRoots(build_settings_, options_);
752
753 for (const base::FilePath& root : roots) {
754 for (const base::FilePath::StringType& pattern : patterns) {
755 base::FileEnumerator it(root, /*recursive*/ true,
756 base::FileEnumerator::FILES, pattern,
757 base::FileEnumerator::FolderSearchPolicy::ALL);
758
759 for (base::FilePath path = it.Next(); !path.empty(); path = it.Next()) {
760 const SourceFile source = FilePathToSourceFile(build_settings_, path);
761 sources.AddSourceFile(source);
762 }
763 }
764 }
765 }
766
767 sources.AddToProject(project_);
768 return true;
769 }
770
AddTargetsFromBuilder(const Builder & builder,Err * err)771 bool XcodeProject::AddTargetsFromBuilder(const Builder& builder, Err* err) {
772 std::unique_ptr<base::Environment> env(base::Environment::Create());
773
774 project_.AddAggregateTarget(
775 "All", GetConfigOutputDir("."),
776 GetBuildScript(options_.root_target_name, options_.ninja_executable,
777 GetConfigOutputDir("."), env.get()));
778
779 const std::optional<std::vector<const Target*>> targets =
780 GetTargetsFromBuilder(builder, err);
781 if (!targets)
782 return false;
783
784 std::map<const Target*, PBXNativeTarget*> bundle_targets;
785
786 const TargetOsType target_os = GetTargetOs(build_settings_->build_args());
787
788 for (const Target* target : *targets) {
789 PBXNativeTarget* native_target = nullptr;
790 switch (target->output_type()) {
791 case Target::EXECUTABLE:
792 if (target_os == WRITER_TARGET_OS_IOS)
793 continue;
794
795 native_target = AddBinaryTarget(target, env.get(), err);
796 if (!native_target)
797 return false;
798
799 break;
800
801 case Target::CREATE_BUNDLE: {
802 if (target->bundle_data().product_type().empty())
803 continue;
804
805 // For XCUITest, two CREATE_BUNDLE targets are generated:
806 // ${target_name}_runner and ${target_name}_module, however, Xcode
807 // requires only one target named ${target_name} to run tests.
808 if (IsXCUITestRunnerTarget(target))
809 continue;
810
811 native_target = AddBundleTarget(target, env.get(), err);
812 if (!native_target)
813 return false;
814
815 bundle_targets.insert(std::make_pair(target, native_target));
816 break;
817 }
818
819 default:
820 break;
821 }
822 }
823
824 if (!AddCXTestSourceFilesForTestModuleTargets(bundle_targets, err))
825 return false;
826
827 // Adding the corresponding test application target as a dependency of xctest
828 // or xcuitest module target in the generated Xcode project so that the
829 // application target is re-compiled when compiling the test module target.
830 if (!AddDependencyTargetsForTestModuleTargets(bundle_targets, err))
831 return false;
832
833 return true;
834 }
835
AddCXTestSourceFilesForTestModuleTargets(const std::map<const Target *,PBXNativeTarget * > & bundle_targets,Err * err)836 bool XcodeProject::AddCXTestSourceFilesForTestModuleTargets(
837 const std::map<const Target*, PBXNativeTarget*>& bundle_targets,
838 Err* err) {
839 const SourceDir source_dir("//");
840
841 // Needs to search for xctest files under the application targets, and this
842 // variable is used to store the results of visited targets, thus making the
843 // search more efficient.
844 XCTestFilesResolver resolver;
845
846 for (const auto& pair : bundle_targets) {
847 const Target* target = pair.first;
848 if (!IsXCTestModuleTarget(target) && !IsXCUITestModuleTarget(target))
849 continue;
850
851 // For XCTest, test files are compiled into the application bundle.
852 // For XCUITest, test files are compiled into the test module bundle.
853 const Target* target_with_xctest_files = nullptr;
854 if (IsXCTestModuleTarget(target)) {
855 auto app_pair = FindApplicationTargetByName(
856 target->defined_from(),
857 target->bundle_data().xcode_test_application_name(), bundle_targets,
858 err);
859 if (!app_pair)
860 return false;
861 target_with_xctest_files = app_pair.value().first;
862 } else {
863 DCHECK(IsXCUITestModuleTarget(target));
864 target_with_xctest_files = target;
865 }
866
867 const SourceFileSet& sources =
868 resolver.SearchFilesForTarget(target_with_xctest_files);
869
870 // Sort files to ensure deterministic generation of the project file (and
871 // nicely sorted file list in Xcode).
872 std::vector<SourceFile> sorted_sources(sources.begin(), sources.end());
873 std::sort(sorted_sources.begin(), sorted_sources.end());
874
875 // Add xctest files to the "Compiler Sources" of corresponding xctest
876 // and xcuitest native targets for proper indexing and for discovery of
877 // tests function.
878 AddXCTestFilesToTestModuleTarget(sorted_sources, pair.second, &project_,
879 source_dir, build_settings_);
880 }
881
882 return true;
883 }
884
AddDependencyTargetsForTestModuleTargets(const std::map<const Target *,PBXNativeTarget * > & bundle_targets,Err * err)885 bool XcodeProject::AddDependencyTargetsForTestModuleTargets(
886 const std::map<const Target*, PBXNativeTarget*>& bundle_targets,
887 Err* err) {
888 for (const auto& pair : bundle_targets) {
889 const Target* target = pair.first;
890 if (!IsXCTestModuleTarget(target) && !IsXCUITestModuleTarget(target))
891 continue;
892
893 auto app_pair = FindApplicationTargetByName(
894 target->defined_from(),
895 target->bundle_data().xcode_test_application_name(), bundle_targets,
896 err);
897 if (!app_pair)
898 return false;
899
900 AddPBXTargetDependency(app_pair.value().second, pair.second, &project_);
901 }
902
903 return true;
904 }
905
AssignIds(Err * err)906 bool XcodeProject::AssignIds(Err* err) {
907 RecursivelyAssignIds(&project_);
908 return true;
909 }
910
WriteFile(Err * err) const911 bool XcodeProject::WriteFile(Err* err) const {
912 DCHECK(!project_.id().empty());
913
914 SourceFile pbxproj_file = build_settings_->build_dir().ResolveRelativeFile(
915 Value(nullptr, project_.Name() + ".xcodeproj/project.pbxproj"), err);
916 if (pbxproj_file.is_null())
917 return false;
918
919 StringOutputBuffer storage;
920 std::ostream pbxproj_string_out(&storage);
921 WriteFileContent(pbxproj_string_out);
922
923 if (!storage.WriteToFileIfChanged(build_settings_->GetFullPath(pbxproj_file),
924 err)) {
925 return false;
926 }
927
928 XcodeWorkspace workspace(build_settings_, options_);
929 return workspace.WriteWorkspace(
930 project_.Name() + ".xcodeproj/project.xcworkspace", err);
931 }
932
GetTargetsFromBuilder(const Builder & builder,Err * err) const933 std::optional<std::vector<const Target*>> XcodeProject::GetTargetsFromBuilder(
934 const Builder& builder,
935 Err* err) const {
936 std::vector<const Target*> all_targets = builder.GetAllResolvedTargets();
937
938 // Filter targets according to the dir_filters_string if defined.
939 if (!options_.dir_filters_string.empty()) {
940 std::vector<LabelPattern> filters;
941 if (!commands::FilterPatternsFromString(
942 build_settings_, options_.dir_filters_string, &filters, err)) {
943 return std::nullopt;
944 }
945
946 std::vector<const Target*> unfiltered_targets;
947 std::swap(unfiltered_targets, all_targets);
948
949 commands::FilterTargetsByPatterns(unfiltered_targets, filters,
950 &all_targets);
951 }
952
953 // Filter out all target of type EXECUTABLE that are direct dependency of
954 // a BUNDLE_DATA target (under the assumption that they will be part of a
955 // CREATE_BUNDLE target generating an application bundle).
956 TargetSet targets(all_targets.begin(), all_targets.end());
957 for (const Target* target : all_targets) {
958 if (!target->settings()->is_default())
959 continue;
960
961 if (target->output_type() != Target::BUNDLE_DATA)
962 continue;
963
964 for (const auto& pair : target->GetDeps(Target::DEPS_LINKED)) {
965 if (pair.ptr->output_type() != Target::EXECUTABLE)
966 continue;
967
968 targets.erase(pair.ptr);
969 }
970 }
971
972 // Sort the list of targets per-label to get a consistent ordering of them
973 // in the generated Xcode project (and thus stability of the file generated).
974 std::vector<const Target*> sorted_targets(targets.begin(), targets.end());
975 std::sort(sorted_targets.begin(), sorted_targets.end(),
976 [](const Target* lhs, const Target* rhs) {
977 return lhs->label() < rhs->label();
978 });
979
980 return sorted_targets;
981 }
982
AddBinaryTarget(const Target * target,base::Environment * env,Err * err)983 PBXNativeTarget* XcodeProject::AddBinaryTarget(const Target* target,
984 base::Environment* env,
985 Err* err) {
986 DCHECK_EQ(target->output_type(), Target::EXECUTABLE);
987
988 std::string output_dir = target->output_dir().value();
989 if (output_dir.empty()) {
990 const Tool* tool = target->toolchain()->GetToolForTargetFinalOutput(target);
991 if (!tool) {
992 std::string tool_name = Tool::GetToolTypeForTargetFinalOutput(target);
993 *err = Err(nullptr, tool_name + " tool not defined",
994 "The toolchain " +
995 target->toolchain()->label().GetUserVisibleName(false) +
996 " used by target " +
997 target->label().GetUserVisibleName(false) +
998 " doesn't define a \"" + tool_name + "\" tool.");
999 return nullptr;
1000 }
1001 output_dir = SubstitutionWriter::ApplyPatternToLinkerAsOutputFile(
1002 target, tool, tool->default_output_dir())
1003 .value();
1004 } else {
1005 output_dir = RebasePath(output_dir, build_settings_->build_dir());
1006 }
1007
1008 return project_.AddNativeTarget(
1009 target->label().name(), "compiled.mach-o.executable",
1010 target->output_name().empty() ? target->label().name()
1011 : target->output_name(),
1012 "com.apple.product-type.tool", GetConfigOutputDir(output_dir),
1013 GetBuildScript(target->label(), options_.ninja_executable,
1014 GetConfigOutputDir("."), env));
1015 }
1016
AddBundleTarget(const Target * target,base::Environment * env,Err * err)1017 PBXNativeTarget* XcodeProject::AddBundleTarget(const Target* target,
1018 base::Environment* env,
1019 Err* err) {
1020 DCHECK_EQ(target->output_type(), Target::CREATE_BUNDLE);
1021
1022 std::string pbxtarget_name = target->label().name();
1023 if (IsXCUITestModuleTarget(target)) {
1024 std::string target_name = target->label().name();
1025 pbxtarget_name = target_name.substr(
1026 0, target_name.rfind(kXCTestModuleTargetNamePostfix));
1027 }
1028
1029 PBXAttributes xcode_extra_attributes =
1030 target->bundle_data().xcode_extra_attributes();
1031 if (options_.build_system == XcodeBuildSystem::kLegacy) {
1032 xcode_extra_attributes["CODE_SIGN_IDENTITY"] = "";
1033 }
1034
1035 const std::string& target_output_name = RebasePath(
1036 target->bundle_data().GetBundleRootDirOutput(target->settings()).value(),
1037 build_settings_->build_dir());
1038
1039 const std::string output_dir =
1040 RebasePath(target->bundle_data().GetBundleDir(target->settings()).value(),
1041 build_settings_->build_dir());
1042
1043 return project_.AddNativeTarget(
1044 pbxtarget_name, std::string(), target_output_name,
1045 target->bundle_data().product_type(), GetConfigOutputDir(output_dir),
1046 GetBuildScript(target->label(), options_.ninja_executable,
1047 GetConfigOutputDir("."), env),
1048 xcode_extra_attributes);
1049 }
1050
GetConfigOutputDir(std::string_view output_dir)1051 std::string XcodeProject::GetConfigOutputDir(std::string_view output_dir) {
1052 if (options_.configuration_build_dir.empty())
1053 return std::string(output_dir);
1054
1055 base::FilePath config_output_dir(options_.configuration_build_dir);
1056 if (output_dir != ".") {
1057 config_output_dir = config_output_dir.Append(UTF8ToFilePath(output_dir));
1058 }
1059
1060 return RebasePath(FilePathToUTF8(config_output_dir.StripTrailingSeparators()),
1061 build_settings_->build_dir(),
1062 build_settings_->root_path_utf8());
1063 }
1064
WriteFileContent(std::ostream & out) const1065 void XcodeProject::WriteFileContent(std::ostream& out) const {
1066 out << "// !$*UTF8*$!\n"
1067 << "{\n"
1068 << "\tarchiveVersion = 1;\n"
1069 << "\tclasses = {\n"
1070 << "\t};\n"
1071 << "\tobjectVersion = 46;\n"
1072 << "\tobjects = {\n";
1073
1074 for (auto& pair : CollectPBXObjectsPerClass(&project_)) {
1075 out << "\n" << "/* Begin " << ToString(pair.first) << " section */\n";
1076 std::sort(pair.second.begin(), pair.second.end(),
1077 [](const PBXObject* a, const PBXObject* b) {
1078 return a->id() < b->id();
1079 });
1080 for (auto* object : pair.second) {
1081 object->Print(out, 2);
1082 }
1083 out << "/* End " << ToString(pair.first) << " section */\n";
1084 }
1085
1086 out << "\t};\n"
1087 << "\trootObject = " << project_.Reference() << ";\n"
1088 << "}\n";
1089 }
1090
1091 // static
RunAndWriteFiles(const BuildSettings * build_settings,const Builder & builder,Options options,Err * err)1092 bool XcodeWriter::RunAndWriteFiles(const BuildSettings* build_settings,
1093 const Builder& builder,
1094 Options options,
1095 Err* err) {
1096 XcodeProject project(build_settings, options);
1097 if (!project.AddSourcesFromBuilder(builder, err))
1098 return false;
1099
1100 if (!project.AddTargetsFromBuilder(builder, err))
1101 return false;
1102
1103 if (!project.AssignIds(err))
1104 return false;
1105
1106 if (!project.WriteFile(err))
1107 return false;
1108
1109 return true;
1110 }
1111