Skip to content

Commit 377314b

Browse files
authored
Allow rules to provide their own rust-analyzer providers (bazelbuild#2487)
This change cleans up the rust-analyzer aspect to support external rules providing their own crate specs. For now only prost implements behavior for this and the rust-analyzer interface is still private. In the future if this proves to be performant and a consistent interface then there should be no issue making this a public part of the `//rust` package. This change incorporates bazelbuild#1875 (special thanks to @snowp!) and addresses performance issues in the generator tool by allowing users of `bazelisk` to ensure their `tools/bazel` scripts run should one be provided and to disable running validation actions when building crate specs.
1 parent b1fc852 commit 377314b

File tree

7 files changed

+157
-58
lines changed

7 files changed

+157
-58
lines changed

proto/prost/private/prost.bzl

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@ load("@rules_proto//proto:defs.bzl", "ProtoInfo", "proto_common")
44
load("//proto/prost:providers.bzl", "ProstProtoInfo")
55
load("//rust:defs.bzl", "rust_common")
66

7+
# buildifier: disable=bzl-visibility
8+
load("//rust/private:providers.bzl", "RustAnalyzerGroupInfo", "RustAnalyzerInfo")
9+
710
# buildifier: disable=bzl-visibility
811
load("//rust/private:rust.bzl", "RUSTC_ATTRS")
912

13+
# buildifier: disable=bzl-visibility
14+
load("//rust/private:rust_analyzer.bzl", "write_rust_analyzer_spec_file")
15+
1016
# buildifier: disable=bzl-visibility
1117
load("//rust/private:rustc.bzl", "rustc_compile_action")
1218

@@ -211,6 +217,7 @@ def _rust_prost_aspect_impl(target, ctx):
211217

212218
direct_deps = []
213219
transitive_deps = [depset(runtime_deps)]
220+
rust_analyzer_deps = []
214221
for proto_dep in proto_deps:
215222
proto_info = proto_dep[ProstProtoInfo]
216223

@@ -220,6 +227,9 @@ def _rust_prost_aspect_impl(target, ctx):
220227
transitive = [proto_info.transitive_dep_infos],
221228
))
222229

230+
if RustAnalyzerInfo in proto_dep:
231+
rust_analyzer_deps.append(proto_dep[RustAnalyzerInfo])
232+
223233
deps = runtime_deps + direct_deps
224234

225235
crate_name = ctx.label.name.replace("-", "_").replace("/", "_")
@@ -244,12 +254,27 @@ def _rust_prost_aspect_impl(target, ctx):
244254
edition = RUST_EDITION,
245255
)
246256

257+
# Always add `test` & `debug_assertions`. See rust-analyzer source code:
258+
# https://github.com/rust-analyzer/rust-analyzer/blob/2021-11-15/crates/project_model/src/workspace.rs#L529-L531
259+
cfgs = ["test", "debug_assertions"]
260+
261+
rust_analyzer_info = write_rust_analyzer_spec_file(ctx, ctx.rule.attr, ctx.label, RustAnalyzerInfo(
262+
crate = dep_variant_info.crate_info,
263+
cfgs = cfgs,
264+
env = dep_variant_info.crate_info.rustc_env,
265+
deps = rust_analyzer_deps,
266+
crate_specs = depset(transitive = [dep.crate_specs for dep in rust_analyzer_deps]),
267+
proc_macro_dylib_path = None,
268+
build_info = dep_variant_info.build_info,
269+
))
270+
247271
return [
248272
ProstProtoInfo(
249273
dep_variant_info = dep_variant_info,
250274
transitive_dep_infos = depset(transitive = transitive_deps),
251275
package_info = package_info_file,
252276
),
277+
rust_analyzer_info,
253278
]
254279

255280
rust_prost_aspect = aspect(
@@ -290,13 +315,13 @@ def _rust_prost_library_impl(ctx):
290315

291316
return [
292317
DefaultInfo(files = depset([dep_variant_info.crate_info.output])),
293-
rust_proto_info,
294318
rust_common.crate_group_info(
295319
dep_variant_infos = depset(
296320
[dep_variant_info],
297321
transitive = [rust_proto_info.transitive_dep_infos],
298322
),
299323
),
324+
RustAnalyzerGroupInfo(deps = [proto_dep[RustAnalyzerInfo]]),
300325
]
301326

302327
rust_prost_library = rule(

rust/private/providers.bzl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,23 @@ TestCrateInfo = provider(
151151
"crate": "CrateInfo: The underlying CrateInfo of the dependency",
152152
},
153153
)
154+
155+
RustAnalyzerInfo = provider(
156+
doc = "RustAnalyzerInfo holds rust crate metadata for targets",
157+
fields = {
158+
"build_info": "BuildInfo: build info for this crate if present",
159+
"cfgs": "List[String]: features or other compilation `--cfg` settings",
160+
"crate": "CrateInfo: Crate information.",
161+
"crate_specs": "Depset[File]: transitive closure of OutputGroupInfo files",
162+
"deps": "List[RustAnalyzerInfo]: direct dependencies",
163+
"env": "Dict[String: String]: Environment variables, used for the `env!` macro",
164+
"proc_macro_dylib_path": "File: compiled shared library output of proc-macro rule",
165+
},
166+
)
167+
168+
RustAnalyzerGroupInfo = provider(
169+
doc = "RustAnalyzerGroupInfo holds multiple RustAnalyzerInfos",
170+
fields = {
171+
"deps": "List[RustAnalyzerInfo]: direct dependencies",
172+
},
173+
)

rust/private/rust_analyzer.bzl

Lines changed: 49 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ given targets. This file can be consumed by rust-analyzer as an alternative
2020
to Cargo.toml files.
2121
"""
2222

23-
load("//proto/prost:providers.bzl", "ProstProtoInfo")
2423
load("//rust/platform:triple_mappings.bzl", "system_to_dylib_ext", "triple_to_system")
2524
load("//rust/private:common.bzl", "rust_common")
25+
load("//rust/private:providers.bzl", "RustAnalyzerGroupInfo", "RustAnalyzerInfo")
2626
load("//rust/private:rustc.bzl", "BuildInfo")
2727
load(
2828
"//rust/private:utils.bzl",
@@ -32,32 +32,53 @@ load(
3232
"find_toolchain",
3333
)
3434

35-
RustAnalyzerInfo = provider(
36-
doc = "RustAnalyzerInfo holds rust crate metadata for targets",
37-
fields = {
38-
"build_info": "BuildInfo: build info for this crate if present",
39-
"cfgs": "List[String]: features or other compilation --cfg settings",
40-
"crate": "rust_common.crate_info",
41-
"crate_specs": "Depset[File]: transitive closure of OutputGroupInfo files",
42-
"deps": "List[RustAnalyzerInfo]: direct dependencies",
43-
"env": "Dict{String: String}: Environment variables, used for the `env!` macro",
44-
"proc_macro_dylib_path": "File: compiled shared library output of proc-macro rule",
45-
},
46-
)
35+
def write_rust_analyzer_spec_file(ctx, attrs, owner, base_info):
36+
"""Write a rust-analyzer spec info file.
4737
48-
RustAnalyzerGroupInfo = provider(
49-
doc = "RustAnalyzerGroupInfo holds multiple RustAnalyzerInfos",
50-
fields = {
51-
"deps": "List[RustAnalyzerInfo]: direct dependencies",
52-
},
53-
)
38+
Args:
39+
ctx (ctx): The current rule's context object.
40+
attrs (dict): A mapping of attributes.
41+
owner (Label): The label of the owner of the spec info.
42+
base_info (RustAnalyzerInfo): The data the resulting RustAnalyzerInfo is based on.
43+
44+
Returns:
45+
RustAnalyzerInfo: Info with the embedded spec file.
46+
"""
47+
crate_spec = ctx.actions.declare_file("{}.rust_analyzer_crate_spec.json".format(owner.name))
48+
49+
rust_analyzer_info = RustAnalyzerInfo(
50+
crate = base_info.crate,
51+
cfgs = base_info.cfgs,
52+
env = base_info.env,
53+
deps = base_info.deps,
54+
crate_specs = depset(direct = [crate_spec], transitive = [base_info.crate_specs]),
55+
proc_macro_dylib_path = base_info.proc_macro_dylib_path,
56+
build_info = base_info.build_info,
57+
)
58+
59+
ctx.actions.write(
60+
output = crate_spec,
61+
content = json.encode_indent(
62+
_create_single_crate(
63+
ctx,
64+
attrs,
65+
rust_analyzer_info,
66+
),
67+
indent = " " * 4,
68+
),
69+
)
70+
71+
return rust_analyzer_info
5472

5573
def _rust_analyzer_aspect_impl(target, ctx):
5674
if (rust_common.crate_info not in target and
5775
rust_common.test_crate_info not in target and
5876
rust_common.crate_group_info not in target):
5977
return []
6078

79+
if RustAnalyzerInfo in target or RustAnalyzerGroupInfo in target:
80+
return []
81+
6182
toolchain = find_toolchain(ctx)
6283

6384
# Always add `test` & `debug_assertions`. See rust-analyzer source code:
@@ -102,28 +123,7 @@ def _rust_analyzer_aspect_impl(target, ctx):
102123
if RustAnalyzerGroupInfo in ctx.rule.attr.actual:
103124
dep_infos.extend(ctx.rule.attr.actual[RustAnalyzerGroupInfo])
104125

105-
if ProstProtoInfo in target:
106-
for info in target[ProstProtoInfo].transitive_dep_infos.to_list():
107-
crate_info = info.crate_info
108-
crate_spec = ctx.actions.declare_file(crate_info.owner.name + ".rust_analyzer_crate_spec")
109-
rust_analyzer_info = RustAnalyzerInfo(
110-
crate = crate_info,
111-
cfgs = cfgs,
112-
env = crate_info.rustc_env,
113-
deps = [],
114-
crate_specs = depset(direct = [crate_spec]),
115-
proc_macro_dylib_path = None,
116-
build_info = info.build_info,
117-
)
118-
ctx.actions.write(
119-
output = crate_spec,
120-
content = json.encode(_create_single_crate(ctx, rust_analyzer_info)),
121-
)
122-
dep_infos.append(rust_analyzer_info)
123-
124-
if ProstProtoInfo in target:
125-
crate_info = target[ProstProtoInfo].dep_variant_info.crate_info
126-
elif rust_common.crate_group_info in target:
126+
if rust_common.crate_group_info in target:
127127
return [RustAnalyzerGroupInfo(deps = dep_infos)]
128128
elif rust_common.crate_info in target:
129129
crate_info = target[rust_common.crate_info]
@@ -132,22 +132,15 @@ def _rust_analyzer_aspect_impl(target, ctx):
132132
else:
133133
fail("Unexpected target type: {}".format(target))
134134

135-
crate_spec = ctx.actions.declare_file(ctx.label.name + ".rust_analyzer_crate_spec")
136-
137-
rust_analyzer_info = RustAnalyzerInfo(
135+
rust_analyzer_info = write_rust_analyzer_spec_file(ctx, ctx.rule.attr, ctx.label, RustAnalyzerInfo(
138136
crate = crate_info,
139137
cfgs = cfgs,
140138
env = crate_info.rustc_env,
141139
deps = dep_infos,
142-
crate_specs = depset(direct = [crate_spec], transitive = [dep.crate_specs for dep in dep_infos]),
140+
crate_specs = depset(transitive = [dep.crate_specs for dep in dep_infos]),
143141
proc_macro_dylib_path = find_proc_macro_dylib_path(toolchain, target),
144142
build_info = build_info,
145-
)
146-
147-
ctx.actions.write(
148-
output = crate_spec,
149-
content = json.encode(_create_single_crate(ctx, rust_analyzer_info)),
150-
)
143+
))
151144

152145
return [
153146
rust_analyzer_info,
@@ -201,12 +194,13 @@ def _crate_id(crate_info):
201194
"""
202195
return "ID-" + crate_info.root.path
203196

204-
def _create_single_crate(ctx, info):
197+
def _create_single_crate(ctx, attrs, info):
205198
"""Creates a crate in the rust-project.json format.
206199
207200
Args:
208-
ctx (ctx): The rule context
209-
info (RustAnalyzerInfo): RustAnalyzerInfo for the current crate
201+
ctx (ctx): The rule context.
202+
attrs (dict): A mapping of attributes.
203+
info (RustAnalyzerInfo): RustAnalyzerInfo for the current crate.
210204
211205
Returns:
212206
(dict) The crate rust-project.json representation
@@ -240,7 +234,7 @@ def _create_single_crate(ctx, info):
240234

241235
# TODO: The only imagined use case is an env var holding a filename in the workspace passed to a
242236
# macro like include_bytes!. Other use cases might exist that require more complex logic.
243-
expand_targets = concat([getattr(ctx.rule.attr, attr, []) for attr in ["data", "compile_data"]])
237+
expand_targets = concat([getattr(attrs, attr, []) for attr in ["data", "compile_data"]])
244238

245239
crate["env"].update({k: dedup_expand_location(ctx, v, expand_targets) for k, v in info.env.items()})
246240

tools/rust_analyzer/aquery.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ pub fn get_crate_specs(
7777

7878
let aquery_output = Command::new(bazel)
7979
.current_dir(workspace)
80+
.env_remove("BAZELISK_SKIP_WRAPPER")
81+
.env_remove("BUILD_WORKING_DIRECTORY")
82+
.env_remove("BUILD_WORKSPACE_DIRECTORY")
8083
.arg("aquery")
8184
.arg("--include_aspects")
8285
.arg("--include_artifacts")
@@ -85,7 +88,7 @@ pub fn get_crate_specs(
8588
))
8689
.arg("--output_groups=rust_analyzer_crate_spec")
8790
.arg(format!(
88-
r#"outputs(".*[.]rust_analyzer_crate_spec",{target_pattern})"#
91+
r#"outputs(".*\.rust_analyzer_crate_spec\.json",{target_pattern})"#
8992
))
9093
.arg("--output=jsonproto")
9194
.output()?;

tools/rust_analyzer/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ pub fn generate_crate_info(
2020

2121
let output = Command::new(bazel.as_ref())
2222
.current_dir(workspace.as_ref())
23+
.env_remove("BAZELISK_SKIP_WRAPPER")
24+
.env_remove("BUILD_WORKING_DIRECTORY")
25+
.env_remove("BUILD_WORKSPACE_DIRECTORY")
2326
.arg("build")
27+
.arg("--norun_validations")
2428
.arg(format!(
2529
"--aspects={}//rust:defs.bzl%rust_analyzer_aspect",
2630
rules_rust.as_ref()

tools/rust_analyzer/main.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,11 @@ fn parse_config() -> anyhow::Result<Config> {
6565

6666
// We need some info from `bazel info`. Fetch it now.
6767
let mut bazel_info_command = Command::new(&config.bazel);
68-
bazel_info_command.arg("info");
68+
bazel_info_command
69+
.env_remove("BAZELISK_SKIP_WRAPPER")
70+
.env_remove("BUILD_WORKING_DIRECTORY")
71+
.env_remove("BUILD_WORKSPACE_DIRECTORY")
72+
.arg("info");
6973
if let Some(workspace) = &config.workspace {
7074
bazel_info_command.current_dir(workspace);
7175
}

tools/rust_analyzer/rust_project.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,23 @@ pub fn generate_rust_project(
168168
skipped_crates.len(),
169169
skipped_crates
170170
);
171+
let crate_map: BTreeMap<String, &CrateSpec> = unmerged_crates
172+
.iter()
173+
.map(|c| (c.crate_id.to_string(), *c))
174+
.collect();
175+
176+
for unmerged_crate in &unmerged_crates {
177+
let mut path = vec![];
178+
if let Some(cycle) = detect_cycle(unmerged_crate, &crate_map, &mut path) {
179+
log::warn!(
180+
"Cycle detected: {:?}",
181+
cycle
182+
.iter()
183+
.map(|c| c.crate_id.to_string())
184+
.collect::<Vec<String>>()
185+
);
186+
}
187+
}
171188
return Err(anyhow!(
172189
"Failed to make progress on building crate dependency graph"
173190
));
@@ -179,6 +196,38 @@ pub fn generate_rust_project(
179196
Ok(project)
180197
}
181198

199+
fn detect_cycle<'a>(
200+
current_crate: &'a CrateSpec,
201+
all_crates: &'a BTreeMap<String, &'a CrateSpec>,
202+
path: &mut Vec<&'a CrateSpec>,
203+
) -> Option<Vec<&'a CrateSpec>> {
204+
if path
205+
.iter()
206+
.any(|dependent_crate| dependent_crate.crate_id == current_crate.crate_id)
207+
{
208+
let mut cycle_path = path.clone();
209+
cycle_path.push(current_crate);
210+
return Some(cycle_path);
211+
}
212+
213+
path.push(current_crate);
214+
215+
for dep in &current_crate.deps {
216+
match all_crates.get(dep) {
217+
Some(dep_crate) => {
218+
if let Some(cycle) = detect_cycle(dep_crate, all_crates, path) {
219+
return Some(cycle);
220+
}
221+
}
222+
None => log::debug!("dep {dep} not found in unmerged crate map"),
223+
}
224+
}
225+
226+
path.pop();
227+
228+
None
229+
}
230+
182231
pub fn write_rust_project(
183232
rust_project_path: &Path,
184233
execution_root: &Path,

0 commit comments

Comments
 (0)