blob: 8216aa40ae2d04c92f9d7ff57aaa176409874c5b [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "gn/xcode_writer.h"
#include <iomanip>
#include <iterator>
#include <map>
#include <memory>
#include <optional>
#include <sstream>
#include <string>
#include <string_view>
#include <utility>
#include "base/environment.h"
#include "base/files/file_enumerator.h"
#include "base/logging.h"
#include "base/sha1.h"
#include "base/stl_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "gn/args.h"
#include "gn/build_settings.h"
#include "gn/builder.h"
#include "gn/commands.h"
#include "gn/deps_iterator.h"
#include "gn/filesystem_utils.h"
#include "gn/item.h"
#include "gn/loader.h"
#include "gn/scheduler.h"
#include "gn/settings.h"
#include "gn/source_file.h"
#include "gn/string_output_buffer.h"
#include "gn/substitution_writer.h"
#include "gn/target.h"
#include "gn/value.h"
#include "gn/variables.h"
#include "gn/xcode_object.h"
namespace {
enum TargetOsType {
WRITER_TARGET_OS_IOS,
WRITER_TARGET_OS_MACOS,
};
const char* kXCTestFileSuffixes[] = {
"egtest.m", "egtest.mm", "egtest.swift", "xctest.m", "xctest.mm",
"xctest.swift", "UITests.m", "UITests.mm", "UITests.swift",
};
const char kXCTestModuleTargetNamePostfix[] = "_module";
const char kXCUITestRunnerTargetNamePostfix[] = "_runner";
struct SafeEnvironmentVariableInfo {
const char* name;
bool capture_at_generation;
};
SafeEnvironmentVariableInfo kSafeEnvironmentVariables[] = {
{"HOME", true},
{"LANG", true},
{"PATH", true},
{"USER", true},
{"TMPDIR", false},
{"ICECC_VERSION", true},
{"ICECC_CLANG_REMOTE_CPP", true}};
TargetOsType GetTargetOs(const Args& args) {
const Value* target_os_value = args.GetArgOverride(variables::kTargetOs);
if (target_os_value) {
if (target_os_value->type() == Value::STRING) {
if (target_os_value->string_value() == "ios")
return WRITER_TARGET_OS_IOS;
}
}
return WRITER_TARGET_OS_MACOS;
}
std::string GetBuildScript(const std::string& target_name,
const std::string& ninja_executable,
const std::string& build_dir,
base::Environment* environment) {
// Launch ninja with a sanitized environment (Xcode sets many environment
// variables overridding settings, including the SDK, thus breaking hermetic
// build).
std::stringstream buffer;
buffer << "exec env -i ";
// Write environment.
for (const auto& variable : kSafeEnvironmentVariables) {
buffer << variable.name << "=";
if (variable.capture_at_generation) {
std::string value;
environment->GetVar(variable.name, &value);
buffer << "'" << value << "'";
} else {
buffer << "\"${" << variable.name << "}\"";
}
buffer << " ";
}
if (ninja_executable.empty()) {
buffer << "ninja";
} else {
buffer << ninja_executable;
}
buffer << " -C " << build_dir;
if (!target_name.empty()) {
buffer << " '" << target_name << "'";
}
return buffer.str();
}
bool IsApplicationTarget(const Target* target) {
return target->output_type() == Target::CREATE_BUNDLE &&
target->bundle_data().product_type() ==
"com.apple.product-type.application";
}
bool IsXCUITestRunnerTarget(const Target* target) {
return IsApplicationTarget(target) &&
base::EndsWith(target->label().name(),
kXCUITestRunnerTargetNamePostfix,
base::CompareCase::SENSITIVE);
}
bool IsXCTestModuleTarget(const Target* target) {
return target->output_type() == Target::CREATE_BUNDLE &&
target->bundle_data().product_type() ==
"com.apple.product-type.bundle.unit-test" &&
base::EndsWith(target->label().name(), kXCTestModuleTargetNamePostfix,
base::CompareCase::SENSITIVE);
}
bool IsXCUITestModuleTarget(const Target* target) {
return target->output_type() == Target::CREATE_BUNDLE &&
target->bundle_data().product_type() ==
"com.apple.product-type.bundle.ui-testing" &&
base::EndsWith(target->label().name(), kXCTestModuleTargetNamePostfix,
base::CompareCase::SENSITIVE);
}
bool IsXCTestFile(const SourceFile& file) {
std::string file_name = file.GetName();
for (size_t i = 0; i < std::size(kXCTestFileSuffixes); ++i) {
if (base::EndsWith(file_name, kXCTestFileSuffixes[i],
base::CompareCase::SENSITIVE)) {
return true;
}
}
return false;
}
// Finds the application target from its target name.
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) {
for (auto& pair : targets) {
const Target* target = pair.first;
if (target->label().name() == target_name) {
if (!IsApplicationTarget(target)) {
*err = Err(node, "host application target \"" + target_name +
"\" not an application bundle");
return std::nullopt;
}
DCHECK(pair.first);
DCHECK(pair.second);
return pair;
}
}
*err =
Err(node, "cannot find host application bundle \"" + target_name + "\"");
return std::nullopt;
}
// Adds |base_pbxtarget| as a dependency of |dependent_pbxtarget| in the
// generated Xcode project.
void AddPBXTargetDependency(const PBXTarget* base_pbxtarget,
PBXTarget* dependent_pbxtarget,
const PBXProject* project) {
auto container_item_proxy =
std::make_unique<PBXContainerItemProxy>(project, base_pbxtarget);
auto dependency = std::make_unique<PBXTargetDependency>(
base_pbxtarget, std::move(container_item_proxy));
dependent_pbxtarget->AddDependency(std::move(dependency));
}
// Returns a SourceFile for absolute path `file_path` below `//`.
SourceFile FilePathToSourceFile(const BuildSettings* build_settings,
const base::FilePath& file_path) {
const std::string file_path_utf8 = FilePathToUTF8(file_path);
return SourceFile("//" + file_path_utf8.substr(
build_settings->root_path_utf8().size() + 1));
}
// Returns the list of patterns to use when looking for additional files
// from `options`.
std::vector<base::FilePath::StringType> GetAdditionalFilesPatterns(
const XcodeWriter::Options& options) {
return base::SplitString(options.additional_files_patterns,
FILE_PATH_LITERAL(";"), base::TRIM_WHITESPACE,
base::SPLIT_WANT_ALL);
}
// Returns the list of roots to use when looking for additional files
// from `options`.
std::vector<base::FilePath> GetAdditionalFilesRoots(
const BuildSettings* build_settings,
const XcodeWriter::Options& options) {
if (options.additional_files_roots.empty()) {
return {build_settings->root_path()};
}
const std::vector<base::FilePath::StringType> roots =
base::SplitString(options.additional_files_roots, FILE_PATH_LITERAL(";"),
base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
std::vector<base::FilePath> root_paths;
for (const base::FilePath::StringType& root : roots) {
const std::string rebased_root =
RebasePath(FilePathToUTF8(root), SourceDir("//"),
build_settings->root_path_utf8());
root_paths.push_back(
build_settings->root_path().Append(UTF8ToFilePath(rebased_root)));
}
return root_paths;
}
// Helper class to resolve list of XCTest files per target.
//
// Uses a cache of file found per intermediate targets to reduce the need
// to shared targets multiple times. It is recommended to reuse the same
// object to resolve all the targets for a project.
class XCTestFilesResolver {
public:
XCTestFilesResolver();
~XCTestFilesResolver();
// Returns a set of all XCTest files for |target|. The returned reference
// may be invalidated the next time this method is called.
const SourceFileSet& SearchFilesForTarget(const Target* target);
private:
std::map<const Target*, SourceFileSet> cache_;
};
XCTestFilesResolver::XCTestFilesResolver() = default;
XCTestFilesResolver::~XCTestFilesResolver() = default;
const SourceFileSet& XCTestFilesResolver::SearchFilesForTarget(
const Target* target) {
// Early return if already visited and processed.
auto iter = cache_.find(target);
if (iter != cache_.end())
return iter->second;
SourceFileSet xctest_files;
for (const SourceFile& file : target->sources()) {
if (IsXCTestFile(file)) {
xctest_files.insert(file);
}
}
// Call recursively on public and private deps.
for (const auto& t : target->public_deps()) {
const SourceFileSet& deps_xctest_files = SearchFilesForTarget(t.ptr);
xctest_files.insert(deps_xctest_files.begin(), deps_xctest_files.end());
}
for (const auto& t : target->private_deps()) {
const SourceFileSet& deps_xctest_files = SearchFilesForTarget(t.ptr);
xctest_files.insert(deps_xctest_files.begin(), deps_xctest_files.end());
}
auto insert = cache_.insert(std::make_pair(target, xctest_files));
DCHECK(insert.second);
return insert.first->second;
}
// Add xctest files to the "Compiler Sources" of corresponding test module
// native targets.
void AddXCTestFilesToTestModuleTarget(const std::vector<SourceFile>& sources,
PBXNativeTarget* native_target,
PBXProject* project,
SourceDir source_dir,
const BuildSettings* build_settings) {
for (const SourceFile& source : sources) {
const std::string source_path = RebasePath(
source.value(), source_dir, build_settings->root_path_utf8());
project->AddSourceFile(source_path, source_path, native_target);
}
}
// Helper class to collect all PBXObject per class.
class CollectPBXObjectsPerClassHelper : public PBXObjectVisitorConst {
public:
CollectPBXObjectsPerClassHelper() = default;
void Visit(const PBXObject* object) override {
DCHECK(object);
objects_per_class_[object->Class()].push_back(object);
}
const std::map<PBXObjectClass, std::vector<const PBXObject*>>&
objects_per_class() const {
return objects_per_class_;
}
private:
std::map<PBXObjectClass, std::vector<const PBXObject*>> objects_per_class_;
CollectPBXObjectsPerClassHelper(const CollectPBXObjectsPerClassHelper&) =
delete;
CollectPBXObjectsPerClassHelper& operator=(
const CollectPBXObjectsPerClassHelper&) = delete;
};
std::map<PBXObjectClass, std::vector<const PBXObject*>>
CollectPBXObjectsPerClass(const PBXProject* project) {
CollectPBXObjectsPerClassHelper visitor;
project->Visit(visitor);
return visitor.objects_per_class();
}
// Helper class to assign unique ids to all PBXObject.
class RecursivelyAssignIdsHelper : public PBXObjectVisitor {
public:
RecursivelyAssignIdsHelper(const std::string& seed)
: seed_(seed), counter_(0) {}
void Visit(PBXObject* object) override {
std::stringstream buffer;
buffer << seed_ << " " << object->Name() << " " << counter_;
std::string hash = base::SHA1HashString(buffer.str());
DCHECK_EQ(hash.size() % 4, 0u);
uint32_t id[3] = {0, 0, 0};
const uint32_t* ptr = reinterpret_cast<const uint32_t*>(hash.data());
for (size_t i = 0; i < hash.size() / 4; i++)
id[i % 3] ^= ptr[i];
object->SetId(base::HexEncode(id, sizeof(id)));
++counter_;
}
private:
std::string seed_;
int64_t counter_;
RecursivelyAssignIdsHelper(const RecursivelyAssignIdsHelper&) = delete;
RecursivelyAssignIdsHelper& operator=(const RecursivelyAssignIdsHelper&) =
delete;
};
void RecursivelyAssignIds(PBXProject* project) {
RecursivelyAssignIdsHelper visitor(project->Name());
project->Visit(visitor);
}
// Returns a list of configuration names from the options passed to the
// generator. If no configuration names have been passed, return default
// value.
std::vector<std::string> ConfigListFromOptions(const std::string& configs) {
std::vector<std::string> result = base::SplitString(
configs, ";", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
if (result.empty())
result.push_back(std::string("Release"));
return result;
}
// Returns the path to root_src_dir from settings.
std::string SourcePathFromBuildSettings(const BuildSettings* build_settings) {
return RebasePath("//", build_settings->build_dir());
}
// Returns the default attributes for the project from settings.
PBXAttributes ProjectAttributesFromBuildSettings(
const BuildSettings* build_settings) {
const TargetOsType target_os = GetTargetOs(build_settings->build_args());
PBXAttributes attributes;
switch (target_os) {
case WRITER_TARGET_OS_IOS:
attributes["SDKROOT"] = "iphoneos";
attributes["TARGETED_DEVICE_FAMILY"] = "1,2";
break;
case WRITER_TARGET_OS_MACOS:
attributes["SDKROOT"] = "macosx";
break;
}
// Xcode complains that the project needs to be upgraded if those keys are
// not set. Since the generated Xcode project is only used for debugging
// and the source of truth for build settings is the .gn files themselves,
// we can safely set them in the project as they won't be used by "ninja".
attributes["ALWAYS_SEARCH_USER_PATHS"] = "NO";
attributes["CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED"] = "YES";
attributes["CLANG_WARN__DUPLICATE_METHOD_MATCH"] = "YES";
attributes["CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING"] = "YES";
attributes["CLANG_WARN_BOOL_CONVERSION"] = "YES";
attributes["CLANG_WARN_COMMA"] = "YES";
attributes["CLANG_WARN_CONSTANT_CONVERSION"] = "YES";
attributes["CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS"] = "YES";
attributes["CLANG_WARN_EMPTY_BODY"] = "YES";
attributes["CLANG_WARN_ENUM_CONVERSION"] = "YES";
attributes["CLANG_WARN_INFINITE_RECURSION"] = "YES";
attributes["CLANG_WARN_INT_CONVERSION"] = "YES";
attributes["CLANG_WARN_NON_LITERAL_NULL_CONVERSION"] = "YES";
attributes["CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF"] = "YES";
attributes["CLANG_WARN_OBJC_LITERAL_CONVERSION"] = "YES";
attributes["CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER"] = "YES";
attributes["CLANG_WARN_RANGE_LOOP_ANALYSIS"] = "YES";
attributes["CLANG_WARN_STRICT_PROTOTYPES"] = "YES";
attributes["CLANG_WARN_SUSPICIOUS_MOVE"] = "YES";
attributes["CLANG_WARN_UNREACHABLE_CODE"] = "YES";
attributes["ENABLE_STRICT_OBJC_MSGSEND"] = "YES";
attributes["ENABLE_TESTABILITY"] = "YES";
attributes["GCC_NO_COMMON_BLOCKS"] = "YES";
attributes["GCC_WARN_64_TO_32_BIT_CONVERSION"] = "YES";
attributes["GCC_WARN_ABOUT_RETURN_TYPE"] = "YES";
attributes["GCC_WARN_UNDECLARED_SELECTOR"] = "YES";
attributes["GCC_WARN_UNINITIALIZED_AUTOS"] = "YES";
attributes["GCC_WARN_UNUSED_FUNCTION"] = "YES";
attributes["GCC_WARN_UNUSED_VARIABLE"] = "YES";
attributes["ONLY_ACTIVE_ARCH"] = "YES";
return attributes;
}
} // namespace
// Class representing the workspace embedded in an xcodeproj file used to
// configure the build settings shared by all targets in the project (used
// to configure the build system).
class XcodeWorkspace {
public:
XcodeWorkspace(const BuildSettings* build_settings,
XcodeWriter::Options options);
~XcodeWorkspace();
XcodeWorkspace(const XcodeWorkspace&) = delete;
XcodeWorkspace& operator=(const XcodeWorkspace&) = delete;
// Generates the .xcworkspace files to disk.
bool WriteWorkspace(const std::string& name, Err* err) const;
private:
// Writes the workspace data file.
bool WriteWorkspaceDataFile(const std::string& name, Err* err) const;
// Writes the settings file.
bool WriteSettingsFile(const std::string& name, Err* err) const;
const BuildSettings* build_settings_ = nullptr;
XcodeWriter::Options options_;
};
XcodeWorkspace::XcodeWorkspace(const BuildSettings* build_settings,
XcodeWriter::Options options)
: build_settings_(build_settings), options_(options) {}
XcodeWorkspace::~XcodeWorkspace() = default;
bool XcodeWorkspace::WriteWorkspace(const std::string& name, Err* err) const {
return WriteWorkspaceDataFile(name, err) && WriteSettingsFile(name, err);
}
bool XcodeWorkspace::WriteWorkspaceDataFile(const std::string& name,
Err* err) const {
const SourceFile source_file =
build_settings_->build_dir().ResolveRelativeFile(
Value(nullptr, name + "/contents.xcworkspacedata"), err);
if (source_file.is_null())
return false;
StringOutputBuffer storage;
std::ostream out(&storage);
out << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
<< "<Workspace\n"
<< " version = \"1.0\">\n"
<< " <FileRef\n"
<< " location = \"self:\">\n"
<< " </FileRef>\n"
<< "</Workspace>\n";
return storage.WriteToFileIfChanged(build_settings_->GetFullPath(source_file),
err);
}
bool XcodeWorkspace::WriteSettingsFile(const std::string& name,
Err* err) const {
const SourceFile source_file =
build_settings_->build_dir().ResolveRelativeFile(
Value(nullptr, name + "/xcshareddata/WorkspaceSettings.xcsettings"),
err);
if (source_file.is_null())
return false;
StringOutputBuffer storage;
std::ostream out(&storage);
out << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
<< "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" "
<< "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
<< "<plist version=\"1.0\">\n"
<< "<dict>\n";
switch (options_.build_system) {
case XcodeBuildSystem::kLegacy:
out << "\t<key>BuildSystemType</key>\n"
<< "\t<string>Original</string>\n";
break;
case XcodeBuildSystem::kNew:
break;
}
out << "</dict>\n"
<< "</plist>\n";
return storage.WriteToFileIfChanged(build_settings_->GetFullPath(source_file),
err);
}
// Class responsible for constructing and writing the .xcodeproj from the
// targets known to gn. It currently requires using the "Legacy build system"
// so it will embed an .xcworkspace file to force the setting.
class XcodeProject {
public:
XcodeProject(const BuildSettings* build_settings,
XcodeWriter::Options options);
~XcodeProject();
// Recursively finds "source" files from |builder| and adds them to the
// project (this includes more than just text source files, e.g. images
// in resources, ...).
bool AddSourcesFromBuilder(const Builder& builder, Err* err);
// Recursively finds targets from |builder| and adds them to the project.
// Only targets of type CREATE_BUNDLE or EXECUTABLE are kept since they
// are the only one that can be run and thus debugged from Xcode.
bool AddTargetsFromBuilder(const Builder& builder, Err* err);
// Assigns ids to all PBXObject that were added to the project. Must be
// called before calling WriteFile().
bool AssignIds(Err* err);
// Generates the project file and the .xcodeproj file to disk if updated
// (i.e. if the generated project is identical to the currently existing
// one, it is not overwritten).
bool WriteFile(Err* err) const;
private:
// Finds all targets that needs to be generated for the project (applies
// the filter passed via |options|).
std::optional<std::vector<const Target*>> GetTargetsFromBuilder(
const Builder& builder,
Err* err) const;
// Adds a target of type EXECUTABLE to the project.
PBXNativeTarget* AddBinaryTarget(const Target* target,
base::Environment* env,
Err* err);
// Adds a target of type CREATE_BUNDLE to the project.
PBXNativeTarget* AddBundleTarget(const Target* target,
base::Environment* env,
Err* err);
// Adds the XCTest source files for all test xctest or xcuitest module target
// to allow Xcode to index the list of tests (thus allowing to run individual
// tests from Xcode UI).
bool AddCXTestSourceFilesForTestModuleTargets(
const std::map<const Target*, PBXNativeTarget*>& bundle_targets,
Err* err);
// Adds the corresponding test application target as dependency of xctest or
// xcuitest module target in the generated Xcode project.
bool AddDependencyTargetsForTestModuleTargets(
const std::map<const Target*, PBXNativeTarget*>& bundle_targets,
Err* err);
// Tweak `output_dir` to be relative to the configuration specific output
// directory (see --xcode-config-build-dir=... flag).
std::string GetConfigOutputDir(std::string_view output_dir);
// Generates the content of the .xcodeproj file into |out|.
void WriteFileContent(std::ostream& out) const;
// Returns whether the file should be added to the project.
bool ShouldIncludeFileInProject(const SourceFile& source) const;
const BuildSettings* build_settings_;
XcodeWriter::Options options_;
PBXProject project_;
};
XcodeProject::XcodeProject(const BuildSettings* build_settings,
XcodeWriter::Options options)
: build_settings_(build_settings),
options_(options),
project_(options.project_name,
ConfigListFromOptions(options.configurations),
SourcePathFromBuildSettings(build_settings),
ProjectAttributesFromBuildSettings(build_settings)) {}
XcodeProject::~XcodeProject() = default;
bool XcodeProject::ShouldIncludeFileInProject(const SourceFile& source) const {
if (IsStringInOutputDir(build_settings_->build_dir(), source.value()))
return false;
if (IsPathAbsolute(source.value()))
return false;
return true;
}
bool XcodeProject::AddSourcesFromBuilder(const Builder& builder, Err* err) {
SourceFileSet sources;
// Add sources from all targets.
for (const Target* target : builder.GetAllResolvedTargets()) {
for (const SourceFile& source : target->sources()) {
if (ShouldIncludeFileInProject(source))
sources.insert(source);
}
for (const SourceFile& source : target->config_values().inputs()) {
if (ShouldIncludeFileInProject(source))
sources.insert(source);
}
for (const SourceFile& source : target->public_headers()) {
if (ShouldIncludeFileInProject(source))
sources.insert(source);
}
const SourceFile& bridge_header = target->swift_values().bridge_header();
if (!bridge_header.is_null() && ShouldIncludeFileInProject(bridge_header)) {
sources.insert(bridge_header);
}
if (target->output_type() == Target::ACTION ||
target->output_type() == Target::ACTION_FOREACH) {
if (ShouldIncludeFileInProject(target->action_values().script()))
sources.insert(target->action_values().script());
}
}
// Add BUILD.gn and *.gni for targets, configs and toolchains.
for (const Item* item : builder.GetAllResolvedItems()) {
if (!item->AsConfig() && !item->AsTarget() && !item->AsToolchain())
continue;
const SourceFile build = builder.loader()->BuildFileForLabel(item->label());
if (ShouldIncludeFileInProject(build))
sources.insert(build);
for (const SourceFile& source :
item->settings()->import_manager().GetImportedFiles()) {
if (ShouldIncludeFileInProject(source))
sources.insert(source);
}
}
// Add other files read by gn (the main dotfile, exec_script scripts, ...).
for (const auto& path : g_scheduler->GetGenDependencies()) {
if (!build_settings_->root_path().IsParent(path))
continue;
const SourceFile source = FilePathToSourceFile(build_settings_, path);
if (ShouldIncludeFileInProject(source))
sources.insert(source);
}
// Add any files from --xcode-additional-files-patterns, using the root
// listed in --xcode-additional-files-roots.
if (!options_.additional_files_patterns.empty()) {
const std::vector<base::FilePath::StringType> patterns =
GetAdditionalFilesPatterns(options_);
const std::vector<base::FilePath> roots =
GetAdditionalFilesRoots(build_settings_, options_);
for (const base::FilePath& root : roots) {
for (const base::FilePath::StringType& pattern : patterns) {
base::FileEnumerator it(root, /*recursive*/ true,
base::FileEnumerator::FILES, pattern,
base::FileEnumerator::FolderSearchPolicy::ALL);
for (base::FilePath path = it.Next(); !path.empty(); path = it.Next()) {
const SourceFile source = FilePathToSourceFile(build_settings_, path);
if (ShouldIncludeFileInProject(source))
sources.insert(source);
}
}
}
}
// Sort files to ensure deterministic generation of the project file (and
// nicely sorted file list in Xcode).
std::vector<SourceFile> sorted_sources(sources.begin(), sources.end());
std::sort(sorted_sources.begin(), sorted_sources.end());
const SourceDir source_dir("//");
for (const SourceFile& source : sorted_sources) {
const std::string source_file = RebasePath(
source.value(), source_dir, build_settings_->root_path_utf8());
project_.AddSourceFileToIndexingTarget(source_file, source_file);
}
return true;
}
bool XcodeProject::AddTargetsFromBuilder(const Builder& builder, Err* err) {
std::unique_ptr<base::Environment> env(base::Environment::Create());
project_.AddAggregateTarget(
"All", GetConfigOutputDir("."),
GetBuildScript(options_.root_target_name, options_.ninja_executable,
GetConfigOutputDir("."), env.get()));
const std::optional<std::vector<const Target*>> targets =
GetTargetsFromBuilder(builder, err);
if (!targets)
return false;
std::map<const Target*, PBXNativeTarget*> bundle_targets;
const TargetOsType target_os = GetTargetOs(build_settings_->build_args());
for (const Target* target : *targets) {
PBXNativeTarget* native_target = nullptr;
switch (target->output_type()) {
case Target::EXECUTABLE:
if (target_os == WRITER_TARGET_OS_IOS)
continue;
native_target = AddBinaryTarget(target, env.get(), err);
if (!native_target)
return false;
break;
case Target::CREATE_BUNDLE: {
if (target->bundle_data().product_type().empty())
continue;
// For XCUITest, two CREATE_BUNDLE targets are generated:
// ${target_name}_runner and ${target_name}_module, however, Xcode
// requires only one target named ${target_name} to run tests.
if (IsXCUITestRunnerTarget(target))
continue;
native_target = AddBundleTarget(target, env.get(), err);
if (!native_target)
return false;
bundle_targets.insert(std::make_pair(target, native_target));
break;
}
default:
break;
}
}
if (!AddCXTestSourceFilesForTestModuleTargets(bundle_targets, err))
return false;
// Adding the corresponding test application target as a dependency of xctest
// or xcuitest module target in the generated Xcode project so that the
// application target is re-compiled when compiling the test module target.
if (!AddDependencyTargetsForTestModuleTargets(bundle_targets, err))
return false;
return true;
}
bool XcodeProject::AddCXTestSourceFilesForTestModuleTargets(
const std::map<const Target*, PBXNativeTarget*>& bundle_targets,
Err* err) {
const SourceDir source_dir("//");
// Needs to search for xctest files under the application targets, and this
// variable is used to store the results of visited targets, thus making the
// search more efficient.
XCTestFilesResolver resolver;
for (const auto& pair : bundle_targets) {
const Target* target = pair.first;
if (!IsXCTestModuleTarget(target) && !IsXCUITestModuleTarget(target))
continue;
// For XCTest, test files are compiled into the application bundle.
// For XCUITest, test files are compiled into the test module bundle.
const Target* target_with_xctest_files = nullptr;
if (IsXCTestModuleTarget(target)) {
auto app_pair = FindApplicationTargetByName(
target->defined_from(),
target->bundle_data().xcode_test_application_name(), bundle_targets,
err);
if (!app_pair)
return false;
target_with_xctest_files = app_pair.value().first;
} else {
DCHECK(IsXCUITestModuleTarget(target));
target_with_xctest_files = target;
}
const SourceFileSet& sources =
resolver.SearchFilesForTarget(target_with_xctest_files);
// Sort files to ensure deterministic generation of the project file (and
// nicely sorted file list in Xcode).
std::vector<SourceFile> sorted_sources(sources.begin(), sources.end());
std::sort(sorted_sources.begin(), sorted_sources.end());
// Add xctest files to the "Compiler Sources" of corresponding xctest
// and xcuitest native targets for proper indexing and for discovery of
// tests function.
AddXCTestFilesToTestModuleTarget(sorted_sources, pair.second, &project_,
source_dir, build_settings_);
}
return true;
}
bool XcodeProject::AddDependencyTargetsForTestModuleTargets(
const std::map<const Target*, PBXNativeTarget*>& bundle_targets,
Err* err) {
for (const auto& pair : bundle_targets) {
const Target* target = pair.first;
if (!IsXCTestModuleTarget(target) && !IsXCUITestModuleTarget(target))
continue;
auto app_pair = FindApplicationTargetByName(
target->defined_from(),
target->bundle_data().xcode_test_application_name(), bundle_targets,
err);
if (!app_pair)
return false;
AddPBXTargetDependency(app_pair.value().second, pair.second, &project_);
}
return true;
}
bool XcodeProject::AssignIds(Err* err) {
RecursivelyAssignIds(&project_);
return true;
}
bool XcodeProject::WriteFile(Err* err) const {
DCHECK(!project_.id().empty());
SourceFile pbxproj_file = build_settings_->build_dir().ResolveRelativeFile(
Value(nullptr, project_.Name() + ".xcodeproj/project.pbxproj"), err);
if (pbxproj_file.is_null())
return false;
StringOutputBuffer storage;
std::ostream pbxproj_string_out(&storage);
WriteFileContent(pbxproj_string_out);
if (!storage.WriteToFileIfChanged(build_settings_->GetFullPath(pbxproj_file),
err)) {
return false;
}
XcodeWorkspace workspace(build_settings_, options_);
return workspace.WriteWorkspace(
project_.Name() + ".xcodeproj/project.xcworkspace", err);
}
std::optional<std::vector<const Target*>> XcodeProject::GetTargetsFromBuilder(
const Builder& builder,
Err* err) const {
std::vector<const Target*> all_targets = builder.GetAllResolvedTargets();
// Filter targets according to the dir_filters_string if defined.
if (!options_.dir_filters_string.empty()) {
std::vector<LabelPattern> filters;
if (!commands::FilterPatternsFromString(
build_settings_, options_.dir_filters_string, &filters, err)) {
return std::nullopt;
}
std::vector<const Target*> unfiltered_targets;
std::swap(unfiltered_targets, all_targets);
commands::FilterTargetsByPatterns(unfiltered_targets, filters,
&all_targets);
}
// Filter out all target of type EXECUTABLE that are direct dependency of
// a BUNDLE_DATA target (under the assumption that they will be part of a
// CREATE_BUNDLE target generating an application bundle).
TargetSet targets(all_targets.begin(), all_targets.end());
for (const Target* target : all_targets) {
if (!target->settings()->is_default())
continue;
if (target->output_type() != Target::BUNDLE_DATA)
continue;
for (const auto& pair : target->GetDeps(Target::DEPS_LINKED)) {
if (pair.ptr->output_type() != Target::EXECUTABLE)
continue;
targets.erase(pair.ptr);
}
}
// Sort the list of targets per-label to get a consistent ordering of them
// in the generated Xcode project (and thus stability of the file generated).
std::vector<const Target*> sorted_targets(targets.begin(), targets.end());
std::sort(sorted_targets.begin(), sorted_targets.end(),
[](const Target* lhs, const Target* rhs) {
return lhs->label() < rhs->label();
});
return sorted_targets;
}
PBXNativeTarget* XcodeProject::AddBinaryTarget(const Target* target,
base::Environment* env,
Err* err) {
DCHECK_EQ(target->output_type(), Target::EXECUTABLE);
std::string output_dir = target->output_dir().value();
if (output_dir.empty()) {
const Tool* tool = target->toolchain()->GetToolForTargetFinalOutput(target);
if (!tool) {
std::string tool_name = Tool::GetToolTypeForTargetFinalOutput(target);
*err = Err(nullptr, tool_name + " tool not defined",
"The toolchain " +
target->toolchain()->label().GetUserVisibleName(false) +
" used by target " +
target->label().GetUserVisibleName(false) +
" doesn't define a \"" + tool_name + "\" tool.");
return nullptr;
}
output_dir = SubstitutionWriter::ApplyPatternToLinkerAsOutputFile(
target, tool, tool->default_output_dir())
.value();
} else {
output_dir = RebasePath(output_dir, build_settings_->build_dir());
}
return project_.AddNativeTarget(
target->label().name(), "compiled.mach-o.executable",
target->output_name().empty() ? target->label().name()
: target->output_name(),
"com.apple.product-type.tool", GetConfigOutputDir(output_dir),
GetBuildScript(target->label().name(), options_.ninja_executable,
GetConfigOutputDir("."), env));
}
PBXNativeTarget* XcodeProject::AddBundleTarget(const Target* target,
base::Environment* env,
Err* err) {
DCHECK_EQ(target->output_type(), Target::CREATE_BUNDLE);
std::string pbxtarget_name = target->label().name();
if (IsXCUITestModuleTarget(target)) {
std::string target_name = target->label().name();
pbxtarget_name = target_name.substr(
0, target_name.rfind(kXCTestModuleTargetNamePostfix));
}
PBXAttributes xcode_extra_attributes =
target->bundle_data().xcode_extra_attributes();
if (options_.build_system == XcodeBuildSystem::kLegacy) {
xcode_extra_attributes["CODE_SIGN_IDENTITY"] = "";
}
const std::string& target_output_name = RebasePath(
target->bundle_data().GetBundleRootDirOutput(target->settings()).value(),
build_settings_->build_dir());
const std::string output_dir =
RebasePath(target->bundle_data().GetBundleDir(target->settings()).value(),
build_settings_->build_dir());
return project_.AddNativeTarget(
pbxtarget_name, std::string(), target_output_name,
target->bundle_data().product_type(), GetConfigOutputDir(output_dir),
GetBuildScript(pbxtarget_name, options_.ninja_executable,
GetConfigOutputDir("."), env),
xcode_extra_attributes);
}
std::string XcodeProject::GetConfigOutputDir(std::string_view output_dir) {
if (options_.configuration_build_dir.empty())
return std::string(output_dir);
base::FilePath config_output_dir(options_.configuration_build_dir);
if (output_dir != ".") {
config_output_dir = config_output_dir.Append(UTF8ToFilePath(output_dir));
}
return RebasePath(FilePathToUTF8(config_output_dir.StripTrailingSeparators()),
build_settings_->build_dir(),
build_settings_->root_path_utf8());
}
void XcodeProject::WriteFileContent(std::ostream& out) const {
out << "// !$*UTF8*$!\n"
<< "{\n"
<< "\tarchiveVersion = 1;\n"
<< "\tclasses = {\n"
<< "\t};\n"
<< "\tobjectVersion = 46;\n"
<< "\tobjects = {\n";
for (auto& pair : CollectPBXObjectsPerClass(&project_)) {
out << "\n"
<< "/* Begin " << ToString(pair.first) << " section */\n";
std::sort(pair.second.begin(), pair.second.end(),
[](const PBXObject* a, const PBXObject* b) {
return a->id() < b->id();
});
for (auto* object : pair.second) {
object->Print(out, 2);
}
out << "/* End " << ToString(pair.first) << " section */\n";
}
out << "\t};\n"
<< "\trootObject = " << project_.Reference() << ";\n"
<< "}\n";
}
// static
bool XcodeWriter::RunAndWriteFiles(const BuildSettings* build_settings,
const Builder& builder,
Options options,
Err* err) {
XcodeProject project(build_settings, options);
if (!project.AddSourcesFromBuilder(builder, err))
return false;
if (!project.AddTargetsFromBuilder(builder, err))
return false;
if (!project.AssignIds(err))
return false;
if (!project.WriteFile(err))
return false;
return true;
}