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