1 //! FIXME: write short doc here
9 process::{Command, Stdio},
12 use anyhow::{Context, Result};
14 use cargo_metadata::{BuildScript, CargoOpt, Message, MetadataCommand, PackageId};
15 use itertools::Itertools;
16 use la_arena::{Arena, Idx};
17 use paths::{AbsPath, AbsPathBuf};
18 use rustc_hash::FxHashMap;
21 use crate::cfg_flag::CfgFlag;
22 use crate::utf8_stdout;
24 /// `CargoWorkspace` represents the logical structure of, well, a Cargo
25 /// workspace. It pretty closely mirrors `cargo metadata` output.
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
32 /// We use absolute paths here, `cargo metadata` guarantees to always produce
34 #[derive(Debug, Clone, Eq, PartialEq)]
35 pub struct CargoWorkspace {
36 packages: Arena<PackageData>,
37 targets: Arena<TargetData>,
38 workspace_root: AbsPathBuf,
41 impl ops::Index<Package> for CargoWorkspace {
42 type Output = PackageData;
43 fn index(&self, index: Package) -> &PackageData {
48 impl ops::Index<Target> for CargoWorkspace {
49 type Output = TargetData;
50 fn index(&self, index: Target) -> &TargetData {
55 #[derive(Default, Clone, Debug, PartialEq, Eq)]
56 pub struct CargoConfig {
57 /// Do not activate the `default` feature.
58 pub no_default_features: bool,
60 /// Activate all available features
61 pub all_features: bool,
63 /// List of features to activate.
64 /// This will be ignored if `cargo_all_features` is true.
65 pub features: Vec<String>,
67 /// Runs cargo check on launch to figure out the correct values of OUT_DIR
68 pub load_out_dirs_from_check: bool,
71 pub target: Option<String>,
73 /// Don't load sysroot crates (`std`, `core` & friends). Might be useful
74 /// when debugging isolated issues.
77 /// rustc private crate source
78 pub rustc_source: Option<AbsPathBuf>,
81 pub type Package = Idx<PackageData>;
83 pub type Target = Idx<TargetData>;
85 /// Information associated with a cargo crate
86 #[derive(Debug, Clone, Eq, PartialEq)]
87 pub struct PackageData {
88 /// Version given in the `Cargo.toml`
90 /// Name as given in the `Cargo.toml`
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
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
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>,
117 #[derive(Debug, Clone, Eq, PartialEq)]
118 pub struct PackageDependency {
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
130 /// Path to the main source file of the target
131 pub root: AbsPathBuf,
133 pub kind: TargetKind,
134 /// Is this target a proc-macro
135 pub is_proc_macro: bool,
138 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
139 pub enum TargetKind {
141 /// Any kind of Cargo lib crate-type (dylib, rlib, proc-macro, ...).
150 fn new(kinds: &[String]) -> TargetKind {
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,
167 pub fn root(&self) -> &AbsPath {
168 self.manifest.parent().unwrap()
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);
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);
189 if !config.features.is_empty() {
190 meta.features(CargoOpt::SomeFeatures(config.features.clone()));
193 if let Some(parent) = cargo_toml.parent() {
194 meta.current_dir(parent.to_path_buf());
196 let target = if let Some(target) = config.target.as_ref() {
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) {
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())
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);
217 log::warn!("Failed to discover host platform: {}", e);
222 if let Some(target) = target {
223 meta.other_options(vec![String::from("--filter-platform"), target]);
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());
233 let workdir = cargo_toml
235 .map(|p| p.to_path_buf())
237 .map(|dir| dir.to_string_lossy().to_string())
238 .unwrap_or_else(|| "<failed to get path>".into());
241 "Failed to run `cargo metadata --manifest-path {}` in `{}`",
242 cargo_toml.display(),
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;
259 let mut pkg_by_id = FxHashMap::default();
260 let mut packages = Arena::default();
261 let mut targets = Arena::default();
263 let ws_members = &meta.workspace_members;
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());
270 let cargo_metadata::Package { id, edition, name, manifest_path, version, .. } =
272 let is_member = ws_members.contains(&id);
273 let edition = edition
275 .with_context(|| format!("Failed to parse edition {}", edition))?;
276 let pkg = packages.alloc(PackageData {
278 version: version.to_string(),
279 manifest: AbsPathBuf::assert(manifest_path),
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(),
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 {
297 root: AbsPathBuf::assert(meta_tgt.src_path.clone()),
298 kind: TargetKind::new(meta_tgt.kind.as_slice()),
301 pkg_data.targets.push(tgt);
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) {
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?).
312 log::error!("Node id do not match in cargo metadata, ignoring {}", node.id);
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) {
322 "Dep node id do not match in cargo metadata, ignoring {}",
328 let dep = PackageDependency { name: dep_node.name, pkg };
329 packages[source].dependencies.push(dep);
331 packages[source].features.extend(node.features);
334 let workspace_root = AbsPathBuf::assert(meta.workspace_root);
335 Ok(CargoWorkspace { packages, targets, workspace_root: workspace_root })
338 pub fn packages<'a>(&'a self) -> impl Iterator<Item = Package> + ExactSizeIterator + 'a {
339 self.packages.iter().map(|(id, _pkg)| id)
342 pub fn target_by_root(&self, root: &AbsPath) -> Option<Target> {
344 .filter_map(|pkg| self[pkg].targets.iter().find(|&&it| &self[it].root == root))
349 pub fn workspace_root(&self) -> &AbsPath {
353 pub fn package_flag(&self, package: &PackageData) -> String {
354 if self.is_unique(&*package.name) {
357 format!("{}:{}", package.name, package.version)
361 fn is_unique(&self, name: &str) -> bool {
362 self.packages.iter().filter(|(_, v)| v.name == name).count() == 1
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)>>,
374 pub(crate) fn load_extern_resources(
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);
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
385 cmd.arg("--all-targets");
387 if let Some(target) = &cargo_features.target {
388 cmd.args(&["--target", target]);
391 if cargo_features.all_features {
392 cmd.arg("--all-features");
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");
399 if !cargo_features.features.is_empty() {
400 cmd.arg("--features");
401 cmd.arg(cargo_features.features.join(" "));
405 cmd.stdout(Stdio::piped()).stderr(Stdio::null()).stdin(Stdio::null());
407 let mut child = cmd.spawn().map(JodChild)?;
408 let child_stdout = child.stdout.take().unwrap();
409 let stdout = BufReader::new(child_stdout);
411 let mut res = ExternResources::default();
412 for message in cargo_metadata::Message::parse_stream(stdout) {
413 if let Ok(message) = message {
415 Message::BuildScriptExecuted(BuildScript {
423 let mut acc = Vec::new();
425 match cfg.parse::<CfgFlag>() {
426 Ok(it) => acc.push(it),
428 anyhow::bail!("invalid cfg from cargo-metadata: {}", err)
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);
442 res.env.insert(package_id, env);
444 Message::CompilerArtifact(message) => {
445 progress(format!("metadata {}", message.target.name));
447 if message.target.kind.contains(&"proc-macro".to_string()) {
448 let package_id = message.package_id;
450 if let Some(filename) = message.filenames.iter().find(|name| is_dylib(name))
452 let filename = AbsPathBuf::assert(filename.clone());
453 res.proc_dylib_paths.insert(package_id, filename);
457 Message::CompilerMessage(message) => {
458 progress(message.target.name.clone());
460 Message::Unknown => (),
461 Message::BuildFinished(_) => {}
462 Message::TextLine(_) => {}
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()) {
473 Some(ext) => matches!(ext.as_str(), "dll" | "dylib" | "so"),
477 /// Recreates the compile-time environment variables that Cargo sets.
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>
484 let mut manifest_dir = package.manifest_path.clone();
486 if let Some(cargo_manifest_dir) = manifest_dir.to_str() {
487 env.push(("CARGO_MANIFEST_DIR".into(), cargo_manifest_dir.into()));
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()));
495 let pre = package.version.pre.iter().map(|id| id.to_string()).format(".");
496 env.push(("CARGO_PKG_VERSION_PRE".into(), pre.to_string()));
498 let authors = package.authors.join(";");
499 env.push(("CARGO_PKG_AUTHORS".into(), authors));
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()));
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));