diff --git a/MODULE.bazel b/MODULE.bazel index cb20895546..e6028e3731 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -43,6 +43,7 @@ internal_deps = use_extension("//rust/private:internal_extensions.bzl", "i") use_repo( internal_deps, "rrra__anyhow-1.0.71", + "rrra__camino-1.1.9", "rrra__clap-4.3.11", "rrra__env_logger-0.10.0", "rrra__itertools-0.11.0", diff --git a/extensions/prost/private/prost.bzl b/extensions/prost/private/prost.bzl index 1a87d6c187..35eac981d9 100644 --- a/extensions/prost/private/prost.bzl +++ b/extensions/prost/private/prost.bzl @@ -271,7 +271,9 @@ def _rust_prost_aspect_impl(target, ctx): env = dep_variant_info.crate_info.rustc_env, deps = depset([dep.id for dep in rust_analyzer_deps]).to_list(), crate_specs = depset(transitive = [dep.crate_specs for dep in rust_analyzer_deps]), - proc_macro_dylib_path = None, + proc_macro_dylibs = depset(transitive = [dep.proc_macro_dylibs for dep in rust_analyzer_deps]), + build_info_out_dirs = depset(transitive = [dep.build_info_out_dirs for dep in rust_analyzer_deps]), + proc_macro_dylib = None, build_info = dep_variant_info.build_info, )) @@ -354,6 +356,8 @@ def _rust_prost_library_impl(ctx): ), RustAnalyzerGroupInfo( crate_specs = proto_dep[RustAnalyzerInfo].crate_specs, + proc_macro_dylibs = proto_dep[RustAnalyzerInfo].proc_macro_dylibs, + build_script_out_dirs = proto_dep[RustAnalyzerInfo].build_script_out_dirs, deps = proto_dep[RustAnalyzerInfo].deps, ), ] diff --git a/rust/private/providers.bzl b/rust/private/providers.bzl index 0482cab7e8..c9530f1a3b 100644 --- a/rust/private/providers.bzl +++ b/rust/private/providers.bzl @@ -162,10 +162,12 @@ RustAnalyzerInfo = provider( "cfgs": "List[String]: features or other compilation `--cfg` settings", "crate": "CrateInfo: Crate information.", "crate_specs": "Depset[File]: transitive closure of OutputGroupInfo files", + "proc_macro_dylibs": "Depset[File]: transitive closure of OutputGroupInfo files", + "build_info_out_dirs": "Depset[File]: transitive closure of OutputGroupInfo files", "deps": "List[String]: IDs of direct dependency crates", "env": "Dict[String: String]: Environment variables, used for the `env!` macro", "id": "String: Arbitrary unique ID for this crate", - "proc_macro_dylib_path": "File: compiled shared library output of proc-macro rule", + "proc_macro_dylib": "File: compiled shared library output of proc-macro rule", }, ) @@ -173,6 +175,8 @@ RustAnalyzerGroupInfo = provider( doc = "RustAnalyzerGroupInfo holds multiple RustAnalyzerInfos", fields = { "crate_specs": "Depset[File]: transitive closure of OutputGroupInfo files", + "proc_macro_dylibs": "Depset[File]: transitive closure of OutputGroupInfo files", + "build_info_out_dirs": "Depset[File]: transitive closure of OutputGroupInfo files", "deps": "List[String]: crate IDs of direct dependencies", }, ) diff --git a/rust/private/rust_analyzer.bzl b/rust/private/rust_analyzer.bzl index 6673a454e5..aa1f627f76 100644 --- a/rust/private/rust_analyzer.bzl +++ b/rust/private/rust_analyzer.bzl @@ -45,6 +45,8 @@ def write_rust_analyzer_spec_file(ctx, attrs, owner, base_info): RustAnalyzerInfo: Info with the embedded spec file. """ crate_spec = ctx.actions.declare_file("{}.rust_analyzer_crate_spec.json".format(owner.name)) + proc_macro_dylibs = [base_info.proc_macro_dylib] if base_info.proc_macro_dylib else None + build_info_out_dirs = [base_info.build_info.out_dir] if base_info.build_info != None and base_info.build_info.out_dir != None else None # Recreate the provider with the spec file embedded in it. rust_analyzer_info = RustAnalyzerInfo( @@ -55,7 +57,9 @@ def write_rust_analyzer_spec_file(ctx, attrs, owner, base_info): deps = base_info.deps, id = base_info.id, crate_specs = depset(direct = [crate_spec], transitive = [base_info.crate_specs]), - proc_macro_dylib_path = base_info.proc_macro_dylib_path, + proc_macro_dylibs = depset(direct = proc_macro_dylibs, transitive = [base_info.proc_macro_dylibs]), + build_info_out_dirs = depset(direct = build_info_out_dirs, transitive = [base_info.build_info_out_dirs]), + proc_macro_dylib = base_info.proc_macro_dylib, build_info = base_info.build_info, ) @@ -101,6 +105,8 @@ def _rust_analyzer_aspect_impl(target, ctx): # Gather required info from dependencies. label_to_id = {} # {Label of dependency => crate_id} crate_specs = [] # [depset of File - transitive crate_spec.json files] + proc_macro_dylibs = [] # [depset of File - transitive crate_spec.json files] + build_script_out_dirs = [] # [depset of File - transitive crate_spec.json files] attrs = ctx.rule.attr all_deps = getattr(attrs, "deps", []) + getattr(attrs, "proc_macro_deps", []) + \ [dep for dep in [getattr(attrs, "crate", None), getattr(attrs, "actual", None)] if dep != None] @@ -108,16 +114,27 @@ def _rust_analyzer_aspect_impl(target, ctx): if RustAnalyzerInfo in dep: label_to_id[dep.label] = dep[RustAnalyzerInfo].id crate_specs.append(dep[RustAnalyzerInfo].crate_specs) + proc_macro_dylibs.append(dep[RustAnalyzerInfo].proc_macro_dylibs) + build_script_out_dirs.append(dep[RustAnalyzerInfo].build_info_out_dirs) if RustAnalyzerGroupInfo in dep: for expanded_dep in dep[RustAnalyzerGroupInfo].deps: label_to_id[expanded_dep] = expanded_dep crate_specs.append(dep[RustAnalyzerGroupInfo].crate_specs) + proc_macro_dylibs.append(dep[RustAnalyzerGroupInfo].proc_macro_dylibs) + build_script_out_dirs.append(dep[RustAnalyzerGroupInfo].build_info_out_dirs) deps = label_to_id.values() crate_specs = depset(transitive = crate_specs) + proc_macro_dylibs = depset(transitive = proc_macro_dylibs) + build_script_out_dirs = depset(transitive = build_script_out_dirs) if rust_common.crate_group_info in target: - return [RustAnalyzerGroupInfo(deps = deps, crate_specs = crate_specs)] + return [RustAnalyzerGroupInfo( + deps = deps, + crate_specs = crate_specs, + proc_macro_dylibs = proc_macro_dylibs, + build_info_out_dirs = build_script_out_dirs, + )] elif rust_common.crate_info in target: crate_info = target[rust_common.crate_info] elif rust_common.test_crate_info in target: @@ -134,6 +151,8 @@ def _rust_analyzer_aspect_impl(target, ctx): # An arbitrary unique and stable identifier. crate_id = "ID-" + crate_info.root.path + proc_macro_dylib = find_proc_macro_dylib(toolchain, target) + rust_analyzer_info = write_rust_analyzer_spec_file(ctx, ctx.rule.attr, ctx.label, RustAnalyzerInfo( id = crate_id, aliases = aliases, @@ -142,23 +161,29 @@ def _rust_analyzer_aspect_impl(target, ctx): env = crate_info.rustc_env, deps = deps, crate_specs = crate_specs, - proc_macro_dylib_path = find_proc_macro_dylib_path(toolchain, target), + proc_macro_dylibs = proc_macro_dylibs, + build_info_out_dirs = build_script_out_dirs, + proc_macro_dylib = proc_macro_dylib, build_info = build_info, )) return [ rust_analyzer_info, - OutputGroupInfo(rust_analyzer_crate_spec = rust_analyzer_info.crate_specs), + OutputGroupInfo( + rust_analyzer_crate_spec = rust_analyzer_info.crate_specs, + rust_analyzer_proc_macro_dylibs = rust_analyzer_info.proc_macro_dylibs, + rust_analyzer_build_info_out_dirs = rust_analyzer_info.build_info_out_dirs, + ), ] -def find_proc_macro_dylib_path(toolchain, target): - """Find the proc_macro_dylib_path of target. Returns None if target crate is not type proc-macro. +def find_proc_macro_dylib(toolchain, target): + """Find the proc_macro_dylib of target. Returns None if target crate is not type proc-macro. Args: toolchain: The current rust toolchain. target: The current target. Returns: - (path): The path to the proc macro dylib, or None if this crate is not a proc-macro. + (File): The path to the proc macro dylib, or None if this crate is not a proc-macro. """ if rust_common.crate_info in target: crate_info = target[rust_common.crate_info] @@ -174,7 +199,7 @@ def find_proc_macro_dylib_path(toolchain, target): for action in target.actions: for output in action.outputs.to_list(): if output.extension == dylib_ext[1:]: - return output.path + return output # Failed to find the dylib path inside a proc-macro crate. # TODO: Should this be an error? @@ -188,7 +213,7 @@ rust_analyzer_aspect = aspect( ) # Paths in the generated JSON file begin with one of these placeholders. -# The gen_rust_project driver will replace them with absolute paths. +# The `rust-analyzer` driver will replace them with absolute paths. _WORKSPACE_TEMPLATE = "__WORKSPACE__/" _EXEC_ROOT_TEMPLATE = "__EXEC_ROOT__/" _OUTPUT_BASE_TEMPLATE = "__OUTPUT_BASE__/" @@ -211,6 +236,7 @@ def _create_single_crate(ctx, attrs, info): crate["edition"] = info.crate.edition crate["env"] = {} crate["crate_type"] = info.crate.type + crate["is_test"] = info.crate.is_test # Switch on external/ to determine if crates are in the workspace or remote. # TODO: Some folks may want to override this for vendored dependencies. @@ -221,6 +247,13 @@ def _create_single_crate(ctx, attrs, info): crate["root_module"] = path_prefix + info.crate.root.path crate["source"] = {"exclude_dirs": [], "include_dirs": []} + # Store build system related info only for local crates + if not is_external and not is_generated: + crate["build"] = { + "label": ctx.label.package + ":" + ctx.label.name, + "build_file": _WORKSPACE_TEMPLATE + ctx.build_file_path, + } + if is_generated: srcs = getattr(ctx.rule.files, "srcs", []) src_map = {src.short_path: src for src in srcs if src.is_source} @@ -259,8 +292,8 @@ def _create_single_crate(ctx, attrs, info): crate["cfg"] = info.cfgs toolchain = find_toolchain(ctx) crate["target"] = (_EXEC_ROOT_TEMPLATE + toolchain.target_json.path) if toolchain.target_json else toolchain.target_flag_value - if info.proc_macro_dylib_path != None: - crate["proc_macro_dylib_path"] = _EXEC_ROOT_TEMPLATE + info.proc_macro_dylib_path + if info.proc_macro_dylib != None: + crate["proc_macro_dylib_path"] = _EXEC_ROOT_TEMPLATE + info.proc_macro_dylib.path return crate def _rust_analyzer_toolchain_impl(ctx): diff --git a/tools/rust_analyzer/3rdparty/BUILD.bazel b/tools/rust_analyzer/3rdparty/BUILD.bazel index 9739042dbc..031fcf6f10 100644 --- a/tools/rust_analyzer/3rdparty/BUILD.bazel +++ b/tools/rust_analyzer/3rdparty/BUILD.bazel @@ -16,6 +16,10 @@ crates_vendor( ], version = "4.3.11", ), + "camino": crate.spec( + features = ["serde1"], + version = "1.1.9", + ), "env_logger": crate.spec( version = "0.10.0", ), diff --git a/tools/rust_analyzer/3rdparty/Cargo.Bazel.lock b/tools/rust_analyzer/3rdparty/Cargo.Bazel.lock index ab1c359e8b..feef41fd2b 100644 --- a/tools/rust_analyzer/3rdparty/Cargo.Bazel.lock +++ b/tools/rust_analyzer/3rdparty/Cargo.Bazel.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "aho-corasick" @@ -72,6 +72,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] + [[package]] name = "cc" version = "1.0.79" @@ -130,6 +139,7 @@ name = "direct-cargo-bazel-deps" version = "0.0.1" dependencies = [ "anyhow", + "camino", "clap", "env_logger", "itertools", diff --git a/tools/rust_analyzer/3rdparty/crates/BUILD.bazel b/tools/rust_analyzer/3rdparty/crates/BUILD.bazel index 690e0f8796..a0128924ef 100644 --- a/tools/rust_analyzer/3rdparty/crates/BUILD.bazel +++ b/tools/rust_analyzer/3rdparty/crates/BUILD.bazel @@ -37,6 +37,12 @@ alias( tags = ["manual"], ) +alias( + name = "camino", + actual = "@rrra__camino-1.1.9//:camino", + tags = ["manual"], +) + alias( name = "clap", actual = "@rrra__clap-4.3.11//:clap", diff --git a/tools/rust_analyzer/3rdparty/crates/BUILD.camino-1.1.9.bazel b/tools/rust_analyzer/3rdparty/crates/BUILD.camino-1.1.9.bazel new file mode 100644 index 0000000000..336c77bce5 --- /dev/null +++ b/tools/rust_analyzer/3rdparty/crates/BUILD.camino-1.1.9.bazel @@ -0,0 +1,133 @@ +############################################################################### +# @generated +# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To +# regenerate this file, run the following: +# +# bazel run @//tools/rust_analyzer/3rdparty:crates_vendor +############################################################################### + +load("@rules_rust//cargo:defs.bzl", "cargo_build_script") +load("@rules_rust//rust:defs.bzl", "rust_library") + +package(default_visibility = ["//visibility:public"]) + +rust_library( + name = "camino", + srcs = glob( + include = ["**/*.rs"], + allow_empty = True, + ), + compile_data = glob( + include = ["**"], + allow_empty = True, + exclude = [ + "**/* *", + ".tmp_git_root/**/*", + "BUILD", + "BUILD.bazel", + "WORKSPACE", + "WORKSPACE.bazel", + ], + ), + crate_features = [ + "serde", + "serde1", + ], + crate_root = "src/lib.rs", + edition = "2018", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-bazel", + "crate-name=camino", + "manual", + "noclippy", + "norustfmt", + ], + target_compatible_with = select({ + "@rules_rust//rust/platform:aarch64-apple-darwin": [], + "@rules_rust//rust/platform:aarch64-pc-windows-msvc": [], + "@rules_rust//rust/platform:aarch64-unknown-linux-gnu": [], + "@rules_rust//rust/platform:aarch64-unknown-nixos-gnu": [], + "@rules_rust//rust/platform:arm-unknown-linux-gnueabi": [], + "@rules_rust//rust/platform:armv7-linux-androideabi": [], + "@rules_rust//rust/platform:armv7-unknown-linux-gnueabi": [], + "@rules_rust//rust/platform:i686-apple-darwin": [], + "@rules_rust//rust/platform:i686-pc-windows-msvc": [], + "@rules_rust//rust/platform:i686-unknown-freebsd": [], + "@rules_rust//rust/platform:i686-unknown-linux-gnu": [], + "@rules_rust//rust/platform:powerpc-unknown-linux-gnu": [], + "@rules_rust//rust/platform:s390x-unknown-linux-gnu": [], + "@rules_rust//rust/platform:x86_64-apple-darwin": [], + "@rules_rust//rust/platform:x86_64-pc-windows-msvc": [], + "@rules_rust//rust/platform:x86_64-unknown-freebsd": [], + "@rules_rust//rust/platform:x86_64-unknown-linux-gnu": [], + "@rules_rust//rust/platform:x86_64-unknown-nixos-gnu": [], + "//conditions:default": ["@platforms//:incompatible"], + }), + version = "1.1.9", + deps = [ + "@rrra__camino-1.1.9//:build_script_build", + "@rrra__serde-1.0.171//:serde", + ], +) + +cargo_build_script( + name = "_bs", + srcs = glob( + include = ["**/*.rs"], + allow_empty = True, + ), + compile_data = glob( + include = ["**"], + allow_empty = True, + exclude = [ + "**/* *", + "**/*.rs", + ".tmp_git_root/**/*", + "BUILD", + "BUILD.bazel", + "WORKSPACE", + "WORKSPACE.bazel", + ], + ), + crate_features = [ + "serde", + "serde1", + ], + crate_name = "build_script_build", + crate_root = "build.rs", + data = glob( + include = ["**"], + allow_empty = True, + exclude = [ + "**/* *", + ".tmp_git_root/**/*", + "BUILD", + "BUILD.bazel", + "WORKSPACE", + "WORKSPACE.bazel", + ], + ), + edition = "2018", + pkg_name = "camino", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-bazel", + "crate-name=camino", + "manual", + "noclippy", + "norustfmt", + ], + version = "1.1.9", + visibility = ["//visibility:private"], +) + +alias( + name = "build_script_build", + actual = ":_bs", + tags = ["manual"], +) diff --git a/tools/rust_analyzer/3rdparty/crates/defs.bzl b/tools/rust_analyzer/3rdparty/crates/defs.bzl index 68dbebc053..e4aa564512 100644 --- a/tools/rust_analyzer/3rdparty/crates/defs.bzl +++ b/tools/rust_analyzer/3rdparty/crates/defs.bzl @@ -296,6 +296,7 @@ _NORMAL_DEPENDENCIES = { "": { _COMMON_CONDITION: { "anyhow": Label("@rrra__anyhow-1.0.71//:anyhow"), + "camino": Label("@rrra__camino-1.1.9//:camino"), "clap": Label("@rrra__clap-4.3.11//:clap"), "env_logger": Label("@rrra__env_logger-0.10.0//:env_logger"), "itertools": Label("@rrra__itertools-0.11.0//:itertools"), @@ -490,6 +491,16 @@ def crate_repositories(): build_file = Label("//tools/rust_analyzer/3rdparty/crates:BUILD.bitflags-1.3.2.bazel"), ) + maybe( + http_archive, + name = "rrra__camino-1.1.9", + sha256 = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3", + type = "tar.gz", + urls = ["https://static.crates.io/crates/camino/1.1.9/download"], + strip_prefix = "camino-1.1.9", + build_file = Label("@rules_rust//tools/rust_analyzer/3rdparty/crates:BUILD.camino-1.1.9.bazel"), + ) + maybe( http_archive, name = "rrra__cc-1.0.79", @@ -992,6 +1003,7 @@ def crate_repositories(): return [ struct(repo = "rrra__anyhow-1.0.71", is_dev_dep = False), + struct(repo = "rrra__camino-1.1.9", is_dev_dep = False), struct(repo = "rrra__clap-4.3.11", is_dev_dep = False), struct(repo = "rrra__env_logger-0.10.0", is_dev_dep = False), struct(repo = "rrra__itertools-0.11.0", is_dev_dep = False), diff --git a/tools/rust_analyzer/BUILD.bazel b/tools/rust_analyzer/BUILD.bazel index 54f5436348..9aa86ff69e 100644 --- a/tools/rust_analyzer/BUILD.bazel +++ b/tools/rust_analyzer/BUILD.bazel @@ -2,9 +2,28 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library") load("//rust:defs.bzl", "rust_binary", "rust_clippy", "rust_library", "rust_test") load("//tools:tool_utils.bzl", "aspect_repository") +rust_binary( + name = "discover_rust_project", + srcs = ["bin/discover_rust_project.rs"], + edition = "2018", + rustc_env = { + "ASPECT_REPOSITORY": aspect_repository(), + }, + 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", + "//util/label", + ], +) + rust_binary( name = "gen_rust_project", - srcs = ["main.rs"], + srcs = ["bin/gen_rust_project.rs"], edition = "2018", rustc_env = { "ASPECT_REPOSITORY": aspect_repository(), @@ -13,6 +32,7 @@ rust_binary( 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", @@ -24,7 +44,7 @@ rust_library( name = "gen_rust_project_lib", srcs = glob( ["**/*.rs"], - exclude = ["main.rs"], + exclude = ["bin"], ), data = [ "//rust/private:rust_analyzer_detect_sysroot", @@ -33,6 +53,8 @@ rust_library( deps = [ "//tools/runfiles", "//tools/rust_analyzer/3rdparty/crates:anyhow", + "//tools/rust_analyzer/3rdparty/crates:camino", + "//tools/rust_analyzer/3rdparty/crates:clap", "//tools/rust_analyzer/3rdparty/crates:log", "//tools/rust_analyzer/3rdparty/crates:serde", "//tools/rust_analyzer/3rdparty/crates:serde_json", diff --git a/tools/rust_analyzer/aquery.rs b/tools/rust_analyzer/aquery.rs index bc98913b05..f3f96dd7aa 100644 --- a/tools/rust_analyzer/aquery.rs +++ b/tools/rust_analyzer/aquery.rs @@ -1,10 +1,9 @@ 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 camino::{Utf8Path, Utf8PathBuf}; use serde::Deserialize; #[derive(Debug, Deserialize)] @@ -51,7 +50,16 @@ pub struct CrateSpec { pub cfg: Vec, pub env: BTreeMap, pub target: String, - pub crate_type: String, + pub crate_type: CrateType, + pub is_test: bool, + pub build: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct CrateSpecBuild { + pub label: String, + pub build_file: Utf8PathBuf, } #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)] @@ -61,22 +69,39 @@ pub struct CrateSpecSource { pub include_dirs: Vec, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum CrateType { + Bin, + Rlib, + Lib, + Dylib, + Cdylib, + Staticlib, + ProcMacro, +} + pub fn get_crate_specs( - bazel: &Path, - workspace: &Path, - execution_root: &Path, + bazel: &Utf8Path, + output_base: &Utf8Path, + workspace: &Utf8Path, + execution_root: &Utf8Path, + config_group: Option<&str>, targets: &[String], rules_rust_name: &str, ) -> anyhow::Result> { + log::info!("running bazel aquery..."); log::debug!("Get crate specs with targets: {:?}", targets); let target_pattern = format!("deps({})", targets.join("+")); - let aquery_output = Command::new(bazel) + let output = Command::new(bazel) .current_dir(workspace) .env_remove("BAZELISK_SKIP_WRAPPER") .env_remove("BUILD_WORKING_DIRECTORY") .env_remove("BUILD_WORKSPACE_DIRECTORY") + .arg(format!("--output_base={output_base}")) .arg("aquery") + .args(config_group.map(|s| format!("--config={s}"))) .arg("--include_aspects") .arg("--include_artifacts") .arg(format!( @@ -89,16 +114,18 @@ pub fn get_crate_specs( .arg("--output=jsonproto") .output()?; + log::debug!("{}", String::from_utf8_lossy(&output.stderr)); + log::info!("bazel aquery finished; parsing spec files..."); + let crate_spec_files = - parse_aquery_output_files(execution_root, &String::from_utf8(aquery_output.stdout)?)?; + parse_aquery_output_files(execution_root, &String::from_utf8(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()))?; + let f = File::open(&file).with_context(|| format!("Failed to open file: {}", file))?; serde_json::from_reader(f) - .with_context(|| format!("Failed to deserialize file: {}", file.display())) + .with_context(|| format!("Failed to deserialize file: {}", file)) }) .collect::>>()?; @@ -106,9 +133,9 @@ pub fn get_crate_specs( } fn parse_aquery_output_files( - execution_root: &Path, + execution_root: &Utf8Path, aquery_stdout: &str, -) -> anyhow::Result> { +) -> 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) { @@ -133,7 +160,7 @@ fn parse_aquery_output_files( .map(|pf| (pf.id, pf)) .collect::>(); - let mut output_files: Vec = Vec::new(); + let mut output_files: Vec = Vec::new(); for action in out.actions { for output_id in action.output_ids { let artifact = artifacts @@ -155,15 +182,15 @@ fn parse_aquery_output_files( fn path_from_fragments( id: u32, fragments: &BTreeMap, -) -> anyhow::Result { +) -> 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()), + .join(Utf8PathBuf::from(&path_fragment.label.clone())), + None => Utf8PathBuf::from(&path_fragment.label.clone()), }; Ok(buf) @@ -177,17 +204,35 @@ fn consolidate_crate_specs(crate_specs: Vec) -> anyhow::Result, +} + +fn project_discovery() -> anyhow::Result<()> { + let Config { + workspace, + execution_root, + output_base, + bazel, + config_group, + specific, + } = Config::parse_and_refine()?; + + let DiscoverProjectArgs { + default_buildfile, + rust_analyzer_argument, + } = specific; + + let ra_arg = match rust_analyzer_argument { + Some(ra_arg) => ra_arg, + None => RustAnalyzerArg::Buildfile(workspace.join(default_buildfile)), + }; + + let rules_rust_name = env!("ASPECT_REPOSITORY"); + + log::info!("got rust-analyzer argument: {ra_arg}"); + + let (buildfile, targets) = + ra_arg.query_target_details(&bazel, &output_base, &workspace, config_group.as_deref())?; + + let targets = &[targets]; + log::debug!("got buildfile: {buildfile}"); + log::debug!("got targets: {targets:?}"); + + // Generate the crate specs. + generate_crate_info( + &bazel, + &output_base, + &workspace, + config_group.as_deref(), + rules_rust_name, + targets, + )?; + + // Use the generated files to print the rust-project.json. + discover_rust_project( + &bazel, + &output_base, + &workspace, + &execution_root, + config_group.as_deref(), + rules_rust_name, + targets, + buildfile, + ) +} + +fn main() { + // Treat logs as progress messages. + env_logger::Builder::from_default_env() + // Never write color/styling info + .write_style(WriteStyle::Never) + // Format logs as progress messages + .format(|fmt, rec| writeln!(fmt, "{}", discovery_progress(rec.args().to_string()))) + // `rust-analyzer` reads the stdout + .filter_level(LevelFilter::Debug) + .target(Target::Stdout) + .init(); + + if let Err(e) = project_discovery() { + discovery_failure(e); + } +} diff --git a/tools/rust_analyzer/bin/gen_rust_project.rs b/tools/rust_analyzer/bin/gen_rust_project.rs new file mode 100644 index 0000000000..6a821a6fcd --- /dev/null +++ b/tools/rust_analyzer/bin/gen_rust_project.rs @@ -0,0 +1,55 @@ +use std::env; + +use clap::Args; +use gen_rust_project_lib::{generate_crate_info, write_rust_project, Config}; + +#[derive(Debug, Args)] +struct GenerateProjectArgs { + /// Space separated list of target patterns that comes after all other args. + #[clap(default_value = "@//...")] + targets: Vec, +} + +// TODO(david): This shells out to an expected rule in the workspace root //:rust_analyzer that the user must define. +// It would be more convenient if it could automatically discover all the rust code in the workspace if this target +// does not exist. +fn main() -> anyhow::Result<()> { + env_logger::init(); + + let Config { + workspace, + execution_root, + output_base, + bazel, + config_group, + specific, + } = Config::parse_and_refine()?; + + let GenerateProjectArgs { targets } = specific; + + let rules_rust_name = env!("ASPECT_REPOSITORY"); + + // Generate the crate specs. + generate_crate_info( + &bazel, + &output_base, + &workspace, + config_group.as_deref(), + rules_rust_name, + &targets, + )?; + + // Use the generated files to write rust-project.json. + write_rust_project( + &bazel, + &output_base, + &workspace, + &execution_root, + config_group.as_deref(), + rules_rust_name, + &targets, + &workspace.join("rust-project.json"), + )?; + + Ok(()) +} diff --git a/tools/rust_analyzer/config.rs b/tools/rust_analyzer/config.rs new file mode 100644 index 0000000000..1572672e29 --- /dev/null +++ b/tools/rust_analyzer/config.rs @@ -0,0 +1,138 @@ +use std::process::Command; + +use anyhow::bail; +use camino::{Utf8Path, Utf8PathBuf}; +use clap::{Args, Parser}; + +#[derive(Debug)] +pub struct Config +where + T: Args, +{ + /// The path to the Bazel workspace directory. If not specified, uses the result of `bazel info workspace`. + pub workspace: Utf8PathBuf, + + /// The path to the Bazel execution root. If not specified, uses the result of `bazel info execution_root`. + pub execution_root: Utf8PathBuf, + + /// The path to the Bazel output user root. If not specified, uses the result of `bazel info output_base`. + pub output_base: Utf8PathBuf, + + /// The path to a Bazel binary + pub bazel: Utf8PathBuf, + + /// A `--config` directive that gets passed to Bazel to be able to pass custom configurations. + pub config_group: Option, + + /// Binary specific config options + pub specific: T, +} + +impl Config +where + T: Args, +{ + // Parse the configuration flags and supplement with bazel info as needed. + pub fn parse_and_refine() -> anyhow::Result { + let ConfigParser { + mut workspace, + mut execution_root, + mut output_base, + bazel, + config_group, + specific, + } = ConfigParser::parse(); + + if workspace.is_some() && execution_root.is_some() && output_base.is_some() { + return Ok(Config { + workspace: workspace.unwrap(), + execution_root: execution_root.unwrap(), + output_base: output_base.unwrap(), + bazel, + config_group, + specific, + }); + } + + // We need some info from `bazel info`. Fetch it now. + let mut bazel_info_command = Command::new(&bazel); + + // Execute bazel info. + let output = bazel_info_command + // Switch to the workspace directory if one was provided. + .current_dir(workspace.as_deref().unwrap_or(Utf8Path::new("."))) + .env_remove("BAZELISK_SKIP_WRAPPER") + .env_remove("BUILD_WORKING_DIRECTORY") + .env_remove("BUILD_WORKSPACE_DIRECTORY") + // Set the output_base if one was provided. + .args(output_base.as_ref().map(|s| format!("--output_base={s}"))) + .arg("info") + .args(config_group.as_ref().map(|s| format!("--config={s}"))) + .output()?; + + if !output.status.success() { + let status = output.status; + let stderr = String::from_utf8_lossy(&output.stderr); + bail!("Failed to run `bazel info` ({status:?}): {stderr}"); + } + + // Extract the output. + let output = String::from_utf8(output.stdout)?; + + let iter = output + .trim() + .split('\n') + .filter_map(|line| line.split_once(':')) + .map(|(k, v)| (k, v.trim())); + + for (k, v) in iter { + match k { + "workspace" => workspace = Some(v.into()), + "execution_root" => execution_root = Some(v.into()), + "output_base" => output_base = Some(v.into()), + _ => continue, + } + } + + let config = Config { + workspace: workspace.expect("'workspace' must exist in bazel info"), + execution_root: execution_root.expect("'execution_root' must exist in bazel info"), + output_base: output_base.expect("'output_base' must exist in bazel info"), + bazel, + config_group, + specific, + }; + + Ok(config) + } +} + +#[derive(Debug, Parser)] +struct ConfigParser +where + T: Args, +{ + /// The path to the Bazel workspace directory. If not specified, uses the result of `bazel info workspace`. + #[clap(long, env = "BUILD_WORKSPACE_DIRECTORY")] + workspace: Option, + + /// The path to the Bazel execution root. If not specified, uses the result of `bazel info execution_root`. + #[clap(long)] + execution_root: Option, + + /// The path to the Bazel output user root. If not specified, uses the result of `bazel info output_base`. + #[clap(long, env = "OUTPUT_BASE")] + output_base: Option, + + /// The path to a Bazel binary + #[clap(long, default_value = "bazel")] + bazel: Utf8PathBuf, + + /// A `--config` directive that gets passed to Bazel to be able to pass custom configurations. + #[clap(long)] + config_group: Option, + + /// Binary specific config options + #[command(flatten)] + specific: T, +} diff --git a/tools/rust_analyzer/lib.rs b/tools/rust_analyzer/lib.rs index 7a6eb28d64..aa932256f3 100644 --- a/tools/rust_analyzer/lib.rs +++ b/tools/rust_analyzer/lib.rs @@ -1,62 +1,153 @@ +mod aquery; +mod config; +mod ra_arg; +mod rust_project; + use std::collections::HashMap; -use std::path::Path; use std::process::Command; -use anyhow::anyhow; +use anyhow::bail; +use camino::{Utf8Path, Utf8PathBuf}; +pub use config::Config; +pub use ra_arg::RustAnalyzerArg; use runfiles::Runfiles; - -mod aquery; -mod rust_project; +use rust_project::{normalize_project_string, DiscoverProject, RustProject}; pub fn generate_crate_info( - bazel: impl AsRef, - workspace: impl AsRef, - rules_rust: impl AsRef, + bazel: &Utf8Path, + output_base: &Utf8Path, + workspace: &Utf8Path, + config_group: Option<&str>, + rules_rust: &str, targets: &[String], ) -> anyhow::Result<()> { + log::info!("running bazel build..."); log::debug!("Building rust_analyzer_crate_spec files for {:?}", targets); - let output = Command::new(bazel.as_ref()) - .current_dir(workspace.as_ref()) + let output = Command::new(bazel) + .current_dir(workspace) .env_remove("BAZELISK_SKIP_WRAPPER") .env_remove("BUILD_WORKING_DIRECTORY") .env_remove("BUILD_WORKSPACE_DIRECTORY") + .arg(format!("--output_base={output_base}")) .arg("build") + .args(config_group.map(|s| format!("--config={s}"))) .arg("--norun_validations") .arg(format!( - "--aspects={}//rust:defs.bzl%rust_analyzer_aspect", - rules_rust.as_ref() + "--aspects={rules_rust}//rust:defs.bzl%rust_analyzer_aspect" )) - .arg("--output_groups=rust_analyzer_crate_spec,rust_generated_srcs") + .arg("--output_groups=rust_analyzer_crate_spec,rust_generated_srcs,rust_analyzer_proc_macro_dylibs,rust_analyzer_build_info_out_dirs") .args(targets) .output()?; if !output.status.success() { - return Err(anyhow!( + bail!( "bazel build failed:({})\n{}", output.status, String::from_utf8_lossy(&output.stderr) - )); + ); } + log::info!("bazel build finished"); + + Ok(()) +} + +pub fn discover_rust_project( + bazel: &Utf8Path, + output_base: &Utf8Path, + workspace: &Utf8Path, + execution_root: &Utf8Path, + config_group: Option<&str>, + rules_rust_name: &str, + targets: &[String], + buildfile: Utf8PathBuf, +) -> anyhow::Result<()> { + let project = generate_rust_project( + bazel, + output_base, + workspace, + execution_root, + config_group, + rules_rust_name, + targets, + )?; + + let discovery = DiscoverProject::Finished { buildfile, project }; + let discovery_str = serde_json::to_string(&discovery)?; + let discovery_str = + normalize_project_string(&discovery_str, workspace, output_base, execution_root); + + println!("{discovery_str}"); + Ok(()) } +/// Log formatting function that generates and writes a [`DiscoverProject::Progress`] +/// message which `rust-analyzer` can display. +pub fn discovery_progress(message: String) -> String { + let discovery = DiscoverProject::Progress { message }; + serde_json::to_string(&discovery).expect("serializable message") +} + +pub fn discovery_failure(error: anyhow::Error) { + let discovery = DiscoverProject::Error { + error: format!("could not generate rust-project.json: {error}"), + source: error.source().as_ref().map(ToString::to_string), + }; + + let discovery_str = serde_json::to_string(&discovery).expect("serializable error"); + println!("{discovery_str}"); +} + pub fn write_rust_project( - bazel: impl AsRef, - workspace: impl AsRef, - rules_rust_name: &impl AsRef, + bazel: &Utf8Path, + output_base: &Utf8Path, + workspace: &Utf8Path, + execution_root: &Utf8Path, + config_group: Option<&str>, + rules_rust_name: &str, targets: &[String], - execution_root: impl AsRef, - output_base: impl AsRef, - rust_project_path: impl AsRef, + rust_project_path: &Utf8Path, ) -> anyhow::Result<()> { + let rust_project = generate_rust_project( + bazel, + output_base, + workspace, + execution_root, + config_group, + rules_rust_name, + targets, + )?; + + rust_project::write_rust_project( + rust_project_path, + output_base, + workspace, + execution_root, + &rust_project, + )?; + + Ok(()) +} + +fn generate_rust_project( + bazel: &Utf8Path, + output_base: &Utf8Path, + workspace: &Utf8Path, + execution_root: &Utf8Path, + config_group: Option<&str>, + rules_rust_name: &str, + targets: &[String], +) -> anyhow::Result { let crate_specs = aquery::get_crate_specs( - bazel.as_ref(), - workspace.as_ref(), - execution_root.as_ref(), + bazel, + output_base, + workspace, + execution_root, + config_group, targets, - rules_rust_name.as_ref(), + rules_rust_name, )?; let path = runfiles::rlocation!( @@ -70,15 +161,5 @@ pub fn write_rust_project( let sysroot_src = &toolchain_info["sysroot_src"]; let sysroot = &toolchain_info["sysroot"]; - let rust_project = rust_project::generate_rust_project(sysroot, sysroot_src, &crate_specs)?; - - rust_project::write_rust_project( - rust_project_path.as_ref(), - workspace.as_ref(), - execution_root.as_ref(), - output_base.as_ref(), - &rust_project, - )?; - - Ok(()) + rust_project::generate_rust_project(bazel, workspace, sysroot, sysroot_src, &crate_specs) } diff --git a/tools/rust_analyzer/main.rs b/tools/rust_analyzer/main.rs deleted file mode 100644 index df4b2f9bde..0000000000 --- a/tools/rust_analyzer/main.rs +++ /dev/null @@ -1,130 +0,0 @@ -use std::collections::HashMap; -use std::env; -use std::path::PathBuf; -use std::process::Command; - -use anyhow::anyhow; -use clap::Parser; -use gen_rust_project_lib::generate_crate_info; -use gen_rust_project_lib::write_rust_project; - -// TODO(david): This shells out to an expected rule in the workspace root //:rust_analyzer that the user must define. -// It would be more convenient if it could automatically discover all the rust code in the workspace if this target -// does not exist. -fn main() -> anyhow::Result<()> { - env_logger::init(); - - let config = parse_config()?; - - let workspace_root = config - .workspace - .as_ref() - .expect("failed to find workspace root, set with --workspace"); - - let execution_root = config - .execution_root - .as_ref() - .expect("failed to find execution root, is --execution-root set correctly?"); - - let output_base = config - .output_base - .as_ref() - .expect("failed to find output base, is -output-base set correctly?"); - - let rules_rust_name = env!("ASPECT_REPOSITORY"); - - // Generate the crate specs. - generate_crate_info( - &config.bazel, - workspace_root, - rules_rust_name, - &config.targets, - )?; - - // Use the generated files to write rust-project.json. - write_rust_project( - &config.bazel, - workspace_root, - &rules_rust_name, - &config.targets, - execution_root, - output_base, - workspace_root.join("rust-project.json"), - )?; - - Ok(()) -} - -// Parse the configuration flags and supplement with bazel info as needed. -fn parse_config() -> anyhow::Result { - let mut config = Config::parse(); - - if config.workspace.is_some() && config.execution_root.is_some() { - return Ok(config); - } - - // We need some info from `bazel info`. Fetch it now. - let mut bazel_info_command = Command::new(&config.bazel); - bazel_info_command - .env_remove("BAZELISK_SKIP_WRAPPER") - .env_remove("BUILD_WORKING_DIRECTORY") - .env_remove("BUILD_WORKSPACE_DIRECTORY") - .arg("info"); - if let Some(workspace) = &config.workspace { - bazel_info_command.current_dir(workspace); - } - - // Execute bazel info. - let output = bazel_info_command.output()?; - if !output.status.success() { - return Err(anyhow!( - "Failed to run `bazel info` ({:?}): {}", - output.status, - String::from_utf8_lossy(&output.stderr) - )); - } - - // Extract the output. - let output = String::from_utf8_lossy(output.stdout.as_slice()); - let bazel_info = output - .trim() - .split('\n') - .map(|line| line.split_at(line.find(':').expect("missing `:` in bazel info output"))) - .map(|(k, v)| (k, (v[1..]).trim())) - .collect::>(); - - if config.workspace.is_none() { - config.workspace = bazel_info.get("workspace").map(Into::into); - } - if config.execution_root.is_none() { - config.execution_root = bazel_info.get("execution_root").map(Into::into); - } - if config.output_base.is_none() { - config.output_base = bazel_info.get("output_base").map(Into::into); - } - - Ok(config) -} - -#[derive(Debug, Parser)] -struct Config { - /// The path to the Bazel workspace directory. If not specified, uses the result of `bazel info workspace`. - #[clap(long, env = "BUILD_WORKSPACE_DIRECTORY")] - workspace: Option, - - /// The path to the Bazel execution root. If not specified, uses the result of `bazel info execution_root`. - #[clap(long)] - execution_root: Option, - - /// The path to the Bazel output user root. If not specified, uses the result of `bazel info output_base`. - #[clap(long, env = "OUTPUT_BASE")] - output_base: Option, - - /// The path to a Bazel binary - #[clap(long, default_value = "bazel")] - bazel: PathBuf, - - /// Space separated list of target patterns that comes after all other args. - #[clap(default_value = "@//...")] - targets: Vec, -} diff --git a/tools/rust_analyzer/ra_arg.rs b/tools/rust_analyzer/ra_arg.rs new file mode 100644 index 0000000000..8be63d985c --- /dev/null +++ b/tools/rust_analyzer/ra_arg.rs @@ -0,0 +1,129 @@ +use anyhow::{bail, Context}; +use camino::{Utf8Path, Utf8PathBuf}; +use serde::{Deserialize, Serialize}; +use std::convert::TryInto; +use std::fmt::Display; +use std::process::Command; +use std::str::FromStr; + +/// The argument that `rust-analyzer` can pass to the command. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum RustAnalyzerArg { + Path(Utf8PathBuf), + Buildfile(Utf8PathBuf), +} + +impl RustAnalyzerArg { + /// Consumes itself to return a build file and the targets to build. + pub fn query_target_details( + self, + bazel: &Utf8Path, + output_base: &Utf8Path, + workspace: &Utf8Path, + config_group: Option<&str>, + ) -> anyhow::Result<(Utf8PathBuf, String)> { + match self { + Self::Path(file) => { + let buildfile = query_buildfile_for_source_file( + bazel, + output_base, + workspace, + config_group, + &file, + )?; + buildfile_to_targets(workspace, &buildfile).map(|t| (buildfile, t)) + } + Self::Buildfile(buildfile) => { + buildfile_to_targets(workspace, &buildfile).map(|t| (buildfile, t)) + } + } + } +} + +impl Display for RustAnalyzerArg { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let arg = serde_json::to_string(self).map_err(|_| std::fmt::Error)?; + write!(f, "{arg}") + } +} + +impl FromStr for RustAnalyzerArg { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + serde_json::from_str(s).map_err(|e| anyhow::anyhow!("rust analyzer argument error: {e}")) + } +} + +/// `rust-analyzer` associates workspaces with buildfiles. Therefore, when it passes in a +/// source file path, we use this function to identify the buildfile the file belongs to. +fn query_buildfile_for_source_file( + bazel: &Utf8Path, + output_base: &Utf8Path, + workspace: &Utf8Path, + config_group: Option<&str>, + file: &Utf8Path, +) -> anyhow::Result { + log::info!("running bazel query on source file: {file}"); + + let stripped_file = file + .strip_prefix(workspace) + .with_context(|| format!("{file} not part of workspace"))?; + + let query_output = Command::new(bazel) + .current_dir(workspace) + .env_remove("BAZELISK_SKIP_WRAPPER") + .env_remove("BUILD_WORKING_DIRECTORY") + .env_remove("BUILD_WORKSPACE_DIRECTORY") + .arg(format!("--output_base={output_base}")) + .arg("query") + .args(config_group.map(|s| format!("--config={s}"))) + .arg("--output=package") + .arg(stripped_file) + .output() + .with_context(|| format!("failed to run bazel query for source file: {stripped_file}"))?; + + log::debug!("{}", String::from_utf8_lossy(&query_output.stderr)); + log::info!("bazel query for source file {file} finished"); + + let text = String::from_utf8(query_output.stdout)?; + let mut lines = text.lines(); + + let package = match lines.next() { + Some(package) if lines.next().is_none() => package, + // We were passed a Rust source file path. + // Technically, if the file is used in multiple packages + // this will error out. + // + // I don't think there's any valid reason for such a situation + // though, so the check here is more for error handling's sake. + Some(_) => bail!("multiple packages returned for {stripped_file}"), + None => bail!("no package found for {stripped_file}"), + }; + + for res in std::fs::read_dir(workspace.join(package))? { + let entry = res?; + if entry.file_name() == "BUILD.bazel" || entry.file_name() == "BUILD" { + return entry.path().try_into().map_err(From::from); + } + } + + bail!("no buildfile found for {file}"); +} + +fn buildfile_to_targets(workspace: &Utf8Path, buildfile: &Utf8Path) -> anyhow::Result { + log::info!("getting targets for buildfile: {buildfile}"); + + let parent_dir = buildfile + .strip_prefix(workspace) + .with_context(|| format!("{buildfile} not part of workspace"))? + .parent(); + + let targets = match parent_dir { + Some(p) if !p.as_str().is_empty() => format!("//{p}/..."), + _ => "//...".to_string(), + }; + + Ok(targets) +} diff --git a/tools/rust_analyzer/rust_project.rs b/tools/rust_analyzer/rust_project.rs index f694267379..7a0ffc398c 100644 --- a/tools/rust_analyzer/rust_project.rs +++ b/tools/rust_analyzer/rust_project.rs @@ -3,12 +3,30 @@ use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::io::ErrorKind; -use std::path::Path; use anyhow::anyhow; +use camino::{Utf8Path, Utf8PathBuf}; use serde::Serialize; -use crate::aquery::CrateSpec; +use crate::aquery::{CrateSpec, CrateType}; + +/// The format that rust_analyzer expects as a response when automatically invoked. +#[derive(Debug, Serialize)] +#[serde(tag = "kind")] +#[serde(rename_all = "snake_case")] +pub enum DiscoverProject { + Finished { + buildfile: Utf8PathBuf, + project: RustProject, + }, + Error { + error: String, + source: Option, + }, + Progress { + message: String, + }, +} /// A `rust-project.json` workspace representation. See /// [rust-analyzer documentation][rd] for a thorough description of this interface. @@ -27,6 +45,10 @@ pub struct RustProject { /// dependencies as well as sysroot crate (libstd, /// libcore and such). crates: Vec, + + /// The set of runnables, such as tests or benchmarks, + /// that can be found in the crate. + runnables: Vec, } /// A `rust-project.json` crate representation. See @@ -74,6 +96,10 @@ pub struct Crate { /// For proc-macro crates, path to compiled proc-macro (.so file). #[serde(skip_serializing_if = "Option::is_none")] proc_macro_dylib_path: Option, + + /// Build information for the crate + #[serde(skip_serializing_if = "Option::is_none")] + build: Option, } #[derive(Debug, Default, Serialize)] @@ -99,7 +125,96 @@ pub struct Dependency { name: String, } +#[derive(Debug, Serialize)] +pub struct Build { + /// The name associated with this crate. + /// + /// This is determined by the build system that produced + /// the `rust-project.json` in question. For instance, if bazel were used, + /// the label might be something like `//ide/rust/rust-analyzer:rust-analyzer`. + /// + /// Do not attempt to parse the contents of this string; it is a build system-specific + /// identifier similar to [`Crate::display_name`]. + pub label: String, + /// Path corresponding to the build system-specific file defining the crate. + /// + /// It is roughly analogous to [`ManifestPath`], but it should *not* be used with + /// [`crate::ProjectManifest::from_manifest_file`], as the build file may not be + /// be in the `rust-project.json`. + pub build_file: Utf8PathBuf, + /// The kind of target. + /// + /// Examples (non-exhaustively) include [`TargetKind::Bin`], [`TargetKind::Lib`], + /// and [`TargetKind::Test`]. This information is used to determine what sort + /// of runnable codelens to provide, if any. + pub target_kind: TargetKind, +} + +#[derive(Clone, Copy, Debug, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum TargetKind { + Bin, + /// Any kind of Cargo lib crate-type (dylib, rlib, proc-macro, ...). + Lib, + Test, +} + +/// A template-like structure for describing runnables. +/// +/// These are used for running and debugging binaries and tests without encoding +/// build system-specific knowledge into rust-analyzer. +/// +/// # Example +/// +/// Below is an example of a test runnable. `{label}` and `{test_id}` +/// are explained in [`Runnable::args`]'s documentation. +/// +/// ```json +/// { +/// "program": "bazel", +/// "args": [ +/// "test", +/// "{label}", +/// "--test_arg", +/// "{test_id}", +/// ], +/// "cwd": "/home/user/repo-root/", +/// "kind": "testOne" +/// } +/// ``` +#[derive(Debug, Serialize)] +pub struct Runnable { + /// The program invoked by the runnable. + /// + /// For example, this might be `cargo`, `bazel`, etc. + pub program: String, + /// The arguments passed to [`Runnable::program`]. + /// + /// The args can contain two template strings: `{label}` and `{test_id}`. + /// rust-analyzer will find and replace `{label}` with [`Build::label`] and + /// `{test_id}` with the test name. + pub args: Vec, + /// The current working directory of the runnable. + pub cwd: Utf8PathBuf, + pub kind: RunnableKind, +} + +/// The kind of runnable. +#[derive(Debug, Clone, Copy, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum RunnableKind { + Check, + + /// Can run a binary. + Run, + + /// Run a single test. + TestOne, +} + pub fn generate_rust_project( + bazel: &Utf8Path, + workspace: &Utf8Path, sysroot: &str, sysroot_src: &str, crates: &BTreeSet, @@ -108,6 +223,31 @@ pub fn generate_rust_project( sysroot: Some(sysroot.into()), sysroot_src: Some(sysroot_src.into()), crates: Vec::new(), + runnables: vec![ + Runnable { + program: bazel.to_string(), + args: vec!["build".to_owned(), "{label}".to_owned()], + cwd: workspace.to_owned(), + kind: RunnableKind::Check, + }, + Runnable { + program: bazel.to_string(), + args: vec![ + "test".to_owned(), + "{label}".to_owned(), + "--test_output".to_owned(), + "streamed".to_owned(), + "--test_arg".to_owned(), + "--nocapture".to_owned(), + "--test_arg".to_owned(), + "--exact".to_owned(), + "--test_arg".to_owned(), + "{test_id}".to_owned(), + ], + cwd: workspace.to_owned(), + kind: RunnableKind::TestOne, + }, + ], }; let mut unmerged_crates: Vec<&CrateSpec> = crates.iter().collect(); @@ -133,6 +273,29 @@ pub fn generate_rust_project( } else { log::trace!("Merging crate {}", &c.crate_id); merged_crates_index.insert(c.crate_id.clone(), project.crates.len()); + + let target_kind = match c.crate_type { + CrateType::Bin if c.is_test => TargetKind::Test, + CrateType::Bin => TargetKind::Bin, + CrateType::Rlib + | CrateType::Lib + | CrateType::Dylib + | CrateType::Cdylib + | CrateType::Staticlib + | CrateType::ProcMacro => TargetKind::Lib, + }; + + if let Some(build) = &c.build { + if target_kind == TargetKind::Bin { + project.runnables.push(Runnable { + program: bazel.to_string(), + args: vec!["run".to_string(), build.label.to_owned()], + cwd: workspace.to_owned(), + kind: RunnableKind::Run, + }); + } + } + project.crates.push(Crate { display_name: Some(c.display_name.clone()), root_module: c.root_module.clone(), @@ -170,6 +333,11 @@ pub fn generate_rust_project( env: Some(c.env.clone()), is_proc_macro: c.proc_macro_dylib_path.is_some(), proc_macro_dylib_path: c.proc_macro_dylib_path.clone(), + build: c.build.as_ref().map(|b| Build { + label: b.label.clone(), + build_file: b.build_file.clone(), + target_kind, + }), }); } } @@ -242,24 +410,12 @@ fn detect_cycle<'a>( } pub fn write_rust_project( - rust_project_path: &Path, - workspace: &Path, - execution_root: &Path, - output_base: &Path, + rust_project_path: &Utf8Path, + output_base: &Utf8Path, + workspace: &Utf8Path, + execution_root: &Utf8Path, rust_project: &RustProject, ) -> anyhow::Result<()> { - let workspace = workspace - .to_str() - .ok_or_else(|| anyhow!("workspace is not valid UTF-8"))?; - - let execution_root = execution_root - .to_str() - .ok_or_else(|| anyhow!("execution_root is not valid UTF-8"))?; - - let output_base = output_base - .to_str() - .ok_or_else(|| anyhow!("output_base is not valid UTF-8"))?; - // Try to remove the existing rust-project.json. It's OK if the file doesn't exist. match std::fs::remove_file(rust_project_path) { Ok(_) => {} @@ -274,11 +430,13 @@ pub fn write_rust_project( // Render the `rust-project.json` file and replace the exec root // placeholders with the path to the local exec root. - let rust_project_content = serde_json::to_string_pretty(rust_project)? - .replace("${pwd}", execution_root) - .replace("__EXEC_ROOT__", execution_root) - .replace("__OUTPUT_BASE__", output_base) - .replace("__WORKSPACE__", workspace); + let rust_project_content = serde_json::to_string_pretty(rust_project)?; + let rust_project_content = normalize_project_string( + &rust_project_content, + workspace, + output_base, + execution_root, + ); // Write the new rust-project.json file. std::fs::write(rust_project_path, rust_project_content)?; @@ -286,6 +444,19 @@ pub fn write_rust_project( Ok(()) } +pub fn normalize_project_string( + input: &str, + workspace: &Utf8Path, + output_base: &Utf8Path, + execution_root: &Utf8Path, +) -> String { + input + .replace("__WORKSPACE__", workspace.as_str()) + .replace("${pwd}", execution_root.as_str()) + .replace("__EXEC_ROOT__", execution_root.as_str()) + .replace("__OUTPUT_BASE__", output_base.as_str()) +} + #[cfg(test)] mod tests { use super::*; @@ -294,6 +465,7 @@ mod tests { #[test] fn generate_rust_project_single() { let project = generate_rust_project( + Utf8Path::new("workspace"), "sysroot", "sysroot_src", &BTreeSet::from([CrateSpec { @@ -309,7 +481,9 @@ mod tests { cfg: vec!["test".into(), "debug_assertions".into()], env: BTreeMap::new(), target: "x86_64-unknown-linux-gnu".into(), - crate_type: "rlib".into(), + crate_type: CrateType::Rlib, + is_test: false, + build: None, }]), ) .expect("expect success"); @@ -325,6 +499,7 @@ mod tests { #[test] fn generate_rust_project_with_deps() { let project = generate_rust_project( + Utf8Path::new("workspace"), "sysroot", "sysroot_src", &BTreeSet::from([ @@ -341,7 +516,9 @@ mod tests { cfg: vec!["test".into(), "debug_assertions".into()], env: BTreeMap::new(), target: "x86_64-unknown-linux-gnu".into(), - crate_type: "rlib".into(), + crate_type: CrateType::Rlib, + is_test: false, + build: None, }, CrateSpec { aliases: BTreeMap::new(), @@ -356,7 +533,9 @@ mod tests { cfg: vec!["test".into(), "debug_assertions".into()], env: BTreeMap::new(), target: "x86_64-unknown-linux-gnu".into(), - crate_type: "rlib".into(), + crate_type: CrateType::Rlib, + is_test: false, + build: None, }, CrateSpec { aliases: BTreeMap::new(), @@ -371,7 +550,9 @@ mod tests { cfg: vec!["test".into(), "debug_assertions".into()], env: BTreeMap::new(), target: "x86_64-unknown-linux-gnu".into(), - crate_type: "rlib".into(), + crate_type: CrateType::Rlib, + is_test: false, + build: None, }, ]), )