diff --git a/cargo-guppy/Cargo.toml b/cargo-guppy/Cargo.toml index 22e90eb158b..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.1.0", path = "../target-spec" } diff --git a/cargo-guppy/src/lib.rs b/cargo-guppy/src/lib.rs index efadf50c94f..8552f4c4bd7 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 { + // The features are unknown. + Some(Platform::new(target, TargetFeatures::Unknown).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,14 +323,12 @@ 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(Some(true)) - .unwrap_or(true); - Some(res) + .map(|meta| { + // Include this dependency if it's optional or mandatory or if the status is + // unknown. + meta.enabled_on(platform) != EnabledStatus::Never }) .unwrap_or(true) } else { diff --git a/guppy/Cargo.toml b/guppy/Cargo.toml index e480eccc34d..3130b43661b 100644 --- a/guppy/Cargo.toml +++ b/guppy/Cargo.toml @@ -28,6 +28,7 @@ maintenance = { status = "actively-developed" } [dependencies] cargo_metadata = "0.9" +cfg-expr = { git = "https://github.com/sunshowers/cfg-expr", branch = "logic" } derivative = "2" fixedbitset = { version = "0.2.0", default-features = false } nested = "0.1.1" @@ -39,6 +40,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.1.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..9c67098986d --- /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.com-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.com-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.com-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.com-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.com-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":"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":[],"target":"cfg(all(unix, not(target_feature = \"sse\")))","registry":null},{"name":"dep-a","source":null,"req":"*","kind":"build","rename":null,"optional":true,"uses_default_features":false,"features":["bar"],"target":"cfg(all(unix, target_feature = \"sse\"))","registry":null},{"name":"dep-a","source":null,"req":"*","kind":"dev","rename":null,"optional":false,"uses_default_features":false,"features":["baz"],"target":"cfg(any(target_feature = \"sse2\", target_feature = \"atomics\"))","registry":null},{"name":"dep-a","source":null,"req":"*","kind":"build","rename":null,"optional":true,"uses_default_features":true,"features":[],"target":"cfg(any(unix, target_feature = \"sse\"))","registry":null},{"name":"dep-a","source":null,"req":"*","kind":"dev","rename":null,"optional":false,"uses_default_features":false,"features":["quux"],"target":"cfg(any(unix, target_feature = \"sse\"))","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":"dep-a","source":null,"req":"*","kind":"build","rename":null,"optional":false,"uses_default_features":false,"features":["foo"],"target":"cfg(target_feature = \"sse\")","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},{"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.com-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.com-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.com-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.com-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.com-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.com-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.com-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.com-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.com-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.com-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.com-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.com-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.com-1ecc6299db9ec823/bytes-0.5.3/benches/bytes.rs","edition":"2018","doctest":false}],"features":{"default":["std"],"std":[]},"manifest_path":"/Users/fakeuser/.cargo/registry/src/github.com-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.com-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.com-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.com-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":"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.com-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.com-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.com-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.com-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":"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.com-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.com-1ecc6299db9ec823/lazy_static-0.1.16/tests/test.rs","edition":"2015","doctest":false}],"features":{"nightly":[]},"manifest_path":"/Users/fakeuser/.cargo/registry/src/github.com-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}],"workspace_members":["testcrate-targets 0.1.0 (path+file:///Users/fakeuser/local/testcrates/testcrate-targets)"],"resolve":{"nodes":[{"id":"lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)","dependencies":[],"deps":[],"features":[]},{"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(all(unix, not(target_feature = \"sse\")))"},{"kind":"dev","target":"cfg(any(unix, target_feature = \"sse\"))"},{"kind":"dev","target":"cfg(any(target_feature = \"sse2\", target_feature = \"atomics\"))"},{"kind":"build","target":"cfg(all(unix, target_feature = \"sse\"))"},{"kind":"build","target":"cfg(any(unix, target_feature = \"sse\"))"},{"kind":"build","target":"cfg(target_feature = \"sse\")"}]},{"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 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)","dependencies":[],"deps":[],"features":[]},{"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":"dep-a 0.1.0 (path+file:///Users/fakeuser/local/testcrates/dep-a)","dependencies":[],"deps":[],"features":["bar","baz","foo","quux"]},{"id":"serde 1.0.105 (registry+https://github.com/rust-lang/crates.io-index)","dependencies":[],"deps":[],"features":["default","std"]},{"id":"lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)","dependencies":[],"deps":[],"features":[]}],"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..8feba4409f2 100644 --- a/guppy/src/errors.rs +++ b/guppy/src/errors.rs @@ -25,6 +25,8 @@ 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 internal error occurred within this `PackageGraph`. PackageGraphInternalError(String), } @@ -46,6 +48,7 @@ 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"), PackageGraphInternalError(msg) => write!(f, "Internal error in package graph: {}", msg), } } @@ -59,6 +62,7 @@ impl error::Error for Error { PackageGraphConstructError(_) => None, UnknownPackageId(_) => None, UnknownFeatureId(_, _) => None, + UnknownCurrentPlatform => None, PackageGraphInternalError(_) => None, } } diff --git a/guppy/src/graph/build.rs b/guppy/src/graph/build.rs index 9b7d95f90f9..30e4c27bfba 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| { + ( + feature.clone(), + dependency_req.feature_enabled_on(feature, ¤t_platform), + ) + }) + .collect(); + + 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..21dfc27bf14 100644 --- a/guppy/src/graph/graph_impl.rs +++ b/guppy/src/graph/graph_impl.rs @@ -4,8 +4,9 @@ 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 cfg_expr::expr::Logic; use fixedbitset::FixedBitSet; use indexmap::IndexMap; use once_cell::sync::OnceCell; @@ -16,6 +17,7 @@ use std::collections::{BTreeMap, HashMap, HashSet}; use std::iter; use std::mem; use std::path::{Path, PathBuf}; +use target_spec::TargetSpec; /// A graph of packages and dependencies between them, parsed from metadata returned by `cargo /// metadata`. @@ -787,10 +789,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 +818,89 @@ 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, or if the status is unknown. To get finer-grained information, use the `enabled` method + /// instead. pub fn optional(&self) -> bool { - self.optional + self.current_enabled != EnabledStatus::Always + } + + /// Returns true if this is an optional dependency on the given platform. + /// + /// This will also return true if this dependency will never be included on this platform at + /// all, or if the status is unknown. To get finer-grained information, use the `enabled_on` + /// method instead. + pub fn optional_on(&self, platform: &Platform<'_>) -> bool { + self.dependency_req.enabled_on(platform) != 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. + /// + /// See the documentation for `EnabledStatus` for more. + pub fn enabled_on(&self, platform: &Platform<'_>) -> EnabledStatus { + 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 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 a list of the features enabled by this dependency. + /// Returns the status of default features of this dependency on the given platform. + /// + /// See the documentation for `EnabledStatus` for more. + pub fn default_features_on(&self, platform: &Platform<'_>) -> EnabledStatus { + 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<'_>) -> EnabledStatus { + self.dependency_req.feature_enabled_on(feature, platform) } /// Returns the target string for this dependency, if specified. This is a string like @@ -829,7 +908,234 @@ 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, + /// The status of this dependency is *unknown* because the evaluation involved target features + /// whose status is unknown. + /// + /// This can only be returned if the set of target features is unknown. In particular, it is + /// guaranteed never to be returned for queries being evaluated against the current platform, + /// since the exact set of target features is already known. + Unknown(UnknownStatus), +} + +impl EnabledStatus { + /// Converts a mandatory evaluation result and a thunk returning the optional result into an + /// `EnabledStatus`. + pub(super) fn new( + mandatory_res: Option, + optional_res_fn: impl FnOnce() -> Option, + ) -> Self { + // mandatory optional | result + // | + // T * | always + // U T | optional present + // U U | unknown [1] + // U F | unknown [1] + // F T | optional + // F U | optional unknown + // F F | never + // + // [1] note that both these cases are collapsed into "unknown" -- for either of these it's + // not known whether the dependency will be included at all. + + match mandatory_res { + Some(true) => EnabledStatus::Always, + None => match optional_res_fn() { + Some(true) => EnabledStatus::Unknown(UnknownStatus::OptionalPresent), + None | Some(false) => EnabledStatus::Unknown(UnknownStatus::Unknown), + }, + Some(false) => match optional_res_fn() { + Some(true) => EnabledStatus::Optional, + None => EnabledStatus::Unknown(UnknownStatus::OptionalUnknown), + Some(false) => EnabledStatus::Never, + }, + } + } + + /// Returns true if the enabled status is not `Unknown`. + pub fn is_known(self) -> bool { + match self { + EnabledStatus::Always | EnabledStatus::Optional | EnabledStatus::Never => true, + EnabledStatus::Unknown(_) => false, + } + } +} + +/// More information about a dependency or feature whose evaluation is unknown. +/// +/// If the result of evaluating a dependency or feature is unknown, this enum specifies why. +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[non_exhaustive] +pub enum UnknownStatus { + /// Whether this dependency or feature is present by default or optionally is unknown. + Unknown, + /// This dependency or feature is present optionally, but whether it is included by default is + /// unknown. + OptionalPresent, + /// This dependency or feature is not present by default, and whether it is included optionally + /// is unknown. + OptionalUnknown, +} + +/// 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<'_>) -> EnabledStatus { + self.eval(|req_impl| &req_impl.build_if, platform) + } + + pub(super) fn default_features_on(&self, platform: &Platform<'_>) -> EnabledStatus { + self.eval(|req_impl| &req_impl.default_features_if, platform) + } + + fn eval( + &self, + pred_fn: impl Fn(&DependencyReqImpl) -> &TargetPredicate, + platform: &Platform<'_>, + ) -> EnabledStatus { + let mandatory_res = pred_fn(&self.mandatory).eval(platform); + EnabledStatus::new(mandatory_res, || pred_fn(&self.optional).eval(platform)) + } + + pub(super) fn feature_enabled_on( + &self, + feature: &str, + platform: &Platform<'_>, + ) -> EnabledStatus { + let matches = move |req: &DependencyReqImpl| { + let mut res = Some(false); + 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), + None => Some(true), + }; + // Short-circuit evaluation if possible. + if target_matches == Some(true) { + return Some(true); + } + res = Logic::or(res, target_matches); + } + res + }; + + EnabledStatus::new(matches(&self.mandatory), || matches(&self.optional)) + } +} + +#[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<'_>) -> Option { + match self { + TargetPredicate::Always => Some(true), + TargetPredicate::Specs(specs) => { + let mut res = Some(false); + for spec in specs.iter() { + let matches = spec.eval(platform); + // Short-circuit evaluation if possible. + if matches == Some(true) { + return Some(true); + } + res = Logic::or(res, matches); + } + res + } + } } } 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..39473c16d6e 100644 --- a/guppy/src/lib.rs +++ b/guppy/src/lib.rs @@ -56,8 +56,11 @@ 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; +// These are inlined -- generally, treat target_spec as a private dependency so expose these types +// as part of guppy's API. +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/dep_helpers.rs b/guppy/src/unit_tests/dep_helpers.rs index de1379ffd4c..3ebfca4c12b 100644 --- a/guppy/src/unit_tests/dep_helpers.rs +++ b/guppy/src/unit_tests/dep_helpers.rs @@ -3,10 +3,11 @@ use crate::graph::feature::{FeatureGraph, FeatureId, FeatureMetadata}; use crate::graph::{ - DependencyDirection, DependencyEdge, DependencyLink, PackageGraph, PackageMetadata, + kind_str, DependencyDirection, DependencyEdge, DependencyLink, DependencyMetadata, + PackageGraph, PackageMetadata, }; use crate::unit_tests::fixtures::PackageDetails; -use crate::{Error, PackageId}; +use crate::{DependencyKind, Error, PackageId}; use pretty_assertions::assert_eq; use std::collections::{BTreeSet, HashSet}; use std::fmt; @@ -307,6 +308,26 @@ pub(crate) fn assert_all_links(graph: &PackageGraph, direction: DependencyDirect msg ); + // The enabled status can't be unknown on the current platform. + for DependencyLink { from, to, edge } in &all_links { + for dep_kind in &[ + DependencyKind::Normal, + DependencyKind::Build, + DependencyKind::Development, + ] { + assert_enabled_status_is_known( + edge.metadata_for_kind(*dep_kind), + &format!( + "{}: {} -> {} ({})", + msg, + from.id(), + to.id(), + kind_str(*dep_kind) + ), + ); + } + } + // all_links should be in the correct order. assert_link_order( all_links, @@ -316,6 +337,32 @@ pub(crate) fn assert_all_links(graph: &PackageGraph, direction: DependencyDirect ); } +fn assert_enabled_status_is_known(metadata: Option<&DependencyMetadata>, msg: &str) { + let metadata = match metadata { + Some(metadata) => metadata, + None => return, + }; + + assert!( + metadata.enabled().is_known(), + "{}: enabled status known for current platform", + msg + ); + assert!( + metadata.default_features().is_known(), + "{}: default feature status known for current platform", + msg + ); + for feature in metadata.features() { + assert!( + metadata.feature_enabled(feature).is_known(), + "{}: for feature '{}', status known for current platform", + msg, + feature + ); + } +} + pub(super) trait GraphAssert<'g> { type Id: Copy + Eq + Hash + fmt::Debug; type Metadata: GraphMetadata<'g, Id = Self::Id>; diff --git a/guppy/src/unit_tests/fixtures.rs b/guppy/src/unit_tests/fixtures.rs index 226bd9f4028..17e38c12a6c 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, + UnknownStatus, 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,294 @@ 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.x86_64-unknown-linux-gnu.build-dependencies] + // bytes = { version = "0.5.2", optional = true, default-features = false, features = ["std"] } + // + // # Platform-specific dev-dependencies. + // + // [target.'cfg(any(target_feature = "sse2", target_feature = "atomics"))'.dev-dependencies] + // dep-a = { path = "../dep-a", default-features = false, features = ["baz"] } + // + // [target.'cfg(all(unix, not(target_feature = "sse")))'.dev-dependencies] + // dep-a = { path = "../dep-a" } + // + // [target.'cfg(any(unix, target_feature = "sse"))'.dev-dependencies] + // dep-a = { path = "../dep-a", default-features = false, features = ["quux"] } + // + // # Platform-specific build dependencies. + // + // [target.'cfg(target_feature = "sse")'.build-dependencies] + // dep-a = { path = "../dep-a", default-features = false, features = ["foo"] } + // + // # any -- evaluates to true for unix. + // [target.'cfg(any(unix, target_feature = "sse"))'.build-dependencies] + // dep-a = { path = "../dep-a", optional = true, default-features = true } + // + // # all -- evaluates to unknown on unixes if the target features are unknown. + // # Evaluates to false on Windows whether target features are known or not. + // [target.'cfg(all(unix, target_feature = "sse"))'.build-dependencies] + // dep-a = { path = "../dep-a", optional = true, default-features = false, features = ["bar"] } + // ``` + 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::Unknown).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::Unknown).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::Unknown. + PlatformStatus::new( + EnabledStatus::Always, + EnabledStatus::Unknown(UnknownStatus::Unknown), + ) + .with_feature_status("foo", EnabledStatus::Never) + .with_feature_status("bar", EnabledStatus::Never) + .with_feature_status("baz", EnabledStatus::Unknown(UnknownStatus::Unknown)) + .with_feature_status("quux", EnabledStatus::Always), + ) + .with_platform_status( + DependencyKind::Development, + i686_windows.clone(), + // i686_windows turns on sse and sse2. + PlatformStatus::new(EnabledStatus::Always, EnabledStatus::Never) + .with_feature_status("foo", EnabledStatus::Never) + .with_feature_status("bar", EnabledStatus::Never) + .with_feature_status("baz", EnabledStatus::Always) + .with_feature_status("quux", EnabledStatus::Always), + ) + .with_platform_status( + DependencyKind::Development, + x86_64_windows.clone(), + // x86_64_windows uses TargetFeatures::Unknown. + PlatformStatus::new( + EnabledStatus::Unknown(UnknownStatus::Unknown), + EnabledStatus::Never, + ) + .with_feature_status("foo", EnabledStatus::Never) + .with_feature_status("bar", EnabledStatus::Never) + .with_feature_status("baz", EnabledStatus::Unknown(UnknownStatus::Unknown)) + .with_feature_status("quux", EnabledStatus::Unknown(UnknownStatus::Unknown)), + ) + .with_platform_status( + DependencyKind::Build, + x86_64_linux.clone(), + // x86_64_linux uses TargetFeature::Unknown. + PlatformStatus::new( + EnabledStatus::Unknown(UnknownStatus::OptionalPresent), + EnabledStatus::Optional, + ) + .with_feature_status("foo", EnabledStatus::Unknown(UnknownStatus::Unknown)) + .with_feature_status( + "bar", + EnabledStatus::Unknown(UnknownStatus::OptionalUnknown), + ) + .with_feature_status("baz", EnabledStatus::Never) + .with_feature_status("quux", EnabledStatus::Never), + ) + .with_platform_status( + DependencyKind::Build, + i686_windows.clone(), + // i686_windows turns on sse and sse2. + PlatformStatus::new(EnabledStatus::Always, EnabledStatus::Optional) + .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::Build, + x86_64_windows.clone(), + // x86_64_windows uses TargetFeatures::Unknown. + PlatformStatus::new( + EnabledStatus::Unknown(UnknownStatus::Unknown), + EnabledStatus::Unknown(UnknownStatus::OptionalUnknown), + ) + .with_feature_status("foo", EnabledStatus::Unknown(UnknownStatus::Unknown)) + .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 +1408,124 @@ 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), + status.enabled, + "{}: for platform '{}', kind {}, enabled is correct", + msg, + platform.triple(), + kind_str(*dep_kind), + ); + assert_eq!( + metadata.default_features_on(platform), + status.default_features, + "{}: for platform '{}', kind {}, default features is correct", + msg, + platform.triple(), + kind_str(*dep_kind), + ); + for (feature, status) in &status.feature_statuses { + assert_eq!( + metadata.feature_enabled_on(feature, platform), + *status, + "{}: for platform '{}', kind {}, feature '{}' has correct status", + msg, + platform.triple(), + kind_str(*dep_kind), + 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 {