diff --git a/rust/private/rust_analyzer.bzl b/rust/private/rust_analyzer.bzl index 89b8dd1f0d..0226f920d3 100644 --- a/rust/private/rust_analyzer.bzl +++ b/rust/private/rust_analyzer.bzl @@ -163,16 +163,77 @@ def _rust_analyzer_aspect_impl(target, ctx): build_info = build_info, )) + output_groups = { + "rust_analyzer_crate_spec": rust_analyzer_info.crate_specs, + "rust_analyzer_proc_macro_dylib": rust_analyzer_info.proc_macro_dylibs, + "rust_analyzer_src": rust_analyzer_info.build_info_out_dirs, + "rust_generated_srcs": depset(transitive = rust_generated_srcs), + } + + # Capture the Rustc action's argv + env so external tools (e.g. an editor + # flycheck shim) can replay a metadata-only typecheck of this crate + # directly, without paying a `bazel build` round-trip per save. + check_command_file = _write_check_command(ctx, target) + if check_command_file: + output_groups["rust_analyzer_check_command"] = depset([check_command_file]) + return [ rust_analyzer_info, - OutputGroupInfo( - rust_analyzer_crate_spec = rust_analyzer_info.crate_specs, - rust_analyzer_proc_macro_dylib = rust_analyzer_info.proc_macro_dylibs, - rust_analyzer_src = rust_analyzer_info.build_info_out_dirs, - rust_generated_srcs = depset(transitive = rust_generated_srcs), - ), + OutputGroupInfo(**output_groups), ] +def _write_check_command(ctx, target): + rustc_action = None + for action in target.actions: + if action.mnemonic == "Rustc": + rustc_action = action + break + if rustc_action == None: + return None + + argv = rustc_action.argv + if argv == None: + return None + + env = {k: v for k, v in rustc_action.env.items()} if rustc_action.env else {} + + output = ctx.actions.declare_file( + "{}.rust_analyzer_check_command.json".format(ctx.label.name), + ) + ctx.actions.write( + output = output, + content = json.encode_indent( + { + "argv": _transform_argv_for_check(argv), + "env": env, + }, + indent = " " * 4, + ), + ) + return output + +def _transform_argv_for_check(argv): + """Rewrite a build-mode rustc argv into a metadata-only typecheck argv. + """ + transformed = [] + skip = False + for arg in argv: + if skip: + skip = False + continue + if arg.startswith("--emit="): + transformed.append("--emit=metadata") + continue + if arg == "--emit": + transformed.append("--emit=metadata") + skip = True + continue + if arg == "--error-format=human" or arg.startswith("--error-format="): + transformed.append("--error-format=json") + continue + transformed.append(arg) + return transformed + def find_proc_macro_dylib(toolchain, target): """Find the proc_macro_dylib of target. Returns None if target crate is not type proc-macro. diff --git a/tools/rust_analyzer/bin/discover_rust_project.rs b/tools/rust_analyzer/bin/discover_rust_project.rs index 9598f0519a..f091147a6a 100644 --- a/tools/rust_analyzer/bin/discover_rust_project.rs +++ b/tools/rust_analyzer/bin/discover_rust_project.rs @@ -12,8 +12,8 @@ use camino::{Utf8Path, Utf8PathBuf}; use clap::Parser; use env_logger::{fmt::Formatter, Target, WriteStyle}; use gen_rust_project_lib::{ - bazel_info, generate_rust_project, DiscoverProject, RustAnalyzerArg, BUILD_FILE_NAMES, - WORKSPACE_ROOT_FILE_NAMES, + bazel_info, generate_rust_project, install_flycheck_symlink, DiscoverProject, RustAnalyzerArg, + BUILD_FILE_NAMES, WORKSPACE_ROOT_FILE_NAMES, }; use log::{LevelFilter, Record}; @@ -44,6 +44,10 @@ fn project_discovery() -> anyhow::Result> { log::info!("got rust-analyzer argument: {rust_analyzer_argument:?}"); + if let Err(error) = install_flycheck_symlink(&workspace, env!("FLYCHECK_RLOCATIONPATH")) { + log::warn!("failed to install flycheck symlink: {error}"); + } + let ra_arg = match rust_analyzer_argument { Some(ra_arg) => ra_arg, None => RustAnalyzerArg::Buildfile(find_workspace_root_file(&workspace)?), diff --git a/tools/rust_analyzer/bin/gen_rust_project.rs b/tools/rust_analyzer/bin/gen_rust_project.rs index b227240a12..6deb756f96 100644 --- a/tools/rust_analyzer/bin/gen_rust_project.rs +++ b/tools/rust_analyzer/bin/gen_rust_project.rs @@ -7,7 +7,7 @@ use std::{ use anyhow::{bail, Context}; use camino::Utf8PathBuf; use clap::Parser; -use gen_rust_project_lib::{bazel_info, generate_rust_project}; +use gen_rust_project_lib::{bazel_info, generate_rust_project, install_flycheck_symlink}; fn write_rust_project() -> anyhow::Result<()> { let Config { @@ -21,6 +21,10 @@ fn write_rust_project() -> anyhow::Result<()> { let rules_rust_name = env!("ASPECT_REPOSITORY"); + if let Err(error) = install_flycheck_symlink(&workspace, env!("FLYCHECK_RLOCATIONPATH")) { + log::warn!("failed to install flycheck symlink: {error}"); + } + let rust_project = generate_rust_project( &bazel, &output_base, diff --git a/tools/rust_analyzer/lib.rs b/tools/rust_analyzer/lib.rs index 93ca7cf3da..ab975a19ec 100644 --- a/tools/rust_analyzer/lib.rs +++ b/tools/rust_analyzer/lib.rs @@ -1,7 +1,7 @@ mod aquery; mod rust_project; -use std::{collections::BTreeMap, convert::TryInto, fs, process::Command}; +use std::{collections::BTreeMap, convert::TryInto, fs, path::Path, process::Command}; use anyhow::{bail, Context}; use camino::{Utf8Path, Utf8PathBuf}; @@ -15,6 +15,57 @@ pub const WORKSPACE_ROOT_FILE_NAMES: &[&str] = pub const BUILD_FILE_NAMES: &[&str] = &["BUILD.bazel", "BUILD"]; +/// Install a symlink at `/.bazel_rust_flycheck` pointing at the +/// bundled `flycheck` binary so rust-analyzer can invoke it directly via +/// `check.overrideCommand`. The indirection sidesteps bzlmod +/// canonical-repo-name fragility (the symlink target is resolved here, where +/// canonical names are fully knowable). +/// +/// Lives at workspace root rather than under `bazel-out` because the +/// `bazel-out` convenience symlink retargets when a different bazel command +/// runs against a different `--output_base`. Monorepos commonly run their IDE +/// bazel under a dedicated output_base separate from CLI bazel, which would +/// strand the symlink and break flycheck whenever a CLI build ran between +/// discoveries. Workspace root is stable across output_base switches; +/// consumers gitignore via their existing `.gitignore` entry. +pub fn install_flycheck_symlink( + workspace: &Utf8Path, + flycheck_rlocationpath: &str, +) -> anyhow::Result<()> { + let runfiles = Runfiles::create().context("failed to load runfiles")?; + let binary: Utf8PathBuf = runfiles + .rlocation(flycheck_rlocationpath) + .with_context(|| { + format!("flycheck binary runfile not found: {flycheck_rlocationpath}") + })? + .try_into()?; + let resolved = binary + .canonicalize_utf8() + .with_context(|| format!("failed to canonicalize {binary}"))?; + let symlink_path = workspace.join(".bazel_rust_flycheck"); + match fs::remove_file(&symlink_path) { + Ok(_) => {} + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => { + return Err(error) + .with_context(|| format!("failed to remove {symlink_path}")); + } + } + symlink_to_file(resolved.as_std_path(), symlink_path.as_std_path()) + .with_context(|| format!("failed to symlink {symlink_path} -> {resolved}"))?; + Ok(()) +} + +#[cfg(unix)] +fn symlink_to_file(target: &Path, link: &Path) -> std::io::Result<()> { + std::os::unix::fs::symlink(target, link) +} + +#[cfg(windows)] +fn symlink_to_file(target: &Path, link: &Path) -> std::io::Result<()> { + std::os::windows::fs::symlink_file(target, link) +} + #[allow(clippy::too_many_arguments)] pub fn generate_rust_project( bazel: &Utf8Path, @@ -111,7 +162,7 @@ fn generate_crate_info( .arg(format!( "--aspects={rules_rust}//tools/rust_analyzer:defs.bzl%rust_analyzer_aspect" )) - .arg("--output_groups=rust_analyzer_crate_spec,rust_generated_srcs,rust_analyzer_proc_macro_dylib,rust_analyzer_src") + .arg("--output_groups=rust_analyzer_crate_spec,rust_generated_srcs,rust_analyzer_proc_macro_dylib,rust_analyzer_src,rust_analyzer_check_command") .args(targets) .output()?;