]> git.lizzy.rs Git - rust.git/blob - src/tools/rust-analyzer/crates/project-model/src/lib.rs
:arrow_up: rust-analyzer
[rust.git] / src / tools / rust-analyzer / crates / project-model / src / lib.rs
1 //! In rust-analyzer, we maintain a strict separation between pure abstract
2 //! semantic project model and a concrete model of a particular build system.
3 //!
4 //! Pure model is represented by the [`base_db::CrateGraph`] from another crate.
5 //!
6 //! In this crate, we are concerned with "real world" project models.
7 //!
8 //! Specifically, here we have a representation for a Cargo project
9 //! ([`CargoWorkspace`]) and for manually specified layout ([`ProjectJson`]).
10 //!
11 //! Roughly, the things we do here are:
12 //!
13 //! * Project discovery (where's the relevant Cargo.toml for the current dir).
14 //! * Custom build steps (`build.rs` code generation and compilation of
15 //!   procedural macros).
16 //! * Lowering of concrete model to a [`base_db::CrateGraph`]
17
18 #![warn(rust_2018_idioms, unused_lifetimes, semicolon_in_expressions_from_macros)]
19
20 mod manifest_path;
21 mod cargo_workspace;
22 mod cfg_flag;
23 mod project_json;
24 mod sysroot;
25 mod workspace;
26 mod rustc_cfg;
27 mod build_scripts;
28
29 #[cfg(test)]
30 mod tests;
31
32 use std::{
33     fs::{self, read_dir, ReadDir},
34     io,
35     process::Command,
36 };
37
38 use anyhow::{bail, format_err, Context, Result};
39 use paths::{AbsPath, AbsPathBuf};
40 use rustc_hash::FxHashSet;
41
42 pub use crate::{
43     build_scripts::WorkspaceBuildScripts,
44     cargo_workspace::{
45         CargoConfig, CargoWorkspace, Package, PackageData, PackageDependency, RustcSource, Target,
46         TargetData, TargetKind, UnsetTestCrates,
47     },
48     manifest_path::ManifestPath,
49     project_json::{ProjectJson, ProjectJsonData},
50     sysroot::Sysroot,
51     workspace::{CfgOverrides, PackageRoot, ProjectWorkspace},
52 };
53
54 #[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
55 pub enum ProjectManifest {
56     ProjectJson(ManifestPath),
57     CargoToml(ManifestPath),
58 }
59
60 impl ProjectManifest {
61     pub fn from_manifest_file(path: AbsPathBuf) -> Result<ProjectManifest> {
62         let path = ManifestPath::try_from(path)
63             .map_err(|path| format_err!("bad manifest path: {}", path.display()))?;
64         if path.file_name().unwrap_or_default() == "rust-project.json" {
65             return Ok(ProjectManifest::ProjectJson(path));
66         }
67         if path.file_name().unwrap_or_default() == "Cargo.toml" {
68             return Ok(ProjectManifest::CargoToml(path));
69         }
70         bail!("project root must point to Cargo.toml or rust-project.json: {}", path.display())
71     }
72
73     pub fn discover_single(path: &AbsPath) -> Result<ProjectManifest> {
74         let mut candidates = ProjectManifest::discover(path)?;
75         let res = match candidates.pop() {
76             None => bail!("no projects"),
77             Some(it) => it,
78         };
79
80         if !candidates.is_empty() {
81             bail!("more than one project")
82         }
83         Ok(res)
84     }
85
86     pub fn discover(path: &AbsPath) -> io::Result<Vec<ProjectManifest>> {
87         if let Some(project_json) = find_in_parent_dirs(path, "rust-project.json") {
88             return Ok(vec![ProjectManifest::ProjectJson(project_json)]);
89         }
90         return find_cargo_toml(path)
91             .map(|paths| paths.into_iter().map(ProjectManifest::CargoToml).collect());
92
93         fn find_cargo_toml(path: &AbsPath) -> io::Result<Vec<ManifestPath>> {
94             match find_in_parent_dirs(path, "Cargo.toml") {
95                 Some(it) => Ok(vec![it]),
96                 None => Ok(find_cargo_toml_in_child_dir(read_dir(path)?)),
97             }
98         }
99
100         fn find_in_parent_dirs(path: &AbsPath, target_file_name: &str) -> Option<ManifestPath> {
101             if path.file_name().unwrap_or_default() == target_file_name {
102                 if let Ok(manifest) = ManifestPath::try_from(path.to_path_buf()) {
103                     return Some(manifest);
104                 }
105             }
106
107             let mut curr = Some(path);
108
109             while let Some(path) = curr {
110                 let candidate = path.join(target_file_name);
111                 if fs::metadata(&candidate).is_ok() {
112                     if let Ok(manifest) = ManifestPath::try_from(candidate) {
113                         return Some(manifest);
114                     }
115                 }
116                 curr = path.parent();
117             }
118
119             None
120         }
121
122         fn find_cargo_toml_in_child_dir(entities: ReadDir) -> Vec<ManifestPath> {
123             // Only one level down to avoid cycles the easy way and stop a runaway scan with large projects
124             entities
125                 .filter_map(Result::ok)
126                 .map(|it| it.path().join("Cargo.toml"))
127                 .filter(|it| it.exists())
128                 .map(AbsPathBuf::assert)
129                 .filter_map(|it| it.try_into().ok())
130                 .collect()
131         }
132     }
133
134     pub fn discover_all(paths: &[AbsPathBuf]) -> Vec<ProjectManifest> {
135         let mut res = paths
136             .iter()
137             .filter_map(|it| ProjectManifest::discover(it.as_ref()).ok())
138             .flatten()
139             .collect::<FxHashSet<_>>()
140             .into_iter()
141             .collect::<Vec<_>>();
142         res.sort();
143         res
144     }
145 }
146
147 fn utf8_stdout(mut cmd: Command) -> Result<String> {
148     let output = cmd.output().with_context(|| format!("{:?} failed", cmd))?;
149     if !output.status.success() {
150         match String::from_utf8(output.stderr) {
151             Ok(stderr) if !stderr.is_empty() => {
152                 bail!("{:?} failed, {}\nstderr:\n{}", cmd, output.status, stderr)
153             }
154             _ => bail!("{:?} failed, {}", cmd, output.status),
155         }
156     }
157     let stdout = String::from_utf8(output.stdout)?;
158     Ok(stdout.trim().to_string())
159 }