blob: 7f931cf6f614fb88660a3afcefcc2a1357ba8780 [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 {
Adam Snaider1c095c92023-07-08 02:09:58 -040018 /// The path to a Rust sysroot.
19 sysroot: Option<String>,
20
Brian Silvermancc09f182022-03-09 15:40:20 -080021 /// 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)]
36pub 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)]
79pub struct Source {
80 include_dirs: Vec<String>,
81 exclude_dirs: Vec<String>,
82}
83
84#[derive(Debug, Serialize)]
85pub 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
94pub fn generate_rust_project(
Adam Snaider1c095c92023-07-08 02:09:58 -040095 sysroot: &str,
Brian Silvermancc09f182022-03-09 15:40:20 -080096 sysroot_src: &str,
97 crates: &BTreeSet<CrateSpec>,
98) -> anyhow::Result<RustProject> {
99 let mut project = RustProject {
Adam Snaider1c095c92023-07-08 02:09:58 -0400100 sysroot: Some(sysroot.into()),
Brian Silvermancc09f182022-03-09 15:40:20 -0800101 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");
Adam Snaider1c095c92023-07-08 02:09:58 -0400139 let dep_crate = &project.crates[crate_index];
Brian Silvermancc09f182022-03-09 15:40:20 -0800140 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 return Err(anyhow!(
172 "Failed to make progress on building crate dependency graph"
173 ));
174 }
175 std::mem::swap(&mut unmerged_crates, &mut skipped_crates);
176 skipped_crates.clear();
177 }
178
179 Ok(project)
180}
181
182pub fn write_rust_project(
183 rust_project_path: &Path,
184 execution_root: &Path,
Brian Silverman5f6f2762022-08-13 19:30:05 -0700185 output_base: &Path,
Brian Silvermancc09f182022-03-09 15:40:20 -0800186 rust_project: &RustProject,
187) -> anyhow::Result<()> {
188 let execution_root = execution_root
189 .to_str()
190 .ok_or_else(|| anyhow!("execution_root is not valid UTF-8"))?;
191
Brian Silverman5f6f2762022-08-13 19:30:05 -0700192 let output_base = output_base
193 .to_str()
194 .ok_or_else(|| anyhow!("output_base is not valid UTF-8"))?;
195
Brian Silvermancc09f182022-03-09 15:40:20 -0800196 // Try to remove the existing rust-project.json. It's OK if the file doesn't exist.
197 match std::fs::remove_file(rust_project_path) {
198 Ok(_) => {}
199 Err(err) if err.kind() == ErrorKind::NotFound => {}
200 Err(err) => {
201 return Err(anyhow!(
202 "Unexpected error removing old rust-project.json: {}",
203 err
204 ))
205 }
206 }
207
208 // Render the `rust-project.json` file and replace the exec root
209 // placeholders with the path to the local exec root.
Brian Silverman5f6f2762022-08-13 19:30:05 -0700210 let rust_project_content = serde_json::to_string(rust_project)?
211 .replace("__EXEC_ROOT__", execution_root)
212 .replace("__OUTPUT_BASE__", output_base);
Brian Silvermancc09f182022-03-09 15:40:20 -0800213
214 // Write the new rust-project.json file.
215 std::fs::write(rust_project_path, rust_project_content)?;
216
217 Ok(())
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 use std::collections::BTreeSet;
225
226 use crate::aquery::CrateSpec;
227
228 /// A simple example with a single crate and no dependencies.
229 #[test]
230 fn generate_rust_project_single() {
231 let project = generate_rust_project(
232 "sysroot",
Adam Snaider1c095c92023-07-08 02:09:58 -0400233 "sysroot_src",
Brian Silvermancc09f182022-03-09 15:40:20 -0800234 &BTreeSet::from([CrateSpec {
235 crate_id: "ID-example".into(),
236 display_name: "example".into(),
237 edition: "2018".into(),
238 root_module: "example/lib.rs".into(),
239 is_workspace_member: true,
240 deps: BTreeSet::new(),
241 proc_macro_dylib_path: None,
242 source: None,
243 cfg: vec!["test".into(), "debug_assertions".into()],
244 env: BTreeMap::new(),
245 target: "x86_64-unknown-linux-gnu".into(),
246 crate_type: "rlib".into(),
247 }]),
248 )
249 .expect("expect success");
250
251 assert_eq!(project.crates.len(), 1);
252 let c = &project.crates[0];
253 assert_eq!(c.display_name, Some("example".into()));
254 assert_eq!(c.root_module, "example/lib.rs");
255 assert_eq!(c.deps.len(), 0);
256 }
257
258 /// An example with a one crate having two dependencies.
259 #[test]
260 fn generate_rust_project_with_deps() {
261 let project = generate_rust_project(
262 "sysroot",
Adam Snaider1c095c92023-07-08 02:09:58 -0400263 "sysroot_src",
Brian Silvermancc09f182022-03-09 15:40:20 -0800264 &BTreeSet::from([
265 CrateSpec {
266 crate_id: "ID-example".into(),
267 display_name: "example".into(),
268 edition: "2018".into(),
269 root_module: "example/lib.rs".into(),
270 is_workspace_member: true,
271 deps: BTreeSet::from(["ID-dep_a".into(), "ID-dep_b".into()]),
272 proc_macro_dylib_path: None,
273 source: None,
274 cfg: vec!["test".into(), "debug_assertions".into()],
275 env: BTreeMap::new(),
276 target: "x86_64-unknown-linux-gnu".into(),
277 crate_type: "rlib".into(),
278 },
279 CrateSpec {
280 crate_id: "ID-dep_a".into(),
281 display_name: "dep_a".into(),
282 edition: "2018".into(),
283 root_module: "dep_a/lib.rs".into(),
284 is_workspace_member: false,
285 deps: BTreeSet::new(),
286 proc_macro_dylib_path: None,
287 source: None,
288 cfg: vec!["test".into(), "debug_assertions".into()],
289 env: BTreeMap::new(),
290 target: "x86_64-unknown-linux-gnu".into(),
291 crate_type: "rlib".into(),
292 },
293 CrateSpec {
294 crate_id: "ID-dep_b".into(),
295 display_name: "dep_b".into(),
296 edition: "2018".into(),
297 root_module: "dep_b/lib.rs".into(),
298 is_workspace_member: false,
299 deps: BTreeSet::new(),
300 proc_macro_dylib_path: None,
301 source: None,
302 cfg: vec!["test".into(), "debug_assertions".into()],
303 env: BTreeMap::new(),
304 target: "x86_64-unknown-linux-gnu".into(),
305 crate_type: "rlib".into(),
306 },
307 ]),
308 )
309 .expect("expect success");
310
311 assert_eq!(project.crates.len(), 3);
312 // Both dep_a and dep_b should be one of the first two crates.
313 assert!(
314 Some("dep_a".into()) == project.crates[0].display_name
315 || Some("dep_a".into()) == project.crates[1].display_name
316 );
317 assert!(
318 Some("dep_b".into()) == project.crates[0].display_name
319 || Some("dep_b".into()) == project.crates[1].display_name
320 );
321 let c = &project.crates[2];
322 assert_eq!(c.display_name, Some("example".into()));
323 }
324}