• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "gn/xcode_writer.h"
6 
7 #include <iomanip>
8 #include <iterator>
9 #include <map>
10 #include <memory>
11 #include <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