• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 pub struct Crate {
37     /// A name used in the package's project declaration
38     #[serde(skip_serializing_if = "Option::is_none")]
39     display_name: Option<String>,
40 
41     /// Path to the root module of the crate.
42     root_module: String,
43 
44     /// Edition of the crate.
45     edition: String,
46 
47     /// Dependencies
48     deps: Vec<Dependency>,
49 
50     /// Should this crate be treated as a member of current "workspace".
51     #[serde(skip_serializing_if = "Option::is_none")]
52     is_workspace_member: Option<bool>,
53 
54     /// Optionally specify the (super)set of `.rs` files comprising this crate.
55     #[serde(skip_serializing_if = "Option::is_none")]
56     source: Option<Source>,
57 
58     /// The set of cfgs activated for a given crate, like
59     /// `["unix", "feature=\"foo\"", "feature=\"bar\""]`.
60     cfg: Vec<String>,
61 
62     /// Target triple for this Crate.
63     #[serde(skip_serializing_if = "Option::is_none")]
64     target: Option<String>,
65 
66     /// Environment variables, used for the `env!` macro
67     #[serde(skip_serializing_if = "Option::is_none")]
68     env: Option<BTreeMap<String, String>>,
69 
70     /// Whether the crate is a proc-macro crate.
71     is_proc_macro: bool,
72 
73     /// For proc-macro crates, path to compiled proc-macro (.so file).
74     #[serde(skip_serializing_if = "Option::is_none")]
75     proc_macro_dylib_path: Option<String>,
76 }
77 
78 #[derive(Debug, Serialize)]
79 pub struct Source {
80     include_dirs: Vec<String>,
81     exclude_dirs: Vec<String>,
82 }
83 
84 #[derive(Debug, Serialize)]
85 pub struct Dependency {
86     /// Index of a crate in the `crates` array.
87     #[serde(rename = "crate")]
88     crate_index: usize,
89 
90     /// The display name of the crate.
91     name: String,
92 }
93 
generate_rust_project( sysroot: &str, sysroot_src: &str, crates: &BTreeSet<CrateSpec>, ) -> anyhow::Result<RustProject>94 pub fn generate_rust_project(
95     sysroot: &str,
96     sysroot_src: &str,
97     crates: &BTreeSet<CrateSpec>,
98 ) -> anyhow::Result<RustProject> {
99     let mut project = RustProject {
100         sysroot: Some(sysroot.into()),
101         sysroot_src: Some(sysroot_src.into()),
102         crates: Vec::new(),
103     };
104 
105     let mut unmerged_crates: Vec<&CrateSpec> = crates.iter().collect();
106     let mut skipped_crates: Vec<&CrateSpec> = Vec::new();
107     let mut merged_crates_index: HashMap<String, usize> = HashMap::new();
108 
109     while !unmerged_crates.is_empty() {
110         for c in unmerged_crates.iter() {
111             if c.deps
112                 .iter()
113                 .any(|dep| !merged_crates_index.contains_key(dep))
114             {
115                 log::trace!(
116                     "Skipped crate {} because missing deps: {:?}",
117                     &c.crate_id,
118                     c.deps
119                         .iter()
120                         .filter(|dep| !merged_crates_index.contains_key(*dep))
121                         .cloned()
122                         .collect::<Vec<_>>()
123                 );
124                 skipped_crates.push(c);
125             } else {
126                 log::trace!("Merging crate {}", &c.crate_id);
127                 merged_crates_index.insert(c.crate_id.clone(), project.crates.len());
128                 project.crates.push(Crate {
129                     display_name: Some(c.display_name.clone()),
130                     root_module: c.root_module.clone(),
131                     edition: c.edition.clone(),
132                     deps: c
133                         .deps
134                         .iter()
135                         .map(|dep| {
136                             let crate_index = *merged_crates_index
137                                 .get(dep)
138                                 .expect("failed to find dependency on second lookup");
139                             let dep_crate = &project.crates[crate_index];
140                             Dependency {
141                                 crate_index,
142                                 name: dep_crate
143                                     .display_name
144                                     .as_ref()
145                                     .expect("all crates should have display_name")
146                                     .clone(),
147                             }
148                         })
149                         .collect(),
150                     is_workspace_member: Some(c.is_workspace_member),
151                     source: c.source.as_ref().map(|s| Source {
152                         exclude_dirs: s.exclude_dirs.clone(),
153                         include_dirs: s.include_dirs.clone(),
154                     }),
155                     cfg: c.cfg.clone(),
156                     target: Some(c.target.clone()),
157                     env: Some(c.env.clone()),
158                     is_proc_macro: c.proc_macro_dylib_path.is_some(),
159                     proc_macro_dylib_path: c.proc_macro_dylib_path.clone(),
160                 });
161             }
162         }
163 
164         // This should not happen, but if it does exit to prevent infinite loop.
165         if unmerged_crates.len() == skipped_crates.len() {
166             log::debug!(
167                 "Did not make progress on {} unmerged crates. Crates: {:?}",
168                 skipped_crates.len(),
169                 skipped_crates
170             );
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             }
188             return Err(anyhow!(
189                 "Failed to make progress on building crate dependency graph"
190             ));
191         }
192         std::mem::swap(&mut unmerged_crates, &mut skipped_crates);
193         skipped_crates.clear();
194     }
195 
196     Ok(project)
197 }
198 
detect_cycle<'a>( current_crate: &'a CrateSpec, all_crates: &'a BTreeMap<String, &'a CrateSpec>, path: &mut Vec<&'a CrateSpec>, ) -> Option<Vec<&'a CrateSpec>>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 
write_rust_project( rust_project_path: &Path, execution_root: &Path, output_base: &Path, rust_project: &RustProject, ) -> anyhow::Result<()>231 pub fn write_rust_project(
232     rust_project_path: &Path,
233     execution_root: &Path,
234     output_base: &Path,
235     rust_project: &RustProject,
236 ) -> anyhow::Result<()> {
237     let execution_root = execution_root
238         .to_str()
239         .ok_or_else(|| anyhow!("execution_root is not valid UTF-8"))?;
240 
241     let output_base = output_base
242         .to_str()
243         .ok_or_else(|| anyhow!("output_base is not valid UTF-8"))?;
244 
245     // Try to remove the existing rust-project.json. It's OK if the file doesn't exist.
246     match std::fs::remove_file(rust_project_path) {
247         Ok(_) => {}
248         Err(err) if err.kind() == ErrorKind::NotFound => {}
249         Err(err) => {
250             return Err(anyhow!(
251                 "Unexpected error removing old rust-project.json: {}",
252                 err
253             ))
254         }
255     }
256 
257     // Render the `rust-project.json` file and replace the exec root
258     // placeholders with the path to the local exec root.
259     let rust_project_content = serde_json::to_string(rust_project)?
260         .replace("${pwd}", execution_root)
261         .replace("__EXEC_ROOT__", execution_root)
262         .replace("__OUTPUT_BASE__", output_base);
263 
264     // Write the new rust-project.json file.
265     std::fs::write(rust_project_path, rust_project_content)?;
266 
267     Ok(())
268 }
269 
270 #[cfg(test)]
271 mod tests {
272     use super::*;
273 
274     use std::collections::BTreeSet;
275 
276     use crate::aquery::CrateSpec;
277 
278     /// A simple example with a single crate and no dependencies.
279     #[test]
generate_rust_project_single()280     fn generate_rust_project_single() {
281         let project = generate_rust_project(
282             "sysroot",
283             "sysroot_src",
284             &BTreeSet::from([CrateSpec {
285                 crate_id: "ID-example".into(),
286                 display_name: "example".into(),
287                 edition: "2018".into(),
288                 root_module: "example/lib.rs".into(),
289                 is_workspace_member: true,
290                 deps: BTreeSet::new(),
291                 proc_macro_dylib_path: None,
292                 source: None,
293                 cfg: vec!["test".into(), "debug_assertions".into()],
294                 env: BTreeMap::new(),
295                 target: "x86_64-unknown-linux-gnu".into(),
296                 crate_type: "rlib".into(),
297             }]),
298         )
299         .expect("expect success");
300 
301         assert_eq!(project.crates.len(), 1);
302         let c = &project.crates[0];
303         assert_eq!(c.display_name, Some("example".into()));
304         assert_eq!(c.root_module, "example/lib.rs");
305         assert_eq!(c.deps.len(), 0);
306     }
307 
308     /// An example with a one crate having two dependencies.
309     #[test]
generate_rust_project_with_deps()310     fn generate_rust_project_with_deps() {
311         let project = generate_rust_project(
312             "sysroot",
313             "sysroot_src",
314             &BTreeSet::from([
315                 CrateSpec {
316                     crate_id: "ID-example".into(),
317                     display_name: "example".into(),
318                     edition: "2018".into(),
319                     root_module: "example/lib.rs".into(),
320                     is_workspace_member: true,
321                     deps: BTreeSet::from(["ID-dep_a".into(), "ID-dep_b".into()]),
322                     proc_macro_dylib_path: None,
323                     source: None,
324                     cfg: vec!["test".into(), "debug_assertions".into()],
325                     env: BTreeMap::new(),
326                     target: "x86_64-unknown-linux-gnu".into(),
327                     crate_type: "rlib".into(),
328                 },
329                 CrateSpec {
330                     crate_id: "ID-dep_a".into(),
331                     display_name: "dep_a".into(),
332                     edition: "2018".into(),
333                     root_module: "dep_a/lib.rs".into(),
334                     is_workspace_member: false,
335                     deps: BTreeSet::new(),
336                     proc_macro_dylib_path: None,
337                     source: None,
338                     cfg: vec!["test".into(), "debug_assertions".into()],
339                     env: BTreeMap::new(),
340                     target: "x86_64-unknown-linux-gnu".into(),
341                     crate_type: "rlib".into(),
342                 },
343                 CrateSpec {
344                     crate_id: "ID-dep_b".into(),
345                     display_name: "dep_b".into(),
346                     edition: "2018".into(),
347                     root_module: "dep_b/lib.rs".into(),
348                     is_workspace_member: false,
349                     deps: BTreeSet::new(),
350                     proc_macro_dylib_path: None,
351                     source: None,
352                     cfg: vec!["test".into(), "debug_assertions".into()],
353                     env: BTreeMap::new(),
354                     target: "x86_64-unknown-linux-gnu".into(),
355                     crate_type: "rlib".into(),
356                 },
357             ]),
358         )
359         .expect("expect success");
360 
361         assert_eq!(project.crates.len(), 3);
362         // Both dep_a and dep_b should be one of the first two crates.
363         assert!(
364             Some("dep_a".into()) == project.crates[0].display_name
365                 || Some("dep_a".into()) == project.crates[1].display_name
366         );
367         assert!(
368             Some("dep_b".into()) == project.crates[0].display_name
369                 || Some("dep_b".into()) == project.crates[1].display_name
370         );
371         let c = &project.crates[2];
372         assert_eq!(c.display_name, Some("example".into()));
373     }
374 }
375