Skip to content

Commit 6b5e5e9

Browse files
committed
Add discoverConfig Support for rust-analyzer
1 parent 301d425 commit 6b5e5e9

24 files changed

Lines changed: 4331 additions & 448 deletions

docs/src/rust_analyzer.md

Lines changed: 338 additions & 104 deletions
Large diffs are not rendered by default.

rust/private/rust_analyzer.bzl

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@ def _rust_analyzer_aspect_impl(target, ctx):
120120
_accumulate_rust_analyzer_infos(dep_infos, labels_to_rais, getattr(ctx.rule.attr, "deps", []))
121121
_accumulate_rust_analyzer_infos(dep_infos, labels_to_rais, getattr(ctx.rule.attr, "proc_macro_deps", []))
122122

123+
# For `rust_test(crate = X)` we add X to dep_infos. Since X has the same
124+
# crate_id as us (same root_module), the dep-list filter in
125+
# `_create_single_crate` later drops it as a self-reference, and
126+
# `consolidate_crate_specs` merges X's spec with ours. End result: one
127+
# rust-analyzer crate with the union of deps and the test target's
128+
# build label.
123129
_accumulate_rust_analyzer_info(dep_infos, labels_to_rais, getattr(ctx.rule.attr, "crate", None))
124130
_accumulate_rust_analyzer_info(dep_infos, labels_to_rais, getattr(ctx.rule.attr, "actual", None))
125131

@@ -213,7 +219,16 @@ _EXEC_ROOT_TEMPLATE = "__EXEC_ROOT__/"
213219
_OUTPUT_BASE_TEMPLATE = "__OUTPUT_BASE__/"
214220

215221
def _crate_id(crate_info):
216-
"""Returns a unique stable identifier for a crate
222+
"""Returns a unique stable identifier for a crate.
223+
224+
Keyed on the crate's root module path so that `rust_library(name = "lib")`
225+
and `rust_test(name = "lib_test", crate = ":lib")` — which share a root
226+
module — produce specs with the SAME crate_id. `consolidate_crate_specs`
227+
then merges them into one rust-analyzer crate with the union of deps and
228+
the test target's `build.label` (so TestOne runnables work). Without that
229+
merge, rust-analyzer ends up with two crates pointing at the same source
230+
file, which its IDE-side runnable detection doesn't handle well — test
231+
codelens silently vanishes.
217232
218233
Returns:
219234
(string): This crate's unique stable id.
@@ -255,7 +270,12 @@ def _create_single_crate(ctx, attrs, info):
255270
if not is_external and not is_generated:
256271
crate["build"] = {
257272
"build_file": _WORKSPACE_TEMPLATE + ctx.build_file_path,
258-
"label": ctx.label.package + ":" + ctx.label.name,
273+
# Emit canonical `//pkg:name` form. Bazel's BEP reports action
274+
# labels in this form, and the flycheck wrapper matches spec
275+
# labels against BEP labels to find each action's stderr for
276+
# diagnostics. Without the leading `//`, the match silently
277+
# fails and the wrapper emits no diagnostics for the crate.
278+
"label": "//" + ctx.label.package + ":" + ctx.label.name,
259279
}
260280

261281
if is_generated:
@@ -310,10 +330,17 @@ def _rlocationpath(file, workspace_name):
310330
return "{}/{}".format(workspace_name, file.short_path)
311331

312332
def _rust_analyzer_toolchain_impl(ctx):
313-
make_variable_info = platform_common.TemplateVariableInfo({
333+
make_vars = {
314334
"RUST_ANALYZER": ctx.file.rust_analyzer.path,
315335
"RUST_ANALYZER_RLOCATIONPATH": _rlocationpath(ctx.file.rust_analyzer, ctx.workspace_name),
316-
})
336+
}
337+
if ctx.file.proc_macro_srv:
338+
make_vars["RUST_ANALYZER_PROC_MACRO_SRV"] = ctx.file.proc_macro_srv.path
339+
make_vars["RUST_ANALYZER_PROC_MACRO_SRV_RLOCATIONPATH"] = _rlocationpath(
340+
ctx.file.proc_macro_srv,
341+
ctx.workspace_name,
342+
)
343+
make_variable_info = platform_common.TemplateVariableInfo(make_vars)
317344

318345
toolchain = platform_common.ToolchainInfo(
319346
proc_macro_srv = ctx.executable.proc_macro_srv,

rust/private/rustfmt.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ rustfmt_test = rule(
313313
def _rustfmt_toolchain_impl(ctx):
314314
make_variables = {
315315
"RUSTFMT": ctx.file.rustfmt.path,
316+
"RUSTFMT_RLOCATIONPATH": _rlocationpath(ctx.file.rustfmt, ctx.workspace_name),
316317
}
317318

318319
if ctx.attr.rustc:

tools/rust_analyzer/BUILD.bazel

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,142 @@ rust_binary(
6060
],
6161
)
6262

63+
# Stable entry point for editors / LSP clients. Locates the Bazel-built
64+
# rust-analyzer via runfiles and `exec`s it so the LSP server matches the
65+
# Bazel toolchain's rustc, sysroot, and (when invoked as its own proc-macro
66+
# subcommand) proc-macro-srv. Point `rust-analyzer.server.path` at
67+
# `bazel-bin/tools/rust_analyzer/rust_analyzer` after `bazel build`.
68+
rust_binary(
69+
name = "rust_analyzer",
70+
srcs = ["bin/rust_analyzer.rs"],
71+
data = [
72+
"//rust/toolchain:current_rust_analyzer_toolchain",
73+
],
74+
edition = "2021",
75+
rustc_env = {
76+
"RUST_ANALYZER_RLOCATIONPATH": "$(RUST_ANALYZER_RLOCATIONPATH)",
77+
},
78+
toolchains = ["//rust/toolchain:current_rust_analyzer_toolchain"],
79+
visibility = ["//visibility:public"],
80+
deps = [
81+
"//rust/runfiles",
82+
],
83+
)
84+
85+
# Companion wrapper for the proc-macro server. Use when an editor's bundled
86+
# rust-analyzer is a different version than the one in the Bazel toolchain
87+
# and proc-macro ABI mismatches cause silent expansion failures — point
88+
# `rust-analyzer.procMacro.server` at
89+
# `bazel-bin/tools/rust_analyzer/rust_analyzer_proc_macro_srv`.
90+
rust_binary(
91+
name = "rust_analyzer_proc_macro_srv",
92+
srcs = ["bin/rust_analyzer_proc_macro_srv.rs"],
93+
data = [
94+
"//rust/toolchain:current_rust_analyzer_toolchain",
95+
],
96+
edition = "2021",
97+
rustc_env = {
98+
"RUST_ANALYZER_PROC_MACRO_SRV_RLOCATIONPATH": "$(RUST_ANALYZER_PROC_MACRO_SRV_RLOCATIONPATH)",
99+
},
100+
toolchains = ["//rust/toolchain:current_rust_analyzer_toolchain"],
101+
visibility = ["//visibility:public"],
102+
deps = [
103+
"//rust/runfiles",
104+
],
105+
)
106+
107+
# Wrapper around the Bazel rustfmt toolchain's binary. setup_vscode wires
108+
# `rust-analyzer.rustfmt.overrideCommand` at the launcher that exec's this;
109+
# the LSP server pipes file contents on stdin and reads formatted output on
110+
# stdout, so users can format `.rs` files without ever installing rustfmt
111+
# on the host.
112+
rust_binary(
113+
name = "rustfmt",
114+
srcs = ["bin/rustfmt.rs"],
115+
data = [
116+
"//rust/toolchain:current_rustfmt_toolchain",
117+
],
118+
edition = "2021",
119+
rustc_env = {
120+
"RUSTFMT_RLOCATIONPATH": "$(RUSTFMT_RLOCATIONPATH)",
121+
},
122+
toolchains = ["//rust/toolchain:current_rustfmt_toolchain"],
123+
visibility = ["//visibility:public"],
124+
deps = [
125+
"//rust/runfiles",
126+
],
127+
)
128+
129+
# On-save flycheck wrapper. The assembled rust-project.json wires its
130+
# `flycheck` runnable to `bazel run` this target with `{label}` and
131+
# `{saved_file}` after rust-analyzer substitutes them. It runs
132+
# `bazel build` with rustc diagnostics enabled, harvests the resulting
133+
# `.rustc-output` files via BEP, and streams rustc JSON to stdout for
134+
# rust-analyzer to render as inline squiggles.
135+
rust_binary(
136+
name = "flycheck",
137+
srcs = ["bin/flycheck.rs"],
138+
edition = "2021",
139+
visibility = ["//visibility:public"],
140+
deps = [
141+
":gen_rust_project_lib",
142+
"//tools/rust_analyzer/3rdparty/crates:anyhow",
143+
"//tools/rust_analyzer/3rdparty/crates:camino",
144+
"//tools/rust_analyzer/3rdparty/crates:clap",
145+
"//tools/rust_analyzer/3rdparty/crates:env_logger",
146+
"//tools/rust_analyzer/3rdparty/crates:log",
147+
"//tools/rust_analyzer/3rdparty/crates:serde_json",
148+
],
149+
)
150+
151+
rust_test(
152+
name = "flycheck_test",
153+
crate = ":flycheck",
154+
)
155+
156+
# One-command bootstrap that wires VSCode, Neovim, Helix, or any other
157+
# rust-analyzer-capable editor at the Bazel rust-analyzer toolchain. Drops
158+
# small launcher scripts at editor-appropriate locations so the LSP /
159+
# discover / flycheck / rustfmt commands keep working across `bazel clean`,
160+
# and (where the editor's config format is mergeable JSON) writes/merges
161+
# the relevant settings. Re-runnable; preserves user keys.
162+
#
163+
# Invoke with a subcommand per IDE: `vscode`, `neovim`, `helix`, `print`.
164+
rust_binary(
165+
name = "setup",
166+
srcs = ["bin/setup.rs"],
167+
# Embedded via `include_str!` into the binary so the launcher templates
168+
# ship inside `setup` itself and need no runfiles resolution. Both POSIX
169+
# and Windows variants ship; setup picks one at runtime.
170+
compile_data = [
171+
"data/launcher_discover_bazel_rust_project.bat",
172+
"data/launcher_discover_bazel_rust_project.sh",
173+
"data/launcher_flycheck.bat",
174+
"data/launcher_flycheck.sh",
175+
"data/launcher_rust_analyzer.bat",
176+
"data/launcher_rust_analyzer.sh",
177+
"data/launcher_rust_analyzer_proc_macro_srv.bat",
178+
"data/launcher_rust_analyzer_proc_macro_srv.sh",
179+
"data/launcher_rustfmt.bat",
180+
"data/launcher_rustfmt.sh",
181+
],
182+
edition = "2021",
183+
visibility = ["//visibility:public"],
184+
deps = [
185+
"//tools/rust_analyzer/3rdparty/crates:anyhow",
186+
"//tools/rust_analyzer/3rdparty/crates:camino",
187+
"//tools/rust_analyzer/3rdparty/crates:clap",
188+
"//tools/rust_analyzer/3rdparty/crates:env_logger",
189+
"//tools/rust_analyzer/3rdparty/crates:log",
190+
"//tools/rust_analyzer/3rdparty/crates:serde_json",
191+
],
192+
)
193+
194+
rust_test(
195+
name = "setup_test",
196+
crate = ":setup",
197+
)
198+
63199
rust_library(
64200
name = "gen_rust_project_lib",
65201
srcs = glob(

tools/rust_analyzer/aquery.rs

Lines changed: 1 addition & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,7 @@
11
use std::collections::{BTreeMap, BTreeSet};
22

3-
use anyhow::Context;
4-
use camino::{Utf8Path, Utf8PathBuf};
53
use serde::Deserialize;
64

7-
use crate::{bazel_command, deserialize_file_content};
8-
9-
#[derive(Debug, Deserialize)]
10-
struct AqueryOutput {
11-
artifacts: Vec<Artifact>,
12-
actions: Vec<Action>,
13-
#[serde(rename = "pathFragments")]
14-
path_fragments: Vec<PathFragment>,
15-
}
16-
17-
#[derive(Debug, Deserialize)]
18-
struct Artifact {
19-
id: u32,
20-
#[serde(rename = "pathFragmentId")]
21-
path_fragment_id: u32,
22-
}
23-
24-
#[derive(Debug, Deserialize)]
25-
struct PathFragment {
26-
id: u32,
27-
label: String,
28-
#[serde(rename = "parentId")]
29-
parent_id: Option<u32>,
30-
}
31-
32-
#[derive(Debug, Deserialize)]
33-
struct Action {
34-
#[serde(rename = "outputIds")]
35-
output_ids: Vec<u32>,
36-
}
37-
385
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)]
396
#[serde(deny_unknown_fields)]
407
pub struct CrateSpec {
@@ -81,125 +48,9 @@ pub enum CrateType {
8148
ProcMacro,
8249
}
8350

84-
#[allow(clippy::too_many_arguments)]
85-
pub fn get_crate_specs(
86-
bazel: &Utf8Path,
87-
output_base: &Utf8Path,
88-
workspace: &Utf8Path,
89-
execution_root: &Utf8Path,
90-
bazel_startup_options: &[String],
91-
bazel_args: &[String],
92-
targets: &[String],
93-
rules_rust_name: &str,
94-
) -> anyhow::Result<BTreeSet<CrateSpec>> {
95-
log::info!("running bazel aquery...");
96-
log::debug!("Get crate specs with targets: {:?}", targets);
97-
let target_pattern = format!("deps({})", targets.join("+"));
98-
99-
let mut aquery_command = bazel_command(bazel, Some(workspace), Some(output_base));
100-
aquery_command
101-
.args(bazel_startup_options)
102-
.arg("aquery")
103-
.args(bazel_args)
104-
.arg("--include_aspects")
105-
.arg("--include_artifacts")
106-
.arg(format!(
107-
"--aspects={rules_rust_name}//rust:defs.bzl%rust_analyzer_aspect"
108-
))
109-
.arg("--output_groups=rust_analyzer_crate_spec")
110-
.arg(format!(
111-
r#"outputs(".*\.rust_analyzer_crate_spec\.json",{target_pattern})"#
112-
))
113-
.arg("--output=jsonproto");
114-
log::trace!("Running aquery: {:#?}", aquery_command);
115-
let aquery_output = aquery_command
116-
.output()
117-
.context("Failed to spawn aquery command")?;
118-
119-
log::info!("bazel aquery finished; parsing spec files...");
120-
121-
let aquery_results = String::from_utf8(aquery_output.stdout)
122-
.context("Failed to decode aquery results as utf-8.")?;
123-
124-
log::trace!("Aquery results: {}", aquery_results);
125-
126-
let crate_spec_files = parse_aquery_output_files(execution_root, &aquery_results)?;
127-
128-
let crate_specs = crate_spec_files
129-
.into_iter()
130-
.map(|file| deserialize_file_content(&file, output_base, workspace, execution_root))
131-
.collect::<anyhow::Result<Vec<CrateSpec>>>()?;
132-
133-
consolidate_crate_specs(crate_specs)
134-
}
135-
136-
fn parse_aquery_output_files(
137-
execution_root: &Utf8Path,
138-
aquery_stdout: &str,
139-
) -> anyhow::Result<Vec<Utf8PathBuf>> {
140-
let out: AqueryOutput = serde_json::from_str(aquery_stdout).map_err(|_| {
141-
// Parsing to `AqueryOutput` failed, try parsing into a `serde_json::Value`:
142-
match serde_json::from_str::<serde_json::Value>(aquery_stdout) {
143-
Ok(serde_json::Value::Object(_)) => {
144-
// If the JSON is an object, it's likely that the aquery command failed.
145-
anyhow::anyhow!("Aquery returned an empty result, are there any Rust targets in the specified paths?.")
146-
}
147-
_ => {
148-
anyhow::anyhow!("Failed to parse aquery output as JSON")
149-
}
150-
}
151-
})?;
152-
153-
let artifacts = out
154-
.artifacts
155-
.iter()
156-
.map(|a| (a.id, a))
157-
.collect::<BTreeMap<_, _>>();
158-
let path_fragments = out
159-
.path_fragments
160-
.iter()
161-
.map(|pf| (pf.id, pf))
162-
.collect::<BTreeMap<_, _>>();
163-
164-
let mut output_files: Vec<Utf8PathBuf> = Vec::new();
165-
for action in out.actions {
166-
for output_id in action.output_ids {
167-
let artifact = artifacts
168-
.get(&output_id)
169-
.expect("internal consistency error in bazel output");
170-
let path = path_from_fragments(artifact.path_fragment_id, &path_fragments)?;
171-
let path = execution_root.join(path);
172-
if path.exists() {
173-
output_files.push(path);
174-
} else {
175-
log::warn!("Skipping missing crate_spec file: {:?}", path);
176-
}
177-
}
178-
}
179-
180-
Ok(output_files)
181-
}
182-
183-
fn path_from_fragments(
184-
id: u32,
185-
fragments: &BTreeMap<u32, &PathFragment>,
186-
) -> anyhow::Result<Utf8PathBuf> {
187-
let path_fragment = fragments
188-
.get(&id)
189-
.expect("internal consistency error in bazel output");
190-
191-
let buf = match path_fragment.parent_id {
192-
Some(parent_id) => path_from_fragments(parent_id, fragments)?
193-
.join(Utf8PathBuf::from(&path_fragment.label.clone())),
194-
None => Utf8PathBuf::from(&path_fragment.label.clone()),
195-
};
196-
197-
Ok(buf)
198-
}
199-
20051
/// Read all crate specs, deduplicating crates with the same ID. This happens when
20152
/// a rust_test depends on a rust_library, for example.
202-
fn consolidate_crate_specs(crate_specs: Vec<CrateSpec>) -> anyhow::Result<BTreeSet<CrateSpec>> {
53+
pub fn consolidate_crate_specs(crate_specs: Vec<CrateSpec>) -> anyhow::Result<BTreeSet<CrateSpec>> {
20354
let mut consolidated_specs: BTreeMap<String, CrateSpec> = BTreeMap::new();
20455
for mut spec in crate_specs.into_iter() {
20556
log::debug!("{:?}", spec);

0 commit comments

Comments
 (0)