`rust-analyzer` discoverConfig integration (#3073)
Adds a target that can be used for project auto-discovery by using the
`discoverConfig` settings as described in the `rust-analyzer` user
manual.
Unlike the `gen_rust_project` target, this can be used for dynamic
project discovery, and passing `{arg}` to `discoverConfig.command` can
split big repositories into multiple, smaller workspaces that
`rust-analyzer` switches between as needed. Large repositories can make
it OOM.
At amo, we've used a similar implementation for a while with great
success, which is why we figured we might upstream it. The changes also
include two additional output groups to ensure that proc-macros and
build script targets are built, as `rust-analyzer` depends on these to
provide complete IDE support.
Additionally, the PR makes use of the `output_base` value in `bazel`
invocations. We found it helpful to have tools such as `rust-analyzer`
and `clippy` run on a separate bazel server than the one used for
building. And a `config_group` argument was added to provide the ability
to provide a config group to `bazel` invocations.
An attempt to get codelens actions to work was done as well,
particularly around tests and binaries. They seem to work, but I'm not
100% sure whether the approach taken is the right one.
Closes #2755 .
---------
Co-authored-by: Krasimir Georgiev <krasimir@google.com>diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml
index 4014ec3..7a931a2 100644
--- a/.bazelci/presubmit.yml
+++ b/.bazelci/presubmit.yml
@@ -885,7 +885,7 @@
build_flags:
- "--compile_one_dependency"
build_targets:
- - "tools/rust_analyzer/main.rs"
+ - "tools/rust_analyzer/bin/gen_rust_project.rs"
extensions_bindgen_linux:
platform: ubuntu2004
name: Extensions Bindgen
diff --git a/.gitignore b/.gitignore
index 0316dc9..74ef480 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,6 +30,9 @@
.vscode
*.code-workspace
+# zed
+.zed
+
# JetBrains
.idea
.idea/**
diff --git a/MODULE.bazel b/MODULE.bazel
index 5171b23..971d086 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -22,6 +22,7 @@
internal_deps,
"rrra",
"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/docs/rust_analyzer.vm b/docs/rust_analyzer.vm
index b974332..e5f794b 100644
--- a/docs/rust_analyzer.vm
+++ b/docs/rust_analyzer.vm
@@ -2,10 +2,12 @@
## Overview
For [non-Cargo projects](https://rust-analyzer.github.io/manual.html#non-cargo-based-projects),
-[rust-analyzer](https://rust-analyzer.github.io/) depends on a `rust-project.json` file at the
-root of the project that describes its structure. The `rust_analyzer` rule facilitates generating
-such a file.
+[rust-analyzer](https://rust-analyzer.github.io/) depends on either a `rust-project.json` file
+at the root of the project that describes its structure or on build system specific
+[project auto-discovery](https://rust-analyzer.github.io/manual.html#rust-analyzer.workspace.discoverConfig).
+The `rust_analyzer` rules facilitate both approaches.
+## rust-project.json approach
### Setup
First, ensure `rules_rust` is setup in your workspace. By default, `rust_register_toolchains` will
@@ -84,4 +86,71 @@
Then you can use a prototype [rust-analyzer plugin](https://marketplace.visualstudio.com/items?itemName=MattStark.bazel-rust-analyzer) that automatically collects the outputs whenever you recompile.
-]]#
+## Project auto-discovery
+### Setup
+
+Auto-discovery makes `rust-analyzer` behave in a Bazel project in a similar fashion to how it does
+in a Cargo project. This is achieved by generating a structure similar to what `rust-project.json`
+contains but, instead of writing that to a file, the data gets piped to `rust-analyzer` directly
+through `stdout`. To use auto-discovery the `rust-analyzer` IDE settings must be configured similar to:
+
+```json
+"rust-analyzer": {
+ "workspace": {
+ "discoverConfig": {
+ "command": ["discover_bazel_rust_project.sh"],
+ "progressLabel": "rust_analyzer",
+ "filesToWatch": ["BUILD", "BUILD.bazel", "MODULE.bazel"]
+ }
+ }
+}
+```
+
+The shell script passed to `discoverConfig.command` is typically meant to wrap the bazel rule invocation,
+primarily for muting `stderr` (because `rust-analyzer` will consider that an error has occurred if anything
+is passed through `stderr`) and, additionally, for specifying rule arguments. E.g:
+
+```shell
+#!/usr/bin/bash
+
+bazel \
+ run \
+ @rules_rust//tools/rust_analyzer:discover_bazel_rust_project -- \
+ --bazel_startup_option=--output_base=~/ide_bazel \
+ --bazel_arg=--watchfs \
+ ${1:+"$1"} 2>/dev/null
+```
+
+The script above also handles an optional CLI argument which gets passed when workspace splitting is
+enabled. The script path should be either absolute or relative to the project root.
+
+### Workspace splitting
+
+The above configuration treats the entire project as a single workspace. However, large codebases might be
+too much to handle for `rust-analyzer` all at once. This can be addressed by splitting the codebase in
+multiple workspaces by extending the `discoverConfig.command` setting:
+
+```json
+"rust-analyzer": {
+ "workspace": {
+ "discoverConfig": {
+ "command": ["discover_bazel_rust_project.sh", "{arg}"],
+ "progressLabel": "rust_analyzer",
+ "filesToWatch": ["BUILD", "BUILD.bazel", "MODULE.bazel"]
+ }
+ }
+}
+```
+
+`{arg}` acts as a placeholder that `rust-analyzer` replaces with the path of the source / build file
+that gets opened.
+
+The root of the workspace will, in this configuration, be the package the crate currently being worked on
+belongs to. This means that only that package and its dependencies get built and indexed by `rust-analyzer`,
+thus allowing for a smaller footprint.
+
+`rust-analyzer` will switch workspaces whenever an out-of-tree file gets opened, essentially indexing that
+crate and its dependencies separately. A caveat of this is that *dependents* of the crate currently being
+worked on are not indexed and won't be tracked by `rust-analyzer`.
+
+]]#
\ No newline at end of file
diff --git a/extensions/prost/private/prost.bzl b/extensions/prost/private/prost.bzl
index 61157e8..c591710 100644
--- a/extensions/prost/private/prost.bzl
+++ b/extensions/prost/private/prost.bzl
@@ -308,6 +308,8 @@
# https://github.com/rust-analyzer/rust-analyzer/blob/2021-11-15/crates/project_model/src/workspace.rs#L529-L531
cfgs = ["test", "debug_assertions"]
+ build_info_out_dirs = [dep_variant_info.build_info.out_dir] if dep_variant_info.build_info != None and dep_variant_info.build_info.out_dir != None else None
+
rust_analyzer_info = write_rust_analyzer_spec_file(ctx, ctx.rule.attr, ctx.label, RustAnalyzerInfo(
aliases = {},
crate = dep_variant_info.crate_info,
@@ -315,7 +317,9 @@
env = dep_variant_info.crate_info.rustc_env,
deps = rust_analyzer_deps,
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(direct = build_info_out_dirs, transitive = [dep.build_info_out_dirs for dep in rust_analyzer_deps]),
+ proc_macro_dylib = None,
build_info = dep_variant_info.build_info,
))
diff --git a/rust/private/providers.bzl b/rust/private/providers.bzl
index 0556ebe..746370b 100644
--- a/rust/private/providers.bzl
+++ b/rust/private/providers.bzl
@@ -159,12 +159,14 @@
fields = {
"aliases": "Dict[RustAnalyzerInfo, String]: Replacement names these targets should be known as in Rust code",
"build_info": "BuildInfo: build info for this crate if present",
+ "build_info_out_dirs": "Depset[File]: transitive closure of build script out dirs",
"cfgs": "List[String]: features or other compilation `--cfg` settings",
"crate": "CrateInfo: Crate information.",
- "crate_specs": "Depset[File]: transitive closure of OutputGroupInfo files",
+ "crate_specs": "Depset[File]: transitive closure of crate spec files",
"deps": "List[RustAnalyzerInfo]: direct dependencies",
"env": "Dict[String: String]: Environment variables, used for the `env!` macro",
- "proc_macro_dylib_path": "File: compiled shared library output of proc-macro rule",
+ "proc_macro_dylib": "File: if this is a proc-macro target, the shared library output",
+ "proc_macro_dylibs": "Depset[File]: transitive closure of proc-macro shared library files",
},
)
diff --git a/rust/private/rust_analyzer.bzl b/rust/private/rust_analyzer.bzl
index bc12306..a08d50c 100644
--- a/rust/private/rust_analyzer.bzl
+++ b/rust/private/rust_analyzer.bzl
@@ -54,7 +54,9 @@
env = base_info.env,
deps = base_info.deps,
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(transitive = [base_info.proc_macro_dylibs]),
+ build_info_out_dirs = depset(transitive = [base_info.build_info_out_dirs]),
+ proc_macro_dylib = base_info.proc_macro_dylib,
build_info = base_info.build_info,
)
@@ -135,6 +137,10 @@
if aliased_target.label in labels_to_rais:
aliases[labels_to_rais[aliased_target.label]] = aliased_name
+ proc_macro_dylib = find_proc_macro_dylib(toolchain, target)
+ proc_macro_dylibs = [proc_macro_dylib] if proc_macro_dylib else None
+ build_info_out_dirs = [build_info.out_dir] if build_info != None and build_info.out_dir != None else None
+
rust_analyzer_info = write_rust_analyzer_spec_file(ctx, ctx.rule.attr, ctx.label, RustAnalyzerInfo(
aliases = aliases,
crate = crate_info,
@@ -142,23 +148,29 @@
env = crate_info.rustc_env,
deps = dep_infos,
crate_specs = depset(transitive = [dep.crate_specs for dep in dep_infos]),
- proc_macro_dylib_path = find_proc_macro_dylib_path(toolchain, target),
+ proc_macro_dylibs = depset(direct = proc_macro_dylibs, transitive = [dep.proc_macro_dylibs for dep in dep_infos]),
+ build_info_out_dirs = depset(direct = build_info_out_dirs, transitive = [dep.build_info_out_dirs for dep in dep_infos]),
+ 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_dylib = rust_analyzer_info.proc_macro_dylibs,
+ rust_analyzer_src = 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 +186,7 @@
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 +200,7 @@
)
# 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__/"
@@ -220,6 +232,7 @@
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.
@@ -230,6 +243,14 @@
crate["root_module"] = path_prefix + info.crate.root.path
crate["source"] = {"exclude_dirs": [], "include_dirs": []}
+ # We're only interested in the build info for local crates as these are the
+ # only ones for which we want build file watching and code lens runnables support.
+ 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,
+ }
+
if is_generated:
srcs = getattr(ctx.rule.files, "srcs", [])
src_map = {src.short_path: src for src in srcs if src.is_source}
@@ -268,8 +289,8 @@
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/test/rust_analyzer/auto_discovery_static_and_shared_lib_test/BUILD.bazel b/test/rust_analyzer/auto_discovery_static_and_shared_lib_test/BUILD.bazel
new file mode 100644
index 0000000..c363ad7
--- /dev/null
+++ b/test/rust_analyzer/auto_discovery_static_and_shared_lib_test/BUILD.bazel
@@ -0,0 +1,42 @@
+load(
+ "@rules_rust//rust:defs.bzl",
+ "rust_shared_library",
+ "rust_static_library",
+ "rust_test",
+)
+
+rust_shared_library(
+ name = "greeter_cdylib",
+ srcs = [
+ "greeter.rs",
+ "shared_lib.rs",
+ ],
+ crate_root = "shared_lib.rs",
+ edition = "2018",
+)
+
+rust_static_library(
+ name = "greeter_staticlib",
+ srcs = [
+ "greeter.rs",
+ "static_lib.rs",
+ ],
+ crate_root = "static_lib.rs",
+ edition = "2018",
+)
+
+rust_test(
+ name = "auto_discovery_json_test",
+ srcs = ["auto_discovery_json_test.rs"],
+ data = [":auto-discovery.json"],
+ edition = "2018",
+ env = {"AUTO_DISCOVERY_JSON": "$(rootpath :auto-discovery.json)"},
+ # This target is tagged as manual since it's not expected to pass in
+ # contexts outside of `//test/rust_analyzer:rust_analyzer_test`. Run
+ # that target to execute this test.
+ tags = ["manual"],
+ deps = [
+ "//test/rust_analyzer/3rdparty/crates:serde",
+ "//test/rust_analyzer/3rdparty/crates:serde_json",
+ ],
+)
diff --git a/test/rust_analyzer/auto_discovery_static_and_shared_lib_test/auto_discovery_json_test.rs b/test/rust_analyzer/auto_discovery_static_and_shared_lib_test/auto_discovery_json_test.rs
new file mode 100644
index 0000000..e015b7c
--- /dev/null
+++ b/test/rust_analyzer/auto_discovery_static_and_shared_lib_test/auto_discovery_json_test.rs
@@ -0,0 +1,57 @@
+#[cfg(test)]
+mod tests {
+ use serde::Deserialize;
+ use std::env;
+ use std::path::PathBuf;
+
+ #[derive(Deserialize)]
+ #[serde(tag = "kind")]
+ #[serde(rename_all = "snake_case")]
+ enum DiscoverProject {
+ Finished { project: Project },
+ Progress {},
+ }
+
+ #[derive(Deserialize)]
+ struct Project {
+ crates: Vec<Crate>,
+ }
+
+ #[derive(Deserialize)]
+ struct Crate {
+ display_name: String,
+ root_module: String,
+ }
+
+ #[test]
+ fn test_static_and_shared_lib() {
+ let rust_project_path = PathBuf::from(env::var("AUTO_DISCOVERY_JSON").unwrap());
+ let content = std::fs::read_to_string(&rust_project_path)
+ .unwrap_or_else(|_| panic!("couldn't open {:?}", &rust_project_path));
+ println!("{}", content);
+
+ for line in content.lines() {
+ let discovery: DiscoverProject =
+ serde_json::from_str(line).expect("Failed to deserialize discovery JSON");
+
+ let project = match discovery {
+ DiscoverProject::Finished { project } => project,
+ DiscoverProject::Progress {} => continue,
+ };
+
+ let cdylib = project
+ .crates
+ .iter()
+ .find(|c| &c.display_name == "greeter_cdylib")
+ .unwrap();
+ assert!(cdylib.root_module.ends_with("/shared_lib.rs"));
+
+ let staticlib = project
+ .crates
+ .iter()
+ .find(|c| &c.display_name == "greeter_staticlib")
+ .unwrap();
+ assert!(staticlib.root_module.ends_with("/static_lib.rs"));
+ }
+ }
+}
diff --git a/test/rust_analyzer/auto_discovery_static_and_shared_lib_test/greeter.rs b/test/rust_analyzer/auto_discovery_static_and_shared_lib_test/greeter.rs
new file mode 100644
index 0000000..ac87521
--- /dev/null
+++ b/test/rust_analyzer/auto_discovery_static_and_shared_lib_test/greeter.rs
@@ -0,0 +1,61 @@
+/// Object that displays a greeting.
+pub struct Greeter {
+ greeting: String,
+}
+
+/// Implementation of Greeter.
+impl Greeter {
+ /// Constructs a new `Greeter`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use hello_lib::greeter::Greeter;
+ ///
+ /// let greeter = Greeter::new("Hello");
+ /// ```
+ pub fn new(greeting: &str) -> Greeter {
+ Greeter {
+ greeting: greeting.to_string(),
+ }
+ }
+
+ /// Returns the greeting as a string.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use hello_lib::greeter::Greeter;
+ ///
+ /// let greeter = Greeter::new("Hello");
+ /// let greeting = greeter.greeting("World");
+ /// ```
+ pub fn greeting(&self, thing: &str) -> String {
+ format!("{} {}", &self.greeting, thing)
+ }
+
+ /// Prints the greeting.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use hello_lib::greeter::Greeter;
+ ///
+ /// let greeter = Greeter::new("Hello");
+ /// greeter.greet("World");
+ /// ```
+ pub fn greet(&self, thing: &str) {
+ println!("{} {}", &self.greeting, thing);
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::Greeter;
+
+ #[test]
+ fn test_greeting() {
+ let hello = Greeter::new("Hi");
+ assert_eq!("Hi Rust", hello.greeting("Rust"));
+ }
+}
diff --git a/test/rust_analyzer/auto_discovery_static_and_shared_lib_test/shared_lib.rs b/test/rust_analyzer/auto_discovery_static_and_shared_lib_test/shared_lib.rs
new file mode 100644
index 0000000..44969c6
--- /dev/null
+++ b/test/rust_analyzer/auto_discovery_static_and_shared_lib_test/shared_lib.rs
@@ -0,0 +1 @@
+pub mod greeter;
diff --git a/test/rust_analyzer/auto_discovery_static_and_shared_lib_test/static_lib.rs b/test/rust_analyzer/auto_discovery_static_and_shared_lib_test/static_lib.rs
new file mode 100644
index 0000000..44969c6
--- /dev/null
+++ b/test/rust_analyzer/auto_discovery_static_and_shared_lib_test/static_lib.rs
@@ -0,0 +1 @@
+pub mod greeter;
diff --git a/test/rust_analyzer/rust_analyzer_test_runner.sh b/test/rust_analyzer/rust_analyzer_test_runner.sh
index 44c35e0..689d95e 100755
--- a/test/rust_analyzer/rust_analyzer_test_runner.sh
+++ b/test/rust_analyzer/rust_analyzer_test_runner.sh
@@ -102,6 +102,10 @@
else
RUST_LOG="${rust_log}" bazel run "@rules_rust//tools/rust_analyzer:gen_rust_project"
fi
+
+ echo "Generating auto-discovery.json..."
+ RUST_LOG="${rust_log}" bazel run "@rules_rust//tools/rust_analyzer:discover_bazel_rust_project" > auto-discovery.json
+
echo "Building..."
bazel build //...
echo "Testing..."
diff --git a/tools/rust_analyzer/3rdparty/BUILD.bazel b/tools/rust_analyzer/3rdparty/BUILD.bazel
index 9739042..6759e52 100644
--- a/tools/rust_analyzer/3rdparty/BUILD.bazel
+++ b/tools/rust_analyzer/3rdparty/BUILD.bazel
@@ -9,6 +9,10 @@
"anyhow": crate.spec(
version = "1.0.71",
),
+ "camino": crate.spec(
+ features = ["serde1"],
+ version = "1.1.9",
+ ),
"clap": crate.spec(
features = [
"derive",
diff --git a/tools/rust_analyzer/3rdparty/Cargo.Bazel.lock b/tools/rust_analyzer/3rdparty/Cargo.Bazel.lock
index ab1c359..feef41f 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"
@@ -73,6 +73,15 @@
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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -130,6 +139,7 @@
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 b33911c..a908c68 100644
--- a/tools/rust_analyzer/3rdparty/crates/BUILD.bazel
+++ b/tools/rust_analyzer/3rdparty/crates/BUILD.bazel
@@ -44,6 +44,18 @@
)
alias(
+ name = "camino-1.1.9",
+ actual = "@rrra__camino-1.1.9//:camino",
+ tags = ["manual"],
+)
+
+alias(
+ name = "camino",
+ actual = "@rrra__camino-1.1.9//:camino",
+ tags = ["manual"],
+)
+
+alias(
name = "clap-4.3.11",
actual = "@rrra__clap-4.3.11//:clap",
tags = ["manual"],
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 0000000..8f01a81
--- /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 bc75a66..081f00b 100644
--- a/tools/rust_analyzer/3rdparty/crates/defs.bzl
+++ b/tools/rust_analyzer/3rdparty/crates/defs.bzl
@@ -296,6 +296,7 @@
"": {
_COMMON_CONDITION: {
"anyhow": Label("@rrra//:anyhow-1.0.71"),
+ "camino": Label("@rrra//:camino-1.1.9"),
"clap": Label("@rrra//:clap-4.3.11"),
"env_logger": Label("@rrra//:env_logger-0.10.0"),
"itertools": Label("@rrra//:itertools-0.11.0"),
@@ -492,6 +493,16 @@
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("//tools/rust_analyzer/3rdparty/crates:BUILD.camino-1.1.9.bazel"),
+ )
+
+ maybe(
+ http_archive,
name = "rrra__cc-1.0.79",
sha256 = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f",
type = "tar.gz",
@@ -992,6 +1003,7 @@
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 e17691d..018ca09 100644
--- a/tools/rust_analyzer/BUILD.bazel
+++ b/tools/rust_analyzer/BUILD.bazel
@@ -3,8 +3,8 @@
load("//tools/private:tool_utils.bzl", "aspect_repository")
rust_binary(
- name = "gen_rust_project",
- srcs = ["main.rs"],
+ name = "discover_bazel_rust_project",
+ srcs = ["bin/discover_rust_project.rs"],
edition = "2018",
rustc_env = {
"ASPECT_REPOSITORY": aspect_repository(),
@@ -13,10 +13,30 @@
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",
+ "//tools/rust_analyzer/3rdparty/crates:serde_json",
+ ],
+)
+
+rust_binary(
+ name = "gen_rust_project",
+ srcs = ["bin/gen_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",
+ "//tools/rust_analyzer/3rdparty/crates:serde_json",
],
)
@@ -24,7 +44,7 @@
name = "gen_rust_project_lib",
srcs = glob(
["**/*.rs"],
- exclude = ["main.rs"],
+ exclude = ["bin"],
),
data = [
"//rust/private:rust_analyzer_detect_sysroot",
@@ -33,6 +53,8 @@
deps = [
"//rust/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 7bef210..f306aa5 100644
--- a/tools/rust_analyzer/aquery.rs
+++ b/tools/rust_analyzer/aquery.rs
@@ -1,11 +1,11 @@
use std::collections::{BTreeMap, BTreeSet};
-use std::path::Path;
-use std::path::PathBuf;
-use std::process::Command;
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>,
@@ -50,7 +50,16 @@
pub cfg: Vec<String>,
pub env: BTreeMap<String, String>,
pub target: String,
- pub crate_type: String,
+ pub crate_type: CrateType,
+ pub is_test: bool,
+ pub build: Option<CrateSpecBuild>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)]
+#[serde(deny_unknown_fields)]
+pub struct CrateSpecBuild {
+ pub label: String,
+ pub build_file: String,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)]
@@ -60,29 +69,38 @@
pub include_dirs: Vec<String>,
}
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum CrateType {
+ Bin,
+ Rlib,
+ Lib,
+ Dylib,
+ Cdylib,
+ Staticlib,
+ ProcMacro,
+}
+
+#[allow(clippy::too_many_arguments)]
pub fn get_crate_specs(
- bazel: &Path,
- config: &Option<String>,
- workspace: &Path,
- execution_root: &Path,
+ 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 config_args = match config {
- Some(config) => vec!["--config", config],
- None => Vec::new(),
- };
- let mut aquery_command = Command::new(bazel);
+ let mut aquery_command = bazel_command(bazel, Some(workspace), Some(output_base));
aquery_command
- .current_dir(workspace)
- .env_remove("BAZELISK_SKIP_WRAPPER")
- .env_remove("BUILD_WORKING_DIRECTORY")
- .env_remove("BUILD_WORKSPACE_DIRECTORY")
+ .args(bazel_startup_options)
.arg("aquery")
- .args(config_args)
+ .args(bazel_args)
.arg("--include_aspects")
.arg("--include_artifacts")
.arg(format!(
@@ -98,6 +116,8 @@
.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.")?;
@@ -107,22 +127,16 @@
let crate_specs = crate_spec_files
.into_iter()
- .map(|file| {
- let content = std::fs::read_to_string(&file)
- .with_context(|| format!("Failed to read file: {}", file.display()))?;
- log::trace!("{}\n{}", file.display(), content);
- serde_json::from_str(&content)
- .with_context(|| format!("Failed to deserialize file: {}", file.display()))
- })
+ .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: &Path,
+ execution_root: &Utf8Path,
aquery_stdout: &str,
-) -> anyhow::Result<Vec<PathBuf>> {
+) -> 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) {
@@ -147,7 +161,7 @@
.map(|pf| (pf.id, pf))
.collect::<BTreeMap<_, _>>();
- let mut output_files: Vec<PathBuf> = Vec::new();
+ let mut output_files: Vec<Utf8PathBuf> = Vec::new();
for action in out.actions {
for output_id in action.output_ids {
let artifact = artifacts
@@ -169,15 +183,15 @@
fn path_from_fragments(
id: u32,
fragments: &BTreeMap<u32, &PathFragment>,
-) -> anyhow::Result<PathBuf> {
+) -> 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(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)
@@ -191,6 +205,7 @@
log::debug!("{:?}", spec);
if let Some(existing) = consolidated_specs.get_mut(&spec.crate_id) {
existing.deps.extend(spec.deps);
+ existing.env.extend(spec.env);
existing.aliases.extend(spec.aliases);
if let Some(source) = &mut existing.source {
@@ -215,9 +230,18 @@
// 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" {
+ if spec.crate_type == CrateType::Rlib {
existing.display_name = spec.display_name;
- existing.crate_type = "rlib".into();
+ existing.crate_type = CrateType::Rlib;
+ existing.is_test = spec.is_test;
+ }
+
+ // We want to use the test target's build label to provide
+ // unit tests codelens actions for library crates in IDEs.
+ if spec.is_test {
+ if let Some(build) = spec.build {
+ existing.build = Some(build);
+ }
}
// For proc-macro crates that exist within the workspace, there will be a
@@ -258,7 +282,12 @@
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: Some(CrateSpecBuild {
+ label: "//:mylib".to_owned(),
+ build_file: "BUILD.bazel".to_owned(),
+ }),
},
CrateSpec {
aliases: BTreeMap::new(),
@@ -273,7 +302,9 @@
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(),
@@ -288,7 +319,9 @@
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(),
@@ -303,7 +336,9 @@
cfg: vec!["test".into(), "debug_assertions".into()],
env: BTreeMap::new(),
target: "x86_64-unknown-linux-gnu".into(),
- crate_type: "bin".into(),
+ crate_type: CrateType::Bin,
+ is_test: true,
+ build: None,
},
];
@@ -323,7 +358,12 @@
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: Some(CrateSpecBuild {
+ label: "//:mylib".to_owned(),
+ build_file: "BUILD.bazel".to_owned(),
+ }),
},
CrateSpec {
aliases: BTreeMap::new(),
@@ -338,7 +378,9 @@
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(),
@@ -353,7 +395,9 @@
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
},
])
);
@@ -375,7 +419,9 @@
cfg: vec!["test".into(), "debug_assertions".into()],
env: BTreeMap::new(),
target: "x86_64-unknown-linux-gnu".into(),
- crate_type: "bin".into(),
+ crate_type: CrateType::Bin,
+ is_test: true,
+ build: None,
},
CrateSpec {
aliases: BTreeMap::new(),
@@ -390,7 +436,9 @@
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(),
@@ -405,7 +453,9 @@
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(),
@@ -420,7 +470,9 @@
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,
},
];
@@ -440,7 +492,9 @@
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(),
@@ -455,7 +509,9 @@
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(),
@@ -470,7 +526,9 @@
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
},
])
);
@@ -497,7 +555,9 @@
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(),
@@ -512,7 +572,9 @@
cfg: vec!["test".into(), "debug_assertions".into()],
env: BTreeMap::new(),
target: "x86_64-unknown-linux-gnu".into(),
- crate_type: "bin".into(),
+ crate_type: CrateType::Bin,
+ is_test: true,
+ build: None,
},
CrateSpec {
aliases: BTreeMap::new(),
@@ -527,7 +589,9 @@
cfg: vec!["test".into(), "debug_assertions".into()],
env: BTreeMap::new(),
target: "x86_64-unknown-linux-gnu".into(),
- crate_type: "bin".into(),
+ crate_type: CrateType::Bin,
+ is_test: false,
+ build: None,
},
CrateSpec {
aliases: BTreeMap::new(),
@@ -542,7 +606,9 @@
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,
},
];
@@ -563,7 +629,9 @@
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(),
@@ -578,7 +646,9 @@
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
},
])
);
@@ -607,7 +677,9 @@
cfg: vec!["test".into(), "debug_assertions".into()],
env: BTreeMap::new(),
target: "x86_64-unknown-linux-gnu".into(),
- crate_type: "proc_macro".into(),
+ crate_type: CrateType::ProcMacro,
+ is_test: false,
+ build: None,
},
CrateSpec {
aliases: BTreeMap::new(),
@@ -624,7 +696,9 @@
cfg: vec!["test".into(), "debug_assertions".into()],
env: BTreeMap::new(),
target: "x86_64-unknown-linux-gnu".into(),
- crate_type: "proc_macro".into(),
+ crate_type: CrateType::ProcMacro,
+ is_test: false,
+ build: None,
},
];
@@ -647,7 +721,9 @@
cfg: vec!["test".into(), "debug_assertions".into()],
env: BTreeMap::new(),
target: "x86_64-unknown-linux-gnu".into(),
- crate_type: "proc_macro".into(),
+ crate_type: CrateType::ProcMacro,
+ is_test: false,
+ build: None,
},])
);
}
@@ -669,7 +745,9 @@
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: true,
+ build: None,
},
CrateSpec {
aliases: BTreeMap::from([("ID-mylib_dep.rs".into(), "aliased_name".into())]),
@@ -684,7 +762,9 @@
cfg: vec!["test".into(), "debug_assertions".into()],
env: BTreeMap::new(),
target: "x86_64-unknown-linux-gnu".into(),
- crate_type: "bin".into(),
+ crate_type: CrateType::Bin,
+ is_test: true,
+ build: None,
},
];
@@ -704,7 +784,9 @@
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: true,
+ build: None,
}])
);
}
@@ -726,7 +808,9 @@
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: true,
+ build: None,
},
CrateSpec {
aliases: BTreeMap::new(),
@@ -744,7 +828,9 @@
cfg: vec!["test".into(), "debug_assertions".into()],
env: BTreeMap::new(),
target: "x86_64-unknown-linux-gnu".into(),
- crate_type: "bin".into(),
+ crate_type: CrateType::Bin,
+ is_test: true,
+ build: None,
},
];
@@ -767,7 +853,9 @@
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: true,
+ build: None,
}])
);
}
@@ -792,7 +880,9 @@
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: true,
+ build: None,
},
CrateSpec {
aliases: BTreeMap::new(),
@@ -810,7 +900,9 @@
cfg: vec!["test".into(), "debug_assertions".into()],
env: BTreeMap::new(),
target: "x86_64-unknown-linux-gnu".into(),
- crate_type: "bin".into(),
+ crate_type: CrateType::Bin,
+ is_test: true,
+ build: None,
},
];
@@ -833,7 +925,9 @@
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: true,
+ build: None,
}])
);
}
diff --git a/tools/rust_analyzer/bin/discover_rust_project.rs b/tools/rust_analyzer/bin/discover_rust_project.rs
new file mode 100644
index 0000000..9598f05
--- /dev/null
+++ b/tools/rust_analyzer/bin/discover_rust_project.rs
@@ -0,0 +1,211 @@
+//! Binary used for automatic Rust workspace discovery by `rust-analyzer`.
+//! See [rust-analyzer documentation][rd] for a thorough description of this interface.
+//! [rd]: <https://rust-analyzer.github.io/manual.html#rust-analyzer.workspace.discoverConfig>.
+
+use std::{
+ env,
+ io::{self, Write},
+};
+
+use anyhow::Context;
+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,
+};
+use log::{LevelFilter, Record};
+
+/// Looks within the current directory for a file that marks a bazel workspace.
+///
+/// # Errors
+///
+/// Returns an error if no file from [`WORKSPACE_ROOT_FILE_NAMES`] is found.
+fn find_workspace_root_file(workspace: &Utf8Path) -> anyhow::Result<Utf8PathBuf> {
+ BUILD_FILE_NAMES
+ .iter()
+ .chain(WORKSPACE_ROOT_FILE_NAMES)
+ .map(|file| workspace.join(file))
+ .find(|p| p.exists())
+ .with_context(|| format!("no root file found for bazel workspace {workspace}"))
+}
+
+fn project_discovery() -> anyhow::Result<DiscoverProject<'static>> {
+ let Config {
+ workspace,
+ execution_root,
+ output_base,
+ bazel,
+ bazel_startup_options,
+ bazel_args,
+ rust_analyzer_argument,
+ } = Config::parse()?;
+
+ log::info!("got rust-analyzer argument: {rust_analyzer_argument:?}");
+
+ let ra_arg = match rust_analyzer_argument {
+ Some(ra_arg) => ra_arg,
+ None => RustAnalyzerArg::Buildfile(find_workspace_root_file(&workspace)?),
+ };
+
+ let rules_rust_name = env!("ASPECT_REPOSITORY");
+
+ log::info!("resolved rust-analyzer argument: {ra_arg:?}");
+
+ let (buildfile, targets) = ra_arg.into_target_details(&workspace)?;
+
+ log::debug!("got buildfile: {buildfile}");
+ log::debug!("got targets: {targets}");
+
+ // Use the generated files to print the rust-project.json.
+ let project = generate_rust_project(
+ &bazel,
+ &output_base,
+ &workspace,
+ &execution_root,
+ &bazel_startup_options,
+ &bazel_args,
+ rules_rust_name,
+ &[targets],
+ )?;
+
+ Ok(DiscoverProject::Finished { buildfile, project })
+}
+
+#[allow(clippy::writeln_empty_string)]
+fn write_discovery<W>(mut writer: W, discovery: DiscoverProject) -> std::io::Result<()>
+where
+ W: Write,
+{
+ serde_json::to_writer(&mut writer, &discovery)?;
+ // `rust-analyzer` reads messages line by line, so we must add a newline after each
+ writeln!(writer, "")
+}
+
+fn main() -> anyhow::Result<()> {
+ let log_format_fn = |fmt: &mut Formatter, rec: &Record| {
+ let message = rec.args();
+ let discovery = DiscoverProject::Progress { message };
+ write_discovery(fmt, discovery)
+ };
+
+ // 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(log_format_fn)
+ // `rust-analyzer` reads the stdout
+ .filter_level(LevelFilter::Debug)
+ .target(Target::Stdout)
+ .init();
+
+ let discovery = match project_discovery() {
+ Ok(discovery) => discovery,
+ Err(error) => DiscoverProject::Error {
+ error: error.to_string(),
+ source: error.source().as_ref().map(ToString::to_string),
+ },
+ };
+
+ write_discovery(io::stdout(), discovery)?;
+ Ok(())
+}
+
+#[derive(Debug)]
+pub struct Config {
+ /// The path to the Bazel workspace directory. If not specified, uses the result of `bazel info workspace`.
+ workspace: Utf8PathBuf,
+
+ /// The path to the Bazel execution root. If not specified, uses the result of `bazel info execution_root`.
+ execution_root: Utf8PathBuf,
+
+ /// The path to the Bazel output user root. If not specified, uses the result of `bazel info output_base`.
+ output_base: Utf8PathBuf,
+
+ /// The path to a Bazel binary.
+ bazel: Utf8PathBuf,
+
+ /// Startup options to pass to `bazel` invocations.
+ /// See the [Command-Line Reference](<https://bazel.build/reference/command-line-reference>)
+ /// for more details.
+ bazel_startup_options: Vec<String>,
+
+ /// Arguments to pass to `bazel` invocations.
+ /// See the [Command-Line Reference](<https://bazel.build/reference/command-line-reference>)
+ /// for more details.
+ bazel_args: Vec<String>,
+
+ /// The argument that `rust-analyzer` can pass to the binary.
+ rust_analyzer_argument: Option<RustAnalyzerArg>,
+}
+
+impl Config {
+ // Parse the configuration flags and supplement with bazel info as needed.
+ pub fn parse() -> anyhow::Result<Self> {
+ let ConfigParser {
+ workspace,
+ bazel,
+ bazel_startup_options,
+ bazel_args,
+ rust_analyzer_argument,
+ } = ConfigParser::parse();
+
+ // We need some info from `bazel info`. Fetch it now.
+ let mut info_map = bazel_info(
+ &bazel,
+ workspace.as_deref(),
+ None,
+ &bazel_startup_options,
+ &bazel_args,
+ )?;
+
+ let config = Config {
+ workspace: info_map
+ .remove("workspace")
+ .expect("'workspace' must exist in bazel info")
+ .into(),
+ execution_root: info_map
+ .remove("execution_root")
+ .expect("'execution_root' must exist in bazel info")
+ .into(),
+ output_base: info_map
+ .remove("output_base")
+ .expect("'output_base' must exist in bazel info")
+ .into(),
+ bazel,
+ bazel_startup_options,
+ bazel_args,
+ rust_analyzer_argument,
+ };
+
+ Ok(config)
+ }
+}
+
+#[derive(Debug, Parser)]
+struct ConfigParser {
+ /// 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<Utf8PathBuf>,
+
+ /// The path to a Bazel binary.
+ #[clap(long, default_value = "bazel")]
+ bazel: Utf8PathBuf,
+
+ /// Startup options to pass to `bazel` invocations.
+ /// See the [Command-Line Reference](<https://bazel.build/reference/command-line-reference>)
+ /// for more details.
+ #[clap(long = "bazel_startup_option")]
+ bazel_startup_options: Vec<String>,
+
+ /// Arguments to pass to `bazel` invocations.
+ /// See the [Command-Line Reference](<https://bazel.build/reference/command-line-reference>)
+ /// for more details.
+ #[clap(long = "bazel_arg")]
+ bazel_args: Vec<String>,
+
+ /// The argument that `rust-analyzer` can pass to the binary.
+ rust_analyzer_argument: Option<RustAnalyzerArg>,
+}
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 0000000..e7892e8
--- /dev/null
+++ b/tools/rust_analyzer/bin/gen_rust_project.rs
@@ -0,0 +1,179 @@
+use std::{
+ env,
+ fs::OpenOptions,
+ io::{BufWriter, ErrorKind},
+};
+
+use anyhow::{bail, Context};
+use camino::Utf8PathBuf;
+use clap::Parser;
+use gen_rust_project_lib::{bazel_info, generate_rust_project};
+
+fn write_rust_project() -> anyhow::Result<()> {
+ let Config {
+ workspace,
+ execution_root,
+ output_base,
+ bazel,
+ bazel_args,
+ targets,
+ } = Config::parse()?;
+
+ let rules_rust_name = env!("ASPECT_REPOSITORY");
+
+ let rust_project = generate_rust_project(
+ &bazel,
+ &output_base,
+ &workspace,
+ &execution_root,
+ &[],
+ &bazel_args,
+ rules_rust_name,
+ &targets,
+ )?;
+
+ let rust_project_path = &workspace.join("rust-project.json");
+
+ // 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(_) => {}
+ Err(err) if err.kind() == ErrorKind::NotFound => {}
+ Err(err) => bail!("Unexpected error removing old rust-project.json: {}", err),
+ }
+
+ // Write the new rust-project.json file.
+ let file = OpenOptions::new()
+ .write(true)
+ .create(true)
+ .truncate(true)
+ .open(rust_project_path)
+ .with_context(|| format!("could not open: {rust_project_path}"))
+ .map(BufWriter::new)?;
+
+ serde_json::to_writer(file, &rust_project)?;
+ Ok(())
+}
+
+// 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();
+
+ // Write rust-project.json.
+ write_rust_project()?;
+ Ok(())
+}
+
+#[derive(Debug)]
+pub struct Config {
+ /// The path to the Bazel workspace directory. If not specified, uses the result of `bazel info workspace`.
+ workspace: Utf8PathBuf,
+
+ /// The path to the Bazel execution root. If not specified, uses the result of `bazel info execution_root`.
+ execution_root: Utf8PathBuf,
+
+ /// The path to the Bazel output user root. If not specified, uses the result of `bazel info output_base`.
+ output_base: Utf8PathBuf,
+
+ /// The path to a Bazel binary.
+ bazel: Utf8PathBuf,
+
+ /// Arguments to pass to `bazel` invocations.
+ /// See the [Command-Line Reference](<https://bazel.build/reference/command-line-reference>)
+ /// for more details.
+ bazel_args: Vec<String>,
+
+ /// Space separated list of target patterns that comes after all other args.
+ targets: Vec<String>,
+}
+
+impl Config {
+ // Parse the configuration flags and supplement with bazel info as needed.
+ pub fn parse() -> anyhow::Result<Self> {
+ let ConfigParser {
+ workspace,
+ execution_root,
+ output_base,
+ bazel,
+ config,
+ targets,
+ } = ConfigParser::parse();
+
+ let bazel_args = config
+ .into_iter()
+ .map(|s| format!("--config={s}"))
+ .collect();
+
+ // Implemented this way instead of a classic `if let` to satisfy the
+ // borrow checker.
+ // See: <https://github.com/rust-lang/rust/issues/54663>
+ #[allow(clippy::unnecessary_unwrap)]
+ 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,
+ bazel_args,
+ targets,
+ });
+ }
+
+ // We need some info from `bazel info`. Fetch it now.
+ let mut info_map = bazel_info(
+ &bazel,
+ workspace.as_deref(),
+ output_base.as_deref(),
+ &[],
+ &[],
+ )?;
+
+ let config = Config {
+ workspace: info_map
+ .remove("workspace")
+ .expect("'workspace' must exist in bazel info")
+ .into(),
+ execution_root: info_map
+ .remove("execution_root")
+ .expect("'execution_root' must exist in bazel info")
+ .into(),
+ output_base: info_map
+ .remove("output_base")
+ .expect("'output_base' must exist in bazel info")
+ .into(),
+ bazel,
+ bazel_args,
+ targets,
+ };
+
+ Ok(config)
+ }
+}
+
+#[derive(Debug, Parser)]
+struct ConfigParser {
+ /// 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<Utf8PathBuf>,
+
+ /// The path to the Bazel execution root. If not specified, uses the result of `bazel info execution_root`.
+ #[clap(long)]
+ execution_root: Option<Utf8PathBuf>,
+
+ /// 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<Utf8PathBuf>,
+
+ /// The path to a Bazel binary.
+ #[clap(long, default_value = "bazel")]
+ bazel: Utf8PathBuf,
+
+ /// A config to pass to Bazel invocations with `--config=<config>`.
+ #[clap(long)]
+ config: Option<String>,
+
+ /// Space separated list of target patterns that comes after all other args.
+ #[clap(default_value = "@//...")]
+ targets: Vec<String>,
+}
diff --git a/tools/rust_analyzer/lib.rs b/tools/rust_analyzer/lib.rs
index 6842216..4ea641e 100644
--- a/tools/rust_analyzer/lib.rs
+++ b/tools/rust_analyzer/lib.rs
@@ -1,94 +1,200 @@
-use std::collections::HashMap;
-use std::path::Path;
-use std::process::Command;
-
-use anyhow::anyhow;
-use runfiles::Runfiles;
-
mod aquery;
mod rust_project;
-pub fn generate_crate_info(
- bazel: impl AsRef<Path>,
- config: &Option<String>,
- workspace: impl AsRef<Path>,
- rules_rust: impl AsRef<str>,
+use std::{collections::BTreeMap, convert::TryInto, fs, process::Command};
+
+use anyhow::{bail, Context};
+use camino::{Utf8Path, Utf8PathBuf};
+use runfiles::Runfiles;
+use rust_project::RustProject;
+pub use rust_project::{DiscoverProject, RustAnalyzerArg};
+use serde::{de::DeserializeOwned, Deserialize};
+
+pub const WORKSPACE_ROOT_FILE_NAMES: &[&str] =
+ &["MODULE.bazel", "REPO.bazel", "WORKSPACE.bazel", "WORKSPACE"];
+
+pub const BUILD_FILE_NAMES: &[&str] = &["BUILD.bazel", "BUILD"];
+
+#[allow(clippy::too_many_arguments)]
+pub fn generate_rust_project(
+ bazel: &Utf8Path,
+ output_base: &Utf8Path,
+ workspace: &Utf8Path,
+ execution_root: &Utf8Path,
+ bazel_startup_options: &[String],
+ bazel_args: &[String],
+ rules_rust_name: &str,
+ targets: &[String],
+) -> anyhow::Result<RustProject> {
+ generate_crate_info(
+ bazel,
+ output_base,
+ workspace,
+ bazel_startup_options,
+ bazel_args,
+ rules_rust_name,
+ targets,
+ )?;
+
+ let crate_specs = aquery::get_crate_specs(
+ bazel,
+ output_base,
+ workspace,
+ execution_root,
+ bazel_startup_options,
+ bazel_args,
+ targets,
+ rules_rust_name,
+ )?;
+
+ let path: Utf8PathBuf = runfiles::rlocation!(
+ Runfiles::create()?,
+ "rules_rust/rust/private/rust_analyzer_detect_sysroot.rust_analyzer_toolchain.json"
+ )
+ .context("toolchain runfile not found")?
+ .try_into()?;
+
+ let toolchain_info = deserialize_file_content(&path, output_base, workspace, execution_root)?;
+
+ rust_project::assemble_rust_project(bazel, workspace, toolchain_info, &crate_specs)
+}
+
+/// Executes `bazel info` to get a map of context information.
+pub fn bazel_info(
+ bazel: &Utf8Path,
+ workspace: Option<&Utf8Path>,
+ output_base: Option<&Utf8Path>,
+ bazel_startup_options: &[String],
+ bazel_args: &[String],
+) -> anyhow::Result<BTreeMap<String, String>> {
+ let output = bazel_command(bazel, workspace, output_base)
+ .args(bazel_startup_options)
+ .arg("info")
+ .args(bazel_args)
+ .output()?;
+
+ if !output.status.success() {
+ let status = output.status;
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ bail!("bazel info failed: ({status:?})\n{stderr}");
+ }
+
+ // Extract and parse the output.
+ let info_map = String::from_utf8(output.stdout)?
+ .trim()
+ .split('\n')
+ .filter_map(|line| line.split_once(':'))
+ .map(|(k, v)| (k.to_owned(), v.trim().to_owned()))
+ .collect();
+
+ Ok(info_map)
+}
+
+fn generate_crate_info(
+ bazel: &Utf8Path,
+ output_base: &Utf8Path,
+ workspace: &Utf8Path,
+ bazel_startup_options: &[String],
+ bazel_args: &[String],
+ rules_rust: &str,
targets: &[String],
) -> anyhow::Result<()> {
+ log::info!("running bazel build...");
log::debug!("Building rust_analyzer_crate_spec files for {:?}", targets);
- let config_args = match config {
- Some(config) => vec!["--config", config],
- None => Vec::new(),
- };
- let output = Command::new(bazel.as_ref())
- .current_dir(workspace.as_ref())
- .env_remove("BAZELISK_SKIP_WRAPPER")
- .env_remove("BUILD_WORKING_DIRECTORY")
- .env_remove("BUILD_WORKSPACE_DIRECTORY")
+ let output = bazel_command(bazel, Some(workspace), Some(output_base))
+ .args(bazel_startup_options)
.arg("build")
- .args(config_args)
+ .args(bazel_args)
.arg("--norun_validations")
.arg("--remote_download_all")
.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_dylib,rust_analyzer_src")
.args(targets)
.output()?;
if !output.status.success() {
- return Err(anyhow!(
- "bazel build failed:({})\n{}",
- output.status,
- String::from_utf8_lossy(&output.stderr)
- ));
+ let status = output.status;
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ bail!("bazel build failed: ({status})\n{stderr}");
}
+ log::info!("bazel build finished");
+
Ok(())
}
-#[allow(clippy::too_many_arguments)]
-pub fn write_rust_project(
- bazel: impl AsRef<Path>,
- config: &Option<String>,
- workspace: impl AsRef<Path>,
- rules_rust_name: &impl AsRef<str>,
- targets: &[String],
- execution_root: impl AsRef<Path>,
- output_base: impl AsRef<Path>,
- rust_project_path: impl AsRef<Path>,
-) -> anyhow::Result<()> {
- let crate_specs = aquery::get_crate_specs(
- bazel.as_ref(),
- config,
- workspace.as_ref(),
- execution_root.as_ref(),
- targets,
- rules_rust_name.as_ref(),
- )?;
+fn bazel_command(
+ bazel: &Utf8Path,
+ workspace: Option<&Utf8Path>,
+ output_base: Option<&Utf8Path>,
+) -> Command {
+ let mut cmd = Command::new(bazel);
- let path = runfiles::rlocation!(
- Runfiles::create()?,
- "rules_rust/rust/private/rust_analyzer_detect_sysroot.rust_analyzer_toolchain.json"
- )
- .unwrap();
- let toolchain_info: HashMap<String, String> =
- serde_json::from_str(&std::fs::read_to_string(path)?)?;
+ cmd
+ // Switch to the workspace directory if one was provided.
+ .current_dir(workspace.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.map(|s| format!("--output_base={s}")));
- let sysroot_src = &toolchain_info["sysroot_src"];
- let sysroot = &toolchain_info["sysroot"];
+ cmd
+}
- let rust_project = rust_project::generate_rust_project(sysroot, sysroot_src, &crate_specs)?;
+fn deserialize_file_content<T>(
+ path: &Utf8Path,
+ output_base: &Utf8Path,
+ workspace: &Utf8Path,
+ execution_root: &Utf8Path,
+) -> anyhow::Result<T>
+where
+ T: DeserializeOwned,
+{
+ let content = fs::read_to_string(path)
+ .with_context(|| format!("failed to read file: {path}"))?
+ .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());
- rust_project::write_rust_project(
- rust_project_path.as_ref(),
- workspace.as_ref(),
- execution_root.as_ref(),
- output_base.as_ref(),
- &rust_project,
- )?;
+ log::trace!("{}\n{}", path, content);
- Ok(())
+ serde_json::from_str(&content).with_context(|| format!("failed to deserialize file: {path}"))
+}
+
+/// `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 source_file_to_buildfile(file: &Utf8Path) -> anyhow::Result<Utf8PathBuf> {
+ // Skip the first element as it's always the full file path.
+ file.ancestors()
+ .skip(1)
+ .flat_map(|dir| BUILD_FILE_NAMES.iter().map(move |build| dir.join(build)))
+ .find(|p| p.exists())
+ .with_context(|| format!("no buildfile found for {file}"))
+}
+
+fn buildfile_to_targets(workspace: &Utf8Path, buildfile: &Utf8Path) -> anyhow::Result<String> {
+ 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}:all"),
+ _ => "//...".to_string(),
+ };
+
+ Ok(targets)
+}
+
+#[derive(Debug, Deserialize)]
+struct ToolchainInfo {
+ sysroot: Utf8PathBuf,
+ sysroot_src: Utf8PathBuf,
}
diff --git a/tools/rust_analyzer/main.rs b/tools/rust_analyzer/main.rs
deleted file mode 100644
index f1dc014..0000000
--- a/tools/rust_analyzer/main.rs
+++ /dev/null
@@ -1,136 +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,
- &config.config,
- workspace_root,
- rules_rust_name,
- &config.targets,
- )?;
-
- // Use the generated files to write rust-project.json.
- write_rust_project(
- &config.bazel,
- &config.config,
- 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<Config> {
- 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::<HashMap<_, _>>();
-
- 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<PathBuf>,
-
- /// The path to the Bazel execution root. If not specified, uses the result of `bazel info execution_root`.
- #[clap(long)]
- execution_root: Option<PathBuf>,
-
- /// 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<PathBuf>,
-
- /// A config to pass to Bazel invocations with `--config=<config>`.
- #[clap(long)]
- config: Option<String>,
-
- /// 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<String>,
-}
diff --git a/tools/rust_analyzer/rust_project.rs b/tools/rust_analyzer/rust_project.rs
index f694267..749347e 100644
--- a/tools/rust_analyzer/rust_project.rs
+++ b/tools/rust_analyzer/rust_project.rs
@@ -1,14 +1,74 @@
//! Library for generating rust_project.json files from a `Vec<CrateSpec>`
//! See official documentation of file format at https://rust-analyzer.github.io/manual.html
-use std::collections::{BTreeMap, BTreeSet, HashMap};
-use std::io::ErrorKind;
-use std::path::Path;
+use core::fmt;
+use std::{
+ collections::{BTreeMap, BTreeSet, HashMap},
+ str::FromStr,
+};
-use anyhow::anyhow;
-use serde::Serialize;
+use anyhow::{anyhow, Context};
+use camino::{Utf8Path, Utf8PathBuf};
+use serde::{Deserialize, Serialize};
-use crate::aquery::CrateSpec;
+use crate::{
+ aquery::{CrateSpec, CrateType},
+ buildfile_to_targets, source_file_to_buildfile, ToolchainInfo,
+};
+
+/// The argument that `rust-analyzer` can pass to the workspace discovery 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 into_target_details(
+ self,
+ workspace: &Utf8Path,
+ ) -> anyhow::Result<(Utf8PathBuf, String)> {
+ match self {
+ Self::Path(file) => {
+ let buildfile = source_file_to_buildfile(&file)?;
+ buildfile_to_targets(workspace, &buildfile).map(|t| (buildfile, t))
+ }
+ Self::Buildfile(buildfile) => {
+ buildfile_to_targets(workspace, &buildfile).map(|t| (buildfile, t))
+ }
+ }
+ }
+}
+
+impl FromStr for RustAnalyzerArg {
+ type Err = anyhow::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ serde_json::from_str(s).context("rust analyzer argument error")
+ }
+}
+
+/// The format that `rust_analyzer` expects as a response when automatically invoked.
+/// See [rust-analyzer documentation][rd] for a thorough description of this interface.
+/// [rd]: <https://rust-analyzer.github.io/manual.html#rust-analyzer.workspace.discoverConfig>.
+#[derive(Debug, Serialize)]
+#[serde(tag = "kind")]
+#[serde(rename_all = "snake_case")]
+pub enum DiscoverProject<'a> {
+ Finished {
+ buildfile: Utf8PathBuf,
+ project: RustProject,
+ },
+ Error {
+ error: String,
+ source: Option<String>,
+ },
+ Progress {
+ message: &'a fmt::Arguments<'a>,
+ },
+}
/// A `rust-project.json` workspace representation. See
/// [rust-analyzer documentation][rd] for a thorough description of this interface.
@@ -16,24 +76,27 @@
#[derive(Debug, Serialize)]
pub struct RustProject {
/// The path to a Rust sysroot.
- sysroot: Option<String>,
+ sysroot: Utf8PathBuf,
/// Path to the directory with *source code* of
/// sysroot crates.
- sysroot_src: Option<String>,
+ sysroot_src: Utf8PathBuf,
/// The set of crates comprising the current
/// project. Must include all transitive
/// dependencies as well as sysroot crate (libstd,
/// libcore and such).
crates: Vec<Crate>,
+
+ /// The set of runnables, such as tests or benchmarks,
+ /// that can be found in the crate.
+ runnables: Vec<Runnable>,
}
/// A `rust-project.json` crate representation. See
/// [rust-analyzer documentation][rd] for a thorough description of this interface.
/// [rd]: https://rust-analyzer.github.io/manual.html#non-cargo-based-projects
#[derive(Debug, Serialize)]
-#[serde(default)]
pub struct Crate {
/// A name used in the package's project declaration
#[serde(skip_serializing_if = "Option::is_none")]
@@ -74,6 +137,10 @@
/// For proc-macro crates, path to compiled proc-macro (.so file).
#[serde(skip_serializing_if = "Option::is_none")]
proc_macro_dylib_path: Option<String>,
+
+ /// Build information for the crate
+ #[serde(skip_serializing_if = "Option::is_none")]
+ build: Option<Build>,
}
#[derive(Debug, Default, Serialize)]
@@ -83,7 +150,6 @@
}
impl Source {
- /// Returns true if no include information has been added.
fn is_empty(&self) -> bool {
self.include_dirs.is_empty() && self.exclude_dirs.is_empty()
}
@@ -99,18 +165,131 @@
name: String,
}
-pub fn generate_rust_project(
- sysroot: &str,
- sysroot_src: &str,
- crates: &BTreeSet<CrateSpec>,
+#[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`].
+ 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`.
+ 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.
+ 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.
+ 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.
+ args: Vec<String>,
+ /// The current working directory of the runnable.
+ cwd: Utf8PathBuf,
+ 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 assemble_rust_project(
+ bazel: &Utf8Path,
+ workspace: &Utf8Path,
+ toolchain_info: ToolchainInfo,
+ crate_specs: &BTreeSet<CrateSpec>,
) -> anyhow::Result<RustProject> {
let mut project = RustProject {
- sysroot: Some(sysroot.into()),
- sysroot_src: Some(sysroot_src.into()),
+ sysroot: toolchain_info.sysroot,
+ sysroot_src: toolchain_info.sysroot_src,
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();
+ let mut unmerged_crates: Vec<&CrateSpec> = crate_specs.iter().collect();
let mut skipped_crates: Vec<&CrateSpec> = Vec::new();
let mut merged_crates_index: HashMap<String, usize> = HashMap::new();
@@ -133,6 +312,29 @@
} 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 +372,11 @@
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().into(),
+ target_kind,
+ }),
});
}
}
@@ -241,51 +448,6 @@
None
}
-pub fn write_rust_project(
- rust_project_path: &Path,
- workspace: &Path,
- execution_root: &Path,
- output_base: &Path,
- 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(_) => {}
- Err(err) if err.kind() == ErrorKind::NotFound => {}
- Err(err) => {
- return Err(anyhow!(
- "Unexpected error removing old rust-project.json: {}",
- err
- ))
- }
- }
-
- // 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);
-
- // Write the new rust-project.json file.
- std::fs::write(rust_project_path, rust_project_content)?;
-
- Ok(())
-}
-
#[cfg(test)]
mod tests {
use super::*;
@@ -293,9 +455,13 @@
/// A simple example with a single crate and no dependencies.
#[test]
fn generate_rust_project_single() {
- let project = generate_rust_project(
- "sysroot",
- "sysroot_src",
+ let project = assemble_rust_project(
+ Utf8Path::new("bazel"),
+ Utf8Path::new("workspace"),
+ ToolchainInfo {
+ sysroot: "sysroot".to_owned().into(),
+ sysroot_src: "sysroot_src".to_owned().into(),
+ },
&BTreeSet::from([CrateSpec {
aliases: BTreeMap::new(),
crate_id: "ID-example".into(),
@@ -309,7 +475,9 @@
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");
@@ -324,9 +492,13 @@
/// An example with a one crate having two dependencies.
#[test]
fn generate_rust_project_with_deps() {
- let project = generate_rust_project(
- "sysroot",
- "sysroot_src",
+ let project = assemble_rust_project(
+ Utf8Path::new("bazel"),
+ Utf8Path::new("workspace"),
+ ToolchainInfo {
+ sysroot: "sysroot".to_owned().into(),
+ sysroot_src: "sysroot_src".to_owned().into(),
+ },
&BTreeSet::from([
CrateSpec {
aliases: BTreeMap::new(),
@@ -341,7 +513,9 @@
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 +530,9 @@
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 +547,9 @@
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,
},
]),
)