blob: 0cc937869bd4a355ed4e458d7ffc116ef3684f06 [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,
180 rust_project: &RustProject,
181) -> anyhow::Result<()> {
182 let execution_root = execution_root
183 .to_str()
184 .ok_or_else(|| anyhow!("execution_root is not valid UTF-8"))?;
185
186 // Try to remove the existing rust-project.json. It's OK if the file doesn't exist.
187 match std::fs::remove_file(rust_project_path) {
188 Ok(_) => {}
189 Err(err) if err.kind() == ErrorKind::NotFound => {}
190 Err(err) => {
191 return Err(anyhow!(
192 "Unexpected error removing old rust-project.json: {}",
193 err
194 ))
195 }
196 }
197
198 // Render the `rust-project.json` file and replace the exec root
199 // placeholders with the path to the local exec root.
200 let rust_project_content =
201 serde_json::to_string(rust_project)?.replace("__EXEC_ROOT__", execution_root);
202
203 // Write the new rust-project.json file.
204 std::fs::write(rust_project_path, rust_project_content)?;
205
206 Ok(())
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 use std::collections::BTreeSet;
214
215 use crate::aquery::CrateSpec;
216
217 /// A simple example with a single crate and no dependencies.
218 #[test]
219 fn generate_rust_project_single() {
220 let project = generate_rust_project(
221 "sysroot",
222 &BTreeSet::from([CrateSpec {
223 crate_id: "ID-example".into(),
224 display_name: "example".into(),
225 edition: "2018".into(),
226 root_module: "example/lib.rs".into(),
227 is_workspace_member: true,
228 deps: BTreeSet::new(),
229 proc_macro_dylib_path: None,
230 source: None,
231 cfg: vec!["test".into(), "debug_assertions".into()],
232 env: BTreeMap::new(),
233 target: "x86_64-unknown-linux-gnu".into(),
234 crate_type: "rlib".into(),
235 }]),
236 )
237 .expect("expect success");
238
239 assert_eq!(project.crates.len(), 1);
240 let c = &project.crates[0];
241 assert_eq!(c.display_name, Some("example".into()));
242 assert_eq!(c.root_module, "example/lib.rs");
243 assert_eq!(c.deps.len(), 0);
244 }
245
246 /// An example with a one crate having two dependencies.
247 #[test]
248 fn generate_rust_project_with_deps() {
249 let project = generate_rust_project(
250 "sysroot",
251 &BTreeSet::from([
252 CrateSpec {
253 crate_id: "ID-example".into(),
254 display_name: "example".into(),
255 edition: "2018".into(),
256 root_module: "example/lib.rs".into(),
257 is_workspace_member: true,
258 deps: BTreeSet::from(["ID-dep_a".into(), "ID-dep_b".into()]),
259 proc_macro_dylib_path: None,
260 source: None,
261 cfg: vec!["test".into(), "debug_assertions".into()],
262 env: BTreeMap::new(),
263 target: "x86_64-unknown-linux-gnu".into(),
264 crate_type: "rlib".into(),
265 },
266 CrateSpec {
267 crate_id: "ID-dep_a".into(),
268 display_name: "dep_a".into(),
269 edition: "2018".into(),
270 root_module: "dep_a/lib.rs".into(),
271 is_workspace_member: false,
272 deps: BTreeSet::new(),
273 proc_macro_dylib_path: None,
274 source: None,
275 cfg: vec!["test".into(), "debug_assertions".into()],
276 env: BTreeMap::new(),
277 target: "x86_64-unknown-linux-gnu".into(),
278 crate_type: "rlib".into(),
279 },
280 CrateSpec {
281 crate_id: "ID-dep_b".into(),
282 display_name: "dep_b".into(),
283 edition: "2018".into(),
284 root_module: "dep_b/lib.rs".into(),
285 is_workspace_member: false,
286 deps: BTreeSet::new(),
287 proc_macro_dylib_path: None,
288 source: None,
289 cfg: vec!["test".into(), "debug_assertions".into()],
290 env: BTreeMap::new(),
291 target: "x86_64-unknown-linux-gnu".into(),
292 crate_type: "rlib".into(),
293 },
294 ]),
295 )
296 .expect("expect success");
297
298 assert_eq!(project.crates.len(), 3);
299 // Both dep_a and dep_b should be one of the first two crates.
300 assert!(
301 Some("dep_a".into()) == project.crates[0].display_name
302 || Some("dep_a".into()) == project.crates[1].display_name
303 );
304 assert!(
305 Some("dep_b".into()) == project.crates[0].display_name
306 || Some("dep_b".into()) == project.crates[1].display_name
307 );
308 let c = &project.crates[2];
309 assert_eq!(c.display_name, Some("example".into()));
310 }
311}