bazelbuild / bazelbuild/rules_rust
gen_rust_project: external crate paths wrong under bzlmod + local_path_override; workspace members not detected
- Dominant language
- Starlark
- Stars
- 843
- Forks
- 651
- Avg merge
- 2d 18h
- Merged PRs (30d)
- 15
Description
**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/+/`. 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/+/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:
```python
# 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")
```
```python
# 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")
```
```python
# mypkg/BUILD.bazel
rust_binary(name = "mybin", srcs = ["src/main.rs"], deps = ["@crates//:anyhow"])
```
```rust
// 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`:
```rust
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`:
```rust
// 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
Contributor guide
Assessment
This issue has not been assessed yet.