Rust compilation tool and toolchain support

Adds the `rustc` tool and support to the toolchain, and also adds
support for that new tool into the testing infrastructure.

Change-Id: I47cae7ce3909ef197a3db81cb2f55ef3ee76055d
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/4883
Commit-Queue: Julie Hockett <juliehockett@google.com>
Reviewed-by: Brett Wilson <brettw@google.com>
diff --git a/build/gen.py b/build/gen.py
index 00752ee..3c7c858 100755
--- a/build/gen.py
+++ b/build/gen.py
@@ -516,6 +516,7 @@
         'tools/gn/qt_creator_writer.cc',
         'tools/gn/runtime_deps.cc',
         'tools/gn/rust_substitution_type.cc',
+        'tools/gn/rust_tool.cc',
         'tools/gn/rust_values.cc',
         'tools/gn/rust_variables.cc',
         'tools/gn/scheduler.cc',
diff --git a/tools/gn/function_toolchain.cc b/tools/gn/function_toolchain.cc
index 3d2a100..e5ee5bc 100644
--- a/tools/gn/function_toolchain.cc
+++ b/tools/gn/function_toolchain.cc
@@ -299,6 +299,9 @@
       "copy_bundle_data": [iOS, macOS] Tool to copy files in a bundle.
       "compile_xcassets": [iOS, macOS] Tool to compile asset catalogs.
 
+    Rust tools:
+      "rustc": Rust compiler and linker
+
 Tool variables
 
     command  [string with substitutions]
@@ -359,6 +362,20 @@
 
         Example: description = "Compiling {{source}}"
 
+    exe_output_extension [string, optional, rust tools only]
+    rlib_output_extension [string, optional, rust tools only]
+    dylib_output_extension [string, optional, rust tools only]
+    cdylib_output_extension [string, optional, rust tools only]
+    proc_macro_output_extension [string, optional, rust tools only]
+        Valid for: Rust tools
+
+        These specify the default tool output for each of the crate types.
+        The default is empty for executables, shared, and static libraries and
+        ".rlib" for rlibs. Note that the Rust compiler complains with an error
+        if external crates do not take the form `lib<name>.rlib` or
+        `lib<name>.<shared_extension>`, where `<shared_extension>` is `.so`,
+        `.dylib`, or `.dll` as appropriate for the platform.
+
     lib_switch  [string, optional, link tools only]
     lib_dir_switch  [string, optional, link tools only]
         Valid for: Linker tools except "alink"
@@ -500,7 +517,8 @@
         added to runtime deps (see "gn help runtime_deps"). By default (if
         runtime_outputs is empty or unspecified), it will be the link_output.
 
-Expansions for tool variables
+)"  // String break to prevent overflowing the 16K max VC string length.
+    R"(Expansions for tool variables
 
   All paths are relative to the root build directory, which is the current
   directory for running all tools. These expansions are available to all tools:
@@ -536,8 +554,7 @@
         Example: "libfoo" for the target named "foo" and an output prefix for
         the linker tool of "lib".
 
-)"  // String break to prevent overflowing the 16K max VC string length.
-    R"(  Compiler tools have the notion of a single input and a single output, along
+  Compiler tools have the notion of a single input and a single output, along
   with a set of compiler-specific flags. The following expansions are
   available:
 
@@ -673,6 +690,39 @@
         assets catalog compiler. Usually based on the target_name of
         the create_bundle target.
 
+  Rust tools have the notion of a single input and a single output, along
+  with a set of compiler-specific flags. The following expansions are
+  available:
+
+    {{crate_name}}
+        Expands to the string representing the crate name of target under
+        compilation.
+
+    {{crate_type}}
+        Expands to the string representing the type of crate for the target
+        under compilation.
+
+    {{externs}}
+        Expands to the list of --extern flags needed to include addition Rust
+        libraries in this target. Includes any specified renamed dependencies.
+
+    {{rustc_output_extension}}
+        Expands to the output extension for this target's crate type.
+
+    {{rustc_output_prefix}}
+        Expands to the prefix for shared and static libraries. This should
+        generally be "lib". Empty for executable targets.
+
+    {{rustdeps}}
+        Expands to the list of -Ldependency=<path> strings needed to compile
+        this target.
+
+    {{rustenv}}
+        Expands to the list of environment variables.
+
+    {{rustflags}}
+        Expands to the list of strings representing Rust compiler flags.
+
 Separate linking and dependencies for shared libraries
 
   Shared libraries are special in that not all changes to them require that
diff --git a/tools/gn/function_toolchain_unittest.cc b/tools/gn/function_toolchain_unittest.cc
index e3aa598..ef301e7 100644
--- a/tools/gn/function_toolchain_unittest.cc
+++ b/tools/gn/function_toolchain_unittest.cc
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 #include "tools/gn/functions.h"
+#include "tools/gn/rust_tool.h"
 #include "tools/gn/scheduler.h"
 #include "tools/gn/test_with_scheduler.h"
 #include "tools/gn/test_with_scope.h"
@@ -58,3 +59,35 @@
     ASSERT_TRUE(err.has_error()) << err.message();
   }
 }
+
+TEST_F(FunctionToolchain, Rust) {
+  TestWithScope setup;
+
+  // These runtime outputs are a subset of the outputs so are OK.
+  {
+    TestParseInput input(
+        R"(toolchain("rust") {
+          tool("rustc") {
+            command = "{{rustenv}} rustc --crate-name {{crate_name}} --crate-type bin {{rustflags}} -o {{output}} {{externs}} {{source}}"
+            description = "RUST {{output}}"
+          }
+        })");
+    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);
+
+    const Tool* rust = toolchain->GetTool(RustTool::kRsToolRustc);
+    ASSERT_TRUE(rust);
+    ASSERT_EQ(rust->command().AsString(),
+              "{{rustenv}} rustc --crate-name {{crate_name}} --crate-type bin "
+              "{{rustflags}} -o {{output}} {{externs}} {{source}}");
+    ASSERT_EQ(rust->description().AsString(), "RUST {{output}}");
+  }
+}
\ No newline at end of file
diff --git a/tools/gn/rust_tool.cc b/tools/gn/rust_tool.cc
new file mode 100644
index 0000000..e4e127d
--- /dev/null
+++ b/tools/gn/rust_tool.cc
@@ -0,0 +1,156 @@
+// Copyright 2019 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 "tools/gn/rust_tool.h"
+
+#include "tools/gn/rust_substitution_type.h"
+#include "tools/gn/target.h"
+
+const char* RustTool::kRsToolRustc = "rustc";
+
+RustTool::RustTool(const char* n) : Tool(n), rlib_output_extension_(".rlib") {
+  CHECK(ValidateName(n));
+}
+
+RustTool::~RustTool() = default;
+
+RustTool* RustTool::AsRust() {
+  return this;
+}
+const RustTool* RustTool::AsRust() const {
+  return this;
+}
+
+bool RustTool::ValidateName(const char* name) const {
+  return name_ == kRsToolRustc;
+}
+
+void RustTool::SetComplete() {
+  SetToolComplete();
+}
+
+bool RustTool::SetOutputExtension(const Value* value,
+                                  std::string* var,
+                                  Err* err) {
+  DCHECK(!complete_);
+  if (!value)
+    return true;  // Not present is fine.
+  if (!value->VerifyTypeIs(Value::STRING, err))
+    return false;
+  if (value->string_value().empty())
+    return true;
+
+  *var = std::move(value->string_value());
+  return true;
+}
+
+bool RustTool::ReadOutputExtensions(Scope* scope, Err* err) {
+  if (!SetOutputExtension(scope->GetValue("exe_output_extension", true),
+                          &exe_output_extension_, err))
+    return false;
+  if (!SetOutputExtension(scope->GetValue("rlib_output_extension", true),
+                          &rlib_output_extension_, err))
+    return false;
+  if (!SetOutputExtension(scope->GetValue("dylib_output_extension", true),
+                          &dylib_output_extension_, err))
+    return false;
+  if (!SetOutputExtension(scope->GetValue("cdylib_output_extension", true),
+                          &cdylib_output_extension_, err))
+    return false;
+  if (!SetOutputExtension(scope->GetValue("staticlib_output_extension", true),
+                          &staticlib_output_extension_, err))
+    return false;
+  if (!SetOutputExtension(scope->GetValue("proc_macro_output_extension", true),
+                          &proc_macro_output_extension_, err))
+    return false;
+  return true;
+}
+
+bool RustTool::ReadOutputsPatternList(Scope* scope,
+                                      const char* var,
+                                      SubstitutionList* field,
+                                      Err* err) {
+  DCHECK(!complete_);
+  const Value* value = scope->GetValue(var, true);
+  if (!value)
+    return true;  // Not present is fine.
+  if (!value->VerifyTypeIs(Value::LIST, err))
+    return false;
+
+  SubstitutionList list;
+  if (!list.Parse(*value, err))
+    return false;
+
+  // Validate the right kinds of patterns are used.
+  if (list.list().empty()) {
+    *err = Err(defined_from(), "\"outputs\" must be specified for this tool.");
+    return false;
+  }
+
+  for (const auto& cur_type : list.required_types()) {
+    if (!IsValidRustSubstitution(cur_type)) {
+      *err = Err(*value, "Pattern not valid here.",
+                 "You used the pattern " + std::string(cur_type->name) +
+                     " which is not valid\nfor this variable.");
+      return false;
+    }
+  }
+
+  *field = std::move(list);
+  return true;
+}
+
+bool RustTool::InitTool(Scope* scope, Toolchain* toolchain, Err* err) {
+  // Initialize default vars.
+  if (!Tool::InitTool(scope, toolchain, err)) {
+    return false;
+  }
+
+  if (!ReadOutputExtensions(scope, err)) {
+    return false;
+  }
+
+  // All Rust tools should have outputs.
+  if (!ReadOutputsPatternList(scope, "outputs", &outputs_, err)) {
+    return false;
+  }
+
+  return true;
+}
+
+bool RustTool::ValidateSubstitution(const Substitution* sub_type) const {
+  if (name_ == kRsToolRustc)
+    return IsValidRustSubstitution(sub_type);
+  NOTREACHED();
+  return false;
+}
+
+const std::string& RustTool::rustc_output_extension(
+    Target::OutputType type,
+    const RustValues::CrateType crate_type) const {
+  switch (crate_type) {
+    case RustValues::CRATE_AUTO: {
+      switch (type) {
+        case Target::EXECUTABLE:
+          return exe_output_extension_;
+        case Target::STATIC_LIBRARY:
+          return staticlib_output_extension_;
+        case Target::RUST_LIBRARY:
+          return rlib_output_extension_;
+        default:
+          NOTREACHED();
+          return exe_output_extension_;
+      }
+    }
+    case RustValues::CRATE_DYLIB:
+      return dylib_output_extension_;
+    case RustValues::CRATE_CDYLIB:
+      return cdylib_output_extension_;
+    case RustValues::CRATE_PROC_MACRO:
+      return proc_macro_output_extension_;
+    default:
+      NOTREACHED();
+      return exe_output_extension_;
+  }
+}
diff --git a/tools/gn/rust_tool.h b/tools/gn/rust_tool.h
new file mode 100644
index 0000000..682f74a
--- /dev/null
+++ b/tools/gn/rust_tool.h
@@ -0,0 +1,98 @@
+// Copyright 2019 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.
+
+#ifndef TOOLS_GN_RUST_TOOL_H_
+#define TOOLS_GN_RUST_TOOL_H_
+
+#include <string>
+
+#include "base/logging.h"
+#include "base/macros.h"
+#include "tools/gn/label.h"
+#include "tools/gn/label_ptr.h"
+#include "tools/gn/rust_values.h"
+#include "tools/gn/source_file.h"
+#include "tools/gn/substitution_list.h"
+#include "tools/gn/substitution_pattern.h"
+#include "tools/gn/target.h"
+#include "tools/gn/tool.h"
+
+class RustTool : public Tool {
+ public:
+  // Rust tools
+  static const char* kRsToolRustc;
+
+  explicit RustTool(const char* n);
+  ~RustTool();
+
+  // Manual RTTI and required functions ---------------------------------------
+
+  bool InitTool(Scope* block_scope, Toolchain* toolchain, Err* err);
+  bool ValidateName(const char* name) const override;
+  void SetComplete() override;
+  bool ValidateSubstitution(const Substitution* sub_type) const override;
+
+  RustTool* AsRust() override;
+  const RustTool* AsRust() const override;
+
+  void set_exe_output_extension(std::string ext) {
+    DCHECK(!complete_);
+    DCHECK(ext.empty() || ext[0] == '.');
+    exe_output_extension_ = std::move(ext);
+  }
+
+  void set_rlib_output_extension(std::string ext) {
+    DCHECK(!complete_);
+    DCHECK(ext.empty() || ext[0] == '.');
+    rlib_output_extension_ = std::move(ext);
+  }
+
+  void set_dylib_output_extension(std::string ext) {
+    DCHECK(!complete_);
+    DCHECK(ext.empty() || ext[0] == '.');
+    dylib_output_extension_ = std::move(ext);
+  }
+
+  void set_cdylib_output_extension(std::string ext) {
+    DCHECK(!complete_);
+    DCHECK(ext.empty() || ext[0] == '.');
+    cdylib_output_extension_ = std::move(ext);
+  }
+
+  void set_staticlib_output_extension(std::string ext) {
+    DCHECK(!complete_);
+    DCHECK(ext.empty() || ext[0] == '.');
+    staticlib_output_extension_ = std::move(ext);
+  }
+
+  void set_proc_macro_output_extension(std::string ext) {
+    DCHECK(!complete_);
+    DCHECK(ext.empty() || ext[0] == '.');
+    proc_macro_output_extension_ = std::move(ext);
+  }
+
+  // Will include a leading "." if nonempty.
+  const std::string& rustc_output_extension(
+      Target::OutputType type,
+      const RustValues::CrateType crate_type) const;
+
+ private:
+  bool SetOutputExtension(const Value* value, std::string* var, Err* err);
+  bool ReadOutputExtensions(Scope* scope, Err* err);
+  bool ReadOutputsPatternList(Scope* scope,
+                              const char* var,
+                              SubstitutionList* field,
+                              Err* err);
+
+  std::string exe_output_extension_;
+  std::string rlib_output_extension_;
+  std::string dylib_output_extension_;
+  std::string cdylib_output_extension_;
+  std::string staticlib_output_extension_;
+  std::string proc_macro_output_extension_;
+
+  DISALLOW_COPY_AND_ASSIGN(RustTool);
+};
+
+#endif  // TOOLS_GN_RUST_TOOL_H_
diff --git a/tools/gn/test_with_scope.cc b/tools/gn/test_with_scope.cc
index 9dfb6c4..4ebbbfa 100644
--- a/tools/gn/test_with_scope.cc
+++ b/tools/gn/test_with_scope.cc
@@ -194,6 +194,24 @@
   SetCommandForTool("touch {{output}}", compile_xcassets_tool.get());
   toolchain->SetTool(std::move(compile_xcassets_tool));
 
+  // RUST
+  std::unique_ptr<Tool> rustc_tool = Tool::CreateTool(RustTool::kRsToolRustc);
+  SetCommandForTool(
+      "{{rustenv}} rustc --edition=2018 --crate-name {{crate_name}} {{source}} "
+      "--crate-type {{crate_type}} {{rustflags}} -o "
+      "{{target_out_dir}}/"
+      "{{rustc_output_prefix}}{{crate_name}}{{rustc_output_extension}} "
+      "{{rustdeps}} {{externs}}",
+      rustc_tool.get());
+  rustc_tool->AsRust()->set_dylib_output_extension(".so");
+  rustc_tool->AsRust()->set_cdylib_output_extension(".so");
+  rustc_tool->AsRust()->set_staticlib_output_extension(".a");
+  rustc_tool->AsRust()->set_proc_macro_output_extension(".so");
+  rustc_tool->AsRust()->set_outputs(SubstitutionList::MakeForTest(
+      "{{target_out_dir}}/"
+      "{{rustc_output_prefix}}{{crate_name}}{{rustc_output_extension}}"));
+  toolchain->SetTool(std::move(rustc_tool));
+
   toolchain->ToolchainSetupComplete();
 }
 
diff --git a/tools/gn/test_with_scope.h b/tools/gn/test_with_scope.h
index 7950259..897ef1b 100644
--- a/tools/gn/test_with_scope.h
+++ b/tools/gn/test_with_scope.h
@@ -15,6 +15,7 @@
 #include "tools/gn/general_tool.h"
 #include "tools/gn/input_file.h"
 #include "tools/gn/parse_tree.h"
+#include "tools/gn/rust_tool.h"
 #include "tools/gn/scope.h"
 #include "tools/gn/scope_per_file_provider.h"
 #include "tools/gn/settings.h"
diff --git a/tools/gn/tool.cc b/tools/gn/tool.cc
index b040ac2..72bce14 100644
--- a/tools/gn/tool.cc
+++ b/tools/gn/tool.cc
@@ -3,8 +3,10 @@
 // found in the LICENSE file.
 
 #include "tools/gn/tool.h"
+
 #include "tools/gn/c_tool.h"
 #include "tools/gn/general_tool.h"
+#include "tools/gn/rust_tool.h"
 #include "tools/gn/target.h"
 
 const char* Tool::kToolNone = "";
@@ -42,6 +44,13 @@
   return nullptr;
 }
 
+RustTool* Tool::AsRust() {
+  return nullptr;
+}
+const RustTool* Tool::AsRust() const {
+  return nullptr;
+}
+
 bool Tool::IsPatternInOutputList(const SubstitutionList& output_list,
                                  const SubstitutionPattern& pattern) const {
   for (const auto& cur : output_list.list()) {
@@ -216,6 +225,11 @@
       return tool;
     return nullptr;
   }
+  if (RustTool* rust_tool = tool->AsRust()) {
+    if (rust_tool->InitTool(scope, toolchain, err))
+      return tool;
+    return nullptr;
+  }
   NOTREACHED();
   *err = Err(function, "Unknown tool type.");
   return nullptr;
@@ -223,6 +237,7 @@
 
 // static
 std::unique_ptr<Tool> Tool::CreateTool(const std::string& name) {
+  // C tools
   if (name == CTool::kCToolCc)
     return std::make_unique<CTool>(CTool::kCToolCc);
   else if (name == CTool::kCToolCxx)
@@ -244,6 +259,7 @@
   else if (name == CTool::kCToolLink)
     return std::make_unique<CTool>(CTool::kCToolLink);
 
+  // General tools
   else if (name == GeneralTool::kGeneralToolAction)
     return std::make_unique<GeneralTool>(GeneralTool::kGeneralToolAction);
   else if (name == GeneralTool::kGeneralToolStamp)
@@ -257,6 +273,10 @@
     return std::make_unique<GeneralTool>(
         GeneralTool::kGeneralToolCompileXCAssets);
 
+  // Rust tool
+  else if (name == RustTool::kRsToolRustc)
+    return std::make_unique<RustTool>(RustTool::kRsToolRustc);
+
   return nullptr;
 }
 
@@ -276,12 +296,13 @@
       return CTool::kCToolAsm;
     case SourceFile::SOURCE_RC:
       return CTool::kCToolRc;
+    case SourceFile::SOURCE_RS:
+      return RustTool::kRsToolRustc;
     case SourceFile::SOURCE_UNKNOWN:
     case SourceFile::SOURCE_H:
     case SourceFile::SOURCE_O:
     case SourceFile::SOURCE_DEF:
     case SourceFile::SOURCE_GO:
-    case SourceFile::SOURCE_RS:
       return kToolNone;
     default:
       NOTREACHED();
@@ -294,6 +315,8 @@
   // The contents of this list might be suprising (i.e. stamp tool for copy
   // rules). See the header for why.
   // TODO(crbug.com/gn/39): Don't emit stamp files for single-output targets.
+  if (target->source_types_used().RustSourceUsed())
+    return RustTool::kRsToolRustc;
   switch (target->output_type()) {
     case Target::GROUP:
       return GeneralTool::kGeneralToolStamp;
diff --git a/tools/gn/tool.h b/tools/gn/tool.h
index a6f177d..f6bf515 100644
--- a/tools/gn/tool.h
+++ b/tools/gn/tool.h
@@ -23,6 +23,7 @@
 
 class CTool;
 class GeneralTool;
+class RustTool;
 
 // To add a new Tool category, create a subclass implementing SetComplete()
 // Add a new category to ToolCategories
@@ -60,6 +61,8 @@
   virtual const CTool* AsC() const;
   virtual GeneralTool* AsGeneral();
   virtual const GeneralTool* AsGeneral() const;
+  virtual RustTool* AsRust();
+  virtual const RustTool* AsRust() const;
 
   // Basic information ---------------------------------------------------------
 
diff --git a/tools/gn/toolchain.cc b/tools/gn/toolchain.cc
index 0d6cbaf..906068d 100644
--- a/tools/gn/toolchain.cc
+++ b/tools/gn/toolchain.cc
@@ -73,6 +73,20 @@
   return nullptr;
 }
 
+RustTool* Toolchain::GetToolAsRust(const char* name) {
+  if (Tool* tool = GetTool(name)) {
+    return tool->AsRust();
+  }
+  return nullptr;
+}
+
+const RustTool* Toolchain::GetToolAsRust(const char* name) const {
+  if (const Tool* tool = GetTool(name)) {
+    return tool->AsRust();
+  }
+  return nullptr;
+}
+
 void Toolchain::SetTool(std::unique_ptr<Tool> t) {
   DCHECK(t->name() != Tool::kToolNone);
   DCHECK(tools_.find(t->name()) == tools_.end());
@@ -101,6 +115,11 @@
   return GetToolAsGeneral(Tool::GetToolTypeForSourceType(type));
 }
 
+const RustTool* Toolchain::GetToolForSourceTypeAsRust(
+    SourceFile::Type type) const {
+  return GetToolAsRust(Tool::GetToolTypeForSourceType(type));
+}
+
 const Tool* Toolchain::GetToolForTargetFinalOutput(const Target* target) const {
   return GetTool(Tool::GetToolTypeForTargetFinalOutput(target));
 }
@@ -114,3 +133,8 @@
     const Target* target) const {
   return GetToolAsGeneral(Tool::GetToolTypeForTargetFinalOutput(target));
 }
+
+const RustTool* Toolchain::GetToolForTargetFinalOutputAsRust(
+    const Target* target) const {
+  return GetToolAsRust(Tool::GetToolTypeForTargetFinalOutput(target));
+}
diff --git a/tools/gn/toolchain.h b/tools/gn/toolchain.h
index cc93de5..afab7f3 100644
--- a/tools/gn/toolchain.h
+++ b/tools/gn/toolchain.h
@@ -60,6 +60,8 @@
   const GeneralTool* GetToolAsGeneral(const char* name) const;
   CTool* GetToolAsC(const char* name);
   const CTool* GetToolAsC(const char* name) const;
+  RustTool* GetToolAsRust(const char* name);
+  const RustTool* GetToolAsRust(const char* name) const;
 
   // Set a tool. When all tools are configured, you should call
   // ToolchainSetupComplete().
@@ -90,6 +92,7 @@
   const Tool* GetToolForSourceType(SourceFile::Type type) const;
   const CTool* GetToolForSourceTypeAsC(SourceFile::Type type) const;
   const GeneralTool* GetToolForSourceTypeAsGeneral(SourceFile::Type type) const;
+  const RustTool* GetToolForSourceTypeAsRust(SourceFile::Type type) const;
 
   // Returns the tool that produces the final output for the given target type.
   // This isn't necessarily the tool you would expect. For copy target, this
@@ -99,6 +102,7 @@
   const CTool* GetToolForTargetFinalOutputAsC(const Target* target) const;
   const GeneralTool* GetToolForTargetFinalOutputAsGeneral(
       const Target* target) const;
+  const RustTool* GetToolForTargetFinalOutputAsRust(const Target* target) const;
 
   const SubstitutionBits& substitution_bits() const {
     DCHECK(setup_complete_);