| //! In rust-analyzer, we maintain a strict separation between pure abstract |
| //! semantic project model and a concrete model of a particular build system. |
| //! |
| //! Pure model is represented by the [`base_db::CrateGraph`] from another crate. |
| //! |
| //! In this crate, we are concerned with "real world" project models. |
| //! |
| //! Specifically, here we have a representation for a Cargo project |
| //! ([`CargoWorkspace`]) and for manually specified layout ([`ProjectJson`]). |
| //! |
| //! Roughly, the things we do here are: |
| //! |
| //! * Project discovery (where's the relevant Cargo.toml for the current dir). |
| //! * Custom build steps (`build.rs` code generation and compilation of |
| //! procedural macros). |
| //! * Lowering of concrete model to a [`base_db::CrateGraph`] |
| |
| pub mod project_json; |
| pub mod toolchain_info { |
| pub mod rustc_cfg; |
| pub mod target_data; |
| pub mod target_tuple; |
| pub mod version; |
| |
| use std::path::Path; |
| |
| use crate::{ManifestPath, Sysroot, cargo_config_file::CargoConfigFile}; |
| |
| #[derive(Copy, Clone)] |
| pub enum QueryConfig<'a> { |
| /// Directly invoke `rustc` to query the desired information. |
| Rustc(&'a Sysroot, &'a Path), |
| /// Attempt to use cargo to query the desired information, honoring cargo configurations. |
| /// If this fails, falls back to invoking `rustc` directly. |
| Cargo(&'a Sysroot, &'a ManifestPath, &'a Option<CargoConfigFile>), |
| } |
| } |
| |
| mod build_dependencies; |
| mod cargo_config_file; |
| mod cargo_workspace; |
| mod env; |
| mod manifest_path; |
| mod sysroot; |
| mod workspace; |
| |
| #[cfg(test)] |
| mod tests; |
| |
| use std::{ |
| fmt, |
| fs::{self, ReadDir, read_dir}, |
| io, |
| process::Command, |
| }; |
| |
| use anyhow::{Context, bail, format_err}; |
| use paths::{AbsPath, AbsPathBuf, Utf8PathBuf}; |
| use rustc_hash::FxHashSet; |
| |
| pub use crate::{ |
| build_dependencies::{ProcMacroDylibPath, WorkspaceBuildScripts}, |
| cargo_workspace::{ |
| CargoConfig, CargoFeatures, CargoMetadataConfig, CargoWorkspace, Package, PackageData, |
| PackageDependency, RustLibSource, Target, TargetData, TargetKind, |
| }, |
| manifest_path::ManifestPath, |
| project_json::{ProjectJson, ProjectJsonData}, |
| sysroot::Sysroot, |
| workspace::{FileLoader, PackageRoot, ProjectWorkspace, ProjectWorkspaceKind}, |
| }; |
| pub use cargo_metadata::Metadata; |
| |
| #[derive(Debug, Clone, PartialEq, Eq)] |
| pub struct ProjectJsonFromCommand { |
| /// The data describing this project, such as its dependencies. |
| pub data: ProjectJsonData, |
| /// The build system specific file that describes this project, |
| /// such as a `my-project/BUCK` file. |
| pub buildfile: AbsPathBuf, |
| } |
| |
| #[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] |
| pub enum ProjectManifest { |
| ProjectJson(ManifestPath), |
| CargoToml(ManifestPath), |
| CargoScript(ManifestPath), |
| } |
| |
| impl ProjectManifest { |
| pub fn from_manifest_file(path: AbsPathBuf) -> anyhow::Result<ProjectManifest> { |
| let path = ManifestPath::try_from(path) |
| .map_err(|path| format_err!("bad manifest path: {path}"))?; |
| if path.file_name().unwrap_or_default() == "rust-project.json" { |
| return Ok(ProjectManifest::ProjectJson(path)); |
| } |
| if path.file_name().unwrap_or_default() == ".rust-project.json" { |
| return Ok(ProjectManifest::ProjectJson(path)); |
| } |
| if path.file_name().unwrap_or_default() == "Cargo.toml" { |
| return Ok(ProjectManifest::CargoToml(path)); |
| } |
| if path.extension().unwrap_or_default() == "rs" { |
| return Ok(ProjectManifest::CargoScript(path)); |
| } |
| bail!( |
| "project root must point to a Cargo.toml, rust-project.json or <script>.rs file: {path}" |
| ); |
| } |
| |
| pub fn discover_single(path: &AbsPath) -> anyhow::Result<ProjectManifest> { |
| let mut candidates = ProjectManifest::discover(path)?; |
| let res = match candidates.pop() { |
| None => bail!("no projects"), |
| Some(it) => it, |
| }; |
| |
| if !candidates.is_empty() { |
| bail!("more than one project"); |
| } |
| Ok(res) |
| } |
| |
| pub fn discover(path: &AbsPath) -> io::Result<Vec<ProjectManifest>> { |
| if let Some(project_json) = find_in_parent_dirs(path, "rust-project.json") { |
| return Ok(vec![ProjectManifest::ProjectJson(project_json)]); |
| } |
| if let Some(project_json) = find_in_parent_dirs(path, ".rust-project.json") { |
| return Ok(vec![ProjectManifest::ProjectJson(project_json)]); |
| } |
| return find_cargo_toml(path) |
| .map(|paths| paths.into_iter().map(ProjectManifest::CargoToml).collect()); |
| |
| fn find_cargo_toml(path: &AbsPath) -> io::Result<Vec<ManifestPath>> { |
| match find_in_parent_dirs(path, "Cargo.toml") { |
| Some(it) => Ok(vec![it]), |
| None => Ok(find_cargo_toml_in_child_dir(read_dir(path)?)), |
| } |
| } |
| |
| fn find_in_parent_dirs(path: &AbsPath, target_file_name: &str) -> Option<ManifestPath> { |
| if path.file_name().unwrap_or_default() == target_file_name |
| && let Ok(manifest) = ManifestPath::try_from(path.to_path_buf()) |
| { |
| return Some(manifest); |
| } |
| |
| let mut curr = Some(path); |
| |
| while let Some(path) = curr { |
| let candidate = path.join(target_file_name); |
| if fs::metadata(&candidate).is_ok() |
| && let Ok(manifest) = ManifestPath::try_from(candidate) |
| { |
| return Some(manifest); |
| } |
| |
| curr = path.parent(); |
| } |
| |
| None |
| } |
| |
| fn find_cargo_toml_in_child_dir(entities: ReadDir) -> Vec<ManifestPath> { |
| // Only one level down to avoid cycles the easy way and stop a runaway scan with large projects |
| entities |
| .filter_map(Result::ok) |
| .map(|it| it.path().join("Cargo.toml")) |
| .filter(|it| it.exists()) |
| .map(Utf8PathBuf::from_path_buf) |
| .filter_map(Result::ok) |
| .map(AbsPathBuf::try_from) |
| .filter_map(Result::ok) |
| .filter_map(|it| it.try_into().ok()) |
| .collect() |
| } |
| } |
| |
| pub fn discover_all(paths: &[AbsPathBuf]) -> Vec<ProjectManifest> { |
| let mut res = paths |
| .iter() |
| .filter_map(|it| ProjectManifest::discover(it.as_ref()).ok()) |
| .flatten() |
| .collect::<FxHashSet<_>>() |
| .into_iter() |
| .collect::<Vec<_>>(); |
| res.sort(); |
| res |
| } |
| |
| pub fn manifest_path(&self) -> &ManifestPath { |
| match self { |
| ProjectManifest::ProjectJson(it) |
| | ProjectManifest::CargoToml(it) |
| | ProjectManifest::CargoScript(it) => it, |
| } |
| } |
| } |
| |
| impl fmt::Display for ProjectManifest { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| fmt::Display::fmt(self.manifest_path(), f) |
| } |
| } |
| |
| fn utf8_stdout(cmd: &mut Command) -> anyhow::Result<String> { |
| let output = cmd.output().with_context(|| format!("{cmd:?} failed"))?; |
| if !output.status.success() { |
| match String::from_utf8(output.stderr) { |
| Ok(stderr) if !stderr.is_empty() => { |
| bail!("{:?} failed, {}\nstderr:\n{}", cmd, output.status, stderr) |
| } |
| _ => bail!("{:?} failed, {}", cmd, output.status), |
| } |
| } |
| let stdout = String::from_utf8(output.stdout)?; |
| Ok(stdout.trim().to_owned()) |
| } |
| |
| #[derive(Clone, Debug, Default, PartialEq, Eq)] |
| pub enum InvocationStrategy { |
| Once, |
| #[default] |
| PerWorkspace, |
| } |
| |
| /// A set of cfg-overrides per crate. |
| #[derive(Default, Debug, Clone, Eq, PartialEq)] |
| pub struct CfgOverrides { |
| /// A global set of overrides matching all crates. |
| pub global: cfg::CfgDiff, |
| /// A set of overrides matching specific crates. |
| pub selective: rustc_hash::FxHashMap<String, cfg::CfgDiff>, |
| } |
| |
| impl CfgOverrides { |
| pub fn len(&self) -> usize { |
| self.global.len() + self.selective.values().map(|it| it.len()).sum::<usize>() |
| } |
| |
| pub fn apply(&self, cfg_options: &mut cfg::CfgOptions, name: &str) { |
| if !self.global.is_empty() { |
| cfg_options.apply_diff(self.global.clone()); |
| }; |
| if let Some(diff) = self.selective.get(name) { |
| cfg_options.apply_diff(diff.clone()); |
| }; |
| } |
| } |
| |
| fn parse_cfg(s: &str) -> Result<cfg::CfgAtom, String> { |
| let res = match s.split_once('=') { |
| Some((key, value)) => { |
| if !(value.starts_with('"') && value.ends_with('"')) { |
| return Err(format!("Invalid cfg ({s:?}), value should be in quotes")); |
| } |
| let key = intern::Symbol::intern(key); |
| let value = intern::Symbol::intern(&value[1..value.len() - 1]); |
| cfg::CfgAtom::KeyValue { key, value } |
| } |
| None => cfg::CfgAtom::Flag(intern::Symbol::intern(s)), |
| }; |
| Ok(res) |
| } |
| |
| #[derive(Clone, Debug, PartialEq, Eq)] |
| pub enum RustSourceWorkspaceConfig { |
| CargoMetadata(CargoMetadataConfig), |
| Json(ProjectJson), |
| } |
| |
| impl Default for RustSourceWorkspaceConfig { |
| fn default() -> Self { |
| RustSourceWorkspaceConfig::default_cargo() |
| } |
| } |
| |
| impl RustSourceWorkspaceConfig { |
| pub fn default_cargo() -> Self { |
| RustSourceWorkspaceConfig::CargoMetadata(Default::default()) |
| } |
| } |