Skip to content

rust-analyzer sysroot enrichment fails: no cargo in rust_analyzer_toolchain #4203

Description

@beeb

Description

When using rules_rust's rust-analyzer integration (rust_analyzer_toolchain +
the rust-project.json produced by discover_bazel_rust_project), hover and
go-to-definition are broken for any local variable/expression whose type is a
generic container from std/alloc (e.g. Option<T>, Arc<T>, RefCell<T>)
instantiated with a workspace-local T.

  • Hovering such a variable shows {unknown} instead of the real type.
  • Go-to-definition into the corresponding std/core source (e.g. jumping
    into RefCell::new) silently does nothing — no navigation, no error.

Plain, non-generic types (local newtypes, etc.) and types coming straight from
an already-fully-inferred function return (e.g. anyhow::Result<...>) are
unaffected — only generic std/alloc containers wrapping a workspace-local type
are impacted.

Expected behavior: hover and go-to-definition should work the same way they do
for identical code loaded via a normal cargo-based project. We verified this
directly — the same source file, same rust-analyzer version, loaded via a
plain Cargo project instead of the Bazel-generated rust-project.json, and
both hover and go-to-definition work correctly there.

Reproduction steps

This reproduces in a minimal, standalone workspace — no other configuration
involved:

  1. Create a workspace with just these three files:

    MODULE.bazel:

    module(name = "rules_rust_ra_repro")
    
    bazel_dep(name = "rules_rust", version = "0.73.0")
    
    rust = use_extension("@rules_rust//rust:extensions.bzl", "rust")
    rust.toolchain(
        edition = "2024",
        versions = ["1.95.0"],
    )
    use_repo(rust, "rust_toolchains")
    
    register_toolchains("@rust_toolchains//:all")

    BUILD.bazel:

    load("@rules_rust//rust:defs.bzl", "rust_library")
    
    rust_library(
        name = "repro",
        srcs = ["src/lib.rs"],
        edition = "2024",
    )

    src/lib.rs:

    use std::cell::RefCell;
    
    pub struct Foo;
    
    pub fn example() -> Option<Foo> {
        let y: Option<Foo> = None;
        let _z = RefCell::new(Foo);
        y
    }
  2. bazel build //:repro — builds cleanly.

  3. bazel run @rules_rust//tools/rust_analyzer:setup -- helix — generates the
    rust-analyzer integration and a rust-project.json-producing discover
    binary under .helix/.rules_rust_analyzer/.

  4. Inspect the sysroot directory referenced by the generated
    rust-project.json's sysroot field
    (<output_base>/external/rules_rust++rust+rust_analyzer_1.95.0_tools):
    it contains rust-analyzer, rustc, rustdoc, rust-gdb, rust-gdbgui,
    rust-lldb — no cargo binary.

  5. Run the generated rust-project.json through rust-analyzer directly
    (no editor needed):

    <sysroot>/bin/rust-analyzer analysis-stats <dir containing rust-project.json>
    

    This logs:

    WARN `cargo metadata` failed and returning succeeded result with
    `--no-deps` error=`cargo metadata` exited with an error: error: unknown
    `-Z` flag specified: lockfile-path
    
  6. Opening the same project in an editor via the generated
    workspace.discoverConfig integration (we used Helix 25.07.1) shows the
    downstream symptom on src/lib.rs: hovering y (type Option<Foo>)
    displays {unknown} instead of the real type, and go-to-definition on
    RefCell does nothing. Earlier in the session, rust-analyzer also logs:

    WARN Workspace `<crate path>` has sysroot errors: sysroot at
    `<sysroot>/lib/rustlib/src/library` is missing a `core` library, try
    running `rustup component add rust-src` to possibly fix this
    

    even though that sysroot directory contains a structurally valid core
    crate (Cargo.toml + src/lib.rs present).

Additional context

  • The -Zlockfile-path failure is not simply a missing-RUSTC_BOOTSTRAP/
    nightly-channel issue: even with RUSTC_BOOTSTRAP=1 forced, the cargo
    that ends up being invoked for this call reports unknown -Z flag specified: lockfile-path outright — the flag isn't recognized by that
    cargo build at all.
  • The rust_analyzer_toolchain-provided sysroot directory (the one referenced
    by the sysroot/sysroot_src fields of the generated rust-project.json)
    contains rust-analyzer, rustc, rustdoc, rust-gdb, rust-gdbgui,
    rust-lldb — but no cargo binary.
  • Reproduces identically on rules_rust 0.71.3 and 0.73.0.
  • Editor used for testing: Helix 25.07.1, via the standard
    workspace.discoverConfig integration generated by
    bazel run @rules_rust//tools/rust_analyzer:setup.
  • Confirmed as a regression relative to a plain Cargo-based project setup:
    identical source and rust-analyzer version, loaded without going through
    rules_rust's discovery/rust-project.json generation, does not exhibit
    either symptom.

Impact

We are not aware of a workaround that stays purely within the Bazel/
rules_rust toolchain — avoiding the issue currently seems to require making
a cargo binary available somewhere rust-analyzer will pick it up from. This
does not block using rules_rust/rust_analyzer_toolchain outright, but it
meaningfully degrades the IDE experience for any code using standard generic
containers over local types — a very common pattern — for any team that
doesn't otherwise have a separate Cargo/rustup toolchain installed.

Workaround

rust-analyzer resolves the cargo for this call purely via PATH — we
confirmed it does not look for one relative to the sysroot it was given
(pointing sysroot at a directory that does contain a matching cargo had
no effect). So the only workaround we found is to make a cargo available on
PATH for the rust-analyzer process itself, matched to the same version as
the rust_analyzer_toolchain's rustc/rust-analyzer (a mismatched, e.g.
system/rustup-provided, cargo either rejects the -Z flag outright or
requires nightly). Bazel does build such a matching cargo as part of the
regular (non-rust-analyzer) rust_toolchain, so we pointed rust-analyzer at
that one via a small wrapper script that Helix's command setting launches
instead of the rust-analyzer binary directly:

#!/bin/sh
output_base=$(bazel info output_base 2>/dev/null)
cargo_dir="$output_base/external/<rust_toolchain_repo>/bin"

export PATH="$cargo_dir:$PATH"
export RUSTC_BOOTSTRAP=1

exec "<path to the rust-analyzer binary>" "$@"

This is entirely external to rules_rust's own configuration surface (there
is no rust-analyzer.cargo.* setting that redirects or disables this call),
so it has to be layered on top of the generated editor integration.

In principle discover_bazel_rust_project/setup could generate a wrapper
like this itself — it already emits small executables into
.helix/.rules_rust_analyzer/ (discover_bazel_rust_project.exe,
flycheck.exe) and has access to both the rust_analyzer_toolchain's
rust-analyzer binary and the regular rust_toolchain's matching cargo
during that step. We're not asking for that specifically, though — a few
reasons it may not be the right fix to bake in as default behavior:

  • RUSTC_BOOTSTRAP=1 unlocks all unstable features for that process,
    silently. Shipping it unconditionally in official tooling is a broader
    escape hatch than "fix this one call."
  • It's inherently version-fragile: it only works because this particular
    cargo/rustc pairing happens to support -Zlockfile-path. A future
    rust-analyzer release could start requiring a different unstable flag the
    matched cargo doesn't have either.
  • It doesn't fix the actual gap (rust-analyzer having no way to redirect or
    disable this call) — it papers over it. A real fix likely belongs upstream
    in rust-analyzer.

Bazel and rules_rust version

  • Bazel: 9.1.1
  • rules_rust: 0.73.0 (also reproduced on 0.71.3)
  • rust-analyzer version from rust_analyzer_toolchain: 1.95.0

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions