]> git.proxmox.com Git - rustc.git/blob - src/tools/rust-analyzer/crates/project-model/src/lib.rs
901dcfd2b110260cf686105884044376d1c41169
[rustc.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 pub mod target_data_layout;
29
30 #[cfg(test)]
31 mod tests;
32
33 use std::{
34 fmt,
35 fs::{self, read_dir, ReadDir},
36 io,
37 process::Command,
38 };
39
40 use anyhow::{bail, format_err, Context};
41 use paths::{AbsPath, AbsPathBuf};
42 use rustc_hash::FxHashSet;
43
44 pub use crate::{
45 build_scripts::WorkspaceBuildScripts,
46 cargo_workspace::{
47 CargoConfig, CargoFeatures, CargoWorkspace, Package, PackageData, PackageDependency,
48 RustLibSource, Target, TargetData, TargetKind,
49 },
50 manifest_path::ManifestPath,
51 project_json::{ProjectJson, ProjectJsonData},
52 sysroot::Sysroot,
53 workspace::{CfgOverrides, PackageRoot, ProjectWorkspace},
54 };
55
56 #[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
57 pub enum ProjectManifest {
58 ProjectJson(ManifestPath),
59 CargoToml(ManifestPath),
60 }
61
62 impl ProjectManifest {
63 pub fn from_manifest_file(path: AbsPathBuf) -> anyhow::Result<ProjectManifest> {
64 let path = ManifestPath::try_from(path)
65 .map_err(|path| format_err!("bad manifest path: {path}"))?;
66 if path.file_name().unwrap_or_default() == "rust-project.json" {
67 return Ok(ProjectManifest::ProjectJson(path));
68 }
69 if path.file_name().unwrap_or_default() == "Cargo.toml" {
70 return Ok(ProjectManifest::CargoToml(path));
71 }
72 bail!("project root must point to Cargo.toml or rust-project.json: {path}");
73 }
74
75 pub fn discover_single(path: &AbsPath) -> anyhow::Result<ProjectManifest> {
76 let mut candidates = ProjectManifest::discover(path)?;
77 let res = match candidates.pop() {
78 None => bail!("no projects"),
79 Some(it) => it,
80 };
81
82 if !candidates.is_empty() {
83 bail!("more than one project");
84 }
85 Ok(res)
86 }
87
88 pub fn discover(path: &AbsPath) -> io::Result<Vec<ProjectManifest>> {
89 if let Some(project_json) = find_in_parent_dirs(path, "rust-project.json") {
90 return Ok(vec![ProjectManifest::ProjectJson(project_json)]);
91 }
92 return find_cargo_toml(path)
93 .map(|paths| paths.into_iter().map(ProjectManifest::CargoToml).collect());
94
95 fn find_cargo_toml(path: &AbsPath) -> io::Result<Vec<ManifestPath>> {
96 match find_in_parent_dirs(path, "Cargo.toml") {
97 Some(it) => Ok(vec![it]),
98 None => Ok(find_cargo_toml_in_child_dir(read_dir(path)?)),
99 }
100 }
101
102 fn find_in_parent_dirs(path: &AbsPath, target_file_name: &str) -> Option<ManifestPath> {
103 if path.file_name().unwrap_or_default() == target_file_name {
104 if let Ok(manifest) = ManifestPath::try_from(path.to_path_buf()) {
105 return Some(manifest);
106 }
107 }
108
109 let mut curr = Some(path);
110
111 while let Some(path) = curr {
112 let candidate = path.join(target_file_name);
113 if fs::metadata(&candidate).is_ok() {
114 if let Ok(manifest) = ManifestPath::try_from(candidate) {
115 return Some(manifest);
116 }
117 }
118 curr = path.parent();
119 }
120
121 None
122 }
123
124 fn find_cargo_toml_in_child_dir(entities: ReadDir) -> Vec<ManifestPath> {
125 // Only one level down to avoid cycles the easy way and stop a runaway scan with large projects
126 entities
127 .filter_map(Result::ok)
128 .map(|it| it.path().join("Cargo.toml"))
129 .filter(|it| it.exists())
130 .map(AbsPathBuf::assert)
131 .filter_map(|it| it.try_into().ok())
132 .collect()
133 }
134 }
135
136 pub fn discover_all(paths: &[AbsPathBuf]) -> Vec<ProjectManifest> {
137 let mut res = paths
138 .iter()
139 .filter_map(|it| ProjectManifest::discover(it.as_ref()).ok())
140 .flatten()
141 .collect::<FxHashSet<_>>()
142 .into_iter()
143 .collect::<Vec<_>>();
144 res.sort();
145 res
146 }
147 }
148
149 impl fmt::Display for ProjectManifest {
150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151 match self {
152 ProjectManifest::ProjectJson(it) | ProjectManifest::CargoToml(it) => {
153 fmt::Display::fmt(&it, f)
154 }
155 }
156 }
157 }
158
159 fn utf8_stdout(mut cmd: Command) -> anyhow::Result<String> {
160 let output = cmd.output().with_context(|| format!("{cmd:?} failed"))?;
161 if !output.status.success() {
162 match String::from_utf8(output.stderr) {
163 Ok(stderr) if !stderr.is_empty() => {
164 bail!("{:?} failed, {}\nstderr:\n{}", cmd, output.status, stderr)
165 }
166 _ => bail!("{:?} failed, {}", cmd, output.status),
167 }
168 }
169 let stdout = String::from_utf8(output.stdout)?;
170 Ok(stdout.trim().to_string())
171 }
172
173 #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
174 pub enum InvocationStrategy {
175 Once,
176 #[default]
177 PerWorkspace,
178 }
179
180 #[derive(Clone, Debug, Default, PartialEq, Eq)]
181 pub enum InvocationLocation {
182 Root(AbsPathBuf),
183 #[default]
184 Workspace,
185 }