Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
463 changes: 359 additions & 104 deletions docs/src/rust_analyzer.md

Large diffs are not rendered by default.

35 changes: 31 additions & 4 deletions rust/private/rust_analyzer.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ def _rust_analyzer_aspect_impl(target, ctx):
_accumulate_rust_analyzer_infos(dep_infos, labels_to_rais, getattr(ctx.rule.attr, "deps", []))
_accumulate_rust_analyzer_infos(dep_infos, labels_to_rais, getattr(ctx.rule.attr, "proc_macro_deps", []))

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

Expand Down Expand Up @@ -213,7 +219,16 @@ _EXEC_ROOT_TEMPLATE = "__EXEC_ROOT__/"
_OUTPUT_BASE_TEMPLATE = "__OUTPUT_BASE__/"

def _crate_id(crate_info):
"""Returns a unique stable identifier for a crate
"""Returns a unique stable identifier for a crate.

Keyed on the crate's root module path so that `rust_library(name = "lib")`
and `rust_test(name = "lib_test", crate = ":lib")` — which share a root
module — produce specs with the SAME crate_id. `consolidate_crate_specs`
then merges them into one rust-analyzer crate with the union of deps and
the test target's `build.label` (so TestOne runnables work). Without that
merge, rust-analyzer ends up with two crates pointing at the same source
file, which its IDE-side runnable detection doesn't handle well — test
codelens silently vanishes.

Returns:
(string): This crate's unique stable id.
Expand Down Expand Up @@ -255,7 +270,12 @@ def _create_single_crate(ctx, attrs, info):
if not is_external and not is_generated:
crate["build"] = {
"build_file": _WORKSPACE_TEMPLATE + ctx.build_file_path,
"label": ctx.label.package + ":" + ctx.label.name,
# Emit canonical `//pkg:name` form. Bazel's BEP reports action
# labels in this form, and the flycheck wrapper matches spec
# labels against BEP labels to find each action's stderr for
# diagnostics. Without the leading `//`, the match silently
# fails and the wrapper emits no diagnostics for the crate.
"label": "//" + ctx.label.package + ":" + ctx.label.name,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an issue here with deps from foreign workspaces?

}

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

def _rust_analyzer_toolchain_impl(ctx):
make_variable_info = platform_common.TemplateVariableInfo({
make_vars = {
"RUST_ANALYZER": ctx.file.rust_analyzer.path,
"RUST_ANALYZER_RLOCATIONPATH": _rlocationpath(ctx.file.rust_analyzer, ctx.workspace_name),
})
}
if ctx.file.proc_macro_srv:
make_vars["RUST_ANALYZER_PROC_MACRO_SRV"] = ctx.file.proc_macro_srv.path
make_vars["RUST_ANALYZER_PROC_MACRO_SRV_RLOCATIONPATH"] = _rlocationpath(
ctx.file.proc_macro_srv,
ctx.workspace_name,
)
make_variable_info = platform_common.TemplateVariableInfo(make_vars)

toolchain = platform_common.ToolchainInfo(
proc_macro_srv = ctx.executable.proc_macro_srv,
Expand Down
1 change: 1 addition & 0 deletions rust/private/rustfmt.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ rustfmt_test = rule(
def _rustfmt_toolchain_impl(ctx):
make_variables = {
"RUSTFMT": ctx.file.rustfmt.path,
"RUSTFMT_RLOCATIONPATH": _rlocationpath(ctx.file.rustfmt, ctx.workspace_name),
}

if ctx.attr.rustc:
Expand Down
136 changes: 136 additions & 0 deletions tools/rust_analyzer/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,142 @@ rust_binary(
],
)

# Stable entry point for editors / LSP clients. Locates the Bazel-built
# rust-analyzer via runfiles and `exec`s it so the LSP server matches the
# Bazel toolchain's rustc, sysroot, and (when invoked as its own proc-macro
# subcommand) proc-macro-srv. Point `rust-analyzer.server.path` at
# `bazel-bin/tools/rust_analyzer/rust_analyzer` after `bazel build`.
rust_binary(
name = "rust_analyzer",
srcs = ["bin/rust_analyzer.rs"],
data = [
"//rust/toolchain:current_rust_analyzer_toolchain",
],
edition = "2021",
rustc_env = {
"RUST_ANALYZER_RLOCATIONPATH": "$(RUST_ANALYZER_RLOCATIONPATH)",
},
toolchains = ["//rust/toolchain:current_rust_analyzer_toolchain"],
visibility = ["//visibility:public"],
deps = [
"//rust/runfiles",
],
)

# Companion wrapper for the proc-macro server. Use when an editor's bundled
# rust-analyzer is a different version than the one in the Bazel toolchain
# and proc-macro ABI mismatches cause silent expansion failures — point
# `rust-analyzer.procMacro.server` at
# `bazel-bin/tools/rust_analyzer/rust_analyzer_proc_macro_srv`.
rust_binary(
name = "rust_analyzer_proc_macro_srv",
srcs = ["bin/rust_analyzer_proc_macro_srv.rs"],
data = [
"//rust/toolchain:current_rust_analyzer_toolchain",
],
edition = "2021",
rustc_env = {
"RUST_ANALYZER_PROC_MACRO_SRV_RLOCATIONPATH": "$(RUST_ANALYZER_PROC_MACRO_SRV_RLOCATIONPATH)",
},
toolchains = ["//rust/toolchain:current_rust_analyzer_toolchain"],
visibility = ["//visibility:public"],
deps = [
"//rust/runfiles",
],
)

# Wrapper around the Bazel rustfmt toolchain's binary. setup_vscode wires
# `rust-analyzer.rustfmt.overrideCommand` at the launcher that exec's this;
# the LSP server pipes file contents on stdin and reads formatted output on
# stdout, so users can format `.rs` files without ever installing rustfmt
# on the host.
rust_binary(
name = "rustfmt",
srcs = ["bin/rustfmt.rs"],
data = [
"//rust/toolchain:current_rustfmt_toolchain",
],
edition = "2021",
rustc_env = {
"RUSTFMT_RLOCATIONPATH": "$(RUSTFMT_RLOCATIONPATH)",
},
toolchains = ["//rust/toolchain:current_rustfmt_toolchain"],
visibility = ["//visibility:public"],
deps = [
"//rust/runfiles",
],
)

# On-save flycheck wrapper. The assembled rust-project.json wires its
# `flycheck` runnable to `bazel run` this target with `{label}` and
# `{saved_file}` after rust-analyzer substitutes them. It runs
# `bazel build` with rustc diagnostics enabled, harvests the resulting
# `.rustc-output` files via BEP, and streams rustc JSON to stdout for
# rust-analyzer to render as inline squiggles.
rust_binary(
name = "flycheck",
srcs = ["bin/flycheck.rs"],
edition = "2021",
visibility = ["//visibility:public"],
deps = [
":gen_rust_project_lib",
"//tools/rust_analyzer/3rdparty/crates:anyhow",
"//tools/rust_analyzer/3rdparty/crates:camino",
"//tools/rust_analyzer/3rdparty/crates:clap",
"//tools/rust_analyzer/3rdparty/crates:env_logger",
"//tools/rust_analyzer/3rdparty/crates:log",
"//tools/rust_analyzer/3rdparty/crates:serde_json",
],
)

rust_test(
name = "flycheck_test",
crate = ":flycheck",
)

# One-command bootstrap that wires VSCode, Neovim, Helix, or any other
# rust-analyzer-capable editor at the Bazel rust-analyzer toolchain. Drops
# small launcher scripts at editor-appropriate locations so the LSP /
# discover / flycheck / rustfmt commands keep working across `bazel clean`,
# and (where the editor's config format is mergeable JSON) writes/merges
# the relevant settings. Re-runnable; preserves user keys.
#
# Invoke with a subcommand per IDE: `vscode`, `neovim`, `helix`, `print`.
rust_binary(
name = "setup",
srcs = ["bin/setup.rs"],
# Embedded via `include_str!` into the binary so the launcher templates
# ship inside `setup` itself and need no runfiles resolution. Both POSIX
# and Windows variants ship; setup picks one at runtime.
compile_data = [
"data/launcher_discover_bazel_rust_project.bat",
"data/launcher_discover_bazel_rust_project.sh",
"data/launcher_flycheck.bat",
"data/launcher_flycheck.sh",
"data/launcher_rust_analyzer.bat",
"data/launcher_rust_analyzer.sh",
"data/launcher_rust_analyzer_proc_macro_srv.bat",
"data/launcher_rust_analyzer_proc_macro_srv.sh",
"data/launcher_rustfmt.bat",
"data/launcher_rustfmt.sh",
],
edition = "2021",
visibility = ["//visibility:public"],
deps = [
"//tools/rust_analyzer/3rdparty/crates:anyhow",
"//tools/rust_analyzer/3rdparty/crates:camino",
"//tools/rust_analyzer/3rdparty/crates:clap",
"//tools/rust_analyzer/3rdparty/crates:env_logger",
"//tools/rust_analyzer/3rdparty/crates:log",
"//tools/rust_analyzer/3rdparty/crates:serde_json",
],
)

rust_test(
name = "setup_test",
crate = ":setup",
)

rust_library(
name = "gen_rust_project_lib",
srcs = glob(
Expand Down
151 changes: 1 addition & 150 deletions tools/rust_analyzer/aquery.rs
Original file line number Diff line number Diff line change
@@ -1,40 +1,7 @@
use std::collections::{BTreeMap, BTreeSet};

use anyhow::Context;
use camino::{Utf8Path, Utf8PathBuf};
use serde::Deserialize;

use crate::{bazel_command, deserialize_file_content};

#[derive(Debug, Deserialize)]
struct AqueryOutput {
artifacts: Vec<Artifact>,
actions: Vec<Action>,
#[serde(rename = "pathFragments")]
path_fragments: Vec<PathFragment>,
}

#[derive(Debug, Deserialize)]
struct Artifact {
id: u32,
#[serde(rename = "pathFragmentId")]
path_fragment_id: u32,
}

#[derive(Debug, Deserialize)]
struct PathFragment {
id: u32,
label: String,
#[serde(rename = "parentId")]
parent_id: Option<u32>,
}

#[derive(Debug, Deserialize)]
struct Action {
#[serde(rename = "outputIds")]
output_ids: Vec<u32>,
}

#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CrateSpec {
Expand Down Expand Up @@ -81,125 +48,9 @@ pub enum CrateType {
ProcMacro,
}

#[allow(clippy::too_many_arguments)]
pub fn get_crate_specs(
bazel: &Utf8Path,
output_base: &Utf8Path,
workspace: &Utf8Path,
execution_root: &Utf8Path,
bazel_startup_options: &[String],
bazel_args: &[String],
targets: &[String],
rules_rust_name: &str,
) -> anyhow::Result<BTreeSet<CrateSpec>> {
log::info!("running bazel aquery...");
log::debug!("Get crate specs with targets: {:?}", targets);
let target_pattern = format!("deps({})", targets.join("+"));

let mut aquery_command = bazel_command(bazel, Some(workspace), Some(output_base));
aquery_command
.args(bazel_startup_options)
.arg("aquery")
.args(bazel_args)
.arg("--include_aspects")
.arg("--include_artifacts")
.arg(format!(
"--aspects={rules_rust_name}//rust:defs.bzl%rust_analyzer_aspect"
))
.arg("--output_groups=rust_analyzer_crate_spec")
.arg(format!(
r#"outputs(".*\.rust_analyzer_crate_spec\.json",{target_pattern})"#
))
.arg("--output=jsonproto");
log::trace!("Running aquery: {:#?}", aquery_command);
let aquery_output = aquery_command
.output()
.context("Failed to spawn aquery command")?;

log::info!("bazel aquery finished; parsing spec files...");

let aquery_results = String::from_utf8(aquery_output.stdout)
.context("Failed to decode aquery results as utf-8.")?;

log::trace!("Aquery results: {}", aquery_results);

let crate_spec_files = parse_aquery_output_files(execution_root, &aquery_results)?;

let crate_specs = crate_spec_files
.into_iter()
.map(|file| deserialize_file_content(&file, output_base, workspace, execution_root))
.collect::<anyhow::Result<Vec<CrateSpec>>>()?;

consolidate_crate_specs(crate_specs)
}

fn parse_aquery_output_files(
execution_root: &Utf8Path,
aquery_stdout: &str,
) -> anyhow::Result<Vec<Utf8PathBuf>> {
let out: AqueryOutput = serde_json::from_str(aquery_stdout).map_err(|_| {
// Parsing to `AqueryOutput` failed, try parsing into a `serde_json::Value`:
match serde_json::from_str::<serde_json::Value>(aquery_stdout) {
Ok(serde_json::Value::Object(_)) => {
// If the JSON is an object, it's likely that the aquery command failed.
anyhow::anyhow!("Aquery returned an empty result, are there any Rust targets in the specified paths?.")
}
_ => {
anyhow::anyhow!("Failed to parse aquery output as JSON")
}
}
})?;

let artifacts = out
.artifacts
.iter()
.map(|a| (a.id, a))
.collect::<BTreeMap<_, _>>();
let path_fragments = out
.path_fragments
.iter()
.map(|pf| (pf.id, pf))
.collect::<BTreeMap<_, _>>();

let mut output_files: Vec<Utf8PathBuf> = Vec::new();
for action in out.actions {
for output_id in action.output_ids {
let artifact = artifacts
.get(&output_id)
.expect("internal consistency error in bazel output");
let path = path_from_fragments(artifact.path_fragment_id, &path_fragments)?;
let path = execution_root.join(path);
if path.exists() {
output_files.push(path);
} else {
log::warn!("Skipping missing crate_spec file: {:?}", path);
}
}
}

Ok(output_files)
}

fn path_from_fragments(
id: u32,
fragments: &BTreeMap<u32, &PathFragment>,
) -> anyhow::Result<Utf8PathBuf> {
let path_fragment = fragments
.get(&id)
.expect("internal consistency error in bazel output");

let buf = match path_fragment.parent_id {
Some(parent_id) => path_from_fragments(parent_id, fragments)?
.join(Utf8PathBuf::from(&path_fragment.label.clone())),
None => Utf8PathBuf::from(&path_fragment.label.clone()),
};

Ok(buf)
}

/// Read all crate specs, deduplicating crates with the same ID. This happens when
/// a rust_test depends on a rust_library, for example.
fn consolidate_crate_specs(crate_specs: Vec<CrateSpec>) -> anyhow::Result<BTreeSet<CrateSpec>> {
pub fn consolidate_crate_specs(crate_specs: Vec<CrateSpec>) -> anyhow::Result<BTreeSet<CrateSpec>> {
let mut consolidated_specs: BTreeMap<String, CrateSpec> = BTreeMap::new();
for mut spec in crate_specs.into_iter() {
log::debug!("{:?}", spec);
Expand Down
Loading
Loading