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