Add command_launcher field to tools

This is a field to specify the command launcher for a given tool's
command. It appends the launcher to the command in the Ninja rule, but
keeps the two separate inside of GN. This allows the IDE writer tools
(e.g. the compilation command database) to be written without the
launcher prefix.

The presence of the launcher in the command has been breaking a number
of developer workflows in VSCode, CLion, and clangd. This should resolve
all of those workflows.

Change-Id: Idec6f530ddc48da01ac0c79b64032c7cc88856a4
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/5200
Commit-Queue: Julie Hockett <juliehockett@google.com>
Reviewed-by: Petr Hosek <phosek@google.com>
Reviewed-by: Brett Wilson <brettw@google.com>
diff --git a/tools/gn/function_toolchain.cc b/tools/gn/function_toolchain.cc
index e5ee5bc..cb68612 100644
--- a/tools/gn/function_toolchain.cc
+++ b/tools/gn/function_toolchain.cc
@@ -309,6 +309,15 @@
 
         The command to run.
 
+    command_launcher  [string]
+        Valid for: all tools except "action" (optional)
+
+        The prefix with which to launch the command (e.g. the path to a Goma or
+        CCache compiler launcher).
+
+        Note that this prefix will not be included in the compilation database or
+        IDE files generated from the build.
+
     default_output_dir  [string with substitutions]
         Valid for: linker tools
 
diff --git a/tools/gn/function_toolchain_unittest.cc b/tools/gn/function_toolchain_unittest.cc
index ef301e7..baf2e26 100644
--- a/tools/gn/function_toolchain_unittest.cc
+++ b/tools/gn/function_toolchain_unittest.cc
@@ -90,4 +90,30 @@
               "{{rustflags}} -o {{output}} {{externs}} {{source}}");
     ASSERT_EQ(rust->description().AsString(), "RUST {{output}}");
   }
+}
+
+TEST_F(FunctionToolchain, CommandLauncher) {
+  TestWithScope setup;
+
+  TestParseInput input(
+      R"(toolchain("good") {
+        tool("cxx") {
+          command_launcher = "/usr/goma/gomacc"
+        }
+      })");
+  ASSERT_FALSE(input.has_error());
+
+  Err err;
+  input.parsed()->Execute(setup.scope(), &err);
+  ASSERT_FALSE(err.has_error()) << err.message();
+
+  // It should have generated a toolchain.
+  ASSERT_EQ(1u, setup.items().size());
+  const Toolchain* toolchain = setup.items()[0]->AsToolchain();
+  ASSERT_TRUE(toolchain);
+
+  // The toolchain should have a link tool with the two outputs.
+  const Tool* link = toolchain->GetTool(CTool::kCToolCxx);
+  ASSERT_TRUE(link);
+  EXPECT_EQ("/usr/goma/gomacc", link->command_launcher());
 }
\ No newline at end of file
diff --git a/tools/gn/ninja_toolchain_writer.cc b/tools/gn/ninja_toolchain_writer.cc
index faf9d8a..6bfeb46 100644
--- a/tools/gn/ninja_toolchain_writer.cc
+++ b/tools/gn/ninja_toolchain_writer.cc
@@ -83,8 +83,7 @@
   EscapeOptions options;
   options.mode = ESCAPE_NINJA_PREFORMATTED_COMMAND;
 
-  CHECK(!tool->command().empty()) << "Command should not be empty";
-  WriteRulePattern("command", tool->command(), options);
+  WriteCommandRulePattern("command", tool->command_launcher(), tool->command(), options);
 
   WriteRulePattern("description", tool->description(), options);
   WriteRulePattern("rspfile", tool->rspfile(), options);
@@ -123,3 +122,18 @@
   SubstitutionWriter::WriteWithNinjaVariables(pattern, options, out_);
   out_ << std::endl;
 }
+
+void NinjaToolchainWriter::WriteCommandRulePattern(
+    const char* name,
+    const std::string& launcher,
+    const SubstitutionPattern& command,
+    const EscapeOptions& options) {
+  CHECK(!command.empty()) << "Command should not be empty";
+  if (command.empty())
+    return;
+  out_ << kIndent << name << " = " ;
+  if (!launcher.empty())
+    out_ << launcher << " ";
+  SubstitutionWriter::WriteWithNinjaVariables(command, options, out_);
+  out_ << std::endl;
+}
diff --git a/tools/gn/ninja_toolchain_writer.h b/tools/gn/ninja_toolchain_writer.h
index 6db8d3c..b9b9906 100644
--- a/tools/gn/ninja_toolchain_writer.h
+++ b/tools/gn/ninja_toolchain_writer.h
@@ -31,6 +31,7 @@
 
  private:
   FRIEND_TEST_ALL_PREFIXES(NinjaToolchainWriter, WriteToolRule);
+  FRIEND_TEST_ALL_PREFIXES(NinjaToolchainWriter, WriteToolRuleWithLauncher);
 
   NinjaToolchainWriter(const Settings* settings,
                        const Toolchain* toolchain,
@@ -44,6 +45,10 @@
   void WriteRulePattern(const char* name,
                         const SubstitutionPattern& pattern,
                         const EscapeOptions& options);
+  void WriteCommandRulePattern(const char* name,
+                               const std::string& launcher,
+                               const SubstitutionPattern& command,
+                               const EscapeOptions& options);
 
   const Settings* settings_;
   const Toolchain* toolchain_;
diff --git a/tools/gn/ninja_toolchain_writer_unittest.cc b/tools/gn/ninja_toolchain_writer_unittest.cc
index 2edc26a..ce1c533 100644
--- a/tools/gn/ninja_toolchain_writer_unittest.cc
+++ b/tools/gn/ninja_toolchain_writer_unittest.cc
@@ -22,3 +22,18 @@
       "-o ${out}\n",
       stream.str());
 }
+
+TEST(NinjaToolchainWriter, WriteToolRuleWithLauncher) {
+  TestWithScope setup;
+
+  std::ostringstream stream;
+  NinjaToolchainWriter writer(setup.settings(), setup.toolchain(), stream);
+  writer.WriteToolRule(setup.toolchain()->GetTool(CTool::kCToolCxx),
+                       std::string("prefix_"));
+
+  EXPECT_EQ(
+      "rule prefix_cxx\n"
+      "  command = launcher c++ ${in} ${cflags} ${cflags_cc} ${defines} ${include_dirs} "
+      "-o ${out}\n",
+      stream.str());
+}
diff --git a/tools/gn/test_with_scope.cc b/tools/gn/test_with_scope.cc
index 4ebbbfa..80d9692 100644
--- a/tools/gn/test_with_scope.cc
+++ b/tools/gn/test_with_scope.cc
@@ -91,6 +91,7 @@
       cxx_tool.get());
   cxx_tool->set_outputs(SubstitutionList::MakeForTest(
       "{{source_out_dir}}/{{target_output_name}}.{{source_name_part}}.o"));
+  cxx_tool->set_command_launcher("launcher");
   toolchain->SetTool(std::move(cxx_tool));
 
   // OBJC
diff --git a/tools/gn/tool.cc b/tools/gn/tool.cc
index 72bce14..8e06d11 100644
--- a/tools/gn/tool.cc
+++ b/tools/gn/tool.cc
@@ -190,6 +190,7 @@
 
 bool Tool::InitTool(Scope* scope, Toolchain* toolchain, Err* err) {
   if (!ReadPattern(scope, "command", &command_, err) ||
+      !ReadString(scope, "command_launcher", &command_launcher_, err) ||
       !ReadOutputExtension(scope, err) ||
       !ReadPattern(scope, "depfile", &depfile_, err) ||
       !ReadPattern(scope, "description", &description_, err) ||
diff --git a/tools/gn/tool.h b/tools/gn/tool.h
index f6bf515..2f853ef 100644
--- a/tools/gn/tool.h
+++ b/tools/gn/tool.h
@@ -83,6 +83,15 @@
     command_ = std::move(cmd);
   }
 
+  // Launcher for the command (e.g. goma)
+  const std::string& command_launcher() const {
+    return command_launcher_;
+  }
+  void set_command_launcher(std::string l) {
+    DCHECK(!complete_);
+    command_launcher_ = std::move(l);
+  }
+
   // Should include a leading "." if nonempty.
   const std::string& default_output_extension() const {
     return default_output_extension_;
@@ -212,6 +221,7 @@
   const char* name_;
 
   SubstitutionPattern command_;
+  std::string command_launcher_;
   std::string default_output_extension_;
   SubstitutionPattern default_output_dir_;
   SubstitutionPattern depfile_;