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