mirror of
				https://github.com/rust-lang/rust-analyzer.git
				synced 2025-10-31 03:54:42 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			280 lines
		
	
	
	
		
			9.1 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			280 lines
		
	
	
	
		
			9.1 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| //! 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_layout;
 | |
|     pub mod target_tuple;
 | |
|     pub mod version;
 | |
| 
 | |
|     use std::path::Path;
 | |
| 
 | |
|     use crate::{ManifestPath, Sysroot};
 | |
| 
 | |
|     #[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),
 | |
|     }
 | |
| }
 | |
| 
 | |
| mod build_dependencies;
 | |
| 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::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 {
 | |
|                 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(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())
 | |
|     }
 | |
| }
 | 
