Fix rust-analyzer being unable to find rust sysroot sources. (#1483)

* Fix rust-analyzer being unable to find rust sysroot sources.

* Update tools/rust_analyzer/main.rs

Co-authored-by: Daniel Wagner-Hall <dawagner@gmail.com>

Co-authored-by: Daniel Wagner-Hall <dawagner@gmail.com>
diff --git a/rust/private/rust_analyzer.bzl b/rust/private/rust_analyzer.bzl
index a73e3aa..8408197 100644
--- a/rust/private/rust_analyzer.bzl
+++ b/rust/private/rust_analyzer.bzl
@@ -122,7 +122,8 @@
     doc = "Annotates rust rules with RustAnalyzerInfo later used to build a rust-project.json",
 )
 
-_exec_root_tmpl = "__EXEC_ROOT__/"
+_EXEC_ROOT_TEMPLATE = "__EXEC_ROOT__/"
+_OUTPUT_BASE_TEMPLATE = "__OUTPUT_BASE__/"
 
 def _crate_id(crate_info):
     """Returns a unique stable identifier for a crate
@@ -155,18 +156,18 @@
     # TODO: Some folks may want to override this for vendored dependencies.
     is_external = info.crate.root.path.startswith("external/")
     is_generated = not info.crate.root.is_source
-    path_prefix = _exec_root_tmpl if is_external or is_generated else ""
+    path_prefix = _EXEC_ROOT_TEMPLATE if is_external or is_generated else ""
     crate["is_workspace_member"] = not is_external
     crate["root_module"] = path_prefix + info.crate.root.path
     crate_root = path_prefix + info.crate.root.dirname
 
     if info.build_info != None:
         out_dir_path = info.build_info.out_dir.path
-        crate["env"].update({"OUT_DIR": _exec_root_tmpl + out_dir_path})
+        crate["env"].update({"OUT_DIR": _EXEC_ROOT_TEMPLATE + out_dir_path})
         crate["source"] = {
             # We have to tell rust-analyzer about our out_dir since it's not under the crate root.
             "exclude_dirs": [],
-            "include_dirs": [crate_root, _exec_root_tmpl + out_dir_path],
+            "include_dirs": [crate_root, _EXEC_ROOT_TEMPLATE + out_dir_path],
         }
 
     # TODO: The only imagined use case is an env var holding a filename in the workspace passed to a
@@ -188,7 +189,7 @@
     crate["cfg"] = info.cfgs
     crate["target"] = find_toolchain(ctx).target_triple
     if info.proc_macro_dylib_path != None:
-        crate["proc_macro_dylib_path"] = _exec_root_tmpl + info.proc_macro_dylib_path
+        crate["proc_macro_dylib_path"] = _EXEC_ROOT_TEMPLATE + info.proc_macro_dylib_path
     return crate
 
 def _rust_analyzer_toolchain_impl(ctx):
@@ -223,7 +224,7 @@
 
     sysroot_src = rustc_srcs.label.package + "/library"
     if rustc_srcs.label.workspace_root:
-        sysroot_src = _exec_root_tmpl + rustc_srcs.label.workspace_root + "/" + sysroot_src
+        sysroot_src = _OUTPUT_BASE_TEMPLATE + rustc_srcs.label.workspace_root + "/" + sysroot_src
 
     sysroot_src_file = ctx.actions.declare_file(ctx.label.name + ".rust_analyzer_sysroot_src")
     ctx.actions.write(
diff --git a/tools/rust_analyzer/lib.rs b/tools/rust_analyzer/lib.rs
index 68db7e0..61f9688 100644
--- a/tools/rust_analyzer/lib.rs
+++ b/tools/rust_analyzer/lib.rs
@@ -45,6 +45,7 @@
     rules_rust_name: &impl AsRef<str>,
     targets: &[String],
     execution_root: impl AsRef<Path>,
+    output_base: impl AsRef<Path>,
     rust_project_path: impl AsRef<Path>,
 ) -> anyhow::Result<()> {
     let crate_specs = aquery::get_crate_specs(
@@ -72,6 +73,7 @@
     rust_project::write_rust_project(
         rust_project_path.as_ref(),
         execution_root.as_ref(),
+        output_base.as_ref(),
         &rust_project,
     )?;
 
diff --git a/tools/rust_analyzer/main.rs b/tools/rust_analyzer/main.rs
index 38a923c..6f13d91 100644
--- a/tools/rust_analyzer/main.rs
+++ b/tools/rust_analyzer/main.rs
@@ -26,6 +26,11 @@
         .as_ref()
         .expect("failed to find execution root, is --execution-root set correctly?");
 
+    let output_base = config
+        .output_base
+        .as_ref()
+        .expect("failed to find output base, is -output-base set correctly?");
+
     let rules_rust_name = env!("ASPECT_REPOSITORY");
 
     // Generate the crate specs.
@@ -43,6 +48,7 @@
         &rules_rust_name,
         &config.targets,
         &execution_root,
+        &output_base,
         &workspace_root.join("rust-project.json"),
     )?;
 
@@ -53,14 +59,6 @@
 fn parse_config() -> anyhow::Result<Config> {
     let mut config = Config::parse();
 
-    // Ensure we know the workspace. If we are under `bazel run`, the
-    // BUILD_WORKSPACE_DIR environment variable will be present.
-    if config.workspace.is_none() {
-        if let Some(ws_dir) = env::var_os("BUILD_WORKSPACE_DIRECTORY") {
-            config.workspace = Some(PathBuf::from(ws_dir));
-        }
-    }
-
     if config.workspace.is_some() && config.execution_root.is_some() {
         return Ok(config);
     }
@@ -97,24 +95,32 @@
     if config.execution_root.is_none() {
         config.execution_root = bazel_info.get("execution_root").map(Into::into);
     }
+    if config.output_base.is_none() {
+        config.output_base = bazel_info.get("output_base").map(Into::into);
+    }
 
     Ok(config)
 }
 
 #[derive(Debug, Parser)]
 struct Config {
-    // If not specified, uses the result of `bazel info workspace`.
-    #[clap(long)]
+    /// The path to the Bazel workspace directory. If not specified, uses the result of `bazel info workspace`.
+    #[clap(long, env = "BUILD_WORKSPACE_DIRECTORY")]
     workspace: Option<PathBuf>,
 
-    // If not specified, uses the result of `bazel info execution_root`.
+    /// The path to the Bazel execution root. If not specified, uses the result of `bazel info execution_root`.
     #[clap(long)]
     execution_root: Option<PathBuf>,
 
+    /// The path to the Bazel output user root. If not specified, uses the result of `bazel info output_base`.
+    #[clap(long, env = "OUTPUT_BASE")]
+    output_base: Option<PathBuf>,
+
+    /// The path to a Bazel binary
     #[clap(long, default_value = "bazel")]
     bazel: PathBuf,
 
-    // Space separated list of target patterns that comes after all other args.
+    /// Space separated list of target patterns that comes after all other args.
     #[clap(default_value = "@//...")]
     targets: Vec<String>,
 }
diff --git a/tools/rust_analyzer/rust_project.rs b/tools/rust_analyzer/rust_project.rs
index 0cc9378..30bb377 100644
--- a/tools/rust_analyzer/rust_project.rs
+++ b/tools/rust_analyzer/rust_project.rs
@@ -177,12 +177,17 @@
 pub fn write_rust_project(
     rust_project_path: &Path,
     execution_root: &Path,
+    output_base: &Path,
     rust_project: &RustProject,
 ) -> anyhow::Result<()> {
     let execution_root = execution_root
         .to_str()
         .ok_or_else(|| anyhow!("execution_root is not valid UTF-8"))?;
 
+    let output_base = output_base
+        .to_str()
+        .ok_or_else(|| anyhow!("output_base is not valid UTF-8"))?;
+
     // Try to remove the existing rust-project.json. It's OK if the file doesn't exist.
     match std::fs::remove_file(rust_project_path) {
         Ok(_) => {}
@@ -197,8 +202,9 @@
 
     // Render the `rust-project.json` file and replace the exec root
     // placeholders with the path to the local exec root.
-    let rust_project_content =
-        serde_json::to_string(rust_project)?.replace("__EXEC_ROOT__", execution_root);
+    let rust_project_content = serde_json::to_string(rust_project)?
+        .replace("__EXEC_ROOT__", execution_root)
+        .replace("__OUTPUT_BASE__", output_base);
 
     // Write the new rust-project.json file.
     std::fs::write(rust_project_path, rust_project_content)?;