Skip to content

rust-analyzer discoverConfig integration #3073

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 56 commits into from
Mar 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
7950704
added usage of camino crate
bobozaur Dec 5, 2024
7aa0704
add discover_rust_project target
bobozaur Dec 7, 2024
ca89f45
fix tests
bobozaur Dec 10, 2024
c74e74f
allow passing in a config group
bobozaur Dec 10, 2024
e618743
remove targets query
bobozaur Dec 12, 2024
4738eaf
add rust_analyzer_proc_macro_dylibs and rust_analyzer_build_info_out_…
bobozaur Dec 13, 2024
a565b73
improve crate spec merging
bobozaur Dec 16, 2024
36d3140
add is_test to crate spec
bobozaur Dec 16, 2024
138d7bd
better runnables support
bobozaur Dec 16, 2024
1a00fdc
rename rust_analyzer_build_info_out_dirs to rust_analyzer_srcs
bobozaur Jan 5, 2025
c56202a
adjust RustAnalyzerInfo provider field comments
bobozaur Jan 5, 2025
ffb6c97
adjust build info storing comment
bobozaur Jan 5, 2025
a65662a
comment the discover_rust_project.rs file
bobozaur Jan 5, 2025
5ff6e7c
move RustAnalyzerArg to rust_project module
bobozaur Jan 5, 2025
a3bdb59
use pkg:all wildcard instead of pkg/...
bobozaur Jan 5, 2025
6a9f026
rename Config::parse_and_refine to Config::parse
bobozaur Jan 5, 2025
fd0c6cd
rename generate_rust_project function to assemble_rust_project
bobozaur Jan 5, 2025
281ce94
extract binary specific code from library
bobozaur Jan 5, 2025
d4c99f2
probe fs for workspace root if no ra arg is passed
bobozaur Jan 5, 2025
56e470a
probe fs to find buildfile instead of bazel query
bobozaur Jan 5, 2025
a805e6c
fix workspace root file finding
bobozaur Jan 6, 2025
fa825fa
tweaked logs
bobozaur Jan 6, 2025
2e25958
extracted changes from write_rust_analyzer_spec_file
bobozaur Jan 6, 2025
cb90729
rebase
bobozaur Jan 9, 2025
886cf3e
let each binary have their own config
bobozaur Jan 9, 2025
559dd6e
completely remove the config group CLI arg
bobozaur Jan 10, 2025
484f29c
add get_bazel_info function
bobozaur Jan 10, 2025
453892b
implement BazelCommand trait
bobozaur Jan 10, 2025
4d76428
add a bazelrc CLI arg
bobozaur Jan 10, 2025
946e8ef
add .zed to .gitignore
bobozaur Jan 20, 2025
003f1a6
fix output groups names
bobozaur Jan 20, 2025
ecaef09
rename project discovery target
bobozaur Jan 20, 2025
53b6abe
fix user manual link
bobozaur Jan 20, 2025
7f250ad
Rename NormalizedProjectString to SerializeProjectJson
bobozaur Jan 20, 2025
c7afa58
improve build/workspace files lookup
bobozaur Jan 20, 2025
9983f9f
fix comment links
bobozaur Jan 20, 2025
4fd1efb
fix lints
bobozaur Jan 20, 2025
7859a7c
refactor placeholder substitution
bobozaur Jan 21, 2025
c9c6745
fix comments
bobozaur Jan 21, 2025
054eb52
add write_discovery function
bobozaur Jan 21, 2025
af5c530
remove execution_root and output_base CLI args
bobozaur Jan 21, 2025
46dfe5c
fix gen_rust_project
bobozaur Jan 21, 2025
e7b1524
inline generate_crate_info calls
bobozaur Jan 21, 2025
e2dc1cb
replace BazelCommand trait with a function
bobozaur Jan 21, 2025
ce8b9f7
move RustAnalyzerArg associated methods to lib.rs
bobozaur Jan 21, 2025
4261a1b
replace bazelrc with additive config args
bobozaur Jan 31, 2025
4568240
Merge branch 'main' into rust-analyzer-discover-config
krasimirgg Feb 17, 2025
f31aed4
Merge branch 'main' into rust-analyzer-discover-config
krasimirgg Mar 10, 2025
ef7d8f8
Merge branch 'main' into rust-analyzer-discover-config
bobozaur Mar 26, 2025
109f5e0
Add auto-discovery usage docs
bobozaur Mar 26, 2025
03cb9c6
Add auto-discovery e2e test
bobozaur Mar 26, 2025
d60d2b3
buildfiles formatting
bobozaur Mar 26, 2025
41be737
run rustfmt
bobozaur Mar 26, 2025
86a4f5c
fix gen_rust_project CI binary path
bobozaur Mar 26, 2025
dad9275
mark auto-discovery test target as manual
bobozaur Mar 26, 2025
41cb150
fix test target build info overriding library one
bobozaur Mar 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bazelci/presubmit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -885,7 +885,7 @@ tasks:
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
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ user.bazelrc
.vscode
*.code-workspace

# zed
.zed

# JetBrains
.idea
.idea/**
Expand Down
1 change: 1 addition & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use_repo(
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",
Expand Down
77 changes: 73 additions & 4 deletions docs/rust_analyzer.vm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -84,4 +86,71 @@ build --@rules_rust//rust/settings:rustc_output_diagnostics=true --output_groups

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`.

]]#
6 changes: 5 additions & 1 deletion extensions/prost/private/prost.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -308,14 +308,18 @@ def _rust_prost_aspect_impl(target, ctx):
# 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,
cfgs = cfgs,
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,
))

Expand Down
6 changes: 4 additions & 2 deletions rust/private/providers.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,14 @@ RustAnalyzerInfo = provider(
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",
},
)

Expand Down
41 changes: 31 additions & 10 deletions rust/private/rust_analyzer.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ def write_rust_analyzer_spec_file(ctx, attrs, owner, base_info):
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,
)

Expand Down Expand Up @@ -135,30 +137,40 @@ def _rust_analyzer_aspect_impl(target, ctx):
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,
cfgs = cfgs,
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]
Expand All @@ -174,7 +186,7 @@ def find_proc_macro_dylib_path(toolchain, target):
for action in target.actions:
for output in action.outputs.to_list():
if output.extension == dylib_ext[1:]:
return output.path
return output

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

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

# Switch on external/ to determine if crates are in the workspace or remote.
# TODO: Some folks may want to override this for vendored dependencies.
Expand All @@ -230,6 +243,14 @@ def _create_single_crate(ctx, attrs, info):
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}
Expand Down Expand Up @@ -268,8 +289,8 @@ def _create_single_crate(ctx, attrs, info):
crate["cfg"] = info.cfgs
toolchain = find_toolchain(ctx)
crate["target"] = (_EXEC_ROOT_TEMPLATE + toolchain.target_json.path) if toolchain.target_json else toolchain.target_flag_value
if info.proc_macro_dylib_path != None:
crate["proc_macro_dylib_path"] = _EXEC_ROOT_TEMPLATE + info.proc_macro_dylib_path
if info.proc_macro_dylib != None:
crate["proc_macro_dylib_path"] = _EXEC_ROOT_TEMPLATE + info.proc_macro_dylib.path
return crate

def _rust_analyzer_toolchain_impl(ctx):
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
],
)
Original file line number Diff line number Diff line change
@@ -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"));
}
}
}
Loading