diff options
Diffstat (limited to 'crates/project_model')
-rw-r--r-- | crates/project_model/Cargo.toml | 25 | ||||
-rw-r--r-- | crates/project_model/src/cargo_workspace.rs | 362 | ||||
-rw-r--r-- | crates/project_model/src/cfg_flag.rs | 51 | ||||
-rw-r--r-- | crates/project_model/src/lib.rs | 544 | ||||
-rw-r--r-- | crates/project_model/src/project_json.rs | 143 | ||||
-rw-r--r-- | crates/project_model/src/sysroot.rs | 173 |
6 files changed, 1298 insertions, 0 deletions
diff --git a/crates/project_model/Cargo.toml b/crates/project_model/Cargo.toml new file mode 100644 index 000000000..386f72f41 --- /dev/null +++ b/crates/project_model/Cargo.toml | |||
@@ -0,0 +1,25 @@ | |||
1 | [package] | ||
2 | name = "project_model" | ||
3 | version = "0.0.0" | ||
4 | license = "MIT OR Apache-2.0" | ||
5 | authors = ["rust-analyzer developers"] | ||
6 | edition = "2018" | ||
7 | |||
8 | [lib] | ||
9 | doctest = false | ||
10 | |||
11 | [dependencies] | ||
12 | log = "0.4.8" | ||
13 | rustc-hash = "1.1.0" | ||
14 | cargo_metadata = "0.11.1" | ||
15 | serde = { version = "1.0.106", features = ["derive"] } | ||
16 | serde_json = "1.0.48" | ||
17 | anyhow = "1.0.26" | ||
18 | |||
19 | arena = { path = "../arena" } | ||
20 | cfg = { path = "../cfg" } | ||
21 | base_db = { path = "../base_db" } | ||
22 | toolchain = { path = "../toolchain" } | ||
23 | proc_macro_api = { path = "../proc_macro_api" } | ||
24 | paths = { path = "../paths" } | ||
25 | stdx = { path = "../stdx" } | ||
diff --git a/crates/project_model/src/cargo_workspace.rs b/crates/project_model/src/cargo_workspace.rs new file mode 100644 index 000000000..e5c2d2b25 --- /dev/null +++ b/crates/project_model/src/cargo_workspace.rs | |||
@@ -0,0 +1,362 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use std::{ | ||
4 | ffi::OsStr, | ||
5 | ops, | ||
6 | path::{Path, PathBuf}, | ||
7 | process::Command, | ||
8 | }; | ||
9 | |||
10 | use anyhow::{Context, Result}; | ||
11 | use arena::{Arena, Idx}; | ||
12 | use base_db::Edition; | ||
13 | use cargo_metadata::{BuildScript, CargoOpt, Message, MetadataCommand, PackageId}; | ||
14 | use paths::{AbsPath, AbsPathBuf}; | ||
15 | use rustc_hash::FxHashMap; | ||
16 | |||
17 | use crate::cfg_flag::CfgFlag; | ||
18 | |||
19 | /// `CargoWorkspace` represents the logical structure of, well, a Cargo | ||
20 | /// workspace. It pretty closely mirrors `cargo metadata` output. | ||
21 | /// | ||
22 | /// Note that internally, rust analyzer uses a different structure: | ||
23 | /// `CrateGraph`. `CrateGraph` is lower-level: it knows only about the crates, | ||
24 | /// while this knows about `Packages` & `Targets`: purely cargo-related | ||
25 | /// concepts. | ||
26 | /// | ||
27 | /// We use absolute paths here, `cargo metadata` guarantees to always produce | ||
28 | /// abs paths. | ||
29 | #[derive(Debug, Clone, Eq, PartialEq)] | ||
30 | pub struct CargoWorkspace { | ||
31 | packages: Arena<PackageData>, | ||
32 | targets: Arena<TargetData>, | ||
33 | workspace_root: AbsPathBuf, | ||
34 | } | ||
35 | |||
36 | impl ops::Index<Package> for CargoWorkspace { | ||
37 | type Output = PackageData; | ||
38 | fn index(&self, index: Package) -> &PackageData { | ||
39 | &self.packages[index] | ||
40 | } | ||
41 | } | ||
42 | |||
43 | impl ops::Index<Target> for CargoWorkspace { | ||
44 | type Output = TargetData; | ||
45 | fn index(&self, index: Target) -> &TargetData { | ||
46 | &self.targets[index] | ||
47 | } | ||
48 | } | ||
49 | |||
50 | #[derive(Default, Clone, Debug, PartialEq, Eq)] | ||
51 | pub struct CargoConfig { | ||
52 | /// Do not activate the `default` feature. | ||
53 | pub no_default_features: bool, | ||
54 | |||
55 | /// Activate all available features | ||
56 | pub all_features: bool, | ||
57 | |||
58 | /// List of features to activate. | ||
59 | /// This will be ignored if `cargo_all_features` is true. | ||
60 | pub features: Vec<String>, | ||
61 | |||
62 | /// Runs cargo check on launch to figure out the correct values of OUT_DIR | ||
63 | pub load_out_dirs_from_check: bool, | ||
64 | |||
65 | /// rustc target | ||
66 | pub target: Option<String>, | ||
67 | } | ||
68 | |||
69 | pub type Package = Idx<PackageData>; | ||
70 | |||
71 | pub type Target = Idx<TargetData>; | ||
72 | |||
73 | #[derive(Debug, Clone, Eq, PartialEq)] | ||
74 | pub struct PackageData { | ||
75 | pub version: String, | ||
76 | pub name: String, | ||
77 | pub manifest: AbsPathBuf, | ||
78 | pub targets: Vec<Target>, | ||
79 | pub is_member: bool, | ||
80 | pub dependencies: Vec<PackageDependency>, | ||
81 | pub edition: Edition, | ||
82 | pub features: Vec<String>, | ||
83 | pub cfgs: Vec<CfgFlag>, | ||
84 | pub out_dir: Option<AbsPathBuf>, | ||
85 | pub proc_macro_dylib_path: Option<AbsPathBuf>, | ||
86 | } | ||
87 | |||
88 | #[derive(Debug, Clone, Eq, PartialEq)] | ||
89 | pub struct PackageDependency { | ||
90 | pub pkg: Package, | ||
91 | pub name: String, | ||
92 | } | ||
93 | |||
94 | #[derive(Debug, Clone, Eq, PartialEq)] | ||
95 | pub struct TargetData { | ||
96 | pub package: Package, | ||
97 | pub name: String, | ||
98 | pub root: AbsPathBuf, | ||
99 | pub kind: TargetKind, | ||
100 | pub is_proc_macro: bool, | ||
101 | } | ||
102 | |||
103 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
104 | pub enum TargetKind { | ||
105 | Bin, | ||
106 | /// Any kind of Cargo lib crate-type (dylib, rlib, proc-macro, ...). | ||
107 | Lib, | ||
108 | Example, | ||
109 | Test, | ||
110 | Bench, | ||
111 | Other, | ||
112 | } | ||
113 | |||
114 | impl TargetKind { | ||
115 | fn new(kinds: &[String]) -> TargetKind { | ||
116 | for kind in kinds { | ||
117 | return match kind.as_str() { | ||
118 | "bin" => TargetKind::Bin, | ||
119 | "test" => TargetKind::Test, | ||
120 | "bench" => TargetKind::Bench, | ||
121 | "example" => TargetKind::Example, | ||
122 | "proc-macro" => TargetKind::Lib, | ||
123 | _ if kind.contains("lib") => TargetKind::Lib, | ||
124 | _ => continue, | ||
125 | }; | ||
126 | } | ||
127 | TargetKind::Other | ||
128 | } | ||
129 | } | ||
130 | |||
131 | impl PackageData { | ||
132 | pub fn root(&self) -> &AbsPath { | ||
133 | self.manifest.parent().unwrap() | ||
134 | } | ||
135 | } | ||
136 | |||
137 | impl CargoWorkspace { | ||
138 | pub fn from_cargo_metadata( | ||
139 | cargo_toml: &AbsPath, | ||
140 | cargo_features: &CargoConfig, | ||
141 | ) -> Result<CargoWorkspace> { | ||
142 | let mut meta = MetadataCommand::new(); | ||
143 | meta.cargo_path(toolchain::cargo()); | ||
144 | meta.manifest_path(cargo_toml.to_path_buf()); | ||
145 | if cargo_features.all_features { | ||
146 | meta.features(CargoOpt::AllFeatures); | ||
147 | } else { | ||
148 | if cargo_features.no_default_features { | ||
149 | // FIXME: `NoDefaultFeatures` is mutual exclusive with `SomeFeatures` | ||
150 | // https://github.com/oli-obk/cargo_metadata/issues/79 | ||
151 | meta.features(CargoOpt::NoDefaultFeatures); | ||
152 | } | ||
153 | if !cargo_features.features.is_empty() { | ||
154 | meta.features(CargoOpt::SomeFeatures(cargo_features.features.clone())); | ||
155 | } | ||
156 | } | ||
157 | if let Some(parent) = cargo_toml.parent() { | ||
158 | meta.current_dir(parent.to_path_buf()); | ||
159 | } | ||
160 | if let Some(target) = cargo_features.target.as_ref() { | ||
161 | meta.other_options(vec![String::from("--filter-platform"), target.clone()]); | ||
162 | } | ||
163 | let mut meta = meta.exec().with_context(|| { | ||
164 | format!("Failed to run `cargo metadata --manifest-path {}`", cargo_toml.display()) | ||
165 | })?; | ||
166 | |||
167 | let mut out_dir_by_id = FxHashMap::default(); | ||
168 | let mut cfgs = FxHashMap::default(); | ||
169 | let mut proc_macro_dylib_paths = FxHashMap::default(); | ||
170 | if cargo_features.load_out_dirs_from_check { | ||
171 | let resources = load_extern_resources(cargo_toml, cargo_features)?; | ||
172 | out_dir_by_id = resources.out_dirs; | ||
173 | cfgs = resources.cfgs; | ||
174 | proc_macro_dylib_paths = resources.proc_dylib_paths; | ||
175 | } | ||
176 | |||
177 | let mut pkg_by_id = FxHashMap::default(); | ||
178 | let mut packages = Arena::default(); | ||
179 | let mut targets = Arena::default(); | ||
180 | |||
181 | let ws_members = &meta.workspace_members; | ||
182 | |||
183 | meta.packages.sort_by(|a, b| a.id.cmp(&b.id)); | ||
184 | for meta_pkg in meta.packages { | ||
185 | let cargo_metadata::Package { id, edition, name, manifest_path, version, .. } = | ||
186 | meta_pkg; | ||
187 | let is_member = ws_members.contains(&id); | ||
188 | let edition = edition | ||
189 | .parse::<Edition>() | ||
190 | .with_context(|| format!("Failed to parse edition {}", edition))?; | ||
191 | let pkg = packages.alloc(PackageData { | ||
192 | name, | ||
193 | version: version.to_string(), | ||
194 | manifest: AbsPathBuf::assert(manifest_path), | ||
195 | targets: Vec::new(), | ||
196 | is_member, | ||
197 | edition, | ||
198 | dependencies: Vec::new(), | ||
199 | features: Vec::new(), | ||
200 | cfgs: cfgs.get(&id).cloned().unwrap_or_default(), | ||
201 | out_dir: out_dir_by_id.get(&id).cloned(), | ||
202 | proc_macro_dylib_path: proc_macro_dylib_paths.get(&id).cloned(), | ||
203 | }); | ||
204 | let pkg_data = &mut packages[pkg]; | ||
205 | pkg_by_id.insert(id, pkg); | ||
206 | for meta_tgt in meta_pkg.targets { | ||
207 | let is_proc_macro = meta_tgt.kind.as_slice() == ["proc-macro"]; | ||
208 | let tgt = targets.alloc(TargetData { | ||
209 | package: pkg, | ||
210 | name: meta_tgt.name, | ||
211 | root: AbsPathBuf::assert(meta_tgt.src_path.clone()), | ||
212 | kind: TargetKind::new(meta_tgt.kind.as_slice()), | ||
213 | is_proc_macro, | ||
214 | }); | ||
215 | pkg_data.targets.push(tgt); | ||
216 | } | ||
217 | } | ||
218 | let resolve = meta.resolve.expect("metadata executed with deps"); | ||
219 | for mut node in resolve.nodes { | ||
220 | let source = match pkg_by_id.get(&node.id) { | ||
221 | Some(&src) => src, | ||
222 | // FIXME: replace this and a similar branch below with `.unwrap`, once | ||
223 | // https://github.com/rust-lang/cargo/issues/7841 | ||
224 | // is fixed and hits stable (around 1.43-is probably?). | ||
225 | None => { | ||
226 | log::error!("Node id do not match in cargo metadata, ignoring {}", node.id); | ||
227 | continue; | ||
228 | } | ||
229 | }; | ||
230 | node.deps.sort_by(|a, b| a.pkg.cmp(&b.pkg)); | ||
231 | for dep_node in node.deps { | ||
232 | let pkg = match pkg_by_id.get(&dep_node.pkg) { | ||
233 | Some(&pkg) => pkg, | ||
234 | None => { | ||
235 | log::error!( | ||
236 | "Dep node id do not match in cargo metadata, ignoring {}", | ||
237 | dep_node.pkg | ||
238 | ); | ||
239 | continue; | ||
240 | } | ||
241 | }; | ||
242 | let dep = PackageDependency { name: dep_node.name, pkg }; | ||
243 | packages[source].dependencies.push(dep); | ||
244 | } | ||
245 | packages[source].features.extend(node.features); | ||
246 | } | ||
247 | |||
248 | let workspace_root = AbsPathBuf::assert(meta.workspace_root); | ||
249 | Ok(CargoWorkspace { packages, targets, workspace_root: workspace_root }) | ||
250 | } | ||
251 | |||
252 | pub fn packages<'a>(&'a self) -> impl Iterator<Item = Package> + ExactSizeIterator + 'a { | ||
253 | self.packages.iter().map(|(id, _pkg)| id) | ||
254 | } | ||
255 | |||
256 | pub fn target_by_root(&self, root: &AbsPath) -> Option<Target> { | ||
257 | self.packages() | ||
258 | .filter_map(|pkg| self[pkg].targets.iter().find(|&&it| &self[it].root == root)) | ||
259 | .next() | ||
260 | .copied() | ||
261 | } | ||
262 | |||
263 | pub fn workspace_root(&self) -> &AbsPath { | ||
264 | &self.workspace_root | ||
265 | } | ||
266 | |||
267 | pub fn package_flag(&self, package: &PackageData) -> String { | ||
268 | if self.is_unique(&*package.name) { | ||
269 | package.name.clone() | ||
270 | } else { | ||
271 | format!("{}:{}", package.name, package.version) | ||
272 | } | ||
273 | } | ||
274 | |||
275 | fn is_unique(&self, name: &str) -> bool { | ||
276 | self.packages.iter().filter(|(_, v)| v.name == name).count() == 1 | ||
277 | } | ||
278 | } | ||
279 | |||
280 | #[derive(Debug, Clone, Default)] | ||
281 | pub struct ExternResources { | ||
282 | out_dirs: FxHashMap<PackageId, AbsPathBuf>, | ||
283 | proc_dylib_paths: FxHashMap<PackageId, AbsPathBuf>, | ||
284 | cfgs: FxHashMap<PackageId, Vec<CfgFlag>>, | ||
285 | } | ||
286 | |||
287 | pub fn load_extern_resources( | ||
288 | cargo_toml: &Path, | ||
289 | cargo_features: &CargoConfig, | ||
290 | ) -> Result<ExternResources> { | ||
291 | let mut cmd = Command::new(toolchain::cargo()); | ||
292 | cmd.args(&["check", "--message-format=json", "--manifest-path"]).arg(cargo_toml); | ||
293 | if cargo_features.all_features { | ||
294 | cmd.arg("--all-features"); | ||
295 | } else { | ||
296 | if cargo_features.no_default_features { | ||
297 | // FIXME: `NoDefaultFeatures` is mutual exclusive with `SomeFeatures` | ||
298 | // https://github.com/oli-obk/cargo_metadata/issues/79 | ||
299 | cmd.arg("--no-default-features"); | ||
300 | } | ||
301 | if !cargo_features.features.is_empty() { | ||
302 | cmd.arg("--features"); | ||
303 | cmd.arg(cargo_features.features.join(" ")); | ||
304 | } | ||
305 | } | ||
306 | |||
307 | let output = cmd.output()?; | ||
308 | |||
309 | let mut res = ExternResources::default(); | ||
310 | |||
311 | for message in cargo_metadata::Message::parse_stream(output.stdout.as_slice()) { | ||
312 | if let Ok(message) = message { | ||
313 | match message { | ||
314 | Message::BuildScriptExecuted(BuildScript { package_id, out_dir, cfgs, .. }) => { | ||
315 | let cfgs = { | ||
316 | let mut acc = Vec::new(); | ||
317 | for cfg in cfgs { | ||
318 | match cfg.parse::<CfgFlag>() { | ||
319 | Ok(it) => acc.push(it), | ||
320 | Err(err) => { | ||
321 | anyhow::bail!("invalid cfg from cargo-metadata: {}", err) | ||
322 | } | ||
323 | }; | ||
324 | } | ||
325 | acc | ||
326 | }; | ||
327 | // cargo_metadata crate returns default (empty) path for | ||
328 | // older cargos, which is not absolute, so work around that. | ||
329 | if out_dir != PathBuf::default() { | ||
330 | let out_dir = AbsPathBuf::assert(out_dir); | ||
331 | res.out_dirs.insert(package_id.clone(), out_dir); | ||
332 | res.cfgs.insert(package_id, cfgs); | ||
333 | } | ||
334 | } | ||
335 | Message::CompilerArtifact(message) => { | ||
336 | if message.target.kind.contains(&"proc-macro".to_string()) { | ||
337 | let package_id = message.package_id; | ||
338 | // Skip rmeta file | ||
339 | if let Some(filename) = message.filenames.iter().find(|name| is_dylib(name)) | ||
340 | { | ||
341 | let filename = AbsPathBuf::assert(filename.clone()); | ||
342 | res.proc_dylib_paths.insert(package_id, filename); | ||
343 | } | ||
344 | } | ||
345 | } | ||
346 | Message::CompilerMessage(_) => (), | ||
347 | Message::Unknown => (), | ||
348 | Message::BuildFinished(_) => {} | ||
349 | Message::TextLine(_) => {} | ||
350 | } | ||
351 | } | ||
352 | } | ||
353 | Ok(res) | ||
354 | } | ||
355 | |||
356 | // FIXME: File a better way to know if it is a dylib | ||
357 | fn is_dylib(path: &Path) -> bool { | ||
358 | match path.extension().and_then(OsStr::to_str).map(|it| it.to_string().to_lowercase()) { | ||
359 | None => false, | ||
360 | Some(ext) => matches!(ext.as_str(), "dll" | "dylib" | "so"), | ||
361 | } | ||
362 | } | ||
diff --git a/crates/project_model/src/cfg_flag.rs b/crates/project_model/src/cfg_flag.rs new file mode 100644 index 000000000..e92962cf6 --- /dev/null +++ b/crates/project_model/src/cfg_flag.rs | |||
@@ -0,0 +1,51 @@ | |||
1 | //! Parsing of CfgFlags as command line arguments, as in | ||
2 | //! | ||
3 | //! rustc main.rs --cfg foo --cfg 'feature="bar"' | ||
4 | use std::str::FromStr; | ||
5 | |||
6 | use cfg::CfgOptions; | ||
7 | use stdx::split_once; | ||
8 | |||
9 | #[derive(Clone, Eq, PartialEq, Debug)] | ||
10 | pub enum CfgFlag { | ||
11 | Atom(String), | ||
12 | KeyValue { key: String, value: String }, | ||
13 | } | ||
14 | |||
15 | impl FromStr for CfgFlag { | ||
16 | type Err = String; | ||
17 | fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
18 | let res = match split_once(s, '=') { | ||
19 | Some((key, value)) => { | ||
20 | if !(value.starts_with('"') && value.ends_with('"')) { | ||
21 | return Err(format!("Invalid cfg ({:?}), value should be in quotes", s)); | ||
22 | } | ||
23 | let key = key.to_string(); | ||
24 | let value = value[1..value.len() - 1].to_string(); | ||
25 | CfgFlag::KeyValue { key, value } | ||
26 | } | ||
27 | None => CfgFlag::Atom(s.into()), | ||
28 | }; | ||
29 | Ok(res) | ||
30 | } | ||
31 | } | ||
32 | |||
33 | impl<'de> serde::Deserialize<'de> for CfgFlag { | ||
34 | fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> | ||
35 | where | ||
36 | D: serde::Deserializer<'de>, | ||
37 | { | ||
38 | String::deserialize(deserializer)?.parse().map_err(serde::de::Error::custom) | ||
39 | } | ||
40 | } | ||
41 | |||
42 | impl Extend<CfgFlag> for CfgOptions { | ||
43 | fn extend<T: IntoIterator<Item = CfgFlag>>(&mut self, iter: T) { | ||
44 | for cfg_flag in iter { | ||
45 | match cfg_flag { | ||
46 | CfgFlag::Atom(it) => self.insert_atom(it.into()), | ||
47 | CfgFlag::KeyValue { key, value } => self.insert_key_value(key.into(), value.into()), | ||
48 | } | ||
49 | } | ||
50 | } | ||
51 | } | ||
diff --git a/crates/project_model/src/lib.rs b/crates/project_model/src/lib.rs new file mode 100644 index 000000000..1f5a94d7f --- /dev/null +++ b/crates/project_model/src/lib.rs | |||
@@ -0,0 +1,544 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | mod cargo_workspace; | ||
4 | mod project_json; | ||
5 | mod sysroot; | ||
6 | mod cfg_flag; | ||
7 | |||
8 | use std::{ | ||
9 | fs::{self, read_dir, ReadDir}, | ||
10 | io, | ||
11 | process::Command, | ||
12 | }; | ||
13 | |||
14 | use anyhow::{bail, Context, Result}; | ||
15 | use base_db::{CrateGraph, CrateId, CrateName, Edition, Env, FileId}; | ||
16 | use cfg::CfgOptions; | ||
17 | use paths::{AbsPath, AbsPathBuf}; | ||
18 | use rustc_hash::{FxHashMap, FxHashSet}; | ||
19 | |||
20 | use crate::cfg_flag::CfgFlag; | ||
21 | |||
22 | pub use crate::{ | ||
23 | cargo_workspace::{CargoConfig, CargoWorkspace, Package, Target, TargetKind}, | ||
24 | project_json::{ProjectJson, ProjectJsonData}, | ||
25 | sysroot::Sysroot, | ||
26 | }; | ||
27 | |||
28 | pub use proc_macro_api::ProcMacroClient; | ||
29 | |||
30 | #[derive(Debug, Clone, Eq, PartialEq)] | ||
31 | pub enum ProjectWorkspace { | ||
32 | /// Project workspace was discovered by running `cargo metadata` and `rustc --print sysroot`. | ||
33 | Cargo { cargo: CargoWorkspace, sysroot: Sysroot }, | ||
34 | /// Project workspace was manually specified using a `rust-project.json` file. | ||
35 | Json { project: ProjectJson }, | ||
36 | } | ||
37 | |||
38 | /// `PackageRoot` describes a package root folder. | ||
39 | /// Which may be an external dependency, or a member of | ||
40 | /// the current workspace. | ||
41 | #[derive(Debug, Clone, Eq, PartialEq, Hash)] | ||
42 | pub struct PackageRoot { | ||
43 | /// Is a member of the current workspace | ||
44 | pub is_member: bool, | ||
45 | pub include: Vec<AbsPathBuf>, | ||
46 | pub exclude: Vec<AbsPathBuf>, | ||
47 | } | ||
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 | impl ProjectWorkspace { | ||
136 | pub fn load( | ||
137 | manifest: ProjectManifest, | ||
138 | cargo_config: &CargoConfig, | ||
139 | with_sysroot: bool, | ||
140 | ) -> Result<ProjectWorkspace> { | ||
141 | let res = match manifest { | ||
142 | ProjectManifest::ProjectJson(project_json) => { | ||
143 | let file = fs::read_to_string(&project_json).with_context(|| { | ||
144 | format!("Failed to read json file {}", project_json.display()) | ||
145 | })?; | ||
146 | let data = serde_json::from_str(&file).with_context(|| { | ||
147 | format!("Failed to deserialize json file {}", project_json.display()) | ||
148 | })?; | ||
149 | let project_location = project_json.parent().unwrap().to_path_buf(); | ||
150 | let project = ProjectJson::new(&project_location, data); | ||
151 | ProjectWorkspace::Json { project } | ||
152 | } | ||
153 | ProjectManifest::CargoToml(cargo_toml) => { | ||
154 | let cargo = CargoWorkspace::from_cargo_metadata(&cargo_toml, cargo_config) | ||
155 | .with_context(|| { | ||
156 | format!( | ||
157 | "Failed to read Cargo metadata from Cargo.toml file {}", | ||
158 | cargo_toml.display() | ||
159 | ) | ||
160 | })?; | ||
161 | let sysroot = if with_sysroot { | ||
162 | Sysroot::discover(&cargo_toml).with_context(|| { | ||
163 | format!( | ||
164 | "Failed to find sysroot for Cargo.toml file {}. Is rust-src installed?", | ||
165 | cargo_toml.display() | ||
166 | ) | ||
167 | })? | ||
168 | } else { | ||
169 | Sysroot::default() | ||
170 | }; | ||
171 | ProjectWorkspace::Cargo { cargo, sysroot } | ||
172 | } | ||
173 | }; | ||
174 | |||
175 | Ok(res) | ||
176 | } | ||
177 | |||
178 | /// Returns the roots for the current `ProjectWorkspace` | ||
179 | /// The return type contains the path and whether or not | ||
180 | /// the root is a member of the current workspace | ||
181 | pub fn to_roots(&self) -> Vec<PackageRoot> { | ||
182 | match self { | ||
183 | ProjectWorkspace::Json { project } => project | ||
184 | .crates | ||
185 | .iter() | ||
186 | .map(|krate| PackageRoot { | ||
187 | is_member: krate.is_workspace_member, | ||
188 | include: krate.include.clone(), | ||
189 | exclude: krate.exclude.clone(), | ||
190 | }) | ||
191 | .collect::<FxHashSet<_>>() | ||
192 | .into_iter() | ||
193 | .collect::<Vec<_>>(), | ||
194 | ProjectWorkspace::Cargo { cargo, sysroot } => cargo | ||
195 | .packages() | ||
196 | .map(|pkg| { | ||
197 | let is_member = cargo[pkg].is_member; | ||
198 | let pkg_root = cargo[pkg].root().to_path_buf(); | ||
199 | |||
200 | let mut include = vec![pkg_root.clone()]; | ||
201 | include.extend(cargo[pkg].out_dir.clone()); | ||
202 | |||
203 | let mut exclude = vec![pkg_root.join(".git")]; | ||
204 | if is_member { | ||
205 | exclude.push(pkg_root.join("target")); | ||
206 | } else { | ||
207 | exclude.push(pkg_root.join("tests")); | ||
208 | exclude.push(pkg_root.join("examples")); | ||
209 | exclude.push(pkg_root.join("benches")); | ||
210 | } | ||
211 | PackageRoot { is_member, include, exclude } | ||
212 | }) | ||
213 | .chain(sysroot.crates().map(|krate| PackageRoot { | ||
214 | is_member: false, | ||
215 | include: vec![sysroot[krate].root_dir().to_path_buf()], | ||
216 | exclude: Vec::new(), | ||
217 | })) | ||
218 | .collect(), | ||
219 | } | ||
220 | } | ||
221 | |||
222 | pub fn proc_macro_dylib_paths(&self) -> Vec<AbsPathBuf> { | ||
223 | match self { | ||
224 | ProjectWorkspace::Json { project } => project | ||
225 | .crates | ||
226 | .iter() | ||
227 | .filter_map(|krate| krate.proc_macro_dylib_path.as_ref()) | ||
228 | .cloned() | ||
229 | .collect(), | ||
230 | ProjectWorkspace::Cargo { cargo, sysroot: _sysroot } => cargo | ||
231 | .packages() | ||
232 | .filter_map(|pkg| cargo[pkg].proc_macro_dylib_path.as_ref()) | ||
233 | .cloned() | ||
234 | .collect(), | ||
235 | } | ||
236 | } | ||
237 | |||
238 | pub fn n_packages(&self) -> usize { | ||
239 | match self { | ||
240 | ProjectWorkspace::Json { project, .. } => project.crates.len(), | ||
241 | ProjectWorkspace::Cargo { cargo, sysroot } => { | ||
242 | cargo.packages().len() + sysroot.crates().len() | ||
243 | } | ||
244 | } | ||
245 | } | ||
246 | |||
247 | pub fn to_crate_graph( | ||
248 | &self, | ||
249 | target: Option<&str>, | ||
250 | proc_macro_client: &ProcMacroClient, | ||
251 | load: &mut dyn FnMut(&AbsPath) -> Option<FileId>, | ||
252 | ) -> CrateGraph { | ||
253 | let mut crate_graph = CrateGraph::default(); | ||
254 | match self { | ||
255 | ProjectWorkspace::Json { project } => { | ||
256 | let mut cfg_cache: FxHashMap<Option<&str>, Vec<CfgFlag>> = FxHashMap::default(); | ||
257 | let crates: FxHashMap<_, _> = project | ||
258 | .crates | ||
259 | .iter() | ||
260 | .enumerate() | ||
261 | .filter_map(|(seq_index, krate)| { | ||
262 | let file_path = &krate.root_module; | ||
263 | let file_id = load(&file_path)?; | ||
264 | |||
265 | let env = krate.env.clone().into_iter().collect(); | ||
266 | let proc_macro = krate | ||
267 | .proc_macro_dylib_path | ||
268 | .clone() | ||
269 | .map(|it| proc_macro_client.by_dylib_path(&it)); | ||
270 | |||
271 | let target = krate.target.as_deref().or(target); | ||
272 | let target_cfgs = cfg_cache | ||
273 | .entry(target) | ||
274 | .or_insert_with(|| get_rustc_cfg_options(target)); | ||
275 | |||
276 | let mut cfg_options = CfgOptions::default(); | ||
277 | cfg_options.extend(target_cfgs.iter().chain(krate.cfg.iter()).cloned()); | ||
278 | |||
279 | // FIXME: No crate name in json definition such that we cannot add OUT_DIR to env | ||
280 | Some(( | ||
281 | CrateId(seq_index as u32), | ||
282 | crate_graph.add_crate_root( | ||
283 | file_id, | ||
284 | krate.edition, | ||
285 | // FIXME json definitions can store the crate name | ||
286 | None, | ||
287 | cfg_options, | ||
288 | env, | ||
289 | proc_macro.unwrap_or_default(), | ||
290 | ), | ||
291 | )) | ||
292 | }) | ||
293 | .collect(); | ||
294 | |||
295 | for (id, krate) in project.crates.iter().enumerate() { | ||
296 | for dep in &krate.deps { | ||
297 | let from_crate_id = CrateId(id as u32); | ||
298 | let to_crate_id = dep.crate_id; | ||
299 | if let (Some(&from), Some(&to)) = | ||
300 | (crates.get(&from_crate_id), crates.get(&to_crate_id)) | ||
301 | { | ||
302 | if crate_graph.add_dep(from, dep.name.clone(), to).is_err() { | ||
303 | log::error!( | ||
304 | "cyclic dependency {:?} -> {:?}", | ||
305 | from_crate_id, | ||
306 | to_crate_id | ||
307 | ); | ||
308 | } | ||
309 | } | ||
310 | } | ||
311 | } | ||
312 | } | ||
313 | ProjectWorkspace::Cargo { cargo, sysroot } => { | ||
314 | let mut cfg_options = CfgOptions::default(); | ||
315 | cfg_options.extend(get_rustc_cfg_options(target)); | ||
316 | |||
317 | let sysroot_crates: FxHashMap<_, _> = sysroot | ||
318 | .crates() | ||
319 | .filter_map(|krate| { | ||
320 | let file_id = load(&sysroot[krate].root)?; | ||
321 | |||
322 | let env = Env::default(); | ||
323 | let proc_macro = vec![]; | ||
324 | let name = sysroot[krate].name.clone(); | ||
325 | let crate_id = crate_graph.add_crate_root( | ||
326 | file_id, | ||
327 | Edition::Edition2018, | ||
328 | Some(name), | ||
329 | cfg_options.clone(), | ||
330 | env, | ||
331 | proc_macro, | ||
332 | ); | ||
333 | Some((krate, crate_id)) | ||
334 | }) | ||
335 | .collect(); | ||
336 | |||
337 | for from in sysroot.crates() { | ||
338 | for &to in sysroot[from].deps.iter() { | ||
339 | let name = &sysroot[to].name; | ||
340 | if let (Some(&from), Some(&to)) = | ||
341 | (sysroot_crates.get(&from), sysroot_crates.get(&to)) | ||
342 | { | ||
343 | if crate_graph.add_dep(from, CrateName::new(name).unwrap(), to).is_err() | ||
344 | { | ||
345 | log::error!("cyclic dependency between sysroot crates") | ||
346 | } | ||
347 | } | ||
348 | } | ||
349 | } | ||
350 | |||
351 | let libcore = sysroot.core().and_then(|it| sysroot_crates.get(&it).copied()); | ||
352 | let liballoc = sysroot.alloc().and_then(|it| sysroot_crates.get(&it).copied()); | ||
353 | let libstd = sysroot.std().and_then(|it| sysroot_crates.get(&it).copied()); | ||
354 | let libproc_macro = | ||
355 | sysroot.proc_macro().and_then(|it| sysroot_crates.get(&it).copied()); | ||
356 | |||
357 | let mut pkg_to_lib_crate = FxHashMap::default(); | ||
358 | let mut pkg_crates = FxHashMap::default(); | ||
359 | |||
360 | // Add test cfg for non-sysroot crates | ||
361 | cfg_options.insert_atom("test".into()); | ||
362 | cfg_options.insert_atom("debug_assertions".into()); | ||
363 | |||
364 | // Next, create crates for each package, target pair | ||
365 | for pkg in cargo.packages() { | ||
366 | let mut lib_tgt = None; | ||
367 | for &tgt in cargo[pkg].targets.iter() { | ||
368 | let root = cargo[tgt].root.as_path(); | ||
369 | if let Some(file_id) = load(root) { | ||
370 | let edition = cargo[pkg].edition; | ||
371 | let cfg_options = { | ||
372 | let mut opts = cfg_options.clone(); | ||
373 | for feature in cargo[pkg].features.iter() { | ||
374 | opts.insert_key_value("feature".into(), feature.into()); | ||
375 | } | ||
376 | opts.extend(cargo[pkg].cfgs.iter().cloned()); | ||
377 | opts | ||
378 | }; | ||
379 | let mut env = Env::default(); | ||
380 | if let Some(out_dir) = &cargo[pkg].out_dir { | ||
381 | // NOTE: cargo and rustc seem to hide non-UTF-8 strings from env! and option_env!() | ||
382 | if let Some(out_dir) = out_dir.to_str().map(|s| s.to_owned()) { | ||
383 | env.set("OUT_DIR", out_dir); | ||
384 | } | ||
385 | } | ||
386 | let proc_macro = cargo[pkg] | ||
387 | .proc_macro_dylib_path | ||
388 | .as_ref() | ||
389 | .map(|it| proc_macro_client.by_dylib_path(&it)) | ||
390 | .unwrap_or_default(); | ||
391 | |||
392 | let crate_id = crate_graph.add_crate_root( | ||
393 | file_id, | ||
394 | edition, | ||
395 | Some(cargo[pkg].name.clone()), | ||
396 | cfg_options, | ||
397 | env, | ||
398 | proc_macro.clone(), | ||
399 | ); | ||
400 | if cargo[tgt].kind == TargetKind::Lib { | ||
401 | lib_tgt = Some((crate_id, cargo[tgt].name.clone())); | ||
402 | pkg_to_lib_crate.insert(pkg, crate_id); | ||
403 | } | ||
404 | if cargo[tgt].is_proc_macro { | ||
405 | if let Some(proc_macro) = libproc_macro { | ||
406 | if crate_graph | ||
407 | .add_dep( | ||
408 | crate_id, | ||
409 | CrateName::new("proc_macro").unwrap(), | ||
410 | proc_macro, | ||
411 | ) | ||
412 | .is_err() | ||
413 | { | ||
414 | log::error!( | ||
415 | "cyclic dependency on proc_macro for {}", | ||
416 | &cargo[pkg].name | ||
417 | ) | ||
418 | } | ||
419 | } | ||
420 | } | ||
421 | |||
422 | pkg_crates.entry(pkg).or_insert_with(Vec::new).push(crate_id); | ||
423 | } | ||
424 | } | ||
425 | |||
426 | // Set deps to the core, std and to the lib target of the current package | ||
427 | for &from in pkg_crates.get(&pkg).into_iter().flatten() { | ||
428 | if let Some((to, name)) = lib_tgt.clone() { | ||
429 | if to != from | ||
430 | && crate_graph | ||
431 | .add_dep( | ||
432 | from, | ||
433 | // For root projects with dashes in their name, | ||
434 | // cargo metadata does not do any normalization, | ||
435 | // so we do it ourselves currently | ||
436 | CrateName::normalize_dashes(&name), | ||
437 | to, | ||
438 | ) | ||
439 | .is_err() | ||
440 | { | ||
441 | { | ||
442 | log::error!( | ||
443 | "cyclic dependency between targets of {}", | ||
444 | &cargo[pkg].name | ||
445 | ) | ||
446 | } | ||
447 | } | ||
448 | } | ||
449 | // core is added as a dependency before std in order to | ||
450 | // mimic rustcs dependency order | ||
451 | if let Some(core) = libcore { | ||
452 | if crate_graph | ||
453 | .add_dep(from, CrateName::new("core").unwrap(), core) | ||
454 | .is_err() | ||
455 | { | ||
456 | log::error!("cyclic dependency on core for {}", &cargo[pkg].name) | ||
457 | } | ||
458 | } | ||
459 | if let Some(alloc) = liballoc { | ||
460 | if crate_graph | ||
461 | .add_dep(from, CrateName::new("alloc").unwrap(), alloc) | ||
462 | .is_err() | ||
463 | { | ||
464 | log::error!("cyclic dependency on alloc for {}", &cargo[pkg].name) | ||
465 | } | ||
466 | } | ||
467 | if let Some(std) = libstd { | ||
468 | if crate_graph | ||
469 | .add_dep(from, CrateName::new("std").unwrap(), std) | ||
470 | .is_err() | ||
471 | { | ||
472 | log::error!("cyclic dependency on std for {}", &cargo[pkg].name) | ||
473 | } | ||
474 | } | ||
475 | } | ||
476 | } | ||
477 | |||
478 | // Now add a dep edge from all targets of upstream to the lib | ||
479 | // target of downstream. | ||
480 | for pkg in cargo.packages() { | ||
481 | for dep in cargo[pkg].dependencies.iter() { | ||
482 | if let Some(&to) = pkg_to_lib_crate.get(&dep.pkg) { | ||
483 | for &from in pkg_crates.get(&pkg).into_iter().flatten() { | ||
484 | if crate_graph | ||
485 | .add_dep(from, CrateName::new(&dep.name).unwrap(), to) | ||
486 | .is_err() | ||
487 | { | ||
488 | log::error!( | ||
489 | "cyclic dependency {} -> {}", | ||
490 | &cargo[pkg].name, | ||
491 | &cargo[dep.pkg].name | ||
492 | ) | ||
493 | } | ||
494 | } | ||
495 | } | ||
496 | } | ||
497 | } | ||
498 | } | ||
499 | } | ||
500 | crate_graph | ||
501 | } | ||
502 | } | ||
503 | |||
504 | fn get_rustc_cfg_options(target: Option<&str>) -> Vec<CfgFlag> { | ||
505 | let mut res = Vec::new(); | ||
506 | |||
507 | // Some nightly-only cfgs, which are required for stdlib | ||
508 | res.push(CfgFlag::Atom("target_thread_local".into())); | ||
509 | for &ty in ["8", "16", "32", "64", "cas", "ptr"].iter() { | ||
510 | for &key in ["target_has_atomic", "target_has_atomic_load_store"].iter() { | ||
511 | res.push(CfgFlag::KeyValue { key: key.to_string(), value: ty.into() }); | ||
512 | } | ||
513 | } | ||
514 | |||
515 | let rustc_cfgs = { | ||
516 | let mut cmd = Command::new(toolchain::rustc()); | ||
517 | cmd.args(&["--print", "cfg", "-O"]); | ||
518 | if let Some(target) = target { | ||
519 | cmd.args(&["--target", target]); | ||
520 | } | ||
521 | utf8_stdout(cmd) | ||
522 | }; | ||
523 | |||
524 | match rustc_cfgs { | ||
525 | Ok(rustc_cfgs) => res.extend(rustc_cfgs.lines().map(|it| it.parse().unwrap())), | ||
526 | Err(e) => log::error!("failed to get rustc cfgs: {:#}", e), | ||
527 | } | ||
528 | |||
529 | res | ||
530 | } | ||
531 | |||
532 | fn utf8_stdout(mut cmd: Command) -> Result<String> { | ||
533 | let output = cmd.output().with_context(|| format!("{:?} failed", cmd))?; | ||
534 | if !output.status.success() { | ||
535 | match String::from_utf8(output.stderr) { | ||
536 | Ok(stderr) if !stderr.is_empty() => { | ||
537 | bail!("{:?} failed, {}\nstderr:\n{}", cmd, output.status, stderr) | ||
538 | } | ||
539 | _ => bail!("{:?} failed, {}", cmd, output.status), | ||
540 | } | ||
541 | } | ||
542 | let stdout = String::from_utf8(output.stdout)?; | ||
543 | Ok(stdout) | ||
544 | } | ||
diff --git a/crates/project_model/src/project_json.rs b/crates/project_model/src/project_json.rs new file mode 100644 index 000000000..060ea5b7d --- /dev/null +++ b/crates/project_model/src/project_json.rs | |||
@@ -0,0 +1,143 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use std::path::PathBuf; | ||
4 | |||
5 | use base_db::{CrateId, CrateName, Dependency, Edition}; | ||
6 | use paths::{AbsPath, AbsPathBuf}; | ||
7 | use rustc_hash::FxHashMap; | ||
8 | use serde::{de, Deserialize}; | ||
9 | |||
10 | use crate::cfg_flag::CfgFlag; | ||
11 | |||
12 | /// Roots and crates that compose this Rust project. | ||
13 | #[derive(Clone, Debug, Eq, PartialEq)] | ||
14 | pub struct ProjectJson { | ||
15 | pub(crate) crates: Vec<Crate>, | ||
16 | } | ||
17 | |||
18 | /// A crate points to the root module of a crate and lists the dependencies of the crate. This is | ||
19 | /// useful in creating the crate graph. | ||
20 | #[derive(Clone, Debug, Eq, PartialEq)] | ||
21 | pub struct Crate { | ||
22 | pub(crate) root_module: AbsPathBuf, | ||
23 | pub(crate) edition: Edition, | ||
24 | pub(crate) deps: Vec<Dependency>, | ||
25 | pub(crate) cfg: Vec<CfgFlag>, | ||
26 | pub(crate) target: Option<String>, | ||
27 | pub(crate) env: FxHashMap<String, String>, | ||
28 | pub(crate) proc_macro_dylib_path: Option<AbsPathBuf>, | ||
29 | pub(crate) is_workspace_member: bool, | ||
30 | pub(crate) include: Vec<AbsPathBuf>, | ||
31 | pub(crate) exclude: Vec<AbsPathBuf>, | ||
32 | } | ||
33 | |||
34 | impl ProjectJson { | ||
35 | pub fn new(base: &AbsPath, data: ProjectJsonData) -> ProjectJson { | ||
36 | ProjectJson { | ||
37 | crates: data | ||
38 | .crates | ||
39 | .into_iter() | ||
40 | .map(|crate_data| { | ||
41 | let is_workspace_member = crate_data.is_workspace_member.unwrap_or_else(|| { | ||
42 | crate_data.root_module.is_relative() | ||
43 | && !crate_data.root_module.starts_with("..") | ||
44 | || crate_data.root_module.starts_with(base) | ||
45 | }); | ||
46 | let root_module = base.join(crate_data.root_module); | ||
47 | let (include, exclude) = match crate_data.source { | ||
48 | Some(src) => { | ||
49 | let absolutize = |dirs: Vec<PathBuf>| { | ||
50 | dirs.into_iter().map(|it| base.join(it)).collect::<Vec<_>>() | ||
51 | }; | ||
52 | (absolutize(src.include_dirs), absolutize(src.exclude_dirs)) | ||
53 | } | ||
54 | None => (vec![root_module.parent().unwrap().to_path_buf()], Vec::new()), | ||
55 | }; | ||
56 | |||
57 | Crate { | ||
58 | root_module, | ||
59 | edition: crate_data.edition.into(), | ||
60 | deps: crate_data | ||
61 | .deps | ||
62 | .into_iter() | ||
63 | .map(|dep_data| Dependency { | ||
64 | crate_id: CrateId(dep_data.krate as u32), | ||
65 | name: dep_data.name, | ||
66 | }) | ||
67 | .collect::<Vec<_>>(), | ||
68 | cfg: crate_data.cfg, | ||
69 | target: crate_data.target, | ||
70 | env: crate_data.env, | ||
71 | proc_macro_dylib_path: crate_data | ||
72 | .proc_macro_dylib_path | ||
73 | .map(|it| base.join(it)), | ||
74 | is_workspace_member, | ||
75 | include, | ||
76 | exclude, | ||
77 | } | ||
78 | }) | ||
79 | .collect::<Vec<_>>(), | ||
80 | } | ||
81 | } | ||
82 | } | ||
83 | |||
84 | #[derive(Deserialize)] | ||
85 | pub struct ProjectJsonData { | ||
86 | crates: Vec<CrateData>, | ||
87 | } | ||
88 | |||
89 | #[derive(Deserialize)] | ||
90 | struct CrateData { | ||
91 | root_module: PathBuf, | ||
92 | edition: EditionData, | ||
93 | deps: Vec<DepData>, | ||
94 | #[serde(default)] | ||
95 | cfg: Vec<CfgFlag>, | ||
96 | target: Option<String>, | ||
97 | #[serde(default)] | ||
98 | env: FxHashMap<String, String>, | ||
99 | proc_macro_dylib_path: Option<PathBuf>, | ||
100 | is_workspace_member: Option<bool>, | ||
101 | source: Option<CrateSource>, | ||
102 | } | ||
103 | |||
104 | #[derive(Deserialize)] | ||
105 | #[serde(rename = "edition")] | ||
106 | enum EditionData { | ||
107 | #[serde(rename = "2015")] | ||
108 | Edition2015, | ||
109 | #[serde(rename = "2018")] | ||
110 | Edition2018, | ||
111 | } | ||
112 | |||
113 | impl From<EditionData> for Edition { | ||
114 | fn from(data: EditionData) -> Self { | ||
115 | match data { | ||
116 | EditionData::Edition2015 => Edition::Edition2015, | ||
117 | EditionData::Edition2018 => Edition::Edition2018, | ||
118 | } | ||
119 | } | ||
120 | } | ||
121 | |||
122 | #[derive(Deserialize)] | ||
123 | struct DepData { | ||
124 | /// Identifies a crate by position in the crates array. | ||
125 | #[serde(rename = "crate")] | ||
126 | krate: usize, | ||
127 | #[serde(deserialize_with = "deserialize_crate_name")] | ||
128 | name: CrateName, | ||
129 | } | ||
130 | |||
131 | #[derive(Deserialize)] | ||
132 | struct CrateSource { | ||
133 | include_dirs: Vec<PathBuf>, | ||
134 | exclude_dirs: Vec<PathBuf>, | ||
135 | } | ||
136 | |||
137 | fn deserialize_crate_name<'de, D>(de: D) -> Result<CrateName, D::Error> | ||
138 | where | ||
139 | D: de::Deserializer<'de>, | ||
140 | { | ||
141 | let name = String::deserialize(de)?; | ||
142 | CrateName::new(&name).map_err(|err| de::Error::custom(format!("invalid crate name: {:?}", err))) | ||
143 | } | ||
diff --git a/crates/project_model/src/sysroot.rs b/crates/project_model/src/sysroot.rs new file mode 100644 index 000000000..8239797b6 --- /dev/null +++ b/crates/project_model/src/sysroot.rs | |||
@@ -0,0 +1,173 @@ | |||
1 | //! FIXME: write short doc here | ||
2 | |||
3 | use std::{convert::TryFrom, env, ops, path::Path, process::Command}; | ||
4 | |||
5 | use anyhow::{bail, format_err, Result}; | ||
6 | use arena::{Arena, Idx}; | ||
7 | use paths::{AbsPath, AbsPathBuf}; | ||
8 | |||
9 | use crate::utf8_stdout; | ||
10 | |||
11 | #[derive(Default, Debug, Clone, Eq, PartialEq)] | ||
12 | pub struct Sysroot { | ||
13 | crates: Arena<SysrootCrateData>, | ||
14 | } | ||
15 | |||
16 | pub type SysrootCrate = Idx<SysrootCrateData>; | ||
17 | |||
18 | #[derive(Debug, Clone, Eq, PartialEq)] | ||
19 | pub struct SysrootCrateData { | ||
20 | pub name: String, | ||
21 | pub root: AbsPathBuf, | ||
22 | pub deps: Vec<SysrootCrate>, | ||
23 | } | ||
24 | |||
25 | impl ops::Index<SysrootCrate> for Sysroot { | ||
26 | type Output = SysrootCrateData; | ||
27 | fn index(&self, index: SysrootCrate) -> &SysrootCrateData { | ||
28 | &self.crates[index] | ||
29 | } | ||
30 | } | ||
31 | |||
32 | impl Sysroot { | ||
33 | pub fn core(&self) -> Option<SysrootCrate> { | ||
34 | self.by_name("core") | ||
35 | } | ||
36 | |||
37 | pub fn alloc(&self) -> Option<SysrootCrate> { | ||
38 | self.by_name("alloc") | ||
39 | } | ||
40 | |||
41 | pub fn std(&self) -> Option<SysrootCrate> { | ||
42 | self.by_name("std") | ||
43 | } | ||
44 | |||
45 | pub fn proc_macro(&self) -> Option<SysrootCrate> { | ||
46 | self.by_name("proc_macro") | ||
47 | } | ||
48 | |||
49 | pub fn crates<'a>(&'a self) -> impl Iterator<Item = SysrootCrate> + ExactSizeIterator + 'a { | ||
50 | self.crates.iter().map(|(id, _data)| id) | ||
51 | } | ||
52 | |||
53 | pub fn discover(cargo_toml: &AbsPath) -> Result<Sysroot> { | ||
54 | let src = get_or_install_rust_src(cargo_toml)?; | ||
55 | let mut sysroot = Sysroot { crates: Arena::default() }; | ||
56 | for name in SYSROOT_CRATES.trim().lines() { | ||
57 | // FIXME: remove this path when 1.47 comes out | ||
58 | // https://github.com/rust-lang/rust/pull/73265 | ||
59 | let root = src.join(format!("lib{}", name)).join("lib.rs"); | ||
60 | if root.exists() { | ||
61 | sysroot.crates.alloc(SysrootCrateData { | ||
62 | name: name.into(), | ||
63 | root, | ||
64 | deps: Vec::new(), | ||
65 | }); | ||
66 | } else { | ||
67 | let root = src.join(name).join("src/lib.rs"); | ||
68 | if root.exists() { | ||
69 | sysroot.crates.alloc(SysrootCrateData { | ||
70 | name: name.into(), | ||
71 | root, | ||
72 | deps: Vec::new(), | ||
73 | }); | ||
74 | } | ||
75 | } | ||
76 | } | ||
77 | if let Some(std) = sysroot.std() { | ||
78 | for dep in STD_DEPS.trim().lines() { | ||
79 | if let Some(dep) = sysroot.by_name(dep) { | ||
80 | sysroot.crates[std].deps.push(dep) | ||
81 | } | ||
82 | } | ||
83 | } | ||
84 | if let Some(alloc) = sysroot.alloc() { | ||
85 | if let Some(core) = sysroot.core() { | ||
86 | sysroot.crates[alloc].deps.push(core); | ||
87 | } | ||
88 | } | ||
89 | Ok(sysroot) | ||
90 | } | ||
91 | |||
92 | fn by_name(&self, name: &str) -> Option<SysrootCrate> { | ||
93 | self.crates.iter().find(|(_id, data)| data.name == name).map(|(id, _data)| id) | ||
94 | } | ||
95 | } | ||
96 | |||
97 | fn get_or_install_rust_src(cargo_toml: &AbsPath) -> Result<AbsPathBuf> { | ||
98 | if let Ok(path) = env::var("RUST_SRC_PATH") { | ||
99 | let path = AbsPathBuf::try_from(path.as_str()) | ||
100 | .map_err(|path| format_err!("RUST_SRC_PATH must be absolute: {}", path.display()))?; | ||
101 | return Ok(path); | ||
102 | } | ||
103 | let current_dir = cargo_toml.parent().unwrap(); | ||
104 | let mut rustc = Command::new(toolchain::rustc()); | ||
105 | rustc.current_dir(current_dir).args(&["--print", "sysroot"]); | ||
106 | let stdout = utf8_stdout(rustc)?; | ||
107 | let sysroot_path = AbsPath::assert(Path::new(stdout.trim())); | ||
108 | let mut src = get_rust_src(sysroot_path); | ||
109 | if src.is_none() { | ||
110 | let mut rustup = Command::new(toolchain::rustup()); | ||
111 | rustup.current_dir(current_dir).args(&["component", "add", "rust-src"]); | ||
112 | utf8_stdout(rustup)?; | ||
113 | src = get_rust_src(sysroot_path); | ||
114 | } | ||
115 | match src { | ||
116 | Some(r) => Ok(r), | ||
117 | None => bail!( | ||
118 | "can't load standard library from sysroot\n\ | ||
119 | {}\n\ | ||
120 | (discovered via `rustc --print sysroot`)\n\ | ||
121 | try running `rustup component add rust-src` or set `RUST_SRC_PATH`", | ||
122 | sysroot_path.display(), | ||
123 | ), | ||
124 | } | ||
125 | } | ||
126 | |||
127 | fn get_rust_src(sysroot_path: &AbsPath) -> Option<AbsPathBuf> { | ||
128 | // try the new path first since the old one still exists | ||
129 | let mut src_path = sysroot_path.join("lib/rustlib/src/rust/library"); | ||
130 | if !src_path.exists() { | ||
131 | // FIXME: remove this path when 1.47 comes out | ||
132 | // https://github.com/rust-lang/rust/pull/73265 | ||
133 | src_path = sysroot_path.join("lib/rustlib/src/rust/src"); | ||
134 | } | ||
135 | if src_path.exists() { | ||
136 | Some(src_path) | ||
137 | } else { | ||
138 | None | ||
139 | } | ||
140 | } | ||
141 | |||
142 | impl SysrootCrateData { | ||
143 | pub fn root_dir(&self) -> &AbsPath { | ||
144 | self.root.parent().unwrap() | ||
145 | } | ||
146 | } | ||
147 | |||
148 | const SYSROOT_CRATES: &str = " | ||
149 | alloc | ||
150 | core | ||
151 | panic_abort | ||
152 | panic_unwind | ||
153 | proc_macro | ||
154 | profiler_builtins | ||
155 | rtstartup | ||
156 | std | ||
157 | stdarch | ||
158 | term | ||
159 | test | ||
160 | unwind"; | ||
161 | |||
162 | const STD_DEPS: &str = " | ||
163 | alloc | ||
164 | core | ||
165 | panic_abort | ||
166 | panic_unwind | ||
167 | profiler_builtins | ||
168 | rtstartup | ||
169 | proc_macro | ||
170 | stdarch | ||
171 | term | ||
172 | test | ||
173 | unwind"; | ||