]> git.lizzy.rs Git - rust.git/blob - crates/project_model/src/cargo_workspace.rs
Merge #7264
[rust.git] / crates / project_model / src / cargo_workspace.rs
1 //! FIXME: write short doc here
2
3 use std::{
4     convert::TryInto,
5     ffi::OsStr,
6     io::BufReader,
7     ops,
8     path::{Path, PathBuf},
9     process::{Command, Stdio},
10 };
11
12 use anyhow::{Context, Result};
13 use arena::{Arena, Idx};
14 use base_db::Edition;
15 use cargo_metadata::{BuildScript, CargoOpt, Message, MetadataCommand, PackageId};
16 use itertools::Itertools;
17 use paths::{AbsPath, AbsPathBuf};
18 use rustc_hash::FxHashMap;
19 use stdx::JodChild;
20
21 use crate::cfg_flag::CfgFlag;
22 use crate::utf8_stdout;
23
24 /// `CargoWorkspace` represents the logical structure of, well, a Cargo
25 /// workspace. It pretty closely mirrors `cargo metadata` output.
26 ///
27 /// Note that internally, rust analyzer uses a different structure:
28 /// `CrateGraph`. `CrateGraph` is lower-level: it knows only about the crates,
29 /// while this knows about `Packages` & `Targets`: purely cargo-related
30 /// concepts.
31 ///
32 /// We use absolute paths here, `cargo metadata` guarantees to always produce
33 /// abs paths.
34 #[derive(Debug, Clone, Eq, PartialEq)]
35 pub struct CargoWorkspace {
36     packages: Arena<PackageData>,
37     targets: Arena<TargetData>,
38     workspace_root: AbsPathBuf,
39 }
40
41 impl ops::Index<Package> for CargoWorkspace {
42     type Output = PackageData;
43     fn index(&self, index: Package) -> &PackageData {
44         &self.packages[index]
45     }
46 }
47
48 impl ops::Index<Target> for CargoWorkspace {
49     type Output = TargetData;
50     fn index(&self, index: Target) -> &TargetData {
51         &self.targets[index]
52     }
53 }
54
55 #[derive(Default, Clone, Debug, PartialEq, Eq)]
56 pub struct CargoConfig {
57     /// Do not activate the `default` feature.
58     pub no_default_features: bool,
59
60     /// Activate all available features
61     pub all_features: bool,
62
63     /// List of features to activate.
64     /// This will be ignored if `cargo_all_features` is true.
65     pub features: Vec<String>,
66
67     /// Runs cargo check on launch to figure out the correct values of OUT_DIR
68     pub load_out_dirs_from_check: bool,
69
70     /// rustc target
71     pub target: Option<String>,
72
73     /// Don't load sysroot crates (`std`, `core` & friends). Might be useful
74     /// when debugging isolated issues.
75     pub no_sysroot: bool,
76
77     /// rustc private crate source
78     pub rustc_source: Option<AbsPathBuf>,
79 }
80
81 pub type Package = Idx<PackageData>;
82
83 pub type Target = Idx<TargetData>;
84
85 /// Information associated with a cargo crate
86 #[derive(Debug, Clone, Eq, PartialEq)]
87 pub struct PackageData {
88     /// Version given in the `Cargo.toml`
89     pub version: String,
90     /// Name as given in the `Cargo.toml`
91     pub name: String,
92     /// Path containing the `Cargo.toml`
93     pub manifest: AbsPathBuf,
94     /// Targets provided by the crate (lib, bin, example, test, ...)
95     pub targets: Vec<Target>,
96     /// Is this package a member of the current workspace
97     pub is_member: bool,
98     /// List of packages this package depends on
99     pub dependencies: Vec<PackageDependency>,
100     /// Rust edition for this package
101     pub edition: Edition,
102     /// List of features to activate
103     pub features: Vec<String>,
104     /// List of config flags defined by this package's build script
105     pub cfgs: Vec<CfgFlag>,
106     /// List of cargo-related environment variables with their value
107     ///
108     /// If the package has a build script which defines environment variables,
109     /// they can also be found here.
110     pub envs: Vec<(String, String)>,
111     /// Directory where a build script might place its output
112     pub out_dir: Option<AbsPathBuf>,
113     /// Path to the proc-macro library file if this package exposes proc-macros
114     pub proc_macro_dylib_path: Option<AbsPathBuf>,
115 }
116
117 #[derive(Debug, Clone, Eq, PartialEq)]
118 pub struct PackageDependency {
119     pub pkg: Package,
120     pub name: String,
121 }
122
123 /// Information associated with a package's target
124 #[derive(Debug, Clone, Eq, PartialEq)]
125 pub struct TargetData {
126     /// Package that provided this target
127     pub package: Package,
128     /// Name as given in the `Cargo.toml` or generated from the file name
129     pub name: String,
130     /// Path to the main source file of the target
131     pub root: AbsPathBuf,
132     /// Kind of target
133     pub kind: TargetKind,
134     /// Is this target a proc-macro
135     pub is_proc_macro: bool,
136 }
137
138 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
139 pub enum TargetKind {
140     Bin,
141     /// Any kind of Cargo lib crate-type (dylib, rlib, proc-macro, ...).
142     Lib,
143     Example,
144     Test,
145     Bench,
146     Other,
147 }
148
149 impl TargetKind {
150     fn new(kinds: &[String]) -> TargetKind {
151         for kind in kinds {
152             return match kind.as_str() {
153                 "bin" => TargetKind::Bin,
154                 "test" => TargetKind::Test,
155                 "bench" => TargetKind::Bench,
156                 "example" => TargetKind::Example,
157                 "proc-macro" => TargetKind::Lib,
158                 _ if kind.contains("lib") => TargetKind::Lib,
159                 _ => continue,
160             };
161         }
162         TargetKind::Other
163     }
164 }
165
166 impl PackageData {
167     pub fn root(&self) -> &AbsPath {
168         self.manifest.parent().unwrap()
169     }
170 }
171
172 impl CargoWorkspace {
173     pub fn from_cargo_metadata(
174         cargo_toml: &AbsPath,
175         config: &CargoConfig,
176         progress: &dyn Fn(String),
177     ) -> Result<CargoWorkspace> {
178         let mut meta = MetadataCommand::new();
179         meta.cargo_path(toolchain::cargo());
180         meta.manifest_path(cargo_toml.to_path_buf());
181         if config.all_features {
182             meta.features(CargoOpt::AllFeatures);
183         } else {
184             if config.no_default_features {
185                 // FIXME: `NoDefaultFeatures` is mutual exclusive with `SomeFeatures`
186                 // https://github.com/oli-obk/cargo_metadata/issues/79
187                 meta.features(CargoOpt::NoDefaultFeatures);
188             }
189             if !config.features.is_empty() {
190                 meta.features(CargoOpt::SomeFeatures(config.features.clone()));
191             }
192         }
193         if let Some(parent) = cargo_toml.parent() {
194             meta.current_dir(parent.to_path_buf());
195         }
196         let target = if let Some(target) = config.target.as_ref() {
197             Some(target.clone())
198         } else {
199             // cargo metadata defaults to giving information for _all_ targets.
200             // In the absence of a preference from the user, we use the host platform.
201             let mut rustc = Command::new(toolchain::rustc());
202             rustc.current_dir(cargo_toml.parent().unwrap()).arg("-vV");
203             log::debug!("Discovering host platform by {:?}", rustc);
204             match utf8_stdout(rustc) {
205                 Ok(stdout) => {
206                     let field = "host: ";
207                     let target = stdout.lines().find_map(|l| l.strip_prefix(field));
208                     if let Some(target) = target {
209                         Some(target.to_string())
210                     } else {
211                         // If we fail to resolve the host platform, it's not the end of the world.
212                         log::info!("rustc -vV did not report host platform, got:\n{}", stdout);
213                         None
214                     }
215                 }
216                 Err(e) => {
217                     log::warn!("Failed to discover host platform: {}", e);
218                     None
219                 }
220             }
221         };
222         if let Some(target) = target {
223             meta.other_options(vec![String::from("--filter-platform"), target]);
224         }
225
226         // FIXME: Currently MetadataCommand is not based on parse_stream,
227         // So we just report it as a whole
228         progress("metadata".to_string());
229         let mut meta = meta.exec().with_context(|| {
230             let cwd: Option<AbsPathBuf> =
231                 std::env::current_dir().ok().and_then(|p| p.try_into().ok());
232
233             let workdir = cargo_toml
234                 .parent()
235                 .map(|p| p.to_path_buf())
236                 .or(cwd)
237                 .map(|dir| dir.to_string_lossy().to_string())
238                 .unwrap_or_else(|| "<failed to get path>".into());
239
240             format!(
241                 "Failed to run `cargo metadata --manifest-path {}` in `{}`",
242                 cargo_toml.display(),
243                 workdir
244             )
245         })?;
246
247         let mut out_dir_by_id = FxHashMap::default();
248         let mut cfgs = FxHashMap::default();
249         let mut envs = FxHashMap::default();
250         let mut proc_macro_dylib_paths = FxHashMap::default();
251         if config.load_out_dirs_from_check {
252             let resources = load_extern_resources(cargo_toml, config, progress)?;
253             out_dir_by_id = resources.out_dirs;
254             cfgs = resources.cfgs;
255             envs = resources.env;
256             proc_macro_dylib_paths = resources.proc_dylib_paths;
257         }
258
259         let mut pkg_by_id = FxHashMap::default();
260         let mut packages = Arena::default();
261         let mut targets = Arena::default();
262
263         let ws_members = &meta.workspace_members;
264
265         meta.packages.sort_by(|a, b| a.id.cmp(&b.id));
266         for meta_pkg in meta.packages {
267             let id = meta_pkg.id.clone();
268             inject_cargo_env(&meta_pkg, envs.entry(id).or_default());
269
270             let cargo_metadata::Package { id, edition, name, manifest_path, version, .. } =
271                 meta_pkg;
272             let is_member = ws_members.contains(&id);
273             let edition = edition
274                 .parse::<Edition>()
275                 .with_context(|| format!("Failed to parse edition {}", edition))?;
276             let pkg = packages.alloc(PackageData {
277                 name,
278                 version: version.to_string(),
279                 manifest: AbsPathBuf::assert(manifest_path),
280                 targets: Vec::new(),
281                 is_member,
282                 edition,
283                 dependencies: Vec::new(),
284                 features: Vec::new(),
285                 cfgs: cfgs.get(&id).cloned().unwrap_or_default(),
286                 envs: envs.get(&id).cloned().unwrap_or_default(),
287                 out_dir: out_dir_by_id.get(&id).cloned(),
288                 proc_macro_dylib_path: proc_macro_dylib_paths.get(&id).cloned(),
289             });
290             let pkg_data = &mut packages[pkg];
291             pkg_by_id.insert(id, pkg);
292             for meta_tgt in meta_pkg.targets {
293                 let is_proc_macro = meta_tgt.kind.as_slice() == ["proc-macro"];
294                 let tgt = targets.alloc(TargetData {
295                     package: pkg,
296                     name: meta_tgt.name,
297                     root: AbsPathBuf::assert(meta_tgt.src_path.clone()),
298                     kind: TargetKind::new(meta_tgt.kind.as_slice()),
299                     is_proc_macro,
300                 });
301                 pkg_data.targets.push(tgt);
302             }
303         }
304         let resolve = meta.resolve.expect("metadata executed with deps");
305         for mut node in resolve.nodes {
306             let source = match pkg_by_id.get(&node.id) {
307                 Some(&src) => src,
308                 // FIXME: replace this and a similar branch below with `.unwrap`, once
309                 // https://github.com/rust-lang/cargo/issues/7841
310                 // is fixed and hits stable (around 1.43-is probably?).
311                 None => {
312                     log::error!("Node id do not match in cargo metadata, ignoring {}", node.id);
313                     continue;
314                 }
315             };
316             node.deps.sort_by(|a, b| a.pkg.cmp(&b.pkg));
317             for dep_node in node.deps {
318                 let pkg = match pkg_by_id.get(&dep_node.pkg) {
319                     Some(&pkg) => pkg,
320                     None => {
321                         log::error!(
322                             "Dep node id do not match in cargo metadata, ignoring {}",
323                             dep_node.pkg
324                         );
325                         continue;
326                     }
327                 };
328                 let dep = PackageDependency { name: dep_node.name, pkg };
329                 packages[source].dependencies.push(dep);
330             }
331             packages[source].features.extend(node.features);
332         }
333
334         let workspace_root = AbsPathBuf::assert(meta.workspace_root);
335         Ok(CargoWorkspace { packages, targets, workspace_root: workspace_root })
336     }
337
338     pub fn packages<'a>(&'a self) -> impl Iterator<Item = Package> + ExactSizeIterator + 'a {
339         self.packages.iter().map(|(id, _pkg)| id)
340     }
341
342     pub fn target_by_root(&self, root: &AbsPath) -> Option<Target> {
343         self.packages()
344             .filter_map(|pkg| self[pkg].targets.iter().find(|&&it| &self[it].root == root))
345             .next()
346             .copied()
347     }
348
349     pub fn workspace_root(&self) -> &AbsPath {
350         &self.workspace_root
351     }
352
353     pub fn package_flag(&self, package: &PackageData) -> String {
354         if self.is_unique(&*package.name) {
355             package.name.clone()
356         } else {
357             format!("{}:{}", package.name, package.version)
358         }
359     }
360
361     fn is_unique(&self, name: &str) -> bool {
362         self.packages.iter().filter(|(_, v)| v.name == name).count() == 1
363     }
364 }
365
366 #[derive(Debug, Clone, Default)]
367 pub(crate) struct ExternResources {
368     out_dirs: FxHashMap<PackageId, AbsPathBuf>,
369     proc_dylib_paths: FxHashMap<PackageId, AbsPathBuf>,
370     cfgs: FxHashMap<PackageId, Vec<CfgFlag>>,
371     env: FxHashMap<PackageId, Vec<(String, String)>>,
372 }
373
374 pub(crate) fn load_extern_resources(
375     cargo_toml: &Path,
376     cargo_features: &CargoConfig,
377     progress: &dyn Fn(String),
378 ) -> Result<ExternResources> {
379     let mut cmd = Command::new(toolchain::cargo());
380     cmd.args(&["check", "--workspace", "--message-format=json", "--manifest-path"]).arg(cargo_toml);
381
382     // --all-targets includes tests, benches and examples in addition to the
383     // default lib and bins. This is an independent concept from the --targets
384     // flag below.
385     cmd.arg("--all-targets");
386
387     if let Some(target) = &cargo_features.target {
388         cmd.args(&["--target", target]);
389     }
390
391     if cargo_features.all_features {
392         cmd.arg("--all-features");
393     } else {
394         if cargo_features.no_default_features {
395             // FIXME: `NoDefaultFeatures` is mutual exclusive with `SomeFeatures`
396             // https://github.com/oli-obk/cargo_metadata/issues/79
397             cmd.arg("--no-default-features");
398         }
399         if !cargo_features.features.is_empty() {
400             cmd.arg("--features");
401             cmd.arg(cargo_features.features.join(" "));
402         }
403     }
404
405     cmd.stdout(Stdio::piped()).stderr(Stdio::null()).stdin(Stdio::null());
406
407     let mut child = cmd.spawn().map(JodChild)?;
408     let child_stdout = child.stdout.take().unwrap();
409     let stdout = BufReader::new(child_stdout);
410
411     let mut res = ExternResources::default();
412     for message in cargo_metadata::Message::parse_stream(stdout) {
413         if let Ok(message) = message {
414             match message {
415                 Message::BuildScriptExecuted(BuildScript {
416                     package_id,
417                     out_dir,
418                     cfgs,
419                     env,
420                     ..
421                 }) => {
422                     let cfgs = {
423                         let mut acc = Vec::new();
424                         for cfg in cfgs {
425                             match cfg.parse::<CfgFlag>() {
426                                 Ok(it) => acc.push(it),
427                                 Err(err) => {
428                                     anyhow::bail!("invalid cfg from cargo-metadata: {}", err)
429                                 }
430                             };
431                         }
432                         acc
433                     };
434                     // cargo_metadata crate returns default (empty) path for
435                     // older cargos, which is not absolute, so work around that.
436                     if out_dir != PathBuf::default() {
437                         let out_dir = AbsPathBuf::assert(out_dir);
438                         res.out_dirs.insert(package_id.clone(), out_dir);
439                         res.cfgs.insert(package_id.clone(), cfgs);
440                     }
441
442                     res.env.insert(package_id, env);
443                 }
444                 Message::CompilerArtifact(message) => {
445                     progress(format!("metadata {}", message.target.name));
446
447                     if message.target.kind.contains(&"proc-macro".to_string()) {
448                         let package_id = message.package_id;
449                         // Skip rmeta file
450                         if let Some(filename) = message.filenames.iter().find(|name| is_dylib(name))
451                         {
452                             let filename = AbsPathBuf::assert(filename.clone());
453                             res.proc_dylib_paths.insert(package_id, filename);
454                         }
455                     }
456                 }
457                 Message::CompilerMessage(message) => {
458                     progress(message.target.name.clone());
459                 }
460                 Message::Unknown => (),
461                 Message::BuildFinished(_) => {}
462                 Message::TextLine(_) => {}
463             }
464         }
465     }
466     Ok(res)
467 }
468
469 // FIXME: File a better way to know if it is a dylib
470 fn is_dylib(path: &Path) -> bool {
471     match path.extension().and_then(OsStr::to_str).map(|it| it.to_string().to_lowercase()) {
472         None => false,
473         Some(ext) => matches!(ext.as_str(), "dll" | "dylib" | "so"),
474     }
475 }
476
477 /// Recreates the compile-time environment variables that Cargo sets.
478 ///
479 /// Should be synced with <https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates>
480 fn inject_cargo_env(package: &cargo_metadata::Package, env: &mut Vec<(String, String)>) {
481     // FIXME: Missing variables:
482     // CARGO, CARGO_PKG_HOMEPAGE, CARGO_CRATE_NAME, CARGO_BIN_NAME, CARGO_BIN_EXE_<name>
483
484     let mut manifest_dir = package.manifest_path.clone();
485     manifest_dir.pop();
486     if let Some(cargo_manifest_dir) = manifest_dir.to_str() {
487         env.push(("CARGO_MANIFEST_DIR".into(), cargo_manifest_dir.into()));
488     }
489
490     env.push(("CARGO_PKG_VERSION".into(), package.version.to_string()));
491     env.push(("CARGO_PKG_VERSION_MAJOR".into(), package.version.major.to_string()));
492     env.push(("CARGO_PKG_VERSION_MINOR".into(), package.version.minor.to_string()));
493     env.push(("CARGO_PKG_VERSION_PATCH".into(), package.version.patch.to_string()));
494
495     let pre = package.version.pre.iter().map(|id| id.to_string()).format(".");
496     env.push(("CARGO_PKG_VERSION_PRE".into(), pre.to_string()));
497
498     let authors = package.authors.join(";");
499     env.push(("CARGO_PKG_AUTHORS".into(), authors));
500
501     env.push(("CARGO_PKG_NAME".into(), package.name.clone()));
502     env.push(("CARGO_PKG_DESCRIPTION".into(), package.description.clone().unwrap_or_default()));
503     //env.push(("CARGO_PKG_HOMEPAGE".into(), package.homepage.clone().unwrap_or_default()));
504     env.push(("CARGO_PKG_REPOSITORY".into(), package.repository.clone().unwrap_or_default()));
505     env.push(("CARGO_PKG_LICENSE".into(), package.license.clone().unwrap_or_default()));
506
507     let license_file =
508         package.license_file.as_ref().map(|buf| buf.display().to_string()).unwrap_or_default();
509     env.push(("CARGO_PKG_LICENSE_FILE".into(), license_file));
510 }