guppy-0.17.25/.cargo_vcs_info.json0000644000000001431046102023000123740ustar { "git": { "sha1": "de0f53293a7d62801bf71db2c5325b1cb9b30773" }, "path_in_vcs": "guppy" }guppy-0.17.25/CHANGELOG.md000064400000000000000000001027471046102023000127700ustar 00000000000000# Changelog ## [0.17.25] - 2026-01-30 ### Added Cargo metadata generated on Windows is now parseable on Unix. Windows paths are always normalized to forward slashes on Unix, which allows operations like `.parent()` to work. (Cross-platform parsing is not fully tested yet so you might run into issues. Please file a bug if you do.) ## [0.17.24] - 2025-12-26 ### Added - `Workspace::build_directory()` returns the build directory for intermediate build artifacts (requires Cargo 1.91+). - `Workspace::default_members()` and `Workspace::default_member_ids()` iterate over workspace default members (requires Cargo 1.71+; returns empty iterator for older Cargo versions). - `PackageLink::registry()` returns the registry URL for a dependency, if it uses a non-default registry. - `PackageLink::path()` returns the file system path for path dependencies. ## [0.17.23] - 2025-10-12 ### Changed - Internal dependency update: `cargo_metadata` updated to 0.23.0. ## [0.17.22] - 2025-09-29 ### Fixed Replaced obsolete `doc_auto_cfg` with `doc_cfg`, to fix Rust nightly builds with the `doc_cfg` flag enabled. ## [0.17.21] - 2025-09-14 ### Changed - Internal dependency update: `cargo_metadata` updated to 0.22.0. ## [0.17.20] - 2025-07-11 ### Changed - Internal dependency update: `cargo_metadata` updated to 0.21.0. - As part of this update, restored compatibility with the unstable [bindeps feature](https://github.com/rust-lang/cargo/issues/9096) -- see [this commit](https://github.com/oli-obk/cargo_metadata/commit/73aaebb0770e1919a218dff564659f17da90067c). - MSRV updated to Rust 1.86, as required by dependencies. ## [0.17.19] - 2025-05-29 ### Changed - Internal dependency update: `cargo_metadata` updated to 0.20.0. - Some older versions of Cargo, when the unstable [bindeps feature](https://github.com/rust-lang/cargo/issues/9096) is enabled, generate JSON output that is no longer supported by `cargo_metadata`. If you run into an error, please update your nightly toolchain. Nightly versions from at least 2024-07 do not appear to produce invalid metadata. - MSRV updated to Rust 1.82. ## [0.17.18] - 2025-04-29 ### Added - `CargoSet::with_package_resolver` supports passing in a `PackageResolver` for additional dynamic filtering of dependency edges. - `CargoSet::target_links` and `host_links` return the set of `PackageLink` instances followed on the target and host platforms, respectively. Thanks to [anforowicz](https://github.com/anforowicz) for these contributions! ## [0.17.17] - 2025-02-21 ### Added - Add `PlatformEval::target_specs` to obtain the list of `TargetSpec` instances backing a platform evaluator. Thanks to [anforowicz](https://github.com/anforowicz) for the contribution! ## [0.17.16] - 2025-02-15 ### Added - `BuildTarget::test_by_default` returns true if tests are run for a build target by default. - `BuildTarget::doc_by_default` returns true if documentation is enabled for a build target, respectively. ### Changed - `BuildTarget::doc_tests` is now `BuildTarget::doctest_by_default`. The old name has been deprecated, but is kept around for compatibility. ## [0.17.15] - 2025-02-15 (This version was yanked due to incorrect documentation.) ## [0.17.14] - 2025-02-11 ### Added - `MetadataCommand::env` adds environment variables to the `cargo metadata` command. Thanks to [anforowicz](https://github.com/anforowicz) for your first contribution! ## [0.17.13] - 2025-02-08 ### Changed - Renamed `PlatformSpec::current` to `PlatformSpec::build_target` to indicate that it is determined at build time, not at runtime. The old method is still available but has been marked deprecated. ## [0.17.12] - 2025-01-05 ### Added Added support for custom sparse registries (`sparse+https://...`). Thanks to [jonhoo](https://github.com/jonhoo) for your first contribution! ## [0.17.11] - 2024-12-22 ### Added Added support for the upcoming [Cargo resolver version 3](https://doc.rust-lang.org/beta/cargo/reference/resolver.html#resolver-versions): within guppy, `CargoResolverVersion::V3`. Resolver version 3 enables MSRV-aware version resolution in Cargo. The portion of dependency resolution that guppy works with (package and feature resolution) happens after dependency versions have been resolved and `Cargo.lock` is refreshed. This means that from guppy's perspective, resolver version 3 is the same as version 2, and `CargoResolverVersion::V3` acts as an alias for `CargoResolverVersion::V2`. ## [0.17.10] - 2024-12-03 ### Changed - Internal dependency update: `cargo_metadata` updated to 0.19.1. - MSRV updated to Rust 1.82. ## [0.17.9] - 2024-12-02 ### Fixed - Graphs can now be generated even if the workspace `Cargo.toml` is within a subdirectory of one of its members. (This is an uncommon situation, but one that is supported by Cargo.) ### Changed - Internal dependency update: `cargo_metadata` updated to 0.19.0. ## [0.17.8] - 2024-10-02 ### Fixed - Fixed a case of dependency matching with renamed packages ([#317]). ### Changed - Update `target-spec` to 3.2.2. [#317]: https://github.com/guppy-rs/guppy/pull/317 ## [0.17.7] - 2024-07-31 ### Changed - Update `target-spec` to 3.2.1. ## [0.17.6] - 2024-07-29 ### Changed - MSRV updated to Rust 1.75. ### Fixed - Fixed feature graph construction accidentally inserting self-loops in some cases ([#292]). This was causing cargo-hakari to crash in some workspaces. - Fixed a small bug in Cargo resolution where packages were incorrectly being marked as activated on the host platform ([`8666ebc`]). [#292]: https://github.com/guppy-rs/guppy/pull/292 [`8666ebc`]: https://github.com/guppy-rs/guppy/commit/8666ebce44e27dae3a59f22a5ce70b7bdb252183 ## [0.17.5] - 2024-02-03 ### Changed - The `Debug` impl for `FeatureSet` is now more useful. (PRs welcome to make the `Debug` impls for types like `PackageSet` more useful as well.) - MSRV updated to Rust 1.73. ### Fixed - Cargo build simulations now consider dev-dependencies of proc-macro crates. Previously, we weren't doing so. ## [0.17.4] - 2023-11-29 ### Fixed - Attempted to address `PackageGraph` creation with artifact dependencies as supported by nightly Rust ([#174]). Note that this is not a complete fix, as documented at [#174]. [#174]: https://github.com/guppy-rs/guppy/issues/174 ## [0.17.3] - 2023-11-16 ### Fixed - Fixed a `PackageGraph` creation edge case ([#158]). [#158]: https://github.com/guppy-rs/guppy/issues/158 ## [0.17.2] - 2023-11-14 ### Fixed - Improve `PackageGraph` creation algorithm to address issues like [nextest-rs/nextest#1090](https://github.com/nextest-rs/nextest/issues/1090). ### Changed - MSRV updated to Rust 1.70. ## [0.17.1] - 2023-07-29 ### Added - `PackageMetadata::minimum_rust_version` provides the `rust-version` field of a package as a `Version`. - `PackageMetadata::rust_version` has been deprecated because it returns a `VersionReq` even though it actually should be a `Version`. In the next major release of guppy, the current definition of `rust_version` will go away and be replaced with `minimum_rust_version`. ## [0.17.0] - 2023-06-25 ### Changed - `target-spec` updated to version 3. ### Fixed - Proptest strategy creator names updated from `prop010_` to `proptest1_`. ## [0.16.0] - 2023-06-19 ### Changed - `target-spec` updated to version 2. - MSRV updated to Rust 1.66. ## [0.15.2] - 2023-01-08 ### Added - `PackageMetadata::to_feature_set` converts a single package to a `FeatureSet`. ### Changed - MSRV updated to Rust 1.62. ## [0.15.1] - 2022-12-04 ### Added - Detailed documentation about dependency cycles in Cargo, as part of the [`Cycles`](https://docs.rs/guppy/latest/guppy/graph/struct.Cycles.html) struct. Thanks [Aria](https://github.com/Gankra) for writing it! ## [0.15.0] - 2022-11-07 ### Changed - `guppy::Error::UnknownRegistryName` now boxes the internal `summary` and is smaller as a result. ## [0.14.4] - 2022-10-05 ### Changed - Internal dependency update: `cargo_metadata` updated to 0.15.1. ## [0.14.3] - 2022-09-30 ### Changed - Repository location update. - MSRV updated to Rust 1.58. Thanks to [Carol Nichols](https://github.com/carols10cents) for her contributions to this release! ## [0.14.2] - 2022-05-29 ### Fixed - On Windows, guppy now behaves correctly when a path dependency is on a different drive from the workspace ([#642]). [#642]: https://github.com/facebookincubator/cargo-guppy/issues/642 ### Changed - Internal dependency updates. ## [0.14.1] - 2022-03-18 ### Added - `Workspace::target_directory` returns the target directory provided in the Cargo metadata. - `Workspace::metadata_table` returns the freeform `workspace.metadata` table. ## [0.14.0] - 2022-03-14 ### Added Support for [weak dependencies and namespaced features]: - Cargo build simulations now take into account weak dependencies and namespaced features. - Optional dependencies (`"dep:foo"`) and namespaced features (`"foo"`) are now represented as separate nodes in a `FeatureGraph`, even with Rust versions prior to 1.60. - Feature names are now represented as a new `FeatureLabel` enum. [weak dependencies and namespaced features]: https://rust-lang.github.io/rfcs/3143-cargo-weak-namespaced-features.html ### Changed - MSRV updated to Rust 1.56. ## [0.13.0] - 2022-02-13 ### Added - `doc_cfg`-based feature labels to rustdoc. - `MetadataCommand::cargo_command` returns the underlying `std::process::Command` instance. ### Changed - `guppy::graph::feature::CrossLink` renamed to `ConditionalLink`, and now covers some same-package features. For more, see the documentation for [`ConditionalLink`]. - Public dependency bump: `target-spec` updated to version 1. ### Fixed - A small fix to Cargo build simulations ([#596](https://github.com/facebookincubator/cargo-guppy/issues/596)). [`ConditionalLink`]: https://docs.rs/guppy/0.13/guppy/graph/feature/struct.ConditionalLink.html ## [0.12.6] - 2021-12-19 ### Added - `PackageMetadata::homepage`, `documentation` and `default_run`, exposed by newer versions of Cargo. ## [0.12.5] - 2021-12-17 ### Added - `guppy` now supports a "light" mode if `--no-deps` is passed in. This mode doesn't provide any information about third-party packages or dependency edges, but is much faster if the only information needed is workspace lookups. ## [0.12.4] - 2021-12-08 - Reverted change in 0.12.3 because of [#524](https://github.com/facebookincubator/cargo-guppy/issues/524). ## [0.12.3] - 2021-11-28 - Internal dependency `guppy-workspace-hack` updated to [`workspace-hack`](https://crates.io/crates/workspace-hack). ## [0.12.2] - 2021-11-25 ### Added - `PackageMetadata::link_between`, `link_from` and `link_to` look up a direct link from one package to another. ## [0.12.1] - 2021-11-23 ### Changed - The `toml` crate is now built with the `preserve_order` feature. - This feature ensures that the key ordering in metadata is preserved. ## [0.12.0] - 2021-11-23 This is a minor breaking change that should not affect most consumers. ### Fixed - Summaries generated by old versions of `guppy` can now be parsed by this version, even if the metadata is in a different format. ### Changed - Relative paths are now stored and presented with forward slashes on all platforms, including Windows. - `guppy-summaries` updated to 0.6.0. ## [0.11.3] - 2021-11-20 ### Added - `PackageMetadata::rust_version` returns the `package.rust-version` field, if specified. Thanks [@foresterre](https://github.com/foresterre)! ## [0.11.2] - 2021-10-06 ### Added - Rudimentary support for alternate registries. This is a temporary workaround until [Cargo issue #9052](https://github.com/rust-lang/cargo/issues/9052) is resolved. - This is currently only hooked up to `hakari`. ## [0.11.1] - 2021-10-01 ### Added - A new abstraction `PlatformSpec` can represent the union of all platforms, the intersection of all platforms, or a single platform. - Methods like `EnabledStatus::required_on` and `EnabledStatus::enabled_on` have been switched to accepting a `&PlatformSpec` rather than a `&Platform`. - `CargoOptions::set_platform` and related methods now accept either a `Platform` or a `PlatformSpec`. - `EnabledStatus::enabled_on_any` is now `EnabledStatus::enabled_on(&PlatformSpec::Any)`. - Omitted packages are now easier to describe while deserializing: they now take a `workspace-members` list of names, and a `third-party` list of specifiers such as `{ name = "serde", version = "1" }`. - The resolver will now also fail if any specifiers are unmatched. ### Changed - Platform-related types have been moved into the new `platform` module at the top level. - In Cargo options summaries, `version = "v1"` and `version = "v2"` have been renamed to `resolver = "1"` and `resolver = "2"` respectively, to align with Cargo. - The old specifiers will continue to work. - Because of the changes to how omitted packages are represented, old-style `CargoOptionsSummary` instances may no longer parse correctly. - MSRV updated to Rust 1.53. ## 0.11.0 - 2021-10-01 (This release was incorrectly made and was yanked.) ## [0.10.1] - 2021-09-13 ### Changed - Public dependency version bumps: - `target-spec` updated to 0.8.0. - As a result, `Platform` no longer has a lifetime parameter. - `guppy-summaries` updated to 0.5.0. - `semver` updated to 1.0. - MSRV updated to Rust 1.51. ## [0.10.0] - 2021-09-13 (This release was yanked because `guppy-summaries` needed to be upgraded as well.) ## [0.9.0] - 2021-03-11 ### Added - `DependencyKind::VALUES` lists out all the values of `DependencyKind`. - `DependencyReq::no_default_features()` returns the enabled status for a dependency when `default-features = false`. ### Changed - `PackageMetadata::publish` now returns a new, more descriptive `PackagePublish` enum ([#320]). - `PackageMetadata::readme` now returns `&Utf8Path` rather than `&Path`. - `BuildTarget::path` now returns `&Utf8Path` rather than `&Path`. [#320]: https://github.com/facebookincubator/cargo-guppy/issues/320 ## [0.8.0] - 2021-02-23 ### Changed - `guppy` now uses [`camino`](https://crates.io/crates/camino) `Utf8Path` and `Utf8PathBuf` wrappers. These wrappers provide type-level assertions that returned paths are valid UTF-8. - Public dependency version bumps: - `proptest` updated to version 1 and the corresponding feature renamed to `proptest1`. ## [0.7.2] - 2021-02-15 ### Fixed - Restored compatibility with Rust 1.48. (1.48 is the MSRV, and is now tested in CI.) ## [0.7.1] - 2021-02-14 ### Changed - Packages within a cycle are now returned in non-dev order. When the direction is forward, if package Foo has a dependency on Bar, and Bar has a cyclic dev-dependency on Foo, then Foo is returned before Bar. (This is not a breaking change because it is an additional constraint on guppy itself, not on its consumers.) ## [0.7.0] - 2021-02-03 ### Added - `PackageSource` now has support for parsing external sources through a new `parse_external` method. - Cargo simulations have some new features: - New `CargoOptions::set_initials_platform` method can be used to simulate builds on exclusively the host platform. - `CargoSet::new` accepts an additional argument, `features_only`, which represents additional inputs that are only used for feature unification. This may be used to simulate, e.g. `cargo build --package foo --package bar`, when you only care about the results of `foo` but specifying `bar` influences the build. - New enum `graph::cargo::BuildPlatform` represents either the target platform or the host. New methods `CargoSet::platform_features` and `CargoSet::platform_direct_deps` accept the `BuildPlatform` enum. - `FeatureSet::contains_package` returns true if a feature set has at least one feature in the given package. - `semver::VersionReq` is now exposed in `guppy`. - `FeatureGraph::resolve_ids` resolves feature IDs into a `FeatureSet`. ### Changed - Feature filters `all_filter`, `default_filter` and `none_filter` have been combined into a single enum `StandardFeatures`. - Cargo builds are now done through `FeatureSet` instances, not `FeatureQuery`. This is because Cargo builds always happen in the forward direction. - `FeatureQuery::resolve_cargo` has been renamed to `FeatureSet::into_cargo_set`. - `CargoOptions::with_` methods have been renamed to begin with either `set_` or `add_`. - `Obs` is now a type rather than a trait. - `CargoOptions::set_proc_macros_on_target` was replaced with `InitialsPlatform::ProcMacrosOnTarget`. - Public dependency version bumps: - `semver` updated to 0.11. - `target-spec` updated to 0.6. ## [0.6.3] - 2021-01-11 ### Fixed - Fix an unintentional use of `serde`'s private exports. ## [0.6.2] - 2020-12-09 ### Fixed - `FeatureGraph::is_default_feature` no longer follows cross-package links. Cyclic dev-dependencies can enable non-default features (such as testing-only features), and previously `is_default_feature` would have returned true for such features. With this change, `is_default_feature` returns false for such features. The `default_filter` feature filter, which uses `is_default_feature`, has been fixed as well. ## [0.6.1] - 2020-12-02 This includes all the changes from version 0.6.0, plus a minor fix: ### Fixed - Removed "Usage" section from the README, the version number there keeps falling out of sync. ## [0.6.0] - 2020-12-02 (Version 0.6.0 wasn't released to crates.io.) ### Added - New feature `rayon1`, which introduces support for parallel iterators with [Rayon](https://github.com/rayon-rs/rayon). Currently, only a few workspace iterators are supported. More methods will be added as required (if you need something, please file an issue or open a PR!) - `PackageSet` and `FeatureSet` now have `PartialEq` and `Eq` implementations. - These implementations check for the graph being same through pointer equality. This means that sets that originate from different `PackageGraph` instances will always be unequal, even if they refer to the same packages. - Added `PackageSet::to_package_query` to convert a `PackageSet` to a `PackageQuery` starting from the same elements. ### Changed - Some methods have been renamed for greater fluency: - `FeatureGraph::query_packages` is now `PackageQuery::to_feature_query`. - `FeatureGraph::resolve_packages` is now `PackageSet::to_feature_set`. - The `semver` dependency has been updated to 0.11. ## [0.5.0] - 2020-06-20 This includes the changes in version 0.5.0-rc.1, plus: ### Added - Support for writing out _build summaries_ for `CargoSet` instances through the optional `summaries` feature. ### Changed - `target-spec` has been upgraded to 0.4. ### Fixed - `MetadataCommand::exec` and `build_graph` are now `&self`, not `&mut self`. ## [0.5.0-rc.1] - 2020-06-12 ### Added - `PackageGraph::query_workspace_paths` and `resolve_workspace_paths` provide convenient ways to create queries and package sets given a list of workspace paths. - `PackageMetadata::source` provides the source of a package (a local path, `crates.io`, a `git` repository or a custom registry). - `PackageQuery::initials` returns the initial set of packages specified in a package query. - `FeatureQuery::initials` returns the initial set of features specified in a feature query. - `FeatureQuery::initial_packages` returns the initial set of _packages_ specified in a feature query. - Improvements to Cargo resolution: - `CargoSet` now carries with it the original query and information about direct third-party dependencies. - A number of bug fixes around edge cases. - `Workspace::members_by_paths` and `Workspace::members_by_names` look up a list of workspace members by path or name, respectively. - `FeatureGraph::all_features_for` returns a list of all known features for a specified package. ### Changed - Lookup methods like `PackageGraph::metadata` now return `Result`s with errors instead of `Option`s. - `target-spec` has been upgraded to 0.3. - `proptest` has been upgraded to 0.10. The feature has accordingly been renamed to `proptest010`. - `Workspace::members` is now `Workspace::iter_by_path`, and `Workspace::members_by_name` is now `Workspace::iter_by_name`. ### Fixed - In `FeatureQuery<'g>` and `FeatureSet<'g>`, the lifetime parameter `'g` is now [covariant]. Compile-time assertions ensure that all lifetime parameters in `guppy` are covariant. [covariant]: https://github.com/sunshowers/lifetime-variance-example/blob/main/src/lib.rs ### Upcoming - Support for _build summaries_ is currently in an experimental state. ## [0.4.1] - 2020-05-07 This is a small followup release with some APIs that were meant to be added to 0.4.0. ### Added - `PackageGraph` now has some new `resolve_` methods: - `resolve_ids`: creates a `PackageSet` with the specified package IDs. - `resolve_workspace`: creates a `PackageSet` with all workspace packages (but no transitive dependencies). - `resolve_workspace_names`: creates a `PackageSet` with the specified workspace packages by name (but no transitive dependencies). ## [0.4.0] - 2020-05-06 This is a major overhaul of `guppy`, with many new features and several changed APIs. ### Added - Support for graph analysis on a per-feature basis. - The APIs are contained in `guppy::graph::feature`, and are accessible through `PackageGraph::feature_graph`. - An almost complete set of queries and operations is available through `FeatureQuery` and `FeatureSet`. - Support for simulating what packages and features would be built by Cargo. - The APIs are contained in `guppy::graph::cargo`, and are accessible by constructing a `FeatureQuery` and using its `resolve_cargo` method. - Both the current resolver and the upcoming [V2 resolver](https://github.com/rust-lang/cargo/pull/7820) are supported, and there are extensive property-based tests to ensure that `guppy` faithfully emulates `cargo`. - `PackageQuery` (and `FeatureQuery`) can now be introspected with new methods `direction` and `starts_from`. - `PackageMetadata` instances now have `has_build_script` and `is_proc_macro` methods. - Add `PackageGraph::query_workspace_names` to make a `PackageQuery` by workspace name. ### Changed - `PackageSet`'s consuming `into_` iterators have been turned into borrowing iterators. - `into_ids` is now `ids`, and `into_links` is now `links`. - Direct dependency and reverse dependency queries now live on `PackageMetadata` instances. - `PackageLink`, instead of having public `from`, `to` and `edge` fields, now has methods which return that data. - The functionality of `PackageEdge` has been subsumed into `PackageLink`. - The data model for platform-specific statuses has been overhauled. See `EnabledStatus`, `PlatformStatus` and `PlatformEval`. - `PackageResolver` (and `FeatureResolver`) improvements. - Resolver instances now have the query passed in, to make it easier to write stateless resolvers. - Resolver instances now take in `&mut self` instead of a plain `&self` (or `FnMut` instead of `Fn`). - `MetadataCommand` has been reimplemented in `guppy`, and now has a `build_graph` method. - `Metadata` has been reworked as well, and renamed to `CargoMetadata`. ### Removed - `PackageGraph::retain_edges` no longer exists: its functionality can be replicated through `PackageResolver`. ## [0.3.1] - 2020-04-15 ### Added - Support for listing and querying build targets (library, binaries, tests, etc) within a package. - `PackageMetadata::build_targets`: iterates over all build targets within a package. - `PackageMetadata::build_target`: retrieves a build target by identifier. ## [0.3.0] - 2020-04-14 This is a breaking release with some minor API changes. ### Added - `PackageGraph::directly_depends_on`: returns true if a package directly depends on another. - `Workspace` has new `member_by_name` and `members_by_name` methods for workspace lookups by name. ### Fixed - `guppy` now checks for duplicate names in workspaces and errors out if it finds any. ### Changed - `Workspace::members` and `Workspace::member_by_path` now return `PackageMetadata` instances, not `PackageId`. ## [0.2.1] - 2020-04-13 ### Fixed - Fixed a build issue on nightly Rust. ## [0.2.0] - 2020-04-13 This is a breaking release. There are no new or removed features, but many existing APIs have been cleaned up. ### Changed - The `select_` methods have been renamed to `query_`. - `PackageSelect` is now `PackageQuery`. - `select_all` is now `resolve_all` and directly produces a `PackageSet`. - `DependencyLink` is now `PackageLink`, and `DependencyEdge` is now `PackageEdge`. - `into_iter_links` is now `PackageSet::into_links`. - `PackageId` is now custom to `guppy` instead of reusing `cargo_metadata::PackageId`. - `PackageDotVisitor` now takes a `&mut DotWrite`. ### Removed - All previously deprecated methods have been cleaned up. ## [0.1.8] - 2020-04-08 ### Added - Implemented package resolution using custom resolvers, represented by the `PackageResolver` trait. - Added new APIs `PackageSelect::resolve_with` and `PackageSelect::resolve_with_fn`. - A `PackageResolver` provides fine-grained control over which links are followed. - It is equivalent to `PackageGraph::retain_edges`, but doesn't borrow mutably and is scoped to a single selector. - Added `PackageSet` to represent a set of known, resolved packages. - `PackageSet` comes with the standard set operations: `len`, `contains`, `union`, `intersection`, `difference` and `symmetric_difference`. - A `PackageSet` can also be iterated on in various ways, listed in the "Deprecated" section. ### Changed - Updated repository links. ### Deprecated - The following `into_` methods on `PackageSelect` have been deprecated and moved to `PackageSet`. - `select.into_iter_ids()` -> `select.resolve().into_ids()` - `select.into_iter_metadatas()` -> `select.resolve().into_metadatas()` - `select.into_root_ids()` -> `select.resolve().into_root_ids()` - `select.into_root_metadatas()` -> `select.resolve().into_root_metadatas()` ## [0.1.7] - 2020-04-05 ### Added - Support for [platform-specific dependencies](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#platform-specific-dependencies), including: - Querying whether a dependency is required or optional on the current platform, or on any other platform. - Evaluating which features are enabled on a platform. - Handling situations where the set of [target features](https://github.com/rust-lang/rfcs/blob/master/text/2045-target-feature.md) isn't known. ### Changed - Internal improvements -- `into_iter_ids` is a further 10-15% faster for large graphs. - Made several internal changes to prepare for feature graph support, coming soon. - Sped up build times by removing some dependencies. ### Deprecated - As part of support for platform-specific dependencies, `DependencyMetadata::target` has been replaced with the `_on` methods. - For example, to figure out if a dependency is enabled on a platform, use the `enabled_on` method. ## [0.1.6] - 2020-03-11 ### Fixed - Handle cyclic dev-dependencies properly. Previously, `guppy` could produce incomplete results if it encountered cycles. ### Changed - As a result of algorithmic improvements to handle cycles, `into_iter_ids` is now around 60% faster for large graphs. ## [0.1.5] - 2020-03-06 ### Fixed - Fix a bug involving situations where different dependency sections depend on the same package with different versions: ```toml [dependencies] lazy_static = "1" [dev-dependencies] lazy_static = "0.2" ``` ## [0.1.4] - 2020-01-26 ### Added - New selector `select_workspace` to select packages that are part of the workspace and all their transitive dependencies. In general, `select_workspace` is preferable over `select_all`. ### Fixed - Fixed a bug in `into_root_ids` and `into_root_metadatas` that would cause it to return packages that aren't roots of another package. ### Changed - Internal upgrades to prepare for upcoming feature graph analysis. ## [0.1.3] - 2019-12-29 ### Added - `PackageSelect::into_root_metadatas` returns package metadatas for all roots within a selection. - New optional feature `proptest010` to help with property testing. ### Changed - Upgrade to `petgraph` 0.5 -- this allows for some internal code to be simplified. ### Deprecated - Package selectors have been renamed. The old names will continue to work for the 0.1 series, but will be removed in the 0.2 series. - `select_transitive_deps` → `select_forward` - `select_reverse_transitive_deps` → `select_reverse` - `select_transitive_deps_directed` → `select_directed` ## [0.1.2] - 2019-11-26 ### Fixed - Fixed the return type of `into_root_ids` to be `impl Iterator` instead of `impl IntoIterator`. ## [0.1.1] - 2019-11-22 ### Fixed - Fixed a publishing issue with version 0.1.0. ## [0.1.0] - 2019-11-22 ### Added - Initial release. [0.17.25]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.17.25 [0.17.24]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.17.24 [0.17.23]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.17.23 [0.17.22]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.17.22 [0.17.21]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.17.21 [0.17.20]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.17.20 [0.17.19]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.17.19 [0.17.18]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.17.18 [0.17.17]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.17.17 [0.17.16]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.17.16 [0.17.15]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.17.15 [0.17.14]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.17.14 [0.17.13]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.17.13 [0.17.12]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.17.12 [0.17.11]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.17.11 [0.17.10]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.17.10 [0.17.9]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.17.9 [0.17.8]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.17.8 [0.17.7]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.17.7 [0.17.6]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.17.6 [0.17.5]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.17.5 [0.17.4]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.17.4 [0.17.3]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.17.3 [0.17.2]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.17.2 [0.17.1]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.17.1 [0.17.0]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.17.0 [0.16.0]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.16.0 [0.15.2]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.15.2 [0.15.1]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.15.1 [0.15.0]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.15.0 [0.14.4]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.14.4 [0.14.3]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.14.3 [0.14.2]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.14.2 [0.14.1]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.14.1 [0.14.0]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.14.0 [0.13.0]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.13.0 [0.12.6]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.12.6 [0.12.5]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.12.5 [0.12.4]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.12.4 [0.12.3]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.12.3 [0.12.2]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.12.2 [0.12.1]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.12.1 [0.12.0]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.12.0 [0.11.3]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.11.3 [0.11.2]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.11.2 [0.11.1]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.11.1 [0.10.1]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.10.1 [0.10.0]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.10.0 [0.9.0]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.9.0 [0.8.0]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.8.0 [0.7.2]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.7.2 [0.7.1]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.7.1 [0.7.0]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.7.0 [0.6.3]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.6.3 [0.6.2]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.6.2 [0.6.1]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.6.1 [0.6.0]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.6.0 [0.5.0]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.5.0 [0.5.0-rc.1]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.5.0-rc.1 [0.4.1]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.4.1 [0.4.0]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.4.0 [0.3.1]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.3.1 [0.3.0]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.3.0 [0.2.1]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.2.1 [0.2.0]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.2.0 [0.1.8]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.1.8 [0.1.7]: https://github.com/guppy-rs/guppy/releases/tag/guppy-0.1.7 [0.1.6]: https://github.com/guppy-rs/guppy/releases/tag/0.1.6 [0.1.5]: https://github.com/guppy-rs/guppy/releases/tag/0.1.5 [0.1.4]: https://github.com/guppy-rs/guppy/releases/tag/0.1.4 [0.1.3]: https://github.com/guppy-rs/guppy/releases/tag/0.1.3 [0.1.2]: https://github.com/guppy-rs/guppy/releases/tag/0.1.2 [0.1.1]: https://github.com/guppy-rs/guppy/releases/tag/0.1.1 [0.1.0]: https://github.com/guppy-rs/guppy/releases/tag/0.1.0 guppy-0.17.25/Cargo.lock0000644000000533261046102023000103620ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "ahash" version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", "getrandom", "once_cell", "version_check", "zerocopy", ] [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bit-set" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "camino" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" dependencies = [ "serde_core", ] [[package]] name = "cargo-platform" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8abf5d501fd757c2d2ee78d0cc40f606e92e3a63544420316565556ed28485e2" dependencies = [ "serde", ] [[package]] name = "cargo_metadata" version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9" dependencies = [ "camino", "cargo-platform", "semver", "serde", "serde_json", "thiserror", ] [[package]] name = "cfg-expr" version = "0.20.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78cef5b5a1a6827c7322ae2a636368a573006b27cfa76c7ebd53e834daeaab6a" dependencies = [ "smallvec", "target-lexicon", ] [[package]] name = "cfg-if" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "crossbeam-deque" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "debug-ignore" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffe7ed1d93f4553003e20b629abe9085e1e81b1429520f897f8f8860bc6dfc21" [[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "diffus" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c0ff24a73b51d9009c40897faf87d31b77345c90ffbf4dc3a1d2957032c5653" dependencies = [ "itertools 0.10.5", ] [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", "windows-sys 0.59.0", ] [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fixedbitset" version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "getrandom" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", "r-efi", "wasi", ] [[package]] name = "guppy" version = "0.17.25" dependencies = [ "ahash", "camino", "cargo_metadata", "cfg-if", "debug-ignore", "fixedbitset", "guppy-summaries", "guppy-workspace-hack", "indexmap 2.13.0", "itertools 0.14.0", "nested", "once_cell", "pathdiff", "petgraph", "pretty_assertions", "proptest", "proptest-derive", "rayon", "semver", "serde", "serde_json", "smallvec", "static_assertions", "target-spec", "toml", ] [[package]] name = "guppy-summaries" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bd039b8f587513b48754811cfa37c2ba079df537b490b602fa641ce18f6e72a" dependencies = [ "camino", "cfg-if", "diffus", "guppy-workspace-hack", "semver", "serde", "toml", ] [[package]] name = "guppy-workspace-hack" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92620684d99f750bae383ecb3be3748142d6095760afd5cbcf2261e9a279d780" [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ "foldhash", ] [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "indexmap" version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", ] [[package]] name = "indexmap" version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", ] [[package]] name = "itertools" version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] [[package]] name = "itertools" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "linux-raw-sys" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "nested" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b420f638f07fe83056b55ea190bb815f609ec5a35e7017884a10f78839c9e" [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "pathdiff" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" dependencies = [ "camino", ] [[package]] name = "petgraph" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset", "hashbrown 0.15.4", "indexmap 2.13.0", ] [[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "pretty_assertions" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" dependencies = [ "diff", "yansi", ] [[package]] name = "proc-macro2" version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] name = "proptest" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fcdab19deb5195a31cf7726a210015ff1496ba1464fd42cb4f537b8b01b471f" dependencies = [ "bit-set", "bit-vec", "bitflags", "lazy_static", "num-traits", "rand", "rand_chacha", "rand_xorshift", "regex-syntax", "rusty-fork", "tempfile", "unarray", ] [[package]] name = "proptest-derive" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "095a99f75c69734802359b682be8daaf8980296731f6470434ea2c652af1dd30" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "quick-error" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ "getrandom", ] [[package]] name = "rand_xorshift" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" dependencies = [ "rand_core", ] [[package]] name = "rayon" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustix" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.61.2", ] [[package]] name = "rusty-fork" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" dependencies = [ "fnv", "quick-error", "tempfile", "wait-timeout", ] [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" dependencies = [ "serde", "serde_core", ] [[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", ] [[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", "serde_core", ] [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "target-lexicon" version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" [[package]] name = "target-spec" version = "3.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "585c173ce474b6257cfb2a107949e48eb1ab9cae21cecbdf13401ae3be4a411a" dependencies = [ "cfg-expr", "guppy-workspace-hack", "proptest", "serde", "target-lexicon", ] [[package]] name = "tempfile" version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", "getrandom", "once_cell", "rustix", "windows-sys 0.61.2", ] [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "toml" version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" dependencies = [ "indexmap 1.9.3", "serde", ] [[package]] name = "unarray" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wait-timeout" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" dependencies = [ "libc", ] [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets", ] [[package]] name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "wit-bindgen-rt" version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags", ] [[package]] name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "zerocopy" version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", "syn", ] guppy-0.17.25/Cargo.toml0000644000000066131046102023000104020ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2024" rust-version = "1.86" name = "guppy" version = "0.17.25" authors = [ "Rain ", "Brandon Williams ", ] build = false exclude = ["README.tpl"] autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Track and query Cargo dependency graphs." documentation = "https://docs.rs/guppy" readme = "README.md" keywords = [ "cargo", "dependencies", "graph", "guppy", ] categories = [ "config", "data-structures", "development-tools", "parser-implementations", ] license = "MIT OR Apache-2.0" repository = "https://github.com/guppy-rs/guppy" resolver = "2" [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg=doc_cfg"] [badges.maintenance] status = "actively-developed" [features] proptest1 = [ "proptest", "proptest-derive", "target-spec/proptest1", ] rayon1 = ["rayon"] summaries = [ "guppy-summaries", "target-spec/summaries", "toml", ] [lib] name = "guppy" path = "src/lib.rs" [[example]] name = "cargo_set_link_filter" path = "examples/cargo_set_link_filter.rs" [[example]] name = "deps" path = "examples/deps.rs" [[example]] name = "print_by_level" path = "examples/print_by_level.rs" [[example]] name = "print_dot" path = "examples/print_dot.rs" [[example]] name = "remove_dev_only" path = "examples/remove_dev_only.rs" [[example]] name = "topo_sort" path = "examples/topo_sort.rs" [[test]] name = "graph-tests" path = "tests/graph-tests/main.rs" [dependencies.ahash] version = "0.8.12" [dependencies.camino] version = "1.2.1" [dependencies.cargo_metadata] version = "0.23.1" [dependencies.cfg-if] version = "1.0.3" [dependencies.debug-ignore] version = "1.0.5" [dependencies.fixedbitset] version = "0.5.7" default-features = false [dependencies.guppy-summaries] version = "0.7.1" optional = true [dependencies.guppy-workspace-hack] version = "0.1.0" [dependencies.indexmap] version = "2.11.4" [dependencies.itertools] version = "0.14.0" [dependencies.nested] version = "0.1.1" [dependencies.once_cell] version = "1.21.3" [dependencies.pathdiff] version = "0.2.3" features = ["camino"] [dependencies.petgraph] version = "0.8.3" default-features = false [dependencies.proptest] version = "1.7.0" optional = true [dependencies.proptest-derive] version = "0.6.0" optional = true [dependencies.rayon] version = "1.10.0" optional = true [dependencies.semver] version = "1.0.27" [dependencies.serde] version = "1.0.228" features = ["derive"] [dependencies.serde_json] version = "1.0.145" [dependencies.smallvec] version = "1.15.1" [dependencies.static_assertions] version = "1.1.0" [dependencies.target-spec] version = "3.5.7" [dependencies.toml] version = "0.5.11" features = ["preserve_order"] optional = true [dev-dependencies.pretty_assertions] version = "1.4.1" [lints.rust.unexpected_cfgs] level = "warn" priority = 0 check-cfg = [ "cfg(doc_cfg)", "cfg(guppy_nightly)", ] guppy-0.17.25/Cargo.toml.orig000064400000000000000000000036631046102023000140430ustar 00000000000000[package] name = "guppy" version = "0.17.25" description = "Track and query Cargo dependency graphs." documentation = "https://docs.rs/guppy" repository = "https://github.com/guppy-rs/guppy" authors = ["Rain ", "Brandon Williams "] license = "MIT OR Apache-2.0" readme = "README.md" keywords = ["cargo", "dependencies", "graph", "guppy"] categories = [ "config", "data-structures", "development-tools", "parser-implementations", ] edition = "2024" exclude = [ # Readme template that doesn't need to be included. "README.tpl", ] rust-version.workspace = true [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg=doc_cfg"] [badges] maintenance = { status = "actively-developed" } [dependencies] ahash.workspace = true guppy-workspace-hack.workspace = true camino = "1.2.1" cargo_metadata.workspace = true cfg-if = "1.0.3" debug-ignore = "1.0.5" guppy-summaries = { version = "0.7.1", path = "../guppy-summaries", optional = true } fixedbitset = { version = "0.5.7", default-features = false } nested = "0.1.1" indexmap = "2.11.4" itertools = "0.14.0" once_cell = "1.21.3" pathdiff = { version = "0.2.3", features = ["camino"] } petgraph = { version = "0.8.3", default-features = false } proptest = { version = "1.7.0", optional = true } proptest-derive = { version = "0.6.0", optional = true } rayon = { version = "1.10.0", optional = true } semver = "1.0.27" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.145" smallvec = "1.15.1" static_assertions = "1.1.0" target-spec = { version = "3.5.7", path = "../target-spec" } toml = { version = "0.5.11", optional = true, features = ["preserve_order"] } [dev-dependencies] fixtures = { path = "../fixtures" } pretty_assertions = "1.4.1" [features] proptest1 = ["proptest", "proptest-derive", "target-spec/proptest1"] rayon1 = ["rayon"] summaries = ["guppy-summaries", "target-spec/summaries", "toml"] [lints] workspace = true guppy-0.17.25/README.md000064400000000000000000000120611046102023000124230ustar 00000000000000# guppy [![guppy on crates.io](https://img.shields.io/crates/v/guppy)](https://crates.io/crates/guppy) [![Documentation (latest release)](https://docs.rs/guppy/badge.svg)](https://docs.rs/guppy/) [![Documentation (main)](https://img.shields.io/badge/docs-main-brightgreen)](https://guppy-rs.github.io/guppy/rustdoc/guppy/) [![License](https://img.shields.io/badge/license-Apache-green.svg)](../LICENSE-APACHE) [![License](https://img.shields.io/badge/license-MIT-green.svg)](../LICENSE-MIT) Track and query Cargo dependency graphs. `guppy` provides a Rust interface to run queries over Cargo dependency graphs. `guppy` parses the output of [`cargo metadata`](https://doc.rust-lang.org/cargo/commands/cargo-metadata.html), then presents a graph interface over it. ## Types and lifetimes The central structure exposed by `guppy` is [`PackageGraph`](crate::graph::PackageGraph). This represents a directed (though [not necessarily acyclic](crate::graph::Cycles)) graph where every node is a package and every edge represents a dependency. Other types borrow data from a `PackageGraph` and have a `'g` lifetime parameter indicating that. A lifetime parameter named `'g` always indicates that data is borrowed from a `PackageGraph`. [`PackageMetadata`](crate::graph::PackageMetadata) contains information about individual packages, such as the data in [the `package` section](https://doc.rust-lang.org/cargo/reference/manifest.html#the-package-section). For traversing the graph, `guppy` provides a few types: * [`PackageLink`](crate::graph::PackageLink) represents both ends of a dependency edge, along with details about the dependency (whether it is dev-only, platform-specific, and so on). * [`PackageQuery`](crate::graph::PackageQuery) represents the input parameters to a dependency traversal: a set of packages and a direction. A traversal is performed with [`PackageQuery::resolve`](crate::graph::PackageQuery::resolve), and fine-grained control over the traversal is achieved with [`PackageQuery::resolve_with_fn`](crate::graph::PackageQuery::resolve_with_fn). * [`PackageSet`](crate::graph::PackageSet) represents the result of a graph traversal. This struct provides several methods to iterate over packages. For some operations, `guppy` builds an auxiliary [`FeatureGraph`](crate::graph::feature::FeatureGraph) the first time it is required. Every node in a `FeatureGraph` is a combination of a package and a feature declared in it, and every edge is a feature dependency. For traversing the feature graph, `guppy` provides the analogous [`FeatureQuery`](crate::graph::feature::FeatureQuery) and [`FeatureSet`](crate::graph::feature::FeatureSet) types. `FeatureSet` also has an [`into_cargo_set`](crate::graph::feature::FeatureSet::into_cargo_set) method, to simulate Cargo builds. This method produces a [`CargoSet`](crate::graph::cargo::CargoSet), which is essentially two `FeatureSet`s along with some more useful information. `guppy`'s data structures are immutable, with some internal caches. All of `guppy`'s types are `Send + Sync`, and all lifetime parameters are [covariant](https://github.com/sunshowers/lifetime-variance-example/). ## Optional features * `proptest1`: Support for [property-based testing](https://jessitron.com/2013/04/25/property-based-testing-what-is-it/) using the [`proptest`](https://altsysrq.github.io/proptest-book/intro.html) framework. * `rayon1`: Support for parallel iterators through [Rayon](docs.rs/rayon/1) (preliminary work so far, more parallel iterators to be added in the future). * `summaries`: Support for writing out [build summaries](https://github.com/guppy-rs/guppy/tree/main/guppy-summaries). ## Examples Print out all direct dependencies of a package: ```rust use guppy::{CargoMetadata, PackageId}; // `guppy` accepts `cargo metadata` JSON output. Use a pre-existing fixture for these examples. let metadata = CargoMetadata::parse_json(include_str!("../../fixtures/small/metadata1.json")).unwrap(); let package_graph = metadata.build_graph().unwrap(); // `guppy` provides several ways to get hold of package IDs. Use a pre-defined one for this // example. let package_id = PackageId::new("testcrate 0.1.0 (path+file:///fakepath/testcrate)"); // The `metadata` method returns information about the package, or `None` if the package ID // wasn't recognized. let package = package_graph.metadata(&package_id).unwrap(); // `direct_links` returns all direct dependencies of a package. for link in package.direct_links() { // A dependency link contains `from()`, `to()` and information about the specifics of the // dependency. println!("direct dependency: {}", link.to().id()); } ``` For more examples, see [the `examples` directory](https://github.com/guppy-rs/guppy/tree/main/guppy/examples). ## Contributing See the [CONTRIBUTING](../CONTRIBUTING.md) file for how to help out. ## License This project is available under the terms of either the [Apache 2.0 license](../LICENSE-APACHE) or the [MIT license](../LICENSE-MIT). guppy-0.17.25/examples/cargo_set_link_filter.rs000064400000000000000000000170331046102023000176640ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 //! Demonstration how `CargoSet` algorithm can accept links that are present on: //! //! 1) any/all platforms (using default `CargoOptions` and `CargoSet::new`) //! 2) a single platform (using `CargoOptions::set_target_platform`) //! 3) a set of platforms (using `CargoSet::with_resolver`) //! //! The last example uses `PackageResolver` as a filter - this is a very //! generic mechanism and can be used to not only filter by platforms, //! but also implement a variety of other filtering options. For example, //! `CargoOptions::add_omitted_packages` could also be implemented using //! `CargoSet::with_resolver` and an appropriate `PackageResolver`. use guppy::{ CargoMetadata, Error, graph::{ DependencyDirection, DependencyReq, PackageLink, PackageQuery, PackageResolver, cargo::{CargoOptions, CargoSet}, feature::StandardFeatures, }, platform::{EnabledTernary, PlatformSpec, PlatformStatus, Triple}, }; /// Custom `guppy::graph::PackageResolver` that will only accept `PackageLink`s /// that are enabled on at least one from the given set of platforms. struct PackageResolverForPlatformSet(Vec); impl PackageResolverForPlatformSet { fn new(platform_set: Vec) -> Self { Self(platform_set) } fn can_platform_status_be_true(&self, platform_status: PlatformStatus) -> bool { self.0.iter().any(|platform_spec| { let is_enabled = platform_status.enabled_on(platform_spec); is_enabled == EnabledTernary::Enabled }) } fn should_include_dependency_req(&self, dependency_req: DependencyReq) -> bool { dependency_req.is_present() && (self.can_platform_status_be_true(dependency_req.status().optional_status()) || self.can_platform_status_be_true(dependency_req.status().required_status())) } } impl<'g> PackageResolver<'g> for PackageResolverForPlatformSet { fn accept(&mut self, _query: &PackageQuery<'g>, link: PackageLink<'g>) -> bool { self.should_include_dependency_req(link.normal()) || self.should_include_dependency_req(link.build()) } } fn win32_platform_spec() -> PlatformSpec { PlatformSpec::Platform(std::sync::Arc::new(target_spec::Platform::from_triple( Triple::new_strict("i686-pc-windows-gnu").unwrap(), target_spec::TargetFeatures::features([ // The full feature list for this target triple can be found with // `rustc --target i686-pc-windows-gnu --print=cfg`, but for // simplicity we only list ones that are relevant for `region` and // `winapi` dependency selection). "windows", ]), ))) } fn win64_platform_spec() -> PlatformSpec { PlatformSpec::Platform(std::sync::Arc::new(target_spec::Platform::from_triple( Triple::new_strict("x86_64-pc-windows-gnu").unwrap(), target_spec::TargetFeatures::features([ // The full feature list for this target triple can be found with // `rustc --target x86_64-pc-windows-gnu --print=cfg`, but for // simplicity we only list ones that are relevant for `region` and // `winapi` dependency selection). "windows", ]), ))) } fn cargo_set_to_package_names(cargo_set: CargoSet) -> Vec { let mut result = cargo_set .target_features() .packages_with_features(DependencyDirection::Forward) .map(|feature_list| { format!( "{}-{}", feature_list.package().name(), feature_list.package().version(), ) }) .collect::>(); result.sort(); result } fn main() -> Result<(), Error> { // `guppy` accepts as input the JSON output from `cargo metadata`. // In this example we use a pre-recorded metadata that has been stored in `metadata1.json`. // In this example metadata: // // * The `winapi` crate depends on either `winapi-i686-pc-windows-gnu` or // `winapi-x86_64-pc-windows-gnu` crate: // // ``` // [target.i686-pc-windows-gnu.dependencies.winapi-i686-pc-windows-gnu] // version = "0.4" // [target.x86_64-pc-windows-gnu.dependencies.winapi-x86_64-pc-windows-gnu] // version = "0.4" // ``` // // * The `region-2.1.2` package depends on either `mach` or `winapi`: // // ``` // [target."cfg(any(target_os = \"macos\", target_os = \"ios\"))".dependencies.mach] // version = "0.2" // [target."cfg(windows)".dependencies.winapi] // version = "0.3" // ``` let metadata = CargoMetadata::parse_json(include_str!("../../fixtures/small/metadata1.json"))?; let package_graph = metadata.build_graph()?; let initials = package_graph .resolve_package_name("region") .to_feature_set(StandardFeatures::Default); let no_extra_features = package_graph .resolve_none() .to_feature_set(StandardFeatures::Default); // First, we get a set of packages that will be built on **any/all** possible platforms. let all_platforms_package_names = { let cargo_options = CargoOptions::new(); let cargo_set = CargoSet::new(initials.clone(), no_extra_features.clone(), &cargo_options)?; cargo_set_to_package_names(cargo_set) }; assert_eq!( all_platforms_package_names, vec![ "bitflags-1.1.0", "libc-0.2.62", "mach-0.2.3", "region-2.1.2", "winapi-0.3.8", "winapi-i686-pc-windows-gnu-0.4.0", "winapi-x86_64-pc-windows-gnu-0.4.0" ], ); // Then, get a set of packages for a **single** platform (by using // `CargoOptions.set_target_platform`). let single_platform_package_names = { let mut cargo_options = CargoOptions::new(); cargo_options.set_target_platform(win32_platform_spec()); let cargo_set = CargoSet::new(initials.clone(), no_extra_features.clone(), &cargo_options)?; cargo_set_to_package_names(cargo_set) }; assert_eq!( single_platform_package_names, vec![ "bitflags-1.1.0", "libc-0.2.62", // No "mach-0.2.3" on `win32_platform_spec`. "region-2.1.2", "winapi-0.3.8", // No "winapi-x86_64-pc-windows-gnu-0.4.0" on `win32_platform_spec`. "winapi-i686-pc-windows-gnu-0.4.0", ], ); // Finally, get a set of packages for a set of target platforms // (by passing a custom resolver to `CargoSet::with_resolver`). let platform_set_package_names = { let cargo_options = CargoOptions::new(); let resolver = PackageResolverForPlatformSet::new(vec![win32_platform_spec(), win64_platform_spec()]); let cargo_set = CargoSet::with_package_resolver(initials, no_extra_features, resolver, &cargo_options)?; cargo_set_to_package_names(cargo_set) }; assert_eq!( platform_set_package_names, vec![ "bitflags-1.1.0", "libc-0.2.62", // No "mach-0.2.3" in a union of `win32_platform_spec` and `win64_platform_spec`. "region-2.1.2", "winapi-0.3.8", // Both `winapi-i686-...` and `winapi-x86_64-...` crates are present in // a union of `win32_platform_spec` and `win64_platform_spec`. "winapi-i686-pc-windows-gnu-0.4.0", "winapi-x86_64-pc-windows-gnu-0.4.0" ], ); Ok(()) } guppy-0.17.25/examples/deps.rs000064400000000000000000000032401046102023000142620ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 //! Print out direct and transitive dependencies of a package. use guppy::{CargoMetadata, Error, PackageId, graph::DependencyDirection}; use std::iter; fn main() -> Result<(), Error> { // `guppy` accepts `cargo metadata` JSON output. Use a pre-existing fixture for these examples. let metadata = CargoMetadata::parse_json(include_str!("../../fixtures/small/metadata1.json"))?; let package_graph = metadata.build_graph()?; // `guppy` provides several ways to get hold of package IDs. Use a pre-defined one for this // example. let package_id = PackageId::new("testcrate 0.1.0 (path+file:///fakepath/testcrate)"); // The `metadata` method returns information about the package, or `None` if the package ID // wasn't recognized. let package = package_graph.metadata(&package_id).unwrap(); // `direct_links` returns all direct dependencies of a package. for link in package.direct_links() { // A dependency link contains `from`, `to` and `edge`. The edge has information about e.g. // whether this is a build dependency. println!("direct: {}", link.to().id()); } // Transitive dependencies are obtained through the `query_` APIs. They are always presented in // topological order. let query = package_graph.query_forward(iter::once(&package_id))?; let package_set = query.resolve(); for dep_id in package_set.package_ids(DependencyDirection::Forward) { // PackageSet also has an `links()` method which returns links instead of package IDs. println!("transitive: {dep_id}"); } Ok(()) } guppy-0.17.25/examples/print_by_level.rs000064400000000000000000000047311046102023000163520ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 //! Print out dependencies of a graph, level by level. //! //! This example will print out duplicate dependencies if they show up at multiple levels. If you //! don't want that, you can maintain a 'seen' set. use guppy::{CargoMetadata, Error}; use std::{ collections::BTreeMap, io::{Write, stdout}, }; fn main() -> Result<(), Error> { // `guppy` accepts `cargo metadata` JSON output. Use a pre-existing fixture for these examples. let metadata = CargoMetadata::parse_json(include_str!("../../fixtures/large/metadata_libra.json"))?; let package_graph = metadata.build_graph()?; // Pick an interesting package to compute dependencies of. let vm_metadata = package_graph .workspace() .member_by_path("language/vm/vm-runtime") .expect("known workspace path"); // Use a BTreeMap to deduplicate dependencies within a level below while keeping package IDs // ordered. let mut current = BTreeMap::new(); current.insert(vm_metadata.id(), vm_metadata); let mut level = 0; // One could use println! directly at the cost of a lot of unnecessary lock and unlock actions. // Grabbing the lock once is more efficient. let stdout = stdout(); let mut f = stdout.lock(); // Keep iterating over package IDs until no more remain. while !current.is_empty() { // Print out details for this level. writeln!(f, "level {level}:").unwrap(); for (id, metadata) in ¤t { writeln!(f, "* {}: {}", id, metadata.name()).unwrap(); } writeln!(f).unwrap(); level += 1; // Compute the package IDs in the next level. let next: BTreeMap<_, _> = current .into_iter() .flat_map(|(id, _)| { // This is a flat_map because each package in current has multiple dependencies, and // we want to collect all of them together. let links = package_graph.metadata(id).expect("valid ID").direct_links(); links.map(|link| { let to = link.to(); // Since we're iterating over transitive dependencies, we use the 'to' ID here. // If we were iterating over transitive reverse deps, we'd use the 'from' ID. (to.id(), to) }) }) .collect(); current = next; } Ok(()) } guppy-0.17.25/examples/print_dot.rs000064400000000000000000000042611046102023000153350ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 //! Print out a dot representation of a subgraph, for formatting with graphviz. //! //! This example prints out a dot representation of the dependencies between all packages in a //! workspace. It skips over any non-workspace packages. //! //! Try running this example with graphviz: //! //! ```text //! cargo run --example print_dot > graph.dot //! dot -Tpng graph.dot -o graph.png //! ``` use guppy::{ CargoMetadata, Error, graph::{DotWrite, PackageDotVisitor, PackageLink, PackageMetadata}, }; use std::fmt; // Define a visitor, which specifies what strings to print out for the graph. struct PackageNameVisitor; impl PackageDotVisitor for PackageNameVisitor { fn visit_package(&self, package: PackageMetadata<'_>, f: &mut DotWrite<'_, '_>) -> fmt::Result { // Print out the name of the package. Other metadata can also be printed out. // // If you need to look at data for other packages, store a reference to the PackageGraph in // the visitor. write!(f, "{}", package.name()) } fn visit_link(&self, link: PackageLink<'_>, f: &mut DotWrite<'_, '_>) -> fmt::Result { if link.dev_only() { write!(f, "dev-only") } else { // Don't print out anything if this isn't a dev-only link. Ok(()) } } } fn main() -> Result<(), Error> { // `guppy` accepts `cargo metadata` JSON output. Use a pre-existing fixture for these examples. let metadata = CargoMetadata::parse_json(include_str!("../../fixtures/large/metadata_libra.json"))?; let package_graph = metadata.build_graph()?; // Non-workspace packages cannot depend on packages within the workspace, so the reverse // transitive deps of workspace packages are exactly the set of workspace packages. let query = package_graph.query_reverse(package_graph.workspace().member_ids())?; let package_set = query.resolve(); // resolve.display_dot() implements `std::fmt::Display`, so it can be written out to a file, a // string, stdout, etc. println!("{}", package_set.display_dot(PackageNameVisitor)); Ok(()) } guppy-0.17.25/examples/remove_dev_only.rs000064400000000000000000000044371046102023000165340ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 //! Remove all dependency links that are dev-only. //! //! Dev-only dependencies are typically not included in release builds, so it's useful to be able //! to filter out those links. use guppy::{ CargoMetadata, Error, graph::{DependencyDirection, PackageLink}, }; use std::iter; fn main() -> Result<(), Error> { // `guppy` accepts `cargo metadata` JSON output. Use a pre-existing fixture for these examples. let metadata = CargoMetadata::parse_json(include_str!("../../fixtures/large/metadata_libra.json"))?; let package_graph = metadata.build_graph()?; // Pick an important binary package and compute the number of dependencies. // // A clone is typically not required but in this case we're mutating the graph, so we need to // release the immutatable borrow. let libra_node_id = package_graph .workspace() .member_by_path("libra-node") .unwrap() .id() .clone(); let before_count = package_graph .query_forward(iter::once(&libra_node_id))? .resolve() .package_ids(DependencyDirection::Forward) .count(); println!("number of packages before: {before_count}"); let resolver_fn = |link: PackageLink<'_>| { if link.dev_only() { println!( "*** filtering out dev-only link: {} -> {}", link.from().name(), link.to().name() ); return false; } true }; let query = package_graph.query_forward(iter::once(&libra_node_id))?; // Use `resolve_with` to filter out dev-only links. let resolve_with_len = query .clone() .resolve_with_fn(|_query, link| { // A package resolver allows for fine-grained control over which links are followed. In general, // it is anything that implements the `PackageResolver` trait. // // Functions with signature FnMut(&PackageQuery<'_>, PackageLink<'_>) -> bool can be // used with `resolve_with_fn`. resolver_fn(link) }) .package_ids(DependencyDirection::Forward) .len(); println!("number of packages after: {resolve_with_len}"); Ok(()) } guppy-0.17.25/examples/topo_sort.rs000064400000000000000000000023401046102023000153570ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 //! Print out crates in a workspace in topological order. //! //! The into_iter_ids and into_iter_metadatas iterators return packages in topological order. Note //! that into_iter_links returns links in "link order" -- see its documentation for more. use guppy::{CargoMetadata, Error, graph::DependencyDirection}; fn main() -> Result<(), Error> { // `guppy` accepts `cargo metadata` JSON output. Use a pre-existing fixture for these examples. let metadata = CargoMetadata::parse_json(include_str!("../../fixtures/large/metadata_libra.json"))?; let package_graph = metadata.build_graph()?; // This produces the set of packages in this workspace. let workspace_set = package_graph.resolve_workspace(); // Iterate over packages in forward topo order. for package in workspace_set.packages(DependencyDirection::Forward) { // All selected packages are in the workspace. let workspace_path = package .source() .workspace_path() .expect("packages in workspace should have workspace path"); println!("{}: {}", package.name(), workspace_path); } Ok(()) } guppy-0.17.25/proptest-regressions/petgraph_support/topo.txt000064400000000000000000000007061046102023000225200ustar 00000000000000# Seeds for failure cases proptest has generated in the past. It is # automatically read and these particular cases re-run before any # novel cases are generated. # # It is recommended to check this file in to source control so that # everyone who runs the test benefits from these saved cases. cc db330d5785485e1f4da6fd06b7225b11ddaff2fa0c16555102f3372d24d377f9 # shrinks to graph = Graph { Ty: "Directed", node_count: 2, edge_count: 1, edges: (0, 0) } guppy-0.17.25/src/debug_ignore.rs000064400000000000000000000016061046102023000147350ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 //! Contains `DebugIgnore`, a newtype wrapper that causes a field to be ignored while printing //! out `Debug` output. use std::{ fmt, ops::{Deref, DerefMut}, }; /// A newtype wrapper that causes this field to be ignored while printing out `Debug` output. /// /// Similar to `#[derivative(ignore)]`, but avoids an extra dependency. #[derive(Copy, Clone, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct DebugIgnore(pub T); impl Deref for DebugIgnore { type Target = T; fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for DebugIgnore { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } impl fmt::Debug for DebugIgnore { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "...") } } guppy-0.17.25/src/dependency_kind.rs000064400000000000000000000025741046102023000154340ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use std::fmt; /// A descriptor for the kind of dependency. /// /// Cargo dependencies may be one of three kinds. #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] pub enum DependencyKind { /// Normal dependencies. /// /// These are specified in the `[dependencies]` section. Normal, /// Dependencies used for development only. /// /// These are specified in the `[dev-dependencies]` section, and are used for tests, /// benchmarks and similar. Development, /// Dependencies used for build scripts. /// /// These are specified in the `[build-dependencies]` section. Build, } impl DependencyKind { /// A list of all the possible values of `DependencyKind`. pub const VALUES: &'static [Self; 3] = &[ DependencyKind::Normal, DependencyKind::Development, DependencyKind::Build, ]; /// Returns a string representing the kind of dependency this is. pub fn to_str(self) -> &'static str { match self { DependencyKind::Normal => "normal", DependencyKind::Development => "dev", DependencyKind::Build => "build", } } } impl fmt::Display for DependencyKind { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.to_str()) } } guppy-0.17.25/src/errors.rs000064400000000000000000000244221046102023000136210ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 //! Contains types that describe errors and warnings that `guppy` methods can return. use crate::{PackageId, graph::feature::FeatureId}; use Error::*; use camino::Utf8PathBuf; use std::{error, fmt}; pub use target_spec::Error as TargetSpecError; /// Error type describing the sorts of errors `guppy` can return. #[derive(Debug)] #[non_exhaustive] pub enum Error { /// An error occurred while executing `cargo metadata`. CommandError(Box), /// An error occurred while parsing `cargo metadata` JSON. MetadataParseError(serde_json::Error), /// An error occurred while serializing `cargo metadata` JSON. MetadataSerializeError(serde_json::Error), /// An error occurred while constructing a `PackageGraph` from parsed metadata. PackageGraphConstructError(String), /// A package ID was unknown to this `PackageGraph`. UnknownPackageId(PackageId), /// A feature ID was unknown to this `FeatureGraph`. UnknownFeatureId(PackageId, String), /// A package specified by path was unknown to this workspace. UnknownWorkspacePath(Utf8PathBuf), /// A package specified by name was unknown to this workspace. UnknownWorkspaceName(String), /// An error was returned by `target-spec`. TargetSpecError(String, TargetSpecError), /// An internal error occurred within this `PackageGraph`. PackageGraphInternalError(String), /// An internal error occurred within this `FeatureGraph`. FeatureGraphInternalError(String), /// A summary ID was unknown to this `PackageGraph`. /// /// This is present if the `summaries` feature is enabled. #[cfg(feature = "summaries")] UnknownSummaryId(guppy_summaries::SummaryId), /// While resolving a [`PackageSetSummary`](crate::graph::summaries::PackageSetSummary), /// some elements were unknown to the `PackageGraph`. /// /// This is present if the `summaries` feature is enabled. #[cfg(feature = "summaries")] UnknownPackageSetSummary { /// A description attached to the error. message: String, /// Summary IDs that weren't known to the `PackageGraph`. unknown_summary_ids: Vec, /// Workspace packages that weren't known to the `PackageGraph`. unknown_workspace_members: Vec, /// Third-party packages that weren't known to the `PackageGraph`. unknown_third_party: Vec, }, /// While resolving a [`PackageSetSummary`](crate::graph::summaries::PackageSetSummary), /// an unknown external registry was encountered. #[cfg(feature = "summaries")] UnknownRegistryName { /// A description attached to the error. message: String, /// The summary for which the name wasn't recognized. summary: Box, /// The registry name that wasn't recognized. registry_name: String, }, /// An error occurred while serializing to TOML. #[cfg(feature = "summaries")] TomlSerializeError(toml::ser::Error), } impl Error { pub(crate) fn command_error(err: cargo_metadata::Error) -> Self { Error::CommandError(Box::new(err)) } pub(crate) fn unknown_feature_id(feature_id: FeatureId<'_>) -> Self { Error::UnknownFeatureId( feature_id.package_id().clone(), feature_id.label().to_string(), ) } } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { CommandError(_) => write!(f, "`cargo metadata` execution failed"), MetadataParseError(_) => write!(f, "`cargo metadata` returned invalid JSON output"), MetadataSerializeError(_) => write!(f, "failed to serialize `cargo metadata` to JSON"), PackageGraphConstructError(s) => write!(f, "failed to construct package graph: {s}"), UnknownPackageId(id) => write!(f, "unknown package ID: {id}"), UnknownFeatureId(package_id, feature) => { write!(f, "unknown feature ID: '{package_id}/{feature}'") } UnknownWorkspacePath(path) => write!(f, "unknown workspace path: {path}"), UnknownWorkspaceName(name) => write!(f, "unknown workspace package name: {name}"), TargetSpecError(msg, _) => write!(f, "target spec error while {msg}"), PackageGraphInternalError(msg) => write!(f, "internal error in package graph: {msg}"), FeatureGraphInternalError(msg) => write!(f, "internal error in feature graph: {msg}"), #[cfg(feature = "summaries")] UnknownSummaryId(summary_id) => write!(f, "unknown summary ID: {summary_id}"), #[cfg(feature = "summaries")] UnknownPackageSetSummary { message, unknown_summary_ids, unknown_workspace_members, unknown_third_party, } => { writeln!(f, "unknown elements: {message}")?; if !unknown_summary_ids.is_empty() { writeln!(f, "* unknown summary IDs:")?; for summary_id in unknown_summary_ids { writeln!(f, " - {summary_id}")?; } } if !unknown_workspace_members.is_empty() { writeln!(f, "* unknown workspace names:")?; for workspace_member in unknown_workspace_members { writeln!(f, " - {workspace_member}")?; } } if !unknown_third_party.is_empty() { writeln!(f, "* unknown third-party:")?; for third_party in unknown_third_party { writeln!(f, " - {third_party}")?; } } Ok(()) } #[cfg(feature = "summaries")] UnknownRegistryName { message, summary, registry_name, } => { writeln!( f, "unknown registry name: {message}\n* for third-party: {summary}\n* name: {registry_name}\n" ) } #[cfg(feature = "summaries")] TomlSerializeError(_) => write!(f, "failed to serialize to TOML"), } } } impl error::Error for Error { fn source(&self) -> Option<&(dyn error::Error + 'static)> { match self { MetadataParseError(err) => Some(err), MetadataSerializeError(err) => Some(err), CommandError(err) => Some(err.as_ref()), PackageGraphConstructError(_) => None, UnknownPackageId(_) => None, UnknownFeatureId(_, _) => None, UnknownWorkspacePath(_) => None, UnknownWorkspaceName(_) => None, TargetSpecError(_, err) => Some(err), PackageGraphInternalError(_) => None, FeatureGraphInternalError(_) => None, #[cfg(feature = "summaries")] UnknownSummaryId(_) => None, #[cfg(feature = "summaries")] UnknownPackageSetSummary { .. } => None, #[cfg(feature = "summaries")] UnknownRegistryName { .. } => None, #[cfg(feature = "summaries")] TomlSerializeError(err) => Some(err), } } } /// Describes warnings emitted during feature graph construction. #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] #[non_exhaustive] pub enum FeatureGraphWarning { /// A feature that was requested is missing from a package. MissingFeature { /// The stage of building the feature graph where the warning occurred. stage: FeatureBuildStage, /// The package ID for which the feature was requested. package_id: PackageId, /// The name of the feature. feature_name: String, }, /// A self-loop was discovered. SelfLoop { /// The package ID for which the self-loop was discovered. package_id: PackageId, /// The name of the feature for which the self-loop was discovered. feature_name: String, }, } impl fmt::Display for FeatureGraphWarning { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use FeatureGraphWarning::*; match self { MissingFeature { stage, package_id, feature_name, } => write!( f, "{stage}: for package '{package_id}', missing feature '{feature_name}'" ), SelfLoop { package_id, feature_name, } => write!( f, "for package '{package_id}', self-loop detected for named feature '{feature_name}'" ), } } } /// Describes the stage of construction at which a feature graph warning occurred. #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] #[non_exhaustive] pub enum FeatureBuildStage { /// The warning occurred while adding edges for the `[features]` section of `Cargo.toml`. AddNamedFeatureEdges { /// The package ID for which edges were being added. package_id: PackageId, /// The feature name from which edges were being added. from_feature: String, }, /// The warning occurred while adding dependency edges. AddDependencyEdges { /// The package ID for which edges were being added. package_id: PackageId, /// The name of the dependency. dep_name: String, }, } impl fmt::Display for FeatureBuildStage { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use FeatureBuildStage::*; match self { AddNamedFeatureEdges { package_id, from_feature, } => write!( f, "for package '{package_id}', while adding named feature edges from '{from_feature}'" ), AddDependencyEdges { package_id, dep_name, } => write!( f, "for package '{package_id}', while adding edges for dependency '{dep_name}'", ), } } } guppy-0.17.25/src/graph/build.rs000064400000000000000000001663461046102023000145210ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::{ Error, PackageId, graph::{ BuildTargetImpl, BuildTargetKindImpl, DepRequiredOrOptional, DependencyReqImpl, NamedFeatureDep, OwnedBuildTargetId, PackageGraph, PackageGraphData, PackageIx, PackageLinkImpl, PackageMetadataImpl, PackagePublishImpl, PackageSourceImpl, WorkspaceImpl, cargo_version_matches, }, sorted_set::SortedSet, }; use ahash::AHashMap; use camino::{Utf8Path, Utf8PathBuf}; use cargo_metadata::{ DepKindInfo, Dependency, DependencyKind, Metadata, Node, NodeDep, Package, Target, }; use fixedbitset::FixedBitSet; use indexmap::{IndexMap, IndexSet}; use once_cell::sync::OnceCell; use petgraph::prelude::*; use semver::{Version, VersionReq}; use smallvec::SmallVec; use std::{ cell::RefCell, collections::{BTreeMap, HashSet}, rc::Rc, }; use target_spec::TargetSpec; impl PackageGraph { /// Constructs a new `PackageGraph` instances from the given metadata. pub(crate) fn build(mut metadata: Metadata) -> Result> { // resolve_nodes is missing if the metadata was generated with --no-deps. let resolve_nodes = metadata.resolve.map(|r| r.nodes).unwrap_or_default(); let workspace_members: HashSet<_> = metadata .workspace_members .into_iter() .map(PackageId::from_metadata) .collect(); // Normalize Windows paths early so all downstream code works correctly. let workspace_root = normalize_windows_path_on_unix(metadata.workspace_root); let workspace_default_members: Vec<_> = if metadata.workspace_default_members.is_available() { metadata .workspace_default_members .iter() .map(|id| PackageId::from_metadata(id.clone())) .collect() } else { Vec::new() }; let mut build_state = GraphBuildState::new( &mut metadata.packages, resolve_nodes, &workspace_root, &workspace_members, )?; let packages: AHashMap<_, _> = metadata .packages .into_iter() .map(|package| build_state.process_package(package)) .collect::>()?; let dep_graph = build_state.finish(); let workspace = WorkspaceImpl::new( workspace_root, normalize_windows_path_on_unix(metadata.target_directory), metadata.build_directory.map(normalize_windows_path_on_unix), metadata.workspace_metadata, &packages, workspace_members, workspace_default_members, )?; Ok(Self { dep_graph, sccs: OnceCell::new(), feature_graph: OnceCell::new(), data: PackageGraphData { packages, workspace, }, }) } } impl WorkspaceImpl { /// Indexes and creates a new workspace. fn new( workspace_root: impl Into, target_directory: impl Into, build_directory: Option, metadata_table: serde_json::Value, packages: &AHashMap, members: impl IntoIterator, default_members: Vec, ) -> Result> { use std::collections::btree_map::Entry; let workspace_root = workspace_root.into(); // Build up the workspace members by path, since most interesting queries are going to // happen by path. let mut members_by_path = BTreeMap::new(); let mut members_by_name = BTreeMap::new(); for id in members { // Strip off the workspace path from the manifest path. let package_metadata = packages.get(&id).ok_or_else(|| { Error::PackageGraphConstructError(format!("workspace member '{id}' not found")) })?; let workspace_path = match &package_metadata.source { PackageSourceImpl::Workspace(path) => path, _ => { return Err(Error::PackageGraphConstructError(format!( "workspace member '{}' at path {:?} not in workspace", id, package_metadata.manifest_path, )) .into()); } }; members_by_path.insert(workspace_path.to_path_buf(), id.clone()); match members_by_name.entry(package_metadata.name.clone()) { Entry::Vacant(vacant) => { vacant.insert(id.clone()); } Entry::Occupied(occupied) => { return Err(Error::PackageGraphConstructError(format!( "duplicate package name in workspace: '{}' is name for '{}' and '{}'", occupied.key(), occupied.get(), id )) .into()); } } } // Validate that all default members are valid workspace members. for id in &default_members { if !members_by_path.values().any(|member_id| member_id == id) { return Err(Error::PackageGraphConstructError(format!( "workspace default member '{id}' not found in workspace members" )) .into()); } } Ok(Self { root: workspace_root, target_directory: target_directory.into(), build_directory, metadata_table, members_by_path, members_by_name, default_members, #[cfg(feature = "proptest1")] name_list: OnceCell::new(), }) } } /// Helper struct for building up dependency graph. struct GraphBuildState<'a> { dep_graph: Graph, package_data: AHashMap>, // The above, except by package name. by_package_name: AHashMap, Vec>>, // The values of resolve_data are the resolved dependencies. This is mutated so it is stored // separately from package_data. resolve_data: AHashMap>, workspace_root: &'a Utf8Path, workspace_members: &'a HashSet, } impl<'a> GraphBuildState<'a> { /// This method drains the list of targets from the package. fn new( packages: &mut [Package], resolve_nodes: Vec, workspace_root: &'a Utf8Path, workspace_members: &'a HashSet, ) -> Result> { // Precomputing the edge count is a roughly 5% performance improvement. let edge_count = resolve_nodes .iter() .map(|node| node.deps.len()) .sum::(); let mut dep_graph = Graph::with_capacity(packages.len(), edge_count); let all_package_data: AHashMap<_, _> = packages .iter_mut() .map(|package| PackageDataValue::new(package, &mut dep_graph)) .collect::>()?; // While it is possible to have duplicate names so the hash map is smaller, just make this // as big as package_data. let mut by_package_name: AHashMap, Vec>> = AHashMap::with_capacity(all_package_data.len()); for package_data in all_package_data.values() { by_package_name .entry(package_data.name.clone()) .or_default() .push(package_data.clone()); } let resolve_data: AHashMap<_, _> = resolve_nodes .into_iter() .map(|node| { ( PackageId::from_metadata(node.id), // This used to return resolved features (node.features) as well but guppy // now does its own feature handling, so it isn't used any more. node.deps, ) }) .collect(); Ok(Self { dep_graph, package_data: all_package_data, by_package_name, resolve_data, workspace_root, workspace_members, }) } fn process_package( &mut self, mut package: Package, ) -> Result<(PackageId, PackageMetadataImpl), Box> { let package_id = PackageId::from_metadata(package.id); // Normalize Windows paths early so all downstream code works correctly. package.manifest_path = normalize_windows_path_on_unix(package.manifest_path); package.license_file = package.license_file.map(normalize_windows_path_on_unix); package.readme = package.readme.map(normalize_windows_path_on_unix); let (package_data, build_targets) = self.package_data_and_remove_build_targets(&package_id)?; let source = if self.workspace_members.contains(&package_id) { PackageSourceImpl::Workspace(self.workspace_path(&package_id, &package.manifest_path)?) } else if let Some(source) = package.source { if source.is_crates_io() { PackageSourceImpl::CratesIo } else { PackageSourceImpl::External(source.repr.into()) } } else { // Path dependency: get the directory from the manifest path. let dirname = match package.manifest_path.parent() { Some(dirname) => dirname, None => { return Err(Error::PackageGraphConstructError(format!( "package '{}': manifest path '{}' does not have parent", package_id, package.manifest_path, )) .into()); } }; PackageSourceImpl::create_path(dirname, self.workspace_root) }; // resolved_deps is missing if the metadata was generated with --no-deps. let resolved_deps = self.resolve_data.remove(&package_id).unwrap_or_default(); let dep_resolver = DependencyResolver::new( &package_id, &self.package_data, &self.by_package_name, &package.dependencies, ); for NodeDep { name: resolved_name, pkg, dep_kinds, .. } in resolved_deps { let dep_id = PackageId::from_metadata(pkg); let (dep_data, deps) = dep_resolver.resolve(&resolved_name, &dep_id, &dep_kinds)?; let link = PackageLinkImpl::new(&package_id, &resolved_name, deps)?; // Use update_edge instead of add_edge to prevent multiple edges from being added // between these two nodes. // XXX maybe check for an existing edge? self.dep_graph .update_edge(package_data.package_ix, dep_data.package_ix, link); } let has_default_feature = package.features.contains_key("default"); // Optional dependencies could in principle be computed by looking at the edges out of this // package, but unresolved dependencies aren't part of the graph so we're going to miss them // (and many optional dependencies will be unresolved). // // XXX: Consider modeling unresolved dependencies in the graph. // // A dependency might be listed multiple times (e.g. as a build dependency and as a normal // one). Some of them might be optional, some might not be. List a dependency here if *any* // of those specifications are optional, since that's how Cargo features work. But also // dedup them. let optional_deps: IndexSet<_> = package .dependencies .into_iter() .filter_map(|dep| { if dep.optional { match dep.rename { Some(rename) => Some(rename.into_boxed_str()), None => Some(dep.name.into_boxed_str()), } } else { None } }) .collect(); // Has the explicit feature by the name of this optional dep been seen? let mut seen_explicit = FixedBitSet::with_capacity(optional_deps.len()); // The feature map contains both optional deps and named features. let mut named_features: IndexMap<_, _> = package .features .into_iter() .map(|(feature_name, deps)| { let mut parsed_deps = SmallVec::with_capacity(deps.len()); for dep in deps { let dep = NamedFeatureDep::from_cargo_string(dep); if let NamedFeatureDep::OptionalDependency(d) = &dep { let index = optional_deps.get_index_of(d.as_ref()).ok_or_else(|| { Error::PackageGraphConstructError(format!( "package '{package_id}': named feature {feature_name} specifies 'dep:{d}', but {d} is not an optional dependency")) })?; seen_explicit.set(index, true); } parsed_deps.push(dep); } Ok((feature_name.into_boxed_str(), parsed_deps)) }) .collect::>()?; // If an optional dependency was not seen explicitly, add an implicit named feature for it. for (index, dep) in optional_deps.iter().enumerate() { if !seen_explicit.contains(index) { named_features.insert( dep.clone(), std::iter::once(NamedFeatureDep::OptionalDependency(dep.clone())).collect(), ); } } // For compatibility with previous versions of guppy -- remove when a breaking change // occurs. let rust_version_req = package .rust_version .as_ref() .map(|rust_version| VersionReq { comparators: vec![semver::Comparator { op: semver::Op::GreaterEq, major: rust_version.major, minor: Some(rust_version.minor), patch: Some(rust_version.patch), // Rust versions don't support pre-release fields. pre: semver::Prerelease::EMPTY, }], }); Ok(( package_id, PackageMetadataImpl { name: package.name.to_string().into(), version: package.version, authors: package.authors, description: package.description.map(|s| s.into()), license: package.license.map(|s| s.into()), license_file: package.license_file.map(|f| f.into()), manifest_path: package.manifest_path.into(), categories: package.categories, keywords: package.keywords, readme: package.readme.map(|s| s.into()), repository: package.repository.map(|s| s.into()), homepage: package.homepage.map(|s| s.into()), documentation: package.documentation.map(|s| s.into()), edition: package.edition.to_string().into_boxed_str(), metadata_table: package.metadata, links: package.links.map(|s| s.into()), publish: PackagePublishImpl::new(package.publish), default_run: package.default_run.map(|s| s.into()), rust_version: package.rust_version, rust_version_req, named_features, optional_deps, package_ix: package_data.package_ix, source, build_targets, has_default_feature, }, )) } fn package_data_and_remove_build_targets( &self, id: &PackageId, ) -> Result<(Rc, BuildTargetMap), Box> { let package_data = self.package_data.get(id).ok_or_else(|| { Error::PackageGraphConstructError(format!("no package data found for package '{id}'")) })?; let package_data = package_data.clone(); let build_targets = std::mem::take(&mut *package_data.build_targets.borrow_mut()); Ok((package_data, build_targets)) } /// Computes the relative path from the workspace root to this package. /// (This might be outside the root, but in valid Cargo metadata outputs /// will never cross drives on Windows.) fn workspace_path( &self, id: &PackageId, manifest_path: &Utf8Path, ) -> Result, Box> { // Get relative path from workspace root to manifest path. let workspace_path = diff_utf8_paths_cross_platform(manifest_path, self.workspace_root) .ok_or_else(|| { Error::PackageGraphConstructError(format!( "workspace member '{id}' at {manifest_path} cannot be reached \ from workspace root {}; paths may be on different drives or UNC shares", self.workspace_root )) })?; let workspace_path = workspace_path.parent().ok_or_else(|| { Error::PackageGraphConstructError(format!( "workspace member '{id}' has invalid manifest path {manifest_path:?}" )) })?; Ok(workspace_path.into()) } fn finish(self) -> Graph { self.dep_graph } } /// Intermediate state for a package as stored in `GraphBuildState`. #[derive(Debug)] struct PackageDataValue { package_ix: NodeIndex, name: Box, resolved_name: ResolvedName, // build_targets is used in two spots: in the constructor here, and removed from this field in // package_data_and_remove_build_targets. build_targets: RefCell, version: Version, } impl PackageDataValue { fn new( package: &mut Package, dep_graph: &mut Graph, ) -> Result<(PackageId, Rc), Box> { let package_id = PackageId::from_metadata(package.id.clone()); let package_ix = dep_graph.add_node(package_id.clone()); // Build up the list of build targets -- this will be used to construct the resolved_name. let mut build_targets = BuildTargets::new(&package_id); for build_target in package.targets.drain(..) { build_targets.add(build_target)?; } let build_targets = build_targets.finish(); let resolved_name = match build_targets.get(&OwnedBuildTargetId::Library) { Some(target) => { let lib_name = target .lib_name .as_deref() .expect("lib_name is always specified for library targets"); if lib_name != package.name.as_str() { ResolvedName::LibNameSpecified(lib_name.to_string()) } else { // The resolved name is the same as the package name. ResolvedName::LibNameNotSpecified(lib_name.replace('-', "_")) } } None => { // This means that it's a weird case like a binary-only dependency (not part of // stable Rust as of 2023-11). This will typically be reflected as an empty resolved // name. ResolvedName::NoLibTarget } }; let value = PackageDataValue { package_ix, name: package.name.to_string().into(), resolved_name, build_targets: RefCell::new(build_targets), version: package.version.clone(), }; Ok((package_id, Rc::new(value))) } } #[derive(Clone, Debug, Eq, PartialEq, Hash)] enum ResolvedName { LibNameSpecified(String), /// This variant has its - replaced with _. LibNameNotSpecified(String), NoLibTarget, } /// Matcher for the resolved name of a dependency. /// /// The "rename" field in a dependency, if present, is generally used. (But not always! There are /// cases where even if a rename is present, the package name is used instead.) #[derive(Clone, Debug, Eq, PartialEq, Hash)] struct ReqResolvedName<'g> { // A renamed name, if any. renamed: Option, // A resolved name created from the lib.name field. resolved_name: &'g ResolvedName, } impl<'g> ReqResolvedName<'g> { fn new(renamed: Option<&str>, resolved_name: &'g ResolvedName) -> Self { Self { renamed: renamed.map(|s| s.replace('-', "_")), resolved_name, } } fn matches(&self, name: &str) -> bool { if let Some(rename) = &self.renamed { if rename == name { return true; } } match self.resolved_name { ResolvedName::LibNameSpecified(resolved_name) => *resolved_name == name, ResolvedName::LibNameNotSpecified(resolved_name) => *resolved_name == name, ResolvedName::NoLibTarget => { // This code path is only hit with nightly Rust as of 2023-11. It depends on Rust // RFC 3028. at https://github.com/rust-lang/cargo/issues/9096. // // This isn't quite right -- if we have two or more non-lib dependencies, we'll // return true for both of them over here. What we need to do instead is use the // extern_name and bin_name fields that are present in nightly DepKindInfo, but that // aren't in stable yet. For now, this is the best we can do. // // (If we're going to be relying on heuristics, it is also possible to use the // package ID over here, but that's documented to be an opaque string. It also // wouldn't be resilient to patch and replace.) name.is_empty() } } } } impl PackageSourceImpl { fn create_path(path: &Utf8Path, workspace_root: &Utf8Path) -> Self { // If we can compute a relative path, use it. Otherwise (e.g., different // drive letters on Windows), fall back to the absolute path. let path_diff = diff_utf8_paths_cross_platform(path, workspace_root) .unwrap_or_else(|| path.to_path_buf()); Self::Path(path_diff.into_boxed_path()) } } impl NamedFeatureDep { fn from_cargo_string(input: impl Into) -> Self { let input = input.into(); match input.split_once('/') { Some((dep_name, feature)) => { if let Some(dep_name_without_q) = dep_name.strip_suffix('?') { Self::dep_named_feature(dep_name_without_q, feature, true) } else { Self::dep_named_feature(dep_name, feature, false) } } None => match input.strip_prefix("dep:") { Some(dep_name) => Self::optional_dependency(dep_name), None => Self::named_feature(input), }, } } } type BuildTargetMap = BTreeMap; struct BuildTargets<'a> { package_id: &'a PackageId, targets: BuildTargetMap, } impl<'a> BuildTargets<'a> { fn new(package_id: &'a PackageId) -> Self { Self { package_id, targets: BTreeMap::new(), } } fn add(&mut self, target: Target) -> Result<(), Box> { use std::collections::btree_map::Entry; // Figure out the id and kind using target.kind and target.crate_types. let mut target_kinds = target .kind .into_iter() .map(|kind| kind.to_string()) .collect::>(); let target_name = target.name.into_boxed_str(); // Store crate types as strings to avoid exposing cargo_metadata in the // public API. let crate_types = SortedSet::new( target .crate_types .into_iter() .map(|ct| ct.to_string()) .collect::>(), ); // The "proc-macro" crate type cannot mix with any other types or kinds. if target_kinds.len() > 1 && Self::is_proc_macro(&target_kinds) { return Err(Error::PackageGraphConstructError(format!( "for package {}, proc-macro mixed with other kinds ({:?})", self.package_id, target_kinds )) .into()); } if crate_types.len() > 1 && Self::is_proc_macro(&crate_types) { return Err(Error::PackageGraphConstructError(format!( "for package {}, proc-macro mixed with other crate types ({})", self.package_id, crate_types )) .into()); } let (id, kind, lib_name) = if target_kinds.len() > 1 { // multiple kinds always means a library target. ( OwnedBuildTargetId::Library, BuildTargetKindImpl::LibraryOrExample(crate_types), Some(target_name), ) } else if let Some(target_kind) = target_kinds.pop() { let (id, lib_name) = match target_kind.as_str() { "custom-build" => (OwnedBuildTargetId::BuildScript, Some(target_name)), "bin" => (OwnedBuildTargetId::Binary(target_name), None), "example" => (OwnedBuildTargetId::Example(target_name), None), "test" => (OwnedBuildTargetId::Test(target_name), None), "bench" => (OwnedBuildTargetId::Benchmark(target_name), None), _other => { // Assume that this is a library crate. (OwnedBuildTargetId::Library, Some(target_name)) } }; let kind = match &id { OwnedBuildTargetId::Library => { if crate_types.as_slice() == ["proc-macro"] { BuildTargetKindImpl::ProcMacro } else { BuildTargetKindImpl::LibraryOrExample(crate_types) } } OwnedBuildTargetId::Example(_) => { BuildTargetKindImpl::LibraryOrExample(crate_types) } _ => { // The crate_types must be exactly "bin". if crate_types.as_slice() != ["bin"] { return Err(Error::PackageGraphConstructError(format!( "for package {}: build target '{:?}' has invalid crate types '{}'", self.package_id, id, crate_types, )) .into()); } BuildTargetKindImpl::Binary } }; (id, kind, lib_name) } else { return Err(Error::PackageGraphConstructError(format!( "for package ID '{}': build target '{}' has no kinds", self.package_id, target_name )) .into()); }; match self.targets.entry(id) { Entry::Occupied(occupied) => { return Err(Error::PackageGraphConstructError(format!( "for package ID '{}': duplicate build targets for {:?}", self.package_id, occupied.key() )) .into()); } Entry::Vacant(vacant) => { vacant.insert(BuildTargetImpl { kind, lib_name, required_features: target.required_features, path: normalize_windows_path_on_unix(target.src_path).into_boxed_path(), edition: target.edition.to_string().into_boxed_str(), doc_by_default: target.doc, doctest_by_default: target.doctest, test_by_default: target.test, }); } } Ok(()) } fn is_proc_macro(list: &[String]) -> bool { list.iter().any(|kind| *kind == "proc-macro") } fn finish(self) -> BuildTargetMap { self.targets } } struct DependencyResolver<'g> { from_id: &'g PackageId, /// The package data, inherited from the graph build state. package_data: &'g AHashMap>, /// This is a list of dependency requirements. We don't know the package ID yet so we don't have /// a great key to work with. This could be improved in the future by matching on requirements /// (though it's hard). dep_reqs: DependencyReqs<'g>, } impl<'g> DependencyResolver<'g> { /// Constructs a new resolver using the provided package data and dependencies. fn new( from_id: &'g PackageId, package_data: &'g AHashMap>, by_package_name: &'g AHashMap, Vec>>, package_deps: impl IntoIterator, ) -> Self { let mut dep_reqs = DependencyReqs::default(); for dep in package_deps { // Determine what the resolved name of each package could be by matching on package name // and version (NOT source, because the source can be patched). let Some(packages) = by_package_name.get(dep.name.as_str()) else { // This dependency did not lead to a resolved package. continue; }; for package in packages { if cargo_version_matches(&dep.req, &package.version) { // The cargo `resolve.deps` map uses one of two things: // // 1. dep.rename with - turned into _, if specified. // 2. lib.name, if specified, otherwise package.name with - turned into _. // // ReqResolvedName tracks both of these. let req_resolved_name = ReqResolvedName::new(dep.rename.as_deref(), &package.resolved_name); dep_reqs.push(req_resolved_name, dep); } } } Self { from_id, package_data, dep_reqs, } } /// Resolves this dependency by finding the `Dependency` items corresponding to this resolved /// name and package ID. fn resolve<'a>( &'a self, resolved_name: &'a str, dep_id: &PackageId, dep_kinds: &'a [DepKindInfo], ) -> Result< ( &'g Rc, impl Iterator + 'a + use<'g, 'a>, ), Error, > { let dep_data = self.package_data.get(dep_id).ok_or_else(|| { Error::PackageGraphConstructError(format!( "{}: no package data found for dependency '{}'", self.from_id, dep_id )) })?; Ok(( dep_data, self.dep_reqs .matches_for(resolved_name, dep_data, dep_kinds), )) } } /// Maintains a list of dependency requirements to match up to for a given package name. #[derive(Clone, Debug, Default)] struct DependencyReqs<'g> { // The keys are (resolved name, dependency). reqs: Vec<(ReqResolvedName<'g>, &'g Dependency)>, } impl<'g> DependencyReqs<'g> { fn push(&mut self, resolved_name: ReqResolvedName<'g>, dependency: &'g Dependency) { self.reqs.push((resolved_name, dependency)); } fn matches_for<'a>( &'a self, resolved_name: &'a str, package_data: &'a PackageDataValue, dep_kinds: &'a [DepKindInfo], ) -> impl Iterator + 'a { self.reqs .iter() .filter_map(move |(req_resolved_name, dep)| { // A dependency requirement matches this package if all of the following are true: // // 1. The resolved_name matches. // 2. The Cargo version matches (XXX is this necessary?) // 3. The dependency kind and target is found in dep_kinds. if !req_resolved_name.matches(resolved_name) { return None; } if !cargo_version_matches(&dep.req, &package_data.version) { return None; } // Some older manifests don't have the dep_kinds field -- in that case we can't // fully match manifests and just accept all such packages. We just can't do better // than that. if dep_kinds.is_empty() { return Some(*dep); } dep_kinds .iter() .any(|dep_kind| dep_kind.kind == dep.kind && dep_kind.target == dep.target) .then_some(*dep) }) } } impl PackageLinkImpl { fn new<'a>( from_id: &PackageId, resolved_name: &str, deps: impl IntoIterator, ) -> Result> { let mut version_req = None; let mut registry = None; let mut path = None; let mut normal = DependencyReqImpl::default(); let mut build = DependencyReqImpl::default(); let mut dev = DependencyReqImpl::default(); // We hope that the dep name is the same for all of these, but it's not guaranteed. let mut dep_name: Option = None; for dep in deps { let rename_or_name = dep.rename.as_ref().unwrap_or(&dep.name); match &dep_name { Some(dn) => { if dn != rename_or_name { // XXX: warn or error on this? } } None => { dep_name = Some(rename_or_name.clone()); } } // Dev dependencies cannot be optional. if dep.kind == DependencyKind::Development && dep.optional { return Err(Error::PackageGraphConstructError(format!( "for package '{}': dev-dependency '{}' marked optional", from_id, dep_name.expect("dep_name set above"), )) .into()); } // Pick the first version req, registry, and path that we come // across. if version_req.is_none() { version_req = Some(dep.req.clone()); } if registry.is_none() { registry = dep.registry.clone(); } if path.is_none() { path = dep.path.clone().map(normalize_windows_path_on_unix); } 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 dep_name = dep_name.ok_or_else(|| { Error::PackageGraphConstructError(format!( "for package '{from_id}': no dependencies found matching '{resolved_name}'", )) })?; let version_req = version_req.unwrap_or_else(|| { panic!( "requires at least one dependency instance: \ from `{from_id}` to `{dep_name}` (resolved name `{resolved_name}`)" ) }); Ok(Self { dep_name, resolved_name: resolved_name.into(), version_req, registry, path, normal, build, dev, }) } } /// 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 required/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. impl DependencyReqImpl { fn add_instance(&mut self, from_id: &PackageId, dep: &Dependency) -> Result<(), Box> { if dep.optional { self.optional.add_instance(from_id, dep) } else { self.required.add_instance(from_id, dep) } } } impl DepRequiredOrOptional { fn add_instance(&mut self, from_id: &PackageId, dep: &Dependency) -> Result<(), Box> { // 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()); } else { self.no_default_features_if.add_spec(target_spec.as_ref()); } for feature in &dep.features { self.feature_targets .entry(feature.clone()) .or_default() .add_spec(target_spec.as_ref()); } Ok(()) } } impl PackagePublishImpl { /// Converts cargo_metadata registries to our own format. fn new(registries: Option>) -> Self { match registries { None => PackagePublishImpl::Unrestricted, Some(registries) => PackagePublishImpl::Registries(registries.into_boxed_slice()), } } } /// The prefix of a Windows absolute path. /// /// This is similar to `std::path::Prefix` but works cross-platform. #[derive(Debug, Clone, PartialEq, Eq)] enum WindowsPathPrefix<'a> { /// A drive letter prefix, e.g., `C:`. Drive(char), /// A UNC prefix, e.g., `\\server\share`. Unc { server: &'a str, share: &'a str }, } impl<'a> WindowsPathPrefix<'a> { /// Parses a Windows path prefix from a string, returning the prefix and /// the remaining path. /// /// Returns `None` if the path doesn't look like a Windows absolute path. fn parse(s: &'a str) -> Option<(Self, &'a str)> { // Handle extended-length prefix \\?\C:\ or \\?\UNC\server\share. if s.starts_with(r"\\?\") || s.starts_with("//?/") { let inner = &s[4..]; // Check for extended-length UNC: \\?\UNC\server\share. if inner.starts_with(r"UNC\") || inner.starts_with("UNC/") { return Self::parse_unc_components(&inner[4..]); } return Self::parse_inner(inner); } // Device prefix \\.\C:\ -- no UNC variant exists for device paths. if s.starts_with(r"\\.\") || s.starts_with("//./") { return Self::parse_inner(&s[4..]); } Self::parse_inner(s) } /// Inner parsing logic for Windows path prefixes. fn parse_inner(s: &'a str) -> Option<(Self, &'a str)> { let bytes = s.as_bytes(); // Drive letter: C:\ or C:/ if bytes.len() >= 3 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':' && (bytes[2] == b'\\' || bytes[2] == b'/') { let drive = bytes[0].to_ascii_uppercase() as char; return Some((Self::Drive(drive), &s[2..])); } // UNC-style paths: \\server\share or //server/share if let Some(rest) = s.strip_prefix(r"\\").or_else(|| s.strip_prefix("//")) { return Self::parse_unc_components(rest); } None } /// Parse UNC server and share from a path after the leading prefix has been /// stripped. Expects format: `server\share\path` or `server/share/path`. fn parse_unc_components(s: &'a str) -> Option<(Self, &'a str)> { // Find the separator between server and share. let sep1 = s.find(['\\', '/'])?; let server = &s[..sep1]; let after_server = &s[sep1 + 1..]; // Find the end of share (next separator or end of string). let sep2 = after_server.find(['\\', '/']).unwrap_or(after_server.len()); let share = &after_server[..sep2]; if server.is_empty() || share.is_empty() { return None; } let remaining = &after_server[sep2..]; Some((Self::Unc { server, share }, remaining)) } } /// On Unix, if the path looks like a Windows absolute path, normalize backslashes /// to forward slashes so that `parent()` and other path operations work correctly. /// /// This is needed because cargo metadata generated on Windows contains paths like /// `C:\Users\foo\Cargo.toml`, and on Unix `Utf8Path::parent()` doesn't recognize /// backslashes as path separators. fn normalize_windows_path_on_unix(path: Utf8PathBuf) -> Utf8PathBuf { #[cfg(windows)] { // On Windows, paths work natively. path } #[cfg(not(windows))] { let s = path.as_str(); if WindowsPathPrefix::parse(s).is_some() { // This looks like a Windows path; normalize backslashes to forward slashes. Utf8PathBuf::from(s.replace('\\', "/")) } else { path } } } /// Computes a relative path from `base` to `path`, handling cross-platform paths. /// /// This function checks whether both paths appear to be Windows-style paths, /// containing backslashes or drive letters like `C:`. If so, it normalizes them /// and computes the relative path manually. Otherwise, it uses native /// `pathdiff::diff_utf8_paths`. /// /// Handles: /// /// - Standard Windows paths: `C:\path\to\file` /// - UNC paths: `\\server\share\path` /// - Extended-length paths: `\\?\C:\path` or `\\.\C:\path` /// /// Returns `None` if the paths have different prefixes (e.g., different drive /// letters or different UNC servers/shares) and thus cannot have a relative /// path computed between them. /// /// We don't handle Windows case folding -- it's assumed that the paths have the /// same case. (pathdiff also makes this assumption.) /// /// This allows parsing cargo metadata generated on Windows when running on /// Unix. fn diff_utf8_paths_cross_platform(path: &Utf8Path, base: &Utf8Path) -> Option { let path_str = path.as_str(); let base_str = base.as_str(); // Try to parse both as Windows paths. let path_parsed = WindowsPathPrefix::parse(path_str); let base_parsed = WindowsPathPrefix::parse(base_str); match (path_parsed, base_parsed) { (Some((path_prefix, path_rest)), Some((base_prefix, base_rest))) => { // Both are Windows paths -- check that prefixes match. if path_prefix != base_prefix { return None; } // Compute relative path from the remaining portions. let normalize = |s: &str| s.replace('\\', "/"); let norm_path = normalize(path_rest); let norm_base = normalize(base_rest); let path_parts: Vec<&str> = norm_path.split('/').filter(|s| !s.is_empty()).collect(); let base_parts: Vec<&str> = norm_base.split('/').filter(|s| !s.is_empty()).collect(); let common_len = path_parts .iter() .zip(base_parts.iter()) .take_while(|(a, b)| a == b) .count(); let ups = base_parts.len() - common_len; let mut result_parts: Vec<&str> = std::iter::repeat_n("..", ups).collect(); result_parts.extend(&path_parts[common_len..]); if result_parts.is_empty() { Some(Utf8PathBuf::from(".")) } else { Some(Utf8PathBuf::from(result_parts.join("/"))) } } (None, None) => { // Neither is a Windows path -- use native diffing. pathdiff::diff_utf8_paths(path, base).map(convert_relative_forward_slashes) } _ => { // Mixed (one Windows, one not) -- cannot compute relative path. None } } } /// Replace backslashes in a relative path with forward slashes on Windows. #[track_caller] fn convert_relative_forward_slashes(rel_path: Utf8PathBuf) -> Utf8PathBuf { cfg_if::cfg_if! { if #[cfg(windows)] { if rel_path.is_relative() { rel_path.as_str().replace("\\", "/").into() } else { rel_path } } else { rel_path }} } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_named_feature_dependency() { assert_eq!( NamedFeatureDep::from_cargo_string("dep/bar"), NamedFeatureDep::dep_named_feature("dep", "bar", false), ); assert_eq!( NamedFeatureDep::from_cargo_string("dep?/bar"), NamedFeatureDep::dep_named_feature("dep", "bar", true), ); assert_eq!( NamedFeatureDep::from_cargo_string("dep:bar"), NamedFeatureDep::optional_dependency("bar"), ); assert_eq!( NamedFeatureDep::from_cargo_string("foo-bar"), NamedFeatureDep::named_feature("foo-bar"), ); } #[test] fn test_create_path() { assert_eq!( PackageSourceImpl::create_path("/data/foo".as_ref(), "/data/bar".as_ref()), PackageSourceImpl::Path("../foo".into()) ); assert_eq!( PackageSourceImpl::create_path("/tmp/foo".as_ref(), "/data/bar".as_ref()), PackageSourceImpl::Path("../../tmp/foo".into()) ); } #[test] fn test_convert_relative_forward_slashes() { let components = vec!["..", "..", "foo", "bar", "baz.txt"]; let path: Utf8PathBuf = components.into_iter().collect(); let path = convert_relative_forward_slashes(path); // This should have forward-slashes, even on Windows. assert_eq!(path.as_str(), "../../foo/bar/baz.txt"); } #[test] fn test_normalize_windows_path_on_unix() { #[cfg(not(windows))] { assert_eq!( normalize_windows_path_on_unix(r"C:\Users\foo\project".into()), Utf8PathBuf::from("C:/Users/foo/project") ); assert_eq!( normalize_windows_path_on_unix(r"\\server\share\path".into()), Utf8PathBuf::from("//server/share/path") ); // Unix paths unchanged. assert_eq!( normalize_windows_path_on_unix("/home/user/project".into()), Utf8PathBuf::from("/home/user/project") ); } #[cfg(windows)] { // Windows paths unchanged on Windows. assert_eq!( normalize_windows_path_on_unix(r"C:\Users\foo\project".into()), Utf8PathBuf::from(r"C:\Users\foo\project") ); } } #[track_caller] fn verify_diff_utf8_paths_cross_platform( path_manifest: &str, path_workspace_root: &str, expected_relative_path: Option<&str>, ) { let relative_path = diff_utf8_paths_cross_platform( Utf8Path::new(path_manifest), Utf8Path::new(path_workspace_root), ); assert_eq!( relative_path.as_deref(), expected_relative_path.map(Utf8Path::new) ); } #[test] fn test_workspace_path_out_of_pocket() { verify_diff_utf8_paths_cross_platform( "/workspace/a/b/Crate/Cargo.toml", "/workspace/a/b/.cargo/workspace", Some("../../Crate/Cargo.toml"), ); } #[test] fn test_diff_utf8_paths_cross_platform_unix() { // Unix paths should work normally. assert_eq!( diff_utf8_paths_cross_platform( "/workspace/a/b/Crate/Cargo.toml".into(), "/workspace/a/b".into() ), Some("Crate/Cargo.toml".into()) ); assert_eq!( diff_utf8_paths_cross_platform( "/workspace/a/b/Crate/Cargo.toml".into(), "/workspace/a".into() ), Some("b/Crate/Cargo.toml".into()) ); assert_eq!( diff_utf8_paths_cross_platform("/tmp/foo".into(), "/data/bar".into()), Some("../../tmp/foo".into()) ); } #[test] fn test_diff_utf8_paths_cross_platform_windows() { // Windows paths should work on any platform. assert_eq!( diff_utf8_paths_cross_platform( r"D:\a\nextest\nextest\cargo-nextest\Cargo.toml".into(), r"D:\a\nextest\nextest".into() ), Some("cargo-nextest/Cargo.toml".into()) ); assert_eq!( diff_utf8_paths_cross_platform( r"D:\a\nextest\nextest\internal-test\Cargo.toml".into(), r"D:\a\nextest\nextest".into() ), Some("internal-test/Cargo.toml".into()) ); // Going up directories. assert_eq!( diff_utf8_paths_cross_platform( r"D:\workspace\a\b\Crate\Cargo.toml".into(), r"D:\workspace\a\b\.cargo\workspace".into() ), Some("../../Crate/Cargo.toml".into()) ); // Same path should give ".". assert_eq!( diff_utf8_paths_cross_platform( r"D:\a\nextest\nextest".into(), r"D:\a\nextest\nextest".into() ), Some(".".into()) ); } #[test] fn test_diff_utf8_paths_cross_platform_unc() { // UNC paths: \\server\share\path assert_eq!( diff_utf8_paths_cross_platform( r"\\server\share\workspace\crate\Cargo.toml".into(), r"\\server\share\workspace".into() ), Some("crate/Cargo.toml".into()) ); // Going up in UNC paths. assert_eq!( diff_utf8_paths_cross_platform( r"\\server\share\workspace\crate\Cargo.toml".into(), r"\\server\share\workspace\other".into() ), Some("../crate/Cargo.toml".into()) ); } #[test] fn test_diff_utf8_paths_cross_platform_extended_length() { // Extended-length paths: \\?\C:\path (used for paths > 260 chars on Windows). assert_eq!( diff_utf8_paths_cross_platform( r"\\?\D:\a\nextest\nextest\cargo-nextest\Cargo.toml".into(), r"\\?\D:\a\nextest\nextest".into() ), Some("cargo-nextest/Cargo.toml".into()) ); // Device paths: \\.\C:\path assert_eq!( diff_utf8_paths_cross_platform( r"\\.\C:\workspace\crate\Cargo.toml".into(), r"\\.\C:\workspace".into() ), Some("crate/Cargo.toml".into()) ); // Mixed: one with prefix, one without. Both still look like Windows // paths due to backslashes. assert_eq!( diff_utf8_paths_cross_platform( r"\\?\D:\a\nextest\cargo-nextest\Cargo.toml".into(), r"D:\a\nextest".into() ), Some("cargo-nextest/Cargo.toml".into()) ); // Device path prefix mixed with non-prefixed. assert_eq!( diff_utf8_paths_cross_platform( r"\\.\C:\workspace\crate\Cargo.toml".into(), r"C:\workspace".into() ), Some("crate/Cargo.toml".into()) ); // Extended-length UNC paths: \\?\UNC\server\share\path. assert_eq!( diff_utf8_paths_cross_platform( r"\\?\UNC\server\share\workspace\crate\Cargo.toml".into(), r"\\?\UNC\server\share\workspace".into() ), Some("crate/Cargo.toml".into()) ); } #[test] fn test_diff_utf8_paths_cross_platform_different_drives() { // Different drives should return None. assert_eq!( diff_utf8_paths_cross_platform(r"D:\foo\bar".into(), r"C:\baz".into()), None ); assert_eq!( diff_utf8_paths_cross_platform(r"C:\foo".into(), r"D:\bar".into()), None ); // Case-insensitive drive letters. assert_eq!( diff_utf8_paths_cross_platform(r"c:\foo".into(), r"C:\bar".into()), Some("../foo".into()) ); } #[test] fn test_diff_utf8_paths_cross_platform_different_unc_servers() { // Different UNC servers should return None. assert_eq!( diff_utf8_paths_cross_platform( r"\\server1\share\path".into(), r"\\server2\share\path".into() ), None ); // Different shares on same server should return None. assert_eq!( diff_utf8_paths_cross_platform( r"\\server\share1\path".into(), r"\\server\share2\path".into() ), None ); // UNC server/share names are case-sensitive in this implementation // (unlike actual Windows). This documents the limitation. assert_eq!( diff_utf8_paths_cross_platform( r"\\SERVER\share\path".into(), r"\\server\share\other".into() ), None, "UNC server names are compared case-sensitively" ); } #[test] fn test_diff_utf8_paths_cross_platform_mixed() { // Mixed Windows and Unix paths should return None. assert_eq!( diff_utf8_paths_cross_platform(r"C:\foo".into(), "/bar".into()), None ); assert_eq!( diff_utf8_paths_cross_platform("/foo".into(), r"D:\bar".into()), None ); } #[test] fn test_diff_utf8_paths_cross_platform_trailing_slashes() { // Trailing slashes should be handled correctly. assert_eq!( diff_utf8_paths_cross_platform(r"C:\foo\".into(), r"C:\foo".into()), Some(".".into()) ); assert_eq!( diff_utf8_paths_cross_platform(r"C:\foo\bar\".into(), r"C:\foo\".into()), Some("bar".into()) ); assert_eq!( diff_utf8_paths_cross_platform(r"C:\foo".into(), r"C:\foo\".into()), Some(".".into()) ); } #[test] fn test_diff_utf8_paths_cross_platform_root_only() { // Root-only paths (just drive letter). assert_eq!( diff_utf8_paths_cross_platform(r"C:\foo".into(), r"C:\".into()), Some("foo".into()) ); assert_eq!( diff_utf8_paths_cross_platform(r"C:\".into(), r"C:\foo".into()), Some("..".into()) ); assert_eq!( diff_utf8_paths_cross_platform(r"C:\".into(), r"C:\".into()), Some(".".into()) ); } #[test] fn test_windows_path_prefix_parse() { // Drive letters. assert_eq!( WindowsPathPrefix::parse(r"C:\foo\bar"), Some((WindowsPathPrefix::Drive('C'), r"\foo\bar")) ); assert_eq!( WindowsPathPrefix::parse("D:/foo/bar"), Some((WindowsPathPrefix::Drive('D'), "/foo/bar")) ); // UNC paths. assert_eq!( WindowsPathPrefix::parse(r"\\server\share\path"), Some(( WindowsPathPrefix::Unc { server: "server", share: "share" }, r"\path" )) ); // Extended-length paths strip to drive. assert_eq!( WindowsPathPrefix::parse(r"\\?\C:\foo"), Some((WindowsPathPrefix::Drive('C'), r"\foo")) ); // Extended-length UNC paths. assert_eq!( WindowsPathPrefix::parse(r"\\?\UNC\server\share\path"), Some(( WindowsPathPrefix::Unc { server: "server", share: "share" }, r"\path" )) ); // Unix paths return None. assert_eq!(WindowsPathPrefix::parse("/foo/bar"), None); assert_eq!(WindowsPathPrefix::parse("relative/path"), None); } #[cfg(windows)] // Test for '\\' and 'X:\' etc on windows mod windows { use super::*; #[test] fn test_create_path_windows() { // Ensure that relative paths are stored with forward slashes. assert_eq!( PackageSourceImpl::create_path("C:\\data\\foo".as_ref(), "C:\\data\\bar".as_ref()), PackageSourceImpl::Path("../foo".into()) ); // Paths that span drives cannot be stored as relative, so the // absolute path is used. assert_eq!( PackageSourceImpl::create_path("D:\\tmp\\foo".as_ref(), "C:\\data\\bar".as_ref()), PackageSourceImpl::Path("D:\\tmp\\foo".into()) ); } #[test] fn test_convert_relative_forward_slashes_absolute() { let components = vec![r"D:\", "X", "..", "foo", "bar", "baz.txt"]; let path: Utf8PathBuf = components.into_iter().collect(); let path = convert_relative_forward_slashes(path); // Absolute path keep using backslash on Windows. assert_eq!(path.as_str(), r"D:\X\..\foo\bar\baz.txt"); } #[test] fn test_workspace_path_out_of_pocket_on_windows_same_drive() { // Same drive: relative path with forward slashes. verify_diff_utf8_paths_cross_platform( r"C:\workspace\a\b\Crate\Cargo.toml", r"C:\workspace\a\b\.cargo\workspace", Some("../../Crate/Cargo.toml"), ); } #[test] fn test_workspace_path_out_of_pocket_on_windows_different_drives() { // Different drives: cannot compute relative path. verify_diff_utf8_paths_cross_platform( r"D:\workspace\a\b\Crate\Cargo.toml", r"C:\workspace\a\b\.cargo\workspace", None, ); } } } guppy-0.17.25/src/graph/build_targets.rs000064400000000000000000000265711046102023000162450ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::sorted_set::SortedSet; use camino::Utf8Path; use std::{borrow::Borrow, cmp::Ordering}; /// A build target in a package. /// /// A build target consists of one or more source files which can be compiled into a crate. /// /// For more, see [Cargo /// Targets](https://doc.rust-lang.org/nightly/cargo/reference/cargo-targets.html) in the Cargo /// reference. pub struct BuildTarget<'g> { id: BuildTargetId<'g>, inner: &'g BuildTargetImpl, } impl<'g> BuildTarget<'g> { // The weird function signature is so that .map(BuildTarget::new) can be called. pub(super) fn new((id, inner): (&'g OwnedBuildTargetId, &'g BuildTargetImpl)) -> Self { Self { id: id.as_borrowed(), inner, } } /// Returns the unique identifier for this build target. #[inline] pub fn id(&self) -> BuildTargetId<'g> { self.id } /// Returns the name of this build target. pub fn name(&self) -> &'g str { match self.id { BuildTargetId::Library | BuildTargetId::BuildScript => self .inner .lib_name .as_ref() .expect("library targets have lib_name set"), other => other.name().expect("non-library targets can't return None"), } } /// Returns the kind of this build target. #[inline] pub fn kind(&self) -> BuildTargetKind<'g> { BuildTargetKind::new(&self.inner.kind) } /// Returns the features required for this build target. /// /// This setting has no effect on the library target. /// /// For more, see [The `required-features` /// field](https://doc.rust-lang.org/nightly/cargo/reference/cargo-targets.html#the-required-features-field) /// in the Cargo reference. #[inline] pub fn required_features(&self) -> &'g [String] { &self.inner.required_features } /// Returns the absolute path of the location where the source for this build target is located. #[inline] pub fn path(&self) -> &'g Utf8Path { &self.inner.path } /// Returns the Rust edition for this build target. #[inline] pub fn edition(&self) -> &'g str { &self.inner.edition } /// Returns true if documentation is generated for this build target by /// default. /// /// This is true by default for library targets, as well as binaries that /// don't share a name with the library they are in. /// /// For more information, see [the Cargo documentation]. /// /// [the Cargo documentation]: https://doc.rust-lang.org/cargo/commands/cargo-doc.html#target-selection #[inline] pub fn doc_by_default(&self) -> bool { self.inner.doc_by_default } /// Returns true if documentation tests are run by default for this build /// target. /// /// For more information, see [the Cargo documentation]. /// /// [the Cargo documentation]: https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-doctest-field #[inline] pub fn doctest_by_default(&self) -> bool { self.inner.doctest_by_default } /// Previous name for [`Self::doctest_by_default`]. #[deprecated(since = "0.17.16", note = "use `doctest_by_default` instead")] #[inline] pub fn doc_tests(&self) -> bool { self.inner.doctest_by_default } /// Returns true if tests are run by default for this build target (i.e. if /// tests are run even if `--all-targets` isn't specified). /// /// This is true by default for libraries, binaries, and test targets. /// /// For more information, see [the Cargo documentation]. /// /// [the Cargo documentation]: https://doc.rust-lang.org/cargo/commands/cargo-test.html#target-selection #[inline] pub fn test_by_default(&self) -> bool { self.inner.test_by_default } } /// An identifier for a build target within a package. #[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] #[non_exhaustive] pub enum BuildTargetId<'g> { /// A library target. /// /// There may be at most one of these in a package. /// /// Defined by the `[lib]` section in `Cargo.toml`. Library, /// A build script. /// /// There may be at most one of these in a package. /// /// Defined by the `build` attribute in `Cargo.toml`. For more about build scripts, see [Build /// Scripts](https://doc.rust-lang.org/nightly/cargo/reference/build-scripts.html) in the Cargo /// reference. BuildScript, /// A binary target with its name. /// /// Defined by the `[[bin]]` section in `Cargo.toml`. Binary(&'g str), /// An example target with its name. /// /// Examples are typically binary, but may be libraries or even both. /// /// Defined by the `[[example]]` section in `Cargo.toml`. Example(&'g str), /// A test target with its name. /// /// Tests are always binary targets. /// /// Defined by the `[[test]]` section in `Cargo.toml`. Test(&'g str), /// A benchmark target with its name. /// /// Benchmarks are always binary targets. /// /// Defined by the `[[bench]]` section in `Cargo.toml`. Benchmark(&'g str), } impl<'g> BuildTargetId<'g> { /// Returns the name embedded in this identifier, or `None` if this is a library target. /// /// To get the name of the library target, use `BuildTarget::name`. pub fn name(&self) -> Option<&'g str> { match self { BuildTargetId::Library => None, BuildTargetId::BuildScript => None, BuildTargetId::Binary(name) => Some(name), BuildTargetId::Example(name) => Some(name), BuildTargetId::Test(name) => Some(name), BuildTargetId::Benchmark(name) => Some(name), } } pub(super) fn as_key(&self) -> &(dyn BuildTargetKey + 'g) { self } } /// The type of build target (library or binary). /// /// Obtained through `BuildTarget::kind`. #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] #[non_exhaustive] pub enum BuildTargetKind<'g> { /// This build target is a library or example, with the specified crate types. /// /// The crate types are sorted and unique, and can therefore be treated like a set. /// /// Note that examples are typically binaries, but they may be libraries as well. Binary /// examples will have the crate type `"bin"`. /// /// For more about crate types, see [The `crate-type` /// field](https://doc.rust-lang.org/nightly/cargo/reference/cargo-targets.html#the-crate-type-field) /// in the Cargo reference. LibraryOrExample(&'g [String]), /// This build target is a procedural macro. /// /// This may only be returned for `BuildTargetId::Library`. This is expressed in a `Cargo.toml` /// file as: /// /// ```toml /// [lib] /// proc-macro = true /// ``` /// /// For more about procedural macros, see [Procedural /// Macros](https://doc.rust-lang.org/reference/procedural-macros.html) in the Rust reference. ProcMacro, /// This build target is a binary target. /// /// This kind is returned for build script, binary, test, and benchmark targets. Binary, } impl<'g> BuildTargetKind<'g> { fn new(inner: &'g BuildTargetKindImpl) -> Self { match inner { BuildTargetKindImpl::LibraryOrExample(crate_types) => { BuildTargetKind::LibraryOrExample(crate_types.as_slice()) } BuildTargetKindImpl::ProcMacro => BuildTargetKind::ProcMacro, BuildTargetKindImpl::Binary => BuildTargetKind::Binary, } } } /// Stored data in a `BuildTarget`. #[derive(Clone, Debug)] pub(super) struct BuildTargetImpl { pub(super) kind: BuildTargetKindImpl, // This is only set if the id is BuildTargetId::Library. pub(super) lib_name: Option>, pub(super) required_features: Vec, pub(super) path: Box, pub(super) edition: Box, pub(super) doc_by_default: bool, pub(super) doctest_by_default: bool, pub(super) test_by_default: bool, } /// Owned version of `BuildTargetId`. #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] #[cfg_attr(all(test, feature = "proptest1"), derive(proptest_derive::Arbitrary))] pub(super) enum OwnedBuildTargetId { Library, BuildScript, Binary(Box), Example(Box), Test(Box), Benchmark(Box), } impl OwnedBuildTargetId { fn as_borrowed(&self) -> BuildTargetId<'_> { match self { OwnedBuildTargetId::Library => BuildTargetId::Library, OwnedBuildTargetId::BuildScript => BuildTargetId::BuildScript, OwnedBuildTargetId::Binary(name) => BuildTargetId::Binary(name.as_ref()), OwnedBuildTargetId::Example(name) => BuildTargetId::Example(name.as_ref()), OwnedBuildTargetId::Test(name) => BuildTargetId::Test(name.as_ref()), OwnedBuildTargetId::Benchmark(name) => BuildTargetId::Benchmark(name.as_ref()), } } } #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] #[non_exhaustive] pub(super) enum BuildTargetKindImpl { LibraryOrExample(SortedSet), ProcMacro, Binary, } // Borrow for complex keys. See https://github.com/sunshowers/borrow-complex-key-example. pub(super) trait BuildTargetKey { fn key(&self) -> BuildTargetId<'_>; } impl BuildTargetKey for BuildTargetId<'_> { fn key(&self) -> BuildTargetId<'_> { *self } } impl BuildTargetKey for OwnedBuildTargetId { fn key(&self) -> BuildTargetId<'_> { self.as_borrowed() } } impl<'g> Borrow for OwnedBuildTargetId { fn borrow(&self) -> &(dyn BuildTargetKey + 'g) { self } } impl PartialEq for dyn BuildTargetKey + '_ { fn eq(&self, other: &Self) -> bool { self.key() == other.key() } } impl Eq for dyn BuildTargetKey + '_ {} // For Borrow to be upheld, PartialOrd and Ord should be consistent. This is checked by the proptest // below. impl PartialOrd for dyn BuildTargetKey + '_ { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for dyn BuildTargetKey + '_ { fn cmp(&self, other: &Self) -> Ordering { self.key().cmp(&other.key()) } } #[cfg(all(test, feature = "proptest1"))] mod tests { use super::*; use proptest::prelude::*; impl OwnedBuildTargetId { fn as_key(&self) -> &dyn BuildTargetKey { self } } proptest! { #[test] fn consistent_borrow(id1 in any::(), id2 in any::()) { prop_assert_eq!( id1.eq(&id1), id1.as_key().eq(id1.as_key()), "consistent eq implementation (same IDs)" ); prop_assert_eq!( id1.eq(&id2), id1.as_key().eq(id2.as_key()), "consistent eq implementation (different IDs)" ); prop_assert_eq!( id1.partial_cmp(&id2), id1.as_key().partial_cmp(id2.as_key()), "consistent partial_cmp implementation" ); prop_assert_eq!( id1.cmp(&id2), id1.as_key().cmp(id2.as_key()), "consistent cmp implementation" ); } } } guppy-0.17.25/src/graph/cargo/build.rs000064400000000000000000000502511046102023000155770ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::{ DependencyKind, Error, graph::{ DependencyDirection, PackageGraph, PackageIx, PackageLink, PackageResolver, PackageSet, cargo::{ CargoIntermediateSet, CargoOptions, CargoResolverVersion, CargoSet, InitialsPlatform, }, feature::{ConditionalLink, FeatureLabel, FeatureQuery, FeatureSet, StandardFeatures}, }, platform::{EnabledTernary, PlatformSpec}, sorted_set::SortedSet, }; use fixedbitset::FixedBitSet; use petgraph::{prelude::*, visit::VisitMap}; pub(super) struct CargoSetBuildState<'a> { opts: &'a CargoOptions<'a>, omitted_packages: SortedSet>, } impl<'a> CargoSetBuildState<'a> { pub(super) fn new<'g>( graph: &'g PackageGraph, opts: &'a CargoOptions<'a>, ) -> Result { let omitted_packages: SortedSet<_> = graph.package_ixs(opts.omitted_packages.iter().copied())?; Ok(Self { opts, omitted_packages, }) } pub(super) fn build<'g>( self, initials: FeatureSet<'g>, features_only: FeatureSet<'g>, resolver: Option<&mut dyn PackageResolver<'g>>, ) -> CargoSet<'g> { match self.opts.resolver { CargoResolverVersion::V1 => self.new_v1(initials, features_only, resolver, false), CargoResolverVersion::V1Install => { let avoid_dev_deps = !self.opts.include_dev; self.new_v1(initials, features_only, resolver, avoid_dev_deps) } // V2 and V3 do the same feature resolution. CargoResolverVersion::V2 | CargoResolverVersion::V3 => { self.new_v2(initials, features_only, resolver) } } } pub(super) fn build_intermediate(self, query: FeatureQuery) -> CargoIntermediateSet { match self.opts.resolver { CargoResolverVersion::V1 => self.new_v1_intermediate(query, false), CargoResolverVersion::V1Install => { let avoid_dev_deps = !self.opts.include_dev; self.new_v1_intermediate(query, avoid_dev_deps) } CargoResolverVersion::V2 | CargoResolverVersion::V3 => self.new_v2_intermediate(query), } } fn new_v1<'g>( self, initials: FeatureSet<'g>, features_only: FeatureSet<'g>, resolver: Option<&mut dyn PackageResolver<'g>>, avoid_dev_deps: bool, ) -> CargoSet<'g> { self.build_set(initials, features_only, resolver, |query| { self.new_v1_intermediate(query, avoid_dev_deps) }) } fn new_v2<'g>( self, initials: FeatureSet<'g>, features_only: FeatureSet<'g>, resolver: Option<&mut dyn PackageResolver<'g>>, ) -> CargoSet<'g> { self.build_set(initials, features_only, resolver, |query| { self.new_v2_intermediate(query) }) } // --- // Helper methods // --- fn is_omitted(&self, package_ix: NodeIndex) -> bool { self.omitted_packages.contains(&package_ix) } fn build_set<'g>( &self, initials: FeatureSet<'g>, features_only: FeatureSet<'g>, mut resolver: Option<&mut dyn PackageResolver<'g>>, intermediate_fn: impl FnOnce(FeatureQuery<'g>) -> CargoIntermediateSet<'g>, ) -> CargoSet<'g> { // Prepare a package query for step 2. let graph = *initials.graph(); // Note that currently, proc macros specified in initials are built on both the target and // the host. let mut host_ixs = Vec::new(); let target_ixs: Vec<_> = initials .ixs_unordered() .filter_map(|feature_ix| { let metadata = graph.metadata_for_ix(feature_ix); let package_ix = metadata.package_ix(); match self.opts.initials_platform { InitialsPlatform::Host => { // Always build on the host. host_ixs.push(package_ix); None } InitialsPlatform::Standard => { // Proc macros on the host platform, everything else on the target platform. if metadata.package().is_proc_macro() { host_ixs.push(package_ix); None } else { Some(package_ix) } } InitialsPlatform::ProcMacrosOnTarget => { // Proc macros on both the host and the target platforms, everything else // on the target platform. if metadata.package().is_proc_macro() { host_ixs.push(package_ix); } Some(package_ix) } } }) .collect(); let target_query = graph .package_graph .query_from_parts(SortedSet::new(target_ixs), DependencyDirection::Forward); // 1. Build the intermediate set containing the features for any possible package that can // be built, including features-only packages. let initials_plus_features_only = initials.union(&features_only); let intermediate_set = intermediate_fn( initials_plus_features_only.to_feature_query(DependencyDirection::Forward), ); let (target_set, host_set) = intermediate_set.target_host_sets(); // While doing traversal 2 below, record any packages discovered along build edges for use // in host ixs, to prepare for step 3. This will also include proc-macros. // This list will contain proc-macro edges out of target packages. let mut proc_macro_edge_ixs = Vec::new(); // This list will contain build dep edges out of target packages. let mut build_dep_edge_ixs = Vec::new(); // This list will contain edges between target packages. let mut target_edge_ixs = Vec::new(); // This list will contain edges between host packages. let mut host_edge_ixs = Vec::new(); let is_enabled = |feature_set: &FeatureSet<'_>, link: &PackageLink<'_>, kind: DependencyKind, platform_spec: &PlatformSpec| { let (from, to) = link.endpoints(); let req_status = link.req_for_kind(kind).status(); // Check the complete set to figure out whether we look at required_on or // enabled_on. let consider_optional = feature_set .contains((from.id(), FeatureLabel::OptionalDependency(link.dep_name()))) .unwrap_or_else(|_| { // If the feature ID isn't present, it means the dependency wasn't declared // as optional. In that case the value doesn't matter. debug_assert!( req_status.optional_status().is_never(), "for {} -> {}, dep '{}' not declared as optional", from.name(), to.name(), link.dep_name() ); false }); if consider_optional { req_status.enabled_on(platform_spec) != EnabledTernary::Disabled } else { req_status.required_on(platform_spec) != EnabledTernary::Disabled } }; // Record workspace + direct third-party deps in these sets. let mut target_direct_deps = FixedBitSet::with_capacity(graph.package_graph.package_count()); let mut host_direct_deps = FixedBitSet::with_capacity(graph.package_graph.package_count()); // 2. Figure out what packages will be included on the target platform, i.e. normal + dev // (if requested). let target_platform = &self.opts.target_platform; let host_platform = &self.opts.host_platform; let target_packages = target_query.resolve_with_fn(|query, link| { let (from, to) = link.endpoints(); if from.in_workspace() { // Mark initials in target_direct_deps. target_direct_deps.visit(from.package_ix()); } if self.is_omitted(to.package_ix()) { // Pretend that the omitted set doesn't exist. return false; } let accepted = resolver .as_mut() .map(|r| r.accept(query, link)) .unwrap_or(true); if !accepted { return false; } // Dev-dependencies are only considered if `from` is an initial. let consider_dev = self.opts.include_dev && query.starts_from(from.id()).expect("valid ID"); // Build dependencies are only considered if there's a build script. let consider_build = from.has_build_script(); let mut follow_target = is_enabled(target_set, &link, DependencyKind::Normal, target_platform) || (consider_dev && is_enabled( target_set, &link, DependencyKind::Development, target_platform, )); // Proc macros build on the host, so for normal/dev dependencies redirect it to the host // instead. let proc_macro_redirect = follow_target && to.is_proc_macro(); // Build dependencies are evaluated against the host platform. let build_dep_redirect = consider_build && is_enabled(target_set, &link, DependencyKind::Build, host_platform); // Finally, process what needs to be done. if build_dep_redirect || proc_macro_redirect { if from.in_workspace() { // The 'to' node is either in the workspace or a direct dependency [a]. host_direct_deps.visit(to.package_ix()); } host_ixs.push(to.package_ix()); } if build_dep_redirect { build_dep_edge_ixs.push(link.edge_ix()); } if proc_macro_redirect { proc_macro_edge_ixs.push(link.edge_ix()); follow_target = false; } if from.in_workspace() && follow_target { // The 'to' node is either in the workspace or a direct dependency. target_direct_deps.visit(to.package_ix()); } if follow_target { target_edge_ixs.push(link.edge_ix()); } follow_target }); // 3. Figure out what packages will be included on the host platform. let host_ixs = SortedSet::new(host_ixs); let host_packages = graph .package_graph .query_from_parts(host_ixs, DependencyDirection::Forward) .resolve_with_fn(|query, link| { let (from, to) = link.endpoints(); if self.is_omitted(to.package_ix()) { // Pretend that the omitted set doesn't exist. return false; } let accepted = resolver .as_mut() .map(|r| r.accept(query, link)) .unwrap_or(true); if !accepted { return false; } // All relevant nodes in host_ixs have already been added to host_direct_deps at [a]. // Dev-dependencies are only considered if `from` is an initial. let consider_dev = self.opts.include_dev && query.starts_from(from.id()).expect("valid ID"); let consider_build = from.has_build_script(); // Only normal and build dependencies are typically considered. Dev-dependencies of // initials are also considered. let res = is_enabled(host_set, &link, DependencyKind::Normal, host_platform) || (consider_build && is_enabled(host_set, &link, DependencyKind::Build, host_platform)) || (consider_dev && is_enabled(host_set, &link, DependencyKind::Development, host_platform)); if res { if from.in_workspace() { // The 'to' node is either in the workspace or a direct dependency. host_direct_deps.visit(to.package_ix()); } host_edge_ixs.push(link.edge_ix()); true } else { false } }); // Finally, the features are whatever packages were selected, intersected with whatever // features were selected. let target_features = target_packages .to_feature_set(StandardFeatures::All) .intersection(target_set); let host_features = host_packages .to_feature_set(StandardFeatures::All) .intersection(host_set); // Also construct the direct dep sets. let target_direct_deps = PackageSet::from_included(graph.package_graph(), target_direct_deps); let host_direct_deps = PackageSet::from_included(graph.package_graph, host_direct_deps); CargoSet { initials, features_only, target_features, host_features, target_direct_deps, host_direct_deps, proc_macro_edge_ixs: SortedSet::new(proc_macro_edge_ixs), build_dep_edge_ixs: SortedSet::new(build_dep_edge_ixs), target_edge_ixs: SortedSet::new(target_edge_ixs), host_edge_ixs: SortedSet::new(host_edge_ixs), } } fn new_v1_intermediate<'g>( &self, query: FeatureQuery<'g>, avoid_dev_deps: bool, ) -> CargoIntermediateSet<'g> { // Perform a "complete" feature query. This will provide more packages than will be // included in the final build, but for each package it will have the correct feature set. let complete_set = query.resolve_with_fn(|query, link| { if self.is_omitted(link.to().package_ix()) { // Pretend that the omitted set doesn't exist. false } else if !avoid_dev_deps && query .starts_from(link.from().feature_id()) .expect("valid ID") { // Follow everything for initials. true } else { // Follow normal and build edges for everything else. !link.dev_only() } }); CargoIntermediateSet::Unified(complete_set) } fn new_v2_intermediate<'g>(&self, query: FeatureQuery<'g>) -> CargoIntermediateSet<'g> { let graph = *query.graph(); // Note that proc macros specified in initials take part in feature resolution // for both target and host ixs. If they didn't, then the query would be partitioned into // host and target ixs instead. // https://github.com/rust-lang/cargo/issues/8312 let mut host_ixs: Vec<_> = query .params .initials() .iter() .filter_map(|feature_ix| { let metadata = graph.metadata_for_ix(*feature_ix); if self.opts.initials_platform == InitialsPlatform::Host || metadata.package().is_proc_macro() { // Proc macros are always unified on the host. Some(metadata.feature_ix()) } else { // Everything else is built on the target. None } }) .collect(); let is_enabled = |link: &ConditionalLink<'_>, kind: DependencyKind, platform_spec: &PlatformSpec| { let platform_status = link.status_for_kind(kind); platform_status.enabled_on(platform_spec) != EnabledTernary::Disabled }; let target_query = if self.opts.initials_platform == InitialsPlatform::Host { // Empty query on the target. graph.query_from_parts(SortedSet::new(vec![]), DependencyDirection::Forward) } else { query }; // Keep a copy of the target query for use in step 2. let target_query_2 = target_query.clone(); // 1. Perform a feature query for the target. let target_platform = &self.opts.target_platform; let host_platform = &self.opts.host_platform; let target = target_query.resolve_with_fn(|query, link| { let (from, to) = link.endpoints(); if self.is_omitted(to.package_ix()) { // Pretend that the omitted set doesn't exist. return false; } let consider_dev = self.opts.include_dev && query.starts_from(from.feature_id()).expect("valid ID"); // This resolver doesn't check for whether this package has a build script. let mut follow_target = is_enabled(&link, DependencyKind::Normal, target_platform) || (consider_dev && is_enabled(&link, DependencyKind::Development, target_platform)); // Proc macros build on the host, so for normal/dev dependencies redirect it to the host // instead. let proc_macro_redirect = follow_target && to.package().is_proc_macro(); // Build dependencies are evaluated against the host platform. let build_dep_redirect = { // If this is a dependency like: // // ``` // [build-dependencies] // cc = { version = "1.0", optional = true } // // [features] // bundled = ["cc"] // ``` // // Then, there is an implicit named feature here called "cc" on the target platform, // which enables the optional dependency "cc". But this does not mean that this // package itself is built on the host platform! // // Detect this situation by ensuring that the package ID of the `from` and `to` // nodes are different. from.package_id() != to.package_id() && is_enabled(&link, DependencyKind::Build, host_platform) }; // Finally, process what needs to be done. if build_dep_redirect || proc_macro_redirect { host_ixs.push(to.feature_ix()); } if proc_macro_redirect { follow_target = false; } follow_target }); // 2. Perform a feature query for the host. let host = graph .query_from_parts(SortedSet::new(host_ixs), DependencyDirection::Forward) .resolve_with_fn(|_, link| { let (from, to) = link.endpoints(); if self.is_omitted(to.package_ix()) { // Pretend that the omitted set doesn't exist. return false; } // During feature resolution, the v2 resolver doesn't check for whether this package // has a build script. It also unifies dev dependencies of initials, even on the // host platform. let consider_dev = self.opts.include_dev && target_query_2 .starts_from(from.feature_id()) .expect("valid ID"); is_enabled(&link, DependencyKind::Normal, host_platform) || is_enabled(&link, DependencyKind::Build, host_platform) || (consider_dev && is_enabled(&link, DependencyKind::Development, host_platform)) }); CargoIntermediateSet::TargetHost { target, host } } } guppy-0.17.25/src/graph/cargo/cargo_api.rs000064400000000000000000000531041046102023000164240ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::{ Error, PackageId, graph::{ DependencyDirection, PackageGraph, PackageIx, PackageLink, PackageResolver, PackageSet, cargo::build::CargoSetBuildState, feature::{FeatureGraph, FeatureSet}, }, platform::PlatformSpec, sorted_set::SortedSet, }; use petgraph::prelude::*; use serde::{Deserialize, Serialize}; use std::{collections::HashSet, fmt}; /// Options for queries which simulate what Cargo does. /// /// This provides control over the resolution algorithm used by `guppy`'s simulation of Cargo. #[derive(Clone, Debug)] pub struct CargoOptions<'a> { pub(crate) resolver: CargoResolverVersion, pub(crate) include_dev: bool, pub(crate) initials_platform: InitialsPlatform, // Use Supercow here to ensure that owned Platform instances are boxed, to reduce stack size. pub(crate) host_platform: PlatformSpec, pub(crate) target_platform: PlatformSpec, pub(crate) omitted_packages: HashSet<&'a PackageId>, } impl<'a> CargoOptions<'a> { /// Creates a new `CargoOptions` with this resolver version and default settings. /// /// The default settings are similar to what a plain `cargo build` does: /// /// * use version 1 of the Cargo resolver /// * exclude dev-dependencies /// * do not build proc macros specified in the query on the target platform /// * resolve dependencies assuming any possible host or target platform /// * do not omit any packages. pub fn new() -> Self { Self { resolver: CargoResolverVersion::V1, include_dev: false, initials_platform: InitialsPlatform::Standard, host_platform: PlatformSpec::Any, target_platform: PlatformSpec::Any, omitted_packages: HashSet::new(), } } /// Sets the Cargo feature resolver version. /// /// For more about feature resolution, see the documentation for `CargoResolverVersion`. pub fn set_resolver(&mut self, resolver: CargoResolverVersion) -> &mut Self { self.resolver = resolver; self } /// If set to true, causes dev-dependencies of the initial set to be followed. /// /// This does not affect transitive dependencies -- for example, a build or dev-dependency's /// further dev-dependencies are never followed. /// /// The default is false, which matches what a plain `cargo build` does. pub fn set_include_dev(&mut self, include_dev: bool) -> &mut Self { self.include_dev = include_dev; self } /// Configures the way initials are treated on the target and the host. /// /// The default is a "standard" build and this does not usually need to be set, but some /// advanced use cases may require it. For more about this option, see the documentation for /// [`InitialsPlatform`](InitialsPlatform). pub fn set_initials_platform(&mut self, initials_platform: InitialsPlatform) -> &mut Self { self.initials_platform = initials_platform; self } /// Sets both the target and host platforms to the provided spec. pub fn set_platform(&mut self, platform_spec: impl Into) -> &mut Self { let platform_spec = platform_spec.into(); self.target_platform = platform_spec.clone(); self.host_platform = platform_spec; self } /// Sets the target platform to the provided spec. pub fn set_target_platform(&mut self, target_platform: impl Into) -> &mut Self { self.target_platform = target_platform.into(); self } /// Sets the host platform to the provided spec. pub fn set_host_platform(&mut self, host_platform: impl Into) -> &mut Self { self.host_platform = host_platform.into(); self } /// Omits edges into the given packages. /// /// This may be useful in order to figure out what additional dependencies or features a /// particular set of packages pulls in. /// /// This method is additive. pub fn add_omitted_packages( &mut self, package_ids: impl IntoIterator, ) -> &mut Self { self.omitted_packages.extend(package_ids); self } } impl Default for CargoOptions<'_> { fn default() -> Self { Self::new() } } /// The version of Cargo's feature resolver to use. #[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] #[cfg_attr(feature = "proptest1", derive(proptest_derive::Arbitrary))] #[serde(rename_all = "kebab-case")] #[non_exhaustive] pub enum CargoResolverVersion { /// The "classic" feature resolver in Rust. /// /// This feature resolver unifies features across inactive platforms, and also unifies features /// across normal, build and dev dependencies for initials. This may produce results that are /// surprising at times. #[serde(rename = "1", alias = "v1")] V1, /// The "classic" feature resolver in Rust, as used by commands like `cargo install`. /// /// This resolver is the same as `V1`, except it doesn't unify features across dev dependencies /// for initials. However, if `CargoOptions::with_dev_deps` is set to true, it behaves /// identically to the V1 resolver. /// /// For more, see /// [avoid-dev-deps](https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#avoid-dev-deps) /// in the Cargo reference. #[serde(rename = "install", alias = "v1-install")] V1Install, /// [Version 2 of the feature resolver](https://doc.rust-lang.org/cargo/reference/resolver.html#feature-resolver-version-2), /// available since Rust 1.51. This feature resolver does not unify features: /// /// * across host (build) and target (regular) dependencies /// * with dev-dependencies for initials, if tests aren't currently being built /// * with [platform-specific dependencies](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#platform-specific-dependencies) that are currently inactive /// /// Version 2 of the feature resolver can be enabled by specifying `resolver /// = "2"` in the workspace's `Cargo.toml`. It is also [the default resolver /// version](https://doc.rust-lang.org/beta/edition-guide/rust-2021/default-cargo-resolver.html) /// for [the Rust 2021 /// edition](https://doc.rust-lang.org/edition-guide/rust-2021/index.html). #[serde(rename = "2", alias = "v2")] V2, /// [Version 3 of the dependency /// resolver](https://doc.rust-lang.org/beta/cargo/reference/resolver.html#resolver-versions), /// available since Rust 1.84. /// /// Version 3 of the resolver enables [MSRV-aware dependency /// resolution](https://doc.rust-lang.org/beta/cargo/reference/config.html#resolverincompatible-rust-versions). /// There are no changes to feature resolution compared to version 2. /// /// Version 3 of the feature resolver can be enabled by specifying `resolver /// = "3"` in the workspace's `Cargo.toml`. It is also [the default resolver /// version](https://doc.rust-lang.org/beta/edition-guide/rust-2024/cargo-resolver.html) /// for [the Rust 2024 /// edition](https://doc.rust-lang.org/beta/edition-guide/rust-2024/index.html). #[serde(rename = "3", alias = "v3")] V3, } /// For a given Cargo build simulation, what platform to assume the initials are being built on. #[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] #[cfg_attr(feature = "proptest1", derive(proptest_derive::Arbitrary))] #[serde(rename_all = "kebab-case")] pub enum InitialsPlatform { /// Assume that the initials are being built on the host platform. /// /// This is most useful for "continuing" simulations, where it is already known that some /// packages are being built on the host and one wishes to find their dependencies. Host, /// Assume a standard build. /// /// In this mode, all initials other than proc-macros are built on the target platform. Proc- /// macros, being compiler plugins, are built on the host. /// /// This is the default for `InitialsPlatform`. Standard, /// Perform a standard build, and also build proc-macros on the target. /// /// Proc-macro crates may include tests, which are run on the target platform. This option is /// most useful for such situations. ProcMacrosOnTarget, } /// The default for `InitialsPlatform`: the `Standard` option. impl Default for InitialsPlatform { fn default() -> Self { InitialsPlatform::Standard } } /// A set of packages and features, as would be built by Cargo. /// /// Cargo implements a set of algorithms to figure out which packages or features are built in /// a given situation. `guppy` implements those algorithms. #[derive(Clone, Debug)] pub struct CargoSet<'g> { pub(super) initials: FeatureSet<'g>, pub(super) features_only: FeatureSet<'g>, pub(super) target_features: FeatureSet<'g>, pub(super) host_features: FeatureSet<'g>, pub(super) target_direct_deps: PackageSet<'g>, pub(super) host_direct_deps: PackageSet<'g>, pub(super) proc_macro_edge_ixs: SortedSet>, pub(super) build_dep_edge_ixs: SortedSet>, pub(super) target_edge_ixs: SortedSet>, pub(super) host_edge_ixs: SortedSet>, } assert_covariant!(CargoSet); impl<'g> CargoSet<'g> { /// Simulates a Cargo build of this feature set, with the given options. /// /// The feature sets are expected to be entirely within the workspace. Its behavior outside the /// workspace isn't defined and may be surprising. /// /// `CargoSet::new` takes two `FeatureSet` instances: /// * `initials`, from which dependencies are followed to build the `CargoSet`. /// * `features_only`, which are additional inputs that are only used for feature /// unification. This may be used to simulate, e.g. `cargo build --package foo --package bar`, /// when you only care about the results of `foo` but specifying `bar` influences the build. /// /// Note that even if a package is in `features_only`, it may be included in the final build set /// through other means (for example, if it is also in `initials` or it is a dependency of one /// of them). /// /// In many cases `features_only` is empty -- in that case you may wish to use /// `FeatureSet::into_cargo_set()`, and it may be more convenient to use that if the code is /// written in a "fluent" style. /// /// pub fn new( initials: FeatureSet<'g>, features_only: FeatureSet<'g>, opts: &CargoOptions<'_>, ) -> Result { Self::new_internal(initials, features_only, None, opts) } /// Like `Cargo.new`, but takes an additional [`PackageResolver`] which can /// be used to filter out some dependency edges, or to collect additional /// information. /// /// [`resolver.accept`] is called for both target and host dependencies. It /// is called after static filtering through /// [`CargoOptions::add_omitted_packages`], but before any other decisions /// are made. /// /// [`resolver.accept`]: PackageResolver::accept pub fn with_package_resolver( initials: FeatureSet<'g>, features_only: FeatureSet<'g>, mut resolver: impl PackageResolver<'g>, opts: &CargoOptions<'_>, ) -> Result { Self::new_internal(initials, features_only, Some(&mut resolver), opts) } /// Internal helper to deduplicate code across `CargoSet::new` and `CargoSet::with_resolver`. fn new_internal( initials: FeatureSet<'g>, features_only: FeatureSet<'g>, resolver: Option<&mut dyn PackageResolver<'g>>, opts: &CargoOptions<'_>, ) -> Result { let build_state = CargoSetBuildState::new(initials.graph().package_graph, opts)?; Ok(build_state.build(initials, features_only, resolver)) } /// Creates a new `CargoIntermediateSet` based on the given query and options. /// /// This set contains an over-estimate of targets and features. /// /// Not part of the stable API, exposed for testing. #[doc(hidden)] pub fn new_intermediate( initials: &FeatureSet<'g>, opts: &CargoOptions<'_>, ) -> Result, Error> { let build_state = CargoSetBuildState::new(initials.graph().package_graph, opts)?; Ok(build_state.build_intermediate(initials.to_feature_query(DependencyDirection::Forward))) } /// Returns the feature graph for this `CargoSet` instance. pub fn feature_graph(&self) -> &FeatureGraph<'g> { self.initials.graph() } /// Returns the package graph for this `CargoSet` instance. pub fn package_graph(&self) -> &'g PackageGraph { self.feature_graph().package_graph } /// Returns the initial packages and features from which the `CargoSet` instance was /// constructed. pub fn initials(&self) -> &FeatureSet<'g> { &self.initials } /// Returns the packages and features that took part in feature unification but were not /// considered part of the final result. /// /// For more about `features_only` and how it influences the build, see the documentation for /// [`CargoSet::new`](CargoSet::new). pub fn features_only(&self) -> &FeatureSet<'g> { &self.features_only } /// Returns the feature set enabled on the target platform. /// /// This represents the packages and features that are included as code in the final build /// artifacts. This is relevant for both cross-compilation and auditing. pub fn target_features(&self) -> &FeatureSet<'g> { &self.target_features } /// Returns the feature set enabled on the host platform. /// /// This represents the packages and features that influence the final build artifacts, but /// whose code is generally not directly included. /// /// This includes all procedural macros, including those specified in the initial query. pub fn host_features(&self) -> &FeatureSet<'g> { &self.host_features } /// Returns the feature set enabled on the specified build platform. pub fn platform_features(&self, build_platform: BuildPlatform) -> &FeatureSet<'g> { match build_platform { BuildPlatform::Target => self.target_features(), BuildPlatform::Host => self.host_features(), } } /// Returns the feature sets across the target and host build platforms. pub fn all_features(&self) -> [(BuildPlatform, &FeatureSet<'g>); 2] { [ (BuildPlatform::Target, self.target_features()), (BuildPlatform::Host, self.host_features()), ] } /// Returns the set of workspace and direct dependency packages on the target platform. /// /// The packages in this set are a subset of the packages in `target_features`. pub fn target_direct_deps(&self) -> &PackageSet<'g> { &self.target_direct_deps } /// Returns the set of workspace and direct dependency packages on the host platform. /// /// The packages in this set are a subset of the packages in `host_features`. pub fn host_direct_deps(&self) -> &PackageSet<'g> { &self.host_direct_deps } /// Returns the set of workspace and direct dependency packages on the specified build platform. pub fn platform_direct_deps(&self, build_platform: BuildPlatform) -> &PackageSet<'g> { match build_platform { BuildPlatform::Target => self.target_direct_deps(), BuildPlatform::Host => self.host_direct_deps(), } } /// Returns the set of workspace and direct dependency packages across the target and host /// build platforms. pub fn all_direct_deps(&self) -> [(BuildPlatform, &PackageSet<'g>); 2] { [ (BuildPlatform::Target, self.target_direct_deps()), (BuildPlatform::Host, self.host_direct_deps()), ] } /// Returns `PackageLink` instances for procedural macro dependencies from target packages. /// /// Procedural macros straddle the line between target and host: they're built for the host /// but generate code that is compiled for the target platform. /// /// ## Notes /// /// Procedural macro packages will be included in the *host* feature set. /// See also [`Self::host_features`]. /// /// The returned iterator will include proc macros that are depended on normally or in dev /// builds from initials (if `include_dev` is set), but not the ones in the /// `[build-dependencies]` section. pub fn proc_macro_links<'a>(&'a self) -> impl ExactSizeIterator> + 'a { let package_graph = self.target_features.graph().package_graph; self.proc_macro_edge_ixs .iter() .map(move |edge_ix| package_graph.edge_ix_to_link(*edge_ix)) } /// Returns `PackageLink` instances for build dependencies from target packages. /// /// ## Notes /// /// For each link, the `from` is built on the target while the `to` is built on the host. /// It is possible (though rare) that a build dependency is also included as a normal /// dependency, or as a dev dependency in which case it will also be built on the target. /// /// The returned iterators will not include build dependencies of host packages -- those are /// also built on the host. pub fn build_dep_links<'a>(&'a self) -> impl ExactSizeIterator> + 'a { let package_graph = self.target_features.graph().package_graph; self.build_dep_edge_ixs .iter() .map(move |edge_ix| package_graph.edge_ix_to_link(*edge_ix)) } /// Returns `PackageLink` instances for normal dependencies between target packages. /// /// ## Notes /// /// For each link, both the `from` and the `to` package are built on the target. /// /// Target packages will be included in the *target* feature set. /// See also [`Self::target_features`]. pub fn target_links<'a>(&'a self) -> impl ExactSizeIterator> + 'a { let package_graph = self.target_features.graph().package_graph; self.target_edge_ixs .iter() .map(move |edge_ix| package_graph.edge_ix_to_link(*edge_ix)) } /// Returns `PackageLink` instances for dependencies between host packages. /// /// ## Notes /// /// For each link, both the `from` and the `to` package are built on the host. /// Typically most links are normal dependencies, but it is possible to have /// build dependencies as well (e.g. dependencies of a build script used /// in a proc-macro package). /// /// Host packages will be included in the *host* feature set. /// See also [`Self::host_features`]. pub fn host_links<'a>(&'a self) -> impl ExactSizeIterator> + 'a { let package_graph = self.target_features.graph().package_graph; self.host_edge_ixs .iter() .map(move |edge_ix| package_graph.edge_ix_to_link(*edge_ix)) } } /// Either the target or the host platform. /// /// When Cargo computes the platforms it is building on, it computes two separate build graphs: one /// for the target platform and one for the host. This is most useful in cross-compilation /// situations where the target is different from the host, but the separate graphs are computed /// whether or not a build cross-compiles. /// /// A `cargo check` can be looked at as a kind of cross-compilation as well--machine code is /// generated and run for the host platform but not the target platform. This is why `cargo check` /// output usually has some lines that say `Compiling` (for the host platform) and some that say /// `Checking` (for the target platform). #[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum BuildPlatform { /// The target platform. /// /// This represents the packages and features that are included as code in the final build /// artifacts. Target, /// The host platform. /// /// This represents build scripts, proc macros and other code that is run on the machine doing /// the compiling. Host, } impl BuildPlatform { /// A list of all possible variants of `BuildPlatform`. pub const VALUES: &'static [Self; 2] = &[BuildPlatform::Target, BuildPlatform::Host]; /// Returns the build platform that's not `self`. pub fn flip(self) -> Self { match self { BuildPlatform::Host => BuildPlatform::Target, BuildPlatform::Target => BuildPlatform::Host, } } } impl fmt::Display for BuildPlatform { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { BuildPlatform::Target => write!(f, "target"), BuildPlatform::Host => write!(f, "host"), } } } /// An intermediate set representing an overestimate of what packages are built, but an accurate /// summary of what features are built given a particular package. /// /// Not part of the stable API, exposed for cargo-compare. #[doc(hidden)] #[derive(Debug)] pub enum CargoIntermediateSet<'g> { Unified(FeatureSet<'g>), TargetHost { target: FeatureSet<'g>, host: FeatureSet<'g>, }, } impl<'g> CargoIntermediateSet<'g> { #[doc(hidden)] pub fn target_host_sets(&self) -> (&FeatureSet<'g>, &FeatureSet<'g>) { match self { CargoIntermediateSet::Unified(set) => (set, set), CargoIntermediateSet::TargetHost { target, host } => (target, host), } } } guppy-0.17.25/src/graph/cargo/mod.rs000064400000000000000000000005541046102023000152600ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 //! Simulations of Cargo behavior. //! //! Cargo comes with a set of algorithms to figure out what packages or features are built. This //! module reimplements those algorithms using `guppy`'s data structures. pub(super) mod build; mod cargo_api; pub use cargo_api::*; guppy-0.17.25/src/graph/cycles.rs000064400000000000000000000342221046102023000146670ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 //! Code for handling cycles in dependency graphs. //! //! See [`Cycles`][] for detailed docs. use crate::{ Error, PackageId, graph::{PackageGraph, PackageIx}, petgraph_support::scc::Sccs, }; /// Contains information about dependency cycles. /// /// More accurately, information about Strongly Connected Components with 2 or more elements. /// Constructed through `PackageGraph::cycles`. /// /// This page includes a bunch of detailed information on cycles, but here's the TLDR: /// /// * Yes, cycles can happen /// * Cycles only happen with dev-dependencies /// * These cycles have properties that make them easy to handle /// * We handle this in APIs like [`PackageSet::packages`][`crate::graph::PackageSet::packages`] /// * As a result, you probably don't actually need this module /// /// The slighly more detailed summary is that any graph of "packages" is conflating /// the "real" package with its tests, which are actually separate binaries. These /// tests *always* depend on the "real" package, and if we bothered to encode that /// then any package with tests would have a cyclic dependency on itself -- so we /// don't encode that. Unfortunately dev-dependencies allow tests to *indirectly* /// depend on the "real" package, creating a cycle you *do* see. /// /// If you only care about "real" builds, you can simply ignore the dev-dependency /// edges and restore a nice and simple DAG that can be topologically sorted. This is what /// we do for you in APIs like [`PackageSet::packages`][`crate::graph::PackageSet::packages`]. /// /// If you care about tests and dev-dependencies, we recommend treating those as /// different from the "real" ones (essentially desugarring the package into two nodes). /// Because all dev builds are roots of the package graph (nothing depends on a test/benchmark), /// they can always go at the start/end (depending on direction) of the topological sort. /// This means you can just do add a second loop before/after the "real" one. /// /// For instance, here's a simple program that recursively computes some property of packages /// (here "whether serde is a transitive dependency"): /// /// ``` /// use guppy::{CargoMetadata, graph::DependencyDirection}; /// use std::collections::HashMap; /// /// let metadata = CargoMetadata::parse_json(include_str!("../../../fixtures/small/metadata1.json")).unwrap(); /// let package_graph = metadata.build_graph().unwrap(); /// let workspace_members = package_graph.resolve_workspace(); /// let dependency_graph = package_graph.query_workspace().resolve(); /// /// // Whether the "real" package uses serde /// let mut package_uses_serde = HashMap::new(); /// // Whether the "dev" package uses serde /// let mut dev_package_uses_serde = HashMap::new(); /// /// // Iterate over packages in reverse topo order (process dependencies first) /// for package in dependency_graph.packages(DependencyDirection::Reverse) { /// // A package uses serde if... /// let uses_serde = if package.name() == "serde" { /// // It is literally serde (base case) /// true /// } else { /// // It has a non-dev-dependency on a package which uses serde /// // (dev-dependencies handled in the second loop) /// package.direct_links().any(|link| { /// !link.dev_only() && package_uses_serde[link.to().id()] /// }) /// }; /// // Record this package's result /// package_uses_serde.insert(package.id(), uses_serde); /// } /// /// // Now iterate over the workspace members to handle their tests (if any) /// // Note that DependencyDirection doesn't matter here, we're literally /// // just looping over every workspace member in arbitrary order! /// for package in workspace_members.packages(DependencyDirection::Reverse) { /// // Check dev-packages using the "real" package results for all links! /// let uses_serde = package.direct_links().any(|link| { /// package_uses_serde[link.to().id()] /// }); /// // Record this dev-package's result /// dev_package_uses_serde.insert(package.id(), uses_serde); /// } /// /// // Now we have all the values computed! /// for (id, &uses_serde) in &package_uses_serde { /// if uses_serde { /// let name = package_graph.metadata(id).unwrap().name(); /// println!("{name} uses serde!"); /// } /// } /// for (id, &uses_serde) in &dev_package_uses_serde { /// if uses_serde { /// let name = package_graph.metadata(id).unwrap().name(); /// println!("{name}'s tests use serde!"); /// } /// } /// ``` /// /// /// /// /// /// # Why Cargo Dependency Graphs Have Cycles /// /// Dependency graphs are generally Directed Acyclic Graphs (DAGs), where each package /// is a node and each dependency is an edge. These graphs are acyclic (contain no cycles) /// because anything else would be a paradox -- how do you build X if it depends on itself? /// You don't! /// /// So why does this API exist? It wouldn't make sense for Cargo to have cycles! /// /// The problem is that "the Cargo dependency graph" is actually two different graphs /// at different levels of abstraction: The Package Graph (Guppy, cargo-metadata), and /// The Build Graph (Cargo's internals). These two graphs are different because each /// package is actually a bunch of different /// [build targets in a trenchcoat][`crate::graph::PackageMetadata::build_targets`] -- libs, /// bins, tests, benches, and so on. In The Build Graph these different build targets get /// their own nodes. In The Package Graph all those targets gets merged together into one /// big node. The Build Graph is always a proper DAG, but The Package Graph can have cycles. /// /// Thankfully these cycles can only be created by one specific (and rare) situation: /// dev-dependencies. **A test/bench target for a package is allowed to indirectly /// depend on the same package's lib/bin target, and this creates apparent cycles /// in the package graph!** That's it! /// /// As we'll see, **simply ignoring all dev-dependency edges eliminates all cycles /// *and* preserves the ordering constraints of the dependency graph.** /// /// /// /// # An Example Cyclic Workspace /// /// As a concrete example, consider [the serde workspace][serde_github], which /// actually has this "problem": there's a "cycle" between serde and serde_derive. /// In normal builds this cycle doesn't exist: serde_derive is actually a standalone /// crate, while [serde (optionally) pulls in serde_derive as a dependency][serde_toml]. /// The "cycle" only appears when testing serde_derive: [serde_derive's tests quite /// reasonably depend on serde][serde_derive_toml] to test the proc-macro's output, /// creating a cycle! /// /// The way to resolve this monstrosity is to realize that the tests for serde_derive /// are actually a completely different binary from the serde_derive *library*. Let's /// call those tests serde_derive_dev. So although the (Package) graph reported by Guppy /// (and cargo-metadata) looks like a cycle: /// /// ```text /// serde <-----+ /// | | /// | | /// +--> serde_derive /// ``` /// /// In actuality, serde_derive_dev breaks the cycle and creates a nice clean DAG /// (in The Build Graph): /// /// ```text /// +-- serde_derive_dev /// | | /// v | /// serde | /// | | /// | v /// +---> serde_derive /// ``` /// /// Here's the really important thing to notice: serde_derive_dev is actually a *root* /// in The Build Graph, and this is always true! Nothing should ever depend on the *tests* /// or *benchmarks* for another library. /// /// This is the key insight to ignoring dev-dependency edges. As we'll see, the roots /// (and leaves) of a DAG are in some sense "ignorable" by the rest of the graph, /// because they can't change the ordering constraints between other packages. /// /// /// /// # Topological Sort Is Great (And Composable) /// /// Now that we understand *why* cycles can happen in the package graph, let's look at /// what those cycles mess up, and how to deal with them. /// /// One of the big reasons everyone loves DAGs is because you can get a Topological /// Sort of them. Topological Sort /// (with [`DependencyDirection::Forward`][`crate::graph::DependencyDirection::Forward`]) /// is just a fancy way of saying "a list where packages always appear before their dependencies" /// (vice-versa for [`DependencyDirection::Reverse`][`crate::graph::DependencyDirection::Reverse`]). /// /// This is really convenient! If you need to do things in "dependency order" you can just /// topologically sort the packages and then boring old for-loops will magically get /// everything done before it's needed. /// /// Unfortunately, you can't get the Topological Sort of a graph with cycles because that /// doesn't make sense. And yet, Guppy has /// [several APIs which do exactly that][`crate::graph::PackageSet::packages`]. /// What gives? The docs say: /// /// > The packages within a dependency cycle will be returned in non-dev order. When the /// > direction is forward, if package Foo has a dependency on Bar, and Bar has a cyclic /// > dev-dependency on Foo, then Foo is returned before Bar. /// /// We just ignore the dev-dependency edges! Problem Solved. /// /// But isn't this throwing out important information that could change the result? Nope! /// /// As we saw in the previous section, all dev-builds are roots in The Build Graph. /// Ignoring all dev-dependency edges is equivalent to deleting all of those roots. /// This may "orphan" dependencies that are only used for dev-builds, but we still /// keep them in the graph and properly include them in the sort. /// /// As it turns out, you can recursively compute the topological sort of a graph as follows: /// /// 1. delete a root (or leaf) /// 2. compute the topological sort of the new graph /// 3. append the root (or leaf) to the start (or end) of the list /// /// **Even although we delete all the dev-nodes from the graph when doing our sort, /// if you want to "add them back" the only thing you need to do is handle them before /// (or after) everything else!** Even better: all the dev-builds are roots at the same /// time, so you can process them in any order! /// /// Just remember that every node with dev-dependencies is really two nodes: the "normal" /// version without dev-dependencies, and the version with them. Exactly how you want /// to express that notion in your code is up to you. (Two different loops is the simplest.) /// /// /// /// /// # Reasoning About Cycles: Strongly Connected Components /// /// Ok but wait, none of that involved Strongly Connected Components! Yeah, isn't that great? 😄 /// /// Oh you still want to "know" about the cycles? Then we've gotta bust out the heavy /// general-purpose machinery. Thankfully the problem of cycles in directed graphs is /// an old and well-studied problem with a conceptually simple solution: hide the cycle /// in a box and pretend that it's just one Really Big Node in the DAG. /// /// Yes, really, that's all that Strongly Connected Components are. More precisely, SCCs /// are defined to be maximal sets of nodes such that "every node in an SCC can reach /// every other node in that SCC" (a property which definitely holds for cycles). /// The reason for this more complicated definition is that you can have a bunch of /// cycles all knotted together in a nasty ball, and trying to tease out individual /// cycles isn't really helpful. So we just wrap the whole ball of nodes up into one /// big "I give up" box and forget about it! /// /// Now, what does this get us? /// /// The graph *between* Strongly Connected Components is *always* a DAG, so you can /// always topologically sort *that*. In really nasty cases this is just vacuously /// true (all the nodes end up in one SCC, and so the "Graph of SCCs" is just one big /// unsorted node). On the other hand, if the graph already *is* a DAG then each node /// is its own SCC, and so we lose nothing. In this way SCCs give us a way to preserve /// all the *nice* parts of our graph while also isolating the problematic parts /// (SCCs with more than 1 node) to something self-contained that we can handle specially. /// /// In the general case, nothing more can be done to order an SCC. By definition every /// node depends on every other node! But as we've seen in the previous section, there /// actually *is* a good way to order packages even with cycles, and so we maintain /// that ordering for our SCCs: it's just the topological sort with all the /// dev-dependencies ignored. /// /// /// /// /// [serde_github]: https://github.com/serde-rs/serde /// [serde_toml]: https://github.com/serde-rs/serde/blob/072145e0e913df7686f001dbf29e43a0ff7afac4/serde/Cargo.toml#L17-L18 /// [serde_derive_toml]: https://github.com/serde-rs/serde/blob/072145e0e913df7686f001dbf29e43a0ff7afac4/serde_derive/Cargo.toml#L29-L30 pub struct Cycles<'g> { package_graph: &'g PackageGraph, sccs: &'g Sccs, } impl<'g> Cycles<'g> { pub(super) fn new(package_graph: &'g PackageGraph) -> Self { Self { package_graph, sccs: package_graph.sccs(), } } /// Returns true if these two IDs are in the same cycle. /// /// This is equivalent to checking if they're in the same Strongly Connected Component. pub fn is_cyclic(&self, a: &PackageId, b: &PackageId) -> Result { let a_ix = self.package_graph.package_ix(a)?; let b_ix = self.package_graph.package_ix(b)?; Ok(self.sccs.is_same_scc(a_ix, b_ix)) } /// Returns all the Strongly Connected Components (SCCs) of 2 or more elements in this graph. /// /// SCCs are returned in topological order: if packages in SCC B depend on packages in SCC /// A, A is returned before B. /// /// Within an SCC, nodes are returned in non-dev order: if package Foo has a dependency on Bar, /// and Bar has a cyclic dev-dependency on Foo, then Foo is returned before Bar. /// /// See the type-level docs for details. pub fn all_cycles(&self) -> impl DoubleEndedIterator> + 'g + use<'g> { let dep_graph = &self.package_graph.dep_graph; self.sccs .multi_sccs() .map(move |scc| scc.iter().map(move |ix| &dep_graph[*ix]).collect()) } } guppy-0.17.25/src/graph/feature/build.rs000064400000000000000000000617471046102023000161530ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::{ errors::{FeatureBuildStage, FeatureGraphWarning}, graph::{ DepRequiredOrOptional, DependencyReq, FeatureIndexInPackage, FeatureIx, NamedFeatureDep, PackageGraph, PackageIx, PackageLink, PackageMetadata, feature::{ ConditionalLinkImpl, FeatureEdge, FeatureGraphImpl, FeatureLabel, FeatureMetadataImpl, FeatureNode, WeakDependencies, WeakIndex, }, }, platform::PlatformStatusImpl, }; use ahash::AHashMap; use cargo_metadata::DependencyKind; use once_cell::sync::OnceCell; use petgraph::{prelude::*, visit::IntoEdgeReferences}; use smallvec::SmallVec; use std::iter; pub(super) type FeaturePetgraph = Graph; pub(super) type FeatureEdgeReference<'g> = <&'g FeaturePetgraph as IntoEdgeReferences>::EdgeRef; #[derive(Debug)] pub(super) struct FeatureGraphBuildState { graph: FeaturePetgraph, // Map from package ixs to the base (first) feature for each package. base_ixs: Vec>, map: AHashMap, weak: WeakDependencies, warnings: Vec, } impl FeatureGraphBuildState { pub(super) fn new(package_graph: &PackageGraph) -> Self { let package_count = package_graph.package_count(); Self { // Each package corresponds to at least one feature ID. graph: Graph::with_capacity(package_count, package_count), // Each package corresponds to exactly one base feature ix, and there's one last ix at // the end. base_ixs: Vec::with_capacity(package_count + 1), map: AHashMap::with_capacity(package_count), weak: WeakDependencies::new(), warnings: vec![], } } /// Add nodes for every feature in this package + the base package, and add edges from every /// feature to the base package. pub(super) fn add_nodes(&mut self, package: PackageMetadata<'_>) { let base_node = FeatureNode::base(package.package_ix()); let base_ix = self.add_node(base_node); self.base_ixs.push(base_ix); FeatureNode::named_features(package) .chain(FeatureNode::optional_deps(package)) .for_each(|feature_node| { let feature_ix = self.add_node(feature_node); self.graph .update_edge(feature_ix, base_ix, FeatureEdge::FeatureToBase); }); } /// Mark the end of adding nodes. pub(super) fn end_nodes(&mut self) { self.base_ixs.push(NodeIndex::new(self.graph.node_count())); } pub(super) fn add_named_feature_edges(&mut self, metadata: PackageMetadata<'_>) { let dep_name_to_link: AHashMap<_, _> = metadata .direct_links() .map(|link| (link.dep_name(), link)) .collect(); metadata .named_features_full() .for_each(|(n, from_feature, feature_deps)| { let from_node = FeatureNode::new(metadata.package_ix(), n); let to_nodes_edges: Vec<_> = feature_deps .iter() .flat_map(|feature_dep| { self.nodes_for_named_feature_dep( metadata, from_feature, feature_dep, &dep_name_to_link, ) }) // The flat_map above holds an &mut reference to self, which is why it needs to // be collected. .collect(); // 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_edges, metadata.graph()); }) } fn nodes_for_named_feature_dep( &mut self, metadata: PackageMetadata<'_>, from_named_feature: &str, feature_dep: &NamedFeatureDep, dep_name_to_link: &AHashMap<&str, PackageLink>, ) -> SmallVec<[(FeatureNode, FeatureEdge); 3]> { let from_label = FeatureLabel::Named(from_named_feature); let mut nodes_edges: SmallVec<[(FeatureNode, FeatureEdge); 3]> = SmallVec::new(); match feature_dep { NamedFeatureDep::DependencyNamedFeature { dep_name, feature, weak, } => { if let Some(link) = dep_name_to_link.get(dep_name.as_ref()) { let weak_index = weak.then(|| self.weak.insert(link.edge_ix())); // Dependency from (`main`, `a`) to (`dep, `foo`) if let Some(cross_node) = self.make_named_feature_node( &metadata, from_label, &link.to(), FeatureLabel::Named(feature.as_ref()), true, ) { // This is a cross-package link. The platform-specific // requirements still apply, so grab them from the // PackageLink. nodes_edges.push(( cross_node, Self::make_named_feature_cross_edge(link, weak_index), )); }; // If the package is present as an optional dependency, it is // implicitly activated by the feature: // from (`main`, `a`) to (`main`, `dep:dep`) if let Some(same_node) = self.make_named_feature_node( &metadata, from_label, &metadata, FeatureLabel::OptionalDependency(dep_name), // Don't warn if this dep isn't optional. false, ) { nodes_edges.push(( same_node, Self::make_named_feature_cross_edge(link, weak_index), )); } // Finally, (`main`, `a`) to (`main`, `dep`) -- if this is a non-weak dependency // and a named feature by this name is present, it also gets activated (even if // the named feature has no relation to the optional dependency). // // For example: // // server = ["hyper/server"] // // will also activate the named feature `hyper`. // // One thing to be careful of here is that we don't want to insert self-edges. // For example: // // tokio = ["dep:tokio", "tokio/net"] // // should not insert a self-edge from `tokio` to `tokio`. The second condition // checks this. if !*weak && &**dep_name != from_named_feature { if let Some(same_named_feature_node) = self.make_named_feature_node( &metadata, from_label, &metadata, FeatureLabel::Named(dep_name), // Don't warn if this dep isn't optional. false, ) { nodes_edges.push(( same_named_feature_node, Self::make_named_feature_cross_edge(link, None), )); } } } } NamedFeatureDep::NamedFeature(feature_name) => { if let Some(same_node) = self.make_named_feature_node( &metadata, from_label, &metadata, FeatureLabel::Named(feature_name.as_ref()), true, ) { nodes_edges.push((same_node, FeatureEdge::NamedFeature)); } } NamedFeatureDep::OptionalDependency(dep_name) => { if let Some(same_node) = self.make_named_feature_node( &metadata, from_label, &metadata, FeatureLabel::OptionalDependency(dep_name.as_ref()), true, ) { if let Some(link) = dep_name_to_link.get(dep_name.as_ref()) { nodes_edges.push(( same_node, FeatureEdge::NamedFeatureDepColon( Self::make_full_conditional_link_impl(link), ), )); } } } }; nodes_edges } fn make_named_feature_node( &mut self, from_package: &PackageMetadata<'_>, from_label: FeatureLabel<'_>, to_package: &PackageMetadata<'_>, to_label: FeatureLabel<'_>, warn: bool, ) -> Option { match to_package.get_feature_idx(to_label) { Some(idx) => Some(FeatureNode::new(to_package.package_ix(), idx)), None => { // It is possible to specify a feature that doesn't actually exist, and cargo will // accept that if the feature isn't resolved. One example is the cfg-if crate, where // version 0.1.9 has the `rustc-dep-of-std` feature commented out, and several // crates try to enable that feature: // https://github.com/alexcrichton/cfg-if/issues/22 // // Since these aren't fatal errors, it seems like the best we can do is to store // such issues as warnings. if warn { self.warnings.push(FeatureGraphWarning::MissingFeature { stage: FeatureBuildStage::AddNamedFeatureEdges { package_id: from_package.id().clone(), from_feature: from_label.to_string(), }, package_id: to_package.id().clone(), feature_name: to_label.to_string(), }); } None } } } /// Creates the cross link for situations like: /// /// ```toml /// [features] /// a = ["dep/foo"] /// ``` /// /// (a link (`from`, `a`) to (`dep`, `foo`) is created. /// /// If `dep` is optional, the edge (`from`, `a`) to (`from`, `dep`) is also a /// `NamedFeatureWithSlash` edge. fn make_named_feature_cross_edge( link: &PackageLink<'_>, weak_index: Option, ) -> FeatureEdge { // This edge is enabled if the feature is enabled, which means the union of (required, // optional) build conditions. FeatureEdge::NamedFeatureWithSlash { link: Self::make_full_conditional_link_impl(link), weak_index, } } // Creates a "full" conditional link, unifying requirements across all dependency lines. // This should not be used in add_dependency_edges below! fn make_full_conditional_link_impl(link: &PackageLink<'_>) -> ConditionalLinkImpl { // This edge is enabled if the feature is enabled, which means the union of (required, // optional) build conditions. fn combine_req_opt(req: DependencyReq<'_>) -> PlatformStatusImpl { let mut required = req.inner.required.build_if.clone(); required.extend(&req.inner.optional.build_if); required } ConditionalLinkImpl { package_edge_ix: link.edge_ix(), normal: combine_req_opt(link.normal()), build: combine_req_opt(link.build()), dev: combine_req_opt(link.dev()), } } pub(super) fn add_dependency_edges(&mut self, link: PackageLink<'_>) { let from = link.from(); // Sometimes the same package is depended on separately in different sections like so: // // bar/Cargo.toml: // // [dependencies] // foo = { version = "1", features = ["a"] } // // [build-dependencies] // foo = { version = "1", features = ["b"] } // // Now if you have a crate 'baz' with: // // [dependencies] // bar = { path = "../bar" } // // ... what features would you expect foo to be built with? You might expect it to just // be built with "a", but as it turns out Cargo actually *unifies* the features, such // that foo is built with both "a" and "b". // // Also, feature unification is impacted by whether the dependency is optional. // // [dependencies] // foo = { version = "1", features = ["a"] } // // [build-dependencies] // foo = { version = "1", optional = true, features = ["b"] } // // This will include 'foo' as a normal dependency but *not* as a build dependency by // 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 'required edge', which will be from the base node for 'from' to the feature nodes // for each required feature in 'to'. // * an 'optional edge', which will be from the feature node (from, dep_name) to the // feature nodes for each optional feature in 'to'. This edge is only added if at least // one line is optional. let unified_metadata = iter::once((DependencyKind::Normal, link.normal())) .chain(iter::once((DependencyKind::Build, link.build()))) .chain(iter::once((DependencyKind::Development, link.dev()))); let mut required_req = FeatureReq::new(link); let mut optional_req = FeatureReq::new(link); for (kind, dependency_req) in unified_metadata { required_req.add_features(kind, &dependency_req.inner.required, &mut self.warnings); optional_req.add_features(kind, &dependency_req.inner.optional, &mut self.warnings); } // Add the required edges (base -> features). self.add_edges( FeatureNode::base(from.package_ix()), required_req.finish(), link.from().graph(), ); 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(FeatureLabel::OptionalDependency(link.dep_name())) .unwrap_or_else(|| { panic!( "while adding feature edges, for package '{}', optional dep '{}' missing", from.id(), link.dep_name(), ); }), ); self.add_edges(from_node, optional_req.finish(), link.from().graph()); } } fn add_node(&mut self, feature_id: FeatureNode) -> NodeIndex { let feature_ix = self.graph.add_node(feature_id); self.map .insert(feature_id, FeatureMetadataImpl { feature_ix }); feature_ix } fn add_edges( &mut self, from_node: FeatureNode, to_nodes_edges: impl IntoIterator, graph: &PackageGraph, ) { // The from node should always be present because it is a known node. let from_ix = self.lookup_node(&from_node).unwrap_or_else(|| { panic!("while adding feature edges, missing 'from': {from_node:?}"); }); let to_nodes_edges = to_nodes_edges.into_iter().collect::>(); 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:?}")); if from_ix == to_ix { let (package_id, feature_label) = from_node.package_id_and_feature_label(graph); self.warnings.push(FeatureGraphWarning::SelfLoop { package_id: package_id.clone(), feature_name: feature_label.to_string(), }); } match self.graph.find_edge(from_ix, to_ix) { Some(edge_ix) => { // The edge already exists. This could be an upgrade from a cross link to a // feature dependency, for example: // // [package] // name = "main" // // [dependencies] // dep = { ..., optional = true } // // [features] // "feat" = ["dep/feat", "dep"] // // "dep/feat" causes a cross link to be created from "main/feat" to "main/dep". // However, the "dep" encountered later upgrades this link to a feature // dependency. // // This could also be an upgrade from a weak to a non-weak dependency: // // [features] // feat = ["dep?/feat", "dep/feat2"] let old_edge = self .graph .edge_weight_mut(edge_ix) .expect("this edge was just found"); #[allow(clippy::single_match)] match (old_edge, edge) { ( FeatureEdge::NamedFeatureWithSlash { weak_index: old_weak_index, .. }, FeatureEdge::NamedFeatureWithSlash { weak_index, .. }, ) => { if old_weak_index.is_some() && weak_index.is_some() { debug_assert_eq!( *old_weak_index, weak_index, "weak indexes should match if some" ); } // Upgrade this edge from weak to non-weak. if weak_index.is_none() { *old_weak_index = None; } } ( old_edge @ FeatureEdge::NamedFeatureWithSlash { .. }, edge @ FeatureEdge::NamedFeature | edge @ FeatureEdge::NamedFeatureDepColon(_), ) => { // Upgrade this edge from / conditional to dep: conditional or unconditional. *old_edge = edge; } ( old_edge @ FeatureEdge::NamedFeatureDepColon(_), edge @ FeatureEdge::NamedFeature, ) => { // Upgrade this edge from dep: conditional to unconditional. // XXX: can this ever happen? *old_edge = edge; } _ => { // In all other cases, leave the old edge alone. } } } None => { self.graph.add_edge(from_ix, to_ix, edge); } } }) } fn lookup_node(&self, node: &FeatureNode) -> Option> { self.map.get(node).map(|metadata| metadata.feature_ix) } pub(super) fn build(self) -> FeatureGraphImpl { FeatureGraphImpl { graph: self.graph, base_ixs: self.base_ixs, map: self.map, warnings: self.warnings, sccs: OnceCell::new(), weak: self.weak, } } } #[derive(Debug)] struct FeatureReq<'g> { link: PackageLink<'g>, to: PackageMetadata<'g>, edge_ix: EdgeIndex, to_default_idx: FeatureIndexInPackage, // This will contain any build states that aren't empty. features: AHashMap, } impl<'g> FeatureReq<'g> { fn new(link: PackageLink<'g>) -> Self { let to = link.to(); Self { link, to, edge_ix: link.edge_ix(), to_default_idx: to .get_feature_idx(FeatureLabel::Named("default")) .unwrap_or(FeatureIndexInPackage::Base), features: AHashMap::new(), } } fn is_empty(&self) -> bool { // self.features only consists of non-empty build states. self.features.is_empty() } fn add_features( &mut self, dep_kind: DependencyKind, req: &DepRequiredOrOptional, warnings: &mut Vec, ) { // Base feature. self.extend(FeatureIndexInPackage::Base, dep_kind, &req.build_if); // Default feature (or base if it isn't present). self.extend(self.to_default_idx, dep_kind, &req.default_features_if); for (feature, status) in &req.feature_targets { match self.to.get_feature_idx(FeatureLabel::Named(feature)) { Some(feature_idx) => { self.extend(feature_idx, dep_kind, status); } 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.link.from().id().clone(), dep_name: self.link.dep_name().to_string(), }, package_id: self.to.id().clone(), feature_name: feature.to_string(), }); } } } } fn extend( &mut self, feature_idx: FeatureIndexInPackage, dep_kind: DependencyKind, status: &PlatformStatusImpl, ) { let package_edge_ix = self.edge_ix; if !status.is_never() { self.features .entry(feature_idx) .or_insert_with(|| DependencyBuildState::new(package_edge_ix)) .extend(dep_kind, status); } } fn finish(self) -> impl Iterator + use<> { let package_ix = self.to.package_ix(); self.features .into_iter() .map(move |(feature_idx, build_state)| { // extend ensures that the build states aren't empty. Double-check that. debug_assert!(!build_state.is_empty(), "build states are always non-empty"); ( FeatureNode::new(package_ix, feature_idx), build_state.finish(), ) }) } } #[derive(Debug)] struct DependencyBuildState { package_edge_ix: EdgeIndex, normal: PlatformStatusImpl, build: PlatformStatusImpl, dev: PlatformStatusImpl, } impl DependencyBuildState { fn new(package_edge_ix: EdgeIndex) -> Self { Self { package_edge_ix, normal: PlatformStatusImpl::default(), build: PlatformStatusImpl::default(), dev: PlatformStatusImpl::default(), } } fn extend(&mut self, dep_kind: DependencyKind, status: &PlatformStatusImpl) { match dep_kind { DependencyKind::Normal => self.normal.extend(status), DependencyKind::Build => self.build.extend(status), DependencyKind::Development => self.dev.extend(status), _ => panic!("unknown dependency kind"), } } fn is_empty(&self) -> bool { self.normal.is_never() && self.build.is_never() && self.dev.is_never() } fn finish(self) -> FeatureEdge { FeatureEdge::DependenciesSection(ConditionalLinkImpl { package_edge_ix: self.package_edge_ix, normal: self.normal, build: self.build, dev: self.dev, }) } } guppy-0.17.25/src/graph/feature/cycles.rs000064400000000000000000000040421046102023000163170ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 //! Code for handling cycles in feature graphs. use crate::{ Error, graph::{ FeatureIx, feature::{FeatureGraph, FeatureId}, }, petgraph_support::scc::Sccs, }; /// Contains information about dependency cycles in feature graphs. /// /// Cargo permits cycles if at least one of the links is dev-only. `Cycles` exposes information /// about such dependencies. /// /// Constructed through `PackageGraph::cycles`. pub struct Cycles<'g> { feature_graph: FeatureGraph<'g>, sccs: &'g Sccs, } impl<'g> Cycles<'g> { pub(super) fn new(feature_graph: FeatureGraph<'g>) -> Self { Self { feature_graph, sccs: feature_graph.sccs(), } } /// Returns true if these two IDs are in the same cycle. pub fn is_cyclic<'a>( &self, a: impl Into>, b: impl Into>, ) -> Result { let a = a.into(); let b = b.into(); let a_ix = self.feature_graph.feature_ix(a)?; let b_ix = self.feature_graph.feature_ix(b)?; Ok(self.sccs.is_same_scc(a_ix, b_ix)) } /// Returns all the cycles of 2 or more elements in this graph. /// /// Cycles are returned in topological order: if features in cycle B depend on features in cycle /// A, A is returned before B. /// /// Within a cycle, nodes are returned in non-dev order: if feature Foo has a dependency on Bar, /// and Bar has a dev-dependency on Foo, then Foo is returned before Bar. pub fn all_cycles(&self) -> impl Iterator>> + 'g + use<'g> { let dep_graph = self.feature_graph.dep_graph(); let package_graph = self.feature_graph.package_graph; self.sccs.multi_sccs().map(move |class| { class .iter() .map(move |feature_ix| FeatureId::from_node(package_graph, &dep_graph[*feature_ix])) .collect() }) } } guppy-0.17.25/src/graph/feature/feature_list.rs000064400000000000000000000142131046102023000175240ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 //! A sorted, deduplicated list of features from a single package. use crate::{ PackageId, graph::{ PackageMetadata, feature::{FeatureId, FeatureLabel}, }, sorted_set::SortedSet, }; use std::{fmt, slice, vec}; /// A sorted, deduplicated list of features from a single package. /// /// This provides a convenient way to query and print out lists of features. /// /// Returned by methods on `FeatureSet`. #[derive(Clone, Eq, PartialEq)] pub struct FeatureList<'g> { package: PackageMetadata<'g>, labels: SortedSet>, } impl<'g> FeatureList<'g> { /// Creates a new `FeatureList` from a package and an iterator over feature labels. pub fn new( package: PackageMetadata<'g>, labels: impl IntoIterator>, ) -> Self { Self { package, labels: labels.into_iter().collect(), } } /// Returns the package corresponding to this feature list. pub fn package(&self) -> &PackageMetadata<'g> { &self.package } /// Returns true if this feature list contains this feature label. pub fn contains(&self, label: FeatureLabel<'_>) -> bool { self.labels.contains(&label) } /// Returns true if this feature list contains the "base" feature. /// /// The "base" feature represents the package with no features enabled. #[inline] pub fn has_base(&self) -> bool { self.contains(FeatureLabel::Base) } /// Returns true if this feature list contains the specified named feature. #[inline] pub fn has_named_feature(&self, feature_name: &str) -> bool { self.contains(FeatureLabel::Named(feature_name)) } /// Returns true if this feature list contains the specified optional dependency. #[inline] pub fn has_optional_dependency(&self, dep_name: &str) -> bool { self.contains(FeatureLabel::OptionalDependency(dep_name)) } /// Returns the list of labels as a slice. /// /// This slice is guaranteed to be sorted and unique. pub fn labels(&self) -> &[FeatureLabel<'g>] { self.labels.as_slice() } /// Returns an iterator containing all named features. /// /// The iterator is guaranteed to be sorted and unique. pub fn named_features(&self) -> impl Iterator + '_ { // XXX: binary search? self.labels.iter().filter_map(|label| match label { FeatureLabel::Named(feature_name) => Some(*feature_name), _ => None, }) } /// Returns an iterator containing all optional dependencies. /// /// The iterator is guaranteed to be sorted and unique. pub fn optional_deps(&self) -> impl Iterator + '_ { // XXX: binary search? self.labels.iter().filter_map(|label| match label { FeatureLabel::OptionalDependency(dep_name) => Some(*dep_name), _ => None, }) } /// Returns a borrowed iterator over feature IDs. pub fn iter<'a>(&'a self) -> Iter<'g, 'a> { self.into_iter() } /// Returns a pretty-printer over the list of feature labels. pub fn display_features<'a>(&'a self) -> DisplayFeatures<'g, 'a> { DisplayFeatures(self.labels()) } /// Returns a vector of feature labels. /// /// The vector is guaranteed to be sorted and unique. pub fn into_labels(self) -> Vec> { self.labels.into_inner().into_vec() } } impl fmt::Debug for FeatureList<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("FeatureList") .field("package", self.package.id()) .field("labels", &self.display_features()) .finish() } } impl<'g> IntoIterator for FeatureList<'g> { type Item = FeatureId<'g>; type IntoIter = IntoIter<'g>; fn into_iter(self) -> Self::IntoIter { IntoIter::new(self) } } impl<'a, 'g> IntoIterator for &'a FeatureList<'g> { type Item = FeatureId<'g>; type IntoIter = Iter<'g, 'a>; fn into_iter(self) -> Self::IntoIter { Iter::new(self) } } /// An owned iterator over a `FeatureList`. pub struct IntoIter<'g> { package_id: &'g PackageId, iter: vec::IntoIter>, } impl<'g> IntoIter<'g> { /// Creates a new iterator. pub fn new(feature_list: FeatureList<'g>) -> Self { Self { package_id: feature_list.package.id(), iter: feature_list.into_labels().into_iter(), } } } impl<'g> Iterator for IntoIter<'g> { type Item = FeatureId<'g>; fn next(&mut self) -> Option { self.iter .next() .map(|label| FeatureId::new(self.package_id, label)) } } /// A borrowed iterator over a `FeatureList`. pub struct Iter<'g, 'a> { package_id: &'g PackageId, iter: slice::Iter<'a, FeatureLabel<'g>>, } impl<'g, 'a> Iter<'g, 'a> { /// Creates a new iterator. pub fn new(feature_list: &'a FeatureList<'g>) -> Self { Self { package_id: feature_list.package.id(), iter: feature_list.labels().iter(), } } } impl<'g> Iterator for Iter<'g, '_> { type Item = FeatureId<'g>; fn next(&mut self) -> Option { self.iter .next() .map(|&label| FeatureId::new(self.package_id, label)) } } /// A pretty-printer for a list of features. /// /// Returned by `FeatureList::display_filters`. #[derive(Clone, Copy)] pub struct DisplayFeatures<'g, 'a>(&'a [FeatureLabel<'g>]); impl fmt::Display for DisplayFeatures<'_, '_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let len = self.0.len(); for (idx, label) in self.0.iter().enumerate() { write!(f, "{label}")?; if idx < len - 1 { write!(f, ", ")?; } } Ok(()) } } impl fmt::Debug for DisplayFeatures<'_, '_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // Use the Display impl as the debug one because it's easier to read. write!(f, "{self}") } } guppy-0.17.25/src/graph/feature/graph_impl.rs000064400000000000000000001052241046102023000171630ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::{ DependencyKind, Error, PackageId, debug_ignore::DebugIgnore, errors::FeatureGraphWarning, graph::{ DependencyDirection, FeatureIndexInPackage, FeatureIx, PackageGraph, PackageIx, PackageLink, PackageMetadata, feature::{ Cycles, FeatureFilter, FeatureList, WeakDependencies, WeakIndex, build::{FeatureGraphBuildState, FeaturePetgraph}, }, }, petgraph_support::{scc::Sccs, topo::TopoWithCycles}, platform::{PlatformStatus, PlatformStatusImpl}, }; use ahash::AHashMap; use once_cell::sync::OnceCell; use petgraph::{ algo::has_path_connecting, prelude::*, visit::{EdgeFiltered, IntoNodeReferences}, }; use std::{fmt, iter, iter::FromIterator}; // Some general notes about feature graphs: // // The set of features for a package is the named features (in the [features] section), plus any // optional dependencies. // // An optional dependency can be either normal or build -- not dev. Note that a dependency can be // marked optional in one section and required in another. In this context, a dependency is a // feature if it is marked as optional in any context. // // Features are *unified*. See the documentation in add_dependency_edges for more. // // There are a few ways features can be enabled. The most common is within a dependency spec. A // feature can also be specified via the command-line. Finally, named features can specify what // features a package depends on: // // ```toml // [features] // foo = ["a/bar", "optional-dep", "baz"] // baz = [] // ``` // // Feature names are unique. A named feature and an optional dep cannot have the same names. impl PackageGraph { /// Returns a derived graph representing every feature of every package. /// /// The feature graph is constructed the first time this method is called. The graph is cached /// so that repeated calls to this method are cheap. pub fn feature_graph(&self) -> FeatureGraph<'_> { let inner = self.get_feature_graph(); FeatureGraph { package_graph: self, inner, } } pub(super) fn get_feature_graph(&self) -> &FeatureGraphImpl { self.feature_graph .get_or_init(|| FeatureGraphImpl::new(self)) } } /// A derived graph representing every feature of every package. /// /// Constructed through `PackageGraph::feature_graph`. #[derive(Clone, Copy, Debug)] pub struct FeatureGraph<'g> { pub(crate) package_graph: &'g PackageGraph, pub(super) inner: &'g FeatureGraphImpl, } assert_covariant!(FeatureGraph); impl<'g> FeatureGraph<'g> { /// Returns any non-fatal warnings encountered while constructing the feature graph. pub fn build_warnings(&self) -> &'g [FeatureGraphWarning] { &self.inner.warnings } /// Returns the `PackageGraph` from which this feature graph was constructed. pub fn package_graph(&self) -> &'g PackageGraph { self.package_graph } /// Returns the total number of (package ID, feature) combinations in this graph. /// /// Includes the "base" feature for each package. pub fn feature_count(&self) -> usize { self.dep_graph().node_count() } /// Returns the number of links in this graph. pub fn link_count(&self) -> usize { self.dep_graph().edge_count() } /// Returns true if this feature graph contains the specified feature. pub fn contains(&self, feature_id: impl Into>) -> bool { let feature_id = feature_id.into(); FeatureNode::from_id(self, feature_id).is_some() } /// Returns metadata for the given feature ID, or `None` if the feature wasn't found. pub fn metadata( &self, feature_id: impl Into>, ) -> Result, Error> { let feature_id = feature_id.into(); let feature_node = FeatureNode::from_id(self, feature_id) .ok_or_else(|| Error::unknown_feature_id(feature_id))?; self.metadata_for_node(feature_node) .ok_or_else(|| Error::unknown_feature_id(feature_id)) } /// Returns all known features for a package. /// /// Returns an error if the package ID was unknown. pub fn all_features_for(&self, package_id: &PackageId) -> Result, Error> { let package = self.package_graph.metadata(package_id)?; let dep_graph = self.dep_graph(); let features = self .feature_ixs_for_package_ix(package.package_ix()) .map(|feature_ix| FeatureId::node_to_feature(package, &dep_graph[feature_ix])); Ok(FeatureList::new(package, features)) } /// Returns true if this feature is included in a package's build by default. /// /// Returns an error if this feature ID is unknown. /// /// ## Cycles /// /// A cyclic dev-dependency may cause additional features to be turned on. This computation /// does *not* follow conditional links and will *not* return true for such additional /// features. pub fn is_default_feature<'a>( &self, feature_id: impl Into>, ) -> Result { let feature_id = feature_id.into(); let default_ix = self.feature_ix( self.package_graph .metadata(feature_id.package_id())? .default_feature_id(), )?; let feature_ix = self.feature_ix(feature_id)?; // Do not follow conditional links. Ok(self.feature_ix_depends_on_no_conditional(default_ix, feature_ix)) } /// Returns true if `feature_a` depends (directly or indirectly) on `feature_b`. /// /// In other words, this returns true if `feature_b` is a (possibly transitive) dependency of /// `feature_a`. /// /// This also returns true if `feature_a` is the same as `feature_b`. /// /// Note that this returns true if `feature_a` [conditionally depends on][ConditionalLink] `feature_b`. pub fn depends_on<'a>( &self, feature_a: impl Into>, feature_b: impl Into>, ) -> Result { let feature_a = feature_a.into(); let feature_b = feature_b.into(); let a_ix = self.feature_ix(feature_a)?; let b_ix = self.feature_ix(feature_b)?; Ok(self.feature_ix_depends_on(a_ix, b_ix)) } /// Returns true if `feature_a` directly depends on `feature_b`. /// /// In other words, this returns true if `feature_a` is a direct dependency of `feature_b`. /// /// This returns false if `feature_a` is the same as `feature_b`. pub fn directly_depends_on<'a>( &self, feature_a: impl Into>, feature_b: impl Into>, ) -> Result { let feature_a = feature_a.into(); let feature_b = feature_b.into(); let a_ix = self.feature_ix(feature_a)?; let b_ix = self.feature_ix(feature_b)?; Ok(self.dep_graph().contains_edge(a_ix, b_ix)) } /// Returns information about dependency cycles. /// /// For more information, see the documentation for `Cycles`. pub fn cycles(&self) -> Cycles<'g> { Cycles::new(*self) } // --- // Helper methods // --- /// Verify basic properties of the feature graph. #[doc(hidden)] pub fn verify(&self) -> Result<(), Error> { let feature_set = self.resolve_all(); for conditional_link in feature_set.conditional_links(DependencyDirection::Forward) { let (from, to) = conditional_link.endpoints(); let is_any = conditional_link.normal().is_present() || conditional_link.build().is_present() || conditional_link.dev().is_present(); if !is_any { return Err(Error::FeatureGraphInternalError(format!( "{} -> {}: no edge info found", from.feature_id(), to.feature_id() ))); } } Ok(()) } /// Returns the strongly connected components for this feature graph. pub(super) fn sccs(&self) -> &'g Sccs { self.inner.sccs.get_or_init(|| { let edge_filtered = EdgeFiltered::from_fn(self.dep_graph(), |edge| match edge.weight() { FeatureEdge::DependenciesSection(link) | FeatureEdge::NamedFeatureDepColon(link) | FeatureEdge::NamedFeatureWithSlash { link, .. } => !link.dev_only(), FeatureEdge::NamedFeature | FeatureEdge::FeatureToBase => true, }); // Sort the entire graph without dev-only edges -- a correct graph would be cycle-free // but we don't currently do a consistency check for this so handle cycles. // TODO: should we check at construction time? or bubble up a warning somehow? let topo = TopoWithCycles::new(&edge_filtered); Sccs::new(&self.inner.graph, |scc| { topo.sort_nodes(scc); }) }) } fn metadata_impl(&self, feature_id: FeatureId<'g>) -> Option<&'g FeatureMetadataImpl> { let feature_node = FeatureNode::from_id(self, feature_id)?; self.metadata_impl_for_node(&feature_node) } pub(in crate::graph) fn metadata_for_ix( &self, feature_ix: NodeIndex, ) -> FeatureMetadata<'g> { self.metadata_for_node(self.dep_graph()[feature_ix]) .expect("valid feature ix") } pub(super) fn metadata_for_node(&self, node: FeatureNode) -> Option> { let inner = self.metadata_impl_for_node(&node)?; Some(FeatureMetadata { graph: DebugIgnore(*self), node, inner, }) } #[inline] fn metadata_impl_for_node(&self, node: &FeatureNode) -> Option<&'g FeatureMetadataImpl> { self.inner.map.get(node) } pub(super) fn dep_graph(&self) -> &'g FeaturePetgraph { &self.inner.graph } /// If this is a conditional edge, return the conditional link. Otherwise, return None. pub(super) fn edge_to_conditional_link( &self, source_ix: NodeIndex, target_ix: NodeIndex, edge_ix: EdgeIndex, edge: Option<&'g FeatureEdge>, ) -> Option<(ConditionalLink<'g>, Option)> { let edge = edge.unwrap_or_else(|| &self.dep_graph()[edge_ix]); match edge { FeatureEdge::NamedFeature | FeatureEdge::FeatureToBase => None, FeatureEdge::DependenciesSection(link) | FeatureEdge::NamedFeatureDepColon(link) => { let link = ConditionalLink::new(*self, source_ix, target_ix, edge_ix, link); // Dependency section and dep:foo style conditional links are always non-weak. let weak_index = None; Some((link, weak_index)) } FeatureEdge::NamedFeatureWithSlash { link, weak_index } => { let link = ConditionalLink::new(*self, source_ix, target_ix, edge_ix, link); Some((link, *weak_index)) } } } fn feature_ix_depends_on( &self, a_ix: NodeIndex, b_ix: NodeIndex, ) -> bool { has_path_connecting(self.dep_graph(), a_ix, b_ix, None) } fn feature_ix_depends_on_no_conditional( &self, a_ix: NodeIndex, b_ix: NodeIndex, ) -> bool { // Filter out conditional edges. let edge_filtered = EdgeFiltered::from_fn(self.dep_graph(), |edge_ref| match edge_ref.weight() { FeatureEdge::FeatureToBase | FeatureEdge::NamedFeature => true, FeatureEdge::DependenciesSection(_) | FeatureEdge::NamedFeatureDepColon(_) | FeatureEdge::NamedFeatureWithSlash { .. } => false, }); has_path_connecting(&edge_filtered, a_ix, b_ix, None) } pub(super) fn feature_ixs_for_package_ix( &self, package_ix: NodeIndex, ) -> impl Iterator> + use<> { let package_ix = package_ix.index(); let base_ix = self.inner.base_ixs[package_ix].index(); // base_ixs has (package count + 1) elements so this access is valid. let next_base_ix = self.inner.base_ixs[package_ix + 1].index(); (base_ix..next_base_ix).map(NodeIndex::new) } pub(super) fn feature_ixs_for_package_ixs( &self, package_ixs: I, ) -> impl Iterator> + 'g + use<'g, I> where I: IntoIterator> + 'g, { // Create a copy of FeatureGraph that will be moved into the closure below. let this = *self; package_ixs .into_iter() .flat_map(move |package_ix| this.feature_ixs_for_package_ix(package_ix)) } pub(in crate::graph) fn feature_ixs_for_package_ixs_filtered( &self, package_ixs: impl IntoIterator>, filter: impl FeatureFilter<'g>, ) -> B where B: FromIterator>, { let mut filter = filter; self.feature_ixs_for_package_ixs(package_ixs) .filter(|feature_ix| { let feature_node = &self.dep_graph()[*feature_ix]; filter.accept(self, FeatureId::from_node(self.package_graph, feature_node)) }) .collect() } pub(in crate::graph) fn package_ix_for_feature_ix( &self, feature_ix: NodeIndex, ) -> NodeIndex { let feature_node = &self.dep_graph()[feature_ix]; feature_node.package_ix() } #[allow(dead_code)] pub(super) fn feature_ixs<'a, B>( &self, feature_ids: impl IntoIterator>, ) -> Result where B: iter::FromIterator>, { feature_ids .into_iter() .map(|feature_id| self.feature_ix(feature_id)) .collect() } pub(super) fn feature_ix( &self, feature_id: FeatureId<'g>, ) -> Result, Error> { let metadata = self .metadata_impl(feature_id) .ok_or_else(|| Error::unknown_feature_id(feature_id))?; Ok(metadata.feature_ix) } } /// An identifier for a (package, feature) pair in a feature graph. /// /// Returned by various methods on `FeatureGraph` and `FeatureQuery`. /// /// `From` impls are available for `(&'g PackageId, &'g str)` and `(&'g PackageId, Option<&'g str>)` /// tuples. #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct FeatureId<'g> { package_id: &'g PackageId, label: FeatureLabel<'g>, } assert_covariant!(FeatureId); impl<'g> FeatureId<'g> { /// Creates a new `FeatureId` with the given [`PackageId`] and [`FeatureLabel`]. pub fn new(package_id: &'g PackageId, label: FeatureLabel<'g>) -> Self { Self { package_id, label } } /// Creates a new `FeatureId` representing a named feature in the `[features]` section, /// or an implicit named feature created by an optional dependency. pub fn named(package_id: &'g PackageId, feature_name: &'g str) -> Self { Self { package_id, label: FeatureLabel::Named(feature_name), } } /// Creates a new `FeatureId` representing an optional dependency. pub fn optional_dependency(package_id: &'g PackageId, dep_name: &'g str) -> Self { Self { package_id, label: FeatureLabel::OptionalDependency(dep_name), } } /// Creates a new `FeatureId` representing the base feature for a package. pub fn base(package_id: &'g PackageId) -> Self { Self { package_id, label: FeatureLabel::Base, } } pub(super) fn from_node(package_graph: &'g PackageGraph, node: &FeatureNode) -> Self { let package_id = &package_graph.dep_graph[node.package_ix]; let metadata = package_graph .metadata(package_id) .expect("package ID should have valid metadata"); let feature = Self::node_to_feature(metadata, node); Self { package_id, label: feature, } } pub(super) fn node_to_feature( metadata: PackageMetadata<'g>, node: &FeatureNode, ) -> FeatureLabel<'g> { metadata.feature_idx_to_label(node.feature_idx) } /// Returns the package ID. pub fn package_id(&self) -> &'g PackageId { self.package_id } /// Returns the [`FeatureLabel`] associated with the feature. pub fn label(&self) -> FeatureLabel<'g> { self.label } /// Returns true if this is the base feature for the package. #[inline] pub fn is_base(&self) -> bool { self.label.kind().is_base() } /// Returns true if this is an optional dependency. #[inline] pub fn is_optional_dependency(self) -> bool { self.label.kind().is_optional_dependency() } /// Returns true if this is a named feature. #[inline] pub fn is_named(self) -> bool { self.label.kind().is_named() } } impl<'g> From<(&'g PackageId, FeatureLabel<'g>)> for FeatureId<'g> { fn from((package_id, label): (&'g PackageId, FeatureLabel<'g>)) -> Self { FeatureId { package_id, label } } } /// The `Display` impl prints out: /// /// * `{package-id}/[base]` for base features. /// * `{package-id}/feature-name` for named features. /// * `{package-id}/dep:dep-name` for optional dependencies. /// /// ## Examples /// /// ``` /// use guppy::PackageId; /// use guppy::graph::feature::FeatureId; /// /// let package_id = PackageId::new("region 2.1.2 (registry+https://github.com/rust-lang/crates.io-index)"); /// /// assert_eq!( /// format!("{}", FeatureId::base(&package_id)), /// "region 2.1.2 (registry+https://github.com/rust-lang/crates.io-index)/[base]" /// ); /// /// assert_eq!( /// format!("{}", FeatureId::named(&package_id, "foo")), /// "region 2.1.2 (registry+https://github.com/rust-lang/crates.io-index)/foo" /// ); /// /// assert_eq!( /// format!("{}", FeatureId::optional_dependency(&package_id, "bar")), /// "region 2.1.2 (registry+https://github.com/rust-lang/crates.io-index)/dep:bar" /// ); /// ``` impl fmt::Display for FeatureId<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}/{}", self.package_id, self.label) } } /// A unique identifier for a feature within a specific package. Forms part of a [`FeatureId`]. #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum FeatureLabel<'g> { /// The "base" feature. Every package has one such feature. Base, /// This is a named feature in the `[features]` section, or an implicit feature that corresponds to /// an optional dependency. /// /// For versions of Cargo prior to 1.60, optional dependencies always create implicit features /// by the same name. For versions 1.60 and greater, optional dependencies may create implicit /// features if the dependency doesn't exist with the name "dep" in it. Named(&'g str), /// This is an optional dependency. OptionalDependency(&'g str), } impl FeatureLabel<'_> { /// Returns the kind of feature this is. /// /// The kind of a feature is simply the enum variant without any associated data. #[inline] pub fn kind(self) -> FeatureKind { match self { Self::Base => FeatureKind::Base, Self::Named(_) => FeatureKind::Named, Self::OptionalDependency(_) => FeatureKind::OptionalDependency, } } } /// The `Display` impl for `FeatureLabel` prints out: /// /// * `[base]` for base labels. /// * `feature-name` for optional dependencies. /// * `dep:dep-name` for named features. /// /// ## Examples /// /// ``` /// use guppy::graph::feature::FeatureLabel; /// /// assert_eq!(format!("{}", FeatureLabel::Base), "[base]"); /// assert_eq!(format!("{}", FeatureLabel::Named("foo")), "foo"); /// assert_eq!(format!("{}", FeatureLabel::OptionalDependency("bar")), "dep:bar"); /// ``` impl fmt::Display for FeatureLabel<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Base => write!(f, "[base]"), Self::Named(feature_name) => write!(f, "{feature_name}"), Self::OptionalDependency(dep_name) => write!(f, "dep:{dep_name}"), } } } /// Metadata for a feature within a package. #[derive(Clone, Copy)] pub struct FeatureMetadata<'g> { graph: DebugIgnore>, node: FeatureNode, inner: &'g FeatureMetadataImpl, } assert_covariant!(FeatureMetadata); impl<'g> FeatureMetadata<'g> { /// Returns the feature ID corresponding to this metadata. pub fn feature_id(&self) -> FeatureId<'g> { FeatureId::from_node(self.graph.package_graph, &self.node) } /// Returns the package ID corresponding to this metadata. pub fn package_id(&self) -> &'g PackageId { &self.graph.package_graph.dep_graph[self.package_ix()] } /// Returns the package metadata corresponding to this feature metadata. pub fn package(&self) -> PackageMetadata<'g> { self.graph .package_graph .metadata(self.package_id()) .expect("valid package ID") } /// Returns the label for this feature. pub fn label(&self) -> FeatureLabel<'g> { self.feature_id().label() } // --- // Helper methods // --- #[inline] pub(in crate::graph) fn package_ix(&self) -> NodeIndex { self.node.package_ix } #[inline] pub(in crate::graph) fn feature_ix(&self) -> NodeIndex { self.inner.feature_ix } } impl fmt::Debug for FeatureMetadata<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("FeatureMetadata") .field("id", &self.feature_id()) .finish() } } /// A graph representing every possible feature of every package, and the connections between them. #[derive(Clone, Debug)] pub(in crate::graph) struct FeatureGraphImpl { pub(super) graph: FeaturePetgraph, // base ixs consists of the base (start) feature indexes for each package. pub(super) base_ixs: Vec>, pub(super) map: AHashMap, pub(super) warnings: Vec, // The strongly connected components of the feature graph. Computed on demand. pub(super) sccs: OnceCell>, pub(super) weak: WeakDependencies, } impl FeatureGraphImpl { /// Creates a new `FeatureGraph` from this `PackageGraph`. pub(super) fn new(package_graph: &PackageGraph) -> Self { let mut build_state = FeatureGraphBuildState::new(package_graph); // Graph returns its node references in order -- check this in debug builds. let mut prev_ix = None; for (package_ix, package_id) in package_graph.dep_graph.node_references() { if let Some(prev_ix) = prev_ix { debug_assert_eq!(package_ix.index(), prev_ix + 1, "package ixs are in order"); } prev_ix = Some(package_ix.index()); let metadata = package_graph .metadata(package_id) .expect("valid package ID"); build_state.add_nodes(metadata); } build_state.end_nodes(); // The choice of bottom-up for this loop and the next is pretty arbitrary. for metadata in package_graph .resolve_all() .packages(DependencyDirection::Reverse) { build_state.add_named_feature_edges(metadata); } for link in package_graph .resolve_all() .links(DependencyDirection::Reverse) { build_state.add_dependency_edges(link); } build_state.build() } } /// A feature dependency that is conditionally activated. /// /// A `ConditionalLink` is typically a link across packages. For example: /// /// ```toml /// [package] /// name = "main" /// /// [dependencies] /// dep = { ... } /// /// [dev-dependencies] /// dev-dep = { ... } /// /// [target.'cfg(unix)'.dependencies] /// unix-dep = { ... } /// /// [features] /// feat = ["dep/feat", "dev-dep/feat", "unix-dep/feat"] /// ``` /// /// In this example, there are `ConditionalLink`s from `main/feat` to `dep/feat`, `dev-dep/feat` and /// `unix-dep/feat`. Each link is only activated if the conditions for it are met. For example, /// the link to `dev-dep/feat` is only followed if Cargo is interested in dev-dependencies of `main`. /// /// If a dependency, for example `unix-dep` above, is optional, an implicit feature is created in /// the package `main` with the name `unix-dep`. In this case, the dependency from `main/feat` to /// `main/unix-dep` is also a `ConditionalLink` representing the same `cfg(unix)` condition. #[derive(Copy, Clone)] pub struct ConditionalLink<'g> { graph: DebugIgnore>, from: &'g FeatureMetadataImpl, to: &'g FeatureMetadataImpl, edge_ix: EdgeIndex, inner: &'g ConditionalLinkImpl, } assert_covariant!(ConditionalLink); impl<'g> ConditionalLink<'g> { #[allow(dead_code)] pub(super) fn new( graph: FeatureGraph<'g>, source_ix: NodeIndex, target_ix: NodeIndex, edge_ix: EdgeIndex, inner: &'g ConditionalLinkImpl, ) -> Self { let dep_graph = graph.dep_graph(); Self { graph: DebugIgnore(graph), from: graph .metadata_impl_for_node(&dep_graph[source_ix]) .expect("valid source ix"), to: graph .metadata_impl_for_node(&dep_graph[target_ix]) .expect("valid target ix"), edge_ix, inner, } } /// Returns the feature which depends on the `to` feature. pub fn from(&self) -> FeatureMetadata<'g> { FeatureMetadata { graph: DebugIgnore(self.graph.0), node: self.graph.dep_graph()[self.from.feature_ix], inner: self.from, } } /// Returns the feature which is depended on by the `from` feature. pub fn to(&self) -> FeatureMetadata<'g> { FeatureMetadata { graph: DebugIgnore(self.graph.0), node: self.graph.dep_graph()[self.to.feature_ix], inner: self.to, } } /// Returns the endpoints as a pair of features `(from, to)`. pub fn endpoints(&self) -> (FeatureMetadata<'g>, FeatureMetadata<'g>) { (self.from(), self.to()) } /// Returns details about this feature dependency from the `[dependencies]` section. pub fn normal(&self) -> PlatformStatus<'g> { PlatformStatus::new(&self.inner.normal) } /// Returns details about this feature dependency from the `[build-dependencies]` section. pub fn build(&self) -> PlatformStatus<'g> { PlatformStatus::new(&self.inner.build) } /// Returns details about this feature dependency from the `[dev-dependencies]` section. pub fn dev(&self) -> PlatformStatus<'g> { PlatformStatus::new(&self.inner.dev) } /// Returns details about this feature dependency from the section specified by the given /// dependency kind. pub fn status_for_kind(&self, kind: DependencyKind) -> PlatformStatus<'g> { match kind { DependencyKind::Normal => self.normal(), DependencyKind::Build => self.build(), DependencyKind::Development => self.dev(), } } /// Returns true if this edge is dev-only, i.e. code from this edge will not be included in /// normal builds. pub fn dev_only(&self) -> bool { self.inner.dev_only() } /// Returns the `PackageLink` from which this `ConditionalLink` was derived. pub fn package_link(&self) -> PackageLink<'g> { self.graph .package_graph .edge_ix_to_link(self.inner.package_edge_ix) } // --- // Helper methods // --- #[allow(dead_code)] pub(super) fn edge_ix(&self) -> EdgeIndex { self.edge_ix } #[allow(dead_code)] pub(in crate::graph) fn package_edge_ix(&self) -> EdgeIndex { self.inner.package_edge_ix } } impl fmt::Debug for ConditionalLink<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("ConditionalLink") .field("from", &self.from()) .field("to", &self.to()) .field("normal", &self.normal()) .field("build", &self.build()) .field("dev", &self.dev()) .finish() } } // --- /// A combination of a package ID and a feature name, forming a node in a `FeatureGraph`. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub(in crate::graph) struct FeatureNode { package_ix: NodeIndex, feature_idx: FeatureIndexInPackage, } impl FeatureNode { /// Returns a new feature node. pub(in crate::graph) fn new( package_ix: NodeIndex, feature_idx: FeatureIndexInPackage, ) -> Self { Self { package_ix, feature_idx, } } /// Base feature node. pub(in crate::graph) fn base(package_ix: NodeIndex) -> Self { Self { package_ix, feature_idx: FeatureIndexInPackage::Base, } } pub(in crate::graph) fn optional_dep(package_ix: NodeIndex, dep_idx: usize) -> Self { Self { package_ix, feature_idx: FeatureIndexInPackage::OptionalDependency(dep_idx), } } pub(in crate::graph) fn named_feature( package_ix: NodeIndex, named_idx: usize, ) -> Self { Self { package_ix, feature_idx: FeatureIndexInPackage::Named(named_idx), } } fn from_id(feature_graph: &FeatureGraph<'_>, id: FeatureId<'_>) -> Option { let metadata = feature_graph.package_graph.metadata(id.package_id()).ok()?; Some(FeatureNode::new( metadata.package_ix(), metadata.get_feature_idx(id.label())?, )) } pub(super) fn named_features(package: PackageMetadata<'_>) -> impl Iterator + '_ { let package_ix = package.package_ix(); package .named_features_full() .map(move |(feature_idx, _, _)| Self { package_ix, feature_idx, }) } pub(super) fn optional_deps(package: PackageMetadata<'_>) -> impl Iterator + '_ { let package_ix = package.package_ix(); package .optional_deps_full() .map(move |(feature_idx, _)| Self { package_ix, feature_idx, }) } pub(in crate::graph) fn package_ix(&self) -> NodeIndex { self.package_ix } pub(in crate::graph) fn package_id_and_feature_label<'g>( &self, graph: &'g PackageGraph, ) -> (&'g PackageId, FeatureLabel<'g>) { let package_id = &graph.dep_graph[self.package_ix]; let metadata = graph.metadata(package_id).unwrap(); let feature_label = metadata.feature_idx_to_label(self.feature_idx); (package_id, feature_label) } } /// Information about why a feature depends on another feature. /// /// Not part of the stable API -- only exposed for FeatureSet::links(). #[derive(Clone, Debug)] #[doc(hidden)] pub enum FeatureEdge { /// This edge is from a feature to its base package. FeatureToBase, /// This is a dependency edge, e.g.: /// /// ```toml /// [dependencies] /// foo = { version = "1", features = ["a", "b"] } /// ``` /// /// (The above is conditional in that it isn't a build dependency. Similarly, it could be /// a target-specific dependency.) /// /// This also includes optional dependencies, for which the "from" node is /// `FeatureLabel::OptionalDependency` rather than `FeatureLabel::Base`. /// /// ```toml /// [dependencies] /// foo = { version = "1", features = ["a", "b"], optional = true } /// ``` DependenciesSection(ConditionalLinkImpl), /// This edge is from a feature depending on other features within the same package: /// /// ```toml /// [features] /// a = ["b"] /// ``` NamedFeature, /// This edge is from a feature to an optional dependency. /// /// ```toml /// [features] /// a = ["dep:foo"] /// ``` NamedFeatureDepColon(ConditionalLinkImpl), /// This is a named feature line of the form /// /// ```toml /// [features] /// a = ["foo/b"] /// # or /// a = ["foo?/b"] /// ``` NamedFeatureWithSlash { link: ConditionalLinkImpl, weak_index: Option, }, } /// Not part of the stable API -- only exposed for FeatureSet::links(). #[derive(Clone, Debug)] #[doc(hidden)] pub struct ConditionalLinkImpl { pub(super) package_edge_ix: EdgeIndex, pub(super) normal: PlatformStatusImpl, pub(super) build: PlatformStatusImpl, pub(super) dev: PlatformStatusImpl, } impl ConditionalLinkImpl { #[inline] fn dev_only(&self) -> bool { self.normal.is_never() && self.build.is_never() } } /// Metadata for a particular feature node. #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub(super) struct FeatureMetadataImpl { pub(super) feature_ix: NodeIndex, } /// The kind of a particular feature within a package. /// /// Returned by `FeatureMetadata`. #[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum FeatureKind { /// The "base" feature. Every package has one such feature. Base, /// This is a named feature in the `[features]` section, or an implicit feature that corresponds to /// an optional dependency. /// /// For versions of Cargo prior to 1.60, optional dependencies always create implicit features /// by the same name. For versions 1.60 and greater, optional dependencies may create implicit /// features if the dependency doesn't exist with the name "dep" in it. Named, /// This is an optional dependency. OptionalDependency, } impl FeatureKind { /// Returns true if this is the base feature. #[inline] pub fn is_base(self) -> bool { matches!(self, Self::Base) } /// Returns true if this is a named feature. #[inline] pub fn is_named(self) -> bool { matches!(self, Self::Named) } /// Returns true if this is an optional dependency. #[inline] pub fn is_optional_dependency(self) -> bool { matches!(self, Self::OptionalDependency) } } guppy-0.17.25/src/graph/feature/mod.rs000064400000000000000000000013501046102023000156130ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 //! Graph analysis for individual features within a package. //! //! `FeatureGraph` can be used to do a more precise analysis than is possible at the package level. //! For example, an optional feature not included a default build can potentially pull in a large //! number of extra dependencies. This module allows for those subgraphs to be filtered out. mod build; mod cycles; pub mod feature_list; mod graph_impl; #[cfg(feature = "proptest1")] mod proptest_helpers; mod query; mod resolve; mod weak; use build::*; pub use cycles::*; pub use feature_list::FeatureList; pub use graph_impl::*; pub use query::*; pub use resolve::*; pub use weak::*; guppy-0.17.25/src/graph/feature/proptest_helpers.rs000064400000000000000000000033551046102023000204450ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::graph::{ feature::{FeatureGraph, FeatureId, FeatureSet}, fixedbitset_strategy, }; use petgraph::prelude::*; use proptest::prelude::*; /// ## Helpers for property testing /// /// The methods in this section allow a `FeatureGraph` to be used in property-based testing /// scenarios. /// /// Currently, [proptest 1](https://docs.rs/proptest/1) is supported if the `proptest1` /// feature is enabled. impl<'g> FeatureGraph<'g> { /// Returns a `Strategy` that generates random feature IDs from this graph. /// /// The IDs so chosen are uniformly random from the entire feature graph. In other words, a /// package with more optional features is more likely to be chosen. /// /// Requires the `proptest1` feature to be enabled. /// /// ## Panics /// /// Panics if there are no packages in the `PackageGraph` from which this `FeatureGraph` was /// derived. pub fn proptest1_id_strategy(&self) -> impl Strategy> + 'g + use<'g> { let dep_graph = self.dep_graph(); let package_graph = self.package_graph; any::().prop_map(move |index| { let feature_ix = NodeIndex::new(index.index(dep_graph.node_count())); FeatureId::from_node(package_graph, &dep_graph[feature_ix]) }) } /// Returns a `Strategy` that generates random feature sets from this graph. pub fn proptest1_set_strategy(&self) -> impl Strategy> + 'g + use<'g> { let this = *self; fixedbitset_strategy(self.feature_count()) .prop_map(move |included| FeatureSet::from_included(this, included)) } } guppy-0.17.25/src/graph/feature/query.rs000064400000000000000000000322041046102023000162030ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::{ Error, PackageId, debug_ignore::DebugIgnore, graph::{ DependencyDirection, FeatureGraphSpec, FeatureIx, PackageIx, PackageMetadata, feature::{ ConditionalLink, FeatureGraph, FeatureId, FeatureLabel, FeatureMetadata, FeatureSet, }, query_core::QueryParams, }, sorted_set::SortedSet, }; use itertools::Itertools; use petgraph::graph::NodeIndex; use std::collections::HashSet; /// Trait representing whether a feature within a package should be selected. /// /// This is conceptually similar to passing `--features` or other similar command-line options to /// Cargo. /// /// Most uses will involve using one of the predefined filters: `all_filter`, `default_filter`, or /// `none_filter`. A customized filter can be provided either through `filter_fn` or by implementing /// this trait. pub trait FeatureFilter<'g> { /// Returns true if this feature ID should be selected in the graph. /// /// Returning false does not prevent this feature ID from being included if it's reachable /// through other means. /// /// In general, `accept` should return true if `feature_id.is_base()` is true. /// /// The feature ID is guaranteed to be in this graph, so it is OK to panic if it isn't found. fn accept(&mut self, graph: &FeatureGraph<'g>, feature_id: FeatureId<'g>) -> bool; } impl<'g, T> FeatureFilter<'g> for &mut T where T: FeatureFilter<'g>, { fn accept(&mut self, graph: &FeatureGraph<'g>, feature_id: FeatureId<'g>) -> bool { (**self).accept(graph, feature_id) } } impl<'g> FeatureFilter<'g> for Box + '_> { fn accept(&mut self, graph: &FeatureGraph<'g>, feature_id: FeatureId<'g>) -> bool { (**self).accept(graph, feature_id) } } impl<'g> FeatureFilter<'g> for &mut dyn FeatureFilter<'g> { fn accept(&mut self, graph: &FeatureGraph<'g>, feature_id: FeatureId<'g>) -> bool { (**self).accept(graph, feature_id) } } /// A `FeatureFilter` which calls the function that's passed in. #[derive(Clone, Debug)] pub struct FeatureFilterFn(F); impl<'g, F> FeatureFilterFn where F: FnMut(&FeatureGraph<'g>, FeatureId<'g>) -> bool, { /// Returns a new instance of this wrapper. pub fn new(f: F) -> Self { FeatureFilterFn(f) } } impl<'g, F> FeatureFilter<'g> for FeatureFilterFn where F: FnMut(&FeatureGraph<'g>, FeatureId<'g>) -> bool, { fn accept(&mut self, graph: &FeatureGraph<'g>, feature_id: FeatureId<'g>) -> bool { (self.0)(graph, feature_id) } } /// Describes one of the standard sets of features recognized by Cargo: none, all or default. /// /// `StandardFeatures` implements `FeatureFilter<'g>`, so it can be passed in as a feature filter /// wherever necessary. #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialOrd, PartialEq)] pub enum StandardFeatures { /// No features. Equivalent to a build with `--no-default-features`. None, /// Default features. Equivalent to a standard `cargo build`. Default, /// All features. Equivalent to `cargo build --all-features`. All, } impl StandardFeatures { /// A list of all the possible values of `StandardFeatures`. pub const VALUES: &'static [Self; 3] = &[ StandardFeatures::None, StandardFeatures::Default, StandardFeatures::All, ]; } impl<'g> FeatureFilter<'g> for StandardFeatures { fn accept(&mut self, graph: &FeatureGraph<'g>, feature_id: FeatureId<'g>) -> bool { match self { StandardFeatures::None => { // The only feature ID that should be accepted is the base one. feature_id.is_base() } StandardFeatures::Default => { // XXX it kinda sucks that we already know about the exact feature ixs but need to go // through the feature ID over here. Might be worth reorganizing the code to not do that. graph .is_default_feature(feature_id) .expect("feature IDs should be valid") } StandardFeatures::All => true, } } } /// Returns a `FeatureFilter` that selects everything from the base filter, plus these additional /// feature names -- regardless of what package they are in. /// /// This is equivalent to a build with `--features`, and is typically meant to be used with one /// package. /// /// For filtering by feature IDs, use `feature_id_filter`. pub fn named_feature_filter<'g: 'a, 'a>( base: impl FeatureFilter<'g> + 'a, features: impl IntoIterator, ) -> impl FeatureFilter<'g> + 'a { let mut base = base; let features: HashSet<_> = features.into_iter().collect(); FeatureFilterFn::new(move |feature_graph, feature_id| { if base.accept(feature_graph, feature_id) { return true; } match feature_id.label() { FeatureLabel::Named(feature) => features.contains(feature), _ => { // This is the base feature. Assume that it has already been selected by the base // filter. false } } }) } /// Returns a `FeatureFilter` that selects everything from the base filter, plus some additional /// feature IDs. /// /// This is a more advanced version of `feature_filter`. pub fn feature_id_filter<'g: 'a, 'a>( base: impl FeatureFilter<'g> + 'a, feature_ids: impl IntoIterator>>, ) -> impl FeatureFilter<'g> + 'a { let mut base = base; let feature_ids: HashSet<_> = feature_ids .into_iter() .map(|feature_id| feature_id.into()) .collect(); FeatureFilterFn::new(move |feature_graph, feature_id| { base.accept(feature_graph, feature_id) || feature_ids.contains(&feature_id) }) } /// A query over a feature graph. /// /// A `FeatureQuery` is the entry point for Cargo resolution, and also provides iterators over /// feature IDs and links. This struct is constructed through the `query_` methods on /// `FeatureGraph`, or through `PackageQuery::to_feature_query`. #[derive(Clone, Debug)] pub struct FeatureQuery<'g> { pub(super) graph: DebugIgnore>, pub(in crate::graph) params: QueryParams, } assert_covariant!(FeatureQuery); /// ## Queries /// /// The methods in this section create queries over subsets of this feature graph. Use the methods /// here to analyze transitive dependencies. impl<'g> FeatureGraph<'g> { /// Creates a new query over the entire workspace. /// /// `query_workspace` will select all workspace packages (subject to the provided filter) and /// their transitive dependencies. pub fn query_workspace(&self, filter: impl FeatureFilter<'g>) -> FeatureQuery<'g> { self.package_graph .query_workspace() .to_feature_query(filter) } /// Creates a new query that returns transitive dependencies of the given feature IDs in the /// specified direction. /// /// Returns an error if any feature IDs are unknown. pub fn query_directed<'a>( &self, feature_ids: impl IntoIterator>>, dep_direction: DependencyDirection, ) -> Result, Error> { match dep_direction { DependencyDirection::Forward => self.query_forward(feature_ids), DependencyDirection::Reverse => self.query_reverse(feature_ids), } } /// Creates a new query that returns transitive dependencies of the given feature IDs. /// /// Returns an error if any feature IDs are unknown. pub fn query_forward<'a>( &self, feature_ids: impl IntoIterator>>, ) -> Result, Error> { let feature_ids = feature_ids.into_iter().map(|feature_id| feature_id.into()); Ok(FeatureQuery { graph: DebugIgnore(*self), params: QueryParams::Forward(self.feature_ixs(feature_ids)?), }) } /// Creates a new query that returns transitive reverse dependencies of the given feature IDs. /// /// Returns an error if any feature IDs are unknown. pub fn query_reverse<'a>( &self, feature_ids: impl IntoIterator>>, ) -> Result, Error> { let feature_ids = feature_ids.into_iter().map(|feature_id| feature_id.into()); Ok(FeatureQuery { graph: DebugIgnore(*self), params: QueryParams::Reverse(self.feature_ixs(feature_ids)?), }) } pub(in crate::graph) fn query_from_parts( &self, feature_ixs: SortedSet>, direction: DependencyDirection, ) -> FeatureQuery<'g> { let params = match direction { DependencyDirection::Forward => QueryParams::Forward(feature_ixs), DependencyDirection::Reverse => QueryParams::Reverse(feature_ixs), }; FeatureQuery { graph: DebugIgnore(*self), params, } } } impl<'g> FeatureQuery<'g> { /// Returns the feature graph the query is going to be executed on. pub fn graph(&self) -> &FeatureGraph<'g> { &self.graph } /// Returns the direction the query is happening in. pub fn direction(&self) -> DependencyDirection { self.params.direction() } /// Returns the list of initial features specified in the query. /// /// The order of features is unspecified. pub fn initials<'a>(&'a self) -> impl ExactSizeIterator> + 'a { let graph = self.graph; self.params .initials() .iter() .map(move |feature_ix| graph.metadata_for_ix(*feature_ix)) } /// Returns the list of initial packages specified in the query. /// /// The order of packages is unspecified. pub fn initial_packages<'a>(&'a self) -> impl Iterator> + 'a { // feature ixs are stored in sorted order by package ix, so dedup() is fine. self.initials().map(|feature| feature.package()).dedup() } /// Returns true if the query starts from the given package. /// /// Returns an error if the package ID is unknown. pub fn starts_from_package(&self, package_id: &PackageId) -> Result { let package_ix = self.graph.package_graph.package_ix(package_id)?; Ok(self.starts_from_package_ix(package_ix)) } /// Returns true if the query starts from the given feature ID. /// /// Returns an error if this feature ID is unknown. pub fn starts_from<'a>(&self, feature_id: impl Into>) -> Result { Ok(self .params .has_initial(self.graph.feature_ix(feature_id.into())?)) } /// Resolves this query into a set of known feature IDs. /// /// This is the entry point for iterators. pub fn resolve(self) -> FeatureSet<'g> { FeatureSet::new(self) } /// Resolves this query into a set of known feature IDs, using the provided resolver to /// determine which links are followed. pub fn resolve_with(self, resolver: impl FeatureResolver<'g>) -> FeatureSet<'g> { FeatureSet::with_resolver(self, resolver) } /// Resolves this query into a set of known feature IDs, using the provided resolver function to /// determine which links are followed. pub fn resolve_with_fn( self, resolver_fn: impl FnMut(&FeatureQuery<'g>, ConditionalLink<'g>) -> bool, ) -> FeatureSet<'g> { self.resolve_with(ResolverFn(resolver_fn)) } // --- // Helper methods // --- pub(in crate::graph) fn starts_from_package_ix( &self, package_ix: NodeIndex, ) -> bool { self.graph .feature_ixs_for_package_ix(package_ix) .any(|feature_ix| self.params.has_initial(feature_ix)) } } /// Represents whether a particular link within a feature graph should be followed during a /// resolve operation. pub trait FeatureResolver<'g> { /// Returns true if this conditional link should be followed during a resolve operation. fn accept(&mut self, query: &FeatureQuery<'g>, link: ConditionalLink<'g>) -> bool; } impl<'g, T> FeatureResolver<'g> for &mut T where T: FeatureResolver<'g>, { fn accept(&mut self, query: &FeatureQuery<'g>, link: ConditionalLink<'g>) -> bool { (**self).accept(query, link) } } impl<'g> FeatureResolver<'g> for Box + '_> { fn accept(&mut self, query: &FeatureQuery<'g>, link: ConditionalLink<'g>) -> bool { (**self).accept(query, link) } } impl<'g> FeatureResolver<'g> for &mut dyn FeatureResolver<'g> { fn accept(&mut self, query: &FeatureQuery<'g>, link: ConditionalLink<'g>) -> bool { (**self).accept(query, link) } } #[derive(Clone, Debug)] struct ResolverFn(pub F); impl<'g, F> FeatureResolver<'g> for ResolverFn where F: FnMut(&FeatureQuery<'g>, ConditionalLink<'g>) -> bool, { fn accept(&mut self, query: &FeatureQuery<'g>, link: ConditionalLink<'g>) -> bool { (self.0)(query, link) } } guppy-0.17.25/src/graph/feature/resolve.rs000064400000000000000000000527471046102023000165330ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use std::fmt; use crate::{ Error, PackageId, debug_ignore::DebugIgnore, graph::{ DependencyDirection, FeatureGraphSpec, FeatureIx, PackageIx, PackageMetadata, PackageSet, cargo::{CargoOptions, CargoSet}, feature::{ ConditionalLink, FeatureEdge, FeatureGraph, FeatureId, FeatureList, FeatureMetadata, FeatureQuery, FeatureResolver, build::FeatureEdgeReference, }, resolve_core::ResolveCore, }, petgraph_support::{IxBitSet, dfs::BufferedEdgeFilterFn}, sorted_set::SortedSet, }; use fixedbitset::FixedBitSet; use itertools::Either; use petgraph::{graph::NodeIndex, visit::EdgeRef}; impl<'g> FeatureGraph<'g> { /// Creates a new `FeatureSet` consisting of all members of this feature graph. /// /// This will include features that aren't depended on by any workspace packages. /// /// In most situations, `query_workspace().resolve()` is preferred. Use `resolve_all` if you /// know you need parts of the graph that aren't accessible from the workspace. pub fn resolve_all(&self) -> FeatureSet<'g> { FeatureSet { graph: DebugIgnore(*self), core: ResolveCore::all_nodes(self.dep_graph()), } } /// Creates a new, empty `FeatureSet` associated with this feature graph. pub fn resolve_none(&self) -> FeatureSet<'g> { FeatureSet { graph: DebugIgnore(*self), core: ResolveCore::empty(), } } /// Creates a new `FeatureSet` consisting of the specified feature IDs. /// /// Returns an error if any feature IDs are unknown. pub fn resolve_ids<'a>( &self, feature_ids: impl IntoIterator>>, ) -> Result, Error> { Ok(FeatureSet { graph: DebugIgnore(*self), core: ResolveCore::from_included::( self.feature_ixs(feature_ids.into_iter().map(|feature| feature.into()))?, ), }) } } /// A set of resolved feature IDs in a feature graph. /// /// Created by `FeatureQuery::resolve`, the `FeatureGraph::resolve_` methods, or from /// `PackageSet::to_feature_set`. #[derive(Clone)] pub struct FeatureSet<'g> { graph: DebugIgnore>, core: ResolveCore, } impl fmt::Debug for FeatureSet<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_set() .entries(self.packages_with_features(DependencyDirection::Forward)) .finish() } } assert_covariant!(FeatureSet); impl<'g> FeatureSet<'g> { pub(super) fn new(query: FeatureQuery<'g>) -> Self { let graph = query.graph; Self { graph, core: ResolveCore::new(graph.dep_graph(), query.params), } } pub(super) fn with_resolver( query: FeatureQuery<'g>, mut resolver: impl FeatureResolver<'g>, ) -> Self { let graph = query.graph; let params = query.params.clone(); // State used by the callback below. let mut buffer_states = graph .inner .weak .new_buffer_states(|link| resolver.accept(&query, link)); let filter_fn = |edge_ref: FeatureEdgeReference<'g>| { match graph.edge_to_conditional_link( edge_ref.source(), edge_ref.target(), edge_ref.id(), Some(edge_ref.weight()), ) { Some((link, weak_index)) => buffer_states.track(edge_ref, link, weak_index), None => { // Feature links within the same package are always followed. Either::Left(Some(edge_ref)) } } .into_iter() }; let core = ResolveCore::with_buffered_edge_filter( graph.dep_graph(), params, BufferedEdgeFilterFn(filter_fn), ); Self { graph, core } } #[allow(dead_code)] pub(in crate::graph) fn from_included( graph: FeatureGraph<'g>, included: impl Into, ) -> Self { Self { graph: DebugIgnore(graph), core: ResolveCore::from_included(included.into()), } } /// Returns the `FeatureGraph` that this feature set was computed against. pub fn graph(&self) -> &FeatureGraph<'g> { &self.graph.0 } /// Returns the number of feature IDs in this set. pub fn len(&self) -> usize { self.core.len() } /// Returns true if no feature IDs were resolved in this set. pub fn is_empty(&self) -> bool { self.core.is_empty() } /// Returns true if this set contains the given feature ID. /// /// Returns an error if this feature ID was unknown. pub fn contains<'a>(&self, feature_id: impl Into>) -> Result { Ok(self .core .contains(self.graph.feature_ix(feature_id.into())?)) } /// Returns true if this set contains this package. /// /// Returns an error if this package ID was unknown. pub fn contains_package(&self, package_id: &PackageId) -> Result { let package = self.graph.package_graph.metadata(package_id)?; Ok(self .graph .feature_ixs_for_package_ix(package.package_ix()) .any(|feature_ix| self.core.contains(feature_ix))) } /// Creates a new `FeatureQuery` from this set in the specified direction. /// /// This is equivalent to constructing a query from all the feature IDs in this set. pub fn to_feature_query(&self, direction: DependencyDirection) -> FeatureQuery<'g> { let feature_ixs = SortedSet::new( self.core .included .ones() .map(NodeIndex::new) .collect::>(), ); self.graph.query_from_parts(feature_ixs, direction) } // --- // Set operations // --- /// Returns a `FeatureSet` that contains all packages present in at least one of `self` /// and `other`. /// /// ## Panics /// /// Panics if the package graphs associated with `self` and `other` don't match. pub fn union(&self, other: &Self) -> Self { assert!( ::std::ptr::eq(self.graph.package_graph, self.graph.package_graph), "package graphs passed into union() match" ); let mut res = self.clone(); res.core.union_with(&other.core); res } /// Returns a `FeatureSet` that contains all packages present in both `self` and `other`. /// /// ## Panics /// /// Panics if the package graphs associated with `self` and `other` don't match. pub fn intersection(&self, other: &Self) -> Self { assert!( ::std::ptr::eq(self.graph.package_graph, self.graph.package_graph), "package graphs passed into intersection() match" ); let mut res = self.clone(); res.core.intersect_with(&other.core); res } /// Returns a `FeatureSet` that contains all packages present in `self` but not `other`. /// /// ## Panics /// /// Panics if the package graphs associated with `self` and `other` don't match. pub fn difference(&self, other: &Self) -> Self { assert!( ::std::ptr::eq(self.graph.package_graph, self.graph.package_graph), "package graphs passed into difference() match" ); Self { graph: self.graph, core: self.core.difference(&other.core), } } /// Returns a `FeatureSet` that contains all packages present in exactly one of `self` and /// `other`. /// /// ## Panics /// /// Panics if the package graphs associated with `self` and `other` don't match. pub fn symmetric_difference(&self, other: &Self) -> Self { assert!( ::std::ptr::eq(self.graph.package_graph, self.graph.package_graph), "package graphs passed into symmetric_difference() match" ); let mut res = self.clone(); res.core.symmetric_difference_with(&other.core); res } /// Returns a `PackageSet` on which a filter has been applied. /// /// Filters out all values for which the callback returns false. /// /// ## Cycles /// /// For packages within a dependency cycle, the callback will be called in non-dev order. When /// the direction is forward, if package Foo has a dependency on Bar, and Bar has a cyclic /// dev-dependency on Foo, then Foo is returned before Bar. pub fn filter( &self, direction: DependencyDirection, mut callback: impl FnMut(FeatureMetadata<'g>) -> bool, ) -> Self { let graph = self.graph; let included: IxBitSet = self .features(direction) .filter_map(move |feature| { let feature_ix = feature.feature_ix(); if callback(feature) { Some(feature_ix) } else { None } }) .collect(); Self::from_included(*graph, included) } /// Partitions this `PackageSet` into two. /// /// The first `PackageSet` contains packages for which the callback returned true, and the /// second one contains packages for which the callback returned false. /// /// ## Cycles /// /// For packages within a dependency cycle, the callback will be called in non-dev order. When /// the direction is forward, if package Foo has a dependency on Bar, and Bar has a cyclic /// dev-dependency on Foo, then Foo is returned before Bar. pub fn partition( &self, direction: DependencyDirection, mut callback: impl FnMut(FeatureMetadata<'g>) -> bool, ) -> (Self, Self) { let graph = self.graph; let mut left = IxBitSet::with_capacity(self.core.included.len()); let mut right = left.clone(); self.features(direction).for_each(|feature| { let feature_ix = feature.feature_ix(); match callback(feature) { true => left.insert_node_ix(feature_ix), false => right.insert_node_ix(feature_ix), } }); ( Self::from_included(*graph, left), Self::from_included(*graph, right), ) } /// Performs filtering and partitioning at the same time. /// /// The first `PackageSet` contains packages for which the callback returned `Some(true)`, and /// the second one contains packages for which the callback returned `Some(false)`. Packages /// for which the callback returned `None` are dropped. /// /// ## Cycles /// /// For packages within a dependency cycle, the callback will be called in non-dev order. When /// the direction is forward, if package Foo has a dependency on Bar, and Bar has a cyclic /// dev-dependency on Foo, then Foo is returned before Bar. pub fn filter_partition( &self, direction: DependencyDirection, mut callback: impl FnMut(FeatureMetadata<'g>) -> Option, ) -> (Self, Self) { let graph = self.graph; let mut left = IxBitSet::with_capacity(self.core.included.len()); let mut right = left.clone(); self.features(direction).for_each(|feature| { let feature_ix = feature.feature_ix(); match callback(feature) { Some(true) => left.insert_node_ix(feature_ix), Some(false) => right.insert_node_ix(feature_ix), None => {} } }); ( Self::from_included(*graph, left), Self::from_included(*graph, right), ) } // --- // Queries around packages // --- /// Returns a list of features present for this package, or `None` if this package is not /// present in the feature set. /// /// Returns an error if the package ID was unknown. pub fn features_for(&self, package_id: &PackageId) -> Result>, Error> { let package = self.graph.package_graph.metadata(package_id)?; Ok(self.features_for_package_impl(package)) } /// Converts this `FeatureSet` into a `PackageSet` containing all packages with any selected /// features (including the "base" feature). pub fn to_package_set(&self) -> PackageSet<'g> { let included: IxBitSet = self .core .included .ones() .map(|feature_ix| { self.graph .package_ix_for_feature_ix(NodeIndex::new(feature_ix)) }) .collect(); PackageSet::from_included(self.graph.package_graph, included.0) } // --- // Cargo set creation // --- /// Converts this feature set into a Cargo set, simulating a Cargo build for it. /// /// The feature set is expected to be entirely within the workspace. Its behavior outside the /// workspace isn't defined and may be surprising. /// /// Returns an error if the `CargoOptions` weren't valid in some way (for example if an omitted /// package ID wasn't known to this graph.) pub fn into_cargo_set(self, opts: &CargoOptions<'_>) -> Result, Error> { let features_only = self.graph.resolve_none(); CargoSet::new(self, features_only, opts) } // --- // Iterators // --- /// Iterates over feature IDs, in topological order in the direction specified. /// /// ## Cycles /// /// The features within a dependency cycle will be returned in non-dev order. When the direction /// is forward, if feature Foo has a dependency on Bar, and Bar has a cyclic dev-dependency on /// Foo, then Foo is returned before Bar. pub fn feature_ids<'a>( &'a self, direction: DependencyDirection, ) -> impl ExactSizeIterator> + 'a { let graph = self.graph; self.core .topo(graph.sccs(), direction) .map(move |feature_ix| { FeatureId::from_node(graph.package_graph(), &graph.dep_graph()[feature_ix]) }) } /// Iterates over feature metadatas, in topological order in the direction specified. /// /// ## Cycles /// /// The features within a dependency cycle will be returned in non-dev order. When the direction /// is forward, if feature Foo has a dependency on Bar, and Bar has a cyclic dev-dependency on /// Foo, then Foo is returned before Bar. pub fn features<'a>( &'a self, direction: DependencyDirection, ) -> impl ExactSizeIterator> + 'a { let graph = self.graph; self.core .topo(graph.sccs(), direction) .map(move |feature_ix| { graph .metadata_for_node(graph.dep_graph()[feature_ix]) .expect("feature node should be known") }) } /// Iterates over package metadatas and their corresponding features, in topological order in /// the direction specified. /// /// ## Cycles /// /// The packages within a dependency cycle will be returned in non-dev order. When the direction /// is forward, if package Foo has a dependency on Bar, and Bar has a cyclic dev-dependency on /// Foo, then Foo is returned before Bar. pub fn packages_with_features<'a>( &'a self, direction: DependencyDirection, ) -> impl Iterator> + 'a { let package_graph = self.graph.package_graph; // Use the package graph's SCCs for the topo order guarantee. package_graph .sccs() .node_iter(direction.into()) .filter_map(move |package_ix| { let package_id = &package_graph.dep_graph()[package_ix]; let package = package_graph .metadata(package_id) .expect("valid package ID"); self.features_for_package_impl(package) }) } /// Returns the set of "root feature" IDs in the specified direction. /// /// * If direction is Forward, return the set of feature IDs that do not have any dependencies /// within the selected graph. /// * If direction is Reverse, return the set of feature IDs that do not have any dependents /// within the selected graph. /// /// ## Cycles /// /// If a root consists of a dependency cycle, all the packages in it will be returned in /// non-dev order (when the direction is forward). pub fn root_ids<'a>( &'a self, direction: DependencyDirection, ) -> impl ExactSizeIterator> + 'a { let dep_graph = self.graph.dep_graph(); let package_graph = self.graph.package_graph; self.core .roots(dep_graph, self.graph.sccs(), direction) .into_iter() .map(move |feature_ix| FeatureId::from_node(package_graph, &dep_graph[feature_ix])) } /// Returns the set of "root feature" metadatas in the specified direction. /// /// * If direction is Forward, return the set of metadatas that do not have any dependencies /// within the selected graph. /// * If direction is Reverse, return the set of metadatas that do not have any dependents /// within the selected graph. /// /// ## Cycles /// /// If a root consists of a dependency cycle, all the packages in it will be returned in /// non-dev order (when the direction is forward). pub fn root_features<'a>( &'a self, direction: DependencyDirection, ) -> impl Iterator> + 'a { let feature_graph = self.graph; self.core .roots(feature_graph.dep_graph(), feature_graph.sccs(), direction) .into_iter() .map(move |feature_ix| { feature_graph .metadata_for_node(feature_graph.dep_graph()[feature_ix]) .expect("feature node should be known") }) } /// Creates an iterator over `ConditionalLink` instances in the direction specified. /// /// ## Cycles /// /// The links in a dependency cycle will be returned in non-dev order. When the direction is /// forward, if feature Foo has a dependency on Bar, and Bar has a cyclic dev-dependency on Foo, /// then the link Foo -> Bar is returned before the link Bar -> Foo. pub fn conditional_links<'a>( &'a self, direction: DependencyDirection, ) -> impl Iterator> + 'a { let graph = self.graph; self.core .links(graph.dep_graph(), graph.sccs(), direction) .filter_map(move |(source_ix, target_ix, edge_ix)| { graph .edge_to_conditional_link(source_ix, target_ix, edge_ix, None) .map(|(link, _)| link) }) } // --- // Helper methods // --- fn features_for_package_impl<'a>( &'a self, package: PackageMetadata<'g>, ) -> Option> { let dep_graph = self.graph.dep_graph(); let core = &self.core; let mut features = self .graph .feature_ixs_for_package_ix(package.package_ix()) .filter_map(|feature_ix| { if core.contains(feature_ix) { Some(FeatureId::node_to_feature(package, &dep_graph[feature_ix])) } else { None } }) .peekable(); if features.peek().is_some() { // At least one feature was returned. Some(FeatureList::new(package, features)) } else { None } } /// Returns all the package ixs without topologically sorting them. pub(in crate::graph) fn ixs_unordered( &self, ) -> impl Iterator> + '_ { self.core.included.ones().map(NodeIndex::new) } /// Returns true if this feature set contains the given package ix. #[allow(dead_code)] pub(in crate::graph) fn contains_package_ix(&self, package_ix: NodeIndex) -> bool { self.graph .feature_ixs_for_package_ix(package_ix) .any(|feature_ix| self.core.contains(feature_ix)) } // Currently a helper for debugging -- will be made public in the future. #[doc(hidden)] pub fn links<'a>( &'a self, direction: DependencyDirection, ) -> impl Iterator, FeatureId<'g>, &'g FeatureEdge)> + 'a { let feature_graph = self.graph; self.core .links(feature_graph.dep_graph(), feature_graph.sccs(), direction) .map(move |(source_ix, target_ix, edge_ix)| { ( FeatureId::from_node( feature_graph.package_graph(), &feature_graph.dep_graph()[source_ix], ), FeatureId::from_node( feature_graph.package_graph(), &feature_graph.dep_graph()[target_ix], ), &feature_graph.dep_graph()[edge_ix], ) }) } } impl PartialEq for FeatureSet<'_> { fn eq(&self, other: &Self) -> bool { ::std::ptr::eq(self.graph.package_graph, other.graph.package_graph) && self.core == other.core } } impl Eq for FeatureSet<'_> {} guppy-0.17.25/src/graph/feature/weak.rs000064400000000000000000000114231046102023000157650ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 //! Support for weak features. use crate::graph::{ PackageIx, feature::{ConditionalLink, FeatureEdgeReference}, }; use indexmap::IndexSet; use itertools::Either; use petgraph::graph::EdgeIndex; use smallvec::SmallVec; /// Data structure that tracks pairs of package indexes that form weak dependencies. #[derive(Clone, Debug)] pub(super) struct WeakDependencies { ixs: IndexSet>, } impl WeakDependencies { pub(super) fn new() -> Self { Self { ixs: IndexSet::new(), } } pub(super) fn insert(&mut self, edge_ix: EdgeIndex) -> WeakIndex { WeakIndex(self.ixs.insert_full(edge_ix).0) } pub(super) fn get(&self, edge_ix: EdgeIndex) -> Option { self.ixs.get_index_of(&edge_ix).map(WeakIndex) } #[inline] pub(super) fn new_buffer_states<'g, F>(&self, accept_fn: F) -> WeakBufferStates<'g, '_, F> where F: FnMut(ConditionalLink<'g>) -> bool, { WeakBufferStates::new(self, self.ixs.len(), accept_fn) } } // Not part of the public API -- exposed for testing. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] #[doc(hidden)] pub struct WeakIndex(pub(super) usize); /// Buffer states for weak indexes, to be used during a feature resolver traversal. pub(super) struct WeakBufferStates<'g, 'a, F> { deps: &'a WeakDependencies, states: SmallVec<[SingleBufferState<'g>; 8]>, accept_fn: F, } impl<'g, 'a, F> WeakBufferStates<'g, 'a, F> where F: FnMut(ConditionalLink<'g>) -> bool, { #[inline] fn new(deps: &'a WeakDependencies, len: usize, accept_fn: F) -> Self { let mut states = SmallVec::with_capacity(len); states.resize_with(len, Default::default); Self { deps, states, accept_fn, } } pub(super) fn track( &mut self, edge_ref: FeatureEdgeReference<'g>, link: ConditionalLink<'g>, weak_index: Option, ) -> Either>, Vec>> { match weak_index { Some(index) => { match &mut self.states[index.0] { SingleBufferState::Buffered(buffer) => { // Package not currently accepted -- add to the buffer. buffer.push((link, edge_ref)); Either::Left(None) } SingleBufferState::Accepted => { // Weak link, but package already accepted. Either::Left((self.accept_fn)(link).then_some(edge_ref)) } } } None => { if !(self.accept_fn)(link) { // This link was not accepted -- ignore its presence. return Either::Left(None); } match self.deps.get(link.package_edge_ix()) { Some(weak_index) => { match std::mem::replace( &mut self.states[weak_index.0], SingleBufferState::Accepted, ) { SingleBufferState::Buffered(buffer) => { // Transition from buffered to accepted. let mut edge_refs: Vec<_> = buffer .into_iter() .filter_map(|(link, edge_ref)| { // Filter buffered links. (self.accept_fn)(link).then_some(edge_ref) }) .collect(); edge_refs.push(edge_ref); Either::Right(edge_refs) } SingleBufferState::Accepted => { // Weak link, but package already accepted. Either::Left(Some(edge_ref)) } } } None => { // Not a weak link. Either::Left(Some(edge_ref)) } } } } } } /// Buffer state for a single weak index in an in-progress resolver. pub(super) enum SingleBufferState<'g> { Buffered(SingleBufferVec<'g>), Accepted, } impl Default for SingleBufferState<'_> { fn default() -> Self { Self::Buffered(SingleBufferVec::new()) } } type SingleBufferVec<'g> = Vec<(ConditionalLink<'g>, FeatureEdgeReference<'g>)>; guppy-0.17.25/src/graph/graph_impl.rs000064400000000000000000002404661046102023000155400ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::{ CargoMetadata, DependencyKind, Error, JsonValue, MetadataCommand, PackageId, graph::{ BuildTarget, BuildTargetId, BuildTargetImpl, BuildTargetKind, Cycles, DependencyDirection, OwnedBuildTargetId, PackageIx, PackageQuery, PackageSet, cargo_version_matches, feature::{FeatureGraphImpl, FeatureId, FeatureLabel, FeatureNode}, }, petgraph_support::{IxBitSet, scc::Sccs, topo::TopoWithCycles}, platform::{EnabledTernary, PlatformSpec, PlatformStatus, PlatformStatusImpl}, }; use ahash::AHashMap; use camino::{Utf8Path, Utf8PathBuf}; use fixedbitset::FixedBitSet; use indexmap::{IndexMap, IndexSet}; use once_cell::sync::OnceCell; use petgraph::{ algo::{DfsSpace, has_path_connecting}, graph::EdgeReference, prelude::*, visit::EdgeFiltered, }; use semver::{Version, VersionReq}; use smallvec::SmallVec; use std::{ collections::{BTreeMap, HashSet}, fmt, iter::{self, FromIterator}, }; use super::feature::{FeatureFilter, FeatureSet}; /// A graph of packages and dependencies between them, parsed from metadata returned by `cargo /// metadata`. /// /// For examples on how to use `PackageGraph`, see /// [the `examples` directory](https://github.com/guppy-rs/guppy/tree/main/guppy/examples) /// in this crate. #[derive(Clone, Debug)] pub struct PackageGraph { // Source of truth data. pub(super) dep_graph: Graph, // The strongly connected components of the graph, computed on demand. pub(super) sccs: OnceCell>, // Feature graph, computed on demand. pub(super) feature_graph: OnceCell, // XXX Should this be in an Arc for quick cloning? Not clear how this would work with node // filters though. pub(super) data: PackageGraphData, } /// Per-package data for a PackageGraph instance. #[derive(Clone, Debug)] pub(super) struct PackageGraphData { pub(super) packages: AHashMap, pub(super) workspace: WorkspaceImpl, } impl PackageGraph { /// Executes the given `MetadataCommand` and constructs a `PackageGraph` from it. pub fn from_command(command: &mut MetadataCommand) -> Result { command.build_graph() } /// Parses the given `Metadata` and constructs a `PackageGraph` from it. pub fn from_metadata(metadata: CargoMetadata) -> Result { Self::build(metadata.0).map_err(|error| *error) } /// Constructs a package graph from the given JSON output of `cargo metadata`. /// /// Generally, `guppy` expects the `cargo metadata` command to be run with `--all-features`, so /// that `guppy` has a full view of the dependency graph. /// /// For full functionality, `cargo metadata` should be run without `--no-deps`, so that `guppy` /// knows about third-party crates and dependency edges. However, `guppy` supports a "light" /// mode if `--no-deps` is run, in which case the following limitations will apply: /// * dependency queries will not work /// * there will be no information about non-workspace crates pub fn from_json(json: impl AsRef) -> Result { let metadata = CargoMetadata::parse_json(json)?; Self::from_metadata(metadata) } /// Verifies internal invariants on this graph. Not part of the documented API. #[doc(hidden)] pub fn verify(&self) -> Result<(), Error> { // Graph structure checks. let node_count = self.dep_graph.node_count(); let package_count = self.data.packages.len(); if node_count != package_count { return Err(Error::PackageGraphInternalError(format!( "number of nodes = {node_count} different from packages = {package_count}", ))); } // TODO: The dependency graph can have cyclic dev-dependencies. Add a check to ensure that // the graph without any dev-only dependencies is acyclic. let workspace = self.workspace(); let workspace_ids: HashSet<_> = workspace.member_ids().collect(); for metadata in self.packages() { let package_id = metadata.id(); match metadata.source().workspace_path() { Some(workspace_path) => { // This package is in the workspace, so the workspace should have information // about it. let metadata2 = workspace.member_by_path(workspace_path); let metadata2_id = metadata2.map(|metadata| metadata.id()); if !matches!(metadata2_id, Ok(id) if id == package_id) { return Err(Error::PackageGraphInternalError(format!( "package {package_id} has workspace path {workspace_path:?} but query by path returned {metadata2_id:?}", ))); } let metadata3 = workspace.member_by_name(metadata.name()); let metadata3_id = metadata3.map(|metadata| metadata.id()); if !matches!(metadata3_id, Ok(id) if id == package_id) { return Err(Error::PackageGraphInternalError(format!( "package {} has name {}, but workspace query by name returned {:?}", package_id, metadata.name(), metadata3_id, ))); } } None => { // This package is not in the workspace. if workspace_ids.contains(package_id) { return Err(Error::PackageGraphInternalError(format!( "package {package_id} has no workspace path but is in workspace", ))); } } } for build_target in metadata.build_targets() { match build_target.id() { BuildTargetId::Library | BuildTargetId::BuildScript => { // Ensure that the name is populated (this may panic if it isn't). build_target.name(); } BuildTargetId::Binary(name) | BuildTargetId::Example(name) | BuildTargetId::Test(name) | BuildTargetId::Benchmark(name) => { if name != build_target.name() { return Err(Error::PackageGraphInternalError(format!( "package {} has build target name mismatch ({} != {})", package_id, name, build_target.name(), ))); } } } let id_kind_mismatch = match build_target.id() { BuildTargetId::Library => match build_target.kind() { BuildTargetKind::LibraryOrExample(_) | BuildTargetKind::ProcMacro => false, BuildTargetKind::Binary => true, }, BuildTargetId::Example(_) => match build_target.kind() { BuildTargetKind::LibraryOrExample(_) => false, BuildTargetKind::ProcMacro | BuildTargetKind::Binary => true, }, BuildTargetId::BuildScript | BuildTargetId::Binary(_) | BuildTargetId::Test(_) | BuildTargetId::Benchmark(_) => match build_target.kind() { BuildTargetKind::LibraryOrExample(_) | BuildTargetKind::ProcMacro => true, BuildTargetKind::Binary => false, }, }; if id_kind_mismatch { return Err(Error::PackageGraphInternalError(format!( "package {} has build target id {:?}, which doesn't match kind {:?}", package_id, build_target.id(), build_target.kind(), ))); } } for link in self.dep_links_ixs_directed(metadata.package_ix(), Outgoing) { let to = link.to(); let to_id = to.id(); let to_version = to.version(); // Two invariants: // 1. At least one of the edges should be specified. // 2. The specified package should match the version dependency. let req = link.version_req(); // A requirement of "*" filters out pre-release versions with the semver crate, // but cargo accepts them. // See https://github.com/steveklabnik/semver/issues/98. if !cargo_version_matches(req, to_version) { return Err(Error::PackageGraphInternalError(format!( "{package_id} -> {to_id}: version ({to_version}) doesn't match requirement ({req:?})", ))); } let is_any = link.normal().is_present() || link.build().is_present() || link.dev().is_present(); if !is_any { return Err(Error::PackageGraphInternalError(format!( "{package_id} -> {to_id}: no edge info found", ))); } } } // Construct and check the feature graph for internal consistency. self.feature_graph().verify()?; Ok(()) } /// Returns information about the workspace. pub fn workspace(&self) -> Workspace<'_> { Workspace { graph: self, inner: &self.data.workspace, } } /// Returns an iterator over all the package IDs in this graph. pub fn package_ids(&self) -> impl ExactSizeIterator { self.data.package_ids() } /// Returns an iterator over all the packages in this graph. pub fn packages(&self) -> impl ExactSizeIterator> { self.data .packages .values() .map(move |inner| PackageMetadata::new(self, inner)) } /// Returns the metadata for the given package ID. pub fn metadata(&self, package_id: &PackageId) -> Result, Error> { let inner = self .data .metadata_impl(package_id) .ok_or_else(|| Error::UnknownPackageId(package_id.clone()))?; Ok(PackageMetadata::new(self, inner)) } /// Returns the number of packages in this graph. pub fn package_count(&self) -> usize { // This can be obtained in two different ways: self.dep_graph.node_count() or // self.data.packages.len(). verify() checks that they return the same results. // // Use this way for symmetry with link_count below (which can only be obtained through the // graph). self.dep_graph.node_count() } /// Returns the number of links in this graph. pub fn link_count(&self) -> usize { self.dep_graph.edge_count() } /// Creates a new cache for `depends_on` queries. /// /// The cache is optional but can speed up some queries. pub fn new_depends_cache(&self) -> DependsCache<'_> { DependsCache::new(self) } /// Returns true if `package_a` depends (directly or indirectly) on `package_b`. /// /// In other words, this returns true if `package_b` is a (possibly transitive) dependency of /// `package_a`. /// /// This also returns true if `package_a` is the same as `package_b`. /// /// For repeated queries, consider using `new_depends_cache` to speed up queries. pub fn depends_on(&self, package_a: &PackageId, package_b: &PackageId) -> Result { let mut depends_cache = self.new_depends_cache(); depends_cache.depends_on(package_a, package_b) } /// Returns true if `package_a` directly depends on `package_b`. /// /// In other words, this returns true if `package_b` is a direct dependency of `package_a`. /// /// This returns false if `package_a` is the same as `package_b`. pub fn directly_depends_on( &self, package_a: &PackageId, package_b: &PackageId, ) -> Result { let a_ix = self.package_ix(package_a)?; let b_ix = self.package_ix(package_b)?; Ok(self.dep_graph.contains_edge(a_ix, b_ix)) } /// Returns information about dependency cycles in this graph. /// /// For more information, see the documentation for `Cycles`. pub fn cycles(&self) -> Cycles<'_> { Cycles::new(self) } // For more traversals, see query.rs. // --- // Helper methods // --- fn dep_links_ixs_directed( &self, package_ix: NodeIndex, dir: Direction, ) -> impl Iterator> { self.dep_graph .edges_directed(package_ix, dir) .map(move |edge| self.edge_ref_to_link(edge)) } fn link_between_ixs( &self, from_ix: NodeIndex, to_ix: NodeIndex, ) -> Option> { self.dep_graph .find_edge(from_ix, to_ix) .map(|edge_ix| self.edge_ix_to_link(edge_ix)) } /// Constructs a map of strongly connected components for this graph. pub(super) fn sccs(&self) -> &Sccs { self.sccs.get_or_init(|| { let edge_filtered = EdgeFiltered::from_fn(&self.dep_graph, |edge| !edge.weight().dev_only()); // Sort the entire graph without dev-only edges -- a correct graph would be cycle-free // but we don't currently do a consistency check for this so handle cycles. // TODO: should we check at construction time? or bubble up a warning somehow? let topo = TopoWithCycles::new(&edge_filtered); Sccs::new(&self.dep_graph, |scc| { topo.sort_nodes(scc); }) }) } /// Invalidates internal caches. Primarily for testing. #[doc(hidden)] pub fn invalidate_caches(&mut self) { self.sccs.take(); self.feature_graph.take(); } /// Returns the inner dependency graph. /// /// Should this be exposed publicly? Not sure. pub(super) fn dep_graph(&self) -> &Graph { &self.dep_graph } /// Maps an edge reference to a dependency link. pub(super) fn edge_ref_to_link<'g>( &'g self, edge: EdgeReference<'g, PackageLinkImpl, PackageIx>, ) -> PackageLink<'g> { PackageLink::new( self, edge.source(), edge.target(), edge.id(), Some(edge.weight()), ) } /// Maps an edge index to a dependency link. pub(super) fn edge_ix_to_link(&self, edge_ix: EdgeIndex) -> PackageLink<'_> { let (source_ix, target_ix) = self .dep_graph .edge_endpoints(edge_ix) .expect("valid edge ix"); PackageLink::new( self, source_ix, target_ix, edge_ix, self.dep_graph.edge_weight(edge_ix), ) } /// Maps an iterator of package IDs to their internal graph node indexes. pub(super) fn package_ixs<'g, 'a, B>( &'g self, package_ids: impl IntoIterator, ) -> Result where B: iter::FromIterator>, { package_ids .into_iter() .map(|package_id| self.package_ix(package_id)) .collect() } /// Maps a package ID to its internal graph node index, and returns an `UnknownPackageId` error /// if the package isn't found. pub(super) fn package_ix(&self, package_id: &PackageId) -> Result, Error> { Ok(self.metadata(package_id)?.package_ix()) } } impl PackageGraphData { /// Returns an iterator over all the package IDs in this graph. pub fn package_ids(&self) -> impl ExactSizeIterator { self.packages.keys() } // --- // Helper methods // --- #[inline] pub(super) fn metadata_impl(&self, package_id: &PackageId) -> Option<&PackageMetadataImpl> { self.packages.get(package_id) } } /// An optional cache used to speed up `depends_on` queries. /// /// Created with `PackageGraph::new_depends_cache()`. #[derive(Clone, Debug)] pub struct DependsCache<'g> { package_graph: &'g PackageGraph, dfs_space: DfsSpace, FixedBitSet>, } impl<'g> DependsCache<'g> { /// Creates a new cache for `depends_on` queries for this package graph. /// /// This holds a shared reference to the package graph. This is to ensure that the cache is /// invalidated if the package graph is mutated. pub fn new(package_graph: &'g PackageGraph) -> Self { Self { package_graph, dfs_space: DfsSpace::new(&package_graph.dep_graph), } } /// Returns true if `package_a` depends (directly or indirectly) on `package_b`. /// /// In other words, this returns true if `package_b` is a (possibly transitive) dependency of /// `package_a`. pub fn depends_on( &mut self, package_a: &PackageId, package_b: &PackageId, ) -> Result { let a_ix = self.package_graph.package_ix(package_a)?; let b_ix = self.package_graph.package_ix(package_b)?; Ok(has_path_connecting( self.package_graph.dep_graph(), a_ix, b_ix, Some(&mut self.dfs_space), )) } } /// Information about a workspace, parsed from metadata returned by `cargo metadata`. /// /// For more about workspaces, see /// [Cargo Workspaces](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html) in *The Rust /// Programming Language*. #[derive(Clone, Debug)] pub struct Workspace<'g> { graph: &'g PackageGraph, pub(super) inner: &'g WorkspaceImpl, } impl<'g> Workspace<'g> { /// Returns the workspace root. pub fn root(&self) -> &'g Utf8Path { &self.inner.root } /// Returns the target directory in which output artifacts are stored. pub fn target_directory(&self) -> &'g Utf8Path { &self.inner.target_directory } /// Returns the build directory in which intermediate build artifacts are /// stored. /// /// This field is only available if the `Metadata` was generated by Cargo /// 1.91 or later. pub fn build_directory(&self) -> Option<&'g Utf8Path> { self.inner.build_directory.as_deref() } /// Returns an iterator over the workspace default members. /// /// Default members are the packages that are built when `cargo build` is /// run without any arguments in the workspace root. /// /// This field is only available if the `Metadata` was generated by Cargo /// 1.71 or later. For older versions, this will return an empty iterator. pub fn default_member_ids(&self) -> impl ExactSizeIterator + use<'g> { self.inner.default_members.iter() } /// Returns an iterator over package metadatas for workspace default /// members. /// /// Default members are the packages that are built when `cargo build` is /// run without any arguments in the workspace root. /// /// This field is only available if the `Metadata` was generated by Cargo /// 1.71 or later. For older versions, this will return an empty iterator. pub fn default_members(&self) -> impl ExactSizeIterator> + use<'g> { let graph = self.graph; self.inner .default_members .iter() .map(move |id| graph.metadata(id).expect("valid package ID")) } /// Returns the number of packages in this workspace. pub fn member_count(&self) -> usize { self.inner.members_by_path.len() } /// Returns true if the workspace contains a package by the given name. pub fn contains_name(&self, name: impl AsRef) -> bool { self.inner.members_by_name.contains_key(name.as_ref()) } /// Returns true if the workspace contains a package by the given workspace path. pub fn contains_path(&self, path: impl AsRef) -> bool { self.inner.members_by_path.contains_key(path.as_ref()) } /// Returns an iterator over package metadatas, sorted by the path they're in. pub fn iter(&self) -> impl ExactSizeIterator> + use<'g> { self.iter_by_path().map(|(_, package)| package) } /// Returns an iterator over workspace paths and package metadatas, sorted by the path /// they're in. pub fn iter_by_path( &self, ) -> impl ExactSizeIterator)> + use<'g> { let graph = self.graph; self.inner.members_by_path.iter().map(move |(path, id)| { ( path.as_path(), graph.metadata(id).expect("valid package ID"), ) }) } /// Returns an iterator over workspace names and package metadatas, sorted by names. pub fn iter_by_name( &self, ) -> impl ExactSizeIterator)> + use<'g> { let graph = self.graph; self.inner .members_by_name .iter() .map(move |(name, id)| (name.as_ref(), graph.metadata(id).expect("valid package ID"))) } /// Returns an iterator over package IDs for workspace members. The package IDs will be returned /// in the same order as `members`, sorted by the path they're in. pub fn member_ids(&self) -> impl ExactSizeIterator + use<'g> { self.inner.members_by_path.values() } /// Maps the given path to the corresponding workspace member. /// /// Returns an error if the path didn't match any workspace members. pub fn member_by_path(&self, path: impl AsRef) -> Result, Error> { let path = path.as_ref(); let id = self .inner .members_by_path .get(path) .ok_or_else(|| Error::UnknownWorkspacePath(path.to_path_buf()))?; Ok(self.graph.metadata(id).expect("valid package ID")) } /// Maps the given paths to their corresponding workspace members, returning a new value of /// the specified collection type (e.g. `Vec`). /// /// Returns an error if any of the paths were unknown. pub fn members_by_paths( &self, paths: impl IntoIterator>, ) -> Result where B: FromIterator>, { paths .into_iter() .map(|path| self.member_by_path(path.as_ref())) .collect() } /// Maps the given name to the corresponding workspace member. /// /// Returns an error if the name didn't match any workspace members. pub fn member_by_name(&self, name: impl AsRef) -> Result, Error> { let name = name.as_ref(); let id = self .inner .members_by_name .get(name) .ok_or_else(|| Error::UnknownWorkspaceName(name.to_string()))?; Ok(self.graph.metadata(id).expect("valid package ID")) } /// Maps the given names to their corresponding workspace members, returning a new value of /// the specified collection type (e.g. `Vec`). /// /// Returns an error if any of the paths were unknown. pub fn members_by_names( &self, names: impl IntoIterator>, ) -> Result where B: FromIterator>, { names .into_iter() .map(|name| self.member_by_name(name.as_ref())) .collect() } /// Returns the freeform metadata table for this workspace. /// /// This is the same as the `workspace.metadata` section of `Cargo.toml`. This section is /// typically used by tools which would like to store workspace configuration in `Cargo.toml`. pub fn metadata_table(&self) -> &'g JsonValue { &self.inner.metadata_table } } #[cfg(feature = "rayon1")] mod workspace_rayon { use super::*; use rayon::prelude::*; /// These parallel iterators require the `rayon1` feature is enabled. impl<'g> Workspace<'g> { /// Returns a parallel iterator over package metadatas, sorted by workspace path. /// /// Requires the `rayon1` feature to be enabled. pub fn par_iter(&self) -> impl ParallelIterator> + use<'g> { self.par_iter_by_path().map(|(_, package)| package) } /// Returns a parallel iterator over workspace paths and package metadatas, sorted by /// workspace paths. /// /// Requires the `rayon1` feature to be enabled. pub fn par_iter_by_path( &self, ) -> impl ParallelIterator)> + use<'g> { let graph = self.graph; self.inner .members_by_path .par_iter() .map(move |(path, id)| { ( path.as_path(), graph.metadata(id).expect("valid package ID"), ) }) } /// Returns a parallel iterator over workspace names and package metadatas, sorted by /// package names. /// /// Requires the `rayon1` feature to be enabled. pub fn par_iter_by_name( &self, ) -> impl ParallelIterator)> + use<'g> { let graph = self.graph; self.inner .members_by_name .par_iter() .map(move |(name, id)| { (name.as_ref(), graph.metadata(id).expect("valid package ID")) }) } } } #[derive(Clone, Debug)] pub(super) struct WorkspaceImpl { pub(super) root: Utf8PathBuf, pub(super) target_directory: Utf8PathBuf, pub(super) build_directory: Option, pub(super) metadata_table: JsonValue, // This is a BTreeMap to allow presenting data in sorted order. pub(super) members_by_path: BTreeMap, pub(super) members_by_name: BTreeMap, PackageId>, pub(super) default_members: Vec, // Cache for members by name (only used for proptests) #[cfg(feature = "proptest1")] pub(super) name_list: OnceCell>>, } /// Information about a specific package in a `PackageGraph`. /// /// Most of the metadata is extracted from `Cargo.toml` files. See /// [the `Cargo.toml` reference](https://doc.rust-lang.org/cargo/reference/manifest.html) for more /// details. #[derive(Copy, Clone)] pub struct PackageMetadata<'g> { graph: &'g PackageGraph, inner: &'g PackageMetadataImpl, } impl fmt::Debug for PackageMetadata<'_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("PackageMetadata") .field("package_id", &self.id().repr()) .field("..", &"..") .finish() } } assert_covariant!(PackageMetadata); impl<'g> PackageMetadata<'g> { pub(super) fn new(graph: &'g PackageGraph, inner: &'g PackageMetadataImpl) -> Self { Self { graph, inner } } /// Returns the unique identifier for this package. pub fn id(&self) -> &'g PackageId { &self.graph.dep_graph[self.inner.package_ix] } /// Returns the package graph this `PackageMetadata` is derived from. pub fn graph(&self) -> &'g PackageGraph { self.graph } /// Creates a `PackageQuery` consisting of this package, in the given direction. /// /// The `PackageQuery` can be used to inspect dependencies in this graph. pub fn to_package_query(&self, direction: DependencyDirection) -> PackageQuery<'g> { self.graph .query_from_parts(iter::once(self.inner.package_ix).collect(), direction) } /// Creates a `PackageSet` consisting of just this package. pub fn to_package_set(&self) -> PackageSet<'g> { let included: IxBitSet = iter::once(self.package_ix()).collect(); PackageSet::from_included(self.graph, included) } /// Creates a `FeatureSet` that consists of all features in the package that match the given /// named filter. pub fn to_feature_set(&self, features: impl FeatureFilter<'g>) -> FeatureSet<'g> { self.to_package_set().to_feature_set(features) } // --- // Dependency traversals // --- /// Returns `PackageLink` instances corresponding to the direct dependencies for this package in /// the specified direction. pub fn direct_links_directed( &self, direction: DependencyDirection, ) -> impl Iterator> + 'g + use<'g> { self.direct_links_impl(direction.into()) } /// Returns `PackageLink` instances corresponding to the direct dependencies for this package. pub fn direct_links(&self) -> impl Iterator> + 'g + use<'g> { self.direct_links_impl(Outgoing) } /// Returns `PackageLink` instances corresponding to the packages that directly depend on this /// one. pub fn reverse_direct_links(&self) -> impl Iterator> + 'g + use<'g> { self.direct_links_impl(Incoming) } /// Returns the direct `PackageLink` between `self` and `other` in the specified direction: /// * `Forward`: from `self` to `other` /// * `Reverse`: from `other` to `self` /// /// Returns `None` if the direct link does not exist, or an error if `to` isn't found in /// `self.graph()`. pub fn link_between( &self, other: &PackageId, direction: DependencyDirection, ) -> Result>, Error> { self.link_between_impl(other, direction.into()) } /// Returns the direct `PackageLink` from `self` to the specified package, or `None` if `self` /// does not directly depend on the specified package. /// /// Returns an error if `to` isn't found in `self.graph()`. pub fn link_to(&self, to: &PackageId) -> Result>, Error> { self.link_between_impl(to, Outgoing) } /// Returns the direct `PackageLink` from the specified package to `self`, or `None` if the /// specified package does not directly depend on `self`. /// /// Returns an error if `from` isn't found in `self.graph()`. pub fn link_from(&self, from: &PackageId) -> Result>, Error> { self.link_between_impl(from, Incoming) } // --- // Package fields // --- /// Returns the name of this package. /// /// This is the same as the `name` field of `Cargo.toml`. pub fn name(&self) -> &'g str { &self.inner.name } /// Returns the version of this package as resolved by Cargo. /// /// This is the same as the `version` field of `Cargo.toml`. pub fn version(&self) -> &'g Version { &self.inner.version } /// Returns the authors of this package. /// /// This is the same as the `authors` field of `Cargo.toml`. pub fn authors(&self) -> &'g [String] { &self.inner.authors } /// Returns a short description for this package. /// /// This is the same as the `description` field of `Cargo.toml`. pub fn description(&self) -> Option<&'g str> { self.inner.description.as_ref().map(|x| x.as_ref()) } /// Returns an SPDX 2.1 license expression for this package, if specified. /// /// This is the same as the `license` field of `Cargo.toml`. Note that `guppy` does not perform /// any validation on this, though `crates.io` does if a crate is uploaded there. pub fn license(&self) -> Option<&'g str> { self.inner.license.as_ref().map(|x| x.as_ref()) } /// Returns the path to a license file for this package, if specified. /// /// This is the same as the `license_file` field of `Cargo.toml`. It is typically only specified /// for nonstandard licenses. pub fn license_file(&self) -> Option<&'g Utf8Path> { self.inner.license_file.as_ref().map(|path| path.as_ref()) } /// Returns the source from which this package was retrieved. /// /// This may be the workspace path, an external path, or a registry like `crates.io`. pub fn source(&self) -> PackageSource<'g> { PackageSource::new(&self.inner.source) } /// Returns true if this package is in the workspace. /// /// For more detailed information, use `source()`. pub fn in_workspace(&self) -> bool { self.source().is_workspace() } /// Returns the full path to the `Cargo.toml` for this package. /// /// This is specific to the system that `cargo metadata` was run on. pub fn manifest_path(&self) -> &'g Utf8Path { &self.inner.manifest_path } /// Returns categories for this package. /// /// This is the same as the `categories` field of `Cargo.toml`. For packages on `crates.io`, /// returned values are guaranteed to be /// [valid category slugs](https://crates.io/category_slugs). pub fn categories(&self) -> &'g [String] { &self.inner.categories } /// Returns keywords for this package. /// /// This is the same as the `keywords` field of `Cargo.toml`. pub fn keywords(&self) -> &'g [String] { &self.inner.keywords } /// Returns a path to the README for this package, if specified. /// /// This is the same as the `readme` field of `Cargo.toml`. The path returned is relative to the /// directory the `Cargo.toml` is in (i.e. relative to the parent of `self.manifest_path()`). pub fn readme(&self) -> Option<&'g Utf8Path> { self.inner.readme.as_ref().map(|path| path.as_ref()) } /// Returns the source code repository for this package, if specified. /// /// This is the same as the `repository` field of `Cargo.toml`. pub fn repository(&self) -> Option<&'g str> { self.inner.repository.as_ref().map(|x| x.as_ref()) } /// Returns the homepage for this package, if specified. /// /// This is the same as the `homepage` field of `Cargo.toml`. pub fn homepage(&self) -> Option<&'g str> { self.inner.homepage.as_ref().map(|x| x.as_ref()) } /// Returns the documentation URL for this package, if specified. /// /// This is the same as the `homepage` field of `Cargo.toml`. pub fn documentation(&self) -> Option<&'g str> { self.inner.documentation.as_ref().map(|x| x.as_ref()) } /// Returns the Rust edition this package is written against. /// /// This is the same as the `edition` field of `Cargo.toml`. It is `"2015"` by default. pub fn edition(&self) -> &'g str { &self.inner.edition } /// Returns the freeform metadata table for this package. /// /// This is the same as the `package.metadata` section of `Cargo.toml`. This section is /// typically used by tools which would like to store package configuration in `Cargo.toml`. pub fn metadata_table(&self) -> &'g JsonValue { &self.inner.metadata_table } /// Returns the name of a native library this package links to, if specified. /// /// This is the same as the `links` field of `Cargo.toml`. See [The `links` Manifest /// Key](https://doc.rust-lang.org/cargo/reference/build-scripts.html#the-links-manifest-key) in /// the Cargo book for more details. pub fn links(&self) -> Option<&'g str> { self.inner.links.as_ref().map(|x| x.as_ref()) } /// Returns the registries to which this package may be published. /// /// This is derived from the `publish` field of `Cargo.toml`. pub fn publish(&self) -> PackagePublish<'g> { PackagePublish::new(&self.inner.publish) } /// Returns the binary that is run by default, if specified. /// /// Information about this binary can be queried using [the `build_target` /// method](Self::build_target). /// /// This is derived from the `default-run` field of `Cargo.toml`. pub fn default_run(&self) -> Option<&'g str> { self.inner.default_run.as_ref().map(|x| x.as_ref()) } /// Returns the minimum Rust compiler version, which should be able to compile the package, if /// specified. /// /// This is the same as the `rust-version` field of `Cargo.toml`. For more, see [the /// `rust-version` field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field) /// in the Cargo reference. pub fn minimum_rust_version(&self) -> Option<&'g Version> { self.inner.rust_version.as_ref() } /// Returns the minimum Rust compiler version, which should be able to compile the package, if /// specified. /// /// Returned as a [`semver::VersionReq`]. This is actually not correct -- it is deprecated and /// will go away in the next major version of guppy: use [`Self::minimum_rust_version`] instead. /// /// This is the same as the `rust-version` field of `Cargo.toml`. For more, see [the /// `rust-version` /// field](https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field) in /// the Cargo reference. #[deprecated( since = "0.17.1", note = "use Self::rust_version instead, it returns a Version" )] pub fn rust_version(&self) -> Option<&'g VersionReq> { self.inner.rust_version_req.as_ref() } /// Returns all the build targets for this package. /// /// For more, see [Cargo /// Targets](https://doc.rust-lang.org/nightly/cargo/reference/cargo-targets.html#cargo-targets) /// in the Cargo reference. pub fn build_targets(&self) -> impl Iterator> + use<'g> { self.inner.build_targets.iter().map(BuildTarget::new) } /// Looks up a build target by identifier. pub fn build_target(&self, id: &BuildTargetId<'_>) -> Option> { self.inner .build_targets .get_key_value(id.as_key()) .map(BuildTarget::new) } /// Returns true if this package is a procedural macro. /// /// For more about procedural macros, see [Procedural /// Macros](https://doc.rust-lang.org/reference/procedural-macros.html) in the Rust reference. pub fn is_proc_macro(&self) -> bool { match self.build_target(&BuildTargetId::Library) { Some(build_target) => matches!(build_target.kind(), BuildTargetKind::ProcMacro), None => false, } } /// Returns true if this package has a build script. /// /// Cargo only follows build dependencies if a build script is set. /// /// For more about build scripts, see [Build /// Scripts](https://doc.rust-lang.org/cargo/reference/build-scripts.html) in the Cargo /// reference. pub fn has_build_script(&self) -> bool { self.build_target(&BuildTargetId::BuildScript).is_some() } /// Returns true if this package has a named feature named `default`. /// /// For more about default features, see [The `[features]` /// section](https://doc.rust-lang.org/cargo/reference/manifest.html#the-features-section) in /// the Cargo reference. pub fn has_default_feature(&self) -> bool { self.inner.has_default_feature } /// Returns the `FeatureId` corresponding to the default feature. pub fn default_feature_id(&self) -> FeatureId<'g> { if self.inner.has_default_feature { FeatureId::new(self.id(), FeatureLabel::Named("default")) } else { FeatureId::base(self.id()) } } /// Returns the list of named features available for this package. This will include a feature /// named "default" if it is defined. /// /// A named feature is listed in the `[features]` section of `Cargo.toml`. For more, see /// [the reference](https://doc.rust-lang.org/cargo/reference/manifest.html#the-features-section). pub fn named_features(&self) -> impl Iterator + 'g + use<'g> { self.named_features_full() .map(|(_, named_feature, _)| named_feature) } // --- // Helper methods // -- #[inline] pub(super) fn package_ix(&self) -> NodeIndex { self.inner.package_ix } fn link_between_impl( &self, other: &PackageId, dir: Direction, ) -> Result>, Error> { let other_ix = self.graph.package_ix(other)?; match dir { Direction::Outgoing => Ok(self.graph.link_between_ixs(self.package_ix(), other_ix)), Direction::Incoming => Ok(self.graph.link_between_ixs(other_ix, self.package_ix())), } } fn direct_links_impl( &self, dir: Direction, ) -> impl Iterator> + 'g + use<'g> { self.graph.dep_links_ixs_directed(self.package_ix(), dir) } pub(super) fn get_feature_idx(&self, label: FeatureLabel<'_>) -> Option { match label { FeatureLabel::Base => Some(FeatureIndexInPackage::Base), FeatureLabel::OptionalDependency(dep_name) => self .inner .optional_deps .get_index_of(dep_name) .map(FeatureIndexInPackage::OptionalDependency), FeatureLabel::Named(feature_name) => self .inner .named_features .get_index_of(feature_name) .map(FeatureIndexInPackage::Named), } } pub(super) fn feature_idx_to_label(&self, idx: FeatureIndexInPackage) -> FeatureLabel<'g> { match idx { FeatureIndexInPackage::Base => FeatureLabel::Base, FeatureIndexInPackage::OptionalDependency(idx) => FeatureLabel::OptionalDependency( self.inner .optional_deps .get_index(idx) .expect("feature idx in optional_deps should be valid") .as_ref(), ), FeatureIndexInPackage::Named(idx) => FeatureLabel::Named( self.inner .named_features .get_index(idx) .expect("feature idx in optional_deps should be valid") .0 .as_ref(), ), } } #[allow(dead_code)] pub(super) fn all_feature_nodes(&self) -> impl Iterator + 'g + use<'g> { let package_ix = self.package_ix(); iter::once(FeatureNode::new( self.package_ix(), FeatureIndexInPackage::Base, )) .chain( (0..self.inner.named_features.len()) .map(move |named_idx| FeatureNode::named_feature(package_ix, named_idx)), ) .chain( (0..self.inner.optional_deps.len()) .map(move |dep_idx| FeatureNode::optional_dep(package_ix, dep_idx)), ) } pub(super) fn named_features_full( &self, ) -> impl Iterator + 'g + use<'g> { self.inner .named_features .iter() // IndexMap is documented to use indexes 0..n without holes, so this enumerate() // is correct. .enumerate() .map(|(idx, (feature, deps))| { ( FeatureIndexInPackage::Named(idx), feature.as_ref(), deps.as_slice(), ) }) } pub(super) fn optional_deps_full( &self, ) -> impl Iterator + 'g + use<'g> { self.inner .optional_deps .iter() // IndexMap is documented to use indexes 0..n without holes, so this enumerate() // is correct. .enumerate() .map(|(idx, dep_name)| { ( FeatureIndexInPackage::OptionalDependency(idx), dep_name.as_ref(), ) }) } } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub(crate) enum FeatureIndexInPackage { Base, OptionalDependency(usize), Named(usize), } /// `PackageMetadata`'s `PartialEq` implementation uses pointer equality for the `PackageGraph`. impl PartialEq for PackageMetadata<'_> { fn eq(&self, other: &Self) -> bool { // Checking for the same package ix is enough as each package is guaranteed to be a 1:1 map // with ixs. std::ptr::eq(self.graph, other.graph) && self.package_ix() == other.package_ix() } } impl Eq for PackageMetadata<'_> {} #[derive(Clone, Debug)] pub(crate) struct PackageMetadataImpl { // Implementation note: we use Box and Box to save on memory use when possible. // Fields extracted from the package. pub(super) name: Box, pub(super) version: Version, pub(super) authors: Vec, pub(super) description: Option>, pub(super) license: Option>, pub(super) license_file: Option>, pub(super) manifest_path: Box, pub(super) categories: Vec, pub(super) keywords: Vec, pub(super) readme: Option>, pub(super) repository: Option>, pub(super) homepage: Option>, pub(super) documentation: Option>, pub(super) edition: Box, pub(super) metadata_table: JsonValue, pub(super) links: Option>, pub(super) publish: PackagePublishImpl, pub(super) default_run: Option>, pub(super) rust_version: Option, pub(super) rust_version_req: Option, pub(super) named_features: IndexMap, SmallVec<[NamedFeatureDep; 4]>>, pub(super) optional_deps: IndexSet>, // Other information. pub(super) package_ix: NodeIndex, pub(super) source: PackageSourceImpl, pub(super) build_targets: BTreeMap, pub(super) has_default_feature: bool, } /// The source of a package. /// /// This enum contains information about where a package is found, and whether it is inside or /// outside the workspace. #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] pub enum PackageSource<'g> { /// This package is in the workspace. /// /// The path is relative to the workspace root. Workspace(&'g Utf8Path), /// This package is a path dependency that isn't in the workspace. /// /// The path is relative to the workspace root. Path(&'g Utf8Path), /// This package is an external dependency. /// /// * For packages retrieved from `crates.io`, the source is the string /// `"registry+https://github.com/rust-lang/crates.io-index"`. /// * For packages retrieved from other registries, the source begins with `"registry+"`. /// * For packages retrieved from Git repositories, the source begins with `"git+"`. External(&'g str), } assert_covariant!(PackageSource); impl<'g> PackageSource<'g> { /// The path to the crates.io registry. pub const CRATES_IO_REGISTRY: &'static str = "registry+https://github.com/rust-lang/crates.io-index"; pub(super) fn new(inner: &'g PackageSourceImpl) -> Self { match inner { PackageSourceImpl::Workspace(path) => PackageSource::Workspace(path), PackageSourceImpl::Path(path) => PackageSource::Path(path), PackageSourceImpl::CratesIo => PackageSource::External(Self::CRATES_IO_REGISTRY), PackageSourceImpl::External(source) => PackageSource::External(source), } } /// Returns true if this package source represents a workspace. pub fn is_workspace(&self) -> bool { matches!(self, PackageSource::Workspace(_)) } /// Returns true if this package source represents a path dependency that isn't in the /// workspace. pub fn is_path(&self) -> bool { matches!(self, PackageSource::Path(_)) } /// Returns true if this package source represents an external dependency. pub fn is_external(&self) -> bool { matches!(self, PackageSource::External(_)) } /// Returns true if the source is `crates.io`. pub fn is_crates_io(&self) -> bool { matches!(self, PackageSource::External(Self::CRATES_IO_REGISTRY)) } /// Returns true if this package is a local dependency, i.e. either in the workspace or a local /// path. pub fn is_local(&self) -> bool { !self.is_external() } /// Returns the path if this is a workspace dependency, or `None` if this is a non-workspace /// dependency. /// /// The path is relative to the workspace root. pub fn workspace_path(&self) -> Option<&'g Utf8Path> { match self { PackageSource::Workspace(path) => Some(path), _ => None, } } /// Returns the local path if this is a local dependency, or `None` if it is an external /// dependency. /// /// The path is relative to the workspace root. pub fn local_path(&self) -> Option<&'g Utf8Path> { match self { PackageSource::Path(path) | PackageSource::Workspace(path) => Some(path), _ => None, } } /// Returns the external source if this is an external dependency, or `None` if it is a local /// dependency. pub fn external_source(&self) -> Option<&'g str> { match self { PackageSource::External(source) => Some(source), _ => None, } } /// Attempts to parse an external source. /// /// Returns `None` if the external dependency could not be recognized, or if it is a local /// dependency. /// /// For more about external sources, see the documentation for [`ExternalSource`](ExternalSource). pub fn parse_external(&self) -> Option> { match self { PackageSource::External(source) => ExternalSource::new(source), _ => None, } } } impl fmt::Display for PackageSource<'_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { PackageSource::Workspace(path) => write!(f, "{path}"), PackageSource::Path(path) => write!(f, "{path}"), PackageSource::External(source) => write!(f, "{source}"), } } } /// More information about an external source. /// /// This provides information about whether an external dependency is a Git dependency or fetched /// from a registry. /// /// Returned by [`PackageSource::parse_external`](PackageSource::parse_external). #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] #[non_exhaustive] pub enum ExternalSource<'g> { /// This is a registry source, e.g. `"registry+https://github.com/rust-lang/crates.io-index"`. /// /// The associated data is the part of the string after the initial `"registry+"`. /// /// # Examples /// /// ``` /// use guppy::graph::ExternalSource; /// /// let source = "registry+https://github.com/rust-lang/crates.io-index"; /// let parsed = ExternalSource::new(source).expect("this source is understood by guppy"); /// /// assert_eq!( /// parsed, /// ExternalSource::Registry("https://github.com/rust-lang/crates.io-index"), /// ); /// ``` Registry(&'g str), /// This is a registry source that uses the [sparse registry protocol][sparse], e.g. `"sparse+https://index.crates.io"`. /// /// The associated data is the part of the string after the initial `"sparse+"`. /// /// # Examples /// /// ``` /// use guppy::graph::ExternalSource; /// /// let source = "sparse+https://index.crates.io"; /// let parsed = ExternalSource::new(source).expect("this source is understood by guppy"); /// /// assert_eq!( /// parsed, /// ExternalSource::Sparse("https://index.crates.io"), /// ); /// ``` /// /// [sparse]: https://doc.rust-lang.org/cargo/reference/registry-index.html#sparse-protocol Sparse(&'g str), /// This is a Git source. /// /// An example of a Git source string is `"git+https://github.com/rust-lang/cargo.git?branch=main#0227f048fcb7c798026ede6cc20c92befc84c3a4"`. /// In this case, the `Cargo.toml` would have contained: /// /// ```toml /// cargo = { git = "https://github.com/rust-lang/cargo.git", branch = "main" } /// ``` /// /// and the `Cargo.lock` would have contained: /// /// ```toml /// [[package]] /// name = "cargo" /// version = "0.46.0" /// source = "git+https://github.com/rust-lang/cargo.git?branch=main#0227f048fcb7c798026ede6cc20c92befc84c3a4 /// dependencies = [ ... ] /// ``` /// /// For more, see [Specifying dependencies from `git` repositories](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#specifying-dependencies-from-git-repositories) /// in the Cargo book. /// /// # Examples /// /// ``` /// use guppy::graph::{ExternalSource, GitReq}; /// /// // A branch source. /// let source = "git+https://github.com/rust-lang/cargo.git?branch=main#0227f048fcb7c798026ede6cc20c92befc84c3a4"; /// let parsed = ExternalSource::new(source).expect("this source is understood by guppy"); /// /// assert_eq!( /// parsed, /// ExternalSource::Git { /// repository: "https://github.com/rust-lang/cargo.git", /// req: GitReq::Branch("main"), /// resolved: "0227f048fcb7c798026ede6cc20c92befc84c3a4", /// } /// ); /// /// // A tag source. /// let source = "git+https://github.com/rust-lang/cargo.git?tag=v0.46.0#0227f048fcb7c798026ede6cc20c92befc84c3a4"; /// let parsed = ExternalSource::new(source).expect("this source is understood by guppy"); /// /// assert_eq!( /// parsed, /// ExternalSource::Git { /// repository: "https://github.com/rust-lang/cargo.git", /// req: GitReq::Tag("v0.46.0"), /// resolved: "0227f048fcb7c798026ede6cc20c92befc84c3a4", /// } /// ); /// /// // A revision source. /// let source = "git+https://github.com/rust-lang/cargo.git?rev=0227f048fcb7c798026ede6cc20c92befc84c3a4#0227f048fcb7c798026ede6cc20c92befc84c3a4"; /// let parsed = ExternalSource::new(source).expect("this source is understood by guppy"); /// /// assert_eq!( /// parsed, /// ExternalSource::Git { /// repository: "https://github.com/rust-lang/cargo.git", /// req: GitReq::Rev("0227f048fcb7c798026ede6cc20c92befc84c3a4"), /// resolved: "0227f048fcb7c798026ede6cc20c92befc84c3a4", /// } /// ); /// /// // A default source. /// let source = "git+https://github.com/gyscos/zstd-rs.git#bc874a57298bdb500cdb5aeac5f23878b6480d0b"; /// let parsed = ExternalSource::new(source).expect("this source is understood by guppy"); /// /// assert_eq!( /// parsed, /// ExternalSource::Git { /// repository: "https://github.com/gyscos/zstd-rs.git", /// req: GitReq::Default, /// resolved: "bc874a57298bdb500cdb5aeac5f23878b6480d0b", /// } /// ); /// ``` Git { /// The repository for this Git source. For the above example, this would be /// `"https://github.com/rust-lang/cargo.git"`. repository: &'g str, /// The revision requested in `Cargo.toml`. This may be a tag, a branch or a specific /// revision (commit hash). /// /// For the above example, `req` would be `GitSource::Branch("main")`. req: GitReq<'g>, /// The resolved revision, as specified in `Cargo.lock`. /// /// For the above example, `resolved_hash` would be `"0227f048fcb7c798026ede6cc20c92befc84c3a4"`. /// /// This is always a commit hash, and if `req` is `GitReq::Rev` then it is expected /// to be the same hash. (However, this is not verified by guppy.) resolved: &'g str, }, } impl<'g> ExternalSource<'g> { /// The string `"registry+"`. /// /// Used for matching with the `Registry` variant. pub const REGISTRY_PLUS: &'static str = "registry+"; /// The string `"sparse+"`. /// /// Also used for matching with the `Sparse` variant. pub const SPARSE_PLUS: &'static str = "sparse+"; /// The string `"git+"`. /// /// Used for matching with the `Git` variant. pub const GIT_PLUS: &'static str = "git+"; /// The string `"?branch="`. /// /// Used for matching with the `Git` variant. pub const BRANCH_EQ: &'static str = "?branch="; /// The string `"?tag="`. /// /// Used for matching with the `Git` variant. pub const TAG_EQ: &'static str = "?tag="; /// The string `"?rev="`. /// /// Used for matching with the `Git` variant. pub const REV_EQ: &'static str = "?rev="; /// The URL for the `crates.io` registry. /// /// This lacks the leading `"registry+`" that's part of the [`PackageSource`]. pub const CRATES_IO_URL: &'static str = "https://github.com/rust-lang/crates.io-index"; /// Attempts to parse the given string as an external source. /// /// Returns `None` if the string could not be recognized as an external source. pub fn new(source: &'g str) -> Option { // We *could* pull in a URL parsing library, but Cargo's sources are so limited that it // seems like a waste to. if let Some(registry) = source.strip_prefix(Self::REGISTRY_PLUS) { // A registry source. Some(ExternalSource::Registry(registry)) } else if let Some(sparse) = source.strip_prefix(Self::SPARSE_PLUS) { // A sparse registry source. Some(ExternalSource::Sparse(sparse)) } else if let Some(rest) = source.strip_prefix(Self::GIT_PLUS) { // A Git source. // Look for a trailing #, which indicates the resolved revision. let (rest, resolved) = rest.rsplit_once('#')?; let (repository, req) = if let Some(idx) = rest.find(Self::BRANCH_EQ) { ( &rest[..idx], GitReq::Branch(&rest[idx + Self::BRANCH_EQ.len()..]), ) } else if let Some(idx) = rest.find(Self::TAG_EQ) { (&rest[..idx], GitReq::Tag(&rest[idx + Self::TAG_EQ.len()..])) } else if let Some(idx) = rest.find(Self::REV_EQ) { (&rest[..idx], GitReq::Rev(&rest[idx + Self::TAG_EQ.len()..])) } else { (rest, GitReq::Default) }; Some(ExternalSource::Git { repository, req, resolved, }) } else { None } } } /// The `Display` implementation for `ExternalSource` returns the string it was constructed from. /// /// # Examples /// /// ``` /// use guppy::graph::{ExternalSource, GitReq}; /// /// let source = ExternalSource::Git { /// repository: "https://github.com/rust-lang/cargo.git", /// req: GitReq::Branch("main"), /// resolved: "0227f048fcb7c798026ede6cc20c92befc84c3a4", /// }; /// /// assert_eq!( /// format!("{}", source), /// "git+https://github.com/rust-lang/cargo.git?branch=main#0227f048fcb7c798026ede6cc20c92befc84c3a4", /// ); /// ``` impl fmt::Display for ExternalSource<'_> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { ExternalSource::Registry(url) => write!(f, "{}{}", Self::REGISTRY_PLUS, url), ExternalSource::Sparse(url) => write!(f, "{}{}", Self::SPARSE_PLUS, url), ExternalSource::Git { repository, req, resolved, } => { write!(f, "{}{}", Self::GIT_PLUS, repository)?; match req { GitReq::Branch(branch) => write!(f, "{}{}", Self::BRANCH_EQ, branch)?, GitReq::Tag(tag) => write!(f, "{}{}", Self::TAG_EQ, tag)?, GitReq::Rev(rev) => write!(f, "{}{}", Self::REV_EQ, rev)?, GitReq::Default => {} }; write!(f, "#{resolved}") } } } } /// A `Cargo.toml` specification for a Git branch, tag, or revision. /// /// For more, including examples, see the documentation for [`ExternalSource::Git`](ExternalSource::Git). #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] #[non_exhaustive] pub enum GitReq<'g> { /// A branch, e.g. `"main"`. /// /// This is specified in `Cargo.toml` as: /// /// ```toml /// [dependencies] /// cargo = { git = "...", branch = "main" } /// ``` Branch(&'g str), /// A tag, e.g. `"guppy-0.5.0"`. /// /// This is specified in `Cargo.toml` as: /// /// ```toml /// [dependencies] /// guppy = { git = "...", tag = "guppy-0.5.0" } /// ``` Tag(&'g str), /// A revision (commit hash), e.g. `"0227f048fcb7c798026ede6cc20c92befc84c3a4"`. /// /// This is specified in `Cargo.toml` as: /// /// ```toml /// [dependencies] /// cargo = { git = "...", rev = "0227f048fcb7c798026ede6cc20c92befc84c3a4" } /// ``` Rev(&'g str), /// Not specified in `Cargo.toml`. Cargo treats this as the main branch by default. /// /// ```toml /// [dependencies] /// cargo = { git = "..." } /// ``` Default, } /// Internal representation of the source of a package. #[derive(Clone, Debug, PartialEq, Eq)] pub(super) enum PackageSourceImpl { Workspace(Box), Path(Box), // Special, common case. CratesIo, External(Box), } /// Locations that a package can be published to. /// /// Returned by [`PackageMetadata::publish`]. #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] #[non_exhaustive] pub enum PackagePublish<'g> { /// Publication of this package is unrestricted. Unrestricted, /// This package can only be published to the listed [package registry]. /// /// If the list is empty, this package cannot be published to any registries. /// /// [package registry]: https://doc.rust-lang.org/cargo/reference/registries.html Registries(&'g [String]), } // TODO: implement PartialOrd/Ord for these as well using lattice rules assert_covariant!(PackagePublish); impl<'g> PackagePublish<'g> { pub(super) fn new(inner: &'g PackagePublishImpl) -> Self { match inner { PackagePublishImpl::Unrestricted => PackagePublish::Unrestricted, PackagePublishImpl::Registries(registries) => PackagePublish::Registries(registries), } } /// The string `"crates-io"`, indicating that a package can be published to /// [crates.io](https://crates.io/). pub const CRATES_IO: &'static str = "crates-io"; /// Returns true if this package can be published to any package registry. /// /// # Examples /// /// ``` /// use guppy::graph::PackagePublish; /// /// assert!(PackagePublish::Unrestricted.is_unrestricted()); /// assert!(!PackagePublish::Registries(&[PackagePublish::CRATES_IO.to_owned()]).is_unrestricted()); /// assert!(!PackagePublish::Registries(&[]).is_unrestricted()); /// ``` pub fn is_unrestricted(&self) -> bool { matches!(self, PackagePublish::Unrestricted) } /// Returns true if a package can be published to the given package registry. /// /// # Examples /// /// ``` /// use guppy::graph::PackagePublish; /// /// // Unrestricted means this package can be published to any registry. /// assert!(PackagePublish::Unrestricted.can_publish_to(PackagePublish::CRATES_IO)); /// assert!(PackagePublish::Unrestricted.can_publish_to("my-registry")); /// /// // Publish to specific registries but not others. /// let crates_io = &[PackagePublish::CRATES_IO.to_owned()]; /// let crates_io_publish = PackagePublish::Registries(crates_io); /// assert!(crates_io_publish.can_publish_to(PackagePublish::CRATES_IO)); /// assert!(!crates_io_publish.can_publish_to("my-registry")); /// /// // Cannot publish to any registries. /// assert!(!PackagePublish::Registries(&[]).can_publish_to(PackagePublish::CRATES_IO)); /// ``` pub fn can_publish_to(&self, registry: impl AsRef) -> bool { let registry = registry.as_ref(); match self { PackagePublish::Unrestricted => true, PackagePublish::Registries(registries) => registries.iter().any(|r| r == registry), } } /// Returns true if a package can be published to crates.io. pub fn can_publish_to_crates_io(&self) -> bool { self.can_publish_to(Self::CRATES_IO) } /// Returns true if a package cannot be published to any registries. /// /// # Examples /// /// ``` /// use guppy::graph::PackagePublish; /// /// assert!(!PackagePublish::Unrestricted.is_never()); /// assert!(!PackagePublish::Registries(&[PackagePublish::CRATES_IO.to_owned()]).is_never()); /// assert!(PackagePublish::Registries(&[]).is_never()); /// ``` pub fn is_never(&self) -> bool { match self { PackagePublish::Unrestricted => false, PackagePublish::Registries(registries) => registries.is_empty(), } } } /// Internal representation of PackagePublish. #[derive(Clone, Debug)] pub(super) enum PackagePublishImpl { Unrestricted, Registries(Box<[String]>), } /// Represents a dependency from one package to another. /// /// This struct contains information about: /// * whether this dependency was renamed in the context of this crate. /// * if this is a normal, dev and/or build dependency. /// * platform-specific information about required, optional and status #[derive(Copy, Clone, Debug)] pub struct PackageLink<'g> { graph: &'g PackageGraph, from: &'g PackageMetadataImpl, to: &'g PackageMetadataImpl, edge_ix: EdgeIndex, inner: &'g PackageLinkImpl, } assert_covariant!(PackageLink); impl<'g> PackageLink<'g> { pub(super) fn new( graph: &'g PackageGraph, source_ix: NodeIndex, target_ix: NodeIndex, edge_ix: EdgeIndex, inner: Option<&'g PackageLinkImpl>, ) -> Self { let from = graph .data .metadata_impl(&graph.dep_graph[source_ix]) .expect("'from' should have associated metadata"); let to = graph .data .metadata_impl(&graph.dep_graph[target_ix]) .expect("'to' should have associated metadata"); Self { graph, from, to, edge_ix, inner: inner.unwrap_or_else(|| &graph.dep_graph[edge_ix]), } } /// Returns the package which depends on the `to` package. pub fn from(&self) -> PackageMetadata<'g> { PackageMetadata::new(self.graph, self.from) } /// Returns the package which is depended on by the `from` package. pub fn to(&self) -> PackageMetadata<'g> { PackageMetadata::new(self.graph, self.to) } /// Returns the endpoints as a pair of packages `(from, to)`. pub fn endpoints(&self) -> (PackageMetadata<'g>, PackageMetadata<'g>) { (self.from(), self.to()) } /// Returns the name for this dependency edge. This can be affected by a crate rename. pub fn dep_name(&self) -> &'g str { &self.inner.dep_name } /// Returns the resolved name for this dependency edge. This may involve renaming the crate and /// replacing - with _. pub fn resolved_name(&self) -> &'g str { &self.inner.resolved_name } /// Returns the semver requirements specified for this dependency. /// /// To get the resolved version, see the `to` field of the `PackageLink` this was part of. /// /// ## Notes /// /// A dependency can be requested multiple times, possibly with different version requirements, /// even if they all end up resolving to the same version. `version_req` will return any of /// those requirements. /// /// See [Specifying Dependencies](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#specifying-dependencies) /// in the Cargo reference for more details. pub fn version_req(&self) -> &'g VersionReq { &self.inner.version_req } /// Returns the registry URL for this dependency, if specified. /// /// Returns `None` for dependencies from crates.io (the default registry) or /// for dependencies without an explicit registry. pub fn registry(&self) -> Option<&'g str> { self.inner.registry.as_deref() } /// Returns the file system path for this dependency, if it is a path /// dependency. /// /// Returns `None` for dependencies from registries or other sources. pub fn path(&self) -> Option<&'g Utf8Path> { self.inner.path.as_deref() } /// Returns details about this dependency from the `[dependencies]` section. pub fn normal(&self) -> DependencyReq<'g> { DependencyReq { inner: &self.inner.normal, } } /// Returns details about this dependency from the `[build-dependencies]` section. pub fn build(&self) -> DependencyReq<'g> { DependencyReq { inner: &self.inner.build, } } /// Returns details about this dependency from the `[dev-dependencies]` section. pub fn dev(&self) -> DependencyReq<'g> { DependencyReq { inner: &self.inner.dev, } } /// Returns details about this dependency from the section specified by the given dependency /// kind. pub fn req_for_kind(&self, kind: DependencyKind) -> DependencyReq<'g> { match kind { DependencyKind::Normal => self.normal(), DependencyKind::Development => self.dev(), DependencyKind::Build => self.build(), } } /// Return true if this edge is dev-only, i.e. code from this edge will not be included in /// normal builds. pub fn dev_only(&self) -> bool { self.inner.dev_only() } // --- // Helper methods // --- /// Returns the edge index. #[allow(dead_code)] pub(super) fn edge_ix(&self) -> EdgeIndex { self.edge_ix } /// Returns (source, target, edge) as a triple of pointers. Useful for testing. #[doc(hidden)] pub fn as_inner_ptrs(&self) -> PackageLinkPtrs { PackageLinkPtrs { from: self.from, to: self.to, inner: self.inner, } } } /// An opaque identifier for a PackageLink's pointers. Used for tests. #[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] #[doc(hidden)] pub struct PackageLinkPtrs { from: *const PackageMetadataImpl, to: *const PackageMetadataImpl, inner: *const PackageLinkImpl, } #[derive(Clone, Debug)] pub(crate) struct PackageLinkImpl { pub(super) dep_name: String, pub(super) resolved_name: String, pub(super) version_req: VersionReq, pub(super) registry: Option, pub(super) path: Option, pub(super) normal: DependencyReqImpl, pub(super) build: DependencyReqImpl, pub(super) dev: DependencyReqImpl, } impl PackageLinkImpl { #[inline] fn dev_only(&self) -> bool { self.normal.enabled().is_never() && self.build.enabled().is_never() } } /// Information about a specific kind of dependency (normal, build or dev) from a package to another /// package. /// /// Usually found within the context of a [`PackageLink`](struct.PackageLink.html). #[derive(Clone, Debug)] pub struct DependencyReq<'g> { pub(super) inner: &'g DependencyReqImpl, } impl<'g> DependencyReq<'g> { /// Returns true if there is at least one `Cargo.toml` entry corresponding to this requirement. /// /// For example, if this dependency is specified in the `[dev-dependencies]` section, /// `edge.dev().is_present()` will return true. pub fn is_present(&self) -> bool { !self.inner.enabled().is_never() } /// Returns the enabled status of this dependency. /// /// `status` is the union of `default_features` and `no_default_features`. /// /// See the documentation for `EnabledStatus` for more. pub fn status(&self) -> EnabledStatus<'g> { self.inner.enabled() } /// Returns the enabled status of this dependency when `default-features = true`. /// /// See the documentation for `EnabledStatus` for more. pub fn default_features(&self) -> EnabledStatus<'g> { self.inner.default_features() } /// Returns the enabled status of this dependency when `default-features = false`. /// /// This is generally less useful than `status` or `default_features`, but is provided for /// completeness. /// /// See the documentation for `EnabledStatus` for more. pub fn no_default_features(&self) -> EnabledStatus<'g> { self.inner.no_default_features() } /// 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) -> impl Iterator + use<'g> { self.inner.all_features() } /// Returns the enabled status of this feature. /// /// 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_status(&self, feature: &str) -> EnabledStatus<'g> { self.inner.feature_status(feature) } } /// Whether a dependency or feature is required, optional, or disabled. /// /// Returned by the methods on `DependencyMetadata`. /// /// ## Examples /// /// ```toml /// [dependencies] /// once_cell = "1" /// ``` /// /// The dependency and default features are *required* 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 } /// ``` /// /// The result is platform-dependent. On Windows, the dependency and default features are both /// *optional*. On non-Windows platforms, the dependency and default features are *disabled*. /// /// ```toml /// [dependencies] /// once_cell = { version = "1", optional = true } /// /// [target.'cfg(windows)'.dependencies] /// once_cell = { version = "1", optional = false, default-features = false } /// ``` /// /// The result is platform-dependent. On Windows, the dependency is *mandatory* 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(Copy, Clone, Debug)] pub struct EnabledStatus<'g> { required: PlatformStatus<'g>, optional: PlatformStatus<'g>, } assert_covariant!(EnabledStatus); impl<'g> EnabledStatus<'g> { pub(super) fn new(required: &'g PlatformStatusImpl, optional: &'g PlatformStatusImpl) -> Self { Self { required: PlatformStatus::new(required), optional: PlatformStatus::new(optional), } } /// Returns true if this dependency is never enabled on any platform. pub fn is_never(&self) -> bool { self.required.is_never() && self.optional.is_never() } /// Evaluates whether this dependency is required on the given platform spec. /// /// Returns `Unknown` if the result was unknown, which may happen if evaluating against an /// individual platform and its target features are unknown. pub fn required_on(&self, platform_spec: &PlatformSpec) -> EnabledTernary { self.required.enabled_on(platform_spec) } /// Evaluates whether this dependency is enabled (required or optional) on the given platform /// spec. /// /// Returns `Unknown` if the result was unknown, which may happen if evaluating against an /// individual platform and its target features are unknown. pub fn enabled_on(&self, platform_spec: &PlatformSpec) -> EnabledTernary { let required = self.required.enabled_on(platform_spec); let optional = self.optional.enabled_on(platform_spec); required | optional } /// Returns the `PlatformStatus` corresponding to whether this dependency is required. pub fn required_status(&self) -> PlatformStatus<'g> { self.required } /// Returns the `PlatformStatus` corresponding to whether this dependency is optional. pub fn optional_status(&self) -> PlatformStatus<'g> { self.optional } } #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub(super) enum NamedFeatureDep { NamedFeature(Box), OptionalDependency(Box), DependencyNamedFeature { dep_name: Box, feature: Box, weak: bool, }, } impl NamedFeatureDep { #[inline] pub(super) fn named_feature(feature_name: impl Into) -> Self { Self::NamedFeature(feature_name.into().into_boxed_str()) } #[inline] pub(super) fn optional_dependency(dep_name: impl Into) -> Self { Self::OptionalDependency(dep_name.into().into_boxed_str()) } #[inline] pub(super) fn dep_named_feature( dep_name: impl Into, feature: impl Into, weak: bool, ) -> Self { Self::DependencyNamedFeature { dep_name: dep_name.into().into_boxed_str(), feature: feature.into().into_boxed_str(), weak, } } } impl fmt::Display for NamedFeatureDep { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::NamedFeature(feature) => write!(f, "{feature}"), Self::OptionalDependency(dep_name) => write!(f, "dep:{dep_name}"), Self::DependencyNamedFeature { dep_name, feature, weak, } => { write!( f, "{}{}/{}", dep_name, if *weak { "?" } else { "" }, feature ) } } } } /// Information about dependency requirements. #[derive(Clone, Debug, Default)] pub(super) struct DependencyReqImpl { pub(super) required: DepRequiredOrOptional, pub(super) optional: DepRequiredOrOptional, } impl DependencyReqImpl { fn all_features(&self) -> impl Iterator { self.required .all_features() .chain(self.optional.all_features()) } pub(super) fn enabled(&self) -> EnabledStatus<'_> { self.make_status(|req_impl| &req_impl.build_if) } pub(super) fn default_features(&self) -> EnabledStatus<'_> { self.make_status(|req_impl| &req_impl.default_features_if) } pub(super) fn no_default_features(&self) -> EnabledStatus<'_> { self.make_status(|req_impl| &req_impl.no_default_features_if) } pub(super) fn feature_status(&self, feature: &str) -> EnabledStatus<'_> { // This PlatformStatusImpl in static memory is so that the lifetimes work out. static DEFAULT_STATUS: PlatformStatusImpl = PlatformStatusImpl::Specs(Vec::new()); self.make_status(|req_impl| { req_impl .feature_targets .get(feature) .unwrap_or(&DEFAULT_STATUS) }) } fn make_status( &self, pred_fn: impl Fn(&DepRequiredOrOptional) -> &PlatformStatusImpl, ) -> EnabledStatus<'_> { EnabledStatus::new(pred_fn(&self.required), pred_fn(&self.optional)) } } /// Information about dependency requirements, scoped to either the dependency being required or /// optional. #[derive(Clone, Debug, Default)] pub(super) struct DepRequiredOrOptional { pub(super) build_if: PlatformStatusImpl, pub(super) default_features_if: PlatformStatusImpl, pub(super) no_default_features_if: PlatformStatusImpl, pub(super) feature_targets: BTreeMap, } impl DepRequiredOrOptional { pub(super) fn all_features(&self) -> impl Iterator { self.feature_targets.keys().map(|s| s.as_str()) } } guppy-0.17.25/src/graph/mod.rs000064400000000000000000000103541046102023000141640ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 //! Entry point for analyzing Cargo dependency graphs. //! //! The main entry point for analyzing graphs is [`PackageGraph`](struct.PackageGraph.html). See its //! documentation for more details. use crate::PackageId; use petgraph::prelude::*; use std::fmt; mod build; mod build_targets; pub mod cargo; mod cycles; pub mod feature; mod graph_impl; #[cfg(feature = "proptest1")] mod proptest_helpers; mod query; mod query_core; mod resolve; mod resolve_core; #[cfg(feature = "summaries")] pub mod summaries; pub use crate::petgraph_support::dot::DotWrite; pub use build_targets::*; pub use cycles::*; pub use graph_impl::*; use once_cell::sync::Lazy; use petgraph::graph::IndexType; #[cfg(feature = "proptest1")] pub use proptest_helpers::*; pub use query::*; pub use resolve::*; use semver::{Version, VersionReq}; /// The direction in which to follow dependencies. /// /// Used by the `_directed` methods. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[cfg_attr(feature = "proptest1", derive(proptest_derive::Arbitrary))] pub enum DependencyDirection { /// Dependencies from this package to other packages. Forward, /// Reverse dependencies from other packages to this one. Reverse, } impl DependencyDirection { /// Returns the opposite direction to this one. pub fn opposite(self) -> Self { match self { DependencyDirection::Forward => DependencyDirection::Reverse, DependencyDirection::Reverse => DependencyDirection::Forward, } } } impl From for DependencyDirection { fn from(direction: Direction) -> Self { match direction { Direction::Outgoing => DependencyDirection::Forward, Direction::Incoming => DependencyDirection::Reverse, } } } impl From for Direction { fn from(direction: DependencyDirection) -> Self { match direction { DependencyDirection::Forward => Direction::Outgoing, DependencyDirection::Reverse => Direction::Incoming, } } } /// Index for PackageGraph. Used for newtype wrapping. #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] struct PackageIx(u32); /// Index for FeatureGraph. Used for newtype wrapping. #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] struct FeatureIx(u32); macro_rules! graph_ix { ($ix_type: ident) => { impl fmt::Display for $ix_type { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.0) } } // From the docs for `IndexType`: // // > Marked `unsafe` because: the trait must faithfully preseve and convert index values. unsafe impl IndexType for $ix_type { #[inline(always)] fn new(x: usize) -> Self { $ix_type(x as u32) } #[inline(always)] fn index(&self) -> usize { self.0 as usize } #[inline(always)] fn max() -> Self { $ix_type(u32::MAX) } } }; } graph_ix!(PackageIx); graph_ix!(FeatureIx); /// Used to group together associated types with a particular graph. trait GraphSpec { type Node; type Edge; type Ix: IndexType; } impl GraphSpec for PackageGraph { type Node = PackageId; type Edge = PackageLinkImpl; type Ix = PackageIx; } /// Marker type to hang `impl GraphSpec` for `FeatureGraph` off of. /// /// Do this instead of `impl<'g> GraphSpec for feature::FeatureGraph<'g>` to deal with lifetime /// variance issues. #[derive(Clone, Debug)] pub(crate) enum FeatureGraphSpec {} impl GraphSpec for FeatureGraphSpec { type Node = feature::FeatureNode; type Edge = feature::FeatureEdge; type Ix = FeatureIx; } // A requirement of "*" filters out pre-release versions with the semver crate, // but cargo accepts them. // See https://github.com/steveklabnik/semver/issues/98. fn cargo_version_matches(req: &VersionReq, version: &Version) -> bool { static MAJOR_WILDCARD: Lazy = Lazy::new(|| VersionReq::parse("*").unwrap()); req == &*MAJOR_WILDCARD || req.matches(version) } guppy-0.17.25/src/graph/proptest_helpers.rs000064400000000000000000000157651046102023000170220ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::{ PackageId, graph::{ PackageGraph, PackageLink, PackageQuery, PackageResolver, Workspace, cargo::{CargoOptions, CargoResolverVersion, InitialsPlatform}, }, platform::PlatformSpec, }; use fixedbitset::FixedBitSet; use petgraph::{prelude::*, visit::VisitMap}; use proptest::{ collection::{hash_set, vec}, prelude::*, }; /// ## Helpers for property testing /// /// The methods in this section allow a `PackageGraph` to be used in property-based testing /// scenarios. /// /// Currently, [proptest 1](https://docs.rs/proptest/1) is supported if the `proptest1` /// feature is enabled. impl PackageGraph { /// Returns a `Strategy` that generates random package IDs from this graph. /// /// Requires the `proptest1` feature to be enabled. /// /// ## Panics /// /// Panics if there are no packages in this `PackageGraph`. pub fn proptest1_id_strategy(&self) -> impl Strategy { let dep_graph = &self.dep_graph; any::().prop_map(move |index| { let package_ix = NodeIndex::new(index.index(dep_graph.node_count())); &self.dep_graph[package_ix] }) } /// Returns a `Strategy` that generates random dependency links from this graph. /// /// Requires the `proptest1` feature to be enabled. /// /// ## Panics /// /// Panics if there are no dependency edges in this `PackageGraph`. pub fn proptest1_link_strategy(&self) -> impl Strategy> { any::().prop_map(move |index| { // Note that this works because PackageGraph uses petgraph::Graph, not StableGraph. If // PackageGraph used StableGraph, a retain_edges call would create holes -- invalid // indexes in the middle of the range. Graph compacts edge indexes so that all // indexes from 0 to link_count are valid. let edge_ix = EdgeIndex::new(index.index(self.link_count())); self.edge_ix_to_link(edge_ix) }) } /// Returns a `Strategy` that generates a random `PackageResolver` instance from this graph. /// /// Requires the `proptest1` feature to be enabled. pub fn proptest1_resolver_strategy(&self) -> impl Strategy + use<> { // Generate a FixedBitSet to filter based off of. fixedbitset_strategy(self.dep_graph.edge_count()).prop_map(Prop010Resolver::new) } /// Returns a `Strategy` that generates a random `CargoOptions` from this graph. /// /// Requires the `proptest1` feature to be enabled. pub fn proptest1_cargo_options_strategy(&self) -> impl Strategy> { let omitted_packages = hash_set(self.proptest1_id_strategy(), 0..4); ( any::(), any::(), any::(), any::(), any::(), omitted_packages, ) .prop_map( |( version, include_dev, initials_platform, host_platform, target_platform, omitted_packages, )| { let mut options = CargoOptions::new(); options .set_resolver(version) .set_include_dev(include_dev) .set_initials_platform(initials_platform) .set_host_platform(host_platform) .set_target_platform(target_platform) .add_omitted_packages(omitted_packages); options }, ) } } /// ## Helpers for property testing /// /// The methods in this section allow a `Workspace` to be used in property-based testing /// scenarios. /// /// Currently, [proptest 1](https://docs.rs/proptest/1) is supported if the `proptest1` /// feature is enabled. impl<'g> Workspace<'g> { /// Returns a `Strategy` that generates random package names from this workspace. /// /// Requires the `proptest1` feature to be enabled. /// /// ## Panics /// /// Panics if there are no packages in this `Workspace`. pub fn proptest1_name_strategy(&self) -> impl Strategy + 'g + use<'g> { let name_list = self.name_list(); (0..name_list.len()).prop_map(move |idx| name_list[idx].as_ref()) } /// Returns a `Strategy` that generates random package IDs from this workspace. /// /// Requires the `proptest1` feature to be enabled. /// /// ## Panics /// /// Panics if there are no packages in this `Workspace`. pub fn proptest1_id_strategy(&self) -> impl Strategy + 'g + use<'g> { let members_by_name = &self.inner.members_by_name; self.proptest1_name_strategy() .prop_map(move |name| &members_by_name[name]) } fn name_list(&self) -> &'g [Box] { self.inner .name_list .get_or_init(|| self.inner.members_by_name.keys().cloned().collect()) } } /// A randomly generated package resolver. /// /// Created by `PackageGraph::proptest1_resolver_strategy`. Requires the `proptest1` feature to be /// enabled. #[derive(Clone, Debug)] pub struct Prop010Resolver { included_edges: FixedBitSet, check_depends_on: bool, } impl Prop010Resolver { fn new(included_edges: FixedBitSet) -> Self { Self { included_edges, check_depends_on: false, } } /// If called with true, this resolver will then verify that any links passed in are in the /// correct direction. pub fn check_depends_on(&mut self, check: bool) { self.check_depends_on = check; } /// Returns true if the given link is accepted by this resolver. pub fn accept_link(&self, link: PackageLink<'_>) -> bool { self.included_edges.is_visited(&link.edge_ix().index()) } } impl<'g> PackageResolver<'g> for Prop010Resolver { fn accept(&mut self, query: &PackageQuery<'g>, link: PackageLink<'g>) -> bool { if self.check_depends_on { assert!( query .graph() .depends_on(link.from().id(), link.to().id()) .expect("valid package IDs"), "package '{}' should depend on '{}'", link.from().id(), link.to().id() ); } self.accept_link(link) } } pub(super) fn fixedbitset_strategy(len: usize) -> impl Strategy { vec(any::(), len).prop_map(|bits| { // FixedBitSet implements FromIterator for indexes, so just collect into it. bits.into_iter() .enumerate() .filter_map(|(idx, bit)| if bit { Some(idx) } else { None }) .collect() }) } guppy-0.17.25/src/graph/query.rs000064400000000000000000000165551046102023000145630ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::{ Error, PackageId, graph::{ DependencyDirection, PackageGraph, PackageIx, PackageLink, PackageMetadata, PackageResolver, PackageSet, ResolverFn, feature::{FeatureFilter, FeatureQuery}, query_core::QueryParams, }, sorted_set::SortedSet, }; use camino::Utf8Path; use petgraph::prelude::*; /// A query over a package graph. /// /// This is the entry point for iterators over IDs and dependency links, and dot graph presentation. /// A `PackageQuery` is constructed through the `query_` methods on `PackageGraph`. #[derive(Clone, Debug)] pub struct PackageQuery<'g> { // The fields are pub(super) for access within the graph module. pub(super) graph: &'g PackageGraph, pub(super) params: QueryParams, } assert_covariant!(PackageQuery); /// ## Queries /// /// The methods in this section create *queries* over subsets of this package graph. Use the methods /// here to analyze transitive dependencies. impl PackageGraph { /// Creates a new forward query over the entire workspace. /// /// `query_workspace` will select all workspace packages and their transitive dependencies. To /// create a `PackageSet` with just workspace packages, use `resolve_workspace`. pub fn query_workspace(&self) -> PackageQuery<'_> { self.query_forward(self.workspace().member_ids()) .expect("workspace packages should all be known") } /// Creates a new forward query over the specified workspace packages by path. /// /// Returns an error if any workspace paths were unknown. pub fn query_workspace_paths( &self, paths: impl IntoIterator>, ) -> Result, Error> { let workspace = self.workspace(); let package_ixs = paths .into_iter() .map(|path| { workspace .member_by_path(path.as_ref()) .map(|package| package.package_ix()) }) .collect::, Error>>()?; Ok(self.query_from_parts(package_ixs, DependencyDirection::Forward)) } /// Creates a new forward query over the specified workspace packages by name. /// /// This is similar to `cargo`'s `--package` option. /// /// Returns an error if any package names were unknown. pub fn query_workspace_names( &self, names: impl IntoIterator>, ) -> Result, Error> { let workspace = self.workspace(); let package_ixs = names .into_iter() .map(|name| { workspace .member_by_name(name.as_ref()) .map(|package| package.package_ix()) }) .collect::, Error>>()?; Ok(self.query_from_parts(package_ixs, DependencyDirection::Forward)) } /// Creates a new query that returns transitive dependencies of the given packages in the /// specified direction. /// /// Returns an error if any package IDs are unknown. pub fn query_directed<'g, 'a>( &'g self, package_ids: impl IntoIterator, dep_direction: DependencyDirection, ) -> Result, Error> { match dep_direction { DependencyDirection::Forward => self.query_forward(package_ids), DependencyDirection::Reverse => self.query_reverse(package_ids), } } /// Creates a new query that returns transitive dependencies of the given packages. /// /// Returns an error if any package IDs are unknown. pub fn query_forward<'g, 'a>( &'g self, package_ids: impl IntoIterator, ) -> Result, Error> { Ok(PackageQuery { graph: self, params: QueryParams::Forward(self.package_ixs(package_ids)?), }) } /// Creates a new query that returns transitive reverse dependencies of the given packages. /// /// Returns an error if any package IDs are unknown. pub fn query_reverse<'g, 'a>( &'g self, package_ids: impl IntoIterator, ) -> Result, Error> { Ok(PackageQuery { graph: self, params: QueryParams::Reverse(self.package_ixs(package_ids)?), }) } pub(super) fn query_from_parts( &self, package_ixs: SortedSet>, direction: DependencyDirection, ) -> PackageQuery<'_> { let params = match direction { DependencyDirection::Forward => QueryParams::Forward(package_ixs), DependencyDirection::Reverse => QueryParams::Reverse(package_ixs), }; PackageQuery { graph: self, params, } } } impl<'g> PackageQuery<'g> { /// Returns the package graph on which the query is going to be executed. pub fn graph(&self) -> &'g PackageGraph { self.graph } /// Returns the direction the query is happening in. pub fn direction(&self) -> DependencyDirection { self.params.direction() } /// Returns the list of initial packages specified in the query. /// /// The order of packages is unspecified. pub fn initials<'a>(&'a self) -> impl ExactSizeIterator> + 'a { let graph = self.graph; self.params.initials().iter().map(move |package_ix| { graph .metadata(&graph.dep_graph[*package_ix]) .expect("valid ID") }) } /// Returns true if the query starts from the given package ID. /// /// Returns an error if this package ID is unknown. pub fn starts_from(&self, package_id: &PackageId) -> Result { Ok(self.params.has_initial(self.graph.package_ix(package_id)?)) } /// Converts this `PackageQuery` into a `FeatureQuery`, using the given feature filter. /// /// This will cause the feature graph to be constructed if it hasn't been done so already. pub fn to_feature_query(&self, filter: impl FeatureFilter<'g>) -> FeatureQuery<'g> { let package_ixs = self.params.initials(); let feature_graph = self.graph.feature_graph(); let feature_ixs = feature_graph.feature_ixs_for_package_ixs_filtered(package_ixs.iter().copied(), filter); feature_graph.query_from_parts(feature_ixs, self.direction()) } /// Resolves this query into a set of known packages, following every link found along the /// way. /// /// This is the entry point for iterators. pub fn resolve(self) -> PackageSet<'g> { PackageSet::new(self) } /// Resolves this query into a set of known packages, using the provided resolver to /// determine which links are followed. pub fn resolve_with(self, resolver: impl PackageResolver<'g>) -> PackageSet<'g> { PackageSet::with_resolver(self, resolver) } /// Resolves this query into a set of known packages, using the provided resolver function /// to determine which links are followed. pub fn resolve_with_fn( self, resolver_fn: impl FnMut(&PackageQuery<'g>, PackageLink<'g>) -> bool, ) -> PackageSet<'g> { self.resolve_with(ResolverFn(resolver_fn)) } } guppy-0.17.25/src/graph/query_core.rs000064400000000000000000000076001046102023000155620ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::{ graph::{DependencyDirection, GraphSpec}, petgraph_support::dfs::{BufferedEdgeFilter, dfs_next_buffered_filter}, sorted_set::SortedSet, }; use fixedbitset::FixedBitSet; use petgraph::{ graph::IndexType, prelude::*, visit::{IntoEdges, IntoNeighbors, Visitable}, }; use std::fmt; pub(super) enum QueryParams { Forward(SortedSet>), Reverse(SortedSet>), } impl QueryParams { pub(super) fn direction(&self) -> DependencyDirection { match self { QueryParams::Forward(_) => DependencyDirection::Forward, QueryParams::Reverse(_) => DependencyDirection::Reverse, } } /// Returns true if this query specifies this package as an initial. pub(super) fn has_initial(&self, initial: NodeIndex) -> bool { match self { QueryParams::Forward(v) => v.contains(&initial), QueryParams::Reverse(v) => v.contains(&initial), } } pub(super) fn initials(&self) -> &[NodeIndex] { match self { QueryParams::Forward(v) => v, QueryParams::Reverse(v) => v, } } } impl Clone for QueryParams where G::Ix: Clone, { fn clone(&self) -> Self { match self { QueryParams::Forward(v) => QueryParams::Forward(v.clone()), QueryParams::Reverse(v) => QueryParams::Reverse(v.clone()), } } } impl fmt::Debug for QueryParams where G::Ix: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { QueryParams::Forward(v) => f.debug_tuple("Forward").field(v).finish(), QueryParams::Reverse(v) => f.debug_tuple("Reverse").field(v).finish(), } } } pub(super) fn all_visit_map(graph: G) -> (FixedBitSet, usize) where G: Visitable, Map = FixedBitSet>, Ix: IndexType, { let mut visit_map = graph.visit_map(); // Mark all nodes visited. visit_map.insert_range(..); let len = visit_map.len(); (visit_map, len) } pub(super) fn reachable_map( graph: G, roots: impl Into>, ) -> (FixedBitSet, usize) where G: Visitable, Map = FixedBitSet> + IntoNeighbors, Ix: IndexType, { // To figure out what nodes are reachable, run a DFS starting from the roots. // This is DfsPostOrder since that handles cycles while a regular DFS doesn't. let mut dfs = DfsPostOrder::empty(graph); dfs.stack = roots.into(); while dfs.next(graph).is_some() {} // Once the DFS is done, the discovered map (or the finished map) is what's reachable. debug_assert_eq!( dfs.discovered, dfs.finished, "discovered and finished maps match at the end" ); let reachable = dfs.discovered; let len = reachable.count_ones(..); (reachable, len) } pub(super) fn reachable_map_buffered_filter( graph: G, mut filter: impl BufferedEdgeFilter, roots: impl Into>, ) -> (FixedBitSet, usize) where G: Visitable, Map = FixedBitSet> + IntoEdges, Ix: IndexType, { // To figure out what nodes are reachable, run a DFS starting from the roots. // This is DfsPostOrder since that handles cycles while a regular DFS doesn't. let mut dfs = DfsPostOrder::empty(graph); dfs.stack = roots.into(); while dfs_next_buffered_filter(&mut dfs, graph, &mut filter).is_some() {} // Once the DFS is done, the discovered map (or the finished map) is what's reachable. debug_assert_eq!( dfs.discovered, dfs.finished, "discovered and finished maps match at the end" ); let reachable = dfs.discovered; let len = reachable.count_ones(..); (reachable, len) } guppy-0.17.25/src/graph/resolve.rs000064400000000000000000000542241046102023000150700ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::{ Error, PackageId, debug_ignore::DebugIgnore, graph::{ DependencyDirection, PackageGraph, PackageIx, PackageLink, PackageLinkImpl, PackageMetadata, PackageQuery, feature::{FeatureFilter, FeatureSet}, resolve_core::{ResolveCore, Topo}, }, petgraph_support::{ IxBitSet, dot::{DotFmt, DotVisitor, DotWrite}, edge_ref::GraphEdgeRef, }, sorted_set::SortedSet, }; use camino::Utf8Path; use fixedbitset::FixedBitSet; use petgraph::{ prelude::*, visit::{NodeFiltered, NodeRef}, }; use std::fmt; impl PackageGraph { /// Creates a new `PackageSet` consisting of all members of this package graph. /// /// This is normally the same as `query_workspace().resolve()`, but can differ if packages have /// been replaced with `[patch]` or `[replace]`. /// /// In most situations, `query_workspace` is preferred. Use `resolve_all` if you know you need /// parts of the graph that aren't accessible from the workspace. pub fn resolve_all(&self) -> PackageSet<'_> { PackageSet { graph: DebugIgnore(self), core: ResolveCore::all_nodes(&self.dep_graph), } } /// Creates a new, empty `PackageSet` associated with this package graph. pub fn resolve_none(&self) -> PackageSet<'_> { PackageSet { graph: DebugIgnore(self), core: ResolveCore::empty(), } } /// Creates a new `PackageSet` consisting of the specified package IDs. /// /// This does not include transitive dependencies. To do so, use the `query_` methods. /// /// Returns an error if any package IDs are unknown. pub fn resolve_ids<'a>( &self, package_ids: impl IntoIterator, ) -> Result, Error> { Ok(PackageSet { graph: DebugIgnore(self), core: ResolveCore::from_included::(self.package_ixs(package_ids)?), }) } /// Creates a new `PackageSet` consisting of all packages in this workspace. /// /// This does not include transitive dependencies. To do so, use `query_workspace`. pub fn resolve_workspace(&self) -> PackageSet<'_> { let included: IxBitSet = self .workspace() .iter_by_path() .map(|(_, package)| package.package_ix()) .collect(); PackageSet { graph: DebugIgnore(self), core: ResolveCore::from_included(included), } } /// Creates a new `PackageSet` consisting of the specified workspace packages by path. /// /// This does not include transitive dependencies. To do so, use `query_workspace_paths`. /// /// Returns an error if any workspace paths were unknown. pub fn resolve_workspace_paths( &self, paths: impl IntoIterator>, ) -> Result, Error> { let workspace = self.workspace(); let included: IxBitSet = paths .into_iter() .map(|path| { workspace .member_by_path(path.as_ref()) .map(|package| package.package_ix()) }) .collect::>()?; Ok(PackageSet { graph: DebugIgnore(self), core: ResolveCore::from_included(included), }) } /// Creates a new `PackageSet` consisting of the specified workspace packages by name. /// /// This does not include transitive dependencies. To do so, use `query_workspace_names`. /// /// Returns an error if any package names were unknown. pub fn resolve_workspace_names( &self, names: impl IntoIterator>, ) -> Result, Error> { let workspace = self.workspace(); let included: IxBitSet = names .into_iter() .map(|name| { workspace .member_by_name(name.as_ref()) .map(|package| package.package_ix()) }) .collect::>()?; Ok(PackageSet { graph: DebugIgnore(self), core: ResolveCore::from_included(included), }) } /// Creates a new `PackageSet` consisting of packages with the given name. /// /// The result is empty if there are no packages with the given name. pub fn resolve_package_name(&self, name: impl AsRef) -> PackageSet<'_> { // Turns out that for reasonably-sized graphs, a linear search across package names is // extremely fast: much faster than trying to do something fancy like use an FST or trie. // // TODO: optimize this in the future, possibly through some sort of hashmap variant that // doesn't require a borrow. let name = name.as_ref(); let included: IxBitSet = self .packages() .filter_map(|package| { if package.name() == name { Some(package.package_ix()) } else { None } }) .collect(); PackageSet::from_included(self, included) } } /// A set of resolved packages in a package graph. /// /// Created by `PackageQuery::resolve`. #[derive(Clone, Debug)] pub struct PackageSet<'g> { graph: DebugIgnore<&'g PackageGraph>, core: ResolveCore, } assert_covariant!(PackageSet); impl<'g> PackageSet<'g> { pub(super) fn new(query: PackageQuery<'g>) -> Self { let graph = query.graph; Self { graph: DebugIgnore(graph), core: ResolveCore::new(graph.dep_graph(), query.params), } } pub(super) fn from_included(graph: &'g PackageGraph, included: impl Into) -> Self { Self { graph: DebugIgnore(graph), core: ResolveCore::from_included(included), } } pub(super) fn with_resolver( query: PackageQuery<'g>, mut resolver: impl PackageResolver<'g>, ) -> Self { let graph = query.graph; let params = query.params.clone(); Self { graph: DebugIgnore(graph), core: ResolveCore::with_edge_filter(graph.dep_graph(), params, |edge| { let link = graph.edge_ref_to_link(edge); resolver.accept(&query, link) }), } } /// Returns the number of packages in this set. pub fn len(&self) -> usize { self.core.len() } /// Returns true if no packages were resolved in this set. pub fn is_empty(&self) -> bool { self.core.is_empty() } /// Returns true if this package ID is contained in this resolve set. /// /// Returns an error if the package ID is unknown. pub fn contains(&self, package_id: &PackageId) -> Result { Ok(self.contains_ix(self.graph.package_ix(package_id)?)) } /// Creates a new `PackageQuery` from this set in the specified direction. /// /// This is equivalent to constructing a query from all the `package_ids`. pub fn to_package_query(&self, direction: DependencyDirection) -> PackageQuery<'g> { let package_ixs = SortedSet::new( self.core .included .ones() .map(NodeIndex::new) .collect::>(), ); self.graph.query_from_parts(package_ixs, direction) } // --- // Set operations // --- /// Returns a `PackageSet` that contains all packages present in at least one of `self` /// and `other`. /// /// ## Panics /// /// Panics if the package graphs associated with `self` and `other` don't match. pub fn union(&self, other: &Self) -> Self { assert!( ::std::ptr::eq(self.graph.0, other.graph.0), "package graphs passed into union() match" ); let mut res = self.clone(); res.core.union_with(&other.core); res } /// Returns a `PackageSet` that contains all packages present in both `self` and `other`. /// /// ## Panics /// /// Panics if the package graphs associated with `self` and `other` don't match. pub fn intersection(&self, other: &Self) -> Self { assert!( ::std::ptr::eq(self.graph.0, other.graph.0), "package graphs passed into intersection() match" ); let mut res = self.clone(); res.core.intersect_with(&other.core); res } /// Returns a `PackageSet` that contains all packages present in `self` but not `other`. /// /// ## Panics /// /// Panics if the package graphs associated with `self` and `other` don't match. pub fn difference(&self, other: &Self) -> Self { assert!( ::std::ptr::eq(self.graph.0, other.graph.0), "package graphs passed into difference() match" ); Self { graph: self.graph, core: self.core.difference(&other.core), } } /// Returns a `PackageSet` that contains all packages present in exactly one of `self` and /// `other`. /// /// ## Panics /// /// Panics if the package graphs associated with `self` and `other` don't match. pub fn symmetric_difference(&self, other: &Self) -> Self { assert!( ::std::ptr::eq(self.graph.0, other.graph.0), "package graphs passed into symmetric_difference() match" ); let mut res = self.clone(); res.core.symmetric_difference_with(&other.core); res } /// Returns a `PackageSet` on which a filter has been applied. /// /// Filters out all values for which the callback returns false. /// /// ## Cycles /// /// For packages within a dependency cycle, the callback will be called in non-dev order. When /// the direction is forward, if package Foo has a dependency on Bar, and Bar has a cyclic /// dev-dependency on Foo, then Foo is returned before Bar. pub fn filter( &self, direction: DependencyDirection, mut callback: impl FnMut(PackageMetadata<'g>) -> bool, ) -> Self { let graph = *self.graph; let included: IxBitSet = self .packages(direction) .filter_map(move |package| { let package_ix = package.package_ix(); if callback(package) { Some(package_ix) } else { None } }) .collect(); Self::from_included(graph, included) } /// Partitions this `PackageSet` into two. /// /// The first `PackageSet` contains packages for which the callback returned true, and the /// second one contains packages for which the callback returned false. /// /// ## Cycles /// /// For packages within a dependency cycle, the callback will be called in non-dev order. When /// the direction is forward, if package Foo has a dependency on Bar, and Bar has a cyclic /// dev-dependency on Foo, then Foo is returned before Bar. pub fn partition( &self, direction: DependencyDirection, mut callback: impl FnMut(PackageMetadata<'g>) -> bool, ) -> (Self, Self) { let graph = *self.graph; let mut left = IxBitSet::with_capacity(self.core.included.len()); let mut right = left.clone(); self.packages(direction).for_each(|package| { let package_ix = package.package_ix(); match callback(package) { true => left.insert_node_ix(package_ix), false => right.insert_node_ix(package_ix), } }); ( Self::from_included(graph, left), Self::from_included(graph, right), ) } /// Performs filtering and partitioning at the same time. /// /// The first `PackageSet` contains packages for which the callback returned `Some(true)`, and /// the second one contains packages for which the callback returned `Some(false)`. Packages /// for which the callback returned `None` are dropped. /// /// ## Cycles /// /// For packages within a dependency cycle, the callback will be called in non-dev order. When /// the direction is forward, if package Foo has a dependency on Bar, and Bar has a cyclic /// dev-dependency on Foo, then Foo is returned before Bar. pub fn filter_partition( &self, direction: DependencyDirection, mut callback: impl FnMut(PackageMetadata<'g>) -> Option, ) -> (Self, Self) { let graph = *self.graph; let mut left = IxBitSet::with_capacity(self.core.included.len()); let mut right = left.clone(); self.packages(direction).for_each(|package| { let package_ix = package.package_ix(); match callback(package) { Some(true) => left.insert_node_ix(package_ix), Some(false) => right.insert_node_ix(package_ix), None => {} } }); ( Self::from_included(graph, left), Self::from_included(graph, right), ) } // --- // Conversion to FeatureSet // --- /// Creates a new `FeatureSet` consisting of all packages in this `PackageSet`, using the given /// feature filter. /// /// This will cause the feature graph to be constructed if it hasn't been done so already. pub fn to_feature_set(&self, filter: impl FeatureFilter<'g>) -> FeatureSet<'g> { let feature_graph = self.graph.feature_graph(); let included: IxBitSet = feature_graph.feature_ixs_for_package_ixs_filtered( // The direction of iteration doesn't matter. self.ixs(DependencyDirection::Forward), filter, ); FeatureSet::from_included(feature_graph, included) } // --- // Iterators // --- /// Iterates over package IDs, in topological order in the direction specified. /// /// ## Cycles /// /// The packages within a dependency cycle will be returned in non-dev order. When the direction /// is forward, if package Foo has a dependency on Bar, and Bar has a cyclic dev-dependency on /// Foo, then Foo is returned before Bar. pub fn package_ids<'a>( &'a self, direction: DependencyDirection, ) -> impl ExactSizeIterator + 'a { let graph = self.graph; self.core .topo(self.graph.sccs(), direction) .map(move |package_ix| &graph.dep_graph[package_ix]) } pub(super) fn ixs(&'g self, direction: DependencyDirection) -> Topo<'g, PackageGraph> { self.core.topo(self.graph.sccs(), direction) } /// Iterates over package metadatas, in topological order in the direction specified. /// /// ## Cycles /// /// The packages within a dependency cycle will be returned in non-dev order. When the direction /// is forward, if package Foo has a dependency on Bar, and Bar has a cyclic dev-dependency on /// Foo, then Foo is returned before Bar. pub fn packages<'a>( &'a self, direction: DependencyDirection, ) -> impl ExactSizeIterator> + 'a { let graph = self.graph; self.package_ids(direction) .map(move |package_id| graph.metadata(package_id).expect("known package IDs")) } /// Returns the set of "root package" IDs in the specified direction. /// /// * If direction is Forward, return the set of packages that do not have any dependencies /// within the selected graph. /// * If direction is Reverse, return the set of packages that do not have any dependents within /// the selected graph. /// /// ## Cycles /// /// If a root consists of a dependency cycle, all the packages in it will be returned in /// non-dev order (when the direction is forward). pub fn root_ids<'a>( &'a self, direction: DependencyDirection, ) -> impl ExactSizeIterator + 'a { let dep_graph = &self.graph.dep_graph; self.core .roots(self.graph.dep_graph(), self.graph.sccs(), direction) .into_iter() .map(move |package_ix| &dep_graph[package_ix]) } /// Returns the set of "root package" metadatas in the specified direction. /// /// * If direction is Forward, return the set of packages that do not have any dependencies /// within the selected graph. /// * If direction is Reverse, return the set of packages that do not have any dependents within /// the selected graph. /// /// ## Cycles /// /// If a root consists of a dependency cycle, all the packages in it will be returned in /// non-dev order (when the direction is forward). pub fn root_packages<'a>( &'a self, direction: DependencyDirection, ) -> impl ExactSizeIterator> + 'a { let package_graph = self.graph; self.core .roots(self.graph.dep_graph(), self.graph.sccs(), direction) .into_iter() .map(move |package_ix| { package_graph .metadata(&package_graph.dep_graph[package_ix]) .expect("invalid node index") }) } /// Creates an iterator over `PackageLink` instances. /// /// If the iteration is in forward order, for any given package, at least one link where the /// package is on the `to` end is returned before any links where the package is on the /// `from` end. /// /// If the iteration is in reverse order, for any given package, at least one link where the /// package is on the `from` end is returned before any links where the package is on the `to` /// end. /// /// ## Cycles /// /// The links in a dependency cycle will be returned in non-dev order. When the direction is /// forward, if package Foo has a dependency on Bar, and Bar has a cyclic dev-dependency on Foo, /// then the link Foo -> Bar is returned before the link Bar -> Foo. pub fn links<'a>( &'a self, direction: DependencyDirection, ) -> impl Iterator> + 'a { let graph = self.graph.0; self.core .links(graph.dep_graph(), graph.sccs(), direction) .map(move |(source_ix, target_ix, edge_ix)| { PackageLink::new(graph, source_ix, target_ix, edge_ix, None) }) } /// Constructs a representation of the selected packages in `dot` format. pub fn display_dot<'a, V: PackageDotVisitor + 'g>( &'a self, visitor: V, ) -> impl fmt::Display + 'a { let node_filtered = NodeFiltered(self.graph.dep_graph(), &self.core.included); DotFmt::new(node_filtered, VisitorWrap::new(self.graph.0, visitor)) } // --- // Helper methods // --- /// Returns all the package ixs without topologically sorting them. #[allow(dead_code)] pub(super) fn ixs_unordered(&self) -> impl Iterator> + '_ { self.core.included.ones().map(NodeIndex::new) } pub(super) fn contains_ix(&self, package_ix: NodeIndex) -> bool { self.core.contains(package_ix) } } impl PartialEq for PackageSet<'_> { fn eq(&self, other: &Self) -> bool { ::std::ptr::eq(self.graph.0, other.graph.0) && self.core == other.core } } impl Eq for PackageSet<'_> {} /// Represents whether a particular link within a package graph should be followed during a /// resolve operation. pub trait PackageResolver<'g> { /// Returns true if this link should be followed during a resolve operation. /// /// Returning false does not prevent the `to` package (or `from` package with `query_reverse`) /// from being included if it's reachable through other means. fn accept(&mut self, query: &PackageQuery<'g>, link: PackageLink<'g>) -> bool; } impl<'g, T> PackageResolver<'g> for &mut T where T: PackageResolver<'g>, { fn accept(&mut self, query: &PackageQuery<'g>, link: PackageLink<'g>) -> bool { (**self).accept(query, link) } } impl<'g> PackageResolver<'g> for Box + '_> { fn accept(&mut self, query: &PackageQuery<'g>, link: PackageLink<'g>) -> bool { (**self).accept(query, link) } } impl<'g> PackageResolver<'g> for &mut dyn PackageResolver<'g> { fn accept(&mut self, query: &PackageQuery<'g>, link: PackageLink<'g>) -> bool { (**self).accept(query, link) } } pub(super) struct ResolverFn(pub(super) F); impl<'g, F> PackageResolver<'g> for ResolverFn where F: FnMut(&PackageQuery<'g>, PackageLink<'g>) -> bool, { fn accept(&mut self, query: &PackageQuery<'g>, link: PackageLink<'g>) -> bool { (self.0)(query, link) } } /// A visitor used for formatting `dot` graphs. pub trait PackageDotVisitor { /// Visits this package. The implementation may output a label for this package to the given /// `DotWrite`. fn visit_package(&self, package: PackageMetadata<'_>, f: &mut DotWrite<'_, '_>) -> fmt::Result; /// Visits this dependency link. The implementation may output a label for this link to the /// given `DotWrite`. fn visit_link(&self, link: PackageLink<'_>, f: &mut DotWrite<'_, '_>) -> fmt::Result; } struct VisitorWrap<'g, V> { graph: &'g PackageGraph, inner: V, } impl<'g, V> VisitorWrap<'g, V> { fn new(graph: &'g PackageGraph, inner: V) -> Self { Self { graph, inner } } } impl<'g, V, NR, ER> DotVisitor for VisitorWrap<'g, V> where V: PackageDotVisitor, NR: NodeRef, Weight = PackageId>, ER: GraphEdgeRef<'g, PackageLinkImpl, PackageIx>, { fn visit_node(&self, node: NR, f: &mut DotWrite<'_, '_>) -> fmt::Result { let metadata = self .graph .metadata(node.weight()) .expect("visited node should have associated metadata"); self.inner.visit_package(metadata, f) } fn visit_edge(&self, edge: ER, f: &mut DotWrite<'_, '_>) -> fmt::Result { let link = self.graph.edge_ref_to_link(edge.into_edge_reference()); self.inner.visit_link(link, f) } } guppy-0.17.25/src/graph/resolve_core.rs000064400000000000000000000247461046102023000161060ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::{ debug_ignore::DebugIgnore, graph::{ DependencyDirection, GraphSpec, query_core::{QueryParams, all_visit_map, reachable_map, reachable_map_buffered_filter}, }, petgraph_support::{ dfs::{BufferedEdgeFilter, ReversedBufferedFilter, SimpleEdgeFilterFn}, scc::{NodeIter, Sccs}, walk::EdgeDfs, }, }; use fixedbitset::FixedBitSet; use petgraph::{ graph::EdgeReference, prelude::*, visit::{NodeFiltered, Reversed, VisitMap}, }; use std::marker::PhantomData; /// Core logic for queries that have been resolved into a known set of packages. /// /// The `G` param ensures that package and feature resolutions aren't mixed up accidentally. #[derive(Clone, Debug)] pub(super) struct ResolveCore { pub(super) included: FixedBitSet, pub(super) len: usize, _phantom: PhantomData, } impl ResolveCore { pub(super) fn new( graph: &Graph, params: QueryParams, ) -> Self { let (included, len) = match params { QueryParams::Forward(initials) => reachable_map(graph, initials.into_inner()), QueryParams::Reverse(initials) => reachable_map(Reversed(graph), initials.into_inner()), }; Self { included, len, _phantom: PhantomData, } } pub(super) fn all_nodes(graph: &Graph) -> Self { let (included, len) = all_visit_map(graph); Self { included, len, _phantom: PhantomData, } } pub(super) fn empty() -> Self { Self { included: FixedBitSet::with_capacity(0), len: 0, _phantom: PhantomData, } } /// The arguments to the edge filter are the (source, target, edge ix), unreversed. pub(super) fn with_edge_filter<'g>( graph: &'g Graph, params: QueryParams, edge_filter: impl FnMut(EdgeReference<'g, G::Edge, G::Ix>) -> bool, ) -> Self { let (included, len) = match params { QueryParams::Forward(initials) => reachable_map_buffered_filter( graph, SimpleEdgeFilterFn(edge_filter), initials.into_inner(), ), QueryParams::Reverse(initials) => reachable_map_buffered_filter( Reversed(graph), ReversedBufferedFilter(SimpleEdgeFilterFn(edge_filter)), initials.into_inner(), ), }; Self { included, len, _phantom: PhantomData, } } /// The arguments to the edge filter are the (source, target, edge ix), unreversed. pub(super) fn with_buffered_edge_filter<'g>( graph: &'g Graph, params: QueryParams, filter: impl BufferedEdgeFilter<&'g Graph>, ) -> Self { let (included, len) = match params { QueryParams::Forward(initials) => { reachable_map_buffered_filter(graph, filter, initials.into_inner()) } QueryParams::Reverse(initials) => reachable_map_buffered_filter( Reversed(graph), ReversedBufferedFilter(filter), initials.into_inner(), ), }; Self { included, len, _phantom: PhantomData, } } pub(super) fn from_included>(included: T) -> Self { let included = included.into(); let len = included.count_ones(..); Self { included, len, _phantom: PhantomData, } } pub(super) fn len(&self) -> usize { self.len } pub(super) fn is_empty(&self) -> bool { self.len == 0 } pub(super) fn contains(&self, ix: NodeIndex) -> bool { self.included.is_visited(&ix) } // --- // Set operations // --- pub(super) fn union_with(&mut self, other: &Self) { self.included.union_with(&other.included); self.invalidate_caches(); } pub(super) fn intersect_with(&mut self, other: &Self) { self.included.intersect_with(&other.included); self.invalidate_caches(); } // fixedbitset 0.2.0 doesn't have a difference_with :( pub(super) fn difference(&self, other: &Self) -> Self { Self::from_included( self.included .difference(&other.included) .collect::(), ) } pub(super) fn symmetric_difference_with(&mut self, other: &Self) { self.included.symmetric_difference_with(&other.included); self.invalidate_caches(); } pub(super) fn invalidate_caches(&mut self) { self.len = self.included.count_ones(..); } /// Returns the root metadatas in the specified direction. pub(super) fn roots( &self, graph: &Graph, sccs: &Sccs, direction: DependencyDirection, ) -> Vec> { // This uses the SCCs in self.sccs. If any node in an SCC is a root, so is any other. match direction { DependencyDirection::Forward => sccs .externals(&NodeFiltered(graph, &self.included)) .collect(), DependencyDirection::Reverse => sccs .externals(&NodeFiltered(Reversed(graph), &self.included)) .collect(), } } pub(super) fn topo<'g>( &'g self, sccs: &'g Sccs, direction: DependencyDirection, ) -> Topo<'g, G> { // --- // IMPORTANT // --- // // This uses the same list of sccs that's computed for the entire graph. This is fine for // resolve() -- over there, if one element of an SCC is present all others will be present // as well. // // * However, with resolve_with() and a custom resolver, it is possible that SCCs in the // main graph aren't in the subgraph. That makes the returned order "incorrect", but it's // a very minor sin and probably not worth the extra complexity to deal with. // * This requires iterating over every node in the graph even if the set of returned nodes // is very small. There's a tradeoff here between allocating memory to store a custom list // of SCCs and just using the one available. More benchmarking is required to figure out // the best approach. // // Note that the SCCs can be computed in reachable_map by adapting parts of kosaraju_scc. let node_iter = sccs.node_iter(direction.into()); Topo { node_iter, included: &self.included, remaining: self.len, } } pub(super) fn links<'g>( &'g self, graph: &'g Graph, sccs: &Sccs, direction: DependencyDirection, ) -> Links<'g, G> { let edge_dfs = match direction { DependencyDirection::Forward => { let filtered_graph = NodeFiltered(graph, &self.included); EdgeDfs::new(&filtered_graph, sccs.externals(&filtered_graph)) } DependencyDirection::Reverse => { let filtered_reversed_graph = NodeFiltered(Reversed(graph), &self.included); EdgeDfs::new( &filtered_reversed_graph, sccs.externals(&filtered_reversed_graph), ) } }; Links { graph: DebugIgnore(graph), included: &self.included, edge_dfs, direction, } } } impl PartialEq for ResolveCore { fn eq(&self, other: &Self) -> bool { if self.len != other.len { return false; } if self.included == other.included { return true; } // At the moment we don't normalize the capacity across self.included instances, so check // the symmetric difference. self.included .symmetric_difference(&other.included) .next() .is_none() } } impl Eq for ResolveCore {} /// An iterator over package nodes in topological order. #[derive(Clone, Debug)] pub(super) struct Topo<'g, G: GraphSpec> { node_iter: NodeIter<'g, G::Ix>, included: &'g FixedBitSet, remaining: usize, } impl Iterator for Topo<'_, G> { type Item = NodeIndex; fn next(&mut self) -> Option { for ix in &mut self.node_iter { if !self.included.is_visited(&ix) { continue; } self.remaining -= 1; return Some(ix); } None } fn size_hint(&self) -> (usize, Option) { (self.remaining, Some(self.remaining)) } } impl ExactSizeIterator for Topo<'_, G> { fn len(&self) -> usize { self.remaining } } /// An iterator over dependency links. #[derive(Clone, Debug)] #[allow(clippy::type_complexity)] pub(super) struct Links<'g, G: GraphSpec> { graph: DebugIgnore<&'g Graph>, included: &'g FixedBitSet, edge_dfs: EdgeDfs, NodeIndex, FixedBitSet>, direction: DependencyDirection, } impl Iterator for Links<'_, G> { #[allow(clippy::type_complexity)] type Item = (NodeIndex, NodeIndex, EdgeIndex); fn next(&mut self) -> Option { match self.direction { DependencyDirection::Forward => { let filtered = NodeFiltered(self.graph.0, self.included); self.edge_dfs.next(&filtered) } DependencyDirection::Reverse => { let filtered_reversed = NodeFiltered(Reversed(self.graph.0), self.included); self.edge_dfs .next(&filtered_reversed) .map(|(source_ix, target_ix, edge_ix)| { // Flip the source and target around since this is a reversed graph, since the // 'from' and 'to' are always right way up. (target_ix, source_ix, edge_ix) }) } } } } guppy-0.17.25/src/graph/summaries/package_set.rs000064400000000000000000001036761046102023000176720ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::{ Error, PackageId, errors::Error::UnknownPackageSetSummary, graph::{ DependencyDirection, ExternalSource, GitReq, PackageGraph, PackageMetadata, PackageSet, PackageSource, }, }; use ahash::AHashMap; use camino::Utf8PathBuf; use guppy_summaries::SummaryId; use semver::VersionReq; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use smallvec::SmallVec; use std::{borrow::Cow, collections::BTreeSet, fmt}; /// A set of packages specified in a summary. Can be resolved into a `PackageSet` given a /// `PackageGraph`. /// /// Requires the `summaries` feature to be enabled. /// /// # Examples /// /// Parsing a summary from a TOML specification, as found in e.g. a config file. /// /// ``` /// # use guppy::graph::summaries::PackageSetSummary; /// # use guppy::MetadataCommand; /// # use guppy::graph::DependencyDirection; /// # use std::collections::HashSet; /// /// // This is an example TOML config for a PackageSet resolved from this workspace. /// static TOML_INPUT: &str = r#" /// workspace-members = ["guppy", "target-spec"] /// ## The version field specifies a range, just like Cargo. /// ## Third-party specifications also include "git" and "path". /// third-party = [ /// { name = "serde", version = "*" }, /// { name = "rayon", version = "1.5" }, /// ] /// "#; /// /// let summary: PackageSetSummary = toml::from_str(TOML_INPUT).expect("input parsed correctly"); /// /// let graph = MetadataCommand::new().build_graph().expect("guppy graph constructed"); /// let package_set = summary /// .to_package_set(&graph, "resolving example TOML") /// .expect("all elements matched"); /// let package_names: HashSet<_> = package_set /// .packages(DependencyDirection::Forward) /// .map(|metadata| metadata.name()) /// .collect(); /// /// let mut expected_names = HashSet::new(); /// expected_names.extend(["guppy", "target-spec", "serde", "rayon"]); /// assert_eq!(package_names, expected_names, "package names matched"); /// ``` /// /// Specifying an invalid package results in an error. /// /// ``` /// # use guppy::graph::summaries::PackageSetSummary; /// # use guppy::MetadataCommand; /// /// // This is an example TOML config that contains package names that are unknown to this /// // workspace. /// static UNKNOWN_TOML_INPUT: &str = r#" /// ## serde is a third-party dependency, so it won't be matched in workspace-members. /// workspace-members = ["unknown-member", "serde"] /// ## guppy is a workspace dependency, so it won't be matched in workspace-members. /// ## serde is present but the version number doesn't match. /// third-party = [ /// { name = "guppy" }, /// { name = "serde", version = "0.9" }, /// { name = "unknown-third-party" }, /// ] /// "#; /// /// let summary: PackageSetSummary = toml::from_str(UNKNOWN_TOML_INPUT).expect("input parsed correctly"); /// /// let graph = MetadataCommand::new().build_graph().expect("guppy graph constructed"); /// let err = summary /// .to_package_set(&graph, "resolving example TOML") /// .expect_err("some elements are unknown"); /// /// assert_eq!( /// format!("{}", err), /// r#"unknown elements: resolving example TOML /// * unknown workspace names: /// - serde /// - unknown-member /// * unknown third-party: /// - { name = "guppy", version = "*", source = crates.io } /// - { name = "serde", version = "^0.9", source = crates.io } /// - { name = "unknown-third-party", version = "*", source = crates.io } /// "# /// ); /// ``` #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct PackageSetSummary { /// A set of summary identifiers. Typically used in generated summaries. /// /// Does not require a `PackageGraph` as context. #[serde(rename = "ids", skip_serializing_if = "BTreeSet::is_empty", default)] pub summary_ids: BTreeSet, /// Workspace packages, specified by names. Typically used in config files. /// /// These require a `PackageGraph` as context. #[serde(skip_serializing_if = "BTreeSet::is_empty", default)] pub workspace_members: BTreeSet, // TODO: also support workspace path globs? // TODO: probably requires https://github.com/BurntSushi/ripgrep/issues/2001 to be fixed // /// Non-workspace packages, including non-workspace path dependencies. Typically used in /// config files. /// /// Requires a `PackageGraph` as context. #[serde(skip_serializing_if = "Vec::is_empty", default)] pub third_party: Vec, } impl PackageSet<'_> { /// Converts this `PackageSet` to a serializable [`PackageSetSummary`]. pub fn to_summary(&self) -> PackageSetSummary { PackageSetSummary::new(self) } } impl PackageSetSummary { /// Constructs a `PackageSetSummary` from a [`PackageSet`]. pub fn new(package_set: &PackageSet<'_>) -> Self { let summary_ids = package_set .packages(DependencyDirection::Forward) .map(|metadata| metadata.to_summary_id()) .collect(); PackageSetSummary { summary_ids, ..PackageSetSummary::default() } } /// Constructs a `PackageSetSummary` from an iterator of [`PackageId`]s. pub fn from_package_ids<'a>( graph: &PackageGraph, package_ids: impl IntoIterator, ) -> Result { let summary_ids = package_ids .into_iter() .map(|package_id| Ok(graph.metadata(package_id)?.to_summary_id())) .collect::>()?; Ok(PackageSetSummary { summary_ids, ..PackageSetSummary::default() }) } /// Returns true if this `PackageSetSummary` is empty. pub fn is_empty(&self) -> bool { self.summary_ids.is_empty() && self.workspace_members.is_empty() && self.third_party.is_empty() } /// Converts this `PackageSetSummary` to a [`PackageSet`]. /// /// Returns an error if any of the elements weren't matched. pub fn to_package_set<'g>( &self, graph: &'g PackageGraph, error_message: impl Into, ) -> Result, Error> { let error_message = error_message.into(); let (package_set, matcher) = self.to_package_set_impl(graph, |_| None, &error_message)?; matcher.finish(graph, error_message)?; Ok(package_set) } /// Converts this `PackageSetSummary` to a [`PackageSet`], with the given source for registry /// names. /// /// Returns an error if any of the elements weren't matched. /// /// This is a temporary workaround until [Cargo issue #9052](https://github.com/rust-lang/cargo/issues/9052) /// is resolved. pub fn to_package_set_registry<'g, 'a>( &'a self, graph: &'g PackageGraph, registry_name_to_url: impl FnMut(&str) -> Option<&'a str>, error_message: impl Into, ) -> Result, Error> { let error_message = error_message.into(); let (package_set, matcher) = self.to_package_set_impl(graph, registry_name_to_url, &error_message)?; matcher.finish(graph, error_message)?; Ok(package_set) } // --- // Helper methods // --- fn to_package_set_impl<'g, 'a>( &'a self, graph: &'g PackageGraph, registry_name_to_url: impl FnMut(&str) -> Option<&'a str>, error_message: &str, ) -> Result<(PackageSet<'g>, PackageMatcher<'a>), Error> { let mut package_matcher = PackageMatcher::new(self, registry_name_to_url, error_message)?; let package_set = graph .resolve_all() .filter(DependencyDirection::Forward, |metadata| { package_matcher.store_is_match(metadata) }); Ok((package_set, package_matcher)) } } /// A selector for external, third-party packages. /// /// A `ThirdPartySummary` is used to specify one or more packages based on the information /// specified. Package names are required, but all other fields are optional. /// /// Requires the `summaries` feature to be enabled. #[derive(Clone, Debug, Eq, PartialEq)] pub struct ThirdPartySummary { /// The name of the package. Must be specified. pub name: String, /// A version specifier for the package. Can be skipped: defaults to [`VersionReq::STAR`]. pub version: VersionReq, /// Where this package can be found. Can be skipped, in which case the source defaults to /// `crates.io`. pub source: ThirdPartySource, } impl fmt::Display for ThirdPartySummary { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{{ name = \"{}\", version = \"{}\", source = {} }}", self.name, self.version, self.source, ) } } /// Describes locations where non-workspace packages (path or external) can be found. /// /// This is a serializable form of part of [`PackageSource`], and is used by [`ThirdPartySummary`]. #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] #[non_exhaustive] pub enum ThirdPartySource { /// A path dependency, relative to the workspace root. Path(Utf8PathBuf), /// A dependency on a registry. `crates.io` is represented as `None`. /// /// This should be the short name of the registry. Registry(Option), /// A dependency on a Git repository. /// /// Contains the name of the Git repository, plus an optional branch, tag or revision /// identifier. Git { /// The repository path. repo: String, /// The Git branch, tag or revision, if specified. req: GitReqSummary, }, /// A URL not otherwise recognized. Url(String), } impl fmt::Display for ThirdPartySource { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ThirdPartySource::Path(path) => write!(f, "{{ path = {path} }}"), ThirdPartySource::Registry(Some(registry)) => { write!(f, "{{ registry = \"{registry}\" }}") } ThirdPartySource::Registry(None) => write!(f, "crates.io"), ThirdPartySource::Git { repo, req } => match req { GitReqSummary::Branch(branch) => { write!(f, "{{ git = \"{repo}\", branch = \"{branch}\" }}") } GitReqSummary::Tag(tag) => { write!(f, "{{ git = \"{repo}\", tag = \"{tag}\" }}") } GitReqSummary::Rev(rev) => { write!(f, "{{ git = \"{repo}\", rev = \"{rev}\" }}") } GitReqSummary::Default => { write!(f, "{{ git = \"{repo}\" }}") } }, ThirdPartySource::Url(url) => write!(f, "{{ url = \"{url}\" }}"), } } } /// A summary specification for a Git branch, tag or revision. #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] #[non_exhaustive] pub enum GitReqSummary { /// A branch, e.g. `"main"`. Branch(String), /// A tag, e.g. `"guppy-0.5.0"`. Tag(String), /// A revision (commit hash), e.g. `"0227f048fcb7c798026ede6cc20c92befc84c3a4"`. Rev(String), /// The main branch by default. Default, } impl GitReq<'_> { /// Converts `self` into a [`GitReqSummary`]. /// /// Requires the `summaries` feature to be enabled. pub fn to_summary(self) -> GitReqSummary { GitReqSummary::new(self) } } impl GitReqSummary { /// Creates a new [`GitReqSummary`] from the provided [`GitReq`]. pub fn new(git_req: GitReq<'_>) -> Self { match git_req { GitReq::Branch(branch) => GitReqSummary::Branch(branch.to_owned()), GitReq::Tag(tag) => GitReqSummary::Tag(tag.to_owned()), GitReq::Rev(rev) => GitReqSummary::Rev(rev.to_owned()), GitReq::Default => GitReqSummary::Default, } } /// Converts `self` into a [`GitReq`]. pub fn as_git_req(&self) -> GitReq<'_> { match self { GitReqSummary::Branch(branch) => GitReq::Branch(branch.as_str()), GitReqSummary::Tag(tag) => GitReq::Tag(tag.as_str()), GitReqSummary::Rev(rev) => GitReq::Rev(rev.as_str()), GitReqSummary::Default => GitReq::Default, } } } // --- // Serialization and deserialization // --- #[derive(Debug, Default, Deserialize, Serialize)] struct ThirdPartySelectFields<'a> { #[serde(borrow)] name: Cow<'a, str>, #[serde(default, skip_serializing_if = "version_req_is_star")] version: VersionReq, // Fields that go into non-workspace source. #[serde( default, skip_serializing_if = "Option::is_none", serialize_with = "serialize_opt_path_fwdslash" )] path: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[serde(borrow)] registry: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] #[serde(borrow)] git: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] #[serde(borrow)] branch: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] #[serde(borrow)] tag: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] #[serde(borrow)] rev: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] #[serde(borrow)] url: Option>, } impl<'de> Deserialize<'de> for ThirdPartySummary { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { let fields = ThirdPartySelectFields::deserialize(deserializer)?; // Look for incompatible fields. let mut found_sources = vec![]; if fields.path.is_some() { found_sources.push("`path`"); } if fields.registry.is_some() { found_sources.push("`registry`"); } if fields.git.is_some() { found_sources.push("`git`"); } if fields.url.is_some() { found_sources.push("`url`"); } let mut found_git = vec![]; if fields.branch.is_some() { found_git.push("`branch`"); } if fields.tag.is_some() { found_git.push("`tag`"); } if fields.rev.is_some() { found_git.push("`rev`"); } // Only one of the above fields can be present. if found_sources.len() > 1 { return Err(serde::de::Error::custom(format!( "for package {}, only one of {} can be present", fields.name, found_sources.join(", ") ))); } let source = if let Some(path) = fields.path { if !found_git.is_empty() { return Err(serde::de::Error::custom(format!( "for package {}, `path` incompatible with {}", fields.name, found_git.join(", ") ))); } ThirdPartySource::Path(path) } else if let Some(git) = fields.git { if found_git.len() > 1 { return Err(serde::de::Error::custom(format!( "for package {}, only one of {} can be present", fields.name, found_git.join(", ") ))); } let req = if let Some(branch) = fields.branch { GitReqSummary::Branch(branch.into_owned()) } else if let Some(tag) = fields.tag { GitReqSummary::Tag(tag.into_owned()) } else if let Some(rev) = fields.rev { GitReqSummary::Rev(rev.into_owned()) } else { GitReqSummary::Default }; ThirdPartySource::Git { repo: git.into_owned(), req, } } else if let Some(url) = fields.url { if !found_git.is_empty() { return Err(serde::de::Error::custom(format!( "for package {}, `url` incompatible with {}", fields.name, found_git.join(", ") ))); } ThirdPartySource::Url(url.into_owned()) } else { if !found_git.is_empty() { if fields.registry.is_some() { return Err(serde::de::Error::custom(format!( "for package {}, `registry` incompatible with {}", fields.name, found_git.join(", "), ))); } else { return Err(serde::de::Error::custom(format!( "for package {}, `git` required for {}", fields.name, found_git.join(", "), ))); } } ThirdPartySource::Registry(fields.registry.map(|registry| registry.into_owned())) }; Ok(Self { name: fields.name.into_owned(), version: fields.version, source, }) } } impl Serialize for ThirdPartySummary { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let mut fields = ThirdPartySelectFields { name: Cow::Borrowed(self.name.as_str()), version: self.version.clone(), ..ThirdPartySelectFields::default() }; match &self.source { ThirdPartySource::Path(path) => { // Using clone rather than Cow::Borrowed here makes the deserialize impl simpler. fields.path = Some(path.clone()); } ThirdPartySource::Url(url) => { fields.url = Some(Cow::Borrowed(url.as_str())); } ThirdPartySource::Registry(registry) => { fields.registry = registry.as_deref().map(Cow::Borrowed); } ThirdPartySource::Git { repo, req } => { fields.git = Some(Cow::Borrowed(repo.as_str())); match req { GitReqSummary::Branch(branch) => { fields.branch = Some(Cow::Borrowed(branch.as_str())) } GitReqSummary::Tag(tag) => fields.tag = Some(Cow::Borrowed(tag.as_str())), GitReqSummary::Rev(rev) => fields.rev = Some(Cow::Borrowed(rev.as_str())), GitReqSummary::Default => {} } } } fields.serialize(serializer) } } fn serialize_opt_path_fwdslash( path: &Option, serializer: S, ) -> Result where S: Serializer, { match path { Some(path) => guppy_summaries::serialize_forward_slashes(path, serializer), None => serializer.serialize_none(), } } fn version_req_is_star(req: &VersionReq) -> bool { req == &VersionReq::STAR } // --- // Package matching // --- #[derive(Debug)] struct PackageMatcher<'a> { // The bools are to ensure that all the packages specified in the summary actually get matched // against something in the metadata. summary_ids: AHashMap<&'a SummaryId, bool>, workspace_members: &'a BTreeSet, third_party: AHashMap<&'a str, SmallVec<[(&'a ThirdPartySummary, bool); 2]>>, registry_names_to_urls: AHashMap<&'a str, &'a str>, } impl<'a> PackageMatcher<'a> { fn new( summary: &'a PackageSetSummary, mut registry_name_to_url: impl FnMut(&str) -> Option<&'a str>, error_message: &str, ) -> Result { let summary_ids = summary .summary_ids .iter() .map(|summary_id| (summary_id, false)) .collect(); let mut third_party: AHashMap<_, SmallVec<[_; 2]>> = AHashMap::new(); let mut registry_names_to_urls = AHashMap::new(); for tp_summary in &summary.third_party { if let ThirdPartySource::Registry(Some(name)) = &tp_summary.source { if !registry_names_to_urls.contains_key(name.as_str()) { match registry_name_to_url(name) { Some(url) => { registry_names_to_urls.insert(name.as_str(), url); } None => { return Err(Error::UnknownRegistryName { message: error_message.to_owned(), summary: Box::new(tp_summary.clone()), registry_name: name.clone(), }); } } } } third_party .entry(tp_summary.name.as_str()) .or_default() .push((tp_summary, false)); } Ok(Self { summary_ids, workspace_members: &summary.workspace_members, third_party, registry_names_to_urls, }) } /// Return whether something is a match, and record matches in `self`. fn store_is_match(&mut self, metadata: PackageMetadata<'_>) -> bool { // Don't short-circuit matches because we want to mark a // TODO: maybe this should involve duplicate detection between summary_ids and workspace/ // third-party let name = metadata.name(); let in_ids = match self.summary_ids.get_mut(&metadata.to_summary_id()) { Some(is_match_store) => { *is_match_store = true; true } None => false, }; let in_selectors = if metadata.in_workspace() { self.workspace_members.contains(name) } else { let registry_names_to_urls = &self.registry_names_to_urls; match self.third_party.get_mut(name) { Some(matches) => { let mut is_match = false; for (summary, is_match_store) in matches { if summary.version.matches(metadata.version()) && Self::source_matches( metadata.source(), &summary.source, registry_names_to_urls, ) { // This is a match. is_match = true; *is_match_store = true; } } is_match } None => false, } }; in_ids || in_selectors } // Returns an error if any elements were unmatched. fn finish(self, graph: &PackageGraph, error_message: impl Into) -> Result<(), Error> { let mut unknown_summary_ids: Vec<_> = self .summary_ids .into_iter() .filter_map( |(summary_id, matched)| { if matched { None } else { Some(summary_id) } }, ) .cloned() .collect(); unknown_summary_ids.sort_unstable(); let workspace = graph.workspace(); let unknown_workspace_members: Vec<_> = self .workspace_members .iter() .filter_map(|member| { if workspace.contains_name(member) { None } else { Some(member.clone()) } }) .collect(); let mut unknown_third_party: Vec<_> = self.third_party .into_iter() .flat_map(|(_, summaries)| { summaries.into_iter().filter_map(|(summary, matched)| { if matched { None } else { Some(summary.clone()) } }) }) .collect(); unknown_third_party.sort_by(|x, y| x.name.cmp(&y.name)); if unknown_summary_ids.is_empty() && unknown_workspace_members.is_empty() && unknown_third_party.is_empty() { Ok(()) } else { Err(UnknownPackageSetSummary { message: error_message.into(), unknown_summary_ids, unknown_workspace_members, unknown_third_party, }) } } // --- // Helper methods // --- fn source_matches( package_source: PackageSource<'_>, third_party_source: &ThirdPartySource, registry_names_to_urls: &AHashMap<&'a str, &'a str>, ) -> bool { match (package_source, third_party_source) { (PackageSource::Workspace(_), _) => { // third-party sources can't match a workspace package false } (PackageSource::Path(package_path), ThirdPartySource::Path(summary_path)) => { package_path == summary_path } (PackageSource::Path(_), _) => false, (PackageSource::External(external), ThirdPartySource::Url(summary_url)) => { external == summary_url } (external, _) => { let external_source = match external.parse_external() { Some(external_source) => external_source, None => { // The only way this can match is with the ThirdPartySource::Url constraint // above. return false; } }; match (external_source, third_party_source) { ( ExternalSource::Registry(external_registry_url), ThirdPartySource::Registry(Some(summary_registry_name)), ) => { let &url = registry_names_to_urls .get(summary_registry_name.as_str()) .expect("all names were already obtained in new()"); url == external_registry_url } ( ExternalSource::Registry(external_registry), ThirdPartySource::Registry(None), ) => external_registry == ExternalSource::CRATES_IO_URL, ( ExternalSource::Git { repository, req, .. }, ThirdPartySource::Git { repo: summary_repo, req: summary_req, }, ) => repository == summary_repo && summary_req.as_git_req() == req, _ => false, } } } } } #[cfg(test)] mod tests { #![allow(clippy::vec_init_then_push)] use super::*; use crate::graph::summaries::SummarySource; use semver::Version; #[test] fn valid() { let mut valids = vec![]; valids.push(("", PackageSetSummary::default())); let mut summary_ids = BTreeSet::new(); summary_ids.insert(SummaryId { name: "x".to_owned(), version: Version::parse("1.0.0").expect("version 1.0.0 parsed"), source: SummarySource::CratesIo, }); valids.push(( r#"[[ids]] name = "x" version = "1.0.0" crates-io = true "#, PackageSetSummary { summary_ids, ..PackageSetSummary::default() }, )); valids.push(( r#"# workspace-members = []"#, PackageSetSummary::default(), )); let mut workspace_members = BTreeSet::new(); workspace_members.insert("abc".to_owned()); valids.push(( r#" workspace-members = ["abc"]"#, PackageSetSummary { workspace_members, ..PackageSetSummary::default() }, )); let mut third_party = vec![]; third_party.push(ThirdPartySummary { name: "foo".to_owned(), version: VersionReq::default(), source: ThirdPartySource::Registry(None), }); valids.push(( r#" third-party = [ { name = "foo" } ]"#, PackageSetSummary { third_party, ..PackageSetSummary::default() }, )); let mut third_party = vec![]; third_party.push(ThirdPartySummary { name: "foo".to_owned(), version: VersionReq::default(), source: ThirdPartySource::Git { repo: "git-repo".to_owned(), req: GitReqSummary::Default, }, }); third_party.push(ThirdPartySummary { name: "foo".to_owned(), version: VersionReq::parse(">2.0").expect("version >2.0 parsed correctly"), source: ThirdPartySource::Registry(Some("foo".to_owned())), }); third_party.push(ThirdPartySummary { name: "bar".to_owned(), version: VersionReq::default(), source: ThirdPartySource::Git { repo: "git-repo".to_owned(), req: GitReqSummary::Branch("x".to_owned()), }, }); third_party.push(ThirdPartySummary { name: "bar".to_owned(), version: VersionReq::default(), source: ThirdPartySource::Git { repo: "git-repo".to_owned(), req: GitReqSummary::Tag("y".to_owned()), }, }); third_party.push(ThirdPartySummary { name: "baz".to_owned(), version: VersionReq::parse("4.1").expect("version 4.1 parsed correctly"), source: ThirdPartySource::Git { repo: "git-repo".to_owned(), req: GitReqSummary::Rev("z".to_owned()), }, }); third_party.push(ThirdPartySummary { name: "baz".to_owned(), version: VersionReq::default(), source: ThirdPartySource::Url("url".to_owned()), }); valids.push(( r#" third-party = [ { name = "foo", git = "git-repo" }, { name = "foo", registry = "foo", version = ">2.0" }, { name = "bar", git = "git-repo", branch = "x" }, { name = "bar", git = "git-repo", tag = "y" }, { name = "baz", git = "git-repo", rev = "z", version = "4.1" }, { name = "baz", version = "*", url = "url" }, ] "#, PackageSetSummary { third_party, ..PackageSetSummary::default() }, )); for (input, expected) in valids { let formatted_input = format_input(input); let actual = toml::de::from_str(input) .unwrap_or_else(|err| panic!("{formatted_input}\ndeserialization error: {err}")); assert_eq!(expected, actual, "{formatted_input}"); let serialized = toml::ser::to_string(&actual) .unwrap_or_else(|err| panic!("{formatted_input}\nserialization error: {err}")); // Check that the serialized output matches by parsing it again. let actual2 = toml::de::from_str(&serialized).unwrap_or_else(|err| { panic!("{formatted_input}\ndeserialization error try 2: {err}") }); assert_eq!(actual, actual2, "{formatted_input}"); } } #[test] fn invalid() { let invalids = &[ ( r#" workspace-members = [ { name = "s" } ]"#, "expected a string for key `workspace-members`", ), ( r#"third-party = [ { git = "git-repo" } ]"#, "missing field `name` for key `third-party`", ), ( r#" third-party = [ { name = "x", path = "foo", git = "git-repo" } ]#", "#, "only one of `path`, `git` can be present", ), ( r#" third-party = [ { name = "x", path = "foo", registry = "y" } ] "#, "only one of `path`, `registry` can be present", ), ( r#" third-party = [ { name = "x", path = "foo", tag = "x" } ] "#, "`path` incompatible with `tag`", ), ( r#" third-party = [ { name = "x", registry = "foo", rev = "z" } ] "#, "`registry` incompatible with `rev`", ), ( r#" third-party = [ { name = "x", branch = "b" } ] "#, "`git` required for `branch`", ), ( r#" third-party = [ { name = "x", git = "g", branch = "b", tag = "t" } ] "#, "only one of `branch`, `tag` can be present", ), ( r#" third-party = [ { name = "x", git = "g", tag = "t", rev = "r" } ] "#, "only one of `tag`, `rev` can be present", ), ]; for (input, err_msg) in invalids { let formatted_input = format_input(input); let err = match toml::de::from_str::(input) { Ok(output) => { panic!("invalid input did not fail, {formatted_input}\noutput: {output:?}",) } Err(err) => err, }; let err_display = format!("{err}"); assert!( err_display.contains(err_msg), "{formatted_input}\nerror message '{err_display}' did not contain '{err_msg}" ); } } fn format_input(input: &str) -> String { format!("input:\n---\n{input}\n---") } } guppy-0.17.25/src/graph/summaries.rs000064400000000000000000000300451046102023000154110ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 //! Generate build summaries from `CargoSet` instances. //! //! Requires the `summaries` feature to be enabled. mod package_set; use crate::{ Error, graph::{ DependencyDirection, PackageGraph, PackageMetadata, PackageSet, PackageSource, cargo::{CargoOptions, CargoResolverVersion, CargoSet, InitialsPlatform}, feature::FeatureSet, }, platform::PlatformSpecSummary, }; pub use guppy_summaries::*; pub use package_set::*; use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; impl CargoSet<'_> { /// Creates a build summary with the given options. /// /// Requires the `summaries` feature to be enabled. pub fn to_summary(&self, opts: &CargoOptions<'_>) -> Result { let initials = self.initials(); let metadata = CargoOptionsSummary::new(initials.graph().package_graph, self.features_only(), opts)?; let target_features = self.target_features(); let host_features = self.host_features(); let mut summary = Summary::with_metadata(&metadata).map_err(Error::TomlSerializeError)?; summary.target_packages = target_features.to_package_map(initials, self.target_direct_deps()); summary.host_packages = host_features.to_package_map(initials, self.host_direct_deps()); Ok(summary) } } impl<'g> FeatureSet<'g> { /// Creates a `PackageMap` from this `FeatureSet`. /// /// `initials` and `direct_deps` are used to assign a PackageStatus. fn to_package_map( &self, initials: &FeatureSet<'g>, direct_deps: &PackageSet<'g>, ) -> PackageMap { self.packages_with_features(DependencyDirection::Forward) .map(|feature_list| { let package = feature_list.package(); let status = if initials.contains_package_ix(package.package_ix()) { PackageStatus::Initial } else if package.in_workspace() { PackageStatus::Workspace } else if direct_deps.contains_ix(package.package_ix()) { PackageStatus::Direct } else { PackageStatus::Transitive }; let info = PackageInfo { status, features: feature_list .named_features() .map(|feature| feature.to_owned()) .collect(), optional_deps: feature_list .optional_deps() .map(|dep| dep.to_owned()) .collect(), }; (feature_list.package().to_summary_id(), info) }) .collect() } } impl PackageGraph { /// Converts this `SummaryId` to a `PackageMetadata`. /// /// Returns an error if the summary ID could not be matched. /// /// Requires the `summaries` feature to be enabled. pub fn metadata_by_summary_id( &self, summary_id: &SummaryId, ) -> Result, Error> { match &summary_id.source { SummarySource::Workspace { workspace_path } => { self.workspace().member_by_path(workspace_path) } _ => { // Do a linear search for now -- this appears to be the easiest thing to do and is // pretty fast. This could potentially be sped up by building an index by name, but // at least for reasonably-sized graphs it's really fast. // // TODO: consider optimizing this in the future. let mut filter = self.packages().filter(|package| { package.name() == summary_id.name && package.version() == &summary_id.version && package.source() == summary_id.source }); filter .next() .ok_or_else(|| Error::UnknownSummaryId(summary_id.clone())) } } } } impl PackageMetadata<'_> { /// Converts this metadata to a `SummaryId`. /// /// Requires the `summaries` feature to be enabled. pub fn to_summary_id(&self) -> SummaryId { SummaryId { name: self.name().to_string(), version: self.version().clone(), source: self.source().to_summary_source(), } } } /// A summary of Cargo options used to build a `CargoSet`. /// /// Requires the `summaries` feature to be enabled. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "kebab-case")] #[non_exhaustive] pub struct CargoOptionsSummary { /// The Cargo resolver version used. /// /// For more information, see the documentation for [`CargoResolverVersion`]. #[serde(alias = "version")] pub resolver: CargoResolverVersion, /// Whether dev-dependencies are included. pub include_dev: bool, /// The platform for which the initials are specified. #[serde(flatten)] pub initials_platform: InitialsPlatformSummary, /// The host platform. #[serde(default)] pub host_platform: PlatformSpecSummary, /// The target platform. #[serde(default)] pub target_platform: PlatformSpecSummary, /// The set of packages omitted from computations. #[serde(skip_serializing_if = "PackageSetSummary::is_empty", default)] pub omitted_packages: PackageSetSummary, /// The packages that formed the features-only set. #[serde(skip_serializing_if = "Vec::is_empty", default)] pub features_only: Vec, } impl CargoOptionsSummary { /// Creates a new `CargoOptionsSummary` from the given Cargo options. pub fn new( graph: &PackageGraph, features_only: &FeatureSet<'_>, opts: &CargoOptions<'_>, ) -> Result { let omitted_packages = PackageSetSummary::from_package_ids(graph, opts.omitted_packages.iter().copied())?; let mut features_only = features_only .packages_with_features(DependencyDirection::Forward) .map(|features| FeaturesOnlySummary { summary_id: features.package().to_summary_id(), features: features .named_features() .map(|feature| feature.to_owned()) .collect(), optional_deps: features .optional_deps() .map(|feature| feature.to_owned()) .collect(), }) .collect::>(); features_only.sort_unstable(); Ok(Self { resolver: opts.resolver, include_dev: opts.include_dev, initials_platform: InitialsPlatformSummary::V2 { initials_platform: opts.initials_platform, }, host_platform: PlatformSpecSummary::new(&opts.host_platform), target_platform: PlatformSpecSummary::new(&opts.target_platform), omitted_packages, features_only, }) } /// Creates a new `CargoOptions` from this summary. pub fn to_cargo_options<'g>( &'g self, package_graph: &'g PackageGraph, ) -> Result, Error> { let omitted_packages = self .omitted_packages .to_package_set(package_graph, "resolving omitted-packages")?; // TODO: return the features-only set let mut options = CargoOptions::new(); options .set_resolver(self.resolver) .set_include_dev(self.include_dev) .set_initials_platform(self.initials_platform.into()) .set_host_platform( self.host_platform.to_platform_spec().map_err(|err| { Error::TargetSpecError("parsing host platform".to_string(), err) })?, ) .set_target_platform(self.target_platform.to_platform_spec().map_err(|err| { Error::TargetSpecError("parsing target platform".to_string(), err) })?) .add_omitted_packages(omitted_packages.package_ids(DependencyDirection::Forward)); Ok(options) } } /// Summary information for `InitialsPlatform`. #[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(untagged, rename_all = "kebab-case")] #[non_exhaustive] pub enum InitialsPlatformSummary { /// The first version of this option, which only allowed setting `proc-macros-on-target`. #[serde(rename_all = "kebab-case")] V1 { /// If set to true, this is treated as `InitialsPlatform::ProcMacrosOnTarget`, otherwise as /// `InitialsPlatform::Standard`. proc_macros_on_target: bool, }, /// The second and current version of this option. #[serde(rename_all = "kebab-case")] V2 { /// The configuration value. initials_platform: InitialsPlatform, }, } impl From for InitialsPlatform { fn from(s: InitialsPlatformSummary) -> Self { match s { InitialsPlatformSummary::V1 { proc_macros_on_target, } => { if proc_macros_on_target { InitialsPlatform::ProcMacrosOnTarget } else { InitialsPlatform::Standard } } InitialsPlatformSummary::V2 { initials_platform } => initials_platform, } } } /// Summary information for a features-only package. /// /// These packages are stored in `CargoOptionsSummary` because they may or may not be in the final /// build set. #[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] #[serde(rename_all = "kebab-case")] #[non_exhaustive] pub struct FeaturesOnlySummary { /// The summary ID for this feature. #[serde(flatten)] pub summary_id: SummaryId, /// The named features built for this package. pub features: BTreeSet, /// The optional dependencies built for this package. #[serde(skip_serializing_if = "BTreeSet::is_empty", default)] pub optional_deps: BTreeSet, } impl PackageSource<'_> { /// Converts a `PackageSource` into a `SummarySource`. /// /// Requires the `summaries` feature to be enabled. pub fn to_summary_source(&self) -> SummarySource { match self { PackageSource::Workspace(path) => SummarySource::workspace(path), PackageSource::Path(path) => SummarySource::path(path), PackageSource::External(source) => { if *source == PackageSource::CRATES_IO_REGISTRY { SummarySource::crates_io() } else { SummarySource::external(*source) } } } } } impl PartialEq for PackageSource<'_> { fn eq(&self, summary_source: &SummarySource) -> bool { match summary_source { SummarySource::Workspace { workspace_path } => { self == &PackageSource::Workspace(workspace_path) } SummarySource::Path { path } => self == &PackageSource::Path(path), SummarySource::CratesIo => { self == &PackageSource::External(PackageSource::CRATES_IO_REGISTRY) } SummarySource::External { source } => self == &PackageSource::External(source), } } } #[cfg(test)] mod tests { use super::*; #[test] fn parse_old_metadata() { // Ensure that previous versions of the metadata parse correctly. // TODO: note that there have been some compatibility breaks, particularly for // omitted-packages. Probably don't need to retain too much backwards compatibility. let metadata = "\ version = 'v1' include-dev = true proc-macros-on-target = false "; let summary: CargoOptionsSummary = toml::from_str(metadata).expect("parsed correctly"); assert_eq!( InitialsPlatform::from(summary.initials_platform), InitialsPlatform::Standard ); } } guppy-0.17.25/src/lib.rs000064400000000000000000000124761046102023000130610ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 //! Track and query Cargo dependency graphs. //! //! `guppy` provides a Rust interface to run queries over Cargo dependency graphs. `guppy` parses //! the output of [`cargo metadata`](https://doc.rust-lang.org/cargo/commands/cargo-metadata.html), //! then presents a graph interface over it. //! //! # Types and lifetimes //! //! The central structure exposed by `guppy` is [`PackageGraph`](crate::graph::PackageGraph). This //! represents a directed (though [not necessarily acyclic](crate::graph::Cycles)) graph where every //! node is a package and every edge represents a dependency. //! //! Other types borrow data from a `PackageGraph` and have a `'g` lifetime parameter indicating //! that. A lifetime parameter named `'g` always indicates that data is borrowed from a //! `PackageGraph`. //! //! [`PackageMetadata`](crate::graph::PackageMetadata) contains information about individual //! packages, such as the data in //! [the `[package]` section](https://doc.rust-lang.org/cargo/reference/manifest.html#the-package-section). //! //! For traversing the graph, `guppy` provides a few types: //! * [`PackageLink`](crate::graph::PackageLink) represents both ends of a dependency edge, along //! with details about the dependency (whether it is dev-only, platform-specific, and so on). //! * [`PackageQuery`](crate::graph::PackageQuery) represents the input parameters to a dependency //! traversal: a set of packages and a direction. A traversal is performed with //! [`PackageQuery::resolve`](crate::graph::PackageQuery::resolve), and fine-grained control over //! the traversal is achieved with //! [`PackageQuery::resolve_with_fn`](crate::graph::PackageQuery::resolve_with_fn). //! * [`PackageSet`](crate::graph::PackageSet) represents the result of a graph traversal. This //! struct provides several methods to iterate over packages. //! //! For some operations, `guppy` builds an auxiliary [`FeatureGraph`](crate::graph::feature::FeatureGraph) //! the first time it is required. Every node in a `FeatureGraph` is a combination of a package and //! a feature declared in it, and every edge is a feature dependency. //! //! For traversing the feature graph, `guppy` provides the analogous [`FeatureQuery`](crate::graph::feature::FeatureQuery) and //! [`FeatureSet`](crate::graph::feature::FeatureSet) types. //! //! `FeatureSet` also has an [`into_cargo_set`](crate::graph::feature::FeatureSet::into_cargo_set) //! method, to simulate Cargo builds. This method produces a [`CargoSet`](crate::graph::cargo::CargoSet), //! which is essentially two `FeatureSet`s along with some more useful information. //! //! `guppy`'s data structures are immutable, with some internal caches. All of `guppy`'s types are //! `Send + Sync`, and all lifetime parameters are [covariant](https://github.com/sunshowers/lifetime-variance-example/). //! //! # Optional features //! //! * `proptest1`: Support for [property-based testing](https://jessitron.com/2013/04/25/property-based-testing-what-is-it/) //! using the [`proptest`](https://altsysrq.github.io/proptest-book/intro.html) framework. //! * `rayon1`: Support for parallel iterators through [Rayon](docs.rs/rayon/1) (preliminary work //! so far, more parallel iterators to be added in the future). //! * `summaries`: Support for writing out [build summaries](https://github.com/guppy-rs/guppy/tree/main/guppy-summaries). //! //! # Examples //! //! Print out all direct dependencies of a package: //! //! ``` //! use guppy::{CargoMetadata, PackageId}; //! //! // `guppy` accepts `cargo metadata` JSON output. Use a pre-existing fixture for these examples. //! let metadata = CargoMetadata::parse_json(include_str!("../../fixtures/small/metadata1.json")).unwrap(); //! let package_graph = metadata.build_graph().unwrap(); //! //! // `guppy` provides several ways to get hold of package IDs. Use a pre-defined one for this //! // example. //! let package_id = PackageId::new("testcrate 0.1.0 (path+file:///fakepath/testcrate)"); //! //! // The `metadata` method returns information about the package, or `None` if the package ID //! // wasn't recognized. //! let package = package_graph.metadata(&package_id).unwrap(); //! //! // `direct_links` returns all direct dependencies of a package. //! for link in package.direct_links() { //! // A dependency link contains `from()`, `to()` and information about the specifics of the //! // dependency. //! println!("direct dependency: {}", link.to().id()); //! } //! ``` //! //! For more examples, see //! [the `examples` directory](https://github.com/guppy-rs/guppy/tree/main/guppy/examples). #![warn(missing_docs)] #![cfg_attr(doc_cfg, feature(doc_cfg))] #[macro_use] mod macros; // TODO: remove in the next major version of guppy #[doc(hidden)] pub use debug_ignore; mod dependency_kind; pub mod errors; pub mod graph; mod metadata_command; mod package_id; pub(crate) mod petgraph_support; pub mod platform; pub(crate) mod sorted_set; #[cfg(test)] mod unit_tests; pub use dependency_kind::*; pub use errors::Error; pub use metadata_command::*; pub use package_id::PackageId; // 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 semver::{Version, VersionReq}; #[doc(no_inline)] pub use serde_json::Value as JsonValue; guppy-0.17.25/src/macros.rs000064400000000000000000000006451046102023000135720ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 //! Test and helper macros. /// Assert that a struct with a single lifetime parameter is covariant. macro_rules! assert_covariant { ($i:ident) => { const _: () = { #[allow(dead_code)] fn assert_covariant<'a, 'b: 'a>(x: $i<'b>) -> $i<'a> { x } }; }; } guppy-0.17.25/src/metadata_command.rs000064400000000000000000000147411046102023000155660ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::{Error, graph::PackageGraph}; use cargo_metadata::CargoOpt; use serde::{Deserialize, Serialize}; use std::{convert::TryFrom, io, path::PathBuf, process::Command}; /// A builder for configuring `cargo metadata` invocations. /// /// This is the most common entry point for constructing a `PackageGraph`. /// /// ## Examples /// /// Build a `PackageGraph` for the Cargo workspace in the current directory: /// /// ```rust /// use guppy::MetadataCommand; /// use guppy::graph::PackageGraph; /// /// let mut cmd = MetadataCommand::new(); /// let package_graph = PackageGraph::from_command(&mut cmd); /// ``` #[derive(Clone, Debug, Default)] pub struct MetadataCommand { inner: cargo_metadata::MetadataCommand, } impl MetadataCommand { /// Creates a default `cargo metadata` command builder. /// /// By default, this will look for `Cargo.toml` in the ancestors of this process's current /// directory. pub fn new() -> Self { let mut inner = cargo_metadata::MetadataCommand::new(); // Always use --all-features so that we get a full view of the graph. inner.features(CargoOpt::AllFeatures); Self { inner } } /// Sets the path to the `cargo` executable. /// /// If unset, this will use the `$CARGO` environment variable, or else `cargo` from `$PATH`. pub fn cargo_path(&mut self, path: impl Into) -> &mut Self { self.inner.cargo_path(path); self } /// Sets the path to `Cargo.toml`. /// /// By default, this will look for `Cargo.toml` in the ancestors of the current directory. Note /// that this doesn't need to be the root `Cargo.toml` in a workspace -- any member of the /// workspace is fine. pub fn manifest_path(&mut self, path: impl Into) -> &mut Self { self.inner.manifest_path(path); self } /// Sets the current directory of the `cargo metadata` process. /// /// By default, the current directory will be inherited from this process. pub fn current_dir(&mut self, path: impl Into) -> &mut Self { self.inner.current_dir(path); self } /// Output information only about the workspace and do not fetch dependencies. /// /// For full functionality, `cargo metadata` should be run without `--no-deps`, so that `guppy` /// knows about third-party crates and dependency edges. However, `guppy` supports a "light" /// mode if `--no-deps` is run, in which case the following limitations will apply: /// * dependency queries will not work /// * there will be no information about non-workspace crates /// /// Constructing a graph with this option can be several times faster than the default. pub fn no_deps(&mut self) -> &mut Self { self.inner.no_deps(); self } // *Do not* implement features. /// Arbitrary flags to pass to `cargo metadata`. These will be added to the end of the /// command invocation. /// /// Note that `guppy` internally: /// * uses `--format-version 1` as its metadata format. /// * passes in `--all-features`, so that `guppy` has a full view of the dependency graph. /// /// Attempting to override either of those options may lead to unexpected results. pub fn other_options( &mut self, options: impl IntoIterator>, ) -> &mut Self { self.inner .other_options(options.into_iter().map(|s| s.into()).collect::>()); self } /// Arbitrary environment variables to set when running cargo. These will be merged into the /// calling environment, overriding any which clash. pub fn env( &mut self, key: impl Into, val: impl Into, ) -> &mut Self { self.inner.env(key, val); self } /// Builds a [`Command`] instance. This is the first part of calling /// [`exec`](Self::exec). pub fn cargo_command(&self) -> Command { self.inner.cargo_command() } /// Runs the configured `cargo metadata` and returns a deserialized `CargoMetadata`. pub fn exec(&self) -> Result { let inner = self.inner.exec().map_err(Error::command_error)?; Ok(CargoMetadata(inner)) } /// Runs the configured `cargo metadata` and returns a parsed `PackageGraph`. pub fn build_graph(&self) -> Result { let metadata = self.exec()?; metadata.build_graph() } } /// Although consuming a `MetadataCommand` is not required for building a `PackageGraph`, this impl /// is provided for convenience. impl TryFrom for PackageGraph { type Error = Error; fn try_from(command: MetadataCommand) -> Result { command.build_graph() } } impl<'a> TryFrom<&'a MetadataCommand> for PackageGraph { type Error = Error; fn try_from(command: &'a MetadataCommand) -> Result { command.build_graph() } } /// Deserialized Cargo metadata. /// /// Returned by a `MetadataCommand` or constructed from `cargo metadata` JSON output. /// /// This is an alternative entry point for constructing a `PackageGraph`, to be used if the JSON /// output of `cargo metadata` is already available. To construct a `PackageGraph` from an on-disk /// Cargo workspace, use [`MetadataCommand`](MetadataCommand). /// /// This struct implements `serde::Serialize` and `Deserialize`. #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(transparent)] pub struct CargoMetadata(pub(crate) cargo_metadata::Metadata); impl CargoMetadata { /// Deserializes this JSON blob into a `CargoMetadata`. pub fn parse_json(json: impl AsRef) -> Result { let inner = serde_json::from_str(json.as_ref()).map_err(Error::MetadataParseError)?; Ok(Self(inner)) } /// Serializes this metadata into the given writer. pub fn serialize(&self, writer: &mut impl io::Write) -> Result<(), Error> { serde_json::to_writer(writer, &self.0).map_err(Error::MetadataSerializeError) } /// Parses this metadata and builds a `PackageGraph` from it. pub fn build_graph(self) -> Result { PackageGraph::from_metadata(self) } } impl TryFrom for PackageGraph { type Error = Error; fn try_from(metadata: CargoMetadata) -> Result { metadata.build_graph() } } guppy-0.17.25/src/package_id.rs000064400000000000000000000024341046102023000143530ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use std::fmt; /// An "opaque" identifier for a package. #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] #[allow(clippy::derived_hash_with_manual_eq)] // safe because the same PartialEq impl is used everywhere pub struct PackageId { /// The underlying string representation of an ID. repr: Box, } impl PackageId { /// Creates a new `PackageId`. pub fn new(s: impl Into>) -> Self { Self { repr: s.into() } } pub(super) fn from_metadata(id: cargo_metadata::PackageId) -> Self { Self { repr: id.repr.into_boxed_str(), } } /// Returns the inner representation of a package ID. This is generally an opaque string and its /// precise format is subject to change. pub fn repr(&self) -> &str { &self.repr } } impl fmt::Display for PackageId { fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result { fmt::Display::fmt(&self.repr, f) } } impl PartialEq<&PackageId> for PackageId { fn eq(&self, other: &&PackageId) -> bool { self.eq(*other) } } impl PartialEq for &PackageId { fn eq(&self, other: &PackageId) -> bool { (*self).eq(other) } } guppy-0.17.25/src/petgraph_support/dfs.rs000064400000000000000000000101631046102023000164640ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use petgraph::{ prelude::*, visit::{ DfsPostOrder, IntoEdgeReferences, IntoEdges, IntoEdgesDirected, Reversed, ReversedEdgeReference, VisitMap, }, }; /// `DfsPostOrder::next`, adapted for a buffered filter that's also FnMut. pub fn dfs_next_buffered_filter( dfs: &mut DfsPostOrder, graph: G, mut buffered_filter: impl BufferedEdgeFilter, ) -> Option where N: Copy + PartialEq, VM: VisitMap, G: IntoEdges, { // Adapted from DfsPostOrder::next in petgraph 0.5.0. while let Some(&nx) = dfs.stack.last() { if dfs.discovered.visit(nx) { // First time visiting `nx`: Push neighbors, don't pop `nx` let neighbors = graph.edges(nx).flat_map(|edge| { buffered_filter .filter(edge) .into_iter() .map(|edge| edge.target()) }); for succ in neighbors { if !dfs.discovered.is_visited(&succ) { dfs.stack.push(succ); } } } else { dfs.stack.pop(); if dfs.finished.visit(nx) { // Second time: All reachable nodes must have been finished return Some(nx); } } } None } /// A buffered filter is a graph traversal edge filter that can buffer up some edges to be /// returned later. pub trait BufferedEdgeFilter where G: IntoEdges, { /// Returns a list of edge references to follow. type Iter: IntoIterator; fn filter(&mut self, edge: G::EdgeRef) -> Self::Iter; } impl BufferedEdgeFilter for &mut T where T: BufferedEdgeFilter, G: IntoEdges, { /// Returns a list of node IDs to follow. type Iter = T::Iter; #[inline] fn filter(&mut self, edge: G::EdgeRef) -> Self::Iter { (*self).filter(edge) } } #[derive(Debug)] pub struct SimpleEdgeFilterFn(pub F); impl BufferedEdgeFilter for SimpleEdgeFilterFn where F: FnMut(G::EdgeRef) -> bool, G: IntoEdges, { type Iter = Option; #[inline] fn filter(&mut self, edge: G::EdgeRef) -> Self::Iter { if (self.0)(edge) { Some(edge) } else { None } } } #[derive(Debug)] pub struct BufferedEdgeFilterFn(pub F); impl BufferedEdgeFilter for BufferedEdgeFilterFn where F: FnMut(G::EdgeRef) -> I, G: IntoEdges, I: IntoIterator, { type Iter = I; #[inline] fn filter(&mut self, edge: G::EdgeRef) -> Self::Iter { (self.0)(edge) } } #[derive(Debug)] pub struct ReversedBufferedFilter(pub T); impl BufferedEdgeFilter> for ReversedBufferedFilter where T: BufferedEdgeFilter, G: IntoEdgesDirected, { type Iter = ReversedEdgeReferences<<>::Iter as IntoIterator>::IntoIter>; fn filter(&mut self, edge: as IntoEdgeReferences>::EdgeRef) -> Self::Iter { ReversedEdgeReferences { iter: self.0.filter(edge.into_unreversed()).into_iter(), } } } // TODO: replace with upstream impl pub struct ReversedEdgeReferences { iter: I, } impl Iterator for ReversedEdgeReferences where I: Iterator, { type Item = ReversedEdgeReference; #[inline] fn next(&mut self) -> Option { // Ugh this sucks! Should be supported upstream. // SAFETY: this is just a newtype. This is SUCH a horrible hack. let item = self.iter.next()?; Some(unsafe { horrible_transmute::>(item) }) } #[inline] fn size_hint(&self) -> (usize, Option) { self.iter.size_hint() } } unsafe fn horrible_transmute(a: A) -> B { unsafe { let b = ::core::ptr::read(&a as *const A as *const B); ::core::mem::forget(a); b } } // Check that the above transmute is actually safe. static_assertions::assert_eq_size!((), ReversedEdgeReference<()>); guppy-0.17.25/src/petgraph_support/dot.rs000064400000000000000000000132001046102023000164710ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use petgraph::{ prelude::*, visit::{GraphProp, IntoEdgeReferences, IntoNodeReferences, NodeIndexable, NodeRef}, }; use std::fmt::{self, Write}; static INDENT: &str = " "; /// A visitor interface for formatting graph labels. pub trait DotVisitor { /// Visits this node. The implementation may output a label for this node to the given /// `DotWrite`. fn visit_node(&self, node: NR, f: &mut DotWrite<'_, '_>) -> fmt::Result; /// Visits this edge. The implementation may output a label for this edge to the given /// `DotWrite`. fn visit_edge(&self, edge: ER, f: &mut DotWrite<'_, '_>) -> fmt::Result; // TODO: allow more customizations? more labels, colors etc to be set? } /// A visitor for formatting graph labels that outputs `fmt::Display` impls for node and edge /// weights. /// /// This visitor will escape backslashes. #[derive(Copy, Clone, Debug)] #[allow(dead_code)] pub struct DisplayVisitor; impl DotVisitor for DisplayVisitor where NR: NodeRef, ER: EdgeRef, NR::Weight: fmt::Display, ER::Weight: fmt::Display, { fn visit_node(&self, node: NR, f: &mut DotWrite<'_, '_>) -> fmt::Result { write!(f, "{}", node.weight()) } fn visit_edge(&self, edge: ER, f: &mut DotWrite<'_, '_>) -> fmt::Result { write!(f, "{}", edge.weight()) } } impl DotVisitor for &T where T: DotVisitor, { fn visit_node(&self, node: NR, f: &mut DotWrite<'_, '_>) -> fmt::Result { (*self).visit_node(node, f) } fn visit_edge(&self, edge: ER, f: &mut DotWrite<'_, '_>) -> fmt::Result { (*self).visit_edge(edge, f) } } #[derive(Clone, Debug)] pub struct DotFmt { graph: G, visitor: V, } impl DotFmt where for<'a> &'a G: IntoEdgeReferences + IntoNodeReferences + GraphProp + NodeIndexable, for<'a> V: DotVisitor<<&'a G as IntoNodeReferences>::NodeRef, <&'a G as IntoEdgeReferences>::EdgeRef>, { /// Creates a new formatter for this graph. #[allow(dead_code)] pub fn new(graph: G, visitor: V) -> Self { Self { graph, visitor } } /// Outputs a graphviz-compatible representation of this graph to the given formatter. pub fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "{} {{", graph_type(&self.graph))?; for node in self.graph.node_references() { write!( f, "{}{} [label=\"", INDENT, (&self.graph).to_index(node.id()) )?; self.visitor.visit_node(node, &mut DotWrite::new(f))?; writeln!(f, "\"]")?; } let edge_str = edge_str(&self.graph); for edge in self.graph.edge_references() { write!( f, "{}{} {} {} [label=\"", INDENT, (&self.graph).to_index(edge.source()), edge_str, (&self.graph).to_index(edge.target()) )?; self.visitor.visit_edge(edge, &mut DotWrite::new(f))?; writeln!(f, "\"]")?; } writeln!(f, "}}") } } impl fmt::Display for DotFmt where for<'a> &'a G: IntoEdgeReferences + IntoNodeReferences + GraphProp + NodeIndexable, for<'a> V: DotVisitor<<&'a G as IntoNodeReferences>::NodeRef, <&'a G as IntoEdgeReferences>::EdgeRef>, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.fmt(f) } } /// A write target for `Dot` graphs. Use with the `write!` macro. pub struct DotWrite<'a, 'b> { f: &'a mut fmt::Formatter<'b>, escape_backslashes: bool, } impl<'a, 'b> DotWrite<'a, 'b> { fn new(f: &'a mut fmt::Formatter<'b>) -> Self { Self { f, escape_backslashes: true, } } /// Sets a config option for whether backslashes should be escaped. Defaults to `true`. /// /// This can be set to `false` if the visitor knows to output graphviz control characters. #[allow(dead_code)] pub fn set_escape_backslashes(&mut self, escape_backslashes: bool) { self.escape_backslashes = escape_backslashes; } /// Glue for usage of the `write!` macro. /// /// This method should generally not be invoked manually, but rather through `write!` or similar /// macros (`println!`, `format!` etc). /// /// Defining this inherent method allows `write!` to work without callers needing to import the /// `std::fmt::Write` trait. pub fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result { // Forward to the fmt::Write impl. Write::write_fmt(self, args) } } impl Write for DotWrite<'_, '_> { fn write_str(&mut self, s: &str) -> fmt::Result { for c in s.chars() { self.write_char(c)?; } Ok(()) } fn write_char(&mut self, c: char) -> fmt::Result { match c { '"' => self.f.write_str(r#"\""#), // \l is for left-justified newlines (\n means center-justified newlines) '\n' => self.f.write_str(r"\l"), // Backslashes are only escaped if the config is set. '\\' if self.escape_backslashes => self.f.write_str(r"\\"), // Other escapes like backslashes are passed through. c => self.f.write_char(c), } } } fn graph_type(graph: G) -> &'static str { if graph.is_directed() { "digraph" } else { "graph" } } fn edge_str(graph: G) -> &'static str { if graph.is_directed() { "->" } else { "--" } } guppy-0.17.25/src/petgraph_support/edge_ref.rs000064400000000000000000000014261046102023000174520ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use petgraph::{ graph::{EdgeReference, IndexType}, prelude::*, visit::ReversedEdgeReference, }; /// Provides a way to obtain graph::EdgeReference instances from arbitrary EdgeRef ones. pub trait GraphEdgeRef<'a, E, Ix: IndexType>: EdgeRef { fn into_edge_reference(self) -> EdgeReference<'a, E, Ix>; } impl<'a, E, Ix: IndexType> GraphEdgeRef<'a, E, Ix> for EdgeReference<'a, E, Ix> { fn into_edge_reference(self) -> EdgeReference<'a, E, Ix> { self } } impl<'a, E, Ix: IndexType> GraphEdgeRef<'a, E, Ix> for ReversedEdgeReference> { fn into_edge_reference(self) -> EdgeReference<'a, E, Ix> { self.into_unreversed() } } guppy-0.17.25/src/petgraph_support/mod.rs000064400000000000000000000032471046102023000164740ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 //! Support for petgraph. //! //! The code in here is generic over petgraph's traits, and could be upstreamed into petgraph if //! desirable. use fixedbitset::FixedBitSet; use petgraph::{graph::IndexType, prelude::*}; use std::iter::FromIterator; pub mod dfs; pub mod dot; pub mod edge_ref; pub mod scc; pub mod topo; pub mod walk; pub fn edge_triple(edge_ref: ER) -> (ER::NodeId, ER::NodeId, ER::EdgeId) { (edge_ref.source(), edge_ref.target(), edge_ref.id()) } #[derive(Clone, Debug, Default)] pub struct IxBitSet(pub FixedBitSet); impl IxBitSet { #[inline] pub(crate) fn with_capacity(bits: usize) -> Self { Self(FixedBitSet::with_capacity(bits)) } #[inline] pub(crate) fn insert_node_ix(&mut self, bit: NodeIndex) { self.0.insert(bit.index()); } } impl From for FixedBitSet { fn from(ix_set: IxBitSet) -> Self { ix_set.0 } } impl FromIterator> for IxBitSet { fn from_iter>>(iter: T) -> Self { IxBitSet(iter.into_iter().map(|node_ix| node_ix.index()).collect()) } } impl FromIterator> for IxBitSet { fn from_iter>>(iter: T) -> Self { IxBitSet(iter.into_iter().map(|edge_ix| edge_ix.index()).collect()) } } impl Extend> for IxBitSet { fn extend>>(&mut self, iter: T) { self.0 .extend(iter.into_iter().map(|node_ix| node_ix.index())); } } guppy-0.17.25/src/petgraph_support/scc.rs000064400000000000000000000133361046102023000164650ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use ahash::AHashMap; use fixedbitset::FixedBitSet; use nested::Nested; use petgraph::{ algo::kosaraju_scc, graph::IndexType, prelude::*, visit::{IntoNeighborsDirected, IntoNodeIdentifiers, VisitMap, Visitable}, }; use std::slice; #[derive(Clone, Debug)] pub(crate) struct Sccs { sccs: Nested>>, // Map of node indexes to the index of the SCC they belong to. If a node is not part of an SCC, // then the corresponding index is not stored here. multi_map: AHashMap, usize>, } impl Sccs { /// Creates a new instance from the provided graph and the given sorter. pub fn new(graph: G, mut scc_sorter: impl FnMut(&mut Vec>)) -> Self where G: IntoNeighborsDirected> + Visitable + IntoNodeIdentifiers, ::Map: VisitMap>, { // Use kosaraju_scc since it is iterative (tarjan_scc is recursive) and package graphs // have unbounded depth. let sccs = kosaraju_scc(graph); let sccs: Nested> = sccs .into_iter() .map(|mut scc| { if scc.len() > 1 { scc_sorter(&mut scc); } scc }) // kosaraju_scc returns its sccs in reverse topological order. Reverse it again for // forward topological order. .rev() .collect(); let mut multi_map = AHashMap::new(); for (idx, scc) in sccs.iter().enumerate() { if scc.len() > 1 { multi_map.extend(scc.iter().map(|ix| (*ix, idx))); } } Self { sccs, multi_map } } /// Returns true if `a` and `b` are in the same scc. pub fn is_same_scc(&self, a: NodeIndex, b: NodeIndex) -> bool { if a == b { return true; } match (self.multi_map.get(&a), self.multi_map.get(&b)) { (Some(a_scc), Some(b_scc)) => a_scc == b_scc, _ => false, } } /// Returns all the SCCs with more than one element. pub fn multi_sccs(&self) -> impl DoubleEndedIterator]> { self.sccs.iter().filter(|scc| scc.len() > 1) } /// Returns all the nodes of this graph that have no incoming edges to them, and all the nodes /// in an SCC into which there are no incoming edges. pub fn externals<'a, G>(&'a self, graph: G) -> impl Iterator> + 'a where G: 'a + IntoNodeIdentifiers + IntoNeighborsDirected>, Ix: IndexType, { // Consider each SCC as one logical node. let mut external_sccs = FixedBitSet::with_capacity(self.sccs.len()); let mut internal_sccs = FixedBitSet::with_capacity(self.sccs.len()); graph .node_identifiers() .filter(move |ix| match self.multi_map.get(ix) { Some(&scc_idx) => { // Consider one node identifier for each scc -- whichever one comes first. if external_sccs.contains(scc_idx) { return true; } if internal_sccs.contains(scc_idx) { return false; } let scc = &self.sccs[scc_idx]; let is_external = scc .iter() .flat_map(|ix| { // Look at all incoming nodes from every SCC member. graph.neighbors_directed(*ix, Incoming) }) .all(|neighbor_ix| { // * Accept any nodes are in the same SCC. // * Any other results imply that this isn't an external scc. match self.multi_map.get(&neighbor_ix) { Some(neighbor_scc_idx) => neighbor_scc_idx == &scc_idx, None => false, } }); if is_external { external_sccs.insert(scc_idx); } else { internal_sccs.insert(scc_idx); } is_external } None => { // Not part of an SCC -- just look at whether there are any incoming nodes // at all. graph.neighbors_directed(*ix, Incoming).next().is_none() } }) } /// Iterate over all nodes in the direction specified. pub fn node_iter(&self, direction: Direction) -> NodeIter<'_, Ix> { NodeIter { node_ixs: self.sccs.data().iter(), direction, } } } /// An iterator over the nodes of strongly connected components. #[derive(Clone, Debug)] pub(crate) struct NodeIter<'a, Ix> { node_ixs: slice::Iter<'a, NodeIndex>, direction: Direction, } impl NodeIter<'_, Ix> { /// Returns the direction this iteration is happening in. #[allow(dead_code)] pub fn direction(&self) -> Direction { self.direction } } impl Iterator for NodeIter<'_, Ix> { type Item = NodeIndex; fn next(&mut self) -> Option> { // Note that outgoing implies iterating over the sccs in forward order, while incoming means // sccs in reverse order. match self.direction { Direction::Outgoing => self.node_ixs.next().copied(), Direction::Incoming => self.node_ixs.next_back().copied(), } } } guppy-0.17.25/src/petgraph_support/topo.rs000064400000000000000000000133071046102023000166740ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use petgraph::{ graph::IndexType, prelude::*, visit::{ GraphRef, IntoNeighborsDirected, IntoNodeIdentifiers, NodeCompactIndexable, VisitMap, Visitable, Walker, }, }; use std::marker::PhantomData; /// A cycle-aware topological sort of a graph. #[derive(Clone, Debug)] pub struct TopoWithCycles { // This is a map of each node index to its corresponding topo index. reverse_index: Box<[usize]>, // Prevent mixing up index types. _phantom: PhantomData, } impl TopoWithCycles { pub fn new(graph: G) -> Self where G: GraphRef + Visitable> + IntoNodeIdentifiers + IntoNeighborsDirected> + NodeCompactIndexable, G::Map: VisitMap>, { // petgraph's default topo algorithms don't handle cycles. Use DfsPostOrder which does. let mut dfs = DfsPostOrder::empty(graph); let roots = graph .node_identifiers() .filter(move |&a| graph.neighbors_directed(a, Incoming).next().is_none()); dfs.stack.extend(roots); let mut topo: Vec> = (&mut dfs).iter(graph).collect(); // dfs returns its data in postorder (reverse topo order), so reverse that for forward topo // order. topo.reverse(); // Because the graph is NodeCompactIndexable, the indexes are in the range // (0..graph.node_count()). // Use this property to build a reverse map. let mut reverse_index = vec![0; graph.node_count()]; topo.iter().enumerate().for_each(|(topo_ix, node_ix)| { reverse_index[node_ix.index()] = topo_ix; }); // topo.len cannot possibly exceed graph.node_count(). assert!( topo.len() <= graph.node_count(), "topo.len() <= graph.node_count() ({} is actually > {})", topo.len(), graph.node_count(), ); if topo.len() < graph.node_count() { // This means there was a cycle in the graph which caused some nodes to be skipped (e.g. // consider a node with a self-loop -- it will be filtered out by the // graph.neighbors_directed call above, and might not end up being part of the topo // order). // // In this case, do a best-effort job: fill in the missing nodes with their reverse // index set to the end of the topo order. We could do something fancier here with sccs, // but for guppy this should never happen in practice. (In fact, the one time this code // was hit there was actually an underlying bug.) let mut next = topo.len(); for n in 0..graph.node_count() { let a = NodeIndex::new(n); if !dfs.finished.is_visited(&a) { // a is a missing index. reverse_index[a.index()] = next; next += 1; } } } Self { reverse_index: reverse_index.into_boxed_slice(), _phantom: PhantomData, } } /// Sort nodes based on the topo order in self. #[inline] pub fn sort_nodes(&self, nodes: &mut [NodeIndex]) { nodes.sort_unstable_by_key(|node_ix| self.topo_ix(*node_ix)) } #[inline] pub fn topo_ix(&self, node_ix: NodeIndex) -> usize { self.reverse_index[node_ix.index()] } } #[cfg(all(test, feature = "proptest1"))] mod proptests { use super::*; use proptest::prelude::*; proptest! { #[test] fn graph_topo_sort(graph in possibly_cyclic_graph()) { let topo = TopoWithCycles::new(&graph); let mut nodes: Vec<_> = graph.node_indices().collect(); check_consistency(&topo, graph.node_count()); topo.sort_nodes(&mut nodes); for (topo_ix, node_ix) in nodes.iter().enumerate() { assert_eq!(topo.topo_ix(*node_ix), topo_ix); } } } fn possibly_cyclic_graph() -> impl Strategy> { // Generate a graph in adjacency list form. N nodes, up to N**2 edges. (1..=100usize) .prop_flat_map(|n| { ( Just(n), prop::collection::vec(prop::collection::vec(0..n, 0..n), n), ) }) .prop_map(|(n, adj)| { let mut graph = Graph::<(), ()>::with_capacity(n, adj.iter().map(|x| x.len()).sum()); for _ in 0..n { // Add all the nodes under consideration. graph.add_node(()); } for (src, dsts) in adj.into_iter().enumerate() { let src = NodeIndex::new(src); for dst in dsts { let dst = NodeIndex::new(dst); graph.update_edge(src, dst, ()); } } graph }) } fn check_consistency(topo: &TopoWithCycles, n: usize) { // Ensure that all indexes are covered and unique. let mut seen = vec![false; n]; for i in 0..n { let topo_ix = topo.topo_ix(NodeIndex::new(i)); assert!( !seen[topo_ix], "topo_ix {topo_ix} should be seen exactly once, but seen twice" ); seen[topo_ix] = true; } for (i, &this_seen) in seen.iter().enumerate() { assert!(this_seen, "topo_ix {i} should be seen, but wasn't"); } } } guppy-0.17.25/src/petgraph_support/walk.rs000064400000000000000000000045621046102023000166540ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::petgraph_support::edge_triple; use petgraph::visit::{IntoEdges, VisitMap, Visitable, Walker}; use std::iter; #[derive(Clone, Debug)] pub(crate) struct EdgeDfs { /// The queue of (source, target, edge) to visit. pub stack: Vec<(N, N, E)>, /// The map of discovered nodes pub discovered: VM, } impl EdgeDfs where E: Copy + PartialEq, N: Copy + PartialEq, VM: VisitMap, { /// Creates a new EdgeDfs, using the graph's visitor map, and puts all edges out of `initials` /// in the queue of edges to visit. pub(crate) fn new(graph: G, initials: impl IntoIterator) -> Self where G: Visitable + IntoEdges, { let mut discovered = graph.visit_map(); let stack = initials .into_iter() .filter_map(|node_ix| { // This check ensures that if a node is repeated in initials, its edges are only // added once. if discovered.visit(node_ix) { Some(graph.edges(node_ix).map(edge_triple)) } else { None } }) .flatten() .collect(); Self { stack, discovered } } /// Creates a new EdgeDfs, using the graph's visitor map, and puts all edges out of `start` /// in the queue of edges to visit. #[allow(dead_code)] pub(crate) fn new_single(graph: G, start: N) -> Self where G: Visitable + IntoEdges, { Self::new(graph, iter::once(start)) } /// Returns the next edge in the dfs, or `None` if no more edges remain. pub fn next(&mut self, graph: G) -> Option<(N, N, E)> where G: IntoEdges, { let (source, target, edge) = self.stack.pop()?; if self.discovered.visit(target) { self.stack.extend(graph.edges(target).map(edge_triple)); } Some((source, target, edge)) } } impl Walker for EdgeDfs where G: IntoEdges + Visitable, { type Item = (G::NodeId, G::NodeId, G::EdgeId); fn walk_next(&mut self, context: G) -> Option { self.next(context) } } guppy-0.17.25/src/platform/mod.rs000064400000000000000000000054641046102023000147150ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 //! Support for dependencies that are only enabled on some platforms. //! //! Most of the time, dependencies are enabled across all platforms. For example, in this //! `Cargo.toml`: //! //! ```toml //! # once_cell 1.5 is enabled on all platforms. //! [dependencies] //! once_cell = "1.5" //! ``` //! //! However, in some cases, dependencies may only be enabled on certain platforms. //! //! ```toml //! # This dependency is only enabled on Linux x86_64. //! [target.x86_64-unknown-linux-gnu.dependencies] //! inotify = "0.9.4" //! //! # This build dependency is enabled on Windows. //! [target.'cfg(windows)'.build-dependencies] //! winapi = "0.3.9" //! ``` //! //! This module provides types that can represent platforms and evaluate expressions. //! //! # Representing platforms //! //! * [`Platform`] represents a single platform. //! * [`Triple`] is a [Rust target triple](https://doc.rust-lang.org/stable/rustc/platform-support.html). //! * [`PlatformSpec`] represents a single platform or a range of platforms, including any platform //! (the union of all possible platforms) and all platforms (the intersection of all possible //! platforms). //! //! # Evaluating platforms //! //! These structs are defined in the context of a [`PackageGraph`](crate::graph::PackageGraph), and //! are typically returned through [`PackageLink`](crate::graph::PackageLink) instances. //! //! * [`PlatformStatus`]: The status of a dependency or a feature which might be platform-dependent. //! * [`PlatformEval`]: A collection of platform specifications like `cfg(unix)`, to evaluate //! against a platform. //! * [`EnabledTernary`]: A three-valued logic representing the status of a dependency or feature //! on a given platform. Includes an additional status to represent situations like unknown //! [target features](https://rust-lang.github.io/rfcs/2045-target-feature.html). //! //! If the `summaries` feature is enabled, this module also supports reading and writing serializable //! summaries of platforms. These can be used both as configuration, and to serialize the results of a //! particular `guppy` evaluation. //! //! For more, about platform-specific dependencies, see [Platform specific //! dependencies](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#platform-specific-dependencies) //! in the Cargo reference. mod platform_eval; mod platform_spec; #[cfg(feature = "proptest1")] mod proptest_helpers; #[cfg(feature = "summaries")] mod summaries; pub use platform_eval::*; pub use platform_spec::*; #[cfg(feature = "summaries")] pub use summaries::*; // 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, Triple}; guppy-0.17.25/src/platform/platform_eval.rs000064400000000000000000000175001046102023000167630ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::platform::{Platform, PlatformSpec}; use std::ops::{BitAnd, BitOr}; use target_spec::TargetSpec; /// The status of a dependency or feature, which is possibly platform-dependent. /// /// This is a sub-status of [`EnabledStatus`](crate::graph::EnabledStatus). #[derive(Copy, Clone, Debug)] pub enum PlatformStatus<'g> { /// This dependency or feature is never enabled on any platforms. Never, /// This dependency or feature is always enabled on all platforms. Always, /// The status is platform-dependent. PlatformDependent { /// An evaluator to run queries against. eval: PlatformEval<'g>, }, } assert_covariant!(PlatformStatus); impl<'g> PlatformStatus<'g> { pub(crate) fn new(specs: &'g PlatformStatusImpl) -> Self { match specs { PlatformStatusImpl::Always => PlatformStatus::Always, PlatformStatusImpl::Specs(specs) => { if specs.is_empty() { PlatformStatus::Never } else { PlatformStatus::PlatformDependent { eval: PlatformEval { specs }, } } } } } /// Returns true if this dependency is always enabled on all platforms. pub fn is_always(&self) -> bool { match self { PlatformStatus::Always => true, PlatformStatus::PlatformDependent { .. } | PlatformStatus::Never => false, } } /// Returns true if this dependency is never enabled on any platform. pub fn is_never(&self) -> bool { match self { PlatformStatus::Never => true, PlatformStatus::PlatformDependent { .. } | PlatformStatus::Always => false, } } /// Returns true if this dependency is possibly enabled on any platform. pub fn is_present(&self) -> bool { !self.is_never() } /// Evaluates whether this dependency is enabled on the given platform spec. /// /// Returns `Unknown` if the result was unknown, which may happen if evaluating against an /// individual platform and its target features are unknown. pub fn enabled_on(&self, platform_spec: &PlatformSpec) -> EnabledTernary { match (self, platform_spec) { (PlatformStatus::Always, _) => EnabledTernary::Enabled, (PlatformStatus::Never, _) => EnabledTernary::Disabled, (PlatformStatus::PlatformDependent { .. }, PlatformSpec::Any) => { EnabledTernary::Enabled } (PlatformStatus::PlatformDependent { eval }, PlatformSpec::Platform(platform)) => { eval.eval(platform) } (PlatformStatus::PlatformDependent { .. }, PlatformSpec::Always) => { EnabledTernary::Disabled } } } } /// Whether a dependency or feature is enabled on a specific platform. /// /// This is a ternary or [three-valued logic](https://en.wikipedia.org/wiki/Three-valued_logic) /// because the result may be unknown in some situations. /// /// Returned by the methods on `EnabledStatus`, `PlatformStatus`, and `PlatformEval`. #[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum EnabledTernary { /// The dependency is disabled on this platform. Disabled, /// The status of this dependency is unknown on this platform. /// /// This may happen if evaluation involves unknown target features. Notably, /// this will not be returned for [`Platform::build_target()`], since the /// target features for the build target platform are determined at compile /// time. Unknown, /// The dependency is enabled on this platform. Enabled, } impl EnabledTernary { fn new(x: Option) -> Self { match x { Some(false) => EnabledTernary::Disabled, None => EnabledTernary::Unknown, Some(true) => EnabledTernary::Enabled, } } /// Returns true if the status is known (either enabled or disabled). pub fn is_known(self) -> bool { match self { EnabledTernary::Disabled | EnabledTernary::Enabled => true, EnabledTernary::Unknown => false, } } } /// AND operation in Kleene K3 logic. impl BitAnd for EnabledTernary { type Output = Self; fn bitand(self, rhs: Self) -> Self::Output { use EnabledTernary::*; match (self, rhs) { (Enabled, Enabled) => Enabled, (Disabled, _) | (_, Disabled) => Disabled, _ => Unknown, } } } /// OR operation in Kleene K3 logic. impl BitOr for EnabledTernary { type Output = Self; fn bitor(self, rhs: Self) -> Self { use EnabledTernary::*; match (self, rhs) { (Disabled, Disabled) => Disabled, (Enabled, _) | (_, Enabled) => Enabled, _ => Unknown, } } } /// An evaluator for platform-specific dependencies. /// /// This represents a collection of platform specifications, of the sort `cfg(unix)`. #[derive(Copy, Clone, Debug)] pub struct PlatformEval<'g> { specs: &'g [TargetSpec], } assert_covariant!(PlatformEval); impl<'g> PlatformEval<'g> { /// Runs this evaluator against the given platform. pub fn eval(&self, platform: &Platform) -> EnabledTernary { let mut res = EnabledTernary::Disabled; for spec in self.specs.iter() { let matches = spec.eval(platform); // Short-circuit evaluation if possible. if matches == Some(true) { return EnabledTernary::Enabled; } res = res | EnabledTernary::new(matches); } res } /// Returns the [`TargetSpec`] instances backing this evaluator. /// /// The result of [`PlatformEval::eval`] against a platform is a logical OR /// of the results of evaluating the platform against each target spec. pub fn target_specs(&self) -> &'g [TargetSpec] { self.specs } } #[derive(Clone, Debug)] pub(crate) enum PlatformStatusImpl { Always, // Empty vector means never. Specs(Vec), } impl PlatformStatusImpl { /// Returns true if this is an empty predicate (i.e. will never match). pub(crate) fn is_never(&self) -> bool { match self { PlatformStatusImpl::Always => false, PlatformStatusImpl::Specs(specs) => specs.is_empty(), } } pub(crate) fn extend(&mut self, other: &PlatformStatusImpl) { // &mut *self is a reborrow to allow *self to work below. match (&mut *self, other) { (PlatformStatusImpl::Always, _) => { // Always stays the same since it means all specs are included. } (PlatformStatusImpl::Specs(_), PlatformStatusImpl::Always) => { // Mark self as Always. *self = PlatformStatusImpl::Always; } (PlatformStatusImpl::Specs(specs), PlatformStatusImpl::Specs(other)) => { specs.extend_from_slice(other.as_slice()); } } } pub(crate) fn add_spec(&mut self, spec: Option<&TargetSpec>) { // &mut *self is a reborrow to allow *self to work below. match (&mut *self, spec) { (PlatformStatusImpl::Always, _) => { // Always stays the same since it means all specs are included. } (PlatformStatusImpl::Specs(_), None) => { // Mark self as Always. *self = PlatformStatusImpl::Always; } (PlatformStatusImpl::Specs(specs), Some(spec)) => { specs.push(spec.clone()); } } } } impl Default for PlatformStatusImpl { fn default() -> Self { // Empty vector means never. PlatformStatusImpl::Specs(vec![]) } } guppy-0.17.25/src/platform/platform_spec.rs000064400000000000000000000057251046102023000167740ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 #[allow(unused_imports)] use crate::platform::EnabledTernary; use crate::{errors::TargetSpecError, platform::Platform}; use std::sync::Arc; /// A specifier for a single platform, or for a range of platforms. /// /// Some uses of `guppy` care about a single platform, and others care about queries against the /// intersection of all hypothetical platforms, or against a union of any of them. `PlatformSpec` /// handles the /// /// `PlatformSpec` does not currently support expressions, but it might in the future, using an /// [SMT solver](https://en.wikipedia.org/wiki/Satisfiability_modulo_theories). #[derive(Clone, Debug)] #[non_exhaustive] pub enum PlatformSpec { /// The intersection of all platforms. /// /// Dependency queries performed against this variant will return [`EnabledTernary::Enabled`] if /// and only if a dependency is not platform-dependent. They can never return /// [`EnabledTernary::Unknown`]. /// /// This variant does not currently understand expressions that always evaluate to true /// (tautologies), like `cfg(any(unix, not(unix)))` or `cfg(all())`. In the future, an SMT /// solver would be able to handle such expressions. Always, /// An individual platform. /// /// Dependency queries performed against this variant will return [`EnabledTernary::Enabled`] if /// and only if a dependency is enabled on this platform. They may also return /// [`EnabledTernary::Unknown`] if a platform is not enabled. Platform(Arc), /// The union of all platforms. /// /// Dependency queries performed against this variant will return [`EnabledTernary::Enabled`] if /// a dependency is enabled on any platform. /// /// This variant does not currently understand expressions that always evaluate to false /// (contradictions), like `cfg(all(unix, not(unix)))` or `cfg(any())`. In the future, an SMT /// solver would be able to handle such expressions. Any, } impl PlatformSpec { /// Previous name for [`Self::build_target`], renamed to clarify what /// `current` means. /// /// This method is deprecated and will be removed in a future version. #[deprecated( since = "0.17.13", note = "this method has been renamed to `build_target`" )] #[inline] pub fn current() -> Result { Self::build_target() } /// Returns a `PlatformSpec` corresponding to the target platform, as /// determined at build time. /// /// Returns an error if the build target was unknown to the version of /// `target-spec` in use. pub fn build_target() -> Result { Ok(PlatformSpec::Platform(Arc::new(Platform::build_target()?))) } } impl>> From for PlatformSpec { #[inline] fn from(platform: T) -> Self { PlatformSpec::Platform(platform.into()) } } guppy-0.17.25/src/platform/proptest_helpers.rs000064400000000000000000000020651046102023000175320ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::platform::{Platform, PlatformSpec, TargetFeatures}; use proptest::prelude::*; /// # Helpers for property testing /// /// The methods in this section allow a `PlatformSpec` to be used in property-based testing /// scenarios. /// /// Currently, [proptest 1](https://docs.rs/proptest/1) is supported if the `proptest1` /// feature is enabled. impl PlatformSpec { /// Returns a [`Strategy`] that generates a random `PlatformSpec` instance. pub fn strategy(platform: impl Strategy) -> impl Strategy { prop_oneof![ 1 => Just(PlatformSpec::Any), 1 => Just(PlatformSpec::Always), 2 => platform.prop_map(PlatformSpec::from), ] } } impl Arbitrary for PlatformSpec { type Parameters = (); type Strategy = BoxedStrategy; fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { Self::strategy(Platform::strategy(any::())).boxed() } } guppy-0.17.25/src/platform/summaries.rs000064400000000000000000000221341046102023000161340ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::{errors::TargetSpecError, platform::PlatformSpec}; use std::sync::Arc; pub use target_spec::summaries::{PlatformSummary, TargetFeaturesSummary}; /// A serializable version of [`PlatformSpec`]. /// /// Requires the `summaries` feature to be enabled. #[derive(Clone, Debug, Eq, PartialEq, Default)] pub enum PlatformSpecSummary { /// The intersection of all platforms. /// /// This is converted to and from [`PlatformSpec::Always`], and is expressed as the string /// `"always"`, or as `spec = "always"`. /// /// # Examples /// /// Deserialize the string `"always"`. /// /// ``` /// # use guppy::platform::PlatformSpecSummary; /// let spec: PlatformSpecSummary = serde_json::from_str(r#""always""#).unwrap(); /// assert_eq!(spec, PlatformSpecSummary::Always); /// ``` /// /// Deserialize `spec = "always"`. /// /// ``` /// # use guppy::platform::PlatformSpecSummary; /// let spec: PlatformSpecSummary = toml::from_str(r#"spec = "always""#).unwrap(); /// assert_eq!(spec, PlatformSpecSummary::Always); /// ``` Always, /// An individual platform. /// /// This is converted to and from [`PlatformSpec::Platform`], and is serialized as the platform /// itself (either a triple string, or a map such as /// `{ triple = "x86_64-unknown-linux-gnu", target-features = [] }`). /// /// # Examples /// /// Deserialize a target triple. /// /// ``` /// # use guppy::platform::{PlatformSummary, PlatformSpecSummary}; /// # use target_spec::summaries::TargetFeaturesSummary; /// # use std::collections::BTreeSet; /// let spec: PlatformSpecSummary = serde_json::from_str(r#""x86_64-unknown-linux-gnu""#).unwrap(); /// assert_eq!( /// spec, /// PlatformSpecSummary::Platform(PlatformSummary::new("x86_64-unknown-linux-gnu")), /// ); /// ``` /// /// Deserialize a target map. /// /// ``` /// # use guppy::platform::{PlatformSummary, PlatformSpecSummary}; /// # use target_spec::summaries::TargetFeaturesSummary; /// # use std::collections::BTreeSet; /// let spec: PlatformSpecSummary = toml::from_str(r#" /// triple = "x86_64-unknown-linux-gnu" /// target-features = [] /// flags = [] /// "#).unwrap(); /// assert_eq!( /// spec, /// PlatformSpecSummary::Platform( /// PlatformSummary::new("x86_64-unknown-linux-gnu") /// .with_target_features(TargetFeaturesSummary::Features(BTreeSet::new())) /// ) /// ); /// ``` Platform(PlatformSummary), /// The union of all platforms. /// /// This is converted to and from [`PlatformSpec::Any`], and is serialized as the string /// `"any"`. /// /// This is also the default, since in many cases one desires to compute the union of enabled /// dependencies across all platforms. /// /// # Examples /// /// Deserialize the string `"any"`. /// /// ``` /// # use guppy::platform::PlatformSpecSummary; /// let spec: PlatformSpecSummary = serde_json::from_str(r#""any""#).unwrap(); /// assert_eq!(spec, PlatformSpecSummary::Any); /// ``` /// /// Deserialize `spec = "any"`. /// /// ``` /// # use guppy::platform::PlatformSpecSummary; /// let spec: PlatformSpecSummary = toml::from_str(r#"spec = "any""#).unwrap(); /// assert_eq!(spec, PlatformSpecSummary::Any); /// ``` #[default] Any, } impl PlatformSpecSummary { /// Creates a new `PlatformSpecSummary` from a [`PlatformSpec`]. pub fn new(platform_spec: &PlatformSpec) -> Self { match platform_spec { PlatformSpec::Always => PlatformSpecSummary::Always, PlatformSpec::Platform(platform) => { PlatformSpecSummary::Platform(platform.to_summary()) } PlatformSpec::Any => PlatformSpecSummary::Any, } } /// Converts `self` to a `PlatformSpec`. /// /// Returns an `Error` if the platform was unknown. pub fn to_platform_spec(&self) -> Result { match self { PlatformSpecSummary::Always => Ok(PlatformSpec::Always), PlatformSpecSummary::Platform(platform) => { Ok(PlatformSpec::Platform(Arc::new(platform.to_platform()?))) } PlatformSpecSummary::Any => Ok(PlatformSpec::Any), } } /// Returns true if `self` is `PlatformSpecSummary::Any`. pub fn is_any(&self) -> bool { matches!(self, PlatformSpecSummary::Any) } } mod serde_impl { use super::*; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::collections::BTreeSet; use target_spec::summaries::TargetFeaturesSummary; impl Serialize for PlatformSpecSummary { fn serialize(&self, serializer: S) -> Result where S: Serializer, { match self { PlatformSpecSummary::Always => Spec { spec: "always" }.serialize(serializer), PlatformSpecSummary::Any => Spec { spec: "any" }.serialize(serializer), PlatformSpecSummary::Platform(platform) => platform.serialize(serializer), } } } // Ideally we'd serialize always or any as just those strings, but that runs into ValueAfterTable // issues with toml. So serialize always/any as "spec = always" etc. #[derive(Serialize)] struct Spec { spec: &'static str, } impl<'de> Deserialize<'de> for PlatformSpecSummary { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { match PlatformSpecSummaryDeserialize::deserialize(deserializer)? { PlatformSpecSummaryDeserialize::String(spec) | PlatformSpecSummaryDeserialize::Spec { spec } => { match spec.as_str() { "always" => Ok(PlatformSpecSummary::Always), "any" => Ok(PlatformSpecSummary::Any), _ => { // TODO: expression parsing would go here Ok(PlatformSpecSummary::Platform(PlatformSummary::new(spec))) } } } PlatformSpecSummaryDeserialize::PlatformFull { triple, custom_json, target_features, flags, } => { let mut summary = PlatformSummary::new(triple); summary.custom_json = custom_json; summary.target_features = target_features; summary.flags = flags; Ok(PlatformSpecSummary::Platform(summary)) } } } } #[derive(Deserialize)] #[serde(untagged)] enum PlatformSpecSummaryDeserialize { String(String), Spec { spec: String, }, #[serde(rename_all = "kebab-case")] PlatformFull { // TODO: there doesn't appear to be any way to defer to the PlatformSummary // deserializer, so copy-paste its logic here. Find a better way? triple: String, #[serde(skip_serializing_if = "Option::is_none", default)] custom_json: Option, #[serde(default)] target_features: TargetFeaturesSummary, #[serde(skip_serializing_if = "BTreeSet::is_empty", default)] flags: BTreeSet, }, } } #[cfg(all(test, feature = "proptest1"))] mod proptests { use super::*; use proptest::prelude::*; use std::collections::HashSet; proptest! { #[test] fn summary_roundtrip(platform_spec in any::()) { let summary = PlatformSpecSummary::new(&platform_spec); let serialized = toml::ser::to_string(&summary).expect("serialization succeeded"); let deserialized: PlatformSpecSummary = toml::from_str(&serialized).expect("deserialization succeeded"); assert_eq!(summary, deserialized, "summary and deserialized should match"); let platform_spec2 = deserialized.to_platform_spec().expect("conversion to PlatformSpec succeeded"); match (platform_spec, platform_spec2) { (PlatformSpec::Any, PlatformSpec::Any) | (PlatformSpec::Always, PlatformSpec::Always) => {}, (PlatformSpec::Platform(platform), PlatformSpec::Platform(platform2)) => { assert_eq!(platform.triple_str(), platform2.triple_str(), "triples match"); assert_eq!(platform.target_features(), platform2.target_features(), "target features match"); assert_eq!(platform.flags().collect::>(), platform2.flags().collect::>(), "flags match"); } (other, other2) => panic!("platform specs do not match: original: {other:?}, roundtrip: {other2:?}"), } } } } guppy-0.17.25/src/sorted_set.rs000064400000000000000000000030371046102023000144570ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use std::{fmt, iter::FromIterator, ops::Deref}; /// An immutable set stored as a sorted vector. #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct SortedSet { inner: Box<[T]>, } type _SortedSetCovariant<'a> = SortedSet<&'a ()>; assert_covariant!(_SortedSetCovariant); impl SortedSet where T: Ord, { /// Creates a new `SortedSet` from a vector or other slice container. pub fn new(v: impl Into>) -> Self { let mut v = v.into(); v.sort(); v.dedup(); Self { inner: v.into() } } // TODO: new + sort by/sort by key? /// Returns true if this sorted vector contains this element. pub fn contains(&self, item: &T) -> bool { self.binary_search(item).is_ok() } /// Returns the data as a slice. pub fn as_slice(&self) -> &[T] { &self.inner } /// Returns the inner data. pub fn into_inner(self) -> Box<[T]> { self.inner } } impl FromIterator for SortedSet where T: Ord, { fn from_iter>(iter: I) -> Self { let v: Vec = iter.into_iter().collect(); Self::new(v) } } impl Deref for SortedSet { type Target = [T]; fn deref(&self) -> &Self::Target { &self.inner } } impl fmt::Display for SortedSet { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{{{}}}", self.as_slice().join(", ")) } } guppy-0.17.25/src/unit_tests/dot_tests.rs000064400000000000000000000044701046102023000165170ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::petgraph_support::dot::{DisplayVisitor, DotFmt, DotVisitor, DotWrite}; use petgraph::{ prelude::*, visit::{EdgeRef, NodeRef}, }; use std::fmt; #[test] fn dot_fmt() { let mut graph = Graph::new(); let a = graph.add_node("A"); // " is escaped. let b = graph.add_node(r#"B1"B2"#); // \ is escaped by DisplayVisitor but not by NoEscapeDisplayVisitor. let c = graph.add_node(r"C1\C2\\C3\lC4\nC5"); // Newlines are converted into \l. let d = graph.add_node("D1\nD2"); graph.add_edge(a, b, 100); graph.add_edge(a, c, 200); graph.add_edge(b, d, 300); graph.add_edge(c, d, 400); let dot_fmt = DotFmt::new(&graph, DisplayVisitor); let output = format!("{dot_fmt}"); static EXPECTED_DOT: &str = r#"digraph { 0 [label="A"] 1 [label="B1\"B2"] 2 [label="C1\\C2\\\\C3\\lC4\\nC5"] 3 [label="D1\lD2"] 0 -> 1 [label="100"] 0 -> 2 [label="200"] 1 -> 3 [label="300"] 2 -> 3 [label="400"] } "#; assert_eq!(&output, EXPECTED_DOT, "dot output matches"); let no_escape_dot_fmt = DotFmt::new(&graph, NoEscapeDisplayVisitor); let output = format!("{no_escape_dot_fmt}"); static EXPECTED_DOT_NO_ESCAPE: &str = r#"digraph { 0 [label="A"] 1 [label="B1\"B2"] 2 [label="C1\C2\\C3\lC4\nC5"] 3 [label="D1\lD2"] 0 -> 1 [label="100"] 0 -> 2 [label="200"] 1 -> 3 [label="300"] 2 -> 3 [label="400"] } "#; assert_eq!( &output, EXPECTED_DOT_NO_ESCAPE, "dot output matches (backslashes not escaped)" ); } /// A visitor for formatting graph labels that outputs `fmt::Display` impls for node and edge /// weights. /// /// This visitor does not escape backslashes. #[derive(Copy, Clone, Debug)] pub struct NoEscapeDisplayVisitor; impl DotVisitor for NoEscapeDisplayVisitor where NR: NodeRef, ER: EdgeRef, NR::Weight: fmt::Display, ER::Weight: fmt::Display, { fn visit_node(&self, node: NR, f: &mut DotWrite<'_, '_>) -> fmt::Result { f.set_escape_backslashes(false); write!(f, "{}", node.weight()) } fn visit_edge(&self, edge: ER, f: &mut DotWrite<'_, '_>) -> fmt::Result { f.set_escape_backslashes(false); write!(f, "{}", edge.weight()) } } guppy-0.17.25/src/unit_tests/mod.rs000064400000000000000000000001541046102023000152610ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 mod dot_tests; guppy-0.17.25/tests/graph-tests/cargo_set_tests.rs000064400000000000000000000361221046102023000203110ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use fixtures::json::JsonFixture; use guppy::graph::{ DependencyDirection, PackageLink, PackageQuery, PackageResolver, cargo::{CargoOptions, CargoSet}, feature::StandardFeatures, }; use std::collections::HashSet; struct PackageResolverForTesting<'a, 'g> { /// Optional filter of `link`s. If `None`, then all links are accepted. link_filter: Option<&'a dyn Fn(PackageLink<'g>) -> bool>, /// The `trace` field stores `link`s that were passed to `fn accept`. /// The links are formatted as `"foo@1.2.3 => bar@4.5.6"`. /// The links are stored in the order of `fn accept` calls. trace: Vec, } impl<'a, 'g> PackageResolverForTesting<'a, 'g> { fn new() -> Self { Self { link_filter: None, trace: vec![], } } fn with_filter(f: &'a impl Fn(PackageLink<'g>) -> bool) -> Self { Self { link_filter: Some(f), trace: vec![], } } } fn link_to_string(link: &PackageLink) -> String { format!( "{}@{} => {}@{}", link.from().name(), link.from().version(), link.to().name(), link.to().version(), ) } fn links_to_strings<'g>(links: impl IntoIterator>) -> Vec { let mut result = links .into_iter() .map(|link| link_to_string(&link)) .collect::>(); result.sort(); result } impl<'g> PackageResolver<'g> for PackageResolverForTesting<'_, 'g> { fn accept(&mut self, _query: &PackageQuery<'g>, link: PackageLink<'g>) -> bool { self.trace.push(link_to_string(&link)); self.link_filter.map(|f| f(link)).unwrap_or(true) } } fn cargo_set_with_resolver<'g>( test_fixture: &'g JsonFixture, root_package_name: &str, resolver: &mut dyn PackageResolver<'g>, ) -> CargoSet<'g> { let package_graph = test_fixture.graph(); let initials = package_graph .resolve_package_name(root_package_name) .to_feature_set(StandardFeatures::Default); let no_extra_features = package_graph .resolve_none() .to_feature_set(StandardFeatures::Default); let cargo_options = CargoOptions::new(); CargoSet::with_package_resolver(initials, no_extra_features, resolver, &cargo_options).unwrap() } fn cargo_set_package_names(cargo_set: &CargoSet) -> Vec { let mut result = cargo_set .target_features() .union(cargo_set.host_features()) .packages_with_features(DependencyDirection::Forward) .map(|feature_list| feature_list.package().name().to_string()) .collect::>(); result.sort(); result } #[test] fn test_package_resolver_visits() { let mut resolver = PackageResolverForTesting::new(); let cargo_set = cargo_set_with_resolver(JsonFixture::metadata1(), "testcrate", &mut resolver); assert_eq!( cargo_set_package_names(&cargo_set), vec![ "aho-corasick", "bitflags", "ctor", "datatest", "datatest-derive", "dtoa", "lazy_static", "libc", "linked-hash-map", "mach", "memchr", "proc-macro2", "quote", "regex", "regex-syntax", "region", "same-file", "serde", "serde_yaml", "syn", "testcrate", "thread_local", "unicode-xid", "version_check", "walkdir", "winapi", "winapi-i686-pc-windows-gnu", "winapi-util", "winapi-x86_64-pc-windows-gnu", "yaml-rust", ], ); assert_eq!( resolver.trace, vec![ "testcrate@0.1.0 => datatest@0.4.2", "datatest@0.4.2 => yaml-rust@0.4.3", "datatest@0.4.2 => walkdir@2.2.9", "datatest@0.4.2 => version_check@0.9.1", "datatest@0.4.2 => serde_yaml@0.8.9", "datatest@0.4.2 => serde@1.0.100", "datatest@0.4.2 => region@2.1.2", "datatest@0.4.2 => regex@1.3.1", "datatest@0.4.2 => datatest-derive@0.4.0", "datatest@0.4.2 => ctor@0.1.10", "regex@1.3.1 => thread_local@0.3.6", "regex@1.3.1 => regex-syntax@0.6.12", "regex@1.3.1 => memchr@2.2.1", "regex@1.3.1 => aho-corasick@0.7.6", "aho-corasick@0.7.6 => memchr@2.2.1", "thread_local@0.3.6 => lazy_static@1.4.0", "region@2.1.2 => winapi@0.3.8", "region@2.1.2 => mach@0.2.3", "region@2.1.2 => libc@0.2.62", "region@2.1.2 => bitflags@1.1.0", "mach@0.2.3 => libc@0.2.62", "winapi@0.3.8 => winapi-x86_64-pc-windows-gnu@0.4.0", "winapi@0.3.8 => winapi-i686-pc-windows-gnu@0.4.0", "serde_yaml@0.8.9 => yaml-rust@0.4.3", "serde_yaml@0.8.9 => serde@1.0.100", "serde_yaml@0.8.9 => linked-hash-map@0.5.2", "serde_yaml@0.8.9 => dtoa@0.4.4", "yaml-rust@0.4.3 => linked-hash-map@0.5.2", "walkdir@2.2.9 => winapi-util@0.1.2", "walkdir@2.2.9 => winapi@0.3.8", "walkdir@2.2.9 => same-file@1.0.5", "same-file@1.0.5 => winapi-util@0.1.2", "winapi-util@0.1.2 => winapi@0.3.8", "ctor@0.1.10 => syn@1.0.5", "ctor@0.1.10 => quote@1.0.2", "quote@1.0.2 => proc-macro2@1.0.3", "proc-macro2@1.0.3 => unicode-xid@0.2.0", "syn@1.0.5 => unicode-xid@0.2.0", "syn@1.0.5 => quote@1.0.2", "syn@1.0.5 => proc-macro2@1.0.3", "datatest-derive@0.4.0 => syn@1.0.5", "datatest-derive@0.4.0 => quote@1.0.2", "datatest-derive@0.4.0 => proc-macro2@1.0.3", ], ); // In this test input none of the links are trimmed by cargo algorithm. let count_of_links_visited_by_resolver = resolver.trace.len(); let count_of_cargo_set_links = cargo_set.proc_macro_links().count() + cargo_set.build_dep_links().count() + cargo_set.target_links().count() + cargo_set.host_links().count(); assert_eq!(count_of_links_visited_by_resolver, count_of_cargo_set_links); assert_eq!( links_to_strings(cargo_set.proc_macro_links()), vec![ "datatest@0.4.2 => ctor@0.1.10", "datatest@0.4.2 => datatest-derive@0.4.0", ], ); assert_eq!( links_to_strings(cargo_set.build_dep_links()), vec!["datatest@0.4.2 => version_check@0.9.1",], ); assert_eq!( links_to_strings(cargo_set.target_links()), vec![ "aho-corasick@0.7.6 => memchr@2.2.1", "datatest@0.4.2 => regex@1.3.1", "datatest@0.4.2 => region@2.1.2", "datatest@0.4.2 => serde@1.0.100", "datatest@0.4.2 => serde_yaml@0.8.9", "datatest@0.4.2 => walkdir@2.2.9", "datatest@0.4.2 => yaml-rust@0.4.3", "mach@0.2.3 => libc@0.2.62", "regex@1.3.1 => aho-corasick@0.7.6", "regex@1.3.1 => memchr@2.2.1", "regex@1.3.1 => regex-syntax@0.6.12", "regex@1.3.1 => thread_local@0.3.6", "region@2.1.2 => bitflags@1.1.0", "region@2.1.2 => libc@0.2.62", "region@2.1.2 => mach@0.2.3", "region@2.1.2 => winapi@0.3.8", "same-file@1.0.5 => winapi-util@0.1.2", "serde_yaml@0.8.9 => dtoa@0.4.4", "serde_yaml@0.8.9 => linked-hash-map@0.5.2", "serde_yaml@0.8.9 => serde@1.0.100", "serde_yaml@0.8.9 => yaml-rust@0.4.3", "testcrate@0.1.0 => datatest@0.4.2", "thread_local@0.3.6 => lazy_static@1.4.0", "walkdir@2.2.9 => same-file@1.0.5", "walkdir@2.2.9 => winapi-util@0.1.2", "walkdir@2.2.9 => winapi@0.3.8", "winapi-util@0.1.2 => winapi@0.3.8", "winapi@0.3.8 => winapi-i686-pc-windows-gnu@0.4.0", "winapi@0.3.8 => winapi-x86_64-pc-windows-gnu@0.4.0", "yaml-rust@0.4.3 => linked-hash-map@0.5.2", ], ); assert_eq!( links_to_strings(cargo_set.host_links()), vec![ "ctor@0.1.10 => quote@1.0.2", "ctor@0.1.10 => syn@1.0.5", "datatest-derive@0.4.0 => proc-macro2@1.0.3", "datatest-derive@0.4.0 => quote@1.0.2", "datatest-derive@0.4.0 => syn@1.0.5", "proc-macro2@1.0.3 => unicode-xid@0.2.0", "quote@1.0.2 => proc-macro2@1.0.3", "syn@1.0.5 => proc-macro2@1.0.3", "syn@1.0.5 => quote@1.0.2", "syn@1.0.5 => unicode-xid@0.2.0", ], ); } #[test] fn test_package_resolver_filtering_normal_links_on_target() { let mut resolver = PackageResolverForTesting::with_filter(&|link| { // Remove `winapi` and `winapu-util` links. This should transitively remove `winapi => // winapi-x86_64-pc-windows-gnu` and `winapi => winapi-i686-pc-windows-gnu`. // // This filter is meant to test whether `CargoSet` algotithm consults the `resolver` // in all required cases. The filter may or may not make sense in practice (here we // can pretend that we are filtering all packages that are only needed on Windows). !link.to().name().starts_with("winapi") }); let cargo_set = cargo_set_with_resolver(JsonFixture::metadata1(), "testcrate", &mut resolver); // No `winapi...` packages (unlike in `test_package_resolver_visits`). let package_names = cargo_set_package_names(&cargo_set) .into_iter() .collect::>(); assert!(!package_names.contains("winapi")); assert!(!package_names.contains("winapi-util")); // No `winapi...` => ... links (unlike in `test_package_resolver_visits`). let trace = resolver.trace.into_iter().collect::>(); assert!(!trace.contains("winapi@0.3.8 => winapi-x86_64-pc-windows-gnu@0.4.0")); assert!(!trace.contains("winapi@0.3.8 => winapi-i686-pc-windows-gnu@0.4.0")); assert!(!trace.contains("winapi-util@0.1.2 => winapi@0.3.8")); // The resolver was asked about these links, but didn't `accept` them. // Therefore these links should be present in the `trace`, but missing from // the final `cargo_set`. let cargo_set_links = links_to_strings(cargo_set.target_links()) .into_iter() .collect::>(); assert!(!cargo_set_links.contains("walkdir@2.2.9 => winapi@0.3.8")); assert!(!cargo_set_links.contains("region@2.1.2 => winapi@0.3.8")); assert!(!cargo_set_links.contains("same-file@1.0.5 => winapi-util@0.1.2")); assert!(trace.contains("walkdir@2.2.9 => winapi@0.3.8")); assert!(trace.contains("region@2.1.2 => winapi@0.3.8")); assert!(trace.contains("same-file@1.0.5 => winapi-util@0.1.2")); } #[test] fn test_package_resolver_filtering_build_links_on_target() { let mut resolver = PackageResolverForTesting::with_filter(&|link| { // Remove `datatest` => `version_check` build dependency. // // This filter is meant to test whether `CargoSet` algotithm consults the `resolver` // in all required cases. The filter may or may not make sense in practice (here // the trimmed down graph would fail to build...). link.to().name() != "version_check" }); let cargo_set = cargo_set_with_resolver(JsonFixture::metadata1(), "testcrate", &mut resolver); // No `version_check...` packages (unlike in `test_package_resolver_visits`). let package_names = cargo_set_package_names(&cargo_set) .into_iter() .collect::>(); assert!(!package_names.contains("version_check")); // If `version_check` has transitive dependencies, then we would test here that // they were not visited/consulted by the `resolver`. // The resolver was asked about these links, but didn't `accept` them. // Therefore these links should be present in the `trace`, but missing from // the final `cargo_set`. let trace = resolver.trace.into_iter().collect::>(); let cargo_set_links = links_to_strings(cargo_set.build_dep_links()) .into_iter() .collect::>(); assert!(!cargo_set_links.contains("datatest@0.4.2 => version_check@0.9.1")); dbg!(&trace); assert!(trace.contains("datatest@0.4.2 => version_check@0.9.1")); } #[test] fn test_package_resolver_filtering_links_on_host() { let mut resolver = PackageResolverForTesting::with_filter(&|link| { // Remove dependencies of `ctor` and `datatest-derive` packages. This should transitively // remove `proc-macro2`, `quote`, `syn`, and `unicode-xid` packages. // // This filter is meant to test whether `CargoSet` algotithm consults the `resolver` // in all required cases. The filter may or may not make sense in practice (here // the trimmed down graph would fail to build...). link.from().name() != "ctor" && link.from().name() != "datatest-derive" }); let cargo_set = cargo_set_with_resolver(JsonFixture::metadata1(), "testcrate", &mut resolver); // No `ctor` not `datatest-derive` dependencies (unlike in `test_package_resolver_visits`). let package_names = cargo_set_package_names(&cargo_set) .into_iter() .collect::>(); assert!(!package_names.contains("proc-macro2")); assert!(!package_names.contains("quote")); assert!(!package_names.contains("syn")); assert!(!package_names.contains("unicode-xid")); // No `syn` => ... links (unlike in `test_package_resolver_visits`). // No `quote` => ... links (unlike in `test_package_resolver_visits`). // No `proc-macro2` ... => links (unlike in `test_package_resolver_visits`). let trace = resolver.trace.into_iter().collect::>(); assert!(!trace.contains("syn@1.0.5 => unicode-xid@0.2.0")); assert!(!trace.contains("syn@1.0.5 => quote@1.0.2")); assert!(!trace.contains("syn@1.0.5 => proc-macro2@1.0.3")); assert!(!trace.contains("quote@1.0.2 => proc-macro2@1.0.3")); assert!(!trace.contains("proc-macro2@1.0.3 => unicode-xid@0.2.0")); // The resolver was asked about these links, but didn't `accept` them. // Therefore these links should be present in the `trace`, but missing from // the final `cargo_set`. let cargo_set_links = links_to_strings(cargo_set.host_links()) .into_iter() .collect::>(); assert!(!cargo_set_links.contains("ctor@0.1.10 => syn@1.0.5")); assert!(!cargo_set_links.contains("ctor@0.1.10 => quote@1.0.2")); assert!(!cargo_set_links.contains("datatest-derive@0.4.0 => syn@1.0.5")); assert!(!cargo_set_links.contains("datatest-derive@0.4.0 => quote@1.0.2")); assert!(!cargo_set_links.contains("datatest-derive@0.4.0 => proc-macro2@1.0.3")); assert!(trace.contains("ctor@0.1.10 => syn@1.0.5")); assert!(trace.contains("ctor@0.1.10 => quote@1.0.2")); assert!(trace.contains("datatest-derive@0.4.0 => syn@1.0.5")); assert!(trace.contains("datatest-derive@0.4.0 => quote@1.0.2")); assert!(trace.contains("datatest-derive@0.4.0 => proc-macro2@1.0.3")); } guppy-0.17.25/tests/graph-tests/feature_helpers.rs000064400000000000000000000011401046102023000202660ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use guppy::{ PackageId, graph::feature::{FeatureLabel, FeatureSet}, }; pub(super) fn assert_features_for_package( feature_set: &FeatureSet<'_>, package_id: &PackageId, expected: Option<&[FeatureLabel<'_>]>, msg: &str, ) { let actual = feature_set .features_for(package_id) .expect("valid package ID"); assert_eq!( actual.as_ref().map(|list| list.labels()), expected, "{msg}: for package {package_id}, features in feature set match" ); } guppy-0.17.25/tests/graph-tests/graph_tests.rs000064400000000000000000000521321046102023000174430ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use fixtures::{ json::{self, JsonFixture}, package_id, }; use guppy::graph::{ BuildTargetId, BuildTargetKind, DependencyDirection, DotWrite, PackageDotVisitor, PackageLink, PackageMetadata, feature::{FeatureId, FeatureLabel, StandardFeatures, named_feature_filter}, }; use std::{fmt, iter}; mod small { use super::*; use crate::feature_helpers::assert_features_for_package; use fixtures::json::METADATA_CYCLE_FEATURES_BASE; use guppy::graph::PackageGraph; use pretty_assertions::assert_eq; // Test workspace_default_members field. #[test] fn metadata_default_members() { let graph = PackageGraph::from_json(include_str!( "../../../fixtures/small/metadata_default_members.json" )) .expect("valid metadata"); let workspace = graph.workspace(); let default_members: Vec<_> = workspace.default_members().collect(); assert_eq!(default_members.len(), 1, "workspace has one default member"); assert_eq!( default_members[0].name(), "testcrate", "default member is testcrate" ); // Test that default_member_ids also works. let default_member_ids: Vec<_> = workspace.default_member_ids().collect(); assert_eq!( default_member_ids.len(), 1, "workspace has one default member ID" ); } // Test specific details extracted from metadata1.json. #[test] fn metadata1() { let metadata1 = JsonFixture::metadata1(); metadata1.verify(); let graph = metadata1.graph(); assert_eq!( graph.workspace().target_directory(), "/fakepath/testcrate/target", "target directory matches" ); let testcrate = graph .metadata(&package_id(json::METADATA1_TESTCRATE)) .expect("root crate should exist"); let mut root_deps: Vec<_> = testcrate.direct_links().collect(); assert_eq!(root_deps.len(), 1, "the root crate has one dependency"); let link = root_deps.pop().expect("the root crate has one dependency"); // XXX test for details of dependency edges as well? assert!(link.normal().is_present(), "normal dependency is defined"); assert!(link.build().is_present(), "build dependency is defined"); assert!(link.dev().is_present(), "dev dependency is defined"); // Print out dot graphs for small subgraphs. static EXPECTED_DOT: &str = r#"digraph { 0 [label="winapi-x86_64-pc-windows-gnu"] 11 [label="mach"] 13 [label="winapi"] 14 [label="libc"] 20 [label="winapi-i686-pc-windows-gnu"] 26 [label="region"] 31 [label="bitflags"] 11 -> 14 [label="libc"] 13 -> 20 [label="winapi-i686-pc-windows-gnu"] 13 -> 0 [label="winapi-x86_64-pc-windows-gnu"] 26 -> 31 [label="bitflags"] 26 -> 14 [label="libc"] 26 -> 11 [label="mach"] 26 -> 13 [label="winapi"] } "#; let package_set = graph .query_forward(iter::once(&package_id(json::METADATA1_REGION))) .unwrap() .resolve(); assert_eq!( EXPECTED_DOT, format!("{}", package_set.display_dot(NameVisitor)), "dot output matches" ); // For reverse reachable ensure that the arrows are in the correct direction. static EXPECTED_DOT_REVERSED: &str = r#"digraph { 1 [label="datatest"] 9 [label="serde_yaml"] 15 [label="dtoa"] 18 [label="testcrate"] 1 -> 9 [label="serde_yaml"] 9 -> 15 [label="dtoa"] 18 -> 1 [label="datatest"] } "#; let package_set = graph .query_reverse(iter::once(&package_id(json::METADATA1_DTOA))) .unwrap() .resolve(); assert_eq!( EXPECTED_DOT_REVERSED, format!("{}", package_set.display_dot(NameVisitor)), "reversed dot output matches" ); // --- // Check that resolve_with works by dropping all edges into libc (compare to example above). static EXPECTED_DOT_NO_LIBC: &str = r#"digraph { 0 [label="winapi-x86_64-pc-windows-gnu"] 11 [label="mach"] 13 [label="winapi"] 20 [label="winapi-i686-pc-windows-gnu"] 26 [label="region"] 31 [label="bitflags"] 13 -> 20 [label="winapi-i686-pc-windows-gnu"] 13 -> 0 [label="winapi-x86_64-pc-windows-gnu"] 26 -> 31 [label="bitflags"] 26 -> 11 [label="mach"] 26 -> 13 [label="winapi"] } "#; let package_set = graph .query_forward(iter::once(&package_id(json::METADATA1_REGION))) .unwrap() .resolve_with_fn(|_, link| link.to().name() != "libc"); assert_eq!( EXPECTED_DOT_NO_LIBC, format!("{}", package_set.display_dot(NameVisitor)), "dot output matches" ); // --- let feature_graph = graph.feature_graph(); assert_eq!(feature_graph.feature_count(), 506, "feature count"); assert_eq!(feature_graph.link_count(), 631, "link count"); let feature_set = feature_graph .query_workspace(StandardFeatures::All) .resolve(); let root_ids: Vec<_> = feature_set.root_ids(DependencyDirection::Forward).collect(); let testcrate_id = package_id(json::METADATA1_TESTCRATE); let expected = vec![FeatureId::named(&testcrate_id, "datatest")]; assert_eq!(root_ids, expected, "feature graph root IDs match"); } proptest_suite!(metadata1); #[test] fn metadata2() { let metadata2 = JsonFixture::metadata2(); metadata2.verify(); let feature_graph = metadata2.graph().feature_graph(); assert_eq!(feature_graph.feature_count(), 484, "feature count"); assert_eq!(feature_graph.link_count(), 589, "link count"); let root_ids: Vec<_> = feature_graph .query_workspace(StandardFeatures::None) .resolve() .root_ids(DependencyDirection::Forward) .collect(); let testcrate_id = package_id(json::METADATA2_TESTCRATE); let expected = vec![FeatureId::base(&testcrate_id)]; assert_eq!(root_ids, expected, "feature graph root IDs match"); } proptest_suite!(metadata2); #[test] fn metadata_dups() { let metadata_dups = JsonFixture::metadata_dups(); metadata_dups.verify(); } proptest_suite!(metadata_dups); #[test] fn metadata_cycle1() { let metadata_cycle1 = JsonFixture::metadata_cycle1(); metadata_cycle1.verify(); } proptest_suite!(metadata_cycle1); #[test] fn metadata_cycle2() { let metadata_cycle2 = JsonFixture::metadata_cycle2(); metadata_cycle2.verify(); } proptest_suite!(metadata_cycle2); #[test] fn metadata_cycle_features() { let metadata_cycle_features = JsonFixture::metadata_cycle_features(); metadata_cycle_features.verify(); let feature_graph = metadata_cycle_features.graph().feature_graph(); let base_id = package_id(METADATA_CYCLE_FEATURES_BASE); let default_id = FeatureId::named(&base_id, "default"); // default, default-enable and default-transitive are default features. for &f in &["default", "default-enable", "default-transitive"] { let this_id = FeatureId::named(&base_id, f); assert!( feature_graph .is_default_feature(this_id) .expect("valid feature ID"), "{f} is a default feature", ); assert!( feature_graph .depends_on(default_id, this_id) .expect("valid feature IDs"), "{default_id} should depend on {this_id} but does not", ); } // helper-enable and helper-transitive are *not* default features even though they are // enabled by the cyclic dev dependency. But the dependency relation is present. for &f in &["helper-enable", "helper-transitive"] { let this_id = FeatureId::named(&base_id, f); assert!( !feature_graph .is_default_feature(this_id) .expect("valid feature ID"), "{f} is NOT a default feature", ); assert!( feature_graph .depends_on(default_id, this_id) .expect("valid feature IDs"), "{default_id} should depend on {this_id} but does not", ); } } proptest_suite!(metadata_cycle_features); // Test Windows path handling in fixtures with path dependencies. #[test] fn metadata_cycle1_windows() { // Same drive: workspace on C:, path dep also on C:. // The path dependency should be stored as a relative path. let fixture = JsonFixture::metadata_cycle1_windows(); fixture.verify(); let graph = fixture.graph(); let helper = graph .metadata(&package_id(json::METADATA_CYCLE1_WINDOWS_HELPER)) .expect("helper package exists"); let source = helper.source(); assert!(source.is_path(), "helper should be a path dependency"); let path = source.local_path().expect("path dependency has local path"); // Same drive should produce a relative path. assert_eq!( path.as_str(), "../testcycles-helper", "same-drive path dependency should be relative" ); } proptest_suite!(metadata_cycle1_windows); #[test] fn metadata_cycle1_windows_different_drives() { // Different drives: workspace on C:, path dep on D:. // The path dependency should fall back to the absolute path. let fixture = JsonFixture::metadata_cycle1_windows_different_drives(); fixture.verify(); let graph = fixture.graph(); let helper = graph .metadata(&package_id( json::METADATA_CYCLE1_WINDOWS_DIFFERENT_DRIVES_HELPER, )) .expect("helper package exists"); let source = helper.source(); assert!(source.is_path(), "helper should be a path dependency"); let path = source.local_path().expect("path dependency has local path"); // Different drives cannot be relative, so the absolute path is stored. // The path is normalized to forward slashes on Unix but not on Windows. #[cfg(windows)] let expected = r"D:\libs\testcycles-helper"; #[cfg(not(windows))] let expected = "D:/libs/testcycles-helper"; assert_eq!( path.as_str(), expected, "different-drive path dependency should be absolute" ); } proptest_suite!(metadata_cycle1_windows_different_drives); // Test that Windows metadata with backslash paths is normalized to forward // slashes on Unix, so all path operations work correctly. On Windows, the // paths should remain with backslashes. #[test] fn windows_metadata_path_normalization() { // Use the different drives fixture since it's more interesting: it has // paths on different drives which cannot be made relative. let fixture = JsonFixture::metadata_cycle1_windows_different_drives(); let graph = fixture.graph(); // On Unix: backslashes should be normalized to forward slashes. // On Windows: forward slashes should not appear in absolute paths. #[cfg(not(windows))] let bad_sep = '\\'; #[cfg(windows)] let bad_sep = '/'; let root = graph.workspace().root(); assert!( !root.as_str().contains(bad_sep), "workspace root should not contain '{}': {}", bad_sep, root ); assert!(root.parent().is_some(), "workspace root should have parent"); let target_dir = graph.workspace().target_directory(); assert!( !target_dir.as_str().contains(bad_sep), "target directory should not contain '{}': {}", bad_sep, target_dir ); for package in graph.packages() { let manifest = package.manifest_path(); assert!( !manifest.as_str().contains(bad_sep), "manifest_path should not contain '{}': {}", bad_sep, manifest ); for target in package.build_targets() { let path = target.path(); assert!( !path.as_str().contains(bad_sep), "build target path should not contain '{}': {}", bad_sep, path ); } } } #[test] fn metadata_targets1() { let metadata_targets1 = JsonFixture::metadata_targets1(); metadata_targets1.verify(); let package_graph = metadata_targets1.graph(); let package_set = package_graph.resolve_all(); let feature_graph = metadata_targets1.graph().feature_graph(); assert_eq!(feature_graph.feature_count(), 38, "feature count"); // Some code that might be useful for debugging. if false { for (source, target, edge) in feature_graph .resolve_all() .links(DependencyDirection::Forward) { let source_metadata = package_graph.metadata(source.package_id()).unwrap(); let target_metadata = package_graph.metadata(target.package_id()).unwrap(); println!( "feature link: {}:{} {} -> {}:{} {} {:?}", source_metadata.name(), source_metadata.version(), source.label(), target_metadata.name(), target_metadata.version(), target.label(), edge ); } } assert_eq!(feature_graph.link_count(), 58, "feature link count"); // Check that resolve_packages + a feature filter works. let feature_set = package_set.to_feature_set(named_feature_filter( StandardFeatures::Default, ["foo", "bar"].iter().copied(), )); let dep_a_id = package_id(json::METADATA_TARGETS1_DEP_A); assert!( feature_set .contains((&dep_a_id, FeatureLabel::Named("foo"))) .expect("valid feature ID") ); assert!( feature_set .contains((&dep_a_id, FeatureLabel::Named("bar"))) .expect("valid feature ID") ); assert!( !feature_set .contains((&dep_a_id, FeatureLabel::Named("baz"))) .expect("valid feature ID") ); assert!( !feature_set .contains((&dep_a_id, FeatureLabel::Named("quux"))) .expect("valid feature ID") ); assert_features_for_package( &feature_set, &package_id(json::METADATA_TARGETS1_TESTCRATE), Some(&[FeatureLabel::Base]), "testcrate", ); assert_features_for_package( &feature_set, &dep_a_id, Some(&[ FeatureLabel::Base, FeatureLabel::Named("bar"), FeatureLabel::Named("foo"), ]), "dep a", ); assert_features_for_package( &feature_set, &package_id(json::METADATA_TARGETS1_LAZY_STATIC_1), Some(&[FeatureLabel::Base]), "lazy_static", ); } proptest_suite!(metadata_targets1); #[test] fn metadata_build_targets1() { let metadata_build_targets1 = JsonFixture::metadata_build_targets1(); metadata_build_targets1.verify(); } // No need for proptests because there are no dependencies involved. #[test] fn metadata_proc_macro1() { let metadata = JsonFixture::metadata_proc_macro1(); metadata.verify(); let graph = metadata.graph(); let package = graph .metadata(&package_id(json::METADATA_PROC_MACRO1_MACRO)) .expect("valid package ID"); assert!(package.is_proc_macro(), "is proc macro"); let build_target_kind = package .build_target(&BuildTargetId::Library) .expect("library package is present") .kind(); assert_eq!( build_target_kind, BuildTargetKind::ProcMacro, "build target kind matches" ); } // No need for proptests because this is a really simple test. } mod large { use super::*; use fixtures::dep_helpers::GraphAssert; #[test] fn metadata_libra() { let metadata_libra = JsonFixture::metadata_libra(); metadata_libra.verify(); } proptest_suite!(metadata_libra); #[test] fn metadata_libra_f0091a4() { let metadata = JsonFixture::metadata_libra_f0091a4(); metadata.verify(); } proptest_suite!(metadata_libra_f0091a4); #[test] fn metadata_libra_9ffd93b() { let metadata = JsonFixture::metadata_libra_9ffd93b(); metadata.verify(); let graph = metadata.graph(); graph.assert_depends_on( &package_id(json::METADATA_LIBRA_ADMISSION_CONTROL_SERVICE), &package_id(json::METADATA_LIBRA_EXECUTOR_UTILS), DependencyDirection::Forward, "admission-control-service should depend on executor-utils", ); graph.assert_not_depends_on( &package_id(json::METADATA_LIBRA_EXECUTOR_UTILS), &package_id(json::METADATA_LIBRA_ADMISSION_CONTROL_SERVICE), DependencyDirection::Forward, "executor-utils should not depend on admission-control-service", ); let proc_macro_packages: Vec<_> = graph .workspace() .iter_by_path() .filter_map(|(_, metadata)| { if metadata.is_proc_macro() { Some(metadata.name()) } else { None } }) .collect(); assert_eq!( proc_macro_packages, ["num-variants", "libra-crypto-derive"], "proc macro packages" ); let build_script_packages: Vec<_> = graph .workspace() .iter_by_path() .filter_map(|(_, metadata)| { if metadata.has_build_script() { Some(metadata.name()) } else { None } }) .collect(); assert_eq!( build_script_packages, [ "admission-control-proto", "libra-dev", "debug-interface", "libra-metrics", "storage-proto", "libra_fuzzer_fuzz", "libra-types" ], "build script packages" ); let mut build_dep_but_no_build_script: Vec<_> = graph .resolve_all() .links(DependencyDirection::Forward) .filter_map(|link| { if link.build().is_present() && !link.from().has_build_script() { Some(link.from().name()) } else { None } }) .collect(); build_dep_but_no_build_script.sort_unstable(); assert_eq!( build_dep_but_no_build_script, ["libra-mempool", "rusoto_signature"], "packages with build deps but no build scripts" ); } proptest_suite!(metadata_libra_9ffd93b); #[test] fn mnemos_b3b4da9() { let metadata = JsonFixture::mnemos_b3b4da9(); metadata.verify(); } proptest_suite!(mnemos_b3b4da9); #[test] fn hyper_util_7afb1ed() { let metadata = JsonFixture::hyper_util_7afb1ed(); metadata.verify(); } proptest_suite!(hyper_util_7afb1ed); } mod guppy_tests { use super::*; use fixtures::json::METADATA_GUPPY_CARGO_GUPPY; use guppy::PackageId; #[test] fn metadata_guppy_44b62fa() { let metadata = JsonFixture::metadata_guppy_44b62fa(); metadata.verify(); // This is --no-deps metadata: check that there are no dependency edges at all. let graph = metadata.graph(); let package = graph .metadata(&PackageId::new(METADATA_GUPPY_CARGO_GUPPY)) .expect("cargo-guppy package found"); assert_eq!( package.direct_links().count(), 0, "no-deps => package has no direct links" ); assert_eq!(graph.link_count(), 0, "no-deps => no edges"); } proptest_suite!(metadata_guppy_44b62fa); } struct NameVisitor; impl PackageDotVisitor for NameVisitor { fn visit_package(&self, package: PackageMetadata<'_>, f: &mut DotWrite<'_, '_>) -> fmt::Result { write!(f, "{}", package.name()) } fn visit_link(&self, link: PackageLink<'_>, f: &mut DotWrite<'_, '_>) -> fmt::Result { write!(f, "{}", link.dep_name()) } } guppy-0.17.25/tests/graph-tests/invalid_tests.rs000064400000000000000000000103401046102023000177630ustar 00000000000000use cargo_metadata::{CrateType, Metadata, Target, TargetKind}; use fixtures::json::JsonFixture; use guppy::{Error, PackageId, errors::FeatureGraphWarning, graph::PackageGraph}; #[test] fn optional_dev_dep() { assert_invalid( include_str!("../../../fixtures/invalid/optional_dev_dep.json"), "dependency 'lazy_static' marked optional", ); } #[test] fn duplicate_workspace_names() { assert_invalid( include_str!("../../../fixtures/invalid/duplicate_workspace_names.json"), "duplicate package name in workspace: 'pkg' is name for", ); } #[test] fn invalid_default_member() { assert_invalid( include_str!("../../../fixtures/invalid/invalid_default_member.json"), "workspace default member 'fake-package 1.0.0 (path+file:///fakepath/fake-package)' not found in workspace members", ); } #[test] fn build_targets_empty_kinds() { assert_invalid( include_str!("../../../fixtures/invalid/build_targets_empty_kinds.json"), "build target 'bench1' has no kinds", ); } #[test] fn build_targets_non_bin() { assert_invalid( include_str!("../../../fixtures/invalid/build_targets_non_bin.json"), "build target 'Binary(\"testcrate\")' has invalid crate types '{cdylib}'", ); } #[test] fn build_targets_duplicate_lib() { assert_invalid( include_str!("../../../fixtures/invalid/build_targets_duplicate_lib.json"), "duplicate build targets for Library", ); } static SELF_LOOP_B: &str = "path+file:///home/fakeuser/dev/tmp/named-feature-self/b#0.1.0"; #[test] fn named_feature_self_loop() { // This is not detected as invalid at construction time, but is instead detected while // constructing the feature graph. // // TODO: ideally, this would be detected at PackageGraph construction time. // // TODO: We currently do not detect loops consisting of multiple named features. We should do // this at construction time. let graph = PackageGraph::from_json(include_str!( "../../../fixtures/invalid/named_feature_self_loop.json" )) .expect("expected metadata to be valid"); let feature_graph = graph.feature_graph(); let warnings = feature_graph.build_warnings(); assert_eq!(warnings.len(), 1); assert_eq!( warnings[0], FeatureGraphWarning::SelfLoop { package_id: PackageId::new(SELF_LOOP_B), feature_name: "a".into(), } ); } #[test] fn proc_macro_mixed_kinds() { fn macro_target(metadata: &mut Metadata) -> &mut Target { let package = metadata .packages .iter_mut() .find(|p| p.name.as_str() == "macro") .expect("valid package"); package .targets .iter_mut() .find(|t| t.name == "macro") .expect("valid target") } let mut metadata: Metadata = serde_json::from_str(JsonFixture::metadata_proc_macro1().json()) .expect("parsing metadata JSON should succeed"); { let target = macro_target(&mut metadata); target.kind = vec![TargetKind::Lib, TargetKind::ProcMacro]; } let json = serde_json::to_string(&metadata).expect("serializing worked"); assert_invalid(&json, "proc-macro mixed with other kinds"); { let target = macro_target(&mut metadata); // Reset target.kind to its old value. target.kind = vec![TargetKind::ProcMacro]; target.crate_types = vec![CrateType::Lib, CrateType::ProcMacro]; } let json = serde_json::to_string(&metadata).expect("serializing worked"); assert_invalid(&json, "proc-macro mixed with other crate types"); } #[test] fn workspace_member_different_drive() { assert_invalid( include_str!("../../../fixtures/invalid/workspace_member_different_drive.json"), "paths may be on different drives or UNC shares", ); } fn assert_invalid(json: &str, search_str: &str) { let err = PackageGraph::from_json(json).expect_err("expected error for invalid metadata"); let Error::PackageGraphConstructError(s) = err else { panic!("expected PackageGraphConstructError, got: {err} ({err:?}"); }; assert!( s.contains(search_str), "expected error to contain '{search_str}', got: {s}" ); } guppy-0.17.25/tests/graph-tests/main.rs000064400000000000000000000006631046102023000160460ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 #[cfg(feature = "proptest1")] #[macro_use] mod proptest_helpers; #[cfg(not(feature = "proptest1"))] macro_rules! proptest_suite { ($name: ident) => { // Empty macro to skip proptests if the proptest feature is disabled. }; } mod cargo_set_tests; mod feature_helpers; mod graph_tests; mod invalid_tests; mod weak_namespaced; guppy-0.17.25/tests/graph-tests/proptest_helpers.rs000064400000000000000000000542361046102023000205310ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use fixtures::dep_helpers::{GraphAssert, GraphMetadata, GraphQuery, GraphSet, assert_link_order}; use guppy::{ PackageId, graph::{ DependencyDirection, PackageGraph, Prop010Resolver, feature::{FeatureId, FeatureLabel, FeatureSet, StandardFeatures}, }, }; use pretty_assertions::assert_eq; use proptest::{collection::vec, prelude::*, sample::Index}; use std::collections::HashSet; macro_rules! proptest_suite { ($name: ident) => { mod $name { use crate::proptest_helpers::*; use fixtures::json::JsonFixture; use guppy::graph::DependencyDirection; use proptest::collection::{hash_set, vec}; use proptest::prelude::*; use proptest::sample::Index; #[test] fn proptest_summary_id_roundtrip() { let fixture = JsonFixture::$name(); let graph = fixture.graph(); proptest!(|(package_id in graph.proptest1_id_strategy())| { let package = graph.metadata(package_id).expect("valid package ID"); let summary_id = package.to_summary_id(); let package2 = graph.metadata_by_summary_id(&summary_id).expect("summary ID is valid"); prop_assert_eq!(package_id, package2.id(), "roundtrip successful"); }) } #[test] fn proptest_query_depends_on() { let fixture = JsonFixture::$name(); let graph = fixture.graph(); proptest!(|( ids in vec(graph.proptest1_id_strategy(), 1..16), query_direction in any::(), iter_direction in any::(), query_indexes in vec(any::(), 0..16), )| { depends_on(graph, &ids, query_direction, iter_direction, query_indexes, "query_depends_on"); }); } #[test] fn proptest_feature_query_depends_on() { let fixture = JsonFixture::$name(); let package_graph = fixture.graph(); let feature_graph = package_graph.feature_graph(); proptest!(|( ids in vec(feature_graph.proptest1_id_strategy(), 1..16), query_direction in any::(), iter_direction in any::(), query_indexes in vec(any::(), 0..16), )| { depends_on(feature_graph, &ids, query_direction, iter_direction, query_indexes, "feature_query_depends_on"); }); } #[test] fn proptest_depends_on_same_package_id() { let fixture = JsonFixture::$name(); let package_graph = fixture.graph(); proptest!(|(query_id in package_graph.proptest1_id_strategy())| { depends_on_same_id(package_graph, query_id); }); } #[test] fn proptest_depends_on_same_feature_id() { let fixture = JsonFixture::$name(); let package_graph = fixture.graph(); let feature_graph = package_graph.feature_graph(); proptest!(|(query_id in feature_graph.proptest1_id_strategy())| { depends_on_same_id(feature_graph, query_id); }); } #[test] fn proptest_query_link_order() { let fixture = JsonFixture::$name(); let graph = fixture.graph(); proptest!(|( ids in vec(graph.proptest1_id_strategy(), 1..16), query_direction in any::(), iter_direction in any::(), )| { link_order(graph, &ids, query_direction, iter_direction, "query_link_order"); }); } #[test] fn proptest_query_roots() { let fixture = JsonFixture::$name(); let graph = fixture.graph(); proptest!(|( ids in vec(graph.proptest1_id_strategy(), 1..16), query_direction in any::(), iter_direction in any::(), query_indexes in vec((any::(), any::()), 0..128), )| { roots( graph, &ids, query_direction, iter_direction, query_indexes, "query_roots", )?; }); } #[test] fn proptest_feature_query_roots() { let fixture = JsonFixture::$name(); let package_graph = fixture.graph(); let feature_graph = package_graph.feature_graph(); proptest!(|( ids in vec(feature_graph.proptest1_id_strategy(), 1..16), query_direction in any::(), iter_direction in any::(), query_indexes in vec((any::(), any::()), 0..128), )| { roots( feature_graph, &ids, query_direction, iter_direction, query_indexes, "feature_query_roots", )?; }); } #[test] fn proptest_resolve_contains() { let fixture = JsonFixture::$name(); let package_graph = fixture.graph(); proptest!(|( query_ids in vec(package_graph.proptest1_id_strategy(), 1..16), direction in any::(), test_ids in vec(package_graph.proptest1_id_strategy(), 0..64), )| { resolve_contains(package_graph, &query_ids, direction, &test_ids); }); } #[test] fn proptest_feature_resolve_contains() { let fixture = JsonFixture::$name(); let package_graph = fixture.graph(); let feature_graph = package_graph.feature_graph(); proptest!(|( query_ids in vec(feature_graph.proptest1_id_strategy(), 1..16), direction in any::(), test_ids in vec(feature_graph.proptest1_id_strategy(), 0..64), )| { resolve_contains(feature_graph, &query_ids, direction, &test_ids); }); } #[test] fn proptest_resolve_ops() { let fixture = JsonFixture::$name(); let package_graph = fixture.graph(); proptest!(|( resolve_tree in ResolveTree::strategy(package_graph.proptest1_id_strategy()) )| { resolve_ops(package_graph, resolve_tree); }); } #[test] fn proptest_feature_resolve_ops() { let fixture = JsonFixture::$name(); let package_graph = fixture.graph(); let feature_graph = package_graph.feature_graph(); proptest!(|( resolve_tree in ResolveTree::strategy(feature_graph.proptest1_id_strategy()) )| { resolve_ops(feature_graph, resolve_tree); }); } #[test] fn proptest_package_feature_set_roundtrip() { let fixture = JsonFixture::$name(); let package_graph = fixture.graph(); let feature_graph = package_graph.feature_graph(); proptest!(|( query_ids in vec(package_graph.proptest1_id_strategy(), 1..16), query_direction in any::(), mut resolver in package_graph.proptest1_resolver_strategy(), test_ids in vec(feature_graph.proptest1_id_strategy(), 1..16), test_direction in any::(), )| { resolver.check_depends_on(true); package_feature_set_roundtrip(package_graph, query_ids, query_direction, resolver, test_ids, test_direction); }); } #[test] fn proptest_feature_set_props() { let fixture = JsonFixture::$name(); let package_graph = fixture.graph(); let feature_graph = package_graph.feature_graph(); proptest!(|( feature_set in feature_graph.proptest1_set_strategy(), direction in any::(), )| { feature_set_props(feature_set, direction); }); } #[test] fn proptest_query_starts_from() { let fixture = JsonFixture::$name(); let package_graph = fixture.graph(); proptest!(|( query_ids in hash_set(package_graph.proptest1_id_strategy(), 0..16), direction in any::(), test_ids in vec(package_graph.proptest1_id_strategy(), 0..16) )| { query_starts_from(package_graph, query_ids, direction, test_ids); }); } #[test] fn proptest_feature_query_starts_from() { let fixture = JsonFixture::$name(); let package_graph = fixture.graph(); let feature_graph = package_graph.feature_graph(); proptest!(|( query_ids in hash_set(feature_graph.proptest1_id_strategy(), 0..16), direction in any::(), test_ids in vec(feature_graph.proptest1_id_strategy(), 0..16) )| { query_starts_from(feature_graph, query_ids, direction, test_ids); }); } } } } /// Test that all results of an into_iter_ids query depend on at least one of the ids in the query /// set. pub(super) fn depends_on<'g, G: GraphAssert<'g>>( graph: G, ids: &[G::Id], query_direction: DependencyDirection, iter_direction: DependencyDirection, query_indexes: Vec, msg: &str, ) { let msg = format!("{msg}: reachable means depends on"); let reachable_ids = graph.ids(ids, query_direction, iter_direction); for index in query_indexes { let query_id = index.get(&reachable_ids); graph.assert_depends_on_any(ids, *query_id, query_direction, &msg); } } /// Test depends_on and directly_depends_on semantics with the same ID. pub(super) fn depends_on_same_id<'g, G: GraphAssert<'g>>(graph: G, query_id: G::Id) { graph.assert_depends_on( query_id, query_id, DependencyDirection::Forward, "depends_on for same ID returns true", ); assert!( !graph .directly_depends_on(query_id, query_id) .expect("valid ID"), "directly_depends_on for same ID returns false", ); } /// Test that all results of an into_iter_links query follow link order. pub(super) fn link_order( graph: &PackageGraph, ids: &[&PackageId], query_direction: DependencyDirection, iter_direction: DependencyDirection, msg: &str, ) { let package_set = graph .query_directed(ids.iter().copied(), query_direction) .unwrap() .resolve(); // If the query and iter directions are the same, the set of initial IDs may be expanded // in case of cycles. If they are the opposite, the set of initial IDs will be different as // well. Compute the root IDs from the graph in that case. let has_cycles = graph.cycles().all_cycles().count() > 0; let initials = if has_cycles || query_direction != iter_direction { package_set.root_ids(iter_direction).collect() } else { ids.to_vec() }; let links = package_set.links(iter_direction); assert_link_order( links, initials, iter_direction, &format!("{msg}: link order"), ); } /// Test that the results of an `root_ids` query don't depend on any other root. pub(super) fn roots<'g, G: GraphAssert<'g>>( graph: G, ids: &[G::Id], query_direction: DependencyDirection, iter_direction: DependencyDirection, query_indexes: Vec<(Index, Index)>, msg: &str, ) -> prop::test_runner::TestCaseResult { let root_ids = graph.root_ids(ids, query_direction, iter_direction); let root_id_set: HashSet<_> = root_ids.iter().copied().collect(); prop_assert_eq!( root_ids.len(), root_id_set.len(), "{}: root IDs should all be unique", msg ); let root_metadatas = graph.root_metadatas(ids, query_direction, iter_direction); prop_assert_eq!( root_ids.len(), root_metadatas.len(), "{}: root IDs and metadatas should have the same count", msg ); let root_id_set_2: HashSet<_> = root_metadatas .iter() .map(|metadata| metadata.id()) .collect(); prop_assert_eq!( root_id_set, root_id_set_2, "{}: root IDs and metadatas should return the same results", msg ); prop_assert!( !root_ids.is_empty(), "ids is non-empty so root ids can't be empty either" ); for (index1, index2) in query_indexes { let id1 = index1.get(&root_ids); let id2 = index2.get(&root_ids); if id1 != id2 { graph.assert_not_depends_on(*id1, *id2, query_direction, msg); } } Ok(()) } pub(super) fn resolve_contains<'g, G: GraphAssert<'g>>( graph: G, query_ids: &[G::Id], direction: DependencyDirection, test_ids: &[G::Id], ) { let resolve_set = graph.resolve(query_ids, direction); for test_id in test_ids { if resolve_set.contains(*test_id) { graph.assert_depends_on_any(query_ids, *test_id, direction, "contains => depends on"); } else { for query_id in query_ids { graph.assert_not_depends_on( *query_id, *test_id, direction, "not contains => not depends on", ); } } } } #[derive(Clone, Debug)] pub(super) enum ResolveTree> { Resolve { initials: Vec, direction: DependencyDirection, }, Union(Box>, Box>), Intersection(Box>, Box>), Difference(Box>, Box>), SymmetricDifference(Box>, Box>), } // The 'statics are required because prop_recursive requires the leaf to be 'static. impl + 'static> ResolveTree { pub(super) fn strategy( id_strategy: impl Strategy + 'static, ) -> impl Strategy> + 'static { let leaf = (vec(id_strategy, 1..16), any::()).prop_map( |(initials, direction)| ResolveTree::Resolve { initials, direction, }, ); leaf.prop_recursive( 4, // 4 levels deep 16, // 2**4 = 16 nodes max 2, // 2 items per non-leaf node, |inner| { prop_oneof![ (inner.clone(), inner.clone()) .prop_map(|(a, b)| ResolveTree::Union(Box::new(a), Box::new(b))), (inner.clone(), inner.clone()) .prop_map(|(a, b)| ResolveTree::Intersection(Box::new(a), Box::new(b))), (inner.clone(), inner.clone()) .prop_map(|(a, b)| ResolveTree::Difference(Box::new(a), Box::new(b))), (inner.clone(), inner).prop_map(|(a, b)| ResolveTree::SymmetricDifference( Box::new(a), Box::new(b) )), ] }, ) } } pub(super) fn resolve_ops>(graph: G, resolve_tree: ResolveTree) { let (resolve, hashset) = resolve_ops_impl(graph, &resolve_tree); assert_eq!( resolve.len(), hashset.len(), "resolve and hashset lengths match" ); let ids: HashSet<_> = resolve .ids(DependencyDirection::Forward) .into_iter() .collect(); assert_eq!(ids, hashset, "operations on resolve and hashset match"); } fn resolve_ops_impl>( graph: G, resolve_tree: &ResolveTree, ) -> (G::Set, HashSet) { match resolve_tree { ResolveTree::Resolve { initials, direction, } => { let resolve_set = graph.resolve(initials, *direction); let ids = resolve_set.ids(*direction).into_iter().collect(); (resolve_set, ids) } ResolveTree::Union(a, b) => { let (resolve_a, hashset_a) = resolve_ops_impl(graph, a); let (resolve_b, hashset_b) = resolve_ops_impl(graph, b); ( resolve_a.union(&resolve_b), hashset_a.union(&hashset_b).copied().collect(), ) } ResolveTree::Intersection(a, b) => { let (resolve_a, hashset_a) = resolve_ops_impl(graph, a); let (resolve_b, hashset_b) = resolve_ops_impl(graph, b); ( resolve_a.intersection(&resolve_b), hashset_a.intersection(&hashset_b).copied().collect(), ) } ResolveTree::Difference(a, b) => { let (resolve_a, hashset_a) = resolve_ops_impl(graph, a); let (resolve_b, hashset_b) = resolve_ops_impl(graph, b); ( resolve_a.difference(&resolve_b), hashset_a.difference(&hashset_b).copied().collect(), ) } ResolveTree::SymmetricDifference(a, b) => { let (resolve_a, hashset_a) = resolve_ops_impl(graph, a); let (resolve_b, hashset_b) = resolve_ops_impl(graph, b); ( resolve_a.symmetric_difference(&resolve_b), hashset_a .symmetric_difference(&hashset_b) .copied() .collect(), ) } } } pub(super) fn package_feature_set_roundtrip( package_graph: &PackageGraph, query_ids: Vec<&PackageId>, query_direction: DependencyDirection, mut resolver: Prop010Resolver, test_ids: Vec, test_direction: DependencyDirection, ) { let package_set = package_graph .query_directed(query_ids.iter().copied(), query_direction) .expect("valid package IDs") .resolve_with(&mut resolver); let all_feature_set = package_set.to_feature_set(StandardFeatures::All); let no_feature_set = package_set.to_feature_set(StandardFeatures::None); for test_id in test_ids { assert_eq!( package_set .contains(test_id.package_id()) .expect("valid package ID"), all_feature_set.contains(test_id).expect("valid feature ID"), "all => package ID present == feature ID present" ); assert_eq!( package_set .contains(test_id.package_id()) .expect("valid package ID"), no_feature_set .contains((test_id.package_id(), FeatureLabel::Base)) .expect("valid feature ID"), "none => package ID present == base feature ID present" ); } let package_ids: Vec<_> = package_set.package_ids(test_direction).collect(); let package_set_2 = all_feature_set.to_package_set(); let package_ids_2: Vec<_> = package_set_2.package_ids(test_direction).collect(); assert_eq!(package_ids, package_ids_2, "package IDs roundtrip"); } pub(super) fn feature_set_props(feature_set: FeatureSet<'_>, direction: DependencyDirection) { // into_ids and into_packages_with_features match (after sorting). let mut feature_ids: Vec<_> = feature_set.feature_ids(direction).collect(); let mut feature_ids_2: Vec<_> = feature_set .packages_with_features(direction) .flat_map(|feature_list| feature_list.into_iter()) .collect(); feature_ids.sort(); feature_ids_2.sort(); assert_eq!( feature_ids, feature_ids_2, "into_ids and into_packages_with_features match" ); // to_package_set and into_packages_with_features match (without sorting). let package_set_ids: Vec<_> = feature_set .to_package_set() .package_ids(direction) .collect(); let feature_set_ids: Vec<_> = feature_set .packages_with_features(direction) .map(|feature_list| { println!( "for id {}, features: {}", feature_list.package().id(), feature_list.display_features(), ); feature_list.package().id() }) .collect(); assert_eq!( package_set_ids, feature_set_ids, "to_package_set and into_packages_with_features match" ); } pub(super) fn query_starts_from<'g, G: GraphAssert<'g>>( graph: G, query_ids: HashSet, direction: DependencyDirection, test_ids: Vec, ) { let query = graph.query(query_ids.iter().copied(), direction); assert_eq!(query.direction(), direction, "query direction"); for query_id in &query_ids { assert!(query.starts_from(*query_id), "starts from"); } for test_id in test_ids { if !query_ids.contains(&test_id) { assert!(!query.starts_from(test_id), "does not start from"); } } } // TODO: More tests for FeatureFilter implementations. guppy-0.17.25/tests/graph-tests/weak_namespaced.rs000064400000000000000000000360571046102023000202370ustar 00000000000000// Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 use crate::feature_helpers::assert_features_for_package; use fixtures::{ json::{self, JsonFixture}, package_id, }; use guppy::graph::{ cargo::{CargoOptions, CargoResolverVersion, CargoSet}, feature::{FeatureLabel, FeatureSet, StandardFeatures, named_feature_filter}, }; use target_spec::Platform; #[test] fn default_features() { let cargo_set = make_linux_cargo_set(feature_set_fn(&[])); assert_features_for_package( cargo_set.target_features(), &package_id(json::METADATA_WEAK_NAMESPACED_ID), Some(&[FeatureLabel::Base]), "while checking Cargo resolution for default features", ); } #[test] fn named_feature_single_dep() { let cargo_set = make_linux_cargo_set(feature_set_fn(&["foo"])); // This should not the named feature foo + the optional dependency arrayvec (but not the // named feature arrayvec). assert_features_for_package( cargo_set.target_features(), &package_id(json::METADATA_WEAK_NAMESPACED_ID), Some(&[ FeatureLabel::Base, FeatureLabel::Named("foo"), FeatureLabel::OptionalDependency("arrayvec"), ]), "while checking Cargo resolution for default + foo", ); } #[test] fn named_feature_same_as_dep_plus_feature() { let cargo_set = make_linux_cargo_set(feature_set_fn(&["smallvec"])); // This should contain foo and both the named and optional dep versions of smallvec. assert_features_for_package( cargo_set.target_features(), &package_id(json::METADATA_WEAK_NAMESPACED_ID), Some(&[ FeatureLabel::Base, FeatureLabel::Named("foo"), FeatureLabel::Named("smallvec"), FeatureLabel::OptionalDependency("arrayvec"), FeatureLabel::OptionalDependency("smallvec"), ]), "while checking Cargo resolution for default + smallvec", ); // smallvec should not have its union feature enabled. assert_features_for_package( cargo_set.target_features(), &package_id(json::METADATA_WEAK_NAMESPACED_SMALLVEC), Some(&[FeatureLabel::Base]), "while checking Cargo resolution for default + smallvec", ); } #[test] fn enabled_non_weak_feature() { let cargo_set = make_linux_cargo_set(feature_set_fn(&["bar"])); // This should enable both the feature and the optional dependency for camino. assert_features_for_package( cargo_set.target_features(), &package_id(json::METADATA_WEAK_NAMESPACED_ID), Some(&[ FeatureLabel::Base, FeatureLabel::Named("arrayvec"), FeatureLabel::Named("bar"), FeatureLabel::OptionalDependency("arrayvec"), ]), "while checking Cargo resolution for default + bar", ); assert_features_for_package( cargo_set.target_features(), &package_id(json::METADATA_WEAK_NAMESPACED_ARRAYVEC), Some(&[FeatureLabel::Base, FeatureLabel::Named("std")]), "while checking Cargo resolution for default + bar", ); } #[test] fn named_feature_does_not_enable_dep_with_same_name() { let cargo_set = make_linux_cargo_set(feature_set_fn(&["arrayvec"])); // This should enable the named feature "arrayvec" but NOT the dependency arrayvec. assert_features_for_package( cargo_set.target_features(), &package_id(json::METADATA_WEAK_NAMESPACED_ID), Some(&[FeatureLabel::Base, FeatureLabel::Named("arrayvec")]), "while checking Cargo resolution for default + arrayvec", ); assert_features_for_package( cargo_set.target_features(), &package_id(json::METADATA_WEAK_NAMESPACED_ARRAYVEC), None, "while checking Cargo resolution for default + arrayvec", ); } #[test] fn enabled_weak_feature_1() { let cargo_set = make_linux_cargo_set(feature_set_fn(&["smallvec", "smallvec-union"])); // This should contain foo and both the named and optional dep versions of smallvec. assert_features_for_package( cargo_set.target_features(), &package_id(json::METADATA_WEAK_NAMESPACED_ID), Some(&[ FeatureLabel::Base, FeatureLabel::Named("foo"), FeatureLabel::Named("smallvec"), FeatureLabel::Named("smallvec-union"), FeatureLabel::OptionalDependency("arrayvec"), FeatureLabel::OptionalDependency("smallvec"), ]), "while checking Cargo resolution for default + smallvec + smallvec-union", ); // smallvec *should* have its union feature enabled. assert_features_for_package( cargo_set.target_features(), &package_id(json::METADATA_WEAK_NAMESPACED_SMALLVEC), Some(&[FeatureLabel::Base, FeatureLabel::Named("union")]), "while checking Cargo resolution for default + smallvec + smallvec-union", ); } #[test] fn enabled_weak_feature_2() { let cargo_set = make_linux_cargo_set(feature_set_fn(&["foo", "baz"])); // This should enable the dependency arrayvec but NOT the named feature. assert_features_for_package( cargo_set.target_features(), &package_id(json::METADATA_WEAK_NAMESPACED_ID), Some(&[ FeatureLabel::Base, FeatureLabel::Named("baz"), FeatureLabel::Named("foo"), FeatureLabel::OptionalDependency("arrayvec"), FeatureLabel::OptionalDependency("pathdiff"), ]), "while checking Cargo resolution for default + foo + baz", ); assert_features_for_package( cargo_set.target_features(), &package_id(json::METADATA_WEAK_NAMESPACED_ARRAYVEC), Some(&[FeatureLabel::Base, FeatureLabel::Named("std")]), "while checking Cargo resolution for default + foo + baz", ); } #[test] fn enabled_weak_feature_3() { let cargo_set = make_linux_cargo_set(feature_set_fn(&["bar", "baz"])); // This should enable BOTH the named feature and the dependency baz. assert_features_for_package( cargo_set.target_features(), &package_id(json::METADATA_WEAK_NAMESPACED_ID), Some(&[ FeatureLabel::Base, FeatureLabel::Named("arrayvec"), FeatureLabel::Named("bar"), FeatureLabel::Named("baz"), FeatureLabel::OptionalDependency("arrayvec"), FeatureLabel::OptionalDependency("pathdiff"), ]), "while checking Cargo resolution for default + bar + baz", ); assert_features_for_package( cargo_set.target_features(), &package_id(json::METADATA_WEAK_NAMESPACED_ARRAYVEC), Some(&[FeatureLabel::Base, FeatureLabel::Named("std")]), "while checking Cargo resolution for default + bar + baz", ); } #[test] fn disabled_weak_feature_1() { let cargo_set = make_linux_cargo_set(feature_set_fn(&["baz"])); // This should NOT enable the dependency OR the named feature arrayvec. assert_features_for_package( cargo_set.target_features(), &package_id(json::METADATA_WEAK_NAMESPACED_ID), Some(&[ FeatureLabel::Base, FeatureLabel::Named("baz"), FeatureLabel::OptionalDependency("pathdiff"), ]), "while checking Cargo resolution for default + baz", ); assert_features_for_package( cargo_set.target_features(), &package_id(json::METADATA_WEAK_NAMESPACED_ARRAYVEC), None, "while checking Cargo resolution for default + baz", ); } #[test] fn disabled_weak_feature_2() { let cargo_set = make_linux_cargo_set(feature_set_fn(&["arrayvec", "baz"])); // This should enable the named feature arrayvec but NOT the dependency. assert_features_for_package( cargo_set.target_features(), &package_id(json::METADATA_WEAK_NAMESPACED_ID), Some(&[ FeatureLabel::Base, FeatureLabel::Named("arrayvec"), FeatureLabel::Named("baz"), FeatureLabel::OptionalDependency("pathdiff"), ]), "while checking Cargo resolution for default + arrayvec + baz", ); assert_features_for_package( cargo_set.target_features(), &package_id(json::METADATA_WEAK_NAMESPACED_ARRAYVEC), None, "while checking Cargo resolution for default + arrayvec + baz", ); } #[test] fn platform_not_matched_features() { fn expected_features_for(name: &'static str) -> Vec> { match name { "windows-dep" => vec![FeatureLabel::Base, FeatureLabel::Named(name)], "windows-named" => vec![ FeatureLabel::Base, FeatureLabel::Named("tinyvec"), FeatureLabel::Named(name), ], "windows-non-weak" | "windows-weak" => { vec![FeatureLabel::Base, FeatureLabel::Named(name)] } _ => unreachable!(), } } for feature_name in [ "windows-dep", "windows-named", "windows-non-weak", "windows-weak", ] { let cargo_set = make_linux_cargo_set(feature_set_fn(&[feature_name])); let msg = format!("while checking Cargo resolution for default + {feature_name}"); assert_features_for_package( cargo_set.target_features(), &package_id(json::METADATA_WEAK_NAMESPACED_ID), Some(&expected_features_for(feature_name)), &msg, ); assert_features_for_package( cargo_set.target_features(), &package_id(json::METADATA_WEAK_NAMESPACED_TINYVEC), None, &msg, ); } } #[test] fn platform_matched_features() { fn expected_features_for_main(name: &'static str) -> Vec> { match name { "windows-dep" => vec![ FeatureLabel::Base, FeatureLabel::Named(name), FeatureLabel::OptionalDependency("tinyvec"), ], "windows-named" | "windows-non-weak" => vec![ FeatureLabel::Base, FeatureLabel::Named("tinyvec"), FeatureLabel::Named(name), FeatureLabel::OptionalDependency("tinyvec"), ], "windows-weak" => vec![FeatureLabel::Base, FeatureLabel::Named(name)], _ => unreachable!(), } } fn expected_features_for_tinyvec(name: &'static str) -> Option>> { match name { "windows-dep" | "windows-named" => Some(vec![FeatureLabel::Base]), "windows-non-weak" => Some(vec![FeatureLabel::Base, FeatureLabel::Named("rustc_1_40")]), "windows-weak" => None, _ => unreachable!(), } } for feature_name in [ "windows-dep", "windows-named", "windows-non-weak", "windows-weak", ] { let cargo_set = make_windows_cargo_set(feature_set_fn(&[feature_name])); let msg = format!("while checking Cargo resolution for default + {feature_name}"); assert_features_for_package( cargo_set.target_features(), &package_id(json::METADATA_WEAK_NAMESPACED_ID), Some(&expected_features_for_main(feature_name)), &msg, ); assert_features_for_package( cargo_set.target_features(), &package_id(json::METADATA_WEAK_NAMESPACED_TINYVEC), expected_features_for_tinyvec(feature_name).as_deref(), &msg, ); } } #[test] fn test_feature_presence() { // Check the existence and non-existence of a few features. let feature_graph = JsonFixture::metadata_weak_namespaced_features() .graph() .feature_graph(); assert!(feature_graph.contains(( &package_id(json::METADATA_WEAK_NAMESPACED_ID), FeatureLabel::OptionalDependency("pathdiff") ))); assert!(feature_graph.contains(( &package_id(json::METADATA_WEAK_NAMESPACED_ID), FeatureLabel::Named("pathdiff2") ))); assert!(!feature_graph.contains(( &package_id(json::METADATA_WEAK_NAMESPACED_ID), FeatureLabel::Named("pathdiff") ))); assert!(!feature_graph.contains(( &package_id(json::METADATA_WEAK_NAMESPACED_ID), FeatureLabel::OptionalDependency("pathdiff2") ))); } /// Test situations where edges have to be upgraded, e.g. /// /// [features] /// foo = ["a?/feat", "a/feat"] /// # or /// foo = ["a/feat", "a"] #[test] fn test_edge_upgrades() { fn expected_features_for(feature_name: &'static str) -> Vec> { match feature_name { "upgrade1" | "upgrade2" | "upgrade3" | "upgrade4" | "upgrade5" | "upgrade6" => vec![ FeatureLabel::Base, FeatureLabel::Named("foo"), FeatureLabel::Named("smallvec"), FeatureLabel::Named(feature_name), FeatureLabel::OptionalDependency("arrayvec"), FeatureLabel::OptionalDependency("smallvec"), ], // These do not activate the named feature smallvec. "upgrade7" | "upgrade8" => vec![ FeatureLabel::Base, FeatureLabel::Named(feature_name), FeatureLabel::OptionalDependency("smallvec"), ], _ => unreachable!(), } } for feature_name in [ "upgrade1", "upgrade2", "upgrade3", "upgrade4", "upgrade5", "upgrade6", "upgrade7", "upgrade8", ] { let cargo_set = make_linux_cargo_set(feature_set_fn(&[feature_name])); let msg = format!("while checking Cargo resolution for default + {feature_name}"); assert_features_for_package( cargo_set.target_features(), &package_id(json::METADATA_WEAK_NAMESPACED_ID), Some(&expected_features_for(feature_name)), &msg, ); assert_features_for_package( cargo_set.target_features(), &package_id(json::METADATA_WEAK_NAMESPACED_SMALLVEC), Some(&[FeatureLabel::Base, FeatureLabel::Named("union")]), &msg, ); } } fn feature_set_fn(named_features: &[&str]) -> FeatureSet<'static> { JsonFixture::metadata_weak_namespaced_features() .graph() .resolve_ids([&package_id(json::METADATA_WEAK_NAMESPACED_ID)]) .expect("valid package ID") .to_feature_set(named_feature_filter( StandardFeatures::Default, named_features.iter().copied(), )) } fn make_linux_cargo_set(feature_set: FeatureSet<'static>) -> CargoSet<'static> { make_cargo_set(feature_set, "x86_64-unknown-linux-gnu") } fn make_windows_cargo_set(feature_set: FeatureSet<'static>) -> CargoSet<'static> { make_cargo_set(feature_set, "x86_64-pc-windows-msvc") } fn make_cargo_set(feature_set: FeatureSet<'static>, triple: &'static str) -> CargoSet<'static> { let mut cargo_options = CargoOptions::new(); cargo_options .set_resolver(CargoResolverVersion::V2) .set_target_platform(Platform::new(triple, target_spec::TargetFeatures::Unknown).unwrap()); feature_set .into_cargo_set(&cargo_options) .expect("resolving cargo should work") }