1 use std::collections::{BTreeMap, BTreeSet};
2 use std::fs::File;
3 use std::option::Option;
4 use std::path::Path;
5 use std::path::PathBuf;
6 use std::process::Command;
7
8 use anyhow::Context;
9 use serde::Deserialize;
10
11 #[derive(Debug, Deserialize)]
12 struct AqueryOutput {
13 artifacts: Vec<Artifact>,
14 actions: Vec<Action>,
15 #[serde(rename = "pathFragments")]
16 path_fragments: Vec<PathFragment>,
17 }
18
19 #[derive(Debug, Deserialize)]
20 struct Artifact {
21 id: u32,
22 #[serde(rename = "pathFragmentId")]
23 path_fragment_id: u32,
24 }
25
26 #[derive(Debug, Deserialize)]
27 struct PathFragment {
28 id: u32,
29 label: String,
30 #[serde(rename = "parentId")]
31 parent_id: Option<u32>,
32 }
33
34 #[derive(Debug, Deserialize)]
35 struct Action {
36 #[serde(rename = "outputIds")]
37 output_ids: Vec<u32>,
38 }
39
40 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)]
41 #[serde(deny_unknown_fields)]
42 pub struct CrateSpec {
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 crate_id: "ID-mylib.rs".into(),
224 display_name: "mylib".into(),
225 edition: "2018".into(),
226 root_module: "mylib.rs".into(),
227 is_workspace_member: true,
228 deps: BTreeSet::from(["ID-lib_dep.rs".into()]),
229 proc_macro_dylib_path: None,
230 source: None,
231 cfg: vec!["test".into(), "debug_assertions".into()],
232 env: BTreeMap::new(),
233 target: "x86_64-unknown-linux-gnu".into(),
234 crate_type: "rlib".into(),
235 },
236 CrateSpec {
237 crate_id: "ID-extra_test_dep.rs".into(),
238 display_name: "extra_test_dep".into(),
239 edition: "2018".into(),
240 root_module: "extra_test_dep.rs".into(),
241 is_workspace_member: true,
242 deps: BTreeSet::new(),
243 proc_macro_dylib_path: None,
244 source: None,
245 cfg: vec!["test".into(), "debug_assertions".into()],
246 env: BTreeMap::new(),
247 target: "x86_64-unknown-linux-gnu".into(),
248 crate_type: "rlib".into(),
249 },
250 CrateSpec {
251 crate_id: "ID-lib_dep.rs".into(),
252 display_name: "lib_dep".into(),
253 edition: "2018".into(),
254 root_module: "lib_dep.rs".into(),
255 is_workspace_member: true,
256 deps: BTreeSet::new(),
257 proc_macro_dylib_path: None,
258 source: None,
259 cfg: vec!["test".into(), "debug_assertions".into()],
260 env: BTreeMap::new(),
261 target: "x86_64-unknown-linux-gnu".into(),
262 crate_type: "rlib".into(),
263 },
264 CrateSpec {
265 crate_id: "ID-mylib.rs".into(),
266 display_name: "mylib_test".into(),
267 edition: "2018".into(),
268 root_module: "mylib.rs".into(),
269 is_workspace_member: true,
270 deps: BTreeSet::from(["ID-extra_test_dep.rs".into()]),
271 proc_macro_dylib_path: None,
272 source: None,
273 cfg: vec!["test".into(), "debug_assertions".into()],
274 env: BTreeMap::new(),
275 target: "x86_64-unknown-linux-gnu".into(),
276 crate_type: "bin".into(),
277 },
278 ];
279
280 assert_eq!(
281 consolidate_crate_specs(crate_specs).unwrap(),
282 BTreeSet::from([
283 CrateSpec {
284 crate_id: "ID-mylib.rs".into(),
285 display_name: "mylib".into(),
286 edition: "2018".into(),
287 root_module: "mylib.rs".into(),
288 is_workspace_member: true,
289 deps: BTreeSet::from(["ID-lib_dep.rs".into(), "ID-extra_test_dep.rs".into()]),
290 proc_macro_dylib_path: None,
291 source: None,
292 cfg: vec!["test".into(), "debug_assertions".into()],
293 env: BTreeMap::new(),
294 target: "x86_64-unknown-linux-gnu".into(),
295 crate_type: "rlib".into(),
296 },
297 CrateSpec {
298 crate_id: "ID-extra_test_dep.rs".into(),
299 display_name: "extra_test_dep".into(),
300 edition: "2018".into(),
301 root_module: "extra_test_dep.rs".into(),
302 is_workspace_member: true,
303 deps: BTreeSet::new(),
304 proc_macro_dylib_path: None,
305 source: None,
306 cfg: vec!["test".into(), "debug_assertions".into()],
307 env: BTreeMap::new(),
308 target: "x86_64-unknown-linux-gnu".into(),
309 crate_type: "rlib".into(),
310 },
311 CrateSpec {
312 crate_id: "ID-lib_dep.rs".into(),
313 display_name: "lib_dep".into(),
314 edition: "2018".into(),
315 root_module: "lib_dep.rs".into(),
316 is_workspace_member: true,
317 deps: BTreeSet::new(),
318 proc_macro_dylib_path: None,
319 source: None,
320 cfg: vec!["test".into(), "debug_assertions".into()],
321 env: BTreeMap::new(),
322 target: "x86_64-unknown-linux-gnu".into(),
323 crate_type: "rlib".into(),
324 },
325 ])
326 );
327 }
328
329 #[test]
consolidate_test_then_lib_specs()330 fn consolidate_test_then_lib_specs() {
331 let crate_specs = vec![
332 CrateSpec {
333 crate_id: "ID-mylib.rs".into(),
334 display_name: "mylib_test".into(),
335 edition: "2018".into(),
336 root_module: "mylib.rs".into(),
337 is_workspace_member: true,
338 deps: BTreeSet::from(["ID-extra_test_dep.rs".into()]),
339 proc_macro_dylib_path: None,
340 source: None,
341 cfg: vec!["test".into(), "debug_assertions".into()],
342 env: BTreeMap::new(),
343 target: "x86_64-unknown-linux-gnu".into(),
344 crate_type: "bin".into(),
345 },
346 CrateSpec {
347 crate_id: "ID-mylib.rs".into(),
348 display_name: "mylib".into(),
349 edition: "2018".into(),
350 root_module: "mylib.rs".into(),
351 is_workspace_member: true,
352 deps: BTreeSet::from(["ID-lib_dep.rs".into()]),
353 proc_macro_dylib_path: None,
354 source: None,
355 cfg: vec!["test".into(), "debug_assertions".into()],
356 env: BTreeMap::new(),
357 target: "x86_64-unknown-linux-gnu".into(),
358 crate_type: "rlib".into(),
359 },
360 CrateSpec {
361 crate_id: "ID-extra_test_dep.rs".into(),
362 display_name: "extra_test_dep".into(),
363 edition: "2018".into(),
364 root_module: "extra_test_dep.rs".into(),
365 is_workspace_member: true,
366 deps: BTreeSet::new(),
367 proc_macro_dylib_path: None,
368 source: None,
369 cfg: vec!["test".into(), "debug_assertions".into()],
370 env: BTreeMap::new(),
371 target: "x86_64-unknown-linux-gnu".into(),
372 crate_type: "rlib".into(),
373 },
374 CrateSpec {
375 crate_id: "ID-lib_dep.rs".into(),
376 display_name: "lib_dep".into(),
377 edition: "2018".into(),
378 root_module: "lib_dep.rs".into(),
379 is_workspace_member: true,
380 deps: BTreeSet::new(),
381 proc_macro_dylib_path: None,
382 source: None,
383 cfg: vec!["test".into(), "debug_assertions".into()],
384 env: BTreeMap::new(),
385 target: "x86_64-unknown-linux-gnu".into(),
386 crate_type: "rlib".into(),
387 },
388 ];
389
390 assert_eq!(
391 consolidate_crate_specs(crate_specs).unwrap(),
392 BTreeSet::from([
393 CrateSpec {
394 crate_id: "ID-mylib.rs".into(),
395 display_name: "mylib".into(),
396 edition: "2018".into(),
397 root_module: "mylib.rs".into(),
398 is_workspace_member: true,
399 deps: BTreeSet::from(["ID-lib_dep.rs".into(), "ID-extra_test_dep.rs".into()]),
400 proc_macro_dylib_path: None,
401 source: None,
402 cfg: vec!["test".into(), "debug_assertions".into()],
403 env: BTreeMap::new(),
404 target: "x86_64-unknown-linux-gnu".into(),
405 crate_type: "rlib".into(),
406 },
407 CrateSpec {
408 crate_id: "ID-extra_test_dep.rs".into(),
409 display_name: "extra_test_dep".into(),
410 edition: "2018".into(),
411 root_module: "extra_test_dep.rs".into(),
412 is_workspace_member: true,
413 deps: BTreeSet::new(),
414 proc_macro_dylib_path: None,
415 source: None,
416 cfg: vec!["test".into(), "debug_assertions".into()],
417 env: BTreeMap::new(),
418 target: "x86_64-unknown-linux-gnu".into(),
419 crate_type: "rlib".into(),
420 },
421 CrateSpec {
422 crate_id: "ID-lib_dep.rs".into(),
423 display_name: "lib_dep".into(),
424 edition: "2018".into(),
425 root_module: "lib_dep.rs".into(),
426 is_workspace_member: true,
427 deps: BTreeSet::new(),
428 proc_macro_dylib_path: None,
429 source: None,
430 cfg: vec!["test".into(), "debug_assertions".into()],
431 env: BTreeMap::new(),
432 target: "x86_64-unknown-linux-gnu".into(),
433 crate_type: "rlib".into(),
434 },
435 ])
436 );
437 }
438
439 #[test]
consolidate_lib_test_main_specs()440 fn consolidate_lib_test_main_specs() {
441 // mylib.rs is a library but has tests and an entry point, and mylib2.rs
442 // depends on mylib.rs. The display_name of the library target mylib.rs
443 // should be "mylib" no matter what order the crate specs is in.
444 // Otherwise Rust Analyzer will not be able to resolve references to
445 // mylib in mylib2.rs.
446 let crate_specs = vec![
447 CrateSpec {
448 crate_id: "ID-mylib.rs".into(),
449 display_name: "mylib".into(),
450 edition: "2018".into(),
451 root_module: "mylib.rs".into(),
452 is_workspace_member: true,
453 deps: BTreeSet::new(),
454 proc_macro_dylib_path: None,
455 source: None,
456 cfg: vec!["test".into(), "debug_assertions".into()],
457 env: BTreeMap::new(),
458 target: "x86_64-unknown-linux-gnu".into(),
459 crate_type: "rlib".into(),
460 },
461 CrateSpec {
462 crate_id: "ID-mylib.rs".into(),
463 display_name: "mylib_test".into(),
464 edition: "2018".into(),
465 root_module: "mylib.rs".into(),
466 is_workspace_member: true,
467 deps: BTreeSet::new(),
468 proc_macro_dylib_path: None,
469 source: None,
470 cfg: vec!["test".into(), "debug_assertions".into()],
471 env: BTreeMap::new(),
472 target: "x86_64-unknown-linux-gnu".into(),
473 crate_type: "bin".into(),
474 },
475 CrateSpec {
476 crate_id: "ID-mylib.rs".into(),
477 display_name: "mylib_main".into(),
478 edition: "2018".into(),
479 root_module: "mylib.rs".into(),
480 is_workspace_member: true,
481 deps: BTreeSet::new(),
482 proc_macro_dylib_path: None,
483 source: None,
484 cfg: vec!["test".into(), "debug_assertions".into()],
485 env: BTreeMap::new(),
486 target: "x86_64-unknown-linux-gnu".into(),
487 crate_type: "bin".into(),
488 },
489 CrateSpec {
490 crate_id: "ID-mylib2.rs".into(),
491 display_name: "mylib2".into(),
492 edition: "2018".into(),
493 root_module: "mylib2.rs".into(),
494 is_workspace_member: true,
495 deps: BTreeSet::from(["ID-mylib.rs".into()]),
496 proc_macro_dylib_path: None,
497 source: None,
498 cfg: vec!["test".into(), "debug_assertions".into()],
499 env: BTreeMap::new(),
500 target: "x86_64-unknown-linux-gnu".into(),
501 crate_type: "rlib".into(),
502 },
503 ];
504
505 for perm in crate_specs.into_iter().permutations(4) {
506 assert_eq!(
507 consolidate_crate_specs(perm).unwrap(),
508 BTreeSet::from([
509 CrateSpec {
510 crate_id: "ID-mylib.rs".into(),
511 display_name: "mylib".into(),
512 edition: "2018".into(),
513 root_module: "mylib.rs".into(),
514 is_workspace_member: true,
515 deps: BTreeSet::from([]),
516 proc_macro_dylib_path: None,
517 source: None,
518 cfg: vec!["test".into(), "debug_assertions".into()],
519 env: BTreeMap::new(),
520 target: "x86_64-unknown-linux-gnu".into(),
521 crate_type: "rlib".into(),
522 },
523 CrateSpec {
524 crate_id: "ID-mylib2.rs".into(),
525 display_name: "mylib2".into(),
526 edition: "2018".into(),
527 root_module: "mylib2.rs".into(),
528 is_workspace_member: true,
529 deps: BTreeSet::from(["ID-mylib.rs".into()]),
530 proc_macro_dylib_path: None,
531 source: None,
532 cfg: vec!["test".into(), "debug_assertions".into()],
533 env: BTreeMap::new(),
534 target: "x86_64-unknown-linux-gnu".into(),
535 crate_type: "rlib".into(),
536 },
537 ])
538 );
539 }
540 }
541
542 #[test]
consolidate_proc_macro_prefer_exec()543 fn consolidate_proc_macro_prefer_exec() {
544 // proc macro crates should prefer the -opt-exec- path which is always generated
545 // during builds where it is used, while the fastbuild version would only be built
546 // when explicitly building that target.
547 let crate_specs = vec![
548 CrateSpec {
549 crate_id: "ID-myproc_macro.rs".into(),
550 display_name: "myproc_macro".into(),
551 edition: "2018".into(),
552 root_module: "myproc_macro.rs".into(),
553 is_workspace_member: true,
554 deps: BTreeSet::new(),
555 proc_macro_dylib_path: Some(
556 "bazel-out/k8-opt-exec-F005BA11/bin/myproc_macro/libmyproc_macro-12345.so"
557 .into(),
558 ),
559 source: None,
560 cfg: vec!["test".into(), "debug_assertions".into()],
561 env: BTreeMap::new(),
562 target: "x86_64-unknown-linux-gnu".into(),
563 crate_type: "proc_macro".into(),
564 },
565 CrateSpec {
566 crate_id: "ID-myproc_macro.rs".into(),
567 display_name: "myproc_macro".into(),
568 edition: "2018".into(),
569 root_module: "myproc_macro.rs".into(),
570 is_workspace_member: true,
571 deps: BTreeSet::new(),
572 proc_macro_dylib_path: Some(
573 "bazel-out/k8-fastbuild/bin/myproc_macro/libmyproc_macro-12345.so".into(),
574 ),
575 source: None,
576 cfg: vec!["test".into(), "debug_assertions".into()],
577 env: BTreeMap::new(),
578 target: "x86_64-unknown-linux-gnu".into(),
579 crate_type: "proc_macro".into(),
580 },
581 ];
582
583 for perm in crate_specs.into_iter().permutations(2) {
584 assert_eq!(
585 consolidate_crate_specs(perm).unwrap(),
586 BTreeSet::from([CrateSpec {
587 crate_id: "ID-myproc_macro.rs".into(),
588 display_name: "myproc_macro".into(),
589 edition: "2018".into(),
590 root_module: "myproc_macro.rs".into(),
591 is_workspace_member: true,
592 deps: BTreeSet::new(),
593 proc_macro_dylib_path: Some(
594 "bazel-out/k8-opt-exec-F005BA11/bin/myproc_macro/libmyproc_macro-12345.so"
595 .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 }
606 }
607