diff options
Diffstat (limited to 'crates/project_model/src/lib.rs')
-rw-r--r-- | crates/project_model/src/lib.rs | 544 |
1 files changed, 544 insertions, 0 deletions
diff --git a/crates/project_model/src/lib.rs b/crates/project_model/src/lib.rs new file mode 100644 index 000000000..ee42198f3 --- /dev/null +++ b/crates/project_model/src/lib.rs | |||
@@ -0,0 +1,544 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | mod cargo_workspace; | ||
4 | mod project_json; | ||
5 | mod sysroot; | ||
6 | mod cfg_flag; | ||
7 | |||
8 | use std::{ | ||
9 | fs::{self, read_dir, ReadDir}, | ||
10 | io, | ||
11 | process::Command, | ||
12 | }; | ||
13 | |||
14 | use anyhow::{bail, Context, Result}; | ||
15 | use cfg::CfgOptions; | ||
16 | use paths::{AbsPath, AbsPathBuf}; | ||
17 | use ra_db::{CrateGraph, CrateId, CrateName, Edition, Env, FileId}; | ||
18 | use rustc_hash::{FxHashMap, FxHashSet}; | ||
19 | |||
20 | use crate::cfg_flag::CfgFlag; | ||
21 | |||
22 | pub use crate::{ | ||
23 | cargo_workspace::{CargoConfig, CargoWorkspace, Package, Target, TargetKind}, | ||
24 | project_json::{ProjectJson, ProjectJsonData}, | ||
25 | sysroot::Sysroot, | ||
26 | }; | ||
27 | |||
28 | pub use ra_proc_macro::ProcMacroClient; | ||
29 | |||
30 | #[derive(Debug, Clone, Eq, PartialEq)] | ||
31 | pub enum ProjectWorkspace { | ||
32 | /// Project workspace was discovered by running `cargo metadata` and `rustc --print sysroot`. | ||
33 | Cargo { cargo: CargoWorkspace, sysroot: Sysroot }, | ||
34 | /// Project workspace was manually specified using a `rust-project.json` file. | ||
35 | Json { project: ProjectJson }, | ||
36 | } | ||
37 | |||
38 | /// `PackageRoot` describes a package root folder. | ||
39 | /// Which may be an external dependency, or a member of | ||
40 | /// the current workspace. | ||
41 | #[derive(Debug, Clone, Eq, PartialEq, Hash)] | ||
42 | pub struct PackageRoot { | ||
43 | /// Is a member of the current workspace | ||
44 | pub is_member: bool, | ||
45 | pub include: Vec<AbsPathBuf>, | ||
46 | pub exclude: Vec<AbsPathBuf>, | ||
47 | } | ||
48 | |||
49 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] | ||
50 | pub enum ProjectManifest { | ||
51 | ProjectJson(AbsPathBuf), | ||
52 | CargoToml(AbsPathBuf), | ||
53 | } | ||
54 | |||
55 | impl ProjectManifest { | ||
56 | pub fn from_manifest_file(path: AbsPathBuf) -> Result<ProjectManifest> { | ||
57 | if path.ends_with("rust-project.json") { | ||
58 | return Ok(ProjectManifest::ProjectJson(path)); | ||
59 | } | ||
60 | if path.ends_with("Cargo.toml") { | ||
61 | return Ok(ProjectManifest::CargoToml(path)); | ||
62 | } | ||
63 | bail!("project root must point to Cargo.toml or rust-project.json: {}", path.display()) | ||
64 | } | ||
65 | |||
66 | pub fn discover_single(path: &AbsPath) -> Result<ProjectManifest> { | ||
67 | let mut candidates = ProjectManifest::discover(path)?; | ||
68 | let res = match candidates.pop() { | ||
69 | None => bail!("no projects"), | ||
70 | Some(it) => it, | ||
71 | }; | ||
72 | |||
73 | if !candidates.is_empty() { | ||
74 | bail!("more than one project") | ||
75 | } | ||
76 | Ok(res) | ||
77 | } | ||
78 | |||
79 | pub fn discover(path: &AbsPath) -> io::Result<Vec<ProjectManifest>> { | ||
80 | if let Some(project_json) = find_in_parent_dirs(path, "rust-project.json") { | ||
81 | return Ok(vec![ProjectManifest::ProjectJson(project_json)]); | ||
82 | } | ||
83 | return find_cargo_toml(path) | ||
84 | .map(|paths| paths.into_iter().map(ProjectManifest::CargoToml).collect()); | ||
85 | |||
86 | fn find_cargo_toml(path: &AbsPath) -> io::Result<Vec<AbsPathBuf>> { | ||
87 | match find_in_parent_dirs(path, "Cargo.toml") { | ||
88 | Some(it) => Ok(vec![it]), | ||
89 | None => Ok(find_cargo_toml_in_child_dir(read_dir(path)?)), | ||
90 | } | ||
91 | } | ||
92 | |||
93 | fn find_in_parent_dirs(path: &AbsPath, target_file_name: &str) -> Option<AbsPathBuf> { | ||
94 | if path.ends_with(target_file_name) { | ||
95 | return Some(path.to_path_buf()); | ||
96 | } | ||
97 | |||
98 | let mut curr = Some(path); | ||
99 | |||
100 | while let Some(path) = curr { | ||
101 | let candidate = path.join(target_file_name); | ||
102 | if candidate.exists() { | ||
103 | return Some(candidate); | ||
104 | } | ||
105 | curr = path.parent(); | ||
106 | } | ||
107 | |||
108 | None | ||
109 | } | ||
110 | |||
111 | fn find_cargo_toml_in_child_dir(entities: ReadDir) -> Vec<AbsPathBuf> { | ||
112 | // Only one level down to avoid cycles the easy way and stop a runaway scan with large projects | ||
113 | entities | ||
114 | .filter_map(Result::ok) | ||
115 | .map(|it| it.path().join("Cargo.toml")) | ||
116 | .filter(|it| it.exists()) | ||
117 | .map(AbsPathBuf::assert) | ||
118 | .collect() | ||
119 | } | ||
120 | } | ||
121 | |||
122 | pub fn discover_all(paths: &[impl AsRef<AbsPath>]) -> Vec<ProjectManifest> { | ||
123 | let mut res = paths | ||
124 | .iter() | ||
125 | .filter_map(|it| ProjectManifest::discover(it.as_ref()).ok()) | ||
126 | .flatten() | ||
127 | .collect::<FxHashSet<_>>() | ||
128 | .into_iter() | ||
129 | .collect::<Vec<_>>(); | ||
130 | res.sort(); | ||
131 | res | ||
132 | } | ||
133 | } | ||
134 | |||
135 | impl ProjectWorkspace { | ||
136 | pub fn load( | ||
137 | manifest: ProjectManifest, | ||
138 | cargo_config: &CargoConfig, | ||
139 | with_sysroot: bool, | ||
140 | ) -> Result<ProjectWorkspace> { | ||
141 | let res = match manifest { | ||
142 | ProjectManifest::ProjectJson(project_json) => { | ||
143 | let file = fs::read_to_string(&project_json).with_context(|| { | ||
144 | format!("Failed to read json file {}", project_json.display()) | ||
145 | })?; | ||
146 | let data = serde_json::from_str(&file).with_context(|| { | ||
147 | format!("Failed to deserialize json file {}", project_json.display()) | ||
148 | })?; | ||
149 | let project_location = project_json.parent().unwrap().to_path_buf(); | ||
150 | let project = ProjectJson::new(&project_location, data); | ||
151 | ProjectWorkspace::Json { project } | ||
152 | } | ||
153 | ProjectManifest::CargoToml(cargo_toml) => { | ||
154 | let cargo = CargoWorkspace::from_cargo_metadata(&cargo_toml, cargo_config) | ||
155 | .with_context(|| { | ||
156 | format!( | ||
157 | "Failed to read Cargo metadata from Cargo.toml file {}", | ||
158 | cargo_toml.display() | ||
159 | ) | ||
160 | })?; | ||
161 | let sysroot = if with_sysroot { | ||
162 | Sysroot::discover(&cargo_toml).with_context(|| { | ||
163 | format!( | ||
164 | "Failed to find sysroot for Cargo.toml file {}. Is rust-src installed?", | ||
165 | cargo_toml.display() | ||
166 | ) | ||
167 | })? | ||
168 | } else { | ||
169 | Sysroot::default() | ||
170 | }; | ||
171 | ProjectWorkspace::Cargo { cargo, sysroot } | ||
172 | } | ||
173 | }; | ||
174 | |||
175 | Ok(res) | ||
176 | } | ||
177 | |||
178 | /// Returns the roots for the current `ProjectWorkspace` | ||
179 | /// The return type contains the path and whether or not | ||
180 | /// the root is a member of the current workspace | ||
181 | pub fn to_roots(&self) -> Vec<PackageRoot> { | ||
182 | match self { | ||
183 | ProjectWorkspace::Json { project } => project | ||
184 | .crates | ||
185 | .iter() | ||
186 | .map(|krate| PackageRoot { | ||
187 | is_member: krate.is_workspace_member, | ||
188 | include: krate.include.clone(), | ||
189 | exclude: krate.exclude.clone(), | ||
190 | }) | ||
191 | .collect::<FxHashSet<_>>() | ||
192 | .into_iter() | ||
193 | .collect::<Vec<_>>(), | ||
194 | ProjectWorkspace::Cargo { cargo, sysroot } => cargo | ||
195 | .packages() | ||
196 | .map(|pkg| { | ||
197 | let is_member = cargo[pkg].is_member; | ||
198 | let pkg_root = cargo[pkg].root().to_path_buf(); | ||
199 | |||
200 | let mut include = vec![pkg_root.clone()]; | ||
201 | include.extend(cargo[pkg].out_dir.clone()); | ||
202 | |||
203 | let mut exclude = vec![pkg_root.join(".git")]; | ||
204 | if is_member { | ||
205 | exclude.push(pkg_root.join("target")); | ||
206 | } else { | ||
207 | exclude.push(pkg_root.join("tests")); | ||
208 | exclude.push(pkg_root.join("examples")); | ||
209 | exclude.push(pkg_root.join("benches")); | ||
210 | } | ||
211 | PackageRoot { is_member, include, exclude } | ||
212 | }) | ||
213 | .chain(sysroot.crates().map(|krate| PackageRoot { | ||
214 | is_member: false, | ||
215 | include: vec![sysroot[krate].root_dir().to_path_buf()], | ||
216 | exclude: Vec::new(), | ||
217 | })) | ||
218 | .collect(), | ||
219 | } | ||
220 | } | ||
221 | |||
222 | pub fn proc_macro_dylib_paths(&self) -> Vec<AbsPathBuf> { | ||
223 | match self { | ||
224 | ProjectWorkspace::Json { project } => project | ||
225 | .crates | ||
226 | .iter() | ||
227 | .filter_map(|krate| krate.proc_macro_dylib_path.as_ref()) | ||
228 | .cloned() | ||
229 | .collect(), | ||
230 | ProjectWorkspace::Cargo { cargo, sysroot: _sysroot } => cargo | ||
231 | .packages() | ||
232 | .filter_map(|pkg| cargo[pkg].proc_macro_dylib_path.as_ref()) | ||
233 | .cloned() | ||
234 | .collect(), | ||
235 | } | ||
236 | } | ||
237 | |||
238 | pub fn n_packages(&self) -> usize { | ||
239 | match self { | ||
240 | ProjectWorkspace::Json { project, .. } => project.crates.len(), | ||
241 | ProjectWorkspace::Cargo { cargo, sysroot } => { | ||
242 | cargo.packages().len() + sysroot.crates().len() | ||
243 | } | ||
244 | } | ||
245 | } | ||
246 | |||
247 | pub fn to_crate_graph( | ||
248 | &self, | ||
249 | target: Option<&str>, | ||
250 | proc_macro_client: &ProcMacroClient, | ||
251 | load: &mut dyn FnMut(&AbsPath) -> Option<FileId>, | ||
252 | ) -> CrateGraph { | ||
253 | let mut crate_graph = CrateGraph::default(); | ||
254 | match self { | ||
255 | ProjectWorkspace::Json { project } => { | ||
256 | let mut cfg_cache: FxHashMap<Option<&str>, Vec<CfgFlag>> = FxHashMap::default(); | ||
257 | let crates: FxHashMap<_, _> = project | ||
258 | .crates | ||
259 | .iter() | ||
260 | .enumerate() | ||
261 | .filter_map(|(seq_index, krate)| { | ||
262 | let file_path = &krate.root_module; | ||
263 | let file_id = load(&file_path)?; | ||
264 | |||
265 | let env = krate.env.clone().into_iter().collect(); | ||
266 | let proc_macro = krate | ||
267 | .proc_macro_dylib_path | ||
268 | .clone() | ||
269 | .map(|it| proc_macro_client.by_dylib_path(&it)); | ||
270 | |||
271 | let target = krate.target.as_deref().or(target); | ||
272 | let target_cfgs = cfg_cache | ||
273 | .entry(target) | ||
274 | .or_insert_with(|| get_rustc_cfg_options(target)); | ||
275 | |||
276 | let mut cfg_options = CfgOptions::default(); | ||
277 | cfg_options.extend(target_cfgs.iter().chain(krate.cfg.iter()).cloned()); | ||
278 | |||
279 | // FIXME: No crate name in json definition such that we cannot add OUT_DIR to env | ||
280 | Some(( | ||
281 | CrateId(seq_index as u32), | ||
282 | crate_graph.add_crate_root( | ||
283 | file_id, | ||
284 | krate.edition, | ||
285 | // FIXME json definitions can store the crate name | ||
286 | None, | ||
287 | cfg_options, | ||
288 | env, | ||
289 | proc_macro.unwrap_or_default(), | ||
290 | ), | ||
291 | )) | ||
292 | }) | ||
293 | .collect(); | ||
294 | |||
295 | for (id, krate) in project.crates.iter().enumerate() { | ||
296 | for dep in &krate.deps { | ||
297 | let from_crate_id = CrateId(id as u32); | ||
298 | let to_crate_id = dep.crate_id; | ||
299 | if let (Some(&from), Some(&to)) = | ||
300 | (crates.get(&from_crate_id), crates.get(&to_crate_id)) | ||
301 | { | ||
302 | if crate_graph.add_dep(from, dep.name.clone(), to).is_err() { | ||
303 | log::error!( | ||
304 | "cyclic dependency {:?} -> {:?}", | ||
305 | from_crate_id, | ||
306 | to_crate_id | ||
307 | ); | ||
308 | } | ||
309 | } | ||
310 | } | ||
311 | } | ||
312 | } | ||
313 | ProjectWorkspace::Cargo { cargo, sysroot } => { | ||
314 | let mut cfg_options = CfgOptions::default(); | ||
315 | cfg_options.extend(get_rustc_cfg_options(target)); | ||
316 | |||
317 | let sysroot_crates: FxHashMap<_, _> = sysroot | ||
318 | .crates() | ||
319 | .filter_map(|krate| { | ||
320 | let file_id = load(&sysroot[krate].root)?; | ||
321 | |||
322 | let env = Env::default(); | ||
323 | let proc_macro = vec![]; | ||
324 | let name = sysroot[krate].name.clone(); | ||
325 | let crate_id = crate_graph.add_crate_root( | ||
326 | file_id, | ||
327 | Edition::Edition2018, | ||
328 | Some(name), | ||
329 | cfg_options.clone(), | ||
330 | env, | ||
331 | proc_macro, | ||
332 | ); | ||
333 | Some((krate, crate_id)) | ||
334 | }) | ||
335 | .collect(); | ||
336 | |||
337 | for from in sysroot.crates() { | ||
338 | for &to in sysroot[from].deps.iter() { | ||
339 | let name = &sysroot[to].name; | ||
340 | if let (Some(&from), Some(&to)) = | ||
341 | (sysroot_crates.get(&from), sysroot_crates.get(&to)) | ||
342 | { | ||
343 | if crate_graph.add_dep(from, CrateName::new(name).unwrap(), to).is_err() | ||
344 | { | ||
345 | log::error!("cyclic dependency between sysroot crates") | ||
346 | } | ||
347 | } | ||
348 | } | ||
349 | } | ||
350 | |||
351 | let libcore = sysroot.core().and_then(|it| sysroot_crates.get(&it).copied()); | ||
352 | let liballoc = sysroot.alloc().and_then(|it| sysroot_crates.get(&it).copied()); | ||
353 | let libstd = sysroot.std().and_then(|it| sysroot_crates.get(&it).copied()); | ||
354 | let libproc_macro = | ||
355 | sysroot.proc_macro().and_then(|it| sysroot_crates.get(&it).copied()); | ||
356 | |||
357 | let mut pkg_to_lib_crate = FxHashMap::default(); | ||
358 | let mut pkg_crates = FxHashMap::default(); | ||
359 | |||
360 | // Add test cfg for non-sysroot crates | ||
361 | cfg_options.insert_atom("test".into()); | ||
362 | cfg_options.insert_atom("debug_assertions".into()); | ||
363 | |||
364 | // Next, create crates for each package, target pair | ||
365 | for pkg in cargo.packages() { | ||
366 | let mut lib_tgt = None; | ||
367 | for &tgt in cargo[pkg].targets.iter() { | ||
368 | let root = cargo[tgt].root.as_path(); | ||
369 | if let Some(file_id) = load(root) { | ||
370 | let edition = cargo[pkg].edition; | ||
371 | let cfg_options = { | ||
372 | let mut opts = cfg_options.clone(); | ||
373 | for feature in cargo[pkg].features.iter() { | ||
374 | opts.insert_key_value("feature".into(), feature.into()); | ||
375 | } | ||
376 | opts.extend(cargo[pkg].cfgs.iter().cloned()); | ||
377 | opts | ||
378 | }; | ||
379 | let mut env = Env::default(); | ||
380 | if let Some(out_dir) = &cargo[pkg].out_dir { | ||
381 | // NOTE: cargo and rustc seem to hide non-UTF-8 strings from env! and option_env!() | ||
382 | if let Some(out_dir) = out_dir.to_str().map(|s| s.to_owned()) { | ||
383 | env.set("OUT_DIR", out_dir); | ||
384 | } | ||
385 | } | ||
386 | let proc_macro = cargo[pkg] | ||
387 | .proc_macro_dylib_path | ||
388 | .as_ref() | ||
389 | .map(|it| proc_macro_client.by_dylib_path(&it)) | ||
390 | .unwrap_or_default(); | ||
391 | |||
392 | let crate_id = crate_graph.add_crate_root( | ||
393 | file_id, | ||
394 | edition, | ||
395 | Some(cargo[pkg].name.clone()), | ||
396 | cfg_options, | ||
397 | env, | ||
398 | proc_macro.clone(), | ||
399 | ); | ||
400 | if cargo[tgt].kind == TargetKind::Lib { | ||
401 | lib_tgt = Some((crate_id, cargo[tgt].name.clone())); | ||
402 | pkg_to_lib_crate.insert(pkg, crate_id); | ||
403 | } | ||
404 | if cargo[tgt].is_proc_macro { | ||
405 | if let Some(proc_macro) = libproc_macro { | ||
406 | if crate_graph | ||
407 | .add_dep( | ||
408 | crate_id, | ||
409 | CrateName::new("proc_macro").unwrap(), | ||
410 | proc_macro, | ||
411 | ) | ||
412 | .is_err() | ||
413 | { | ||
414 | log::error!( | ||
415 | "cyclic dependency on proc_macro for {}", | ||
416 | &cargo[pkg].name | ||
417 | ) | ||
418 | } | ||
419 | } | ||
420 | } | ||
421 | |||
422 | pkg_crates.entry(pkg).or_insert_with(Vec::new).push(crate_id); | ||
423 | } | ||
424 | } | ||
425 | |||
426 | // Set deps to the core, std and to the lib target of the current package | ||
427 | for &from in pkg_crates.get(&pkg).into_iter().flatten() { | ||
428 | if let Some((to, name)) = lib_tgt.clone() { | ||
429 | if to != from | ||
430 | && crate_graph | ||
431 | .add_dep( | ||
432 | from, | ||
433 | // For root projects with dashes in their name, | ||
434 | // cargo metadata does not do any normalization, | ||
435 | // so we do it ourselves currently | ||
436 | CrateName::normalize_dashes(&name), | ||
437 | to, | ||
438 | ) | ||
439 | .is_err() | ||
440 | { | ||
441 | { | ||
442 | log::error!( | ||
443 | "cyclic dependency between targets of {}", | ||
444 | &cargo[pkg].name | ||
445 | ) | ||
446 | } | ||
447 | } | ||
448 | } | ||
449 | // core is added as a dependency before std in order to | ||
450 | // mimic rustcs dependency order | ||
451 | if let Some(core) = libcore { | ||
452 | if crate_graph | ||
453 | .add_dep(from, CrateName::new("core").unwrap(), core) | ||
454 | .is_err() | ||
455 | { | ||
456 | log::error!("cyclic dependency on core for {}", &cargo[pkg].name) | ||
457 | } | ||
458 | } | ||
459 | if let Some(alloc) = liballoc { | ||
460 | if crate_graph | ||
461 | .add_dep(from, CrateName::new("alloc").unwrap(), alloc) | ||
462 | .is_err() | ||
463 | { | ||
464 | log::error!("cyclic dependency on alloc for {}", &cargo[pkg].name) | ||
465 | } | ||
466 | } | ||
467 | if let Some(std) = libstd { | ||
468 | if crate_graph | ||
469 | .add_dep(from, CrateName::new("std").unwrap(), std) | ||
470 | .is_err() | ||
471 | { | ||
472 | log::error!("cyclic dependency on std for {}", &cargo[pkg].name) | ||
473 | } | ||
474 | } | ||
475 | } | ||
476 | } | ||
477 | |||
478 | // Now add a dep edge from all targets of upstream to the lib | ||
479 | // target of downstream. | ||
480 | for pkg in cargo.packages() { | ||
481 | for dep in cargo[pkg].dependencies.iter() { | ||
482 | if let Some(&to) = pkg_to_lib_crate.get(&dep.pkg) { | ||
483 | for &from in pkg_crates.get(&pkg).into_iter().flatten() { | ||
484 | if crate_graph | ||
485 | .add_dep(from, CrateName::new(&dep.name).unwrap(), to) | ||
486 | .is_err() | ||
487 | { | ||
488 | log::error!( | ||
489 | "cyclic dependency {} -> {}", | ||
490 | &cargo[pkg].name, | ||
491 | &cargo[dep.pkg].name | ||
492 | ) | ||
493 | } | ||
494 | } | ||
495 | } | ||
496 | } | ||
497 | } | ||
498 | } | ||
499 | } | ||
500 | crate_graph | ||
501 | } | ||
502 | } | ||
503 | |||
504 | fn get_rustc_cfg_options(target: Option<&str>) -> Vec<CfgFlag> { | ||
505 | let mut res = Vec::new(); | ||
506 | |||
507 | // Some nightly-only cfgs, which are required for stdlib | ||
508 | res.push(CfgFlag::Atom("target_thread_local".into())); | ||
509 | for &ty in ["8", "16", "32", "64", "cas", "ptr"].iter() { | ||
510 | for &key in ["target_has_atomic", "target_has_atomic_load_store"].iter() { | ||
511 | res.push(CfgFlag::KeyValue { key: key.to_string(), value: ty.into() }); | ||
512 | } | ||
513 | } | ||
514 | |||
515 | let rustc_cfgs = { | ||
516 | let mut cmd = Command::new(toolchain::rustc()); | ||
517 | cmd.args(&["--print", "cfg", "-O"]); | ||
518 | if let Some(target) = target { | ||
519 | cmd.args(&["--target", target]); | ||
520 | } | ||
521 | utf8_stdout(cmd) | ||
522 | }; | ||
523 | |||
524 | match rustc_cfgs { | ||
525 | Ok(rustc_cfgs) => res.extend(rustc_cfgs.lines().map(|it| it.parse().unwrap())), | ||
526 | Err(e) => log::error!("failed to get rustc cfgs: {:#}", e), | ||
527 | } | ||
528 | |||
529 | res | ||
530 | } | ||
531 | |||
532 | fn utf8_stdout(mut cmd: Command) -> Result<String> { | ||
533 | let output = cmd.output().with_context(|| format!("{:?} failed", cmd))?; | ||
534 | if !output.status.success() { | ||
535 | match String::from_utf8(output.stderr) { | ||
536 | Ok(stderr) if !stderr.is_empty() => { | ||
537 | bail!("{:?} failed, {}\nstderr:\n{}", cmd, output.status, stderr) | ||
538 | } | ||
539 | _ => bail!("{:?} failed, {}", cmd, output.status), | ||
540 | } | ||
541 | } | ||
542 | let stdout = String::from_utf8(output.stdout)?; | ||
543 | Ok(stdout) | ||
544 | } | ||