From a962df4d0e0e57c6967d2ec5be8bfaf4957f6297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Sj=C3=B6str=C3=B6m?= Date: Sat, 8 Aug 2026 23:19:33 +0200 Subject: [PATCH] fix(rust_analyzer): resolve external crate sources via output_base, not execroot gen_rust_project emits external crate paths through the execution root (__EXEC_ROOT__/external/...). The execroot's external/ symlinks are ephemeral -- only guaranteed to exist while a build action is running -- so once the build finishes those paths dangle and rust-analyzer cannot read any external crate sources. The result is no completions, no go-to-definition, and no docs for third-party dependencies. The real, stable location of external repository sources is {output_base}/external/. Rewrite {execution_root}/external/ prefixes to {output_base}/external/ during template substitution, while leaving bazel-out/ paths (generated files) under the execution root where they belong. Fixes the first bug described in #4057. Assisted-by: Claude (Anthropic AI assistant) --- tools/rust_analyzer/lib.rs | 41 +++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/tools/rust_analyzer/lib.rs b/tools/rust_analyzer/lib.rs index 6733caedf3..dc3238ea9f 100644 --- a/tools/rust_analyzer/lib.rs +++ b/tools/rust_analyzer/lib.rs @@ -496,7 +496,16 @@ where .replace("__WORKSPACE__", workspace.as_str()) .replace("${pwd}", execution_root.as_str()) .replace("__EXEC_ROOT__", execution_root.as_str()) - .replace("__OUTPUT_BASE__", output_base.as_str()); + .replace("__OUTPUT_BASE__", output_base.as_str()) + // External repository sources live at `{output_base}/external/`, not inside + // the execution root. The `{execution_root}/external/` symlinks are ephemeral + // (only guaranteed to exist while an action is running), so paths through the + // execution root break once the build finishes, leaving rust-analyzer unable + // to read external crate sources. + .replace( + &format!("{execution_root}/external/"), + &format!("{output_base}/external/"), + ); serde_json::from_str(&content).context("failed to deserialize after template substitution") } @@ -557,6 +566,36 @@ pub struct ToolchainInfo { mod tests { use super::*; + #[test] + fn substitution_rewrites_external_paths_to_output_base() { + let output_base = Utf8Path::new("/output_base"); + let workspace = Utf8Path::new("/workspace"); + let execution_root = Utf8Path::new("/output_base/execroot/_main"); + + let content = r#"[ + "__EXEC_ROOT__/external/rules_rust+crate+anyhow-1.0.0/src/lib.rs", + "__EXEC_ROOT__/bazel-out/k8-fastbuild/bin/proto/gen.rs", + "__OUTPUT_BASE__/external/toolchain/lib/rustlib/src", + "__WORKSPACE__/mylib/src/lib.rs" + ]"#; + + let paths: Vec = + deserialize_with_substitution(content, output_base, workspace, execution_root).unwrap(); + + assert_eq!( + paths, + vec![ + // `external/` paths must resolve via the output base; the + // execroot symlinks are gone after the build. + "/output_base/external/rules_rust+crate+anyhow-1.0.0/src/lib.rs", + // `bazel-out/` paths stay under the execution root. + "/output_base/execroot/_main/bazel-out/k8-fastbuild/bin/proto/gen.rs", + "/output_base/external/toolchain/lib/rustlib/src", + "/workspace/mylib/src/lib.rs", + ], + ); + } + #[test] fn dir_to_bazel_package_normalizes_backslashes() { // Windows-shaped input (post-strip_prefix.parent()): backslashes.