• 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 <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