Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/project-model/src/lib.rs')
| -rw-r--r-- | crates/project-model/src/lib.rs | 157 |
1 files changed, 157 insertions, 0 deletions
diff --git a/crates/project-model/src/lib.rs b/crates/project-model/src/lib.rs new file mode 100644 index 0000000000..1caf6b59bc --- /dev/null +++ b/crates/project-model/src/lib.rs @@ -0,0 +1,157 @@ +//! 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 conserned 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`] + +mod manifest_path; +mod cargo_workspace; +mod cfg_flag; +mod project_json; +mod sysroot; +mod workspace; +mod rustc_cfg; +mod build_scripts; + +#[cfg(test)] +mod tests; + +use std::{ + fs::{self, read_dir, ReadDir}, + io, + process::Command, +}; + +use anyhow::{bail, format_err, Context, Result}; +use paths::{AbsPath, AbsPathBuf}; +use rustc_hash::FxHashSet; + +pub use crate::{ + build_scripts::WorkspaceBuildScripts, + cargo_workspace::{ + CargoConfig, CargoWorkspace, Package, PackageData, PackageDependency, RustcSource, Target, + TargetData, TargetKind, UnsetTestCrates, + }, + manifest_path::ManifestPath, + project_json::{ProjectJson, ProjectJsonData}, + sysroot::Sysroot, + workspace::{CfgOverrides, PackageRoot, ProjectWorkspace}, +}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub enum ProjectManifest { + ProjectJson(ManifestPath), + CargoToml(ManifestPath), +} + +impl ProjectManifest { + pub fn from_manifest_file(path: AbsPathBuf) -> Result<ProjectManifest> { + let path = ManifestPath::try_from(path) + .map_err(|path| format_err!("bad manifest path: {}", path.display()))?; + 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)); + } + bail!("project root must point to Cargo.toml or rust-project.json: {}", path.display()) + } + + pub fn discover_single(path: &AbsPath) -> 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)]); + } + 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 { + if 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() { + if 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(AbsPathBuf::assert) + .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 + } +} + +fn utf8_stdout(mut cmd: Command) -> Result<String> { + let output = cmd.output().with_context(|| format!("{:?} failed", cmd))?; + 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_string()) +} |