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