diff --git a/src/cargo/ops/cargo_output_metadata.rs b/src/cargo/ops/cargo_output_metadata.rs index 4599b7e004d..ca28733dc05 100644 --- a/src/cargo/ops/cargo_output_metadata.rs +++ b/src/cargo/ops/cargo_output_metadata.rs @@ -1,10 +1,9 @@ use crate::core::compiler::{CompileKind, CompileTarget, TargetInfo}; use crate::core::resolver::{Resolve, ResolveOpts}; -use crate::core::{Dependency, Package, PackageId, Workspace}; +use crate::core::{Package, PackageId, Workspace}; use crate::ops::{self, Packages}; use crate::util::CargoResult; -use cargo_platform::Cfg; -use serde::ser; + use serde::Serialize; use std::collections::HashMap; use std::path::PathBuf; @@ -35,53 +34,14 @@ pub fn output_metadata(ws: &Workspace<'_>, opt: &OutputMetadataOptions) -> Cargo let packages = ws.members().cloned().collect(); (packages, None) } else { - let specs = Packages::All.to_package_id_specs(ws)?; - let opts = ResolveOpts::new( + let resolve_opts = ResolveOpts::new( /*dev_deps*/ true, &opt.features, opt.all_features, !opt.no_default_features, ); - let ws_resolve = ops::resolve_ws_with_opts(ws, opts, &specs)?; - let mut package_map = HashMap::new(); - for pkg in ws_resolve - .pkg_set - .get_many(ws_resolve.pkg_set.package_ids())? - { - package_map.insert(pkg.package_id(), pkg.clone()); - } - let packages = package_map.values().map(|p| (*p).clone()).collect(); - let rustc = ws.config().load_global_rustc(Some(ws))?; - let (target, cfg) = match &opt.filter_platform { - Some(platform) => { - if platform == "host" { - let ti = - TargetInfo::new(ws.config(), CompileKind::Host, &rustc, CompileKind::Host)?; - ( - Some(rustc.host.as_str().to_string()), - Some(ti.cfg().iter().cloned().collect()), - ) - } else { - let kind = CompileKind::Target(CompileTarget::new(platform)?); - let ti = TargetInfo::new(ws.config(), kind, &rustc, kind)?; - ( - Some(platform.clone()), - Some(ti.cfg().iter().cloned().collect()), - ) - } - } - None => (None, None), - }; - let resolve = Some(MetadataResolve { - helper: ResolveHelper { - packages: package_map, - resolve: ws_resolve.targeted_resolve, - target, - cfg, - }, - root: ws.current_opt().map(|pkg| pkg.package_id()), - }); - (packages, resolve) + let (packages, resolve) = build_resolve_graph(ws, resolve_opts, &opt.filter_platform)?; + (packages, Some(resolve)) }; Ok(ExportInfo { @@ -104,81 +64,123 @@ pub struct ExportInfo { workspace_root: PathBuf, } -/// Newtype wrapper to provide a custom `Serialize` implementation. -/// The one from lock file does not fit because it uses a non-standard -/// format for `PackageId`s #[derive(Serialize)] struct MetadataResolve { - #[serde(rename = "nodes", serialize_with = "serialize_resolve")] - helper: ResolveHelper, + nodes: Vec, root: Option, } -struct ResolveHelper { - packages: HashMap, - resolve: Resolve, - target: Option, - cfg: Option>, +#[derive(Serialize)] +struct MetadataResolveNode { + id: PackageId, + dependencies: Vec, + deps: Vec, + features: Vec, } -fn serialize_resolve(helper: &ResolveHelper, s: S) -> Result -where - S: ser::Serializer, -{ - let ResolveHelper { - packages, - resolve, - target, - cfg, - } = helper; +#[derive(Serialize)] +struct Dep { + name: String, + pkg: PackageId, +} - #[derive(Serialize)] - struct Dep { - name: String, - pkg: PackageId, +fn build_resolve_graph( + ws: &Workspace<'_>, + resolve_opts: ResolveOpts, + target: &Option, +) -> CargoResult<(Vec, MetadataResolve)> { + let target_info = match target { + Some(target) => { + let config = ws.config(); + let ct = CompileTarget::new(target)?; + let short_name = ct.short_name().to_string(); + let kind = CompileKind::Target(ct); + let rustc = config.load_global_rustc(Some(ws))?; + Some((short_name, TargetInfo::new(config, kind, &rustc, kind)?)) + } + None => None, + }; + // Resolve entire workspace. + let specs = Packages::All.to_package_id_specs(ws)?; + let ws_resolve = ops::resolve_ws_with_opts(ws, resolve_opts, &specs)?; + // Download all Packages. This is needed to serialize the information + // for every package. In theory this could honor target filtering, + // but that would be somewhat complex. + let mut package_map: HashMap = ws_resolve + .pkg_set + .get_many(ws_resolve.pkg_set.package_ids())? + .into_iter() + .map(|pkg| (pkg.package_id(), pkg.clone())) + .collect(); + // Start from the workspace roots, and recurse through filling out the + // map, filtering targets as necessary. + let mut node_map = HashMap::new(); + for member_pkg in ws.members() { + build_resolve_graph_r( + &mut node_map, + member_pkg.package_id(), + &ws_resolve.targeted_resolve, + &package_map, + target_info.as_ref(), + ); } + // Get a Vec of Packages. + let actual_packages = package_map + .drain() + .filter_map(|(pkg_id, pkg)| node_map.get(&pkg_id).map(|_| pkg)) + .collect(); + let mr = MetadataResolve { + nodes: node_map.drain().map(|(_pkg_id, node)| node).collect(), + root: ws.current_opt().map(|pkg| pkg.package_id()), + }; + Ok((actual_packages, mr)) +} - #[derive(Serialize)] - struct Node<'a> { - id: PackageId, - dependencies: Vec, - deps: Vec, - features: Vec<&'a str>, +fn build_resolve_graph_r( + node_map: &mut HashMap, + pkg_id: PackageId, + resolve: &Resolve, + package_map: &HashMap, + target: Option<&(String, TargetInfo)>, +) { + if node_map.contains_key(&pkg_id) { + return; } - - // A filter for removing platform dependencies. - let dep_filter = |(_pkg, deps): &(PackageId, &[Dependency])| match (target, cfg) { - (Some(target), Some(cfg)) => deps.iter().any(|dep| { - let platform = match dep.platform() { - Some(p) => p, - None => return true, - }; - platform.matches(target, cfg) - }), - (None, None) => true, - _ => unreachable!(), + let features = resolve + .features_sorted(pkg_id) + .into_iter() + .map(|s| s.to_string()) + .collect(); + let deps: Vec = resolve + .deps(pkg_id) + .filter(|(_dep_id, deps)| match target { + Some((short_name, info)) => deps.iter().any(|dep| { + let platform = match dep.platform() { + Some(p) => p, + None => return true, + }; + platform.matches(short_name, info.cfg()) + }), + None => true, + }) + .filter_map(|(dep_id, _deps)| { + package_map + .get(&dep_id) + .and_then(|pkg| pkg.targets().iter().find(|t| t.is_lib())) + .and_then(|lib_target| resolve.extern_crate_name(pkg_id, dep_id, lib_target).ok()) + .map(|name| Dep { name, pkg: dep_id }) + }) + .collect(); + let dumb_deps: Vec = deps.iter().map(|dep| dep.pkg).collect(); + let to_visit = dumb_deps.clone(); + let node = MetadataResolveNode { + id: pkg_id, + dependencies: dumb_deps, + deps, + features, }; - - s.collect_seq(resolve.iter().map(|id| { - Node { - id, - dependencies: resolve - .deps(id) - .filter(dep_filter) - .map(|(pkg, _deps)| pkg) - .collect(), - deps: resolve - .deps(id) - .filter(dep_filter) - .filter_map(|(pkg, _deps)| { - packages - .get(&pkg) - .and_then(|pkg| pkg.targets().iter().find(|t| t.is_lib())) - .and_then(|lib_target| resolve.extern_crate_name(id, pkg, lib_target).ok()) - .map(|name| Dep { name, pkg }) - }) - .collect(), - features: resolve.features_sorted(id), - } - })) + node_map.insert(pkg_id, node); + for dep_id in to_visit { + build_resolve_graph_r(node_map, dep_id, resolve, package_map, target); + } } diff --git a/tests/testsuite/metadata.rs b/tests/testsuite/metadata.rs index 08efbd40b06..d56c70e00f9 100644 --- a/tests/testsuite/metadata.rs +++ b/tests/testsuite/metadata.rs @@ -1795,15 +1795,81 @@ fn deps_with_bin_only() { .file("bdep/src/main.rs", "fn main() {}") .build(); - let output = p - .cargo("metadata") - .exec_with_output() - .expect("cargo metadata failed"); - let stdout = std::str::from_utf8(&output.stdout).unwrap(); - let meta: serde_json::Value = serde_json::from_str(stdout).expect("failed to parse json"); - let nodes = &meta["resolve"]["nodes"]; - assert!(nodes[0]["deps"].as_array().unwrap().is_empty()); - assert!(nodes[1]["deps"].as_array().unwrap().is_empty()); + p.cargo("metadata") + .with_json( + r#" +{ + "packages": [ + { + "name": "foo", + "version": "0.1.0", + "id": "foo 0.1.0 ([..])", + "license": null, + "license_file": null, + "description": null, + "source": null, + "dependencies": [ + { + "name": "bdep", + "source": null, + "req": "*", + "kind": null, + "rename": null, + "optional": false, + "uses_default_features": true, + "features": [], + "target": null, + "registry": null + } + ], + "targets": [ + { + "kind": [ + "lib" + ], + "crate_types": [ + "lib" + ], + "name": "foo", + "src_path": "[..]/foo/src/lib.rs", + "edition": "2015", + "doctest": true + } + ], + "features": {}, + "manifest_path": "[..]/foo/Cargo.toml", + "metadata": null, + "publish": null, + "authors": [], + "categories": [], + "keywords": [], + "readme": null, + "repository": null, + "edition": "2015", + "links": null + } + ], + "workspace_members": [ + "foo 0.1.0 ([..])" + ], + "resolve": { + "nodes": [ + { + "id": "foo 0.1.0 ([..])", + "dependencies": [], + "deps": [], + "features": [] + } + ], + "root": "foo 0.1.0 ([..])" + }, + "target_directory": "[..]/foo/target", + "version": 1, + "workspace_root": "[..]foo" +} +"#, + ) + .run(); } #[cargo_test] @@ -1818,22 +1884,22 @@ fn filter_platform() { "Cargo.toml", &format!( r#" - [package] - name = "foo" - version = "0.1.0" + [package] + name = "foo" + version = "0.1.0" - [dependencies] - normal-dep = "0.0.1" + [dependencies] + normal-dep = "0.0.1" - [target.{}.dependencies] - host-dep = "0.0.1" + [target.{}.dependencies] + host-dep = "0.0.1" - [target.{}.dependencies] - alt-dep = "0.0.1" + [target.{}.dependencies] + alt-dep = "0.0.1" - [target.'cfg(foobar)'.dependencies] - cfg-dep = "0.0.1" - "#, + [target.'cfg(foobar)'.dependencies] + cfg-dep = "0.0.1" + "#, rustc_host(), alternate() ), @@ -1841,11 +1907,7 @@ fn filter_platform() { .file("src/lib.rs", "") .build(); - p.cargo("metadata") - .with_json( - &r#" -{ - "packages": [ + let alt_dep = r#" { "name": "alt-dep", "version": "0.0.1", @@ -1881,6 +1943,9 @@ fn filter_platform() { "edition": "2015", "links": null }, + "#; + + let cfg_dep = r#" { "name": "cfg-dep", "version": "0.0.1", @@ -1916,6 +1981,9 @@ fn filter_platform() { "edition": "2015", "links": null }, + "#; + + let host_dep = r#" { "name": "host-dep", "version": "0.0.1", @@ -1951,6 +2019,9 @@ fn filter_platform() { "edition": "2015", "links": null }, + "#; + + let normal_dep = r#" { "name": "normal-dep", "version": "0.0.1", @@ -1986,6 +2057,9 @@ fn filter_platform() { "edition": "2015", "links": null }, + "#; + + let foo = r#" { "name": "foo", "version": "0.1.0", @@ -2070,6 +2144,21 @@ fn filter_platform() { "edition": "2015", "links": null } + "# + .replace("$ALT", &alternate()) + .replace("$HOST", &rustc_host()); + + // Normal metadata, no filtering, returns *everything*. + p.cargo("metadata") + .with_json( + &r#" +{ + "packages": [ + $ALT_DEP + $CFG_DEP + $HOST_DEP + $NORMAL_DEP + $FOO ], "workspace_members": [ "foo 0.1.0 (path+file:[..]foo)" @@ -2136,17 +2225,25 @@ fn filter_platform() { "workspace_root": "[..]/foo" } "# - .replace("$ALT", &alternate()) - .replace("$HOST", &rustc_host()), + .replace("$ALT_DEP", alt_dep) + .replace("$CFG_DEP", cfg_dep) + .replace("$HOST_DEP", host_dep) + .replace("$NORMAL_DEP", normal_dep) + .replace("$FOO", &foo), ) .run(); + // Filter on alternate, removes cfg and host. p.cargo("metadata --filter-platform") .arg(alternate()) .with_json( - r#" + &r#" { - "packages": "{...}", + "packages": [ + $ALT_DEP + $NORMAL_DEP + $FOO + ], "workspace_members": "{...}", "resolve": { "nodes": [ @@ -2156,12 +2253,6 @@ fn filter_platform() { "deps": [], "features": [] }, - { - "id": "cfg-dep 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "dependencies": [], - "deps": [], - "features": [] - }, { "id": "foo 0.1.0 (path+file:[..]foo)", "dependencies": [ @@ -2180,12 +2271,6 @@ fn filter_platform() { ], "features": [] }, - { - "id": "host-dep 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "dependencies": [], - "deps": [], - "features": [] - }, { "id": "normal-dep 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "dependencies": [], @@ -2199,28 +2284,27 @@ fn filter_platform() { "version": 1, "workspace_root": "[..]foo" } -"#, +"# + .replace("$ALT_DEP", alt_dep) + .replace("$NORMAL_DEP", normal_dep) + .replace("$FOO", &foo), ) .run(); - let host_json = r#" + // Filter on host, removes alt and cfg. + p.cargo("metadata --filter-platform") + .arg(rustc_host()) + .with_json( + &r#" { - "packages": "{...}", + "packages": [ + $HOST_DEP + $NORMAL_DEP + $FOO + ], "workspace_members": "{...}", "resolve": { "nodes": [ - { - "id": "alt-dep 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "dependencies": [], - "deps": [], - "features": [] - }, - { - "id": "cfg-dep 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "dependencies": [], - "deps": [], - "features": [] - }, { "id": "foo 0.1.0 (path+file:[..]foo)", "dependencies": [ @@ -2258,31 +2342,29 @@ fn filter_platform() { "version": 1, "workspace_root": "[..]foo" } -"#; - - p.cargo("metadata --filter-platform=host") - .with_json(host_json) +"# + .replace("$HOST_DEP", host_dep) + .replace("$NORMAL_DEP", normal_dep) + .replace("$FOO", &foo), + ) .run(); + + // Filter host with cfg, removes alt only p.cargo("metadata --filter-platform") .arg(rustc_host()) - .with_json(host_json) - .run(); - - p.cargo("metadata --filter-platform=host") .env("RUSTFLAGS", "--cfg=foobar") .with_json( - r#" + &r#" { - "packages": "{...}", + "packages": [ + $CFG_DEP + $HOST_DEP + $NORMAL_DEP + $FOO + ], "workspace_members": "{...}", "resolve": { "nodes": [ - { - "id": "alt-dep 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "dependencies": [], - "deps": [], - "features": [] - }, { "id": "cfg-dep 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "dependencies": [], @@ -2331,7 +2413,11 @@ fn filter_platform() { "version": 1, "workspace_root": "[..]/foo" } -"#, +"# + .replace("$CFG_DEP", cfg_dep) + .replace("$HOST_DEP", host_dep) + .replace("$NORMAL_DEP", normal_dep) + .replace("$FOO", &foo), ) .run(); }