From f2f6be4aad81f3928f9c7702bcd7501a6a7ca2ac Mon Sep 17 00:00:00 2001 From: Rain Date: Fri, 27 Mar 2020 19:27:02 -0700 Subject: [PATCH] [guppy] handle platform dependencies + update feature graph construction This is a complete overhaul of the way platform-specific dependencies are handled. Based on my experimentation and reading the Cargo source code, I believe that this is now correct. Doing so also required feature graph construction to be updated, so the feature graph construction is ready for the new feature resolver, and is now platform-sensitive as well. The APIs for the feature graph are still to come, but making the data model be full-fidelity allows for both the current and new feature resolvers to be implemented. Now that target logic is internalized in guppy, cargo-guppy no longer needs to do its own evaluation. For more about the new feature resolver, see: * https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#features * https://github.com/rust-lang/cargo/issues/7914 * https://github.com/rust-lang/cargo/issues/7915 * https://github.com/rust-lang/cargo/issues/7916 --- cargo-guppy/Cargo.toml | 1 - cargo-guppy/src/lib.rs | 20 +- guppy/Cargo.toml | 1 + guppy/fixtures/small/metadata_targets1.json | 1 + guppy/src/errors.rs | 18 + guppy/src/graph/build.rs | 263 +++++++++--- guppy/src/graph/feature/build.rs | 294 +++++++++----- guppy/src/graph/feature/graph_impl.rs | 32 +- guppy/src/graph/graph_impl.rs | 273 ++++++++++++- guppy/src/graph/mod.rs | 2 +- guppy/src/lib.rs | 4 +- guppy/src/platform.rs | 20 + guppy/src/unit_tests/fixtures.rs | 420 +++++++++++++++++++- guppy/src/unit_tests/graph_tests.rs | 10 +- 14 files changed, 1148 insertions(+), 211 deletions(-) create mode 100644 guppy/fixtures/small/metadata_targets1.json create mode 100644 guppy/src/platform.rs diff --git a/cargo-guppy/Cargo.toml b/cargo-guppy/Cargo.toml index b49ece24eae..476e02da924 100644 --- a/cargo-guppy/Cargo.toml +++ b/cargo-guppy/Cargo.toml @@ -13,4 +13,3 @@ itertools = "0.9.0" serde = "1.0.40" serde_json = "1.0.40" structopt = "0.3.0" -target-spec = { version = "0.2.0", path = "../target-spec" } diff --git a/cargo-guppy/src/lib.rs b/cargo-guppy/src/lib.rs index 04732c3b6fd..014c6e4fff2 100644 --- a/cargo-guppy/src/lib.rs +++ b/cargo-guppy/src/lib.rs @@ -3,9 +3,10 @@ use anyhow; use clap::arg_enum; +use guppy::graph::EnabledStatus; use guppy::{ graph::{DependencyLink, DotWrite, PackageDotVisitor, PackageGraph, PackageMetadata}, - MetadataCommand, PackageId, + MetadataCommand, PackageId, Platform, TargetFeatures, }; use itertools; use std::cmp; @@ -15,7 +16,6 @@ use std::fs; use std::io::Write; use std::iter; use structopt::StructOpt; -use target_spec; mod diff; @@ -305,6 +305,13 @@ fn narrow_graph(pkg_graph: &mut PackageGraph, options: &FilterOptions) { } } + let platform = if let Some(ref target) = options.target { + // Accept all features for the target filtering below. + Some(Platform::new(target, TargetFeatures::All).unwrap()) + } else { + None + }; + pkg_graph.retain_edges(|_, DependencyLink { from, to, edge }| { // filter by the kind of dependency (--kind) // NOTE: We always retain all workspace deps in the graph, otherwise @@ -316,12 +323,11 @@ fn narrow_graph(pkg_graph: &mut PackageGraph, options: &FilterOptions) { }; // filter out irrelevant dependencies for a specific target (--target) - let include_target = if let Some(ref target) = options.target { + let include_target = if let Some(platform) = &platform { edge.normal() - .and_then(|meta| meta.target()) - .and_then(|edge_target| { - let res = target_spec::eval(edge_target, target).unwrap_or(true); - Some(res) + .map(|meta| { + // Include this dependency if it's optional or mandatory. + meta.enabled_on(platform).unwrap() != EnabledStatus::Never }) .unwrap_or(true) } else { diff --git a/guppy/Cargo.toml b/guppy/Cargo.toml index e480eccc34d..408d8ebc5f6 100644 --- a/guppy/Cargo.toml +++ b/guppy/Cargo.toml @@ -39,6 +39,7 @@ proptest-derive = { version = "0.1.2", optional = true } semver = "0.9.0" serde = { version = "1.0.99", features = ["derive"] } serde_json = "1.0.40" +target-spec = { version = "0.2.0", path = "../target-spec" } [dev-dependencies] assert_matches = "1.3.0" diff --git a/guppy/fixtures/small/metadata_targets1.json b/guppy/fixtures/small/metadata_targets1.json new file mode 100644 index 00000000000..fb96471afb5 --- /dev/null +++ b/guppy/fixtures/small/metadata_targets1.json @@ -0,0 +1 @@ +{"packages":[{"name":"lazy_static","version":"0.2.11","id":"lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)","license":"MIT/Apache-2.0","license_file":null,"description":"A macro for declaring lazily evaluated statics in Rust.","source":"registry+https://github.com/rust-lang/crates.io-index","dependencies":[{"name":"compiletest_rs","source":"registry+https://github.com/rust-lang/crates.io-index","req":"^0.3","kind":null,"rename":null,"optional":true,"uses_default_features":true,"features":[],"target":null,"registry":null},{"name":"spin","source":"registry+https://github.com/rust-lang/crates.io-index","req":"^0.4.6","kind":null,"rename":null,"optional":true,"uses_default_features":true,"features":[],"target":null,"registry":null}],"targets":[{"kind":["lib"],"crate_types":["lib"],"name":"lazy_static","src_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/lazy_static-0.2.11/src/lib.rs","edition":"2015","doctest":true},{"kind":["test"],"crate_types":["bin"],"name":"test","src_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/lazy_static-0.2.11/tests/test.rs","edition":"2015","doctest":false},{"kind":["test"],"crate_types":["bin"],"name":"compile_tests","src_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/lazy_static-0.2.11/tests/compile_tests.rs","edition":"2015","doctest":false},{"kind":["test"],"crate_types":["bin"],"name":"no_std","src_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/lazy_static-0.2.11/tests/no_std.rs","edition":"2015","doctest":false}],"features":{"compiletest":["compiletest_rs"],"nightly":[],"spin_no_std":["nightly","spin"]},"manifest_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/lazy_static-0.2.11/Cargo.toml","metadata":null,"publish":null,"authors":["Marvin Löbel "],"categories":["no-std","rust-patterns","memory-management"],"keywords":["macro","lazy","static"],"readme":"README.md","repository":"https://github.com/rust-lang-nursery/lazy-static.rs","edition":"2015","links":null},{"name":"dep-a","version":"0.1.0","id":"dep-a 0.1.0 (path+file:///Users/fakeuser/local/testcrates/dep-a)","license":null,"license_file":null,"description":null,"source":null,"dependencies":[],"targets":[{"kind":["lib"],"crate_types":["lib"],"name":"dep-a","src_path":"/Users/fakeuser/local/testcrates/dep-a/src/lib.rs","edition":"2018","doctest":true}],"features":{"bar":[],"baz":[],"foo":[],"quux":[]},"manifest_path":"/Users/fakeuser/local/testcrates/dep-a/Cargo.toml","metadata":null,"publish":null,"authors":["Fake Author "],"categories":[],"keywords":[],"readme":null,"repository":null,"edition":"2018","links":null},{"name":"bytes","version":"0.5.3","id":"bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)","license":"MIT","license_file":null,"description":"Types and traits for working with bytes","source":"registry+https://github.com/rust-lang/crates.io-index","dependencies":[{"name":"serde","source":"registry+https://github.com/rust-lang/crates.io-index","req":"^1.0","kind":null,"rename":null,"optional":true,"uses_default_features":true,"features":[],"target":null,"registry":null},{"name":"loom","source":"registry+https://github.com/rust-lang/crates.io-index","req":"^0.2.10","kind":"dev","rename":null,"optional":false,"uses_default_features":true,"features":[],"target":null,"registry":null},{"name":"serde_test","source":"registry+https://github.com/rust-lang/crates.io-index","req":"^1.0","kind":"dev","rename":null,"optional":false,"uses_default_features":true,"features":[],"target":null,"registry":null}],"targets":[{"kind":["lib"],"crate_types":["lib"],"name":"bytes","src_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/bytes-0.5.3/src/lib.rs","edition":"2018","doctest":true},{"kind":["test"],"crate_types":["bin"],"name":"test_buf","src_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/bytes-0.5.3/tests/test_buf.rs","edition":"2018","doctest":false},{"kind":["test"],"crate_types":["bin"],"name":"test_bytes","src_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/bytes-0.5.3/tests/test_bytes.rs","edition":"2018","doctest":false},{"kind":["test"],"crate_types":["bin"],"name":"test_debug","src_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/bytes-0.5.3/tests/test_debug.rs","edition":"2018","doctest":false},{"kind":["test"],"crate_types":["bin"],"name":"test_iter","src_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/bytes-0.5.3/tests/test_iter.rs","edition":"2018","doctest":false},{"kind":["test"],"crate_types":["bin"],"name":"test_reader","src_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/bytes-0.5.3/tests/test_reader.rs","edition":"2018","doctest":false},{"kind":["test"],"crate_types":["bin"],"name":"test_chain","src_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/bytes-0.5.3/tests/test_chain.rs","edition":"2018","doctest":false},{"kind":["test"],"crate_types":["bin"],"name":"test_serde","src_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/bytes-0.5.3/tests/test_serde.rs","edition":"2018","doctest":false},{"kind":["test"],"crate_types":["bin"],"name":"test_buf_mut","src_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/bytes-0.5.3/tests/test_buf_mut.rs","edition":"2018","doctest":false},{"kind":["test"],"crate_types":["bin"],"name":"test_take","src_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/bytes-0.5.3/tests/test_take.rs","edition":"2018","doctest":false},{"kind":["bench"],"crate_types":["bin"],"name":"buf","src_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/bytes-0.5.3/benches/buf.rs","edition":"2018","doctest":false},{"kind":["bench"],"crate_types":["bin"],"name":"bytes_mut","src_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/bytes-0.5.3/benches/bytes_mut.rs","edition":"2018","doctest":false},{"kind":["bench"],"crate_types":["bin"],"name":"bytes","src_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/bytes-0.5.3/benches/bytes.rs","edition":"2018","doctest":false}],"features":{"default":["std"],"std":[]},"manifest_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/bytes-0.5.3/Cargo.toml","metadata":null,"publish":null,"authors":["Carl Lerche ","Sean McArthur "],"categories":["network-programming","data-structures"],"keywords":["buffers","zero-copy","io"],"readme":"README.md","repository":"https://github.com/tokio-rs/bytes","edition":"2018","links":null},{"name":"serde","version":"1.0.105","id":"serde 1.0.105 (registry+https://github.com/rust-lang/crates.io-index)","license":"MIT OR Apache-2.0","license_file":null,"description":"A generic serialization/deserialization framework","source":"registry+https://github.com/rust-lang/crates.io-index","dependencies":[{"name":"serde_derive","source":"registry+https://github.com/rust-lang/crates.io-index","req":"= 1.0.105","kind":null,"rename":null,"optional":true,"uses_default_features":true,"features":[],"target":null,"registry":null},{"name":"serde_derive","source":"registry+https://github.com/rust-lang/crates.io-index","req":"^1.0","kind":"dev","rename":null,"optional":false,"uses_default_features":true,"features":[],"target":null,"registry":null}],"targets":[{"kind":["lib"],"crate_types":["lib"],"name":"serde","src_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/serde-1.0.105/src/lib.rs","edition":"2015","doctest":true},{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/serde-1.0.105/build.rs","edition":"2015","doctest":false}],"features":{"alloc":[],"default":["std"],"derive":["serde_derive"],"rc":[],"std":[],"unstable":[]},"manifest_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/serde-1.0.105/Cargo.toml","metadata":{"docs":{"rs":{"targets":["x86_64-unknown-linux-gnu"]}},"playground":{"features":["derive","rc"]}},"publish":null,"authors":["Erick Tryzelaar ","David Tolnay "],"categories":["encoding"],"keywords":["serde","serialization","no_std"],"readme":"crates-io.md","repository":"https://github.com/serde-rs/serde","edition":"2015","links":null},{"name":"lazy_static","version":"0.1.16","id":"lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)","license":"MIT","license_file":null,"description":"A macro for declaring lazily evaluated statics in Rust.","source":"registry+https://github.com/rust-lang/crates.io-index","dependencies":[],"targets":[{"kind":["lib"],"crate_types":["lib"],"name":"lazy_static","src_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/lazy_static-0.1.16/src/lib.rs","edition":"2015","doctest":true},{"kind":["test"],"crate_types":["bin"],"name":"test","src_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/lazy_static-0.1.16/tests/test.rs","edition":"2015","doctest":false}],"features":{"nightly":[]},"manifest_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/lazy_static-0.1.16/Cargo.toml","metadata":null,"publish":null,"authors":["Marvin Löbel "],"categories":[],"keywords":["macro","lazy","static"],"readme":"README.md","repository":"https://github.com/rust-lang-nursery/lazy-static.rs","edition":"2015","links":null},{"name":"lazy_static","version":"1.4.0","id":"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)","license":"MIT/Apache-2.0","license_file":null,"description":"A macro for declaring lazily evaluated statics in Rust.","source":"registry+https://github.com/rust-lang/crates.io-index","dependencies":[{"name":"spin","source":"registry+https://github.com/rust-lang/crates.io-index","req":"^0.5.0","kind":null,"rename":null,"optional":true,"uses_default_features":true,"features":[],"target":null,"registry":null},{"name":"doc-comment","source":"registry+https://github.com/rust-lang/crates.io-index","req":"^0.3.1","kind":"dev","rename":null,"optional":false,"uses_default_features":true,"features":[],"target":null,"registry":null}],"targets":[{"kind":["lib"],"crate_types":["lib"],"name":"lazy_static","src_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/lazy_static-1.4.0/src/lib.rs","edition":"2015","doctest":true},{"kind":["test"],"crate_types":["bin"],"name":"test","src_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/lazy_static-1.4.0/tests/test.rs","edition":"2015","doctest":false},{"kind":["test"],"crate_types":["bin"],"name":"no_std","src_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/lazy_static-1.4.0/tests/no_std.rs","edition":"2015","doctest":false}],"features":{"spin_no_std":["spin"]},"manifest_path":"/Users/fakeuser/.cargo/registry/src/github.aaakk.us.kg-1ecc6299db9ec823/lazy_static-1.4.0/Cargo.toml","metadata":null,"publish":null,"authors":["Marvin Löbel "],"categories":["no-std","rust-patterns","memory-management"],"keywords":["macro","lazy","static"],"readme":"README.md","repository":"https://github.com/rust-lang-nursery/lazy-static.rs","edition":"2015","links":null},{"name":"testcrate-targets","version":"0.1.0","id":"testcrate-targets 0.1.0 (path+file:///Users/fakeuser/local/testcrates/testcrate-targets)","license":null,"license_file":null,"description":null,"source":null,"dependencies":[{"name":"bytes","source":"registry+https://github.com/rust-lang/crates.io-index","req":"^0.5","kind":null,"rename":null,"optional":false,"uses_default_features":false,"features":["serde"],"target":null,"registry":null},{"name":"dep-a","source":null,"req":"*","kind":null,"rename":null,"optional":true,"uses_default_features":true,"features":[],"target":null,"registry":null},{"name":"lazy_static","source":"registry+https://github.com/rust-lang/crates.io-index","req":"^1","kind":null,"rename":null,"optional":false,"uses_default_features":true,"features":[],"target":null,"registry":null},{"name":"dep-a","source":null,"req":"*","kind":"dev","rename":null,"optional":false,"uses_default_features":true,"features":["baz"],"target":"cfg(any(target_feature = \"sse2\", target_feature = \"atomics\"))","registry":null},{"name":"dep-a","source":null,"req":"*","kind":null,"rename":null,"optional":false,"uses_default_features":true,"features":["foo"],"target":"cfg(not(windows))","registry":null},{"name":"lazy_static","source":"registry+https://github.com/rust-lang/crates.io-index","req":"^0.2","kind":null,"rename":null,"optional":false,"uses_default_features":true,"features":[],"target":"cfg(not(windows))","registry":null},{"name":"bytes","source":"registry+https://github.com/rust-lang/crates.io-index","req":"= 0.5.3","kind":null,"rename":null,"optional":false,"uses_default_features":true,"features":[],"target":"cfg(target_arch = \"x86\")","registry":null},{"name":"dep-a","source":null,"req":"*","kind":null,"rename":null,"optional":false,"uses_default_features":true,"features":["bar"],"target":"cfg(target_arch = \"x86\")","registry":null},{"name":"lazy_static","source":"registry+https://github.com/rust-lang/crates.io-index","req":"^0.1","kind":"dev","rename":null,"optional":false,"uses_default_features":true,"features":[],"target":"cfg(windows)","registry":null},{"name":"bytes","source":"registry+https://github.com/rust-lang/crates.io-index","req":"^0.5.2","kind":"build","rename":null,"optional":true,"uses_default_features":false,"features":["std"],"target":"x86_64-unknown-linux-gnu","registry":null}],"targets":[{"kind":["lib"],"crate_types":["lib"],"name":"testcrate-targets","src_path":"/Users/fakeuser/local/testcrates/testcrate-targets/src/lib.rs","edition":"2018","doctest":true}],"features":{},"manifest_path":"/Users/fakeuser/local/testcrates/testcrate-targets/Cargo.toml","metadata":null,"publish":null,"authors":["Fake Author "],"categories":[],"keywords":[],"readme":null,"repository":null,"edition":"2018","links":null}],"workspace_members":["testcrate-targets 0.1.0 (path+file:///Users/fakeuser/local/testcrates/testcrate-targets)"],"resolve":{"nodes":[{"id":"bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)","dependencies":["serde 1.0.105 (registry+https://github.com/rust-lang/crates.io-index)"],"deps":[{"name":"serde","pkg":"serde 1.0.105 (registry+https://github.com/rust-lang/crates.io-index)","dep_kinds":[{"kind":null,"target":null}]}],"features":["default","serde","std"]},{"id":"testcrate-targets 0.1.0 (path+file:///Users/fakeuser/local/testcrates/testcrate-targets)","dependencies":["bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)","dep-a 0.1.0 (path+file:///Users/fakeuser/local/testcrates/dep-a)","lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)","lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)","lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)"],"deps":[{"name":"bytes","pkg":"bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)","dep_kinds":[{"kind":null,"target":null},{"kind":null,"target":"cfg(target_arch = \"x86\")"},{"kind":"build","target":"x86_64-unknown-linux-gnu"}]},{"name":"dep_a","pkg":"dep-a 0.1.0 (path+file:///Users/fakeuser/local/testcrates/dep-a)","dep_kinds":[{"kind":null,"target":null},{"kind":null,"target":"cfg(not(windows))"},{"kind":null,"target":"cfg(target_arch = \"x86\")"},{"kind":"dev","target":"cfg(any(target_feature = \"sse2\", target_feature = \"atomics\"))"}]},{"name":"lazy_static","pkg":"lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)","dep_kinds":[{"kind":"dev","target":"cfg(windows)"}]},{"name":"lazy_static","pkg":"lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)","dep_kinds":[{"kind":null,"target":"cfg(not(windows))"}]},{"name":"lazy_static","pkg":"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)","dep_kinds":[{"kind":null,"target":null}]}],"features":["bytes","dep-a"]},{"id":"lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)","dependencies":[],"deps":[],"features":[]},{"id":"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)","dependencies":[],"deps":[],"features":[]},{"id":"lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)","dependencies":[],"deps":[],"features":[]},{"id":"dep-a 0.1.0 (path+file:///Users/fakeuser/local/testcrates/dep-a)","dependencies":[],"deps":[],"features":["bar","baz","foo"]},{"id":"serde 1.0.105 (registry+https://github.com/rust-lang/crates.io-index)","dependencies":[],"deps":[],"features":["default","std"]}],"root":"testcrate-targets 0.1.0 (path+file:///Users/fakeuser/local/testcrates/testcrate-targets)"},"target_directory":"/Users/fakeuser/local/testcrates/testcrate-targets/target","version":1,"workspace_root":"/Users/fakeuser/local/testcrates/testcrate-targets"} diff --git a/guppy/src/errors.rs b/guppy/src/errors.rs index 52feb16c9e5..64da61b52e8 100644 --- a/guppy/src/errors.rs +++ b/guppy/src/errors.rs @@ -25,6 +25,16 @@ pub enum Error { UnknownPackageId(PackageId), /// A feature ID was unknown to this `FeatureGraph`. UnknownFeatureId(PackageId, Option), + /// The platform `guppy` is running on is unknown. + UnknownCurrentPlatform, + /// An error occurred while evaluating a target specification against the given platform. + #[non_exhaustive] + TargetEvalError { + /// The given platform. + platform: &'static str, + /// The error that occurred while evaluating the target specification. + err: Box, + }, /// An internal error occurred within this `PackageGraph`. PackageGraphInternalError(String), } @@ -46,6 +56,12 @@ impl fmt::Display for Error { Some(feature) => write!(f, "Unknown feature ID: '{}' '{}'", package_id, feature), None => write!(f, "Unknown feature ID: '{}' (base)", package_id), }, + UnknownCurrentPlatform => write!(f, "Unknown current platform"), + TargetEvalError { platform, err } => write!( + f, + "Error while evaluating target specifications against platform '{}': {}", + platform, err + ), PackageGraphInternalError(msg) => write!(f, "Internal error in package graph: {}", msg), } } @@ -59,6 +75,8 @@ impl error::Error for Error { PackageGraphConstructError(_) => None, UnknownPackageId(_) => None, UnknownFeatureId(_, _) => None, + UnknownCurrentPlatform => None, + TargetEvalError { err, .. } => Some(err.as_ref()), PackageGraphInternalError(_) => None, } } diff --git a/guppy/src/graph/build.rs b/guppy/src/graph/build.rs index 9b7d95f90f9..0505277071e 100644 --- a/guppy/src/graph/build.rs +++ b/guppy/src/graph/build.rs @@ -2,16 +2,18 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::graph::{ - cargo_version_matches, kind_str, DependencyEdge, DependencyMetadata, PackageGraph, - PackageGraphData, PackageIx, PackageMetadata, Workspace, + cargo_version_matches, DependencyEdge, DependencyMetadata, DependencyReq, DependencyReqImpl, + PackageGraph, PackageGraphData, PackageIx, PackageMetadata, TargetPredicate, Workspace, }; -use crate::{Error, Metadata, PackageId}; +use crate::{Error, Metadata, PackageId, Platform}; use cargo_metadata::{Dependency, DependencyKind, NodeDep, Package, Resolve}; use once_cell::sync::OnceCell; use petgraph::prelude::*; -use semver::Version; +use semver::{Version, VersionReq}; use std::collections::{BTreeMap, HashMap, HashSet}; +use std::mem; use std::path::{Path, PathBuf}; +use target_spec::TargetSpec; impl PackageGraph { /// Constructs a new `PackageGraph` instances from the given metadata. @@ -437,10 +439,9 @@ impl DependencyEdge { resolved_name: &str, deps: impl IntoIterator, ) -> Result { - // deps should have at most 1 normal dependency, 1 build dep and 1 dev dep. - let mut normal: Option = None; - let mut build: Option = None; - let mut dev: Option = None; + let mut normal = DependencyBuildState::default(); + let mut build = DependencyBuildState::default(); + let mut dev = DependencyBuildState::default(); for dep in deps { // Dev dependencies cannot be optional. if dep.kind == DependencyKind::Development && dep.optional { @@ -450,71 +451,207 @@ impl DependencyEdge { ))); } - let to_set = match dep.kind { - DependencyKind::Normal => &mut normal, - DependencyKind::Build => &mut build, - DependencyKind::Development => &mut dev, + match dep.kind { + DependencyKind::Normal => normal.add_instance(from_id, dep)?, + DependencyKind::Build => build.add_instance(from_id, dep)?, + DependencyKind::Development => dev.add_instance(from_id, dep)?, _ => { // unknown dependency kind -- can't do much with this! continue; } }; - let metadata = DependencyMetadata { - version_req: dep.req.clone(), - optional: dep.optional, - uses_default_features: dep.uses_default_features, - features: dep.features.clone(), - target: dep.target.as_ref().map(|t| format!("{}", t)), - }; - - // It is typically an error for the same dependency to be listed multiple times for - // the same kind, but there are some situations in which it's possible. The main one - // is if there's a custom 'target' field -- one real world example is at - // https://github.com/alexcrichton/flate2-rs/blob/5751ad9/Cargo.toml#L29-L33: - // - // [dependencies] - // miniz_oxide = { version = "0.3.2", optional = true} - // - // [target.'cfg(all(target_arch = "wasm32", not(target_os = "emscripten")))'.dependencies] - // miniz_oxide = "0.3.2" - // - // For now, prefer target = null (the more general target) in such cases, and error out - // if both sides are null. - // - // TODO: Handle this better, probably through some sort of target resolution. - let write_to_set = match to_set { - Some(old) => match (old.target(), metadata.target()) { - (Some(_), None) => true, - (None, Some(_)) => false, - (Some(_), Some(_)) => { - // Both targets are set. We don't yet know if they are mutually exclusive, - // so take the first one. - // XXX This is wrong and needs to be fixed along with target resolution - // in general. - false - } - (None, None) => { - return Err(Error::PackageGraphConstructError(format!( - "{}: duplicate dependencies found for '{}' (kind: {})", - from_id, - name, - kind_str(dep.kind) - ))) - } - }, - None => true, - }; - if write_to_set { - to_set.replace(metadata); - } } Ok(DependencyEdge { dep_name: name.into(), resolved_name: resolved_name.into(), - normal, - build, - dev, + normal: normal.finish()?, + build: build.finish()?, + dev: dev.finish()?, }) } } + +/// It is possible to specify a dependency several times within the same section through +/// platform-specific dependencies and the [target] section. For example: +/// https://github.com/alexcrichton/flate2-rs/blob/5751ad9/Cargo.toml#L29-L33 +/// +/// ```toml +/// [dependencies] +/// miniz_oxide = { version = "0.3.2", optional = true} +/// +/// [target.'cfg(all(target_arch = "wasm32", not(target_os = "emscripten")))'.dependencies] +/// miniz_oxide = "0.3.2" +/// ``` +/// +/// (From here on, each separate time a particular version of a dependency +/// is listed, it is called an "instance".) +/// +/// For such situations, there are two separate analyses that happen: +/// +/// 1. Whether the dependency is included at all. This is a union of all instances, conditional on +/// the specifics of the `[target]` lines. +/// 2. What features are enabled. As of cargo 1.42, this is unified across all instances but +/// separately for mandatory/optional instances. +/// +/// Note that the new feature resolver +/// (https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#features)'s `itarget` setting +/// causes this union-ing to *not* happen, so that's why we store all the features enabled by +/// each target separately. +#[derive(Debug, Default)] +struct DependencyBuildState { + // This is the `req` field from the first instance seen if there are any, or `None` if none are + // seen. + version_req: Option, + dependency_req: DependencyReq, + // Set if there's a single target -- mostly there for backwards compat support. + single_target: Option, +} + +impl DependencyBuildState { + fn add_instance(&mut self, from_id: &PackageId, dep: &Dependency) -> Result<(), Error> { + match &self.version_req { + Some(_) => { + // There's more than one instance, so mark the single target `None`. + self.single_target = None; + } + None => { + self.version_req = Some(dep.req.clone()); + self.single_target = dep.target.as_ref().map(|platform| format!("{}", platform)); + } + } + self.dependency_req.add_instance(from_id, dep)?; + + Ok(()) + } + + fn finish(self) -> Result, Error> { + let version_req = match self.version_req { + Some(version_req) => version_req, + None => { + // No instances seen. + return Ok(None); + } + }; + + let dependency_req = self.dependency_req; + + // Evaluate this dependency against the current platform. + let current_platform = Platform::current().ok_or(Error::UnknownCurrentPlatform)?; + let current_enabled = dependency_req.enabled_on(¤t_platform)?; + let current_default_features = dependency_req.default_features_on(¤t_platform)?; + + // Collect all features from both the optional and mandatory instances. + let all_features: HashSet<_> = dependency_req.all_features().collect(); + let all_features: Vec<_> = all_features + .into_iter() + .map(|feature| feature.to_string()) + .collect(); + + // Collect the status of every feature on this platform. + let current_feature_statuses = all_features + .iter() + .map(|feature| { + Ok(( + feature.clone(), + dependency_req.feature_enabled_on(feature, ¤t_platform)?, + )) + }) + .collect::, Error>>()?; + + Ok(Some(DependencyMetadata { + version_req, + dependency_req, + current_enabled, + current_default_features, + all_features, + current_feature_statuses, + single_target: self.single_target, + })) + } +} + +impl DependencyReq { + fn add_instance(&mut self, from_id: &PackageId, dep: &Dependency) -> Result<(), Error> { + if dep.optional { + self.optional.add_instance(from_id, dep) + } else { + self.mandatory.add_instance(from_id, dep) + } + } + + fn all_features(&self) -> impl Iterator { + self.mandatory + .all_features() + .chain(self.optional.all_features()) + } +} + +impl DependencyReqImpl { + fn add_instance(&mut self, from_id: &PackageId, dep: &Dependency) -> Result<(), Error> { + // target_spec is None if this is not a platform-specific dependency. + let target_spec = match dep.target.as_ref() { + Some(spec_or_triple) => { + // This is a platform-specific dependency, so add it to the list of specs. + let spec_or_triple = format!("{}", spec_or_triple); + let target_spec: TargetSpec = spec_or_triple.parse().map_err(|err| { + Error::PackageGraphConstructError(format!( + "for package '{}': for dependency '{}', parsing target '{}' failed: {}", + from_id, dep.name, spec_or_triple, err + )) + })?; + Some(target_spec) + } + None => None, + }; + + self.build_if.add_spec(target_spec.as_ref()); + if dep.uses_default_features { + self.default_features_if.add_spec(target_spec.as_ref()); + } + self.target_features + .push((target_spec, dep.features.clone())); + Ok(()) + } +} + +impl TargetPredicate { + pub(super) fn extend(&mut self, other: &TargetPredicate) { + // &mut *self is a reborrow to allow mem::replace to work below. + match (&mut *self, other) { + (TargetPredicate::Always, _) => { + // Always stays the same since it means all specs are included. + } + (TargetPredicate::Specs(_), TargetPredicate::Always) => { + // Mark self as Always. + mem::replace(self, TargetPredicate::Always); + } + (TargetPredicate::Specs(specs), TargetPredicate::Specs(other)) => { + specs.extend_from_slice(other.as_slice()); + } + } + } + + pub(super) fn add_spec(&mut self, spec: Option<&TargetSpec>) { + // &mut *self is a reborrow to allow mem::replace to work below. + match (&mut *self, spec) { + (TargetPredicate::Always, _) => { + // Always stays the same since it means all specs are included. + } + (TargetPredicate::Specs(_), None) => { + // Mark self as Always. + mem::replace(self, TargetPredicate::Always); + } + (TargetPredicate::Specs(specs), Some(spec)) => { + specs.push(spec.clone()); + } + } + } +} + +impl Default for TargetPredicate { + fn default() -> Self { + // Empty vector means never. + TargetPredicate::Specs(vec![]) + } +} diff --git a/guppy/src/graph/feature/build.rs b/guppy/src/graph/feature/build.rs index de59462a13a..95edd9ac4b0 100644 --- a/guppy/src/graph/feature/build.rs +++ b/guppy/src/graph/feature/build.rs @@ -5,10 +5,15 @@ use crate::errors::{FeatureBuildStage, FeatureGraphWarning}; use crate::graph::feature::{ FeatureEdge, FeatureGraphImpl, FeatureMetadataImpl, FeatureNode, FeatureType, }; -use crate::graph::{DependencyLink, FeatureIx, PackageGraph, PackageMetadata}; +use crate::graph::{ + DependencyEdge, DependencyLink, DependencyReqImpl, FeatureIx, PackageGraph, PackageMetadata, + TargetPredicate, +}; +use cargo_metadata::DependencyKind; use once_cell::sync::OnceCell; use petgraph::prelude::*; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; +use target_spec::TargetSpec; #[derive(Debug)] pub(super) struct FeatureGraphBuildState<'g> { @@ -141,7 +146,12 @@ impl<'g> FeatureGraphBuildState<'g> { // Don't create a map to the base 'from' node since it is already created in // add_nodes. - self.add_edges(from_node, to_nodes, FeatureEdge::FeatureDependency); + self.add_edges( + from_node, + to_nodes + .into_iter() + .map(|to_node| (to_node, FeatureEdge::FeatureDependency)), + ); }) } @@ -181,59 +191,17 @@ impl<'g> FeatureGraphBuildState<'g> { // be built with "a", but as it turns out Cargo actually *unifies* the features, such // that foo is built with both "a" and "b". // - // There's one nuance: Cargo doesn't consider dev-dependencies of non-workspace - // packages. So if 'from' is a workspace package, look at normal, dev and build - // dependencies. If it isn't, look at normal and build dependencies. + // Nuances + // ------- + // + // Cargo doesn't consider dev-dependencies of non-workspace packages. So if 'from' is a + // workspace package, look at normal, dev and build dependencies. If it isn't, look at + // normal and build dependencies. // // XXX double check the assertion that Cargo doesn't consider dev-dependencies of // non-workspace crates. - let unified_metadata = - edge.normal() - .into_iter() - .chain(edge.build()) - .chain(if from.in_workspace() { - edge.dev() - } else { - None - }); - - let mut unified_features = HashSet::new(); - for metadata in unified_metadata { - let default_idx = match ( - metadata.uses_default_features(), - to.get_feature_idx("default"), - ) { - (true, Some(default_idx)) => Some(default_idx), - // Packages without an explicit feature named "default" get pointed to the base. - _ => None, - }; - - let feature_idxs = - default_idx - .into_iter() - .chain(metadata.features().iter().filter_map(|to_feature| { - match to.get_feature_idx(to_feature) { - Some(feature_idx) => Some(feature_idx), - None => { - // The destination feature is missing -- this is accepted by cargo - // in some circumstances, so use a warning rather than an error. - self.warnings.push(FeatureGraphWarning::MissingFeature { - stage: FeatureBuildStage::AddDependencyEdges { - package_id: from.id().clone(), - dep_name: edge.dep_name().to_string(), - }, - package_id: to.id().clone(), - feature_name: to_feature.clone(), - }); - None - } - } - })); - unified_features.extend(feature_idxs); - } - - // What feature unification does not impact, though, is whether the dependency is - // actually included in the build or not. Again, consider: + // + // Also, feature unification is impacted by whether the dependency is optional. // // [dependencies] // foo = { version = "1", features = ["a"] } @@ -242,49 +210,55 @@ impl<'g> FeatureGraphBuildState<'g> { // foo = { version = "1", optional = true, features = ["b"] } // // This will include 'foo' as a normal dependency but *not* as a build dependency by - // default. However, the normal dependency will include both features "a" and "b". + // default. + // * Without '--features foo', the `foo` dependency will be built with "a". + // * With '--features foo', `foo` will be both a normal and a build dependency, with + // features "a" and "b" in both instances. // // This means that up to two separate edges have to be represented: - // * a 'mandatory edge', which will be from the base node for 'from' to the feature - // nodes for each feature in 'to'. + // * a 'mandatory edge', which will be from the base node for 'from' to the feature nodes + // for each mandatory feature in 'to'. // * an 'optional edge', which will be from the feature node (from, dep_name) to the - // feature nodes for each feature in 'to'. - - fn extract(x: Option, expected_val: T, track: &mut bool) -> bool { - match &x { - Some(val) if val == &expected_val => { - *track = true; - true - } - _ => false, - } - } + // feature nodes for each optional feature in 'to'. This edge is only added if at least + // one line is optional. - // None = no edge, false = mandatory, true = optional - let normal = edge.normal().map(|metadata| metadata.optional()); - let build = edge.build().map(|metadata| metadata.optional()); - // None = no edge, () = mandatory (dev dependencies cannot be optional) - let dev = edge.dev().map(|_| ()); + let unified_metadata = edge + .normal() + .map(|metadata| (DependencyKind::Normal, metadata)) + .into_iter() + .chain( + edge.build() + .map(|metadata| (DependencyKind::Build, metadata)), + ) + .chain(if from.in_workspace() { + edge.dev() + .map(|metadata| (DependencyKind::Development, metadata)) + } else { + None + }); - // These variables track whether the edges should actually be added to the graph -- an edge - // where everything's set to false won't be. - let mut add_optional = false; - let mut add_mandatory = false; + let mut mandatory_req = FeatureReq::new(from, to, edge); + let mut optional_req = FeatureReq::new(from, to, edge); + for (dep_kind, metadata) in unified_metadata { + mandatory_req.add_features( + dep_kind, + &metadata.dependency_req.mandatory, + &mut self.warnings, + ); + optional_req.add_features( + dep_kind, + &metadata.dependency_req.optional, + &mut self.warnings, + ); + } - let optional_edge = FeatureEdge::Dependency { - normal: extract(normal, true, &mut add_optional), - build: extract(build, true, &mut add_optional), - dev: false, - }; - let mandatory_edge = FeatureEdge::Dependency { - normal: extract(normal, false, &mut add_mandatory), - build: extract(build, false, &mut add_mandatory), - dev: extract(dev, (), &mut add_mandatory), - }; + // Add the mandatory edges (base -> features). + self.add_edges(FeatureNode::base(from.package_ix), mandatory_req.finish()); - if add_optional { - // If add_optional is true, the dep name would have been added as an optional dependency - // node to the package metadata. + if !optional_req.is_empty() { + // This means that there is at least one instance of this dependency with optional = + // true. The dep name should have been added as an optional dependency node to the + // package metadata. let from_node = FeatureNode::new( from.package_ix, from.get_feature_idx(edge.dep_name()).unwrap_or_else(|| { @@ -295,15 +269,7 @@ impl<'g> FeatureGraphBuildState<'g> { ); }), ); - let to_nodes = - FeatureNode::base_and_all_features(to.package_ix, unified_features.iter().copied()); - self.add_edges(from_node, to_nodes, optional_edge); - } - if add_mandatory { - let from_node = FeatureNode::base(from.package_ix); - let to_nodes = - FeatureNode::base_and_all_features(to.package_ix, unified_features.iter().copied()); - self.add_edges(from_node, to_nodes, mandatory_edge); + self.add_edges(from_node, optional_req.finish()); } } @@ -326,8 +292,7 @@ impl<'g> FeatureGraphBuildState<'g> { fn add_edges( &mut self, from_node: FeatureNode, - to_nodes: impl IntoIterator, - edge: FeatureEdge, + to_nodes_edges: impl IntoIterator, ) { // The from node should always be present because it is a known node. let from_ix = self.lookup_node(&from_node).unwrap_or_else(|| { @@ -336,11 +301,11 @@ impl<'g> FeatureGraphBuildState<'g> { from_node ); }); - to_nodes.into_iter().for_each(|to_node| { + to_nodes_edges.into_iter().for_each(|(to_node, edge)| { let to_ix = self.lookup_node(&to_node).unwrap_or_else(|| { panic!("while adding feature edges, missing 'to': {:?}", to_node) }); - self.graph.update_edge(from_ix, to_ix, edge.clone()); + self.graph.update_edge(from_ix, to_ix, edge); }) } @@ -357,3 +322,128 @@ impl<'g> FeatureGraphBuildState<'g> { } } } + +#[derive(Debug)] +struct FeatureReq<'g> { + from: &'g PackageMetadata, + to: &'g PackageMetadata, + edge: &'g DependencyEdge, + to_default_idx: Option, + features: HashMap, DependencyBuildState>, +} + +impl<'g> FeatureReq<'g> { + fn new(from: &'g PackageMetadata, to: &'g PackageMetadata, edge: &'g DependencyEdge) -> Self { + Self { + from, + to, + edge, + to_default_idx: to.get_feature_idx("default"), + features: HashMap::new(), + } + } + + fn is_empty(&self) -> bool { + // add_features below is guaranteed to add at least one element to the hashmap (to the base + // feature) if req.target_features is non-empty. + self.features.is_empty() + } + + fn add_features( + &mut self, + dep_kind: DependencyKind, + req: &DependencyReqImpl, + warnings: &mut Vec, + ) { + if let (Some(default_idx), false) = + (self.to_default_idx, req.default_features_if.is_never()) + { + // Add all the conditions for the default feature to the default predicate. + self.features + .entry(Some(default_idx)) + .or_default() + .add_predicate(dep_kind, &req.default_features_if); + } else { + // Packages without an explicit feature named "default" get pointed to the base. Whether + // default features are enabled or not becomes irrelevant in that case. + } + + for (target_spec, features) in &req.target_features { + // Base feature. + self.features + .entry(None) + .or_default() + .add_spec(dep_kind, target_spec.as_ref()); + + for to_feature in features { + match self.to.get_feature_idx(to_feature) { + Some(feature_idx) => { + self.features + .entry(Some(feature_idx)) + .or_default() + .add_spec(dep_kind, target_spec.as_ref()); + } + None => { + // The destination feature is missing -- this is accepted by cargo + // in some circumstances, so use a warning rather than an error. + warnings.push(FeatureGraphWarning::MissingFeature { + stage: FeatureBuildStage::AddDependencyEdges { + package_id: self.from.id().clone(), + dep_name: self.edge.dep_name().to_string(), + }, + package_id: self.to.id().clone(), + feature_name: to_feature.to_string(), + }); + } + } + } + } + } + + fn finish(self) -> impl Iterator { + let package_ix = self.to.package_ix; + self.features + .into_iter() + .map(move |(feature_idx, build_state)| { + ( + FeatureNode::new_opt(package_ix, feature_idx), + build_state.finish(), + ) + }) + } +} + +#[derive(Debug, Default)] +struct DependencyBuildState { + normal: TargetPredicate, + build: TargetPredicate, + dev: TargetPredicate, +} + +impl DependencyBuildState { + fn add_predicate(&mut self, dep_kind: DependencyKind, pred: &TargetPredicate) { + match dep_kind { + DependencyKind::Normal => self.normal.extend(pred), + DependencyKind::Build => self.build.extend(pred), + DependencyKind::Development => self.dev.extend(pred), + _ => panic!("unknown dependency kind"), + } + } + + fn add_spec(&mut self, dep_kind: DependencyKind, spec: Option<&TargetSpec>) { + match dep_kind { + DependencyKind::Normal => self.normal.add_spec(spec), + DependencyKind::Build => self.build.add_spec(spec), + DependencyKind::Development => self.dev.add_spec(spec), + _ => panic!("unknown dependency kind"), + } + } + + fn finish(self) -> FeatureEdge { + FeatureEdge::Dependency { + normal: self.normal, + build: self.build, + dev: self.dev, + } + } +} diff --git a/guppy/src/graph/feature/graph_impl.rs b/guppy/src/graph/feature/graph_impl.rs index 49510a2ddea..bb3f9eef0e8 100644 --- a/guppy/src/graph/feature/graph_impl.rs +++ b/guppy/src/graph/feature/graph_impl.rs @@ -4,7 +4,9 @@ use crate::errors::FeatureGraphWarning; use crate::graph::feature::build::FeatureGraphBuildState; use crate::graph::feature::{Cycles, FeatureFilter}; -use crate::graph::{DependencyDirection, FeatureIx, PackageGraph, PackageIx, PackageMetadata}; +use crate::graph::{ + DependencyDirection, FeatureIx, PackageGraph, PackageIx, PackageMetadata, TargetPredicate, +}; use crate::petgraph_support::scc::Sccs; use crate::Error; use cargo_metadata::PackageId; @@ -410,6 +412,17 @@ impl FeatureNode { } } + /// Returns a new feature node, can also be the base. + pub(in crate::graph) fn new_opt( + package_ix: NodeIndex, + feature_idx: Option, + ) -> Self { + Self { + package_ix, + feature_idx, + } + } + fn from_id(feature_graph: &FeatureGraph<'_>, id: FeatureId<'_>) -> Option { let metadata = feature_graph.package_graph.metadata(id.package_id())?; match id.feature() { @@ -421,17 +434,6 @@ impl FeatureNode { } } - pub(super) fn base_and_all_features<'a>( - package_ix: NodeIndex, - feature_idxs: impl IntoIterator + 'a, - ) -> impl Iterator + 'a { - iter::once(Self::base(package_ix)).chain( - feature_idxs - .into_iter() - .map(move |feature_idx| Self::new(package_ix, feature_idx)), - ) - } - pub(super) fn named_features<'g>( package: &'g PackageMetadata, ) -> impl Iterator + 'g { @@ -455,9 +457,9 @@ pub(in crate::graph) enum FeatureEdge { /// foo = { version = "1", features = ["a", "b"] } /// ``` Dependency { - normal: bool, - build: bool, - dev: bool, + normal: TargetPredicate, + build: TargetPredicate, + dev: TargetPredicate, }, /// This edge is from a feature depending on other features: /// diff --git a/guppy/src/graph/graph_impl.rs b/guppy/src/graph/graph_impl.rs index e9b918d785c..34201a353b0 100644 --- a/guppy/src/graph/graph_impl.rs +++ b/guppy/src/graph/graph_impl.rs @@ -4,7 +4,7 @@ use crate::graph::feature::{FeatureGraphImpl, FeatureId, FeatureNode}; use crate::graph::{cargo_version_matches, kind_str, Cycles, DependencyDirection, PackageIx}; use crate::petgraph_support::scc::Sccs; -use crate::{Error, JsonValue, Metadata, MetadataCommand, PackageId}; +use crate::{Error, JsonValue, Metadata, MetadataCommand, PackageId, Platform}; use cargo_metadata::{DependencyKind, NodeDep}; use fixedbitset::FixedBitSet; use indexmap::IndexMap; @@ -16,6 +16,7 @@ use std::collections::{BTreeMap, HashMap, HashSet}; use std::iter; use std::mem; use std::path::{Path, PathBuf}; +use target_spec::{EvalError, TargetSpec}; /// A graph of packages and dependencies between them, parsed from metadata returned by `cargo /// metadata`. @@ -787,10 +788,17 @@ impl DependencyEdge { #[derive(Clone, Debug)] pub struct DependencyMetadata { pub(super) version_req: VersionReq, - pub(super) optional: bool, - pub(super) uses_default_features: bool, - pub(super) features: Vec, - pub(super) target: Option, + pub(super) dependency_req: DependencyReq, + + // Results of some queries as evaluated on the current platform. + pub(super) current_enabled: EnabledStatus, + pub(super) current_default_features: EnabledStatus, + pub(super) all_features: Vec, + pub(super) current_feature_statuses: HashMap, + + // single_target is deprecated -- it is only Some if there's exactly one instance of this + // dependency. + pub(super) single_target: Option, } impl DependencyMetadata { @@ -809,19 +817,83 @@ impl DependencyMetadata { &self.version_req } - /// Returns true if this is an optional dependency. + /// Returns true if this is an optional dependency on the platform `guppy` is running on. + /// + /// This will also return true if this dependency will never be included on this platform at + /// all. To get finer-grained information, use the `enabled` method instead. pub fn optional(&self) -> bool { - self.optional + self.current_enabled != EnabledStatus::Always + } + + /// Returns the enabled status of this dependency on the platform `guppy` is running on. + /// + /// See the documentation for `EnabledStatus` for more. + pub fn enabled(&self) -> EnabledStatus { + self.current_enabled + } + + /// Returns the enabled status of this dependency on the given platform. + /// + /// Returns an error if the triple wasn't recognized or if an error happened during evaluation. + pub fn enabled_on(&self, platform: &Platform<'_>) -> Result { + self.dependency_req.enabled_on(platform) } - /// Returns true if the default features of this dependency are enabled. + /// Returns true if the default features of this dependency are enabled on the platform `guppy` + /// is running on. + /// + /// It is possible for default features to be turned off by default, but be optionally included. + /// This method returns true in those cases. To get finer-grained information, use + /// the `default_features` method instead. pub fn uses_default_features(&self) -> bool { - self.uses_default_features + self.current_default_features != EnabledStatus::Never } - /// Returns a list of the features enabled by this dependency. + /// Returns the status of default features on the platform `guppy` is running on. + /// + /// See the documentation for `EnabledStatus` for more. + pub fn default_features(&self) -> EnabledStatus { + self.current_default_features + } + + /// Returns the status of default features of this dependency on the given platform. + /// + /// Returns an error if the triple wasn't recognized or if an error happened during evaluation. + pub fn default_features_on(&self, platform: &Platform<'_>) -> Result { + self.dependency_req.default_features_on(platform) + } + + /// Returns a list of all features possibly enabled by this dependency. This includes features + /// that are only turned on if the dependency is optional, or features enabled by inactive + /// platforms. pub fn features(&self) -> &[String] { - &self.features + &self.all_features + } + + /// Returns the enabled status of the feature on the platform `guppy` is running on. + /// + /// Note that as of Rust 1.42, the default feature resolver behaves in potentially surprising + /// ways. See the [Cargo + /// reference](https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#features) for + /// more. + /// + /// See the documentation for `EnabledStatus` for more. + pub fn feature_enabled(&self, feature: &str) -> EnabledStatus { + self.current_feature_statuses + .get(feature) + .copied() + .unwrap_or(EnabledStatus::Never) + } + + /// Returns the enabled status of the feature on the given platform. + /// + /// See the documentation of `EnabledStatus` for more. + pub fn feature_enabled_on( + &self, + feature: &str, + platform: &Platform<'_>, + ) -> Result { + self.dependency_req.feature_enabled_on(feature, platform) } /// Returns the target string for this dependency, if specified. This is a string like @@ -829,7 +901,184 @@ impl DependencyMetadata { /// /// See [Platform specific dependencies](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#platform-specific-dependencies) /// in the Cargo reference for more details. + /// + /// This will return `None` if this dependency is specified for more than one target + /// (including unconditionally, as e.g. `[dependencies]`). Therefore, this is deprecated in + /// favor of `enabled_on`. + #[deprecated(since = "0.1.7", note = "use `enabled_on` instead")] pub fn target(&self) -> Option<&str> { - self.target.as_deref() + self.single_target.as_deref() + } +} + +/// Whether a dependency or feature is enabled on a specific platform. +/// +/// Returned by the methods on `DependencyMetadata`. +/// +/// ## Examples +/// +/// ```toml +/// [dependencies] +/// once_cell = "1" +/// ``` +/// +/// The dependency and default features are *always* enabled on all platforms. +/// +/// ```toml +/// [dependencies] +/// once_cell = { version = "1", optional = true } +/// ``` +/// +/// The dependency and default features are *optional* on all platforms. +/// +/// ```toml +/// [target.'cfg(windows)'.dependencies] +/// once_cell = { version = "1", optional = true } +/// ``` +/// +/// On Windows, the dependency and default features are both *optional*. On non-Windows platforms, +/// the dependency and default features are *never* enabled. +/// +/// ```toml +/// [dependencies] +/// once_cell = { version = "1", optional = true } +/// +/// [target.'cfg(windows)'.dependencies] +/// once_cell = { version = "1", optional = false, default-features = false } +/// ``` +/// +/// On Windows, the dependency is *always* enabled and default features are *optional* (i.e. enabled +/// if the `once_cell` feature is turned on). +/// +/// On Unix platforms, the dependency and default features are both *optional*. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum EnabledStatus { + /// This dependency or feature is always enabled on this platform. + Always, + /// This dependency or feature is optionally enabled on this platform. + Optional, + /// This dependency or feature is never enabled on this platform, even if the optional + /// dependency is turned on. + Never, +} + +/// Information about dependency requirements. +#[derive(Clone, Debug, Default)] +pub(super) struct DependencyReq { + pub(super) mandatory: DependencyReqImpl, + pub(super) optional: DependencyReqImpl, +} + +impl DependencyReq { + pub(super) fn enabled_on(&self, platform: &Platform<'_>) -> Result { + self.eval(|req_impl| &req_impl.build_if, platform) + } + + pub(super) fn default_features_on( + &self, + platform: &Platform<'_>, + ) -> Result { + self.eval(|req_impl| &req_impl.default_features_if, platform) + } + + fn eval( + &self, + pred_fn: impl Fn(&DependencyReqImpl) -> &TargetPredicate, + platform: &Platform<'_>, + ) -> Result { + let map_err = move |err: EvalError| Error::TargetEvalError { + platform: platform.triple(), + err: Box::new(err), + }; + if pred_fn(&self.mandatory).eval(platform).map_err(map_err)? { + return Ok(EnabledStatus::Always); + } + if pred_fn(&self.optional).eval(platform).map_err(map_err)? { + return Ok(EnabledStatus::Optional); + } + Ok(EnabledStatus::Never) + } + + pub(super) fn feature_enabled_on( + &self, + feature: &str, + platform: &Platform<'_>, + ) -> Result { + let map_err = move |err: EvalError| Error::TargetEvalError { + platform: platform.triple(), + err: Box::new(err), + }; + + let matches = move |req: &DependencyReqImpl| { + for (target, features) in &req.target_features { + if !features.iter().any(|f| f == feature) { + continue; + } + let target_matches = match target { + Some(spec) => spec.eval(platform).map_err(map_err)?, + None => true, + }; + if target_matches { + return Ok(true); + } + } + Ok(false) + }; + + if matches(&self.mandatory)? { + return Ok(EnabledStatus::Always); + } + if matches(&self.optional)? { + return Ok(EnabledStatus::Optional); + } + Ok(EnabledStatus::Never) + } +} + +#[derive(Clone, Debug, Default)] +pub(super) struct DependencyReqImpl { + pub(super) build_if: TargetPredicate, + pub(super) default_features_if: TargetPredicate, + pub(super) target_features: Vec<(Option, Vec)>, +} + +impl DependencyReqImpl { + pub(super) fn all_features(&self) -> impl Iterator { + self.target_features + .iter() + .flat_map(|(_, features)| features) + .map(|s| s.as_str()) + } +} + +#[derive(Clone, Debug)] +pub(super) enum TargetPredicate { + Always, + // Empty vector means never. + Specs(Vec), +} + +impl TargetPredicate { + /// Returns true if this is an empty predicate (i.e. will never match). + pub(super) fn is_never(&self) -> bool { + match self { + TargetPredicate::Always => false, + TargetPredicate::Specs(specs) => specs.is_empty(), + } + } + + /// Evaluates this target against the given platform triple. + pub(super) fn eval(&self, platform: &Platform<'_>) -> Result { + match self { + TargetPredicate::Always => Ok(true), + TargetPredicate::Specs(specs) => { + for spec in specs.iter() { + if spec.eval(platform)? { + return Ok(true); + } + } + Ok(false) + } + } } } diff --git a/guppy/src/graph/mod.rs b/guppy/src/graph/mod.rs index a34339a0093..5ae5b4299f0 100644 --- a/guppy/src/graph/mod.rs +++ b/guppy/src/graph/mod.rs @@ -127,7 +127,7 @@ impl<'g> GraphSpec for feature::FeatureGraph<'g> { type Ix = FeatureIx; } -fn kind_str(kind: DependencyKind) -> &'static str { +pub(crate) fn kind_str(kind: DependencyKind) -> &'static str { match kind { DependencyKind::Normal => "normal", DependencyKind::Build => "build", diff --git a/guppy/src/lib.rs b/guppy/src/lib.rs index 3a6618be10e..9811fd485ae 100644 --- a/guppy/src/lib.rs +++ b/guppy/src/lib.rs @@ -56,8 +56,10 @@ pub use errors::Error; // Public re-exports for upstream crates used in APIs. The no_inline ensures that they show up as // re-exports in documentation. #[doc(no_inline)] -pub use cargo_metadata::{Metadata, MetadataCommand, PackageId}; +pub use cargo_metadata::{DependencyKind, Metadata, MetadataCommand, PackageId}; #[doc(no_inline)] pub use semver::Version; #[doc(no_inline)] pub use serde_json::Value as JsonValue; +#[doc(no_inline)] +pub use target_spec::{Platform, TargetFeatures}; diff --git a/guppy/src/platform.rs b/guppy/src/platform.rs new file mode 100644 index 00000000000..44cdac13a20 --- /dev/null +++ b/guppy/src/platform.rs @@ -0,0 +1,20 @@ +// Copyright (c) The cargo-guppy Contributors +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use once_cell::sync::Lazy; +use platforms::guess_current;q + +/// Represents a specific platform to evaluate targets against. + +/// Returns the platform (target triple) that `guppy` believes it is running on. +/// +/// This is not perfect, and may return `None` on some esoteric platforms. +/// +/// The current platform is used to construct `PackageGraph` instances, so if this returns `None`, +/// `guppy` will not be able to construct them. +pub fn current_platform() -> Option<&'static str> { + static CURRENT_PLATFORM: Lazy> = + Lazy::new(|| guess_current().map(|current| current.target_triple)); + + *CURRENT_PLATFORM +} diff --git a/guppy/src/unit_tests/fixtures.rs b/guppy/src/unit_tests/fixtures.rs index 226bd9f4028..c852b11ebce 100644 --- a/guppy/src/unit_tests/fixtures.rs +++ b/guppy/src/unit_tests/fixtures.rs @@ -2,16 +2,20 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::errors::FeatureBuildStage; -use crate::graph::{DependencyDirection, PackageGraph, PackageMetadata, Workspace}; +use crate::graph::{ + kind_str, DependencyDirection, DependencyEdge, EnabledStatus, PackageGraph, PackageMetadata, + Workspace, +}; use crate::unit_tests::dep_helpers::{ assert_all_links, assert_deps_internal, assert_topo_ids, assert_topo_metadatas, assert_transitive_deps_internal, }; -use crate::{errors::FeatureGraphWarning, PackageId}; +use crate::{errors::FeatureGraphWarning, DependencyKind, PackageId, Platform}; use pretty_assertions::assert_eq; use semver::Version; use std::collections::{BTreeMap, HashMap}; use std::path::PathBuf; +use target_spec::TargetFeatures; // Metadata along with interesting crate names. pub(crate) static METADATA1: &str = include_str!("../../fixtures/small/metadata1.json"); @@ -58,6 +62,21 @@ pub(crate) static METADATA_CYCLE2_LOWER_A: &str = pub(crate) static METADATA_CYCLE2_LOWER_B: &str = "lower-b 0.1.0 (path+file:///Users/fakeuser/local/testcrates/cycle2/lower-b)"; +pub(crate) static METADATA_TARGETS1: &str = + include_str!("../../fixtures/small/metadata_targets1.json"); +pub(crate) static METADATA_TARGETS1_TESTCRATE: &str = + "testcrate-targets 0.1.0 (path+file:///Users/fakeuser/local/testcrates/testcrate-targets)"; +pub(crate) static METADATA_TARGETS1_LAZY_STATIC_1: &str = + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)"; +pub(crate) static METADATA_TARGETS1_LAZY_STATIC_02: &str = + "lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)"; +pub(crate) static METADATA_TARGETS1_LAZY_STATIC_01: &str = + "lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)"; +pub(crate) static METADATA_TARGETS1_BYTES: &str = + "bytes 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)"; +pub(crate) static METADATA_TARGETS1_DEP_A: &str = + "dep-a 0.1.0 (path+file:///Users/fakeuser/local/testcrates/dep-a)"; + pub(crate) static METADATA_LIBRA: &str = include_str!("../../fixtures/large/metadata_libra.json"); pub(crate) static METADATA_LIBRA_ADMISSION_CONTROL_SERVICE: &str = "admission-control-service 0.1.0 (path+file:///Users/fakeuser/local/libra/admission_control/admission-control-service)"; @@ -183,6 +202,9 @@ impl Fixture { } } + self.details + .assert_link_details(&self.graph, "link details"); + // Tests for the feature graph. self.details .assert_feature_graph_warnings(&self.graph, "feature graph warnings"); @@ -225,6 +247,13 @@ impl Fixture { } } + pub(crate) fn metadata_targets1() -> Self { + Self { + graph: Self::parse_graph(METADATA_TARGETS1), + details: FixtureDetails::metadata_targets1(), + } + } + pub(crate) fn metadata_libra() -> Self { Self { graph: Self::parse_graph(METADATA_LIBRA), @@ -257,6 +286,7 @@ impl Fixture { pub(crate) struct FixtureDetails { workspace_members: Option>, package_details: HashMap, + link_details: HashMap<(PackageId, PackageId), LinkDetails>, feature_graph_warnings: Vec, cycles: Vec>, } @@ -266,6 +296,7 @@ impl FixtureDetails { Self { workspace_members: None, package_details, + link_details: HashMap::new(), feature_graph_warnings: vec![], cycles: vec![], } @@ -284,6 +315,14 @@ impl FixtureDetails { self } + pub(crate) fn with_link_details<'a>( + mut self, + link_details: HashMap<(PackageId, PackageId), LinkDetails>, + ) -> Self { + self.link_details = link_details; + self + } + pub(crate) fn with_feature_graph_warnings( mut self, mut warnings: Vec, @@ -413,6 +452,36 @@ impl FixtureDetails { ) } + // --- + // Links + // --- + + pub(crate) fn assert_link_details(&self, graph: &PackageGraph, msg: &str) { + for ((from, to), details) in &self.link_details { + let mut links: Vec<_> = graph + .dep_links(from) + .unwrap_or_else(|| panic!("{}: known package ID '{}' should be valid", msg, from)) + .filter(|link| link.to.id() == to) + .collect(); + assert_eq!( + links.len(), + 1, + "{}: exactly 1 link between '{}' and '{}'", + msg, + from, + to + ); + + let link = links.pop().unwrap(); + let msg = format!("{}: {} -> {}", msg, from, to); + details.assert_metadata(link.edge, &msg); + } + } + + // --- + // Features + // --- + pub(crate) fn has_named_features(&self, id: &PackageId) -> bool { self.package_details[id].named_features.is_some() } @@ -428,6 +497,16 @@ impl FixtureDetails { assert_eq!(expected, &actual, "{}", msg); } + pub(crate) fn assert_feature_graph_warnings(&self, graph: &PackageGraph, msg: &str) { + let mut actual: Vec<_> = graph.feature_graph().build_warnings().to_vec(); + actual.sort(); + assert_eq!(&self.feature_graph_warnings, &actual, "{}", msg); + } + + // --- + // Cycles + // --- + pub(crate) fn assert_cycles(&self, graph: &PackageGraph, msg: &str) { let mut actual: Vec<_> = graph .cycles() @@ -444,12 +523,6 @@ impl FixtureDetails { assert_eq!(&self.cycles, &actual, "{}", msg); } - pub(crate) fn assert_feature_graph_warnings(&self, graph: &PackageGraph, msg: &str) { - let mut actual: Vec<_> = graph.feature_graph().build_warnings().to_vec(); - actual.sort(); - assert_eq!(&self.feature_graph_warnings, &actual, "{}", msg); - } - // Specific fixtures follow. pub(crate) fn metadata1() -> Self { @@ -737,6 +810,225 @@ impl FixtureDetails { ]) } + pub(crate) fn metadata_targets1() -> Self { + // In the testcrate: + // + // [dependencies] + // lazy_static = "1" + // bytes = { version = "0.5", default-features = false, features = ["serde"] } + // dep-a = { path = "../dep-a", optional = true } + // + // [target.'cfg(not(windows))'.dependencies] + // lazy_static = "0.2" + // dep-a = { path = "../dep-a", features = ["foo"] } + // + // [target.'cfg(windows)'.dev-dependencies] + // lazy_static = "0.1" + // + // [target.'cfg(target_arch = "x86")'.dependencies] + // bytes = { version = "=0.5.3", optional = false } + // dep-a = { path = "../dep-a", features = ["bar"] } + // + // [target.'cfg(any(target_feature = "sse2", target_feature = "atomics"))'.dev-dependencies] + // dep-a = { path = "../dep-a", features = ["baz"] } + // + // [target.x86_64-unknown-linux-gnu.build-dependencies] + // bytes = { version = "0.5.2", optional = true, default-features = false, features = ["std"] } + + let mut details = HashMap::new(); + + PackageDetails::new( + METADATA_TARGETS1_TESTCRATE, + "testcrate-targets", + "0.1.0", + vec![FAKE_AUTHOR], + None, + None, + ) + .with_deps(vec![ + ("lazy_static", METADATA_TARGETS1_LAZY_STATIC_1), + ("lazy_static", METADATA_TARGETS1_LAZY_STATIC_02), + ("lazy_static", METADATA_TARGETS1_LAZY_STATIC_01), + ("bytes", METADATA_TARGETS1_BYTES), + ("dep-a", METADATA_TARGETS1_DEP_A), + ]) + .insert_into(&mut details); + + let x86_64_linux = Platform::new("x86_64-unknown-linux-gnu", TargetFeatures::All).unwrap(); + let i686_windows = Platform::new( + "i686-pc-windows-msvc", + TargetFeatures::features(&["sse", "sse2"]), + ) + .unwrap(); + let x86_64_windows = + Platform::new("x86_64-pc-windows-msvc", TargetFeatures::none()).unwrap(); + + let mut link_details = HashMap::new(); + + // testcrate -> lazy_static 1. + LinkDetails::new( + package_id(METADATA_TARGETS1_TESTCRATE), + package_id(METADATA_TARGETS1_LAZY_STATIC_1), + ) + .with_platform_status( + DependencyKind::Normal, + x86_64_linux.clone(), + PlatformStatus::new(EnabledStatus::Always, EnabledStatus::Always), + ) + .with_platform_status( + DependencyKind::Normal, + i686_windows.clone(), + PlatformStatus::new(EnabledStatus::Always, EnabledStatus::Always), + ) + .insert_into(&mut link_details); + + // testcrate -> lazy_static 0.2. + // Included on not-Windows. + LinkDetails::new( + package_id(METADATA_TARGETS1_TESTCRATE), + package_id(METADATA_TARGETS1_LAZY_STATIC_02), + ) + .with_platform_status( + DependencyKind::Normal, + x86_64_linux.clone(), + PlatformStatus::new(EnabledStatus::Always, EnabledStatus::Always), + ) + .with_platform_status( + DependencyKind::Normal, + i686_windows.clone(), + PlatformStatus::new(EnabledStatus::Never, EnabledStatus::Never), + ) + .insert_into(&mut link_details); + + // testcrate -> lazy_static 0.1. + // Included as a dev-dependency on Windows. + LinkDetails::new( + package_id(METADATA_TARGETS1_TESTCRATE), + package_id(METADATA_TARGETS1_LAZY_STATIC_01), + ) + .with_platform_status( + DependencyKind::Development, + x86_64_linux.clone(), + PlatformStatus::new(EnabledStatus::Never, EnabledStatus::Never), + ) + .with_platform_status( + DependencyKind::Development, + i686_windows.clone(), + PlatformStatus::new(EnabledStatus::Always, EnabledStatus::Always), + ) + .insert_into(&mut link_details); + + // testcrate -> bytes. + // As a normal dependency, this is always built but default-features varies. + // As a build dependency, it is only present on Linux. + LinkDetails::new( + package_id(METADATA_TARGETS1_TESTCRATE), + package_id(METADATA_TARGETS1_BYTES), + ) + .with_platform_status( + DependencyKind::Normal, + x86_64_linux.clone(), + PlatformStatus::new(EnabledStatus::Always, EnabledStatus::Never) + .with_feature_status("serde", EnabledStatus::Always) + .with_feature_status("std", EnabledStatus::Never), + ) + .with_platform_status( + DependencyKind::Normal, + i686_windows.clone(), + PlatformStatus::new(EnabledStatus::Always, EnabledStatus::Always) + .with_feature_status("serde", EnabledStatus::Always) + .with_feature_status("std", EnabledStatus::Never), + ) + .with_features(DependencyKind::Normal, vec!["serde"]) + .with_platform_status( + DependencyKind::Build, + x86_64_linux.clone(), + PlatformStatus::new(EnabledStatus::Optional, EnabledStatus::Never) + .with_feature_status("serde", EnabledStatus::Never) + .with_feature_status("std", EnabledStatus::Optional), + ) + .with_platform_status( + DependencyKind::Build, + i686_windows.clone(), + PlatformStatus::new(EnabledStatus::Never, EnabledStatus::Never) + .with_feature_status("serde", EnabledStatus::Never) + .with_feature_status("std", EnabledStatus::Never), + ) + .with_features(DependencyKind::Build, vec!["std"]) + .insert_into(&mut link_details); + + // testcrate -> dep-a. + // As a normal dependency, this is optionally built by default, but on not-Windows or on x86 + // it is mandatory. + // As a dev dependency, it is present if sse2 or atomics are turned on. + LinkDetails::new( + package_id(METADATA_TARGETS1_TESTCRATE), + package_id(METADATA_TARGETS1_DEP_A), + ) + .with_platform_status( + DependencyKind::Normal, + x86_64_linux.clone(), + PlatformStatus::new(EnabledStatus::Always, EnabledStatus::Always) + .with_feature_status("foo", EnabledStatus::Always) + .with_feature_status("bar", EnabledStatus::Never) + .with_feature_status("baz", EnabledStatus::Never) + .with_feature_status("quux", EnabledStatus::Never), + ) + .with_platform_status( + DependencyKind::Normal, + i686_windows.clone(), + PlatformStatus::new(EnabledStatus::Always, EnabledStatus::Always) + .with_feature_status("foo", EnabledStatus::Never) + .with_feature_status("bar", EnabledStatus::Always) + .with_feature_status("baz", EnabledStatus::Never) + .with_feature_status("quux", EnabledStatus::Never), + ) + .with_platform_status( + DependencyKind::Normal, + x86_64_windows.clone(), + PlatformStatus::new(EnabledStatus::Optional, EnabledStatus::Optional) + .with_feature_status("foo", EnabledStatus::Never) + .with_feature_status("bar", EnabledStatus::Never) + .with_feature_status("baz", EnabledStatus::Never) + .with_feature_status("quux", EnabledStatus::Never), + ) + .with_platform_status( + DependencyKind::Development, + x86_64_linux.clone(), + // x86_64_linux uses TargetFeature::All. + PlatformStatus::new(EnabledStatus::Always, EnabledStatus::Always) + .with_feature_status("foo", EnabledStatus::Never) + .with_feature_status("bar", EnabledStatus::Never) + .with_feature_status("baz", EnabledStatus::Always) + .with_feature_status("quux", EnabledStatus::Never), + ) + .with_platform_status( + DependencyKind::Development, + i686_windows.clone(), + // i686_windows turns on sse2. + PlatformStatus::new(EnabledStatus::Always, EnabledStatus::Always) + .with_feature_status("foo", EnabledStatus::Never) + .with_feature_status("bar", EnabledStatus::Never) + .with_feature_status("baz", EnabledStatus::Always) + .with_feature_status("quux", EnabledStatus::Never), + ) + .with_platform_status( + DependencyKind::Development, + x86_64_windows.clone(), + // x86_64_windows turns on no features. + PlatformStatus::new(EnabledStatus::Never, EnabledStatus::Never) + .with_feature_status("foo", EnabledStatus::Never) + .with_feature_status("bar", EnabledStatus::Never) + .with_feature_status("baz", EnabledStatus::Never) + .with_feature_status("quux", EnabledStatus::Never), + ) + .insert_into(&mut link_details); + + Self::new(details) + .with_workspace_members(vec![("", METADATA_TARGETS1_TESTCRATE)]) + .with_link_details(link_details) + } + pub(crate) fn metadata_libra() -> Self { let mut details = HashMap::new(); @@ -1047,6 +1339,118 @@ impl PackageDetails { } } +#[derive(Clone, Debug)] +pub(crate) struct LinkDetails { + from: PackageId, + to: PackageId, + platform_statuses: Vec<(DependencyKind, Platform<'static>, PlatformStatus)>, + features: Vec<(DependencyKind, Vec<&'static str>)>, +} + +impl LinkDetails { + pub(crate) fn new(from: PackageId, to: PackageId) -> Self { + Self { + from, + to, + platform_statuses: vec![], + features: vec![], + } + } + + pub(crate) fn with_platform_status( + mut self, + dep_kind: DependencyKind, + platform: Platform<'static>, + status: PlatformStatus, + ) -> Self { + self.platform_statuses.push((dep_kind, platform, status)); + self + } + + pub(crate) fn with_features( + mut self, + dep_kind: DependencyKind, + mut features: Vec<&'static str>, + ) -> Self { + features.sort(); + self.features.push((dep_kind, features)); + self + } + + pub(crate) fn insert_into(self, map: &mut HashMap<(PackageId, PackageId), Self>) { + map.insert((self.from.clone(), self.to.clone()), self); + } + + pub(crate) fn assert_metadata(&self, edge: &DependencyEdge, msg: &str) { + for (dep_kind, platform, status) in &self.platform_statuses { + let metadata = edge.metadata_for_kind(*dep_kind).unwrap_or_else(|| { + panic!( + "{}: dependency metadata not found for kind {}", + msg, + kind_str(*dep_kind) + ) + }); + assert_eq!( + metadata.enabled_on(platform).unwrap(), + status.enabled, + "{}: enabled is correct", + msg + ); + assert_eq!( + metadata.default_features_on(platform).unwrap(), + status.default_features, + "{}: default features is correct", + msg + ); + for (feature, status) in &status.feature_statuses { + assert_eq!( + metadata.feature_enabled_on(feature, platform).unwrap(), + *status, + "{}: feature '{}' has correct status", + msg, + feature + ); + } + } + + for (dep_kind, features) in &self.features { + let metadata = edge.metadata_for_kind(*dep_kind).unwrap_or_else(|| { + panic!( + "{}: dependency metadata not found for kind {}", + msg, + kind_str(*dep_kind) + ) + }); + let mut actual_features: Vec<_> = + metadata.features().iter().map(|s| s.as_str()).collect(); + actual_features.sort(); + assert_eq!(&actual_features, features, "{}: features is correct", msg); + } + } +} + +#[derive(Clone, Debug)] +pub(crate) struct PlatformStatus { + enabled: EnabledStatus, + default_features: EnabledStatus, + feature_statuses: HashMap, +} + +impl PlatformStatus { + fn new(enabled: EnabledStatus, default_features: EnabledStatus) -> Self { + Self { + enabled, + default_features, + feature_statuses: HashMap::new(), + } + } + + fn with_feature_status(mut self, feature: &str, status: EnabledStatus) -> Self { + self.feature_statuses.insert(feature.to_string(), status); + self + } +} + /// Helper for creating `PackageId` instances in test code. pub(crate) fn package_id(s: impl Into) -> PackageId { PackageId { repr: s.into() } diff --git a/guppy/src/unit_tests/graph_tests.rs b/guppy/src/unit_tests/graph_tests.rs index 4a52fd6c00b..3d234b954d6 100644 --- a/guppy/src/unit_tests/graph_tests.rs +++ b/guppy/src/unit_tests/graph_tests.rs @@ -89,7 +89,7 @@ mod small { let feature_graph = graph.feature_graph(); assert_eq!(feature_graph.feature_count(), 492, "feature count"); - assert_eq!(feature_graph.link_count(), 609, "link count"); + assert_eq!(feature_graph.link_count(), 608, "link count"); let root_ids: Vec<_> = feature_graph .select_workspace(all_filter()) .into_root_ids(DependencyDirection::Forward) @@ -143,6 +143,14 @@ mod small { } proptest_suite!(metadata_cycle2); + + #[test] + fn metadata_targets1() { + let metadata_targets1 = Fixture::metadata_targets1(); + metadata_targets1.verify(); + } + + proptest_suite!(metadata_targets1); } mod large {