Skip to content

Commit 4d399f5

Browse files
bobozaurkrasimirgg
andauthored
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 <[email protected]>
1 parent 922b8d0 commit 4d399f5

25 files changed

+1452
-361
lines changed

.bazelci/presubmit.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -885,7 +885,7 @@ tasks:
885885
build_flags:
886886
- "--compile_one_dependency"
887887
build_targets:
888-
- "tools/rust_analyzer/main.rs"
888+
- "tools/rust_analyzer/bin/gen_rust_project.rs"
889889
extensions_bindgen_linux:
890890
platform: ubuntu2004
891891
name: Extensions Bindgen

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ user.bazelrc
3030
.vscode
3131
*.code-workspace
3232

33+
# zed
34+
.zed
35+
3336
# JetBrains
3437
.idea
3538
.idea/**

MODULE.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use_repo(
2222
internal_deps,
2323
"rrra",
2424
"rrra__anyhow-1.0.71",
25+
"rrra__camino-1.1.9",
2526
"rrra__clap-4.3.11",
2627
"rrra__env_logger-0.10.0",
2728
"rrra__itertools-0.11.0",

docs/rust_analyzer.vm

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
## Overview
33

44
For [non-Cargo projects](https://rust-analyzer.github.io/manual.html#non-cargo-based-projects),
5-
[rust-analyzer](https://rust-analyzer.github.io/) depends on a `rust-project.json` file at the
6-
root of the project that describes its structure. The `rust_analyzer` rule facilitates generating
7-
such a file.
5+
[rust-analyzer](https://rust-analyzer.github.io/) depends on either a `rust-project.json` file
6+
at the root of the project that describes its structure or on build system specific
7+
[project auto-discovery](https://rust-analyzer.github.io/manual.html#rust-analyzer.workspace.discoverConfig).
8+
The `rust_analyzer` rules facilitate both approaches.
89

10+
## rust-project.json approach
911
### Setup
1012

1113
First, ensure `rules_rust` is setup in your workspace. By default, `rust_register_toolchains` will
@@ -84,4 +86,71 @@ build --@rules_rust//rust/settings:rustc_output_diagnostics=true --output_groups
8486

8587
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.
8688

87-
]]#
89+
## Project auto-discovery
90+
### Setup
91+
92+
Auto-discovery makes `rust-analyzer` behave in a Bazel project in a similar fashion to how it does
93+
in a Cargo project. This is achieved by generating a structure similar to what `rust-project.json`
94+
contains but, instead of writing that to a file, the data gets piped to `rust-analyzer` directly
95+
through `stdout`. To use auto-discovery the `rust-analyzer` IDE settings must be configured similar to:
96+
97+
```json
98+
"rust-analyzer": {
99+
"workspace": {
100+
"discoverConfig": {
101+
"command": ["discover_bazel_rust_project.sh"],
102+
"progressLabel": "rust_analyzer",
103+
"filesToWatch": ["BUILD", "BUILD.bazel", "MODULE.bazel"]
104+
}
105+
}
106+
}
107+
```
108+
109+
The shell script passed to `discoverConfig.command` is typically meant to wrap the bazel rule invocation,
110+
primarily for muting `stderr` (because `rust-analyzer` will consider that an error has occurred if anything
111+
is passed through `stderr`) and, additionally, for specifying rule arguments. E.g:
112+
113+
```shell
114+
#!/usr/bin/bash
115+
116+
bazel \
117+
run \
118+
@rules_rust//tools/rust_analyzer:discover_bazel_rust_project -- \
119+
--bazel_startup_option=--output_base=~/ide_bazel \
120+
--bazel_arg=--watchfs \
121+
${1:+"$1"} 2>/dev/null
122+
```
123+
124+
The script above also handles an optional CLI argument which gets passed when workspace splitting is
125+
enabled. The script path should be either absolute or relative to the project root.
126+
127+
### Workspace splitting
128+
129+
The above configuration treats the entire project as a single workspace. However, large codebases might be
130+
too much to handle for `rust-analyzer` all at once. This can be addressed by splitting the codebase in
131+
multiple workspaces by extending the `discoverConfig.command` setting:
132+
133+
```json
134+
"rust-analyzer": {
135+
"workspace": {
136+
"discoverConfig": {
137+
"command": ["discover_bazel_rust_project.sh", "{arg}"],
138+
"progressLabel": "rust_analyzer",
139+
"filesToWatch": ["BUILD", "BUILD.bazel", "MODULE.bazel"]
140+
}
141+
}
142+
}
143+
```
144+
145+
`{arg}` acts as a placeholder that `rust-analyzer` replaces with the path of the source / build file
146+
that gets opened.
147+
148+
The root of the workspace will, in this configuration, be the package the crate currently being worked on
149+
belongs to. This means that only that package and its dependencies get built and indexed by `rust-analyzer`,
150+
thus allowing for a smaller footprint.
151+
152+
`rust-analyzer` will switch workspaces whenever an out-of-tree file gets opened, essentially indexing that
153+
crate and its dependencies separately. A caveat of this is that *dependents* of the crate currently being
154+
worked on are not indexed and won't be tracked by `rust-analyzer`.
155+
156+
]]#

extensions/prost/private/prost.bzl

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,14 +308,18 @@ def _rust_prost_aspect_impl(target, ctx):
308308
# https://github.com/rust-analyzer/rust-analyzer/blob/2021-11-15/crates/project_model/src/workspace.rs#L529-L531
309309
cfgs = ["test", "debug_assertions"]
310310

311+
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
312+
311313
rust_analyzer_info = write_rust_analyzer_spec_file(ctx, ctx.rule.attr, ctx.label, RustAnalyzerInfo(
312314
aliases = {},
313315
crate = dep_variant_info.crate_info,
314316
cfgs = cfgs,
315317
env = dep_variant_info.crate_info.rustc_env,
316318
deps = rust_analyzer_deps,
317319
crate_specs = depset(transitive = [dep.crate_specs for dep in rust_analyzer_deps]),
318-
proc_macro_dylib_path = None,
320+
proc_macro_dylibs = depset(transitive = [dep.proc_macro_dylibs for dep in rust_analyzer_deps]),
321+
build_info_out_dirs = depset(direct = build_info_out_dirs, transitive = [dep.build_info_out_dirs for dep in rust_analyzer_deps]),
322+
proc_macro_dylib = None,
319323
build_info = dep_variant_info.build_info,
320324
))
321325

rust/private/providers.bzl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,14 @@ RustAnalyzerInfo = provider(
159159
fields = {
160160
"aliases": "Dict[RustAnalyzerInfo, String]: Replacement names these targets should be known as in Rust code",
161161
"build_info": "BuildInfo: build info for this crate if present",
162+
"build_info_out_dirs": "Depset[File]: transitive closure of build script out dirs",
162163
"cfgs": "List[String]: features or other compilation `--cfg` settings",
163164
"crate": "CrateInfo: Crate information.",
164-
"crate_specs": "Depset[File]: transitive closure of OutputGroupInfo files",
165+
"crate_specs": "Depset[File]: transitive closure of crate spec files",
165166
"deps": "List[RustAnalyzerInfo]: direct dependencies",
166167
"env": "Dict[String: String]: Environment variables, used for the `env!` macro",
167-
"proc_macro_dylib_path": "File: compiled shared library output of proc-macro rule",
168+
"proc_macro_dylib": "File: if this is a proc-macro target, the shared library output",
169+
"proc_macro_dylibs": "Depset[File]: transitive closure of proc-macro shared library files",
168170
},
169171
)
170172

rust/private/rust_analyzer.bzl

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ def write_rust_analyzer_spec_file(ctx, attrs, owner, base_info):
5454
env = base_info.env,
5555
deps = base_info.deps,
5656
crate_specs = depset(direct = [crate_spec], transitive = [base_info.crate_specs]),
57-
proc_macro_dylib_path = base_info.proc_macro_dylib_path,
57+
proc_macro_dylibs = depset(transitive = [base_info.proc_macro_dylibs]),
58+
build_info_out_dirs = depset(transitive = [base_info.build_info_out_dirs]),
59+
proc_macro_dylib = base_info.proc_macro_dylib,
5860
build_info = base_info.build_info,
5961
)
6062

@@ -135,30 +137,40 @@ def _rust_analyzer_aspect_impl(target, ctx):
135137
if aliased_target.label in labels_to_rais:
136138
aliases[labels_to_rais[aliased_target.label]] = aliased_name
137139

140+
proc_macro_dylib = find_proc_macro_dylib(toolchain, target)
141+
proc_macro_dylibs = [proc_macro_dylib] if proc_macro_dylib else None
142+
build_info_out_dirs = [build_info.out_dir] if build_info != None and build_info.out_dir != None else None
143+
138144
rust_analyzer_info = write_rust_analyzer_spec_file(ctx, ctx.rule.attr, ctx.label, RustAnalyzerInfo(
139145
aliases = aliases,
140146
crate = crate_info,
141147
cfgs = cfgs,
142148
env = crate_info.rustc_env,
143149
deps = dep_infos,
144150
crate_specs = depset(transitive = [dep.crate_specs for dep in dep_infos]),
145-
proc_macro_dylib_path = find_proc_macro_dylib_path(toolchain, target),
151+
proc_macro_dylibs = depset(direct = proc_macro_dylibs, transitive = [dep.proc_macro_dylibs for dep in dep_infos]),
152+
build_info_out_dirs = depset(direct = build_info_out_dirs, transitive = [dep.build_info_out_dirs for dep in dep_infos]),
153+
proc_macro_dylib = proc_macro_dylib,
146154
build_info = build_info,
147155
))
148156

149157
return [
150158
rust_analyzer_info,
151-
OutputGroupInfo(rust_analyzer_crate_spec = rust_analyzer_info.crate_specs),
159+
OutputGroupInfo(
160+
rust_analyzer_crate_spec = rust_analyzer_info.crate_specs,
161+
rust_analyzer_proc_macro_dylib = rust_analyzer_info.proc_macro_dylibs,
162+
rust_analyzer_src = rust_analyzer_info.build_info_out_dirs,
163+
),
152164
]
153165

154-
def find_proc_macro_dylib_path(toolchain, target):
155-
"""Find the proc_macro_dylib_path of target. Returns None if target crate is not type proc-macro.
166+
def find_proc_macro_dylib(toolchain, target):
167+
"""Find the proc_macro_dylib of target. Returns None if target crate is not type proc-macro.
156168
157169
Args:
158170
toolchain: The current rust toolchain.
159171
target: The current target.
160172
Returns:
161-
(path): The path to the proc macro dylib, or None if this crate is not a proc-macro.
173+
(File): The path to the proc macro dylib, or None if this crate is not a proc-macro.
162174
"""
163175
if rust_common.crate_info in target:
164176
crate_info = target[rust_common.crate_info]
@@ -174,7 +186,7 @@ def find_proc_macro_dylib_path(toolchain, target):
174186
for action in target.actions:
175187
for output in action.outputs.to_list():
176188
if output.extension == dylib_ext[1:]:
177-
return output.path
189+
return output
178190

179191
# Failed to find the dylib path inside a proc-macro crate.
180192
# TODO: Should this be an error?
@@ -188,7 +200,7 @@ rust_analyzer_aspect = aspect(
188200
)
189201

190202
# Paths in the generated JSON file begin with one of these placeholders.
191-
# The gen_rust_project driver will replace them with absolute paths.
203+
# The `rust-analyzer` driver will replace them with absolute paths.
192204
_WORKSPACE_TEMPLATE = "__WORKSPACE__/"
193205
_EXEC_ROOT_TEMPLATE = "__EXEC_ROOT__/"
194206
_OUTPUT_BASE_TEMPLATE = "__OUTPUT_BASE__/"
@@ -220,6 +232,7 @@ def _create_single_crate(ctx, attrs, info):
220232
crate["edition"] = info.crate.edition
221233
crate["env"] = {}
222234
crate["crate_type"] = info.crate.type
235+
crate["is_test"] = info.crate.is_test
223236

224237
# Switch on external/ to determine if crates are in the workspace or remote.
225238
# TODO: Some folks may want to override this for vendored dependencies.
@@ -230,6 +243,14 @@ def _create_single_crate(ctx, attrs, info):
230243
crate["root_module"] = path_prefix + info.crate.root.path
231244
crate["source"] = {"exclude_dirs": [], "include_dirs": []}
232245

246+
# We're only interested in the build info for local crates as these are the
247+
# only ones for which we want build file watching and code lens runnables support.
248+
if not is_external and not is_generated:
249+
crate["build"] = {
250+
"build_file": _WORKSPACE_TEMPLATE + ctx.build_file_path,
251+
"label": ctx.label.package + ":" + ctx.label.name,
252+
}
253+
233254
if is_generated:
234255
srcs = getattr(ctx.rule.files, "srcs", [])
235256
src_map = {src.short_path: src for src in srcs if src.is_source}
@@ -268,8 +289,8 @@ def _create_single_crate(ctx, attrs, info):
268289
crate["cfg"] = info.cfgs
269290
toolchain = find_toolchain(ctx)
270291
crate["target"] = (_EXEC_ROOT_TEMPLATE + toolchain.target_json.path) if toolchain.target_json else toolchain.target_flag_value
271-
if info.proc_macro_dylib_path != None:
272-
crate["proc_macro_dylib_path"] = _EXEC_ROOT_TEMPLATE + info.proc_macro_dylib_path
292+
if info.proc_macro_dylib != None:
293+
crate["proc_macro_dylib_path"] = _EXEC_ROOT_TEMPLATE + info.proc_macro_dylib.path
273294
return crate
274295

275296
def _rust_analyzer_toolchain_impl(ctx):
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
load(
2+
"@rules_rust//rust:defs.bzl",
3+
"rust_shared_library",
4+
"rust_static_library",
5+
"rust_test",
6+
)
7+
8+
rust_shared_library(
9+
name = "greeter_cdylib",
10+
srcs = [
11+
"greeter.rs",
12+
"shared_lib.rs",
13+
],
14+
crate_root = "shared_lib.rs",
15+
edition = "2018",
16+
)
17+
18+
rust_static_library(
19+
name = "greeter_staticlib",
20+
srcs = [
21+
"greeter.rs",
22+
"static_lib.rs",
23+
],
24+
crate_root = "static_lib.rs",
25+
edition = "2018",
26+
)
27+
28+
rust_test(
29+
name = "auto_discovery_json_test",
30+
srcs = ["auto_discovery_json_test.rs"],
31+
data = [":auto-discovery.json"],
32+
edition = "2018",
33+
env = {"AUTO_DISCOVERY_JSON": "$(rootpath :auto-discovery.json)"},
34+
# This target is tagged as manual since it's not expected to pass in
35+
# contexts outside of `//test/rust_analyzer:rust_analyzer_test`. Run
36+
# that target to execute this test.
37+
tags = ["manual"],
38+
deps = [
39+
"//test/rust_analyzer/3rdparty/crates:serde",
40+
"//test/rust_analyzer/3rdparty/crates:serde_json",
41+
],
42+
)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#[cfg(test)]
2+
mod tests {
3+
use serde::Deserialize;
4+
use std::env;
5+
use std::path::PathBuf;
6+
7+
#[derive(Deserialize)]
8+
#[serde(tag = "kind")]
9+
#[serde(rename_all = "snake_case")]
10+
enum DiscoverProject {
11+
Finished { project: Project },
12+
Progress {},
13+
}
14+
15+
#[derive(Deserialize)]
16+
struct Project {
17+
crates: Vec<Crate>,
18+
}
19+
20+
#[derive(Deserialize)]
21+
struct Crate {
22+
display_name: String,
23+
root_module: String,
24+
}
25+
26+
#[test]
27+
fn test_static_and_shared_lib() {
28+
let rust_project_path = PathBuf::from(env::var("AUTO_DISCOVERY_JSON").unwrap());
29+
let content = std::fs::read_to_string(&rust_project_path)
30+
.unwrap_or_else(|_| panic!("couldn't open {:?}", &rust_project_path));
31+
println!("{}", content);
32+
33+
for line in content.lines() {
34+
let discovery: DiscoverProject =
35+
serde_json::from_str(line).expect("Failed to deserialize discovery JSON");
36+
37+
let project = match discovery {
38+
DiscoverProject::Finished { project } => project,
39+
DiscoverProject::Progress {} => continue,
40+
};
41+
42+
let cdylib = project
43+
.crates
44+
.iter()
45+
.find(|c| &c.display_name == "greeter_cdylib")
46+
.unwrap();
47+
assert!(cdylib.root_module.ends_with("/shared_lib.rs"));
48+
49+
let staticlib = project
50+
.crates
51+
.iter()
52+
.find(|c| &c.display_name == "greeter_staticlib")
53+
.unwrap();
54+
assert!(staticlib.root_module.ends_with("/static_lib.rs"));
55+
}
56+
}
57+
}

0 commit comments

Comments
 (0)