use std::collections::{BTreeMap, BTreeSet}; use std::fs::File; use std::path::Path; use std::path::PathBuf; use std::process::Command; use anyhow::Context; use serde::Deserialize; #[derive(Debug, Deserialize)] struct AqueryOutput { artifacts: Vec, actions: Vec, #[serde(rename = "pathFragments")] path_fragments: Vec, } #[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, } #[derive(Debug, Deserialize)] struct Action { #[serde(rename = "outputIds")] output_ids: Vec, } #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)] #[serde(deny_unknown_fields)] pub struct CrateSpec { pub aliases: BTreeMap, pub crate_id: String, pub display_name: String, pub edition: String, pub root_module: String, pub is_workspace_member: bool, pub deps: BTreeSet, pub proc_macro_dylib_path: Option, pub source: Option, pub cfg: Vec, pub env: BTreeMap, pub target: String, pub crate_type: String, } #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)] #[serde(deny_unknown_fields)] pub struct CrateSpecSource { pub exclude_dirs: Vec, pub include_dirs: Vec, } pub fn get_crate_specs( bazel: &Path, workspace: &Path, execution_root: &Path, targets: &[String], rules_rust_name: &str, ) -> anyhow::Result> { log::debug!("Get crate specs with targets: {:?}", targets); let target_pattern = targets .iter() .map(|t| format!("deps({t})")) .collect::>() .join("+"); let aquery_output = Command::new(bazel) .current_dir(workspace) .env_remove("BAZELISK_SKIP_WRAPPER") .env_remove("BUILD_WORKING_DIRECTORY") .env_remove("BUILD_WORKSPACE_DIRECTORY") .arg("aquery") .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") .output()?; let crate_spec_files = parse_aquery_output_files(execution_root, &String::from_utf8(aquery_output.stdout)?)?; let crate_specs = crate_spec_files .into_iter() .map(|file| { let f = File::open(&file) .with_context(|| format!("Failed to open file: {}", file.display()))?; serde_json::from_reader(f) .with_context(|| format!("Failed to deserialize file: {}", file.display())) }) .collect::>>()?; consolidate_crate_specs(crate_specs) } fn parse_aquery_output_files( execution_root: &Path, aquery_stdout: &str, ) -> anyhow::Result> { 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::(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::>(); let path_fragments = out .path_fragments .iter() .map(|pf| (pf.id, pf)) .collect::>(); let mut output_files: Vec = 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, ) -> anyhow::Result { 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(PathBuf::from(&path_fragment.label.clone())), None => PathBuf::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) -> anyhow::Result> { let mut consolidated_specs: BTreeMap = BTreeMap::new(); for mut spec in crate_specs.into_iter() { log::debug!("{:?}", spec); if let Some(existing) = consolidated_specs.get_mut(&spec.crate_id) { existing.deps.extend(spec.deps); spec.cfg.retain(|cfg| !existing.cfg.contains(cfg)); existing.cfg.extend(spec.cfg); // display_name should match the library's crate name because Rust Analyzer // seems to use display_name for matching crate entries in rust-project.json // against symbols in source files. For more details, see // https://github.com/bazelbuild/rules_rust/issues/1032 if spec.crate_type == "rlib" { existing.display_name = spec.display_name; existing.crate_type = "rlib".into(); } // For proc-macro crates that exist within the workspace, there will be a // generated crate-spec in both the fastbuild and opt-exec configuration. // Prefer proc macro paths with an opt-exec component in the path. if let Some(dylib_path) = spec.proc_macro_dylib_path.as_ref() { const OPT_PATH_COMPONENT: &str = "-opt-exec-"; if dylib_path.contains(OPT_PATH_COMPONENT) { existing.proc_macro_dylib_path.replace(dylib_path.clone()); } } } else { consolidated_specs.insert(spec.crate_id.clone(), spec); } } Ok(consolidated_specs.into_values().collect()) } #[cfg(test)] mod test { use super::*; use itertools::Itertools; #[test] fn consolidate_lib_then_test_specs() { let crate_specs = vec![ CrateSpec { aliases: BTreeMap::new(), crate_id: "ID-mylib.rs".into(), display_name: "mylib".into(), edition: "2018".into(), root_module: "mylib.rs".into(), is_workspace_member: true, deps: BTreeSet::from(["ID-lib_dep.rs".into()]), proc_macro_dylib_path: None, source: None, cfg: vec!["test".into(), "debug_assertions".into()], env: BTreeMap::new(), target: "x86_64-unknown-linux-gnu".into(), crate_type: "rlib".into(), }, CrateSpec { aliases: BTreeMap::new(), crate_id: "ID-extra_test_dep.rs".into(), display_name: "extra_test_dep".into(), edition: "2018".into(), root_module: "extra_test_dep.rs".into(), is_workspace_member: true, deps: BTreeSet::new(), proc_macro_dylib_path: None, source: None, cfg: vec!["test".into(), "debug_assertions".into()], env: BTreeMap::new(), target: "x86_64-unknown-linux-gnu".into(), crate_type: "rlib".into(), }, CrateSpec { aliases: BTreeMap::new(), crate_id: "ID-lib_dep.rs".into(), display_name: "lib_dep".into(), edition: "2018".into(), root_module: "lib_dep.rs".into(), is_workspace_member: true, deps: BTreeSet::new(), proc_macro_dylib_path: None, source: None, cfg: vec!["test".into(), "debug_assertions".into()], env: BTreeMap::new(), target: "x86_64-unknown-linux-gnu".into(), crate_type: "rlib".into(), }, CrateSpec { aliases: BTreeMap::new(), crate_id: "ID-mylib.rs".into(), display_name: "mylib_test".into(), edition: "2018".into(), root_module: "mylib.rs".into(), is_workspace_member: true, deps: BTreeSet::from(["ID-extra_test_dep.rs".into()]), proc_macro_dylib_path: None, source: None, cfg: vec!["test".into(), "debug_assertions".into()], env: BTreeMap::new(), target: "x86_64-unknown-linux-gnu".into(), crate_type: "bin".into(), }, ]; assert_eq!( consolidate_crate_specs(crate_specs).unwrap(), BTreeSet::from([ CrateSpec { aliases: BTreeMap::new(), crate_id: "ID-mylib.rs".into(), display_name: "mylib".into(), edition: "2018".into(), root_module: "mylib.rs".into(), is_workspace_member: true, deps: BTreeSet::from(["ID-lib_dep.rs".into(), "ID-extra_test_dep.rs".into()]), proc_macro_dylib_path: None, source: None, cfg: vec!["test".into(), "debug_assertions".into()], env: BTreeMap::new(), target: "x86_64-unknown-linux-gnu".into(), crate_type: "rlib".into(), }, CrateSpec { aliases: BTreeMap::new(), crate_id: "ID-extra_test_dep.rs".into(), display_name: "extra_test_dep".into(), edition: "2018".into(), root_module: "extra_test_dep.rs".into(), is_workspace_member: true, deps: BTreeSet::new(), proc_macro_dylib_path: None, source: None, cfg: vec!["test".into(), "debug_assertions".into()], env: BTreeMap::new(), target: "x86_64-unknown-linux-gnu".into(), crate_type: "rlib".into(), }, CrateSpec { aliases: BTreeMap::new(), crate_id: "ID-lib_dep.rs".into(), display_name: "lib_dep".into(), edition: "2018".into(), root_module: "lib_dep.rs".into(), is_workspace_member: true, deps: BTreeSet::new(), proc_macro_dylib_path: None, source: None, cfg: vec!["test".into(), "debug_assertions".into()], env: BTreeMap::new(), target: "x86_64-unknown-linux-gnu".into(), crate_type: "rlib".into(), }, ]) ); } #[test] fn consolidate_test_then_lib_specs() { let crate_specs = vec![ CrateSpec { aliases: BTreeMap::new(), crate_id: "ID-mylib.rs".into(), display_name: "mylib_test".into(), edition: "2018".into(), root_module: "mylib.rs".into(), is_workspace_member: true, deps: BTreeSet::from(["ID-extra_test_dep.rs".into()]), proc_macro_dylib_path: None, source: None, cfg: vec!["test".into(), "debug_assertions".into()], env: BTreeMap::new(), target: "x86_64-unknown-linux-gnu".into(), crate_type: "bin".into(), }, CrateSpec { aliases: BTreeMap::new(), crate_id: "ID-mylib.rs".into(), display_name: "mylib".into(), edition: "2018".into(), root_module: "mylib.rs".into(), is_workspace_member: true, deps: BTreeSet::from(["ID-lib_dep.rs".into()]), proc_macro_dylib_path: None, source: None, cfg: vec!["test".into(), "debug_assertions".into()], env: BTreeMap::new(), target: "x86_64-unknown-linux-gnu".into(), crate_type: "rlib".into(), }, CrateSpec { aliases: BTreeMap::new(), crate_id: "ID-extra_test_dep.rs".into(), display_name: "extra_test_dep".into(), edition: "2018".into(), root_module: "extra_test_dep.rs".into(), is_workspace_member: true, deps: BTreeSet::new(), proc_macro_dylib_path: None, source: None, cfg: vec!["test".into(), "debug_assertions".into()], env: BTreeMap::new(), target: "x86_64-unknown-linux-gnu".into(), crate_type: "rlib".into(), }, CrateSpec { aliases: BTreeMap::new(), crate_id: "ID-lib_dep.rs".into(), display_name: "lib_dep".into(), edition: "2018".into(), root_module: "lib_dep.rs".into(), is_workspace_member: true, deps: BTreeSet::new(), proc_macro_dylib_path: None, source: None, cfg: vec!["test".into(), "debug_assertions".into()], env: BTreeMap::new(), target: "x86_64-unknown-linux-gnu".into(), crate_type: "rlib".into(), }, ]; assert_eq!( consolidate_crate_specs(crate_specs).unwrap(), BTreeSet::from([ CrateSpec { aliases: BTreeMap::new(), crate_id: "ID-mylib.rs".into(), display_name: "mylib".into(), edition: "2018".into(), root_module: "mylib.rs".into(), is_workspace_member: true, deps: BTreeSet::from(["ID-lib_dep.rs".into(), "ID-extra_test_dep.rs".into()]), proc_macro_dylib_path: None, source: None, cfg: vec!["test".into(), "debug_assertions".into()], env: BTreeMap::new(), target: "x86_64-unknown-linux-gnu".into(), crate_type: "rlib".into(), }, CrateSpec { aliases: BTreeMap::new(), crate_id: "ID-extra_test_dep.rs".into(), display_name: "extra_test_dep".into(), edition: "2018".into(), root_module: "extra_test_dep.rs".into(), is_workspace_member: true, deps: BTreeSet::new(), proc_macro_dylib_path: None, source: None, cfg: vec!["test".into(), "debug_assertions".into()], env: BTreeMap::new(), target: "x86_64-unknown-linux-gnu".into(), crate_type: "rlib".into(), }, CrateSpec { aliases: BTreeMap::new(), crate_id: "ID-lib_dep.rs".into(), display_name: "lib_dep".into(), edition: "2018".into(), root_module: "lib_dep.rs".into(), is_workspace_member: true, deps: BTreeSet::new(), proc_macro_dylib_path: None, source: None, cfg: vec!["test".into(), "debug_assertions".into()], env: BTreeMap::new(), target: "x86_64-unknown-linux-gnu".into(), crate_type: "rlib".into(), }, ]) ); } #[test] fn consolidate_lib_test_main_specs() { // mylib.rs is a library but has tests and an entry point, and mylib2.rs // depends on mylib.rs. The display_name of the library target mylib.rs // should be "mylib" no matter what order the crate specs is in. // Otherwise Rust Analyzer will not be able to resolve references to // mylib in mylib2.rs. let crate_specs = vec![ CrateSpec { aliases: BTreeMap::new(), crate_id: "ID-mylib.rs".into(), display_name: "mylib".into(), edition: "2018".into(), root_module: "mylib.rs".into(), is_workspace_member: true, deps: BTreeSet::new(), proc_macro_dylib_path: None, source: None, cfg: vec!["test".into(), "debug_assertions".into()], env: BTreeMap::new(), target: "x86_64-unknown-linux-gnu".into(), crate_type: "rlib".into(), }, CrateSpec { aliases: BTreeMap::new(), crate_id: "ID-mylib.rs".into(), display_name: "mylib_test".into(), edition: "2018".into(), root_module: "mylib.rs".into(), is_workspace_member: true, deps: BTreeSet::new(), proc_macro_dylib_path: None, source: None, cfg: vec!["test".into(), "debug_assertions".into()], env: BTreeMap::new(), target: "x86_64-unknown-linux-gnu".into(), crate_type: "bin".into(), }, CrateSpec { aliases: BTreeMap::new(), crate_id: "ID-mylib.rs".into(), display_name: "mylib_main".into(), edition: "2018".into(), root_module: "mylib.rs".into(), is_workspace_member: true, deps: BTreeSet::new(), proc_macro_dylib_path: None, source: None, cfg: vec!["test".into(), "debug_assertions".into()], env: BTreeMap::new(), target: "x86_64-unknown-linux-gnu".into(), crate_type: "bin".into(), }, CrateSpec { aliases: BTreeMap::new(), crate_id: "ID-mylib2.rs".into(), display_name: "mylib2".into(), edition: "2018".into(), root_module: "mylib2.rs".into(), is_workspace_member: true, deps: BTreeSet::from(["ID-mylib.rs".into()]), proc_macro_dylib_path: None, source: None, cfg: vec!["test".into(), "debug_assertions".into()], env: BTreeMap::new(), target: "x86_64-unknown-linux-gnu".into(), crate_type: "rlib".into(), }, ]; for perm in crate_specs.into_iter().permutations(4) { assert_eq!( consolidate_crate_specs(perm).unwrap(), BTreeSet::from([ CrateSpec { aliases: BTreeMap::new(), crate_id: "ID-mylib.rs".into(), display_name: "mylib".into(), edition: "2018".into(), root_module: "mylib.rs".into(), is_workspace_member: true, deps: BTreeSet::from([]), proc_macro_dylib_path: None, source: None, cfg: vec!["test".into(), "debug_assertions".into()], env: BTreeMap::new(), target: "x86_64-unknown-linux-gnu".into(), crate_type: "rlib".into(), }, CrateSpec { aliases: BTreeMap::new(), crate_id: "ID-mylib2.rs".into(), display_name: "mylib2".into(), edition: "2018".into(), root_module: "mylib2.rs".into(), is_workspace_member: true, deps: BTreeSet::from(["ID-mylib.rs".into()]), proc_macro_dylib_path: None, source: None, cfg: vec!["test".into(), "debug_assertions".into()], env: BTreeMap::new(), target: "x86_64-unknown-linux-gnu".into(), crate_type: "rlib".into(), }, ]) ); } } #[test] fn consolidate_proc_macro_prefer_exec() { // proc macro crates should prefer the -opt-exec- path which is always generated // during builds where it is used, while the fastbuild version would only be built // when explicitly building that target. let crate_specs = vec![ CrateSpec { aliases: BTreeMap::new(), crate_id: "ID-myproc_macro.rs".into(), display_name: "myproc_macro".into(), edition: "2018".into(), root_module: "myproc_macro.rs".into(), is_workspace_member: true, deps: BTreeSet::new(), proc_macro_dylib_path: Some( "bazel-out/k8-opt-exec-F005BA11/bin/myproc_macro/libmyproc_macro-12345.so" .into(), ), source: None, cfg: vec!["test".into(), "debug_assertions".into()], env: BTreeMap::new(), target: "x86_64-unknown-linux-gnu".into(), crate_type: "proc_macro".into(), }, CrateSpec { aliases: BTreeMap::new(), crate_id: "ID-myproc_macro.rs".into(), display_name: "myproc_macro".into(), edition: "2018".into(), root_module: "myproc_macro.rs".into(), is_workspace_member: true, deps: BTreeSet::new(), proc_macro_dylib_path: Some( "bazel-out/k8-fastbuild/bin/myproc_macro/libmyproc_macro-12345.so".into(), ), source: None, cfg: vec!["test".into(), "debug_assertions".into()], env: BTreeMap::new(), target: "x86_64-unknown-linux-gnu".into(), crate_type: "proc_macro".into(), }, ]; for perm in crate_specs.into_iter().permutations(2) { assert_eq!( consolidate_crate_specs(perm).unwrap(), BTreeSet::from([CrateSpec { aliases: BTreeMap::new(), crate_id: "ID-myproc_macro.rs".into(), display_name: "myproc_macro".into(), edition: "2018".into(), root_module: "myproc_macro.rs".into(), is_workspace_member: true, deps: BTreeSet::new(), proc_macro_dylib_path: Some( "bazel-out/k8-opt-exec-F005BA11/bin/myproc_macro/libmyproc_macro-12345.so" .into() ), source: None, cfg: vec!["test".into(), "debug_assertions".into()], env: BTreeMap::new(), target: "x86_64-unknown-linux-gnu".into(), crate_type: "proc_macro".into(), },]) ); } } }