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 ¤t_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