aboutsummaryrefslogtreecommitdiff
path: root/crates/project_model/src/cargo_workspace.rs
diff options
context:
space:
mode:
authorPavan Kumar Sunkara <[email protected]>2020-08-13 11:05:30 +0100
committerPavan Kumar Sunkara <[email protected]>2020-08-13 11:05:30 +0100
commiteac24d52e672c0a9c118e8969bf1b839c3e7f1f3 (patch)
tree4e1218cc53bef75f54df35be80c6a254b85b8d9c /crates/project_model/src/cargo_workspace.rs
parentb5cb16fb90b4a1076604c5795552ee4abe07a057 (diff)
Rename ra_project_model -> project_model
Diffstat (limited to 'crates/project_model/src/cargo_workspace.rs')
-rw-r--r--crates/project_model/src/cargo_workspace.rs362
1 files changed, 362 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}