From 91e83d70ed0c586f1748c50d63665fff34473928 Mon Sep 17 00:00:00 2001 From: Andrew Liu <24590316+aliu@users.noreply.github.com> Date: Sun, 9 Feb 2025 21:40:14 +0900 Subject: [PATCH 1/2] test: verify feature unification behavior in workspaces --- tests/testsuite/feature_unification.rs | 185 +++++++++++++++++++++++++ tests/testsuite/main.rs | 1 + 2 files changed, 186 insertions(+) create mode 100644 tests/testsuite/feature_unification.rs diff --git a/tests/testsuite/feature_unification.rs b/tests/testsuite/feature_unification.rs new file mode 100644 index 00000000000..3349f9452af --- /dev/null +++ b/tests/testsuite/feature_unification.rs @@ -0,0 +1,185 @@ +//! Tests for workspace feature unification. + +use cargo_test_support::prelude::*; +use cargo_test_support::{basic_manifest, cargo_process, project, str}; + +#[cargo_test] +fn workspace_feature_unification() { + let p = project() + .file( + ".cargo/config.toml", + r#" + [resolver] + feature-unification = "workspace" + "#, + ) + .file( + "Cargo.toml", + r#" + [workspace] + resolver = "2" + members = ["common", "a", "b"] + "#, + ) + .file( + "common/Cargo.toml", + r#" + [package] + name = "common" + version = "0.1.0" + edition = "2021" + + [features] + a = [] + b = [] + "#, + ) + .file( + "common/src/lib.rs", + r#" + #[cfg(not(all(feature = "a", feature = "b")))] + compile_error!("features were not unified"); + "#, + ) + .file( + "a/Cargo.toml", + r#" + [package] + name = "a" + version = "0.1.0" + edition = "2021" + + [dependencies] + common = { path = "../common", features = ["a"] } + "#, + ) + .file("a/src/lib.rs", "") + .file( + "b/Cargo.toml", + r#" + [package] + name = "b" + version = "0.1.0" + edition = "2021" + + [dependencies] + common = { path = "../common", features = ["b"] } + "#, + ) + .file("b/src/lib.rs", "") + .build(); + + p.cargo("check -p common") + .with_stderr_contains("[WARNING] unused config key `resolver.feature-unification` in `[ROOT]/foo/.cargo/config.toml`") + .with_stderr_contains("[ERROR] features were not unified") + .with_status(101) + .run(); + p.cargo("check -p a") + .with_stderr_contains("[WARNING] unused config key `resolver.feature-unification` in `[ROOT]/foo/.cargo/config.toml`") + .with_stderr_contains("[ERROR] features were not unified") + .with_status(101) + .run(); + p.cargo("check -p b") + .with_stderr_contains("[WARNING] unused config key `resolver.feature-unification` in `[ROOT]/foo/.cargo/config.toml`") + .with_stderr_contains("[ERROR] features were not unified") + .with_status(101) + .run(); + p.cargo("check") + .with_stderr_contains("[WARNING] unused config key `resolver.feature-unification` in `[ROOT]/foo/.cargo/config.toml`") + .run(); +} + +#[cargo_test] +fn cargo_install_ignores_config() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "a" + version = "0.1.0" + edition = "2021" + + [dependencies] + common = { path = "common", features = ["a"] } + + [workspace] + members = ["common", "b"] + "#, + ) + .file("src/main.rs", "fn main() {}") + .file( + "common/Cargo.toml", + r#" + [package] + name = "common" + version = "0.1.0" + edition = "2021" + + [features] + a = [] + b = [] + "#, + ) + .file( + "common/src/lib.rs", + r#" + #[cfg(all(feature = "a", feature = "b"))] + compile_error!("features should not be unified"); + "#, + ) + .file( + "b/Cargo.toml", + r#" + [package] + name = "b" + version = "0.1.0" + edition = "2021" + + [dependencies] + common = { path = "../common", features = ["b"] } + "#, + ) + .file("b/src/lib.rs", "") + .build(); + + cargo_process("install --path") + .arg(p.root()) + .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "workspace") + .with_stderr_data(str![[r#" +[INSTALLING] a v0.1.0 ([ROOT]/foo) +[COMPILING] common v0.1.0 ([ROOT]/foo/common) +[COMPILING] a v0.1.0 ([ROOT]/foo) +[FINISHED] `release` profile [optimized] target(s) in [ELAPSED]s +[INSTALLING] [ROOT]/home/.cargo/bin/a +[INSTALLED] package `a v0.1.0 ([ROOT]/foo)` (executable `a`) +[WARNING] be sure to add `[ROOT]/home/.cargo/bin` to your PATH to be able to run the installed binaries + +"#]]) + .run(); +} + +#[cargo_test] +fn unstable_config_on_stable() { + let p = project() + .file( + "Cargo.toml", + r#" + [workspace] + resolver = "2" + members = ["bar"] + "#, + ) + .file("bar/Cargo.toml", &basic_manifest("bar", "0.1.0")) + .file("bar/src/lib.rs", "") + .build(); + + p.cargo("check") + .env("CARGO_RESOLVER_FEATURE_UNIFICATION", "workspace") + .with_stderr_data(str![[r#" +[CHECKING] bar v0.1.0 ([ROOT]/foo/bar) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [ELAPSED]s + +"#]]) + .run(); +} diff --git a/tests/testsuite/main.rs b/tests/testsuite/main.rs index 72146c4ab79..385b3690b63 100644 --- a/tests/testsuite/main.rs +++ b/tests/testsuite/main.rs @@ -85,6 +85,7 @@ mod doc; mod docscrape; mod edition; mod error; +mod feature_unification; mod features; mod features2; mod features_namespaced; From a471adbd2cda58f6a0cfe0f7ed777b140d0801b3 Mon Sep 17 00:00:00 2001 From: Andrew Liu <24590316+aliu@users.noreply.github.com> Date: Sun, 9 Feb 2025 21:40:34 +0900 Subject: [PATCH 2/2] feat: implement workspace feature unification --- src/cargo/core/features.rs | 2 + src/cargo/core/workspace.rs | 28 ++++++++-- src/cargo/ops/cargo_install.rs | 2 + src/cargo/ops/fix.rs | 1 + src/cargo/ops/resolve.rs | 5 ++ src/cargo/util/context/mod.rs | 8 +++ tests/testsuite/cargo/z_help/stdout.term.svg | 56 ++++++++++---------- tests/testsuite/feature_unification.rs | 44 ++++++++++----- 8 files changed, 102 insertions(+), 44 deletions(-) diff --git a/src/cargo/core/features.rs b/src/cargo/core/features.rs index 774e725e07a..64d37016b2a 100644 --- a/src/cargo/core/features.rs +++ b/src/cargo/core/features.rs @@ -770,6 +770,7 @@ unstable_cli_options!( direct_minimal_versions: bool = ("Resolve minimal dependency versions instead of maximum (direct dependencies only)"), doctest_xcompile: bool = ("Compile and run doctests for non-host target using runner config"), dual_proc_macros: bool = ("Build proc-macros for both the host and the target"), + feature_unification: bool = ("Enable new feature unification modes in workspaces"), features: Option>, gc: bool = ("Track cache usage and \"garbage collect\" unused files"), #[serde(deserialize_with = "deserialize_git_features")] @@ -1271,6 +1272,7 @@ impl CliUnstable { "direct-minimal-versions" => self.direct_minimal_versions = parse_empty(k, v)?, "doctest-xcompile" => self.doctest_xcompile = parse_empty(k, v)?, "dual-proc-macros" => self.dual_proc_macros = parse_empty(k, v)?, + "feature-unification" => self.feature_unification = parse_empty(k, v)?, "gc" => self.gc = parse_empty(k, v)?, "git" => { self.git = v.map_or_else( diff --git a/src/cargo/core/workspace.rs b/src/cargo/core/workspace.rs index da27130d957..b988ca6686a 100644 --- a/src/cargo/core/workspace.rs +++ b/src/cargo/core/workspace.rs @@ -21,6 +21,7 @@ use crate::core::{ use crate::core::{EitherManifest, Package, SourceId, VirtualManifest}; use crate::ops; use crate::sources::{PathSource, SourceConfigMap, CRATES_IO_INDEX, CRATES_IO_REGISTRY}; +use crate::util::context::FeatureUnification; use crate::util::edit_distance; use crate::util::errors::{CargoResult, ManifestError}; use crate::util::interning::InternedString; @@ -112,7 +113,8 @@ pub struct Workspace<'gctx> { /// and other places that use rust version. /// This is set based on the resolver version, config settings, and CLI flags. resolve_honors_rust_version: bool, - + /// The feature unification mode used when building packages. + resolve_feature_unification: FeatureUnification, /// Workspace-level custom metadata custom_metadata: Option, @@ -246,6 +248,7 @@ impl<'gctx> Workspace<'gctx> { requested_lockfile_path: None, resolve_behavior: ResolveBehavior::V1, resolve_honors_rust_version: false, + resolve_feature_unification: FeatureUnification::Selected, custom_metadata: None, local_overlays: HashMap::new(), } @@ -307,13 +310,20 @@ impl<'gctx> Workspace<'gctx> { } } } - if let CargoResolverConfig { - incompatible_rust_versions: Some(incompatible_rust_versions), - } = self.gctx().get::("resolver")? - { + let config = self.gctx().get::("resolver")?; + if let Some(incompatible_rust_versions) = config.incompatible_rust_versions { self.resolve_honors_rust_version = incompatible_rust_versions == IncompatibleRustVersions::Fallback; } + if self.gctx().cli_unstable().feature_unification { + self.resolve_feature_unification = config + .feature_unification + .unwrap_or(FeatureUnification::Selected); + } else if config.feature_unification.is_some() { + self.gctx() + .shell() + .warn("ignoring `resolver.feature-unification` without `-Zfeature-unification`")?; + }; Ok(()) } @@ -663,6 +673,14 @@ impl<'gctx> Workspace<'gctx> { self.resolve_honors_rust_version } + pub fn set_resolve_feature_unification(&mut self, feature_unification: FeatureUnification) { + self.resolve_feature_unification = feature_unification; + } + + pub fn resolve_feature_unification(&self) -> FeatureUnification { + self.resolve_feature_unification + } + pub fn custom_metadata(&self) -> Option<&toml::Value> { self.custom_metadata.as_ref() } diff --git a/src/cargo/ops/cargo_install.rs b/src/cargo/ops/cargo_install.rs index 74583deebcb..6e5222810dd 100644 --- a/src/cargo/ops/cargo_install.rs +++ b/src/cargo/ops/cargo_install.rs @@ -9,6 +9,7 @@ use crate::ops::{common_for_install_and_uninstall::*, FilterRule}; use crate::ops::{CompileFilter, Packages}; use crate::sources::source::Source; use crate::sources::{GitSource, PathSource, SourceConfigMap}; +use crate::util::context::FeatureUnification; use crate::util::errors::CargoResult; use crate::util::{Filesystem, GlobalContext, Rustc}; use crate::{drop_println, ops}; @@ -862,6 +863,7 @@ fn make_ws_rustc_target<'gctx>( ws.set_resolve_honors_rust_version(Some(false)); ws }; + ws.set_resolve_feature_unification(FeatureUnification::Selected); ws.set_ignore_lock(gctx.lock_update_allowed()); ws.set_requested_lockfile_path(lockfile_path.map(|p| p.to_path_buf())); // if --lockfile-path is set, imply --locked diff --git a/src/cargo/ops/fix.rs b/src/cargo/ops/fix.rs index fdd2b33f1df..1d5d5a94009 100644 --- a/src/cargo/ops/fix.rs +++ b/src/cargo/ops/fix.rs @@ -122,6 +122,7 @@ pub fn fix( } let mut ws = Workspace::new(&root_manifest, gctx)?; ws.set_resolve_honors_rust_version(Some(original_ws.resolve_honors_rust_version())); + ws.set_resolve_feature_unification(original_ws.resolve_feature_unification()); ws.set_requested_lockfile_path(opts.requested_lockfile_path.clone()); // Spin up our lock server, which our subprocesses will use to synchronize fixes. diff --git a/src/cargo/ops/resolve.rs b/src/cargo/ops/resolve.rs index 223833ac8c3..8ed6fd1a693 100644 --- a/src/cargo/ops/resolve.rs +++ b/src/cargo/ops/resolve.rs @@ -75,6 +75,7 @@ use crate::core::Workspace; use crate::ops; use crate::sources::RecursivePathSource; use crate::util::cache_lock::CacheLockMode; +use crate::util::context::FeatureUnification; use crate::util::errors::CargoResult; use crate::util::CanonicalUrl; use anyhow::Context as _; @@ -144,6 +145,10 @@ pub fn resolve_ws_with_opts<'gctx>( force_all_targets: ForceAllTargets, dry_run: bool, ) -> CargoResult> { + let specs = match ws.resolve_feature_unification() { + FeatureUnification::Selected => specs, + FeatureUnification::Workspace => &ops::Packages::All(Vec::new()).to_package_id_specs(ws)?, + }; let mut registry = ws.package_registry()?; let (resolve, resolved_with_overrides) = if ws.ignore_lock() { let add_patches = true; diff --git a/src/cargo/util/context/mod.rs b/src/cargo/util/context/mod.rs index 45a8ba2c57b..0b388978d69 100644 --- a/src/cargo/util/context/mod.rs +++ b/src/cargo/util/context/mod.rs @@ -2745,6 +2745,7 @@ impl BuildTargetConfig { #[serde(rename_all = "kebab-case")] pub struct CargoResolverConfig { pub incompatible_rust_versions: Option, + pub feature_unification: Option, } #[derive(Debug, Deserialize, PartialEq, Eq)] @@ -2754,6 +2755,13 @@ pub enum IncompatibleRustVersions { Fallback, } +#[derive(Copy, Clone, Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum FeatureUnification { + Selected, + Workspace, +} + #[derive(Deserialize, Default)] #[serde(rename_all = "kebab-case")] pub struct TermConfig { diff --git a/tests/testsuite/cargo/z_help/stdout.term.svg b/tests/testsuite/cargo/z_help/stdout.term.svg index 2c6b292a5be..45700298477 100644 --- a/tests/testsuite/cargo/z_help/stdout.term.svg +++ b/tests/testsuite/cargo/z_help/stdout.term.svg @@ -1,4 +1,4 @@ - +