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