rust-project.json support

This is an experimental format for describing the Rust build graph.
It is currently used by rust-analyzer, the offical Rust LSP server.

Change-Id: I63724a3f349c1494f9d0b412a697a42b5e3cddb2
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/8040
Commit-Queue: Scott Graham <scottmg@chromium.org>
Reviewed-by: Petr Hosek <phosek@google.com>
Reviewed-by: Brett Wilson <brettw@chromium.org>
diff --git a/build/gen.py b/build/gen.py
index 95d9b44..44d32c8 100755
--- a/build/gen.py
+++ b/build/gen.py
@@ -471,6 +471,7 @@
         'src/gn/command_refs.cc',
         'src/gn/commands.cc',
         'src/gn/compile_commands_writer.cc',
+        'src/gn/rust_project_writer.cc',
         'src/gn/config.cc',
         'src/gn/config_values.cc',
         'src/gn/config_values_extractors.cc',
@@ -628,6 +629,7 @@
         'src/gn/inherited_libraries_unittest.cc',
         'src/gn/input_conversion_unittest.cc',
         'src/gn/json_project_writer_unittest.cc',
+        'src/gn/rust_project_writer_unittest.cc',
         'src/gn/label_pattern_unittest.cc',
         'src/gn/label_unittest.cc',
         'src/gn/loader_unittest.cc',
diff --git a/src/gn/command_gen.cc b/src/gn/command_gen.cc
index 8bbe60d..7c59820 100644
--- a/src/gn/command_gen.cc
+++ b/src/gn/command_gen.cc
@@ -17,6 +17,7 @@
 #include "gn/ninja_writer.h"
 #include "gn/qt_creator_writer.h"
 #include "gn/runtime_deps.h"
+#include "gn/rust_project_writer.h"
 #include "gn/scheduler.h"
 #include "gn/setup.h"
 #include "gn/standard_out.h"
@@ -52,6 +53,7 @@
 const char kSwitchJsonIdeScript[] = "json-ide-script";
 const char kSwitchJsonIdeScriptArgs[] = "json-ide-script-args";
 const char kSwitchExportCompileCommands[] = "export-compile-commands";
+const char kSwitchExportRustProject[] = "export-rust-project";
 
 // Extracts extra parameters for XcodeWriter from command-line flags.
 XcodeWriter::Options XcodeWriterOptionsFromCommandLine(
@@ -294,6 +296,25 @@
   return false;
 }
 
+bool RunRustProjectWriter(const BuildSettings* build_settings,
+                          const Builder& builder,
+                          Err* err) {
+  const base::CommandLine* command_line =
+      base::CommandLine::ForCurrentProcess();
+  bool quiet = command_line->HasSwitch(switches::kQuiet);
+  base::ElapsedTimer timer;
+
+  std::string file_name = "rust-project.json";
+  bool res = RustProjectWriter::RunAndWriteFiles(build_settings, builder,
+                                                 file_name, quiet, err);
+  if (res && !quiet) {
+    OutputString("Generating rust-project.json took " +
+                 base::Int64ToString(timer.Elapsed().InMilliseconds()) +
+                 "ms\n");
+  }
+  return res;
+}
+
 bool RunCompileCommandsWriter(const BuildSettings* build_settings,
                               const Builder& builder,
                               Err* err) {
@@ -435,6 +456,12 @@
 
 Compilation Database
 
+  --export-rust-project
+      Produces a rust-project.json file in the root of the build directory
+      This is used for various tools in the Rust ecosystem allowing for the
+      replay of individual compilations independent of the build system.
+      This is an unstable format and likely to change without warning.
+
   --export-compile-commands[=<target_name1,target_name2...>]
       Produces a compile_commands.json file in the root of the build directory
       containing an array of “command objects”, where each command object
@@ -526,6 +553,12 @@
     return 1;
   }
 
+  if (command_line->HasSwitch(kSwitchExportRustProject) &&
+      !RunRustProjectWriter(&setup->build_settings(), setup->builder(), &err)) {
+    err.PrintToStdout();
+    return 1;
+  }
+
   TickDelta elapsed_time = timer.Elapsed();
 
   if (!command_line->HasSwitch(switches::kQuiet)) {
diff --git a/src/gn/rust_project_writer.cc b/src/gn/rust_project_writer.cc
new file mode 100644
index 0000000..a3a1dd4
--- /dev/null
+++ b/src/gn/rust_project_writer.cc
@@ -0,0 +1,343 @@
+// Copyright 2020 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/rust_project_writer.h"
+
+#include <fstream>
+#include <sstream>
+#include <tuple>
+
+#include "base/json/string_escape.h"
+#include "gn/builder.h"
+#include "gn/filesystem_utils.h"
+#include "gn/ninja_target_command_util.h"
+#include "gn/rust_tool.h"
+#include "gn/source_file.h"
+#include "gn/tool.h"
+
+#if defined(OS_WINDOWS)
+#define NEWLINE "\r\n"
+#else
+#define NEWLINE "\n"
+#endif
+
+// Current structure of rust-project.json output file
+//
+// {
+//    "roots": [] // always empty for GN. To be deprecated.
+//    "crates": [
+//        {
+//            "atom_cfgs": [], // atom config options
+//            "deps": [
+//                {
+//                    "crate": 1, // index into crate array
+//                    "name": "alloc" // extern name of dependency
+//                },
+//            ],
+//            "edition": "2018", // edition of crate
+//            "key_value_cfgs": {
+//              "rust_panic": "abort" // key value config options
+//            },
+//            "root_module": "absolute path to crate"
+//        },
+// }
+//
+
+bool RustProjectWriter::RunAndWriteFiles(const BuildSettings* build_settings,
+                                         const Builder& builder,
+                                         const std::string& file_name,
+                                         bool quiet,
+                                         Err* err) {
+  SourceFile output_file = build_settings->build_dir().ResolveRelativeFile(
+      Value(nullptr, file_name), err);
+  if (output_file.is_null())
+    return false;
+
+  base::FilePath output_path = build_settings->GetFullPath(output_file);
+
+  std::vector<const Target*> all_targets = builder.GetAllResolvedTargets();
+
+  std::ofstream json;
+  json.open(FilePathToUTF8(output_path).c_str(),
+            std::ios_base::out | std::ios_base::binary);
+  if (json.fail())
+    return false;
+
+  RenderJSON(build_settings, all_targets, json);
+
+  return true;
+}
+
+using TargetIdxMap = std::unordered_map<const Target*, uint32_t>;
+using SysrootIdxMap =
+    std::unordered_map<std::string_view,
+                       std::unordered_map<std::string_view, uint32_t>>;
+
+void WriteDeps(const Target* target,
+               TargetIdxMap& lookup,
+               SysrootIdxMap& sysroot_lookup,
+               std::ostream& rust_project) {
+  bool first = true;
+
+  rust_project << "      \"deps\": [";
+
+  // Check if this target has had it's sysroot setup yet
+  auto rust_tool =
+      target->toolchain()->GetToolForSourceTypeAsRust(SourceFile::SOURCE_RS);
+  auto current_sysroot = rust_tool->GetSysroot();
+  if (current_sysroot != "") {
+    // TODO(bwb) If this library doesn't depend on std, use core instead
+    auto std_idx = sysroot_lookup[current_sysroot].find("std");
+    if (std_idx != sysroot_lookup[current_sysroot].end()) {
+      if (!first)
+        rust_project << ",";
+      rust_project << NEWLINE << "        {" NEWLINE
+                   << "          \"crate\": " << std::to_string(std_idx->second)
+                   << "," NEWLINE << "          \"name\": \"std\"" NEWLINE
+                   << "        }";
+      first = false;
+    }
+  }
+
+  for (const auto& dep : target->rust_values().transitive_libs().GetOrdered()) {
+    auto idx = lookup[dep];
+    if (!first)
+      rust_project << ",";
+    rust_project << NEWLINE << "        {" NEWLINE
+                 << "          \"crate\": " << std::to_string(idx)
+                 << "," NEWLINE << "          \"name\": \""
+                 << dep->rust_values().crate_name() << "\"" NEWLINE
+                 << "        }";
+    first = false;
+  }
+  rust_project << NEWLINE "      ]," NEWLINE;
+}
+
+// TODO(bwb) Parse sysroot structure from toml files. This is fragile and might
+// break if upstream changes the dependency structure.
+const std::string_view sysroot_crates[] = {"std",
+                                           "core",
+                                           "alloc",
+                                           "collections",
+                                           "libc",
+                                           "panic_unwind",
+                                           "proc_macro",
+                                           "rustc_unicode",
+                                           "std_unicode",
+                                           "test",
+                                           "alloc_jemalloc",
+                                           "alloc_system",
+                                           "compiler_builtins",
+                                           "getopts",
+                                           "panic_unwind",
+                                           "panic_abort",
+                                           "unwind",
+                                           "build_helper",
+                                           "rustc_asan",
+                                           "rustc_lsan",
+                                           "rustc_msan",
+                                           "rustc_tsan",
+                                           "syntax"};
+
+const std::string_view std_deps[] = {
+    "alloc",
+    "core",
+    "panic_abort",
+    "unwind",
+};
+
+void AddSysrootCrate(const std::string_view crate,
+                     const std::string_view current_sysroot,
+                     uint32_t* count,
+                     SysrootIdxMap& sysroot_lookup,
+                     std::ostream& rust_project,
+                     const BuildSettings* build_settings,
+                     bool first) {
+  if (crate == "std") {
+    for (auto dep : std_deps) {
+      AddSysrootCrate(dep, current_sysroot, count, sysroot_lookup, rust_project,
+                      build_settings, first);
+      first = false;
+    }
+  }
+
+  if (!first)
+    rust_project << "," NEWLINE;
+  sysroot_lookup[current_sysroot].insert(std::make_pair(crate, *count));
+
+  base::FilePath rebased_out_dir =
+      build_settings->GetFullPath(build_settings->build_dir());
+  auto crate_path =
+      FilePathToUTF8(rebased_out_dir) + std::string(current_sysroot) +
+      "/lib/rustlib/src/rust/src/lib" + std::string(crate) + "/lib.rs";
+  base::FilePath crate_root = build_settings->GetFullPath(crate_path, false);
+
+  rust_project << "    {" NEWLINE;
+  rust_project << "      \"crate_id\": " << std::to_string(*count)
+               << "," NEWLINE;
+  rust_project << "      \"root_module\": \"" << FilePathToUTF8(crate_root)
+               << "\"," NEWLINE;
+  rust_project << "      \"edition\": \"2018\"," NEWLINE;
+  rust_project << "      \"deps\": [";
+  (*count)++;
+  if (crate == "std") {
+    first = true;
+    for (auto dep : std_deps) {
+      auto idx = sysroot_lookup[current_sysroot][dep];
+      if (!first) {
+        rust_project << ",";
+      }
+      first = false;
+      rust_project << NEWLINE << "        {" NEWLINE
+                   << "          \"crate\": " << std::to_string(idx)
+                   << "," NEWLINE << "          \"name\": \"" << dep
+                   << "\"" NEWLINE << "        }";
+    }
+  }
+  rust_project << NEWLINE "      ]," NEWLINE;
+
+  rust_project << "      \"atom_cfgs\": []," NEWLINE
+                  "      \"key_value_cfgs\": {}" NEWLINE;
+  rust_project << "    }";
+}
+
+void AddTarget(const Target* target,
+               uint32_t* count,
+               TargetIdxMap& lookup,
+               SysrootIdxMap& sysroot_lookup,
+               const BuildSettings* build_settings,
+               std::ostream& rust_project,
+               bool first) {
+  if (lookup.find(target) != lookup.end()) {
+    // If target is already in the lookup, we don't add it again.
+    return;
+  }
+
+  // Check what sysroot this target needs.
+  auto rust_tool =
+      target->toolchain()->GetToolForSourceTypeAsRust(SourceFile::SOURCE_RS);
+  auto current_sysroot = rust_tool->GetSysroot();
+  if (current_sysroot != "" && sysroot_lookup.count(current_sysroot) == 0) {
+    for (const auto& crate : sysroot_crates) {
+      AddSysrootCrate(crate, current_sysroot, count, sysroot_lookup,
+                      rust_project, build_settings, first);
+      first = false;
+    }
+  }
+
+  // Add each dependency first before we write any of the parent target.
+  for (const auto& dep : target->rust_values().transitive_libs().GetOrdered()) {
+    AddTarget(dep, count, lookup, sysroot_lookup, build_settings, rust_project,
+              first);
+    first = false;
+  }
+
+  if (!first) {
+    rust_project << "," NEWLINE;
+  }
+
+  // Construct the crate info.
+  rust_project << "    {" NEWLINE;
+  rust_project << "      \"crate_id\": " << std::to_string(*count)
+               << "," NEWLINE;
+
+  // Add the target to the crate lookup.
+  lookup.insert(std::make_pair(target, *count));
+  (*count)++;
+
+  base::FilePath crate_root =
+      build_settings->GetFullPath(target->rust_values().crate_root());
+
+  rust_project << "      \"root_module\": \"" << FilePathToUTF8(crate_root)
+               << "\"," NEWLINE;
+
+  WriteDeps(target, lookup, sysroot_lookup, rust_project);
+
+  std::string cfg_prefix("--cfg=");
+  std::string edition_prefix("--edition=");
+  std::vector<std::string> atoms;
+  std::vector<std::tuple<std::string, std::string>> kvs;
+
+  bool edition_set = false;
+  for (ConfigValuesIterator iter(target); !iter.done(); iter.Next()) {
+    for (const auto& flag : iter.cur().rustflags()) {
+      // extract the edition of this target
+      if (!flag.compare(0, edition_prefix.size(), edition_prefix)) {
+        auto edition = flag.substr(edition_prefix.size());
+        rust_project << "      \"edition\": \"" << edition << "\"," NEWLINE;
+        edition_set = true;
+      }
+      // Can't directly print cfgs since they come in any order.
+      // If they have an = they are a k/v cfg, otherwise an atom cfg.
+      if (!flag.compare(0, cfg_prefix.size(), cfg_prefix)) {
+        auto cfg = flag.substr(cfg_prefix.size());
+        auto idx = cfg.rfind("=");
+        if (idx == std::string::npos) {
+          atoms.push_back(cfg);
+        } else {
+          std::string key = cfg.substr(0, idx);
+          std::string value = cfg.substr(idx + 1);
+          kvs.push_back(std::make_pair(key, value));
+        }
+      }
+    }
+  }
+
+  if (!edition_set)
+    rust_project << "      \"edition\": \"2015\"," NEWLINE;
+
+  rust_project << "      \"atom_cfgs\": [";
+  bool first_atom = true;
+  for (const auto& cfg : atoms) {
+    if (!first_atom) {
+      rust_project << ",";
+    }
+    first_atom = false;
+    rust_project << NEWLINE;
+    rust_project << "        \"" << cfg << "\"";
+  }
+  rust_project << NEWLINE;
+  rust_project << "      ]," NEWLINE;
+
+  rust_project << "      \"key_value_cfgs\": {";
+  bool first_kv = true;
+  for (const auto cfg : kvs) {
+    if (!first_kv) {
+      rust_project << ",";
+    }
+    first_kv = false;
+    rust_project << NEWLINE << "        \"" << std::get<0>(cfg)
+                 << "\" : " << std::get<1>(cfg);
+  }
+  rust_project << NEWLINE;
+  rust_project << "      }" NEWLINE;
+  rust_project << "    }";
+}
+
+void RustProjectWriter::RenderJSON(const BuildSettings* build_settings,
+                                   std::vector<const Target*>& all_targets,
+                                   std::ostream& rust_project) {
+  TargetIdxMap lookup;
+  SysrootIdxMap sysroot_lookup;
+  uint32_t count = 0;
+  bool first = true;
+
+  rust_project << "{" NEWLINE;
+
+  rust_project << "  \"roots\": []," NEWLINE;
+  rust_project << "  \"crates\": [" NEWLINE;
+
+  // All the crates defined in the project.
+  for (const auto* target : all_targets) {
+    if (!target->IsBinary() || !target->source_types_used().RustSourceUsed())
+      continue;
+
+    AddTarget(target, &count, lookup, sysroot_lookup, build_settings,
+              rust_project, first);
+    first = false;
+  }
+
+  rust_project << NEWLINE "  ]" NEWLINE;
+  rust_project << "}" NEWLINE;
+}
diff --git a/src/gn/rust_project_writer.h b/src/gn/rust_project_writer.h
new file mode 100644
index 0000000..8140e06
--- /dev/null
+++ b/src/gn/rust_project_writer.h
@@ -0,0 +1,37 @@
+// Copyright 2020 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_PROJECT_WRITER_H_
+#define TOOLS_GN_RUST_PROJECT_WRITER_H_
+
+#include "gn/err.h"
+#include "gn/target.h"
+
+class Builder;
+class BuildSettings;
+
+// rust-project.json is an output format describing the rust build graph. It is
+// used by rust-analyzer (a LSP server), similiar to compile-commands.json.
+//
+// an example output is in rust_project_writer.cc
+class RustProjectWriter {
+ public:
+  // Write Rust build graph into a json file located by parameter file_name.
+  //
+  // Parameter quiet is not used.
+  static bool RunAndWriteFiles(const BuildSettings* build_setting,
+                               const Builder& builder,
+                               const std::string& file_name,
+                               bool quiet,
+                               Err* err);
+  static void RenderJSON(const BuildSettings* build_settings,
+                         std::vector<const Target*>& all_targets,
+                         std::ostream& rust_project);
+
+ private:
+  // This fuction visits the deps graph of a target in a DFS fashion.
+  static void VisitDeps(const Target* target, std::set<const Target*>* visited);
+};
+
+#endif  // TOOLS_GN_RUST_PROJECT_WRITER_H_
diff --git a/src/gn/rust_project_writer_unittest.cc b/src/gn/rust_project_writer_unittest.cc
new file mode 100644
index 0000000..1d8d153
--- /dev/null
+++ b/src/gn/rust_project_writer_unittest.cc
@@ -0,0 +1,223 @@
+// Copyright 2020 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/rust_project_writer.h"
+#include "base/strings/string_util.h"
+#include "gn/substitution_list.h"
+#include "gn/target.h"
+#include "gn/test_with_scheduler.h"
+#include "gn/test_with_scope.h"
+#include "util/build_config.h"
+#include "util/test/test.h"
+
+using RustProjectJSONWriter = TestWithScheduler;
+
+TEST_F(RustProjectJSONWriter, OneRustTarget) {
+  Err err;
+  TestWithScope setup;
+
+  Target target(setup.settings(), Label(SourceDir("//foo/"), "bar"));
+  target.set_output_type(Target::RUST_LIBRARY);
+  target.visibility().SetPublic();
+  SourceFile lib("//foo/lib.rs");
+  target.sources().push_back(lib);
+  target.source_types_used().Set(SourceFile::SOURCE_RS);
+  target.rust_values().set_crate_root(lib);
+  target.rust_values().crate_name() = "foo";
+  target.SetToolchain(setup.toolchain());
+  ASSERT_TRUE(target.OnResolved(&err));
+
+  std::ostringstream stream;
+  std::vector<const Target*> targets;
+  targets.push_back(&target);
+  RustProjectWriter::RenderJSON(setup.build_settings(), targets, stream);
+  std::string out = stream.str();
+#if defined(OS_WIN)
+  base::ReplaceSubstringsAfterOffset(&out, 0, "\r\n", "\n");
+#endif
+  const char expected_json[] =
+      "{\n"
+      "  \"roots\": [],\n"
+      "  \"crates\": [\n"
+      "    {\n"
+      "      \"crate_id\": 0,\n"
+      "      \"root_module\": \"foo/lib.rs\",\n"
+      "      \"deps\": [\n"
+      "      ],\n"
+      "      \"edition\": \"2015\",\n"
+      "      \"atom_cfgs\": [\n"
+      "      ],\n"
+      "      \"key_value_cfgs\": {\n"
+      "      }\n"
+      "    }\n"
+      "  ]\n"
+      "}\n";
+
+  EXPECT_EQ(expected_json, out);
+}
+
+TEST_F(RustProjectJSONWriter, RustTargetDep) {
+  Err err;
+  TestWithScope setup;
+
+  Target dep(setup.settings(), Label(SourceDir("//tortoise/"), "bar"));
+  dep.set_output_type(Target::RUST_LIBRARY);
+  dep.visibility().SetPublic();
+  SourceFile tlib("//tortoise/lib.rs");
+  dep.sources().push_back(tlib);
+  dep.source_types_used().Set(SourceFile::SOURCE_RS);
+  dep.rust_values().set_crate_root(tlib);
+  dep.rust_values().crate_name() = "tortoise";
+  dep.SetToolchain(setup.toolchain());
+
+  Target target(setup.settings(), Label(SourceDir("//hare/"), "bar"));
+  target.set_output_type(Target::RUST_LIBRARY);
+  target.visibility().SetPublic();
+  SourceFile harelib("//hare/lib.rs");
+  target.sources().push_back(harelib);
+  target.source_types_used().Set(SourceFile::SOURCE_RS);
+  target.rust_values().set_crate_root(harelib);
+  target.rust_values().crate_name() = "hare";
+  target.public_deps().push_back(LabelTargetPair(&dep));
+  target.SetToolchain(setup.toolchain());
+  ASSERT_TRUE(target.OnResolved(&err));
+
+  std::ostringstream stream;
+  std::vector<const Target*> targets;
+  targets.push_back(&target);
+  RustProjectWriter::RenderJSON(setup.build_settings(), targets, stream);
+  std::string out = stream.str();
+#if defined(OS_WIN)
+  base::ReplaceSubstringsAfterOffset(&out, 0, "\r\n", "\n");
+#endif
+  const char expected_json[] =
+      "{\n"
+      "  \"roots\": [],\n"
+      "  \"crates\": [\n"
+      "    {\n"
+      "      \"crate_id\": 0,\n"
+      "      \"root_module\": \"tortoise/lib.rs\",\n"
+      "      \"deps\": [\n"
+      "      ],\n"
+      "      \"edition\": \"2015\",\n"
+      "      \"atom_cfgs\": [\n"
+      "      ],\n"
+      "      \"key_value_cfgs\": {\n"
+      "      }\n"
+      "    },\n"
+      "    {\n"
+      "      \"crate_id\": 1,\n"
+      "      \"root_module\": \"hare/lib.rs\",\n"
+      "      \"deps\": [\n"
+      "        {\n"
+      "          \"crate\": 0,\n"
+      "          \"name\": \"tortoise\"\n"
+      "        }\n"
+      "      ],\n"
+      "      \"edition\": \"2015\",\n"
+      "      \"atom_cfgs\": [\n"
+      "      ],\n"
+      "      \"key_value_cfgs\": {\n"
+      "      }\n"
+      "    }\n"
+      "  ]\n"
+      "}\n";
+
+  EXPECT_EQ(expected_json, out);
+}
+
+TEST_F(RustProjectJSONWriter, RustTargetDepTwo) {
+  Err err;
+  TestWithScope setup;
+
+  Target dep(setup.settings(), Label(SourceDir("//tortoise/"), "bar"));
+  dep.set_output_type(Target::RUST_LIBRARY);
+  dep.visibility().SetPublic();
+  SourceFile tlib("//tortoise/lib.rs");
+  dep.sources().push_back(tlib);
+  dep.source_types_used().Set(SourceFile::SOURCE_RS);
+  dep.rust_values().set_crate_root(tlib);
+  dep.rust_values().crate_name() = "tortoise";
+  dep.SetToolchain(setup.toolchain());
+
+  Target dep2(setup.settings(), Label(SourceDir("//achilles/"), "bar"));
+  dep2.set_output_type(Target::RUST_LIBRARY);
+  dep2.visibility().SetPublic();
+  SourceFile alib("//achilles/lib.rs");
+  dep2.sources().push_back(alib);
+  dep2.source_types_used().Set(SourceFile::SOURCE_RS);
+  dep2.rust_values().set_crate_root(alib);
+  dep2.rust_values().crate_name() = "achilles";
+  dep2.SetToolchain(setup.toolchain());
+
+  Target target(setup.settings(), Label(SourceDir("//hare/"), "bar"));
+  target.set_output_type(Target::RUST_LIBRARY);
+  target.visibility().SetPublic();
+  SourceFile harelib("//hare/lib.rs");
+  target.sources().push_back(harelib);
+  target.source_types_used().Set(SourceFile::SOURCE_RS);
+  target.rust_values().set_crate_root(harelib);
+  target.rust_values().crate_name() = "hare";
+  target.public_deps().push_back(LabelTargetPair(&dep));
+  target.public_deps().push_back(LabelTargetPair(&dep2));
+  target.SetToolchain(setup.toolchain());
+  ASSERT_TRUE(target.OnResolved(&err));
+
+  std::ostringstream stream;
+  std::vector<const Target*> targets;
+  targets.push_back(&target);
+  RustProjectWriter::RenderJSON(setup.build_settings(), targets, stream);
+  std::string out = stream.str();
+#if defined(OS_WIN)
+  base::ReplaceSubstringsAfterOffset(&out, 0, "\r\n", "\n");
+#endif
+  const char expected_json[] =
+      "{\n"
+      "  \"roots\": [],\n"
+      "  \"crates\": [\n"
+      "    {\n"
+      "      \"crate_id\": 0,\n"
+      "      \"root_module\": \"tortoise/lib.rs\",\n"
+      "      \"deps\": [\n"
+      "      ],\n"
+      "      \"edition\": \"2015\",\n"
+      "      \"atom_cfgs\": [\n"
+      "      ],\n"
+      "      \"key_value_cfgs\": {\n"
+      "      }\n"
+      "    },\n"
+      "    {\n"
+      "      \"crate_id\": 1,\n"
+      "      \"root_module\": \"achilles/lib.rs\",\n"
+      "      \"deps\": [\n"
+      "      ],\n"
+      "      \"edition\": \"2015\",\n"
+      "      \"atom_cfgs\": [\n"
+      "      ],\n"
+      "      \"key_value_cfgs\": {\n"
+      "      }\n"
+      "    },\n"
+      "    {\n"
+      "      \"crate_id\": 2,\n"
+      "      \"root_module\": \"hare/lib.rs\",\n"
+      "      \"deps\": [\n"
+      "        {\n"
+      "          \"crate\": 0,\n"
+      "          \"name\": \"tortoise\"\n"
+      "        },\n"
+      "        {\n"
+      "          \"crate\": 1,\n"
+      "          \"name\": \"achilles\"\n"
+      "        }\n"
+      "      ],\n"
+      "      \"edition\": \"2015\",\n"
+      "      \"atom_cfgs\": [\n"
+      "      ],\n"
+      "      \"key_value_cfgs\": {\n"
+      "      }\n"
+      "    }\n"
+      "  ]\n"
+      "}\n";
+  EXPECT_EQ(expected_json, out);
+}
diff --git a/src/gn/rust_tool.cc b/src/gn/rust_tool.cc
index 6fcb697..0b8921f 100644
--- a/src/gn/rust_tool.cc
+++ b/src/gn/rust_tool.cc
@@ -42,6 +42,10 @@
   SetToolComplete();
 }
 
+std::string_view RustTool::GetSysroot() const {
+  return rust_sysroot_;
+}
+
 bool RustTool::SetOutputExtension(const Value* value,
                                   std::string* var,
                                   Err* err) {
@@ -101,6 +105,11 @@
   if (!ReadOutputsPatternList(scope, "outputs", &outputs_, err)) {
     return false;
   }
+
+  // Check for a sysroot. Sets an empty string when not explicitly set.
+  if (!ReadString(scope, "rust_sysroot", &rust_sysroot_, err)) {
+    return false;
+  }
   return true;
 }
 
diff --git a/src/gn/rust_tool.h b/src/gn/rust_tool.h
index 0b3eddb..6a4fdf7 100644
--- a/src/gn/rust_tool.h
+++ b/src/gn/rust_tool.h
@@ -41,7 +41,11 @@
   RustTool* AsRust() override;
   const RustTool* AsRust() const override;
 
+  std::string_view GetSysroot() const;
+
  private:
+  std::string rust_sysroot_;
+
   bool SetOutputExtension(const Value* value, std::string* var, Err* err);
   bool ReadOutputsPatternList(Scope* scope,
                               const char* var,