1use core::fmt;
5use std::collections::{BTreeMap, BTreeSet, HashMap};
6use std::str::FromStr;
7
8use anyhow::Context;
9use camino::{Utf8Path, Utf8PathBuf};
10
11use crate::query::{CrateSpec, CrateType};
12use crate::{ToolchainInfo, buildfile_to_targets, source_file_to_buildfile};
13
14#[derive(Clone, Debug, serde_derive::Deserialize, serde_derive::Serialize)]
16#[serde(rename_all = "camelCase")]
17pub enum RustAnalyzerArg {
18 Path(Utf8PathBuf),
19 Buildfile(Utf8PathBuf),
20}
21
22impl RustAnalyzerArg {
23 pub fn into_target_details(self, workspace: &Utf8Path) -> anyhow::Result<(Utf8PathBuf, String)> {
25 match self {
26 Self::Path(file) => {
27 let buildfile = source_file_to_buildfile(&file)?;
28 buildfile_to_targets(workspace, &buildfile).map(|t| (buildfile, t))
29 }
30 Self::Buildfile(buildfile) => buildfile_to_targets(workspace, &buildfile).map(|t| (buildfile, t)),
31 }
32 }
33}
34
35impl FromStr for RustAnalyzerArg {
36 type Err = anyhow::Error;
37
38 fn from_str(s: &str) -> Result<Self, Self::Err> {
39 serde_json::from_str(s).context("rust analyzer argument error")
40 }
41}
42
43#[derive(Debug, serde_derive::Serialize)]
48#[serde(tag = "kind")]
49#[serde(rename_all = "snake_case")]
50pub enum DiscoverProject<'a> {
51 Finished {
52 buildfile: Utf8PathBuf,
53 project: RustProject,
54 },
55 Error {
56 error: String,
57 source: Option<String>,
58 },
59 Progress {
60 message: &'a fmt::Arguments<'a>,
61 },
62}
63
64#[derive(Debug, serde_derive::Serialize)]
69pub struct RustProject {
70 sysroot: Utf8PathBuf,
72
73 sysroot_src: Utf8PathBuf,
76
77 crates: Vec<Crate>,
82
83 runnables: Vec<Runnable>,
86}
87
88#[derive(Debug, serde_derive::Serialize)]
93pub struct Crate {
94 #[serde(skip_serializing_if = "Option::is_none")]
96 display_name: Option<String>,
97
98 root_module: String,
100
101 edition: String,
103
104 deps: Vec<Dependency>,
106
107 #[serde(skip_serializing_if = "Option::is_none")]
109 is_workspace_member: Option<bool>,
110
111 #[serde(skip_serializing_if = "Source::is_empty")]
113 source: Source,
114
115 cfg: BTreeSet<String>,
118
119 #[serde(skip_serializing_if = "Option::is_none")]
121 target: Option<String>,
122
123 #[serde(skip_serializing_if = "Option::is_none")]
125 env: Option<BTreeMap<String, String>>,
126
127 is_proc_macro: bool,
129
130 #[serde(skip_serializing_if = "Option::is_none")]
132 proc_macro_dylib_path: Option<String>,
133
134 #[serde(skip_serializing_if = "Option::is_none")]
136 build: Option<Build>,
137}
138
139#[derive(Debug, Default, serde_derive::Serialize)]
140pub struct Source {
141 include_dirs: Vec<String>,
142 exclude_dirs: Vec<String>,
143}
144
145impl Source {
146 fn is_empty(&self) -> bool {
147 self.include_dirs.is_empty() && self.exclude_dirs.is_empty()
148 }
149}
150
151#[derive(Debug, serde_derive::Serialize)]
152pub struct Dependency {
153 #[serde(rename = "crate")]
155 crate_index: usize,
156
157 name: String,
159}
160
161#[derive(Debug, serde_derive::Serialize)]
162pub struct Build {
163 label: String,
172 build_file: Utf8PathBuf,
174 target_kind: TargetKind,
180 runnables: Vec<Runnable>,
181}
182
183#[derive(Clone, Copy, Debug, PartialEq, serde_derive::Serialize)]
184#[serde(rename_all = "camelCase")]
185pub enum TargetKind {
186 Bin,
187 Lib,
189 Test,
190}
191
192#[derive(Debug, serde_derive::Serialize)]
216pub struct Runnable {
217 program: String,
221 args: Vec<String>,
227 cwd: Utf8PathBuf,
229 kind: RunnableKind,
230}
231
232#[derive(Debug, Clone, Copy, serde_derive::Serialize)]
234#[serde(rename_all = "camelCase")]
235pub enum RunnableKind {
236 Check,
238
239 Run,
241
242 TestOne,
244
245 TestMod,
247
248 DocTest,
250}
251
252pub fn assemble_rust_project(
253 bazel: &Utf8Path,
254 workspace: &Utf8Path,
255 output_base: &Utf8Path,
256 toolchain_info: ToolchainInfo,
257 crate_specs: impl IntoIterator<Item = CrateSpec>,
258) -> anyhow::Result<RustProject> {
259 let mut project = RustProject {
260 sysroot: toolchain_info.sysroot,
261 sysroot_src: toolchain_info.sysroot_src,
262 crates: Vec::new(),
263 runnables: vec![],
264 };
265
266 let mut all_crates: Vec<_> = crate_specs.into_iter().collect();
267
268 all_crates.sort_by_key(|a| !a.is_test);
269
270 let merged_crates_index: HashMap<_, _> = all_crates.iter().enumerate().map(|(idx, c)| (&c.crate_id, idx)).collect();
271
272 for c in all_crates.iter() {
273 log::trace!("Merging crate {}", &c.crate_id);
274
275 let target_kind = match c.crate_type {
276 CrateType::Bin if c.is_test => TargetKind::Test,
277 CrateType::Bin => TargetKind::Bin,
278 CrateType::Rlib
279 | CrateType::Lib
280 | CrateType::Dylib
281 | CrateType::Cdylib
282 | CrateType::Staticlib
283 | CrateType::ProcMacro => TargetKind::Lib,
284 };
285
286 let mut runnables = Vec::new();
287
288 if let Some(info) = &c.info {
289 if let Some(crate_label) = &info.crate_label
290 && matches!(target_kind, TargetKind::Bin)
291 {
292 runnables.push(Runnable {
293 program: bazel.to_string(),
294 args: vec!["run".to_string(), format!("//{crate_label}")],
295 cwd: workspace.to_owned(),
296 kind: RunnableKind::Run,
297 });
298 }
299
300 if let Some(test_label) = &info.test_label {
301 runnables.extend([
302 Runnable {
303 program: bazel.to_string(),
304 args: vec![
305 format!("--output_base={output_base}"),
306 "test".to_owned(),
307 format!("//{test_label}"),
308 "--test_output".to_owned(),
309 "streamed".to_owned(),
310 "--test_arg".to_owned(),
311 "--exact".to_owned(),
312 "--test_arg".to_owned(),
313 "{test_id}".to_owned(),
314 ],
315 cwd: workspace.to_owned(),
316 kind: RunnableKind::TestOne,
317 },
318 Runnable {
319 program: bazel.to_string(),
320 args: vec![
321 format!("--output_base={output_base}"),
322 "test".to_owned(),
323 format!("//{test_label}"),
324 "--test_output".to_owned(),
325 "streamed".to_owned(),
326 "--test_arg".to_owned(),
327 "{path}".to_owned(),
328 ],
329 cwd: workspace.to_owned(),
330 kind: RunnableKind::TestMod,
331 },
332 ]);
333 }
334
335 if let Some(doc_test_label) = &info.doc_test_label {
336 runnables.push(Runnable {
337 program: bazel.to_string(),
338 args: vec![
339 format!("--output_base={output_base}"),
340 "test".to_owned(),
341 format!("//{doc_test_label}"),
342 "--test_output".to_owned(),
343 "streamed".to_owned(),
344 "--test_arg".to_owned(),
345 "{test_id}".to_owned(),
346 ],
347 cwd: workspace.to_owned(),
348 kind: RunnableKind::DocTest,
349 });
350 }
351
352 if let Some(clippy_label) = &info.clippy_label {
353 runnables.push(Runnable {
354 program: bazel.to_string(),
355 args: vec![
356 format!("--output_base={output_base}"),
357 "run".to_owned(),
358 "--config=wrapper".to_owned(),
359 "//misc/utils/rust/analyzer/check".to_owned(),
360 "--".to_owned(),
361 "--config=wrapper".to_owned(),
362 format!("//{clippy_label}"),
363 ],
364 cwd: workspace.to_owned(),
365 kind: RunnableKind::Check,
366 });
367 }
368 }
369
370 project.crates.push(Crate {
371 display_name: Some(c.display_name.clone()),
372 root_module: c.root_module.clone(),
373 edition: c.edition.clone(),
374 deps: c
375 .deps
376 .iter()
377 .map(|dep| {
378 let crate_index = *merged_crates_index
379 .get(dep)
380 .expect("failed to find dependency on second lookup");
381 let dep_crate = &all_crates[crate_index];
382 let name = if let Some(alias) = c.aliases.get(dep) {
383 alias.clone()
384 } else {
385 dep_crate.display_name.clone()
386 };
387 Dependency { crate_index, name }
388 })
389 .collect(),
390 is_workspace_member: Some(c.is_workspace_member),
391 source: match &c.source {
392 Some(s) => Source {
393 exclude_dirs: s.exclude_dirs.clone(),
394 include_dirs: s.include_dirs.clone(),
395 },
396 None => Source::default(),
397 },
398 cfg: c.cfg.clone(),
399 target: Some(c.target.clone()),
400 env: Some(c.env.clone()),
401 is_proc_macro: c.proc_macro_dylib_path.is_some(),
402 proc_macro_dylib_path: c.proc_macro_dylib_path.clone(),
403 build: c.build.as_ref().map(|b| Build {
404 label: b.label.clone(),
405 build_file: b.build_file.clone().into(),
406 target_kind,
407 runnables,
408 }),
409 });
410 }
411
412 Ok(project)
413}