Add support for multiple configuration to Xcode project generator

Add two flags `--xcode-configs` and `--xcode-config-build-dir` that
allow generating an Xcode project supporting multiple configurations.

This will speed up the generation of the Xcode project used by the
Chromium project by removing the need to use a script that modify
the project generated by gn.

Bug: chromium:1331345
Change-Id: Ifb5e652ba60f1a5228e9eddda8b2cdd83b82aac9
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/14142
Reviewed-by: Brett Wilson <brettw@chromium.org>
Commit-Queue: Sylvain Defresne <sdefresne@chromium.org>
diff --git a/docs/reference.md b/docs/reference.md
index 6ffec1d..084398d 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -828,6 +828,23 @@
       "legacy" - Legacy Build system
       "new" - New Build System
 
+  --xcode-configs=<config_name_list>
+      Configure the list of build configuration supported by the generated
+      project. If specified, must be a list of semicolon-separated strings.
+      If ommitted, a single configuration will be used in the generated
+      project derived from the build directory.
+
+  --xcode-config-build-dir=<string>
+      If present, must be a path relative to the source directory. It will
+      default to $root_out_dir if ommitted. The path is assumed to point to
+      the directory where ninja needs to be invoked. This variable can be
+      used to build for multiple configuration / platform / environment from
+      the same generated Xcode project (assuming that the user has created a
+      gn build directory with the correct args.gn for each).
+
+      One useful value is to use Xcode variables such as '${CONFIGURATION}'
+      or '${EFFECTIVE_PLATFORM}'.
+
   --ninja-executable=<string>
       Can be used to specify the ninja executable to use when building.
 
diff --git a/src/gn/command_gen.cc b/src/gn/command_gen.cc
index 23f15a0..fb68cf2 100644
--- a/src/gn/command_gen.cc
+++ b/src/gn/command_gen.cc
@@ -56,6 +56,8 @@
 const char kSwitchXcodeBuildSystem[] = "xcode-build-system";
 const char kSwitchXcodeBuildsystemValueLegacy[] = "legacy";
 const char kSwitchXcodeBuildsystemValueNew[] = "new";
+const char kSwitchXcodeConfigurations[] = "xcode-configs";
+const char kSwitchXcodeConfigurationBuildPath[] = "xcode-config-build-dir";
 const char kSwitchJsonFileName[] = "json-file-name";
 const char kSwitchJsonIdeScript[] = "json-ide-script";
 const char kSwitchJsonIdeScriptArgs[] = "json-ide-script-args";
@@ -257,6 +259,8 @@
         command_line->GetSwitchValueASCII(kSwitchIdeRootTarget),
         command_line->GetSwitchValueASCII(kSwitchNinjaExecutable),
         command_line->GetSwitchValueASCII(kSwitchFilters),
+        command_line->GetSwitchValueASCII(kSwitchXcodeConfigurations),
+        command_line->GetSwitchValuePath(kSwitchXcodeConfigurationBuildPath),
         XcodeBuildSystem::kLegacy,
     };
 
@@ -516,6 +520,23 @@
       "legacy" - Legacy Build system
       "new" - New Build System
 
+  --xcode-configs=<config_name_list>
+      Configure the list of build configuration supported by the generated
+      project. If specified, must be a list of semicolon-separated strings.
+      If ommitted, a single configuration will be used in the generated
+      project derived from the build directory.
+
+  --xcode-config-build-dir=<string>
+      If present, must be a path relative to the source directory. It will
+      default to $root_out_dir if ommitted. The path is assumed to point to
+      the directory where ninja needs to be invoked. This variable can be
+      used to build for multiple configuration / platform / environment from
+      the same generated Xcode project (assuming that the user has created a
+      gn build directory with the correct args.gn for each).
+
+      One useful value is to use Xcode variables such as '${CONFIGURATION}'
+      or '${EFFECTIVE_PLATFORM}'.
+
   --ninja-executable=<string>
       Can be used to specify the ninja executable to use when building.
 
diff --git a/src/gn/xcode_object.cc b/src/gn/xcode_object.cc
index 2bd65e3..ab357bb 100644
--- a/src/gn/xcode_object.cc
+++ b/src/gn/xcode_object.cc
@@ -388,10 +388,10 @@
 
 PBXTarget::PBXTarget(const std::string& name,
                      const std::string& shell_script,
-                     const std::string& config_name,
+                     const std::vector<std::string>& configs,
                      const PBXAttributes& attributes)
     : configurations_(
-          std::make_unique<XCConfigurationList>(config_name, attributes, this)),
+          std::make_unique<XCConfigurationList>(configs, attributes, this)),
       name_(name) {
   if (!shell_script.empty()) {
     build_phases_.push_back(
@@ -432,9 +432,9 @@
 
 PBXAggregateTarget::PBXAggregateTarget(const std::string& name,
                                        const std::string& shell_script,
-                                       const std::string& config_name,
+                                       const std::vector<std::string>& configs,
                                        const PBXAttributes& attributes)
-    : PBXTarget(name, shell_script, config_name, attributes) {}
+    : PBXTarget(name, shell_script, configs, attributes) {}
 
 PBXAggregateTarget::~PBXAggregateTarget() = default;
 
@@ -715,12 +715,12 @@
 
 PBXNativeTarget::PBXNativeTarget(const std::string& name,
                                  const std::string& shell_script,
-                                 const std::string& config_name,
+                                 const std::vector<std::string>& configs,
                                  const PBXAttributes& attributes,
                                  const std::string& product_type,
                                  const std::string& product_name,
                                  const PBXFileReference* product_reference)
-    : PBXTarget(name, shell_script, config_name, attributes),
+    : PBXTarget(name, shell_script, configs, attributes),
       product_reference_(product_reference),
       product_type_(product_type),
       product_name_(product_name) {
@@ -773,15 +773,15 @@
 // PBXProject -----------------------------------------------------------------
 
 PBXProject::PBXProject(const std::string& name,
-                       const std::string& config_name,
+                       std::vector<std::string> configs,
                        const std::string& source_path,
                        const PBXAttributes& attributes)
-    : name_(name), config_name_(config_name), target_for_indexing_(nullptr) {
+    : name_(name), configs_(std::move(configs)), target_for_indexing_(nullptr) {
   main_group_ = std::make_unique<PBXMainGroup>(source_path);
   products_ = main_group_->CreateChild<PBXProductsGroup>();
 
   configurations_ =
-      std::make_unique<XCConfigurationList>(config_name, attributes, this);
+      std::make_unique<XCConfigurationList>(configs_, attributes, this);
 }
 
 PBXProject::~PBXProject() = default;
@@ -809,15 +809,16 @@
 }
 
 void PBXProject::AddAggregateTarget(const std::string& name,
+                                    const std::string& output_dir,
                                     const std::string& shell_script) {
   PBXAttributes attributes;
   attributes["CLANG_ENABLE_OBJC_WEAK"] = "YES";
   attributes["CODE_SIGNING_REQUIRED"] = "NO";
-  attributes["CONFIGURATION_BUILD_DIR"] = ".";
+  attributes["CONFIGURATION_BUILD_DIR"] = output_dir;
   attributes["PRODUCT_NAME"] = name;
 
   targets_.push_back(std::make_unique<PBXAggregateTarget>(
-      name, shell_script, config_name_, attributes));
+      name, shell_script, configs_, attributes));
 }
 
 void PBXProject::AddIndexingTarget() {
@@ -835,8 +836,8 @@
 
   const char product_type[] = "com.apple.product-type.tool";
   targets_.push_back(std::make_unique<PBXNativeTarget>(
-      "sources", std::string(), config_name_, attributes, product_type,
-      "sources", product_reference));
+      "sources", std::string(), configs_, attributes, product_type, "sources",
+      product_reference));
   target_for_indexing_ = static_cast<PBXNativeTarget*>(targets_.back().get());
 }
 
@@ -873,7 +874,7 @@
   attributes["EXCLUDED_SOURCE_FILE_NAMES"] = "*.*";
 
   targets_.push_back(std::make_unique<PBXNativeTarget>(
-      name, shell_script, config_name_, attributes, output_type, product_name,
+      name, shell_script, configs_, attributes, output_type, product_name,
       product));
   return static_cast<PBXNativeTarget*>(targets_.back().get());
 }
@@ -1091,13 +1092,16 @@
 
 // XCConfigurationList --------------------------------------------------------
 
-XCConfigurationList::XCConfigurationList(const std::string& name,
-                                         const PBXAttributes& attributes,
-                                         const PBXObject* owner_reference)
+XCConfigurationList::XCConfigurationList(
+    const std::vector<std::string>& configs,
+    const PBXAttributes& attributes,
+    const PBXObject* owner_reference)
     : owner_reference_(owner_reference) {
   DCHECK(owner_reference_);
-  configurations_.push_back(
-      std::make_unique<XCBuildConfiguration>(name, attributes));
+  for (const std::string& config_name : configs) {
+    configurations_.push_back(
+        std::make_unique<XCBuildConfiguration>(config_name, attributes));
+  }
 }
 
 XCConfigurationList::~XCConfigurationList() = default;
@@ -1134,7 +1138,7 @@
   out << indent_str << Reference() << " = {\n";
   PrintProperty(out, rules, "isa", ToString(Class()));
   PrintProperty(out, rules, "buildConfigurations", configurations_);
-  PrintProperty(out, rules, "defaultConfigurationIsVisible", 1u);
+  PrintProperty(out, rules, "defaultConfigurationIsVisible", 0u);
   PrintProperty(out, rules, "defaultConfigurationName",
                 configurations_[0]->Name());
   out << indent_str << "};\n";
diff --git a/src/gn/xcode_object.h b/src/gn/xcode_object.h
index 544c17c..076e993 100644
--- a/src/gn/xcode_object.h
+++ b/src/gn/xcode_object.h
@@ -144,7 +144,7 @@
  public:
   PBXTarget(const std::string& name,
             const std::string& shell_script,
-            const std::string& config_name,
+            const std::vector<std::string>& configs,
             const PBXAttributes& attributes);
   ~PBXTarget() override;
 
@@ -174,7 +174,7 @@
  public:
   PBXAggregateTarget(const std::string& name,
                      const std::string& shell_script,
-                     const std::string& config_name,
+                     const std::vector<std::string>& configs,
                      const PBXAttributes& attributes);
   ~PBXAggregateTarget() override;
 
@@ -340,7 +340,7 @@
  public:
   PBXNativeTarget(const std::string& name,
                   const std::string& shell_script,
-                  const std::string& config_name,
+                  const std::vector<std::string>& configs,
                   const PBXAttributes& attributes,
                   const std::string& product_type,
                   const std::string& product_name,
@@ -369,7 +369,7 @@
 class PBXProject : public PBXObject {
  public:
   PBXProject(const std::string& name,
-             const std::string& config_name,
+             std::vector<std::string> configs,
              const std::string& source_path,
              const PBXAttributes& attributes);
   ~PBXProject() override;
@@ -380,6 +380,7 @@
                      const std::string& source_path,
                      PBXNativeTarget* target);
   void AddAggregateTarget(const std::string& name,
+                          const std::string& output_dir,
                           const std::string& shell_script);
   void AddIndexingTarget();
   PBXNativeTarget* AddNativeTarget(
@@ -411,7 +412,7 @@
   std::string project_root_;
   std::vector<std::unique_ptr<PBXTarget>> targets_;
   std::string name_;
-  std::string config_name_;
+  std::vector<std::string> configs_;
 
   PBXGroup* products_ = nullptr;
   PBXNativeTarget* target_for_indexing_ = nullptr;
@@ -523,7 +524,7 @@
 
 class XCConfigurationList : public PBXObject {
  public:
-  XCConfigurationList(const std::string& name,
+  XCConfigurationList(const std::vector<std::string>& configs,
                       const PBXAttributes& attributes,
                       const PBXObject* owner_reference);
   ~XCConfigurationList() override;
diff --git a/src/gn/xcode_object_unittest.cc b/src/gn/xcode_object_unittest.cc
index 0498c3f..e20a1d0 100644
--- a/src/gn/xcode_object_unittest.cc
+++ b/src/gn/xcode_object_unittest.cc
@@ -38,7 +38,7 @@
 // Instantiate a PBXProject object with arbitrary names.
 std::unique_ptr<PBXProject> GetPBXProjectObject() {
   std::unique_ptr<PBXProject> pbx_project(
-      new PBXProject("project", "config", "out/build", PBXAttributes()));
+      new PBXProject("project", {"config"}, "out/build", PBXAttributes()));
   return pbx_project;
 }
 
@@ -61,7 +61,7 @@
 // Instantiate a PBXAggregateTarget object with arbitrary names.
 std::unique_ptr<PBXAggregateTarget> GetPBXAggregateTargetObject() {
   std::unique_ptr<PBXAggregateTarget> pbx_aggregate_target(
-      new PBXAggregateTarget("target_name", "shell_script", "config_name",
+      new PBXAggregateTarget("target_name", "shell_script", {"config_name"},
                              PBXAttributes()));
   return pbx_aggregate_target;
 }
@@ -70,7 +70,7 @@
 std::unique_ptr<PBXNativeTarget> GetPBXNativeTargetObject(
     const PBXFileReference* product_reference) {
   std::unique_ptr<PBXNativeTarget> pbx_native_target(new PBXNativeTarget(
-      "target_name", "ninja gn_unittests", "config_name", PBXAttributes(),
+      "target_name", "ninja gn_unittests", {"config_name"}, PBXAttributes(),
       "com.apple.product-type.application", "product_name", product_reference));
   return pbx_native_target;
 }
@@ -104,7 +104,7 @@
 std::unique_ptr<XCConfigurationList> GetXCConfigurationListObject(
     const PBXObject* owner_reference) {
   std::unique_ptr<XCConfigurationList> xc_configuration_list(
-      new XCConfigurationList("config_list_name", PBXAttributes(),
+      new XCConfigurationList({"config_list_name"}, PBXAttributes(),
                               owner_reference));
   return xc_configuration_list;
 }
diff --git a/src/gn/xcode_writer.cc b/src/gn/xcode_writer.cc
index 809d6a7..0ec1fa8 100644
--- a/src/gn/xcode_writer.cc
+++ b/src/gn/xcode_writer.cc
@@ -11,6 +11,7 @@
 #include <optional>
 #include <sstream>
 #include <string>
+#include <string_view>
 #include <utility>
 
 #include "base/environment.h"
@@ -18,6 +19,7 @@
 #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"
@@ -77,12 +79,17 @@
   return WRITER_TARGET_OS_MACOS;
 }
 
-std::string GetNinjaExecutable(const std::string& ninja_executable) {
-  return ninja_executable.empty() ? "ninja" : ninja_executable;
-}
-
-std::string ComputeScriptEnviron(base::Environment* environment) {
+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) {
@@ -94,18 +101,15 @@
     }
     buffer << " ";
   }
-  return buffer.str();
-}
 
-std::string GetBuildScript(const std::string& target_name,
-                           const std::string& ninja_executable,
-                           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 " << ComputeScriptEnviron(environment);
-  buffer << GetNinjaExecutable(ninja_executable) << " -C .";
+  if (ninja_executable.empty()) {
+    buffer << "ninja";
+  } else {
+    buffer << ninja_executable;
+  }
+
+  buffer << " -C " << build_dir;
+
   if (!target_name.empty()) {
     buffer << " '" << target_name << "'";
   }
@@ -323,21 +327,17 @@
   project->Visit(visitor);
 }
 
-// Returns a configuration name derived from the build directory. This gives
-// standard names if using the Xcode convention of naming the build directory
-// out/$configuration-$platform (e.g. out/Debug-iphonesimulator).
-std::string ConfigNameFromBuildSettings(const BuildSettings* build_settings) {
-  std::string config_name = FilePathToUTF8(build_settings->build_dir()
-                                               .Resolve(base::FilePath())
-                                               .StripTrailingSeparators()
-                                               .BaseName());
+// 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);
 
-  std::string::size_type separator = config_name.find('-');
-  if (separator != std::string::npos)
-    config_name = config_name.substr(0, separator);
+  if (result.empty())
+    result.push_back(std::string("Release"));
 
-  DCHECK(!config_name.empty());
-  return config_name;
+  return result;
 }
 
 // Returns the path to root_src_dir from settings.
@@ -550,6 +550,10 @@
       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;
 
@@ -566,7 +570,7 @@
     : build_settings_(build_settings),
       options_(options),
       project_(options.project_name,
-               ConfigNameFromBuildSettings(build_settings),
+               ConfigListFromOptions(options.configurations),
                SourcePathFromBuildSettings(build_settings),
                ProjectAttributesFromBuildSettings(build_settings)) {}
 
@@ -662,8 +666,9 @@
   std::unique_ptr<base::Environment> env(base::Environment::Create());
 
   project_.AddAggregateTarget(
-      "All", GetBuildScript(options_.root_target_name,
-                            options_.ninja_executable, env.get()));
+      "All", GetConfigOutputDir("."),
+      GetBuildScript(options_.root_target_name, options_.ninja_executable,
+                     GetConfigOutputDir("."), env.get()));
 
   const std::optional<std::vector<const Target*>> targets =
       GetTargetsFromBuilder(builder, err);
@@ -898,8 +903,9 @@
       target->label().name(), "compiled.mach-o.executable",
       target->output_name().empty() ? target->label().name()
                                     : target->output_name(),
-      "com.apple.product-type.tool", output_dir,
-      GetBuildScript(target->label().name(), options_.ninja_executable, env));
+      "com.apple.product-type.tool", GetConfigOutputDir(output_dir),
+      GetBuildScript(target->label().name(), options_.ninja_executable,
+                     GetConfigOutputDir("."), env));
 }
 
 PBXNativeTarget* XcodeProject::AddBundleTarget(const Target* target,
@@ -923,16 +929,33 @@
   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(), output_dir,
-      GetBuildScript(pbxtarget_name, options_.ninja_executable, env),
+      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"
diff --git a/src/gn/xcode_writer.h b/src/gn/xcode_writer.h
index 96ed14f..7edd674 100644
--- a/src/gn/xcode_writer.h
+++ b/src/gn/xcode_writer.h
@@ -11,6 +11,8 @@
 #include <string>
 #include <vector>
 
+#include "base/files/file_path.h"
+
 class Builder;
 class BuildSettings;
 class Err;
@@ -42,6 +44,18 @@
     // files for those target will still be listed in the generated project).
     std::string dir_filters_string;
 
+    // If specified, should be a semicolon-separated list of configuration
+    // names. It will be used to generate all the configuration variations
+    // in the project. If empty, the project is assumed to only use a single
+    // configuration "Release".
+    std::string configurations;
+
+    // If specified, should be the path for the configuration's build
+    // directory. It can use Xcode variables such as ${CONFIGURATION} or
+    // ${EFFECTIVE_PLATFORM_NAME}. If empty, it is assumed to be the same
+    // as the project directory.
+    base::FilePath configuration_build_dir;
+
     // Control which version of the build system should be used for the
     // generated Xcode project.
     XcodeBuildSystem build_system = XcodeBuildSystem::kLegacy;