Skip to content

gen_rust_project: external crate paths wrong under bzlmod + local_path_override; workspace members not detected #4057

@lsjostro

Description

@lsjostro

Description

Two bugs in gen_rust_project that together make rust-analyzer non-functional when using bzlmod with local_path_override.

Bug 1: External crate paths point into the ephemeral execroot

gen_rust_project generates root_module paths like:

{output_base}/execroot/_main/external/rules_rust+crate+anyhow-1.0.0/src/lib.rs

These paths do not exist after the build completes. The execroot/ directory is a build-time sandbox; its external/ symlinks are only valid during an action. The real cached sources live at:

{output_base}/external/rules_rust+crate+anyhow-1.0.0/src/lib.rs

Because rust-analyzer cannot read these paths, it has no type information for any external crate — no completions, no go-to-definition, no inline documentation for dependencies like clap, anyhow, serde, etc.

The root cause is in tools/rust_analyzer/lib.rs, deserialize_file_content: __EXEC_ROOT__ is replaced with execution_root ({output_base}/execroot/_main), which is correct for bazel-out/ paths but wrong for external/ paths.

Bug 2: local_path_override modules not detected as workspace members

In bzlmod, a module declared with local_path_override has its sources in the real workspace tree, accessed via a symlink at {output_base}/external/<module>+/. The aspect marks these crates is_workspace_member: false because the build system has no way to communicate this. rust-analyzer therefore shows no diagnostics (errors, warnings) for these crates.

After Bug 1 is fixed, the root_module path for such crates ({output_base}/external/<module>+/src/main.rs) resolves through the symlink back into the workspace tree. The fix is to canonicalize the path and check if it falls under workspace.

Reproduction steps

Minimal bzlmod workspace with a local_path_override module:

# MODULE.bazel (root)
bazel_dep(name = "rules_rust", version = "0.70.0")
bazel_dep(name = "mypkg", version = "0.1.0")
local_path_override(module_name = "mypkg", path = "mypkg")
# mypkg/MODULE.bazel
module(name = "mypkg", version = "0.1.0")
bazel_dep(name = "rules_rust", version = "0.70.0")
crate = use_extension("@rules_rust//crate_universe:extension.bzl", "crate")
crate.from_cargo(name = "crates", manifests = ["//tools/mybin:Cargo.toml"])
use_repo(crate, "crates")
# mypkg/BUILD.bazel
rust_binary(name = "mybin", srcs = ["src/main.rs"], deps = ["@crates//:anyhow"])
// mypkg/src/main.rs
fn main() { let _e = anyhow::anyhow!("test"); }

Run gen_rust_project and open src/main.rs in any editor with rust-analyzer:

  • anyhow:: produces no completions (Bug 1)
  • Errors in src/main.rs are not shown (Bug 2)

Proposed fixes:

tools/rust_analyzer/lib.rs:

let content = fs::read_to_string(path)?
    ...
    .replace("__EXEC_ROOT__", execution_root.as_str())
    .replace("__OUTPUT_BASE__", output_base.as_str())
    // External crate sources live at output_base/external/, not inside execroot.
    // execroot symlinks are ephemeral (only valid during a build action).
    .replace(
        &format!("{}/external/", execution_root),
        &format!("{}/external/", output_base),
    );

tools/rust_analyzer/rust_project.rs, inside assemble_rust_project:

// Crates from local_path_override modules resolve through an external/ symlink
// back into the real workspace tree. Detect this so rust-analyzer shows diagnostics.
let is_workspace_member = c.is_workspace_member
    || std::path::Path::new(&c.root_module)
        .canonicalize()
        .map(|p| p.starts_with(workspace.as_std_path()))
        .unwrap_or(false);

Additional context

Workaround (used in our project): post-process the generated rust-project.json with a Python script that replaces execroot/_main/external/ with external/ in all paths and sets is_workspace_member: true for crates whose root_module resolves to a path under the workspace.

Bug 1 likely affects all bzlmod projects, not just those using local_path_override, since execroot/external/ symlinks are never stable across builds.

Impact

rust-analyzer is effectively broken for any Rust code in a bzlmod local_path_override module. Workaround exists (post-processing script) but it is fragile. Does not block updating to a newer rules_rust version, but makes IDE support unusable without the workaround.

Bazel and rules_rust version

  • Bazel: 9.x (bzlmod)
  • rules_rust: 0.70.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-triageThe ticket needs maintainer attention.

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions