xref: /aosp_15_r20/external/bazelbuild-rules_rust/tools/rust_analyzer/rust_project.rs (revision d4726bddaa87cc4778e7472feed243fa4b6c267f)
1 //! Library for generating rust_project.json files from a `Vec<CrateSpec>`
2 //! See official documentation of file format at https://rust-analyzer.github.io/manual.html
3 
4 use std::collections::{BTreeMap, BTreeSet, HashMap};
5 use std::io::ErrorKind;
6 use std::path::Path;
7 
8 use anyhow::anyhow;
9 use serde::Serialize;
10 
11 use crate::aquery::CrateSpec;
12 
13 /// A `rust-project.json` workspace representation. See
14 /// [rust-analyzer documentation][rd] for a thorough description of this interface.
15 /// [rd]: https://rust-analyzer.github.io/manual.html#non-cargo-based-projects
16 #[derive(Debug, Serialize)]
17 pub struct RustProject {
18     /// The path to a Rust sysroot.
19     sysroot: Option<String>,
20 
21     /// Path to the directory with *source code* of
22     /// sysroot crates.
23     sysroot_src: Option<String>,
24 
25     /// The set of crates comprising the current
26     /// project. Must include all transitive
27     /// dependencies as well as sysroot crate (libstd,
28     /// libcore and such).
29     crates: Vec<Crate>,
30 }
31 
32 /// A `rust-project.json` crate representation. See
33 /// [rust-analyzer documentation][rd] for a thorough description of this interface.
34 /// [rd]: https://rust-analyzer.github.io/manual.html#non-cargo-based-projects
35 #[derive(Debug, Serialize)]
36 #[serde(default)]
37 pub struct Crate {
38     /// A name used in the package's project declaration
39     #[serde(skip_serializing_if = "Option::is_none")]
40     display_name: Option<String>,
41 
42     /// Path to the root module of the crate.
43     root_module: String,
44 
45     /// Edition of the crate.
46     edition: String,
47 
48     /// Dependencies
49     deps: Vec<Dependency>,
50 
51     /// Should this crate be treated as a member of current "workspace".
52     #[serde(skip_serializing_if = "Option::is_none")]
53     is_workspace_member: Option<bool>,
54 
55     /// Optionally specify the (super)set of `.rs` files comprising this crate.
56     #[serde(skip_serializing_if = "Source::is_empty")]
57     source: Source,
58 
59     /// The set of cfgs activated for a given crate, like
60     /// `["unix", "feature=\"foo\"", "feature=\"bar\""]`.
61     cfg: Vec<String>,
62 
63     /// Target triple for this Crate.
64     #[serde(skip_serializing_if = "Option::is_none")]
65     target: Option<String>,
66 
67     /// Environment variables, used for the `env!` macro
68     #[serde(skip_serializing_if = "Option::is_none")]
69     env: Option<BTreeMap<String, String>>,
70 
71     /// Whether the crate is a proc-macro crate.
72     is_proc_macro: bool,
73 
74     /// For proc-macro crates, path to compiled proc-macro (.so file).
75     #[serde(skip_serializing_if = "Option::is_none")]
76     proc_macro_dylib_path: Option<String>,
77 }
78 
79 #[derive(Debug, Default, Serialize)]
80 pub struct Source {
81     include_dirs: Vec<String>,
82     exclude_dirs: Vec<String>,
83 }
84 
85 impl Source {
86     /// Returns true if no include information has been added.
is_empty(&self) -> bool87     fn is_empty(&self) -> bool {
88         self.include_dirs.is_empty() && self.exclude_dirs.is_empty()
89     }
90 }
91 
92 #[derive(Debug, Serialize)]
93 pub struct Dependency {
94     /// Index of a crate in the `crates` array.
95     #[serde(rename = "crate")]
96     crate_index: usize,
97 
98     /// The display name of the crate.
99     name: String,
100 }
101 
generate_rust_project( sysroot: &str, sysroot_src: &str, crates: &BTreeSet<CrateSpec>, ) -> anyhow::Result<RustProject>102 pub fn generate_rust_project(
103     sysroot: &str,
104     sysroot_src: &str,
105     crates: &BTreeSet<CrateSpec>,
106 ) -> anyhow::Result<RustProject> {
107     let mut project = RustProject {
108         sysroot: Some(sysroot.into()),
109         sysroot_src: Some(sysroot_src.into()),
110         crates: Vec::new(),
111     };
112 
113     let mut unmerged_crates: Vec<&CrateSpec> = crates.iter().collect();
114     let mut skipped_crates: Vec<&CrateSpec> = Vec::new();
115     let mut merged_crates_index: HashMap<String, usize> = HashMap::new();
116 
117     while !unmerged_crates.is_empty() {
118         for c in unmerged_crates.iter() {
119             if c.deps
120                 .iter()
121                 .any(|dep| !merged_crates_index.contains_key(dep))
122             {
123                 log::trace!(
124                     "Skipped crate {} because missing deps: {:?}",
125                     &c.crate_id,
126                     c.deps
127                         .iter()
128                         .filter(|dep| !merged_crates_index.contains_key(*dep))
129                         .cloned()
130                         .collect::<Vec<_>>()
131                 );
132                 skipped_crates.push(c);
133             } else {
134                 log::trace!("Merging crate {}", &c.crate_id);
135                 merged_crates_index.insert(c.crate_id.clone(), project.crates.len());
136                 project.crates.push(Crate {
137                     display_name: Some(c.display_name.clone()),
138                     root_module: c.root_module.clone(),
139                     edition: c.edition.clone(),
140                     deps: c
141                         .deps
142                         .iter()
143                         .map(|dep| {
144                             let crate_index = *merged_crates_index
145                                 .get(dep)
146                                 .expect("failed to find dependency on second lookup");
147                             let dep_crate = &project.crates[crate_index];
148                             let name = if let Some(alias) = c.aliases.get(dep) {
149                                 alias.clone()
150                             } else {
151                                 dep_crate
152                                     .display_name
153                                     .as_ref()
154                                     .expect("all crates should have display_name")
155                                     .clone()
156                             };
157                             Dependency { crate_index, name }
158                         })
159                         .collect(),
160                     is_workspace_member: Some(c.is_workspace_member),
161                     source: match &c.source {
162                         Some(s) => Source {
163                             exclude_dirs: s.exclude_dirs.clone(),
164                             include_dirs: s.include_dirs.clone(),
165                         },
166                         None => Source::default(),
167                     },
168                     cfg: c.cfg.clone(),
169                     target: Some(c.target.clone()),
170                     env: Some(c.env.clone()),
171                     is_proc_macro: c.proc_macro_dylib_path.is_some(),
172                     proc_macro_dylib_path: c.proc_macro_dylib_path.clone(),
173                 });
174             }
175         }
176 
177         // This should not happen, but if it does exit to prevent infinite loop.
178         if unmerged_crates.len() == skipped_crates.len() {
179             log::debug!(
180                 "Did not make progress on {} unmerged crates. Crates: {:?}",
181                 skipped_crates.len(),
182                 skipped_crates
183             );
184             let crate_map: BTreeMap<String, &CrateSpec> = unmerged_crates
185                 .iter()
186                 .map(|c| (c.crate_id.to_string(), *c))
187                 .collect();
188 
189             for unmerged_crate in &unmerged_crates {
190                 let mut path = vec![];
191                 if let Some(cycle) = detect_cycle(unmerged_crate, &crate_map, &mut path) {
192                     log::warn!(
193                         "Cycle detected: {:?}",
194                         cycle
195                             .iter()
196                             .map(|c| c.crate_id.to_string())
197                             .collect::<Vec<String>>()
198                     );
199                 }
200             }
201             return Err(anyhow!(
202                 "Failed to make progress on building crate dependency graph"
203             ));
204         }
205         std::mem::swap(&mut unmerged_crates, &mut skipped_crates);
206         skipped_crates.clear();
207     }
208 
209     Ok(project)
210 }
211 
detect_cycle<'a>( current_crate: &'a CrateSpec, all_crates: &'a BTreeMap<String, &'a CrateSpec>, path: &mut Vec<&'a CrateSpec>, ) -> Option<Vec<&'a CrateSpec>>212 fn detect_cycle<'a>(
213     current_crate: &'a CrateSpec,
214     all_crates: &'a BTreeMap<String, &'a CrateSpec>,
215     path: &mut Vec<&'a CrateSpec>,
216 ) -> Option<Vec<&'a CrateSpec>> {
217     if path
218         .iter()
219         .any(|dependent_crate| dependent_crate.crate_id == current_crate.crate_id)
220     {
221         let mut cycle_path = path.clone();
222         cycle_path.push(current_crate);
223         return Some(cycle_path);
224     }
225 
226     path.push(current_crate);
227 
228     for dep in &current_crate.deps {
229         match all_crates.get(dep) {
230             Some(dep_crate) => {
231                 if let Some(cycle) = detect_cycle(dep_crate, all_crates, path) {
232                     return Some(cycle);
233                 }
234             }
235             None => log::debug!("dep {dep} not found in unmerged crate map"),
236         }
237     }
238 
239     path.pop();
240 
241     None
242 }
243 
write_rust_project( rust_project_path: &Path, execution_root: &Path, output_base: &Path, rust_project: &RustProject, ) -> anyhow::Result<()>244 pub fn write_rust_project(
245     rust_project_path: &Path,
246     execution_root: &Path,
247     output_base: &Path,
248     rust_project: &RustProject,
249 ) -> anyhow::Result<()> {
250     let execution_root = execution_root
251         .to_str()
252         .ok_or_else(|| anyhow!("execution_root is not valid UTF-8"))?;
253 
254     let output_base = output_base
255         .to_str()
256         .ok_or_else(|| anyhow!("output_base is not valid UTF-8"))?;
257 
258     // Try to remove the existing rust-project.json. It's OK if the file doesn't exist.
259     match std::fs::remove_file(rust_project_path) {
260         Ok(_) => {}
261         Err(err) if err.kind() == ErrorKind::NotFound => {}
262         Err(err) => {
263             return Err(anyhow!(
264                 "Unexpected error removing old rust-project.json: {}",
265                 err
266             ))
267         }
268     }
269 
270     // Render the `rust-project.json` file and replace the exec root
271     // placeholders with the path to the local exec root.
272     let rust_project_content = serde_json::to_string_pretty(rust_project)?
273         .replace("${pwd}", execution_root)
274         .replace("__EXEC_ROOT__", execution_root)
275         .replace("__OUTPUT_BASE__", output_base);
276 
277     // Write the new rust-project.json file.
278     std::fs::write(rust_project_path, rust_project_content)?;
279 
280     Ok(())
281 }
282 
283 #[cfg(test)]
284 mod tests {
285     use super::*;
286 
287     /// A simple example with a single crate and no dependencies.
288     #[test]
generate_rust_project_single()289     fn generate_rust_project_single() {
290         let project = generate_rust_project(
291             "sysroot",
292             "sysroot_src",
293             &BTreeSet::from([CrateSpec {
294                 aliases: BTreeMap::new(),
295                 crate_id: "ID-example".into(),
296                 display_name: "example".into(),
297                 edition: "2018".into(),
298                 root_module: "example/lib.rs".into(),
299                 is_workspace_member: true,
300                 deps: BTreeSet::new(),
301                 proc_macro_dylib_path: None,
302                 source: None,
303                 cfg: vec!["test".into(), "debug_assertions".into()],
304                 env: BTreeMap::new(),
305                 target: "x86_64-unknown-linux-gnu".into(),
306                 crate_type: "rlib".into(),
307             }]),
308         )
309         .expect("expect success");
310 
311         assert_eq!(project.crates.len(), 1);
312         let c = &project.crates[0];
313         assert_eq!(c.display_name, Some("example".into()));
314         assert_eq!(c.root_module, "example/lib.rs");
315         assert_eq!(c.deps.len(), 0);
316     }
317 
318     /// An example with a one crate having two dependencies.
319     #[test]
generate_rust_project_with_deps()320     fn generate_rust_project_with_deps() {
321         let project = generate_rust_project(
322             "sysroot",
323             "sysroot_src",
324             &BTreeSet::from([
325                 CrateSpec {
326                     aliases: BTreeMap::new(),
327                     crate_id: "ID-example".into(),
328                     display_name: "example".into(),
329                     edition: "2018".into(),
330                     root_module: "example/lib.rs".into(),
331                     is_workspace_member: true,
332                     deps: BTreeSet::from(["ID-dep_a".into(), "ID-dep_b".into()]),
333                     proc_macro_dylib_path: None,
334                     source: None,
335                     cfg: vec!["test".into(), "debug_assertions".into()],
336                     env: BTreeMap::new(),
337                     target: "x86_64-unknown-linux-gnu".into(),
338                     crate_type: "rlib".into(),
339                 },
340                 CrateSpec {
341                     aliases: BTreeMap::new(),
342                     crate_id: "ID-dep_a".into(),
343                     display_name: "dep_a".into(),
344                     edition: "2018".into(),
345                     root_module: "dep_a/lib.rs".into(),
346                     is_workspace_member: false,
347                     deps: BTreeSet::new(),
348                     proc_macro_dylib_path: None,
349                     source: None,
350                     cfg: vec!["test".into(), "debug_assertions".into()],
351                     env: BTreeMap::new(),
352                     target: "x86_64-unknown-linux-gnu".into(),
353                     crate_type: "rlib".into(),
354                 },
355                 CrateSpec {
356                     aliases: BTreeMap::new(),
357                     crate_id: "ID-dep_b".into(),
358                     display_name: "dep_b".into(),
359                     edition: "2018".into(),
360                     root_module: "dep_b/lib.rs".into(),
361                     is_workspace_member: false,
362                     deps: BTreeSet::new(),
363                     proc_macro_dylib_path: None,
364                     source: None,
365                     cfg: vec!["test".into(), "debug_assertions".into()],
366                     env: BTreeMap::new(),
367                     target: "x86_64-unknown-linux-gnu".into(),
368                     crate_type: "rlib".into(),
369                 },
370             ]),
371         )
372         .expect("expect success");
373 
374         assert_eq!(project.crates.len(), 3);
375         // Both dep_a and dep_b should be one of the first two crates.
376         assert!(
377             Some("dep_a".into()) == project.crates[0].display_name
378                 || Some("dep_a".into()) == project.crates[1].display_name
379         );
380         assert!(
381             Some("dep_b".into()) == project.crates[0].display_name
382                 || Some("dep_b".into()) == project.crates[1].display_name
383         );
384         let c = &project.crates[2];
385         assert_eq!(c.display_name, Some("example".into()));
386     }
387 }
388