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.
4 //! Pure model is represented by the [`base_db::CrateGraph`] from another crate.
6 //! In this crate, we are conserned with "real world" project models.
8 //! Specifically, here we have a representation for a Cargo project
9 //! ([`CargoWorkspace`]) and for manually specified layout ([`ProjectJson`]).
11 //! Roughly, the things we do here are:
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`]
27 fs::{read_dir, ReadDir},
32 use anyhow::{bail, Context, Result};
33 use paths::{AbsPath, AbsPathBuf};
34 use rustc_hash::FxHashSet;
37 build_data::{BuildDataCollector, BuildDataResult},
39 CargoConfig, CargoWorkspace, Package, PackageData, PackageDependency, RustcSource, Target,
40 TargetData, TargetKind,
42 project_json::{ProjectJson, ProjectJsonData},
44 workspace::{PackageRoot, ProjectWorkspace},
47 pub use proc_macro_api::ProcMacroClient;
49 #[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
50 pub enum ProjectManifest {
51 ProjectJson(AbsPathBuf),
52 CargoToml(AbsPathBuf),
53 DetachedFile(AbsPathBuf),
56 impl ProjectManifest {
57 pub fn from_manifest_file(path: AbsPathBuf) -> Result<ProjectManifest> {
58 if path.ends_with("rust-project.json") {
59 return Ok(ProjectManifest::ProjectJson(path));
61 if path.ends_with("Cargo.toml") {
62 return Ok(ProjectManifest::CargoToml(path));
64 bail!("project root must point to Cargo.toml or rust-project.json: {}", path.display())
67 pub fn discover_single(path: &AbsPath) -> Result<ProjectManifest> {
68 let mut candidates = ProjectManifest::discover(path)?;
69 let res = match candidates.pop() {
70 None => bail!("no projects"),
74 if !candidates.is_empty() {
75 bail!("more than one project")
80 pub fn discover(path: &AbsPath) -> io::Result<Vec<ProjectManifest>> {
81 if let Some(project_json) = find_in_parent_dirs(path, "rust-project.json") {
82 return Ok(vec![ProjectManifest::ProjectJson(project_json)]);
84 return find_cargo_toml(path)
85 .map(|paths| paths.into_iter().map(ProjectManifest::CargoToml).collect());
87 fn find_cargo_toml(path: &AbsPath) -> io::Result<Vec<AbsPathBuf>> {
88 match find_in_parent_dirs(path, "Cargo.toml") {
89 Some(it) => Ok(vec![it]),
90 None => Ok(find_cargo_toml_in_child_dir(read_dir(path)?)),
94 fn find_in_parent_dirs(path: &AbsPath, target_file_name: &str) -> Option<AbsPathBuf> {
95 if path.ends_with(target_file_name) {
96 return Some(path.to_path_buf());
99 let mut curr = Some(path);
101 while let Some(path) = curr {
102 let candidate = path.join(target_file_name);
103 if candidate.exists() {
104 return Some(candidate);
106 curr = path.parent();
112 fn find_cargo_toml_in_child_dir(entities: ReadDir) -> Vec<AbsPathBuf> {
113 // Only one level down to avoid cycles the easy way and stop a runaway scan with large projects
115 .filter_map(Result::ok)
116 .map(|it| it.path().join("Cargo.toml"))
117 .filter(|it| it.exists())
118 .map(AbsPathBuf::assert)
123 pub fn discover_all(paths: &[impl AsRef<AbsPath>]) -> Vec<ProjectManifest> {
126 .filter_map(|it| ProjectManifest::discover(it.as_ref()).ok())
128 .collect::<FxHashSet<_>>()
130 .collect::<Vec<_>>();
136 fn utf8_stdout(mut cmd: Command) -> Result<String> {
137 let output = cmd.output().with_context(|| format!("{:?} failed", cmd))?;
138 if !output.status.success() {
139 match String::from_utf8(output.stderr) {
140 Ok(stderr) if !stderr.is_empty() => {
141 bail!("{:?} failed, {}\nstderr:\n{}", cmd, output.status, stderr)
143 _ => bail!("{:?} failed, {}", cmd, output.status),
146 let stdout = String::from_utf8(output.stdout)?;
147 Ok(stdout.trim().to_string())