ra_ap_project_model/
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
18pub mod project_json;
19pub mod toolchain_info {
20    pub mod rustc_cfg;
21    pub mod target_data_layout;
22    pub mod target_tuple;
23    pub mod version;
24
25    use std::path::Path;
26
27    use crate::{ManifestPath, Sysroot};
28
29    #[derive(Copy, Clone)]
30    pub enum QueryConfig<'a> {
31        /// Directly invoke `rustc` to query the desired information.
32        Rustc(&'a Sysroot, &'a Path),
33        /// Attempt to use cargo to query the desired information, honoring cargo configurations.
34        /// If this fails, falls back to invoking `rustc` directly.
35        Cargo(&'a Sysroot, &'a ManifestPath),
36    }
37}
38
39mod build_dependencies;
40mod cargo_workspace;
41mod env;
42mod manifest_path;
43mod sysroot;
44mod workspace;
45
46#[cfg(test)]
47mod tests;
48
49use std::{
50    fmt,
51    fs::{self, ReadDir, read_dir},
52    io,
53    process::Command,
54};
55
56use anyhow::{Context, bail, format_err};
57use paths::{AbsPath, AbsPathBuf, Utf8PathBuf};
58use rustc_hash::FxHashSet;
59
60pub use crate::{
61    build_dependencies::WorkspaceBuildScripts,
62    cargo_workspace::{
63        CargoConfig, CargoFeatures, CargoMetadataConfig, CargoWorkspace, Package, PackageData,
64        PackageDependency, RustLibSource, Target, TargetData, TargetKind,
65    },
66    manifest_path::ManifestPath,
67    project_json::{ProjectJson, ProjectJsonData},
68    sysroot::Sysroot,
69    workspace::{FileLoader, PackageRoot, ProjectWorkspace, ProjectWorkspaceKind},
70};
71pub use cargo_metadata::Metadata;
72
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct ProjectJsonFromCommand {
75    /// The data describing this project, such as its dependencies.
76    pub data: ProjectJsonData,
77    /// The build system specific file that describes this project,
78    /// such as a `my-project/BUCK` file.
79    pub buildfile: AbsPathBuf,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
83pub enum ProjectManifest {
84    ProjectJson(ManifestPath),
85    CargoToml(ManifestPath),
86    CargoScript(ManifestPath),
87}
88
89impl ProjectManifest {
90    pub fn from_manifest_file(path: AbsPathBuf) -> anyhow::Result<ProjectManifest> {
91        let path = ManifestPath::try_from(path)
92            .map_err(|path| format_err!("bad manifest path: {path}"))?;
93        if path.file_name().unwrap_or_default() == "rust-project.json" {
94            return Ok(ProjectManifest::ProjectJson(path));
95        }
96        if path.file_name().unwrap_or_default() == ".rust-project.json" {
97            return Ok(ProjectManifest::ProjectJson(path));
98        }
99        if path.file_name().unwrap_or_default() == "Cargo.toml" {
100            return Ok(ProjectManifest::CargoToml(path));
101        }
102        if path.extension().unwrap_or_default() == "rs" {
103            return Ok(ProjectManifest::CargoScript(path));
104        }
105        bail!(
106            "project root must point to a Cargo.toml, rust-project.json or <script>.rs file: {path}"
107        );
108    }
109
110    pub fn discover_single(path: &AbsPath) -> anyhow::Result<ProjectManifest> {
111        let mut candidates = ProjectManifest::discover(path)?;
112        let res = match candidates.pop() {
113            None => bail!("no projects"),
114            Some(it) => it,
115        };
116
117        if !candidates.is_empty() {
118            bail!("more than one project");
119        }
120        Ok(res)
121    }
122
123    pub fn discover(path: &AbsPath) -> io::Result<Vec<ProjectManifest>> {
124        if let Some(project_json) = find_in_parent_dirs(path, "rust-project.json") {
125            return Ok(vec![ProjectManifest::ProjectJson(project_json)]);
126        }
127        if let Some(project_json) = find_in_parent_dirs(path, ".rust-project.json") {
128            return Ok(vec![ProjectManifest::ProjectJson(project_json)]);
129        }
130        return find_cargo_toml(path)
131            .map(|paths| paths.into_iter().map(ProjectManifest::CargoToml).collect());
132
133        fn find_cargo_toml(path: &AbsPath) -> io::Result<Vec<ManifestPath>> {
134            match find_in_parent_dirs(path, "Cargo.toml") {
135                Some(it) => Ok(vec![it]),
136                None => Ok(find_cargo_toml_in_child_dir(read_dir(path)?)),
137            }
138        }
139
140        fn find_in_parent_dirs(path: &AbsPath, target_file_name: &str) -> Option<ManifestPath> {
141            if path.file_name().unwrap_or_default() == target_file_name {
142                if let Ok(manifest) = ManifestPath::try_from(path.to_path_buf()) {
143                    return Some(manifest);
144                }
145            }
146
147            let mut curr = Some(path);
148
149            while let Some(path) = curr {
150                let candidate = path.join(target_file_name);
151                if fs::metadata(&candidate).is_ok() {
152                    if let Ok(manifest) = ManifestPath::try_from(candidate) {
153                        return Some(manifest);
154                    }
155                }
156                curr = path.parent();
157            }
158
159            None
160        }
161
162        fn find_cargo_toml_in_child_dir(entities: ReadDir) -> Vec<ManifestPath> {
163            // Only one level down to avoid cycles the easy way and stop a runaway scan with large projects
164            entities
165                .filter_map(Result::ok)
166                .map(|it| it.path().join("Cargo.toml"))
167                .filter(|it| it.exists())
168                .map(Utf8PathBuf::from_path_buf)
169                .filter_map(Result::ok)
170                .map(AbsPathBuf::try_from)
171                .filter_map(Result::ok)
172                .filter_map(|it| it.try_into().ok())
173                .collect()
174        }
175    }
176
177    pub fn discover_all(paths: &[AbsPathBuf]) -> Vec<ProjectManifest> {
178        let mut res = paths
179            .iter()
180            .filter_map(|it| ProjectManifest::discover(it.as_ref()).ok())
181            .flatten()
182            .collect::<FxHashSet<_>>()
183            .into_iter()
184            .collect::<Vec<_>>();
185        res.sort();
186        res
187    }
188
189    pub fn manifest_path(&self) -> &ManifestPath {
190        match self {
191            ProjectManifest::ProjectJson(it)
192            | ProjectManifest::CargoToml(it)
193            | ProjectManifest::CargoScript(it) => it,
194        }
195    }
196}
197
198impl fmt::Display for ProjectManifest {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        fmt::Display::fmt(self.manifest_path(), f)
201    }
202}
203
204fn utf8_stdout(cmd: &mut Command) -> anyhow::Result<String> {
205    let output = cmd.output().with_context(|| format!("{cmd:?} failed"))?;
206    if !output.status.success() {
207        match String::from_utf8(output.stderr) {
208            Ok(stderr) if !stderr.is_empty() => {
209                bail!("{:?} failed, {}\nstderr:\n{}", cmd, output.status, stderr)
210            }
211            _ => bail!("{:?} failed, {}", cmd, output.status),
212        }
213    }
214    let stdout = String::from_utf8(output.stdout)?;
215    Ok(stdout.trim().to_owned())
216}
217
218#[derive(Clone, Debug, Default, PartialEq, Eq)]
219pub enum InvocationStrategy {
220    Once,
221    #[default]
222    PerWorkspace,
223}
224
225/// A set of cfg-overrides per crate.
226#[derive(Default, Debug, Clone, Eq, PartialEq)]
227pub struct CfgOverrides {
228    /// A global set of overrides matching all crates.
229    pub global: cfg::CfgDiff,
230    /// A set of overrides matching specific crates.
231    pub selective: rustc_hash::FxHashMap<String, cfg::CfgDiff>,
232}
233
234impl CfgOverrides {
235    pub fn len(&self) -> usize {
236        self.global.len() + self.selective.values().map(|it| it.len()).sum::<usize>()
237    }
238
239    pub fn apply(&self, cfg_options: &mut cfg::CfgOptions, name: &str) {
240        if !self.global.is_empty() {
241            cfg_options.apply_diff(self.global.clone());
242        };
243        if let Some(diff) = self.selective.get(name) {
244            cfg_options.apply_diff(diff.clone());
245        };
246    }
247}
248
249fn parse_cfg(s: &str) -> Result<cfg::CfgAtom, String> {
250    let res = match s.split_once('=') {
251        Some((key, value)) => {
252            if !(value.starts_with('"') && value.ends_with('"')) {
253                return Err(format!("Invalid cfg ({s:?}), value should be in quotes"));
254            }
255            let key = intern::Symbol::intern(key);
256            let value = intern::Symbol::intern(&value[1..value.len() - 1]);
257            cfg::CfgAtom::KeyValue { key, value }
258        }
259        None => cfg::CfgAtom::Flag(intern::Symbol::intern(s)),
260    };
261    Ok(res)
262}
263
264#[derive(Clone, Debug, PartialEq, Eq)]
265pub enum RustSourceWorkspaceConfig {
266    CargoMetadata(CargoMetadataConfig),
267    Json(ProjectJson),
268}
269
270impl Default for RustSourceWorkspaceConfig {
271    fn default() -> Self {
272        RustSourceWorkspaceConfig::default_cargo()
273    }
274}
275
276impl RustSourceWorkspaceConfig {
277    pub fn default_cargo() -> Self {
278        RustSourceWorkspaceConfig::CargoMetadata(Default::default())
279    }
280}