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,