]> git.lizzy.rs Git - rust.git/blob - crates/project_model/src/lib.rs
Drag detached files towards loading
[rust.git] / crates / project_model / src / 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 conserned 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
18 mod cargo_workspace;
19 mod cfg_flag;
20 mod project_json;
21 mod sysroot;
22 mod workspace;
23 mod rustc_cfg;
24 mod build_data;
25
26 use std::{
27     fs::{read_dir, ReadDir},
28     io,
29     process::Command,
30 };
31
32 use anyhow::{bail, Context, Result};
33 use paths::{AbsPath, AbsPathBuf};
34 use rustc_hash::FxHashSet;
35
36 pub use crate::{
37     build_data::{BuildDataCollector, BuildDataResult},
38     cargo_workspace::{
39         CargoConfig, CargoWorkspace, Package, PackageData, PackageDependency, RustcSource, Target,
40         TargetData, TargetKind,
41     },
42     project_json::{ProjectJson, ProjectJsonData},
43     sysroot::Sysroot,
44     workspace::{PackageRoot, ProjectWorkspace},
45 };
46
47 pub use proc_macro_api::ProcMacroClient;
48
49 #[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
50 pub enum ProjectManifest {
51     ProjectJson(AbsPathBuf),
52     CargoToml(AbsPathBuf),
53 }
54
55 impl ProjectManifest {
56     pub fn from_manifest_file(path: AbsPathBuf) -> Result<ProjectManifest> {
57         if path.ends_with("rust-project.json") {
58             return Ok(ProjectManifest::ProjectJson(path));
59         }
60         if path.ends_with("Cargo.toml") {
61             return Ok(ProjectManifest::CargoToml(path));
62         }
63         bail!("project root must point to Cargo.toml or rust-project.json: {}", path.display())
64     }
65
66     pub fn discover_single(path: &AbsPath) -> Result<ProjectManifest> {
67         let mut candidates = ProjectManifest::discover(path)?;
68         let res = match candidates.pop() {
69             None => bail!("no projects"),
70             Some(it) => it,
71         };
72
73         if !candidates.is_empty() {
74             bail!("more than one project")
75         }
76         Ok(res)
77     }
78
79     pub fn discover(path: &AbsPath) -> io::Result<Vec<ProjectManifest>> {
80         if let Some(project_json) = find_in_parent_dirs(path, "rust-project.json") {
81             return Ok(vec![ProjectManifest::ProjectJson(project_json)]);
82         }
83         return find_cargo_toml(path)
84             .map(|paths| paths.into_iter().map(ProjectManifest::CargoToml).collect());
85
86         fn find_cargo_toml(path: &AbsPath) -> io::Result<Vec<AbsPathBuf>> {
87             match find_in_parent_dirs(path, "Cargo.toml") {
88                 Some(it) => Ok(vec![it]),
89                 None => Ok(find_cargo_toml_in_child_dir(read_dir(path)?)),
90             }
91         }
92
93         fn find_in_parent_dirs(path: &AbsPath, target_file_name: &str) -> Option<AbsPathBuf> {
94             if path.ends_with(target_file_name) {
95                 return Some(path.to_path_buf());
96             }
97
98             let mut curr = Some(path);
99
100             while let Some(path) = curr {
101                 let candidate = path.join(target_file_name);
102                 if candidate.exists() {
103                     return Some(candidate);
104                 }
105                 curr = path.parent();
106             }
107
108             None
109         }
110
111         fn find_cargo_toml_in_child_dir(entities: ReadDir) -> Vec<AbsPathBuf> {
112             // Only one level down to avoid cycles the easy way and stop a runaway scan with large projects
113             entities
114                 .filter_map(Result::ok)
115                 .map(|it| it.path().join("Cargo.toml"))
116                 .filter(|it| it.exists())
117                 .map(AbsPathBuf::assert)
118                 .collect()
119         }
120     }
121
122     pub fn discover_all(paths: &[impl AsRef<AbsPath>]) -> Vec<ProjectManifest> {
123         let mut res = paths
124             .iter()
125             .filter_map(|it| ProjectManifest::discover(it.as_ref()).ok())
126             .flatten()
127             .collect::<FxHashSet<_>>()
128             .into_iter()
129             .collect::<Vec<_>>();
130         res.sort();
131         res
132     }
133 }
134
135 fn utf8_stdout(mut cmd: Command) -> Result<String> {
136     let output = cmd.output().with_context(|| format!("{:?} failed", cmd))?;
137     if !output.status.success() {
138         match String::from_utf8(output.stderr) {
139             Ok(stderr) if !stderr.is_empty() => {
140                 bail!("{:?} failed, {}\nstderr:\n{}", cmd, output.status, stderr)
141             }
142             _ => bail!("{:?} failed, {}", cmd, output.status),
143         }
144     }
145     let stdout = String::from_utf8(output.stdout)?;
146     Ok(stdout.trim().to_string())
147 }