1 use std::collections::{BTreeMap, BTreeSet};
2 use std::fs::File;
3 use std::path::Path;
4 use std::path::PathBuf;
5 use std::process::Command;
6
7 use anyhow::Context;
8 use serde::Deserialize;
9
10 #[derive(Debug, Deserialize)]
11 struct AqueryOutput {
12 artifacts: Vec<Artifact>,
13 actions: Vec<Action>,
14 #[serde(rename = "pathFragments")]
15 path_fragments: Vec<PathFragment>,
16 }
17
18 #[derive(Debug, Deserialize)]
19 struct Artifact {
20 id: u32,
21 #[serde(rename = "pathFragmentId")]
22 path_fragment_id: u32,
23 }
24
25 #[derive(Debug, Deserialize)]
26 struct PathFragment {
27 id: u32,
28 label: String,
29 #[serde(rename = "parentId")]
30 parent_id: Option<u32>,
31 }
32
33 #[derive(Debug, Deserialize)]
34 struct Action {
35 #[serde(rename = "outputIds")]
36 output_ids: Vec<u32>,
37 }
38
39 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)]
40 #[serde(deny_unknown_fields)]
41 pub struct CrateSpec {
42 pub aliases: BTreeMap<String, String>,
43 pub crate_id: String,
44 pub display_name: String,
45 pub edition: String,
46 pub root_module: String,
47 pub is_workspace_member: bool,
48 pub deps: BTreeSet<String>,
49 pub proc_macro_dylib_path: Option<String>,
50 pub source: Option<CrateSpecSource>,
51 pub cfg: Vec<String>,
52 pub env: BTreeMap<String, String>,
53 pub target: String,
54 pub crate_type: String,
55 }
56
57 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)]
58 #[serde(deny_unknown_fields)]
59 pub struct CrateSpecSource {
60 pub exclude_dirs: Vec<String>,
61 pub include_dirs: Vec<String>,
62 }
63
get_crate_specs( bazel: &Path, workspace: &Path, execution_root: &Path, targets: &[String], rules_rust_name: &str, ) -> anyhow::Result<BTreeSet<CrateSpec>>64 pub fn get_crate_specs(
65 bazel: &Path,
66 workspace: &Path,
67 execution_root: &Path,
68 targets: &[String],
69 rules_rust_name: &str,
70 ) -> anyhow::Result<BTreeSet<CrateSpec>> {
71 log::debug!("Get crate specs with targets: {:?}", targets);
72 let target_pattern = targets
73 .iter()
74 .map(|t| format!("deps({t})"))
75 .collect::<Vec<_>>()
76 .join("+");
77
78 let aquery_output = Command::new(bazel)
79 .current_dir(workspace)
80 .env_remove("BAZELISK_SKIP_WRAPPER")
81 .env_remove("BUILD_WORKING_DIRECTORY")
82 .env_remove("BUILD_WORKSPACE_DIRECTORY")
83 .arg("aquery")
84 .arg("--include_aspects")
85 .arg("--include_artifacts")
86 .arg(format!(
87 "--aspects={rules_rust_name}//rust:defs.bzl%rust_analyzer_aspect"
88 ))
89 .arg("--output_groups=rust_analyzer_crate_spec")
90 .arg(format!(
91 r#"outputs(".*\.rust_analyzer_crate_spec\.json",{target_pattern})"#
92 ))
93 .arg("--output=jsonproto")
94 .output()?;
95
96 let crate_spec_files =
97 parse_aquery_output_files(execution_root, &String::from_utf8(aquery_output.stdout)?)?;
98
99 let crate_specs = crate_spec_files
100 .into_iter()
101 .map(|file| {
102 let f = File::open(&file)
103 .with_context(|| format!("Failed to open file: {}", file.display()))?;
104 serde_json::from_reader(f)
105 .with_context(|| format!("Failed to deserialize file: {}", file.display()))
106 })
107 .collect::<anyhow::Result<Vec<CrateSpec>>>()?;
108
109 consolidate_crate_specs(crate_specs)
110 }
111
parse_aquery_output_files( execution_root: &Path, aquery_stdout: &str, ) -> anyhow::Result<Vec<PathBuf>>112 fn parse_aquery_output_files(
113 execution_root: &Path,
114 aquery_stdout: &str,
115 ) -> anyhow::Result<Vec<PathBuf>> {
116 let out: AqueryOutput = serde_json::from_str(aquery_stdout).map_err(|_| {
117 // Parsing to `AqueryOutput` failed, try parsing into a `serde_json::Value`:
118 match serde_json::from_str::<serde_json::Value>(aquery_stdout) {
119 Ok(serde_json::Value::Object(_)) => {
120 // If the JSON is an object, it's likely that the aquery command failed.
121 anyhow::anyhow!("Aquery returned an empty result, are there any Rust targets in the specified paths?.")
122 }
123 _ => {
124 anyhow::anyhow!("Failed to parse aquery output as JSON")
125 }
126 }
127 })?;
128
129 let artifacts = out
130 .artifacts
131 .iter()
132 .map(|a| (a.id, a))
133 .collect::<BTreeMap<_, _>>();
134 let path_fragments = out
135 .path_fragments
136 .iter()
137 .map(|pf| (pf.id, pf))
138 .collect::<BTreeMap<_, _>>();
139
140 let mut output_files: Vec<PathBuf> = Vec::new();
141 for action in out.actions {
142 for output_id in action.output_ids {
143 let artifact = artifacts
144 .get(&output_id)
145 .expect("internal consistency error in bazel output");
146 let path = path_from_fragments(artifact.path_fragment_id, &path_fragments)?;
147 let path = execution_root.join(path);
148 if path.exists() {
149 output_files.push(path);
150 } else {
151 log::warn!("Skipping missing crate_spec file: {:?}", path);
152 }
153 }
154 }
155
156 Ok(output_files)
157 }
158
path_from_fragments( id: u32, fragments: &BTreeMap<u32, &PathFragment>, ) -> anyhow::Result<PathBuf>159 fn path_from_fragments(
160 id: u32,
161 fragments: &BTreeMap<u32, &PathFragment>,
162 ) -> anyhow::Result<PathBuf> {
163 let path_fragment = fragments
164 .get(&id)
165 .expect("internal consistency error in bazel output");
166
167 let buf = match path_fragment.parent_id {
168 Some(parent_id) => path_from_fragments(parent_id, fragments)?
169 .join(PathBuf::from(&path_fragment.label.clone())),
170 None => PathBuf::from(&path_fragment.label.clone()),
171 };
172
173 Ok(buf)
174 }
175
176 /// Read all crate specs, deduplicating crates with the same ID. This happens when
177 /// a rust_test depends on a rust_library, for example.
consolidate_crate_specs(crate_specs: Vec<CrateSpec>) -> anyhow::Result<BTreeSet<CrateSpec>>178 fn consolidate_crate_specs(crate_specs: Vec<CrateSpec>) -> anyhow::Result<BTreeSet<CrateSpec>> {
179 let mut consolidated_specs: BTreeMap<String, CrateSpec> = BTreeMap::new();
180 for mut spec in crate_specs.into_iter() {
181 log::debug!("{:?}", spec);
182 if let Some(existing) = consolidated_specs.get_mut(&spec.crate_id) {
183 existing.deps.extend(spec.deps);
184
185 spec.cfg.retain(|cfg| !existing.cfg.contains(cfg));
186 existing.cfg.extend(spec.cfg);
187
188 // display_name should match the library's crate name because Rust Analyzer
189 // seems to use display_name for matching crate entries in rust-project.json
190 // against symbols in source files. For more details, see
191 // https://github.com/bazelbuild/rules_rust/issues/1032
192 if spec.crate_type == "rlib" {
193 existing.display_name = spec.display_name;
194 existing.crate_type = "rlib".into();
195 }
196
197 // For proc-macro crates that exist within the workspace, there will be a
198 // generated crate-spec in both the fastbuild and opt-exec configuration.
199 // Prefer proc macro paths with an opt-exec component in the path.
200 if let Some(dylib_path) = spec.proc_macro_dylib_path.as_ref() {
201 const OPT_PATH_COMPONENT: &str = "-opt-exec-";
202 if dylib_path.contains(OPT_PATH_COMPONENT) {
203 existing.proc_macro_dylib_path.replace(dylib_path.clone());
204 }
205 }
206 } else {
207 consolidated_specs.insert(spec.crate_id.clone(), spec);
208 }
209 }
210
211 Ok(consolidated_specs.into_values().collect())
212 }
213
214 #[cfg(test)]
215 mod test {
216 use super::*;
217 use itertools::Itertools;
218
219 #[test]
consolidate_lib_then_test_specs()220 fn consolidate_lib_then_test_specs() {
221 let crate_specs = vec![
222 CrateSpec {
223 aliases: BTreeMap::new(),
224 crate_id: "ID-mylib.rs".into(),
225 display_name: "mylib".into(),
226 edition: "2018".into(),
227 root_module: "mylib.rs".into(),
228 is_workspace_member: true,
229 deps: BTreeSet::from(["ID-lib_dep.rs".into()]),
230 proc_macro_dylib_path: None,
231 source: None,
232 cfg: vec!["test".into(), "debug_assertions".into()],
233 env: BTreeMap::new(),
234 target: "x86_64-unknown-linux-gnu".into(),
235 crate_type: "rlib".into(),
236 },
237 CrateSpec {
238 aliases: BTreeMap::new(),
239 crate_id: "ID-extra_test_dep.rs".into(),
240 display_name: "extra_test_dep".into(),
241 edition: "2018".into(),
242 root_module: "extra_test_dep.rs".into(),
243 is_workspace_member: true,
244 deps: BTreeSet::new(),
245 proc_macro_dylib_path: None,
246 source: None,
247 cfg: vec!["test".into(), "debug_assertions".into()],
248 env: BTreeMap::new(),
249 target: "x86_64-unknown-linux-gnu".into(),
250 crate_type: "rlib".into(),
251 },
252 CrateSpec {
253 aliases: BTreeMap::new(),
254 crate_id: "ID-lib_dep.rs".into(),
255 display_name: "lib_dep".into(),
256 edition: "2018".into(),
257 root_module: "lib_dep.rs".into(),
258 is_workspace_member: true,
259 deps: BTreeSet::new(),
260 proc_macro_dylib_path: None,
261 source: None,
262 cfg: vec!["test".into(), "debug_assertions".into()],
263 env: BTreeMap::new(),
264 target: "x86_64-unknown-linux-gnu".into(),
265 crate_type: "rlib".into(),
266 },
267 CrateSpec {
268 aliases: BTreeMap::new(),
269 crate_id: "ID-mylib.rs".into(),
270 display_name: "mylib_test".into(),
271 edition: "2018".into(),
272 root_module: "mylib.rs".into(),
273 is_workspace_member: true,
274 deps: BTreeSet::from(["ID-extra_test_dep.rs".into()]),
275 proc_macro_dylib_path: None,
276 source: None,
277 cfg: vec!["test".into(), "debug_assertions".into()],
278 env: BTreeMap::new(),
279 target: "x86_64-unknown-linux-gnu".into(),
280 crate_type: "bin".into(),
281 },
282 ];
283
284 assert_eq!(
285 consolidate_crate_specs(crate_specs).unwrap(),
286 BTreeSet::from([
287 CrateSpec {
288 aliases: BTreeMap::new(),
289 crate_id: "ID-mylib.rs".into(),
290 display_name: "mylib".into(),
291 edition: "2018".into(),
292 root_module: "mylib.rs".into(),
293 is_workspace_member: true,
294 deps: BTreeSet::from(["ID-lib_dep.rs".into(), "ID-extra_test_dep.rs".into()]),
295 proc_macro_dylib_path: None,
296 source: None,
297 cfg: vec!["test".into(), "debug_assertions".into()],
298 env: BTreeMap::new(),
299 target: "x86_64-unknown-linux-gnu".into(),
300 crate_type: "rlib".into(),
301 },
302 CrateSpec {
303 aliases: BTreeMap::new(),
304 crate_id: "ID-extra_test_dep.rs".into(),
305 display_name: "extra_test_dep".into(),
306 edition: "2018".into(),
307 root_module: "extra_test_dep.rs".into(),
308 is_workspace_member: true,
309 deps: BTreeSet::new(),
310 proc_macro_dylib_path: None,
311 source: None,
312 cfg: vec!["test".into(), "debug_assertions".into()],
313 env: BTreeMap::new(),
314 target: "x86_64-unknown-linux-gnu".into(),
315 crate_type: "rlib".into(),
316 },
317 CrateSpec {
318 aliases: BTreeMap::new(),
319 crate_id: "ID-lib_dep.rs".into(),
320 display_name: "lib_dep".into(),
321 edition: "2018".into(),
322 root_module: "lib_dep.rs".into(),
323 is_workspace_member: true,
324 deps: BTreeSet::new(),
325 proc_macro_dylib_path: None,
326 source: None,
327 cfg: vec!["test".into(), "debug_assertions".into()],
328 env: BTreeMap::new(),
329 target: "x86_64-unknown-linux-gnu".into(),
330 crate_type: "rlib".into(),
331 },
332 ])
333 );
334 }
335
336 #[test]
consolidate_test_then_lib_specs()337 fn consolidate_test_then_lib_specs() {
338 let crate_specs = vec![
339 CrateSpec {
340 aliases: BTreeMap::new(),
341 crate_id: "ID-mylib.rs".into(),
342 display_name: "mylib_test".into(),
343 edition: "2018".into(),
344 root_module: "mylib.rs".into(),
345 is_workspace_member: true,
346 deps: BTreeSet::from(["ID-extra_test_dep.rs".into()]),
347 proc_macro_dylib_path: None,
348 source: None,
349 cfg: vec!["test".into(), "debug_assertions".into()],
350 env: BTreeMap::new(),
351 target: "x86_64-unknown-linux-gnu".into(),
352 crate_type: "bin".into(),
353 },
354 CrateSpec {
355 aliases: BTreeMap::new(),
356 crate_id: "ID-mylib.rs".into(),
357 display_name: "mylib".into(),
358 edition: "2018".into(),
359 root_module: "mylib.rs".into(),
360 is_workspace_member: true,
361 deps: BTreeSet::from(["ID-lib_dep.rs".into()]),
362 proc_macro_dylib_path: None,
363 source: None,
364 cfg: vec!["test".into(), "debug_assertions".into()],
365 env: BTreeMap::new(),
366 target: "x86_64-unknown-linux-gnu".into(),
367 crate_type: "rlib".into(),
368 },
369 CrateSpec {
370 aliases: BTreeMap::new(),
371 crate_id: "ID-extra_test_dep.rs".into(),
372 display_name: "extra_test_dep".into(),
373 edition: "2018".into(),
374 root_module: "extra_test_dep.rs".into(),
375 is_workspace_member: true,
376 deps: BTreeSet::new(),
377 proc_macro_dylib_path: None,
378 source: None,
379 cfg: vec!["test".into(), "debug_assertions".into()],
380 env: BTreeMap::new(),
381 target: "x86_64-unknown-linux-gnu".into(),
382 crate_type: "rlib".into(),
383 },
384 CrateSpec {
385 aliases: BTreeMap::new(),
386 crate_id: "ID-lib_dep.rs".into(),
387 display_name: "lib_dep".into(),
388 edition: "2018".into(),
389 root_module: "lib_dep.rs".into(),
390 is_workspace_member: true,
391 deps: BTreeSet::new(),
392 proc_macro_dylib_path: None,
393 source: None,
394 cfg: vec!["test".into(), "debug_assertions".into()],
395 env: BTreeMap::new(),
396 target: "x86_64-unknown-linux-gnu".into(),
397 crate_type: "rlib".into(),
398 },
399 ];
400
401 assert_eq!(
402 consolidate_crate_specs(crate_specs).unwrap(),
403 BTreeSet::from([
404 CrateSpec {
405 aliases: BTreeMap::new(),
406 crate_id: "ID-mylib.rs".into(),
407 display_name: "mylib".into(),
408 edition: "2018".into(),
409 root_module: "mylib.rs".into(),
410 is_workspace_member: true,
411 deps: BTreeSet::from(["ID-lib_dep.rs".into(), "ID-extra_test_dep.rs".into()]),
412 proc_macro_dylib_path: None,
413 source: None,
414 cfg: vec!["test".into(), "debug_assertions".into()],
415 env: BTreeMap::new(),
416 target: "x86_64-unknown-linux-gnu".into(),
417 crate_type: "rlib".into(),
418 },
419 CrateSpec {
420 aliases: BTreeMap::new(),
421 crate_id: "ID-extra_test_dep.rs".into(),
422 display_name: "extra_test_dep".into(),
423 edition: "2018".into(),
424 root_module: "extra_test_dep.rs".into(),
425 is_workspace_member: true,
426 deps: BTreeSet::new(),
427 proc_macro_dylib_path: None,
428 source: None,
429 cfg: vec!["test".into(), "debug_assertions".into()],
430 env: BTreeMap::new(),
431 target: "x86_64-unknown-linux-gnu".into(),
432 crate_type: "rlib".into(),
433 },
434 CrateSpec {
435 aliases: BTreeMap::new(),
436 crate_id: "ID-lib_dep.rs".into(),
437 display_name: "lib_dep".into(),
438 edition: "2018".into(),
439 root_module: "lib_dep.rs".into(),
440 is_workspace_member: true,
441 deps: BTreeSet::new(),
442 proc_macro_dylib_path: None,
443 source: None,
444 cfg: vec!["test".into(), "debug_assertions".into()],
445 env: BTreeMap::new(),
446 target: "x86_64-unknown-linux-gnu".into(),
447 crate_type: "rlib".into(),
448 },
449 ])
450 );
451 }
452
453 #[test]
consolidate_lib_test_main_specs()454 fn consolidate_lib_test_main_specs() {
455 // mylib.rs is a library but has tests and an entry point, and mylib2.rs
456 // depends on mylib.rs. The display_name of the library target mylib.rs
457 // should be "mylib" no matter what order the crate specs is in.
458 // Otherwise Rust Analyzer will not be able to resolve references to
459 // mylib in mylib2.rs.
460 let crate_specs = vec![
461 CrateSpec {
462 aliases: BTreeMap::new(),
463 crate_id: "ID-mylib.rs".into(),
464 display_name: "mylib".into(),
465 edition: "2018".into(),
466 root_module: "mylib.rs".into(),
467 is_workspace_member: true,
468 deps: BTreeSet::new(),
469 proc_macro_dylib_path: None,
470 source: None,
471 cfg: vec!["test".into(), "debug_assertions".into()],
472 env: BTreeMap::new(),
473 target: "x86_64-unknown-linux-gnu".into(),
474 crate_type: "rlib".into(),
475 },
476 CrateSpec {
477 aliases: BTreeMap::new(),
478 crate_id: "ID-mylib.rs".into(),
479 display_name: "mylib_test".into(),
480 edition: "2018".into(),
481 root_module: "mylib.rs".into(),
482 is_workspace_member: true,
483 deps: BTreeSet::new(),
484 proc_macro_dylib_path: None,
485 source: None,
486 cfg: vec!["test".into(), "debug_assertions".into()],
487 env: BTreeMap::new(),
488 target: "x86_64-unknown-linux-gnu".into(),
489 crate_type: "bin".into(),
490 },
491 CrateSpec {
492 aliases: BTreeMap::new(),
493 crate_id: "ID-mylib.rs".into(),
494 display_name: "mylib_main".into(),
495 edition: "2018".into(),
496 root_module: "mylib.rs".into(),
497 is_workspace_member: true,
498 deps: BTreeSet::new(),
499 proc_macro_dylib_path: None,
500 source: None,
501 cfg: vec!["test".into(), "debug_assertions".into()],
502 env: BTreeMap::new(),
503 target: "x86_64-unknown-linux-gnu".into(),
504 crate_type: "bin".into(),
505 },
506 CrateSpec {
507 aliases: BTreeMap::new(),
508 crate_id: "ID-mylib2.rs".into(),
509 display_name: "mylib2".into(),
510 edition: "2018".into(),
511 root_module: "mylib2.rs".into(),
512 is_workspace_member: true,
513 deps: BTreeSet::from(["ID-mylib.rs".into()]),
514 proc_macro_dylib_path: None,
515 source: None,
516 cfg: vec!["test".into(), "debug_assertions".into()],
517 env: BTreeMap::new(),
518 target: "x86_64-unknown-linux-gnu".into(),
519 crate_type: "rlib".into(),
520 },
521 ];
522
523 for perm in crate_specs.into_iter().permutations(4) {
524 assert_eq!(
525 consolidate_crate_specs(perm).unwrap(),
526 BTreeSet::from([
527 CrateSpec {
528 aliases: BTreeMap::new(),
529 crate_id: "ID-mylib.rs".into(),
530 display_name: "mylib".into(),
531 edition: "2018".into(),
532 root_module: "mylib.rs".into(),
533 is_workspace_member: true,
534 deps: BTreeSet::from([]),
535 proc_macro_dylib_path: None,
536 source: None,
537 cfg: vec!["test".into(), "debug_assertions".into()],
538 env: BTreeMap::new(),
539 target: "x86_64-unknown-linux-gnu".into(),
540 crate_type: "rlib".into(),
541 },
542 CrateSpec {
543 aliases: BTreeMap::new(),
544 crate_id: "ID-mylib2.rs".into(),
545 display_name: "mylib2".into(),
546 edition: "2018".into(),
547 root_module: "mylib2.rs".into(),
548 is_workspace_member: true,
549 deps: BTreeSet::from(["ID-mylib.rs".into()]),
550 proc_macro_dylib_path: None,
551 source: None,
552 cfg: vec!["test".into(), "debug_assertions".into()],
553 env: BTreeMap::new(),
554 target: "x86_64-unknown-linux-gnu".into(),
555 crate_type: "rlib".into(),
556 },
557 ])
558 );
559 }
560 }
561
562 #[test]
consolidate_proc_macro_prefer_exec()563 fn consolidate_proc_macro_prefer_exec() {
564 // proc macro crates should prefer the -opt-exec- path which is always generated
565 // during builds where it is used, while the fastbuild version would only be built
566 // when explicitly building that target.
567 let crate_specs = vec![
568 CrateSpec {
569 aliases: BTreeMap::new(),
570 crate_id: "ID-myproc_macro.rs".into(),
571 display_name: "myproc_macro".into(),
572 edition: "2018".into(),
573 root_module: "myproc_macro.rs".into(),
574 is_workspace_member: true,
575 deps: BTreeSet::new(),
576 proc_macro_dylib_path: Some(
577 "bazel-out/k8-opt-exec-F005BA11/bin/myproc_macro/libmyproc_macro-12345.so"
578 .into(),
579 ),
580 source: None,
581 cfg: vec!["test".into(), "debug_assertions".into()],
582 env: BTreeMap::new(),
583 target: "x86_64-unknown-linux-gnu".into(),
584 crate_type: "proc_macro".into(),
585 },
586 CrateSpec {
587 aliases: BTreeMap::new(),
588 crate_id: "ID-myproc_macro.rs".into(),
589 display_name: "myproc_macro".into(),
590 edition: "2018".into(),
591 root_module: "myproc_macro.rs".into(),
592 is_workspace_member: true,
593 deps: BTreeSet::new(),
594 proc_macro_dylib_path: Some(
595 "bazel-out/k8-fastbuild/bin/myproc_macro/libmyproc_macro-12345.so".into(),
596 ),
597 source: None,
598 cfg: vec!["test".into(), "debug_assertions".into()],
599 env: BTreeMap::new(),
600 target: "x86_64-unknown-linux-gnu".into(),
601 crate_type: "proc_macro".into(),
602 },
603 ];
604
605 for perm in crate_specs.into_iter().permutations(2) {
606 assert_eq!(
607 consolidate_crate_specs(perm).unwrap(),
608 BTreeSet::from([CrateSpec {
609 aliases: BTreeMap::new(),
610 crate_id: "ID-myproc_macro.rs".into(),
611 display_name: "myproc_macro".into(),
612 edition: "2018".into(),
613 root_module: "myproc_macro.rs".into(),
614 is_workspace_member: true,
615 deps: BTreeSet::new(),
616 proc_macro_dylib_path: Some(
617 "bazel-out/k8-opt-exec-F005BA11/bin/myproc_macro/libmyproc_macro-12345.so"
618 .into()
619 ),
620 source: None,
621 cfg: vec!["test".into(), "debug_assertions".into()],
622 env: BTreeMap::new(),
623 target: "x86_64-unknown-linux-gnu".into(),
624 crate_type: "proc_macro".into(),
625 },])
626 );
627 }
628 }
629 }
630