aboutsummaryrefslogtreecommitdiff
path: root/crates/project_model/src
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2020-08-13 11:10:03 +0100
committerGitHub <[email protected]>2020-08-13 11:10:03 +0100
commitde1d93455f85747410efb69c28e0c1379e8e328a (patch)
tree20eb4df06fc99236a30a98bba8e7ea3200b87042 /crates/project_model/src
parent8870a5ea138bb4ba048140d90d728721c4b4ad4b (diff)
parenteac24d52e672c0a9c118e8969bf1b839c3e7f1f3 (diff)
Merge #5744
5744: Rename ra_project_model -> project_model r=matklad a=pksunkara Co-authored-by: Pavan Kumar Sunkara <[email protected]>
Diffstat (limited to 'crates/project_model/src')
-rw-r--r--crates/project_model/src/cargo_workspace.rs362
-rw-r--r--crates/project_model/src/cfg_flag.rs51
-rw-r--r--crates/project_model/src/lib.rs544
-rw-r--r--crates/project_model/src/project_json.rs143
-rw-r--r--crates/project_model/src/sysroot.rs173
5 files changed, 1273 insertions, 0 deletions
diff --git a/crates/project_model/src/cargo_workspace.rs b/crates/project_model/src/cargo_workspace.rs
new file mode 100644
index 000000000..abf8dca96
--- /dev/null
+++ b/crates/project_model/src/cargo_workspace.rs
@@ -0,0 +1,362 @@
1//! FIXME: write short doc here
2
3use std::{
4 ffi::OsStr,
5 ops,
6 path::{Path, PathBuf},
7 process::Command,
8};
9
10use anyhow::{Context, Result};
11use arena::{Arena, Idx};
12use cargo_metadata::{BuildScript, CargoOpt, Message, MetadataCommand, PackageId};
13use paths::{AbsPath, AbsPathBuf};
14use ra_db::Edition;
15use rustc_hash::FxHashMap;
16
17use 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)]
30pub struct CargoWorkspace {
31 packages: Arena<PackageData>,
32 targets: Arena<TargetData>,
33 workspace_root: AbsPathBuf,
34}
35
36impl ops::Index<Package> for CargoWorkspace {
37 type Output = PackageData;
38 fn index(&self, index: Package) -> &PackageData {
39 &self.packages[index]
40 }
41}
42
43impl 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)]
51pub 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
69pub type Package = Idx<PackageData>;
70
71pub type Target = Idx<TargetData>;
72
73#[derive(Debug, Clone, Eq, PartialEq)]
74pub 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)]
89pub struct PackageDependency {
90 pub pkg: Package,
91 pub name: String,
92}
93
94#[derive(Debug, Clone, Eq, PartialEq)]
95pub 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)]
104pub 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
114impl 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
131impl PackageData {
132 pub fn root(&self) -> &AbsPath {
133 self.manifest.parent().unwrap()
134 }
135}
136
137impl 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)]
281pub struct ExternResources {
282 out_dirs: FxHashMap<PackageId, AbsPathBuf>,
283 proc_dylib_paths: FxHashMap<PackageId, AbsPathBuf>,
284 cfgs: FxHashMap<PackageId, Vec<CfgFlag>>,
285}
286
287pub 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
357fn 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"'
4use std::str::FromStr;
5
6use cfg::CfgOptions;
7use stdx::split_once;
8
9#[derive(Clone, Eq, PartialEq, Debug)]
10pub enum CfgFlag {
11 Atom(String),
12 KeyValue { key: String, value: String },
13}
14
15impl 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
33impl<'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
42impl 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..ee42198f3
--- /dev/null
+++ b/crates/project_model/src/lib.rs
@@ -0,0 +1,544 @@
1//! FIXME: write short doc here
2
3mod cargo_workspace;
4mod project_json;
5mod sysroot;
6mod cfg_flag;
7
8use std::{
9 fs::{self, read_dir, ReadDir},
10 io,
11 process::Command,
12};
13
14use anyhow::{bail, Context, Result};
15use cfg::CfgOptions;
16use paths::{AbsPath, AbsPathBuf};
17use ra_db::{CrateGraph, CrateId, CrateName, Edition, Env, FileId};
18use rustc_hash::{FxHashMap, FxHashSet};
19
20use crate::cfg_flag::CfgFlag;
21
22pub use crate::{
23 cargo_workspace::{CargoConfig, CargoWorkspace, Package, Target, TargetKind},
24 project_json::{ProjectJson, ProjectJsonData},
25 sysroot::Sysroot,
26};
27
28pub use ra_proc_macro::ProcMacroClient;
29
30#[derive(Debug, Clone, Eq, PartialEq)]
31pub 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)]
42pub 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)]
50pub enum ProjectManifest {
51 ProjectJson(AbsPathBuf),
52 CargoToml(AbsPathBuf),
53}
54
55impl 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
135impl 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
504fn 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
532fn 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..e3f3163f6
--- /dev/null
+++ b/crates/project_model/src/project_json.rs
@@ -0,0 +1,143 @@
1//! FIXME: write short doc here
2
3use std::path::PathBuf;
4
5use paths::{AbsPath, AbsPathBuf};
6use ra_db::{CrateId, CrateName, Dependency, Edition};
7use rustc_hash::FxHashMap;
8use serde::{de, Deserialize};
9
10use crate::cfg_flag::CfgFlag;
11
12/// Roots and crates that compose this Rust project.
13#[derive(Clone, Debug, Eq, PartialEq)]
14pub 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)]
21pub 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
34impl 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)]
85pub struct ProjectJsonData {
86 crates: Vec<CrateData>,
87}
88
89#[derive(Deserialize)]
90struct 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")]
106enum EditionData {
107 #[serde(rename = "2015")]
108 Edition2015,
109 #[serde(rename = "2018")]
110 Edition2018,
111}
112
113impl 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)]
123struct 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)]
132struct CrateSource {
133 include_dirs: Vec<PathBuf>,
134 exclude_dirs: Vec<PathBuf>,
135}
136
137fn deserialize_crate_name<'de, D>(de: D) -> Result<CrateName, D::Error>
138where
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
3use std::{convert::TryFrom, env, ops, path::Path, process::Command};
4
5use anyhow::{bail, format_err, Result};
6use arena::{Arena, Idx};
7use paths::{AbsPath, AbsPathBuf};
8
9use crate::utf8_stdout;
10
11#[derive(Default, Debug, Clone, Eq, PartialEq)]
12pub struct Sysroot {
13 crates: Arena<SysrootCrateData>,
14}
15
16pub type SysrootCrate = Idx<SysrootCrateData>;
17
18#[derive(Debug, Clone, Eq, PartialEq)]
19pub struct SysrootCrateData {
20 pub name: String,
21 pub root: AbsPathBuf,
22 pub deps: Vec<SysrootCrate>,
23}
24
25impl ops::Index<SysrootCrate> for Sysroot {
26 type Output = SysrootCrateData;
27 fn index(&self, index: SysrootCrate) -> &SysrootCrateData {
28 &self.crates[index]
29 }
30}
31
32impl 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
97fn 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
127fn 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
142impl SysrootCrateData {
143 pub fn root_dir(&self) -> &AbsPath {
144 self.root.parent().unwrap()
145 }
146}
147
148const SYSROOT_CRATES: &str = "
149alloc
150core
151panic_abort
152panic_unwind
153proc_macro
154profiler_builtins
155rtstartup
156std
157stdarch
158term
159test
160unwind";
161
162const STD_DEPS: &str = "
163alloc
164core
165panic_abort
166panic_unwind
167profiler_builtins
168rtstartup
169proc_macro
170stdarch
171term
172test
173unwind";