From 3f7de75ca02141340489ed45ab13496127b82243 Mon Sep 17 00:00:00 2001 From: Simon Berger Date: Mon, 3 Nov 2025 14:35:45 +0000 Subject: [PATCH 1/2] chore: update Cargo.lock --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index c88a05a..1f0e3a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,7 +84,7 @@ dependencies = [ [[package]] name = "cargo-feature-combinations" -version = "0.0.40" +version = "0.0.41" dependencies = [ "cargo-util-schemas", "cargo_metadata", From 3f0a1677754f969806a6c757f271ff9c1b71b11d Mon Sep 17 00:00:00 2001 From: Simon Berger Date: Mon, 3 Nov 2025 15:37:59 +0000 Subject: [PATCH 2/2] feat: allow packages to be excluded in workspace metadata --- README.md | 3 +- src/config.rs | 7 ++++ src/lib.rs | 106 ++++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 99 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 0400321..b35d2d7 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,8 @@ exclude_features = ["default", "full"] # formerly "denylist" # Include features in the feature combination matrix include_features = ["feature-that-must-always-be-set"] -# When using a cargo workspace, you can exclude packages in the *root* `Cargo.toml` +# When using workspaces, you can exclude packages in the workspace metadata, +# or the metadata of the *root* package. exclude_packages = ["package-a", "package-b"] # In the end, always add these exact combinations to the overall feature matrix, diff --git a/src/config.rs b/src/config.rs index 2cbdeeb..b7592e8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -24,6 +24,13 @@ pub struct Config { pub deprecated: DeprecatedConfig, } +#[derive(Serialize, Deserialize, Default, Debug)] +pub struct WorkspaceConfig { + /// List of package names to exclude from the workspace analysis. + #[serde(default)] + pub exclude_packages: HashSet, +} + #[derive(Serialize, Deserialize, Default, Debug)] pub struct DeprecatedConfig { #[serde(default)] diff --git a/src/lib.rs b/src/lib.rs index 1619086..25e3788 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,7 @@ mod config; mod tee; -use crate::config::Config; +use crate::config::{Config, WorkspaceConfig}; use color_eyre::eyre::{self, WrapErr}; use itertools::Itertools; use regex::Regex; @@ -15,6 +15,8 @@ use std::sync::LazyLock; use std::time::{Duration, Instant}; use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; +const METADATA_KEY: &str = "cargo-feature-combinations"; + static CYAN: LazyLock = LazyLock::new(|| color_spec(Color::Cyan, true)); static RED: LazyLock = LazyLock::new(|| color_spec(Color::Red, true)); static YELLOW: LazyLock = LazyLock::new(|| color_spec(Color::Yellow, true)); @@ -91,6 +93,40 @@ impl ArgumentParser for Vec { } } +pub trait Workspace { + /// Returns the workspace configuration section for feature combinations. + fn workspace_config(&self) -> eyre::Result; + + /// Returns the packages relevant for feature combinations. + fn packages_for_fc(&self) -> eyre::Result>; +} + +impl Workspace for cargo_metadata::Metadata { + fn workspace_config(&self) -> eyre::Result { + let config: WorkspaceConfig = match self.workspace_metadata.get(METADATA_KEY) { + Some(config) => serde_json::from_value(config.clone())?, + None => Default::default(), + }; + Ok(config) + } + + fn packages_for_fc(&self) -> eyre::Result> { + let mut packages = self.workspace_packages(); + + let workspace_config = self.workspace_config()?; + // filter packages based on workspace metadata configuration + packages.retain(|p| !workspace_config.exclude_packages.contains(p.name.as_str())); + + if let Some(root_package) = self.root_package() { + let config = root_package.config()?; + // filter packages based on root package Cargo.toml configuration + packages.retain(|p| !config.exclude_packages.contains(p.name.as_str())); + } + + Ok(packages) + } +} + pub trait Package { /// Parses the config for this package if present. /// @@ -110,12 +146,9 @@ pub trait Package { impl Package for cargo_metadata::Package { fn config(&self) -> eyre::Result { - let mut config = match self.metadata.get("cargo-feature-combinations") { - Some(config) => { - let config: Config = serde_json::from_value(config.clone())?; - config - } - None => Config::default(), + let mut config: Config = match self.metadata.get(METADATA_KEY) { + Some(config) => serde_json::from_value(config.clone())?, + None => Default::default(), }; // handle deprecated config values @@ -795,7 +828,7 @@ pub fn run(bin_name: &str) -> eyre::Result<()> { cmd.manifest_path(manifest_path); } let metadata = cmd.exec()?; - let mut packages = metadata.workspace_packages(); + let mut packages = metadata.packages_for_fc()?; // filter excluded packages via CLI arguments packages.retain(|p| !options.exclude_packages.contains(p.name.as_str())); @@ -809,12 +842,6 @@ pub fn run(bin_name: &str) -> eyre::Result<()> { }); } - if let Some(root_package) = metadata.root_package() { - let config = root_package.config()?; - // filter packages based on root package Cargo.toml configuration - packages.retain(|p| !config.exclude_packages.contains(p.name.as_str())); - } - // filter packages based on CLI options if !options.packages.is_empty() { packages.retain(|p| options.packages.contains(p.name.as_str())); @@ -839,9 +866,9 @@ pub fn run(bin_name: &str) -> eyre::Result<()> { #[cfg(test)] mod test { - use super::{Package, error_counts, warning_counts}; - use crate::config::Config; + use super::{Config, Package, Workspace, error_counts, warning_counts}; use color_eyre::eyre; + use serde_json::json; use similar_asserts::assert_eq as sim_assert_eq; use std::collections::HashSet; @@ -1021,6 +1048,41 @@ mod test { Ok(()) } + #[test] + fn workspace_with_package() -> eyre::Result<()> { + init(); + + let package = package_with_features(&[])?; + let metadata = workspace_builder() + .packages(vec![package.clone()]) + .workspace_members(vec![package.id.clone()]) + .build()?; + + let packages = metadata.packages_for_fc()?; + sim_assert_eq!(packages, vec![&package]); + Ok(()) + } + + #[test] + fn workspace_with_excluded_package() -> eyre::Result<()> { + init(); + + let package = package_with_features(&[])?; + let metadata = workspace_builder() + .packages(vec![package.clone()]) + .workspace_members(vec![package.id.clone()]) + .workspace_metadata(json!({ + "cargo-feature-combinations": { + "exclude_packages": [package.name] + } + })) + .build()?; + + let packages = metadata.packages_for_fc()?; + assert!(packages.is_empty(), "expected no packages after exclusion"); + Ok(()) + } + fn package_with_features(features: &[&str]) -> eyre::Result { use cargo_metadata::{PackageBuilder, PackageId}; use cargo_util_schemas::manifest::PackageName; @@ -1042,4 +1104,16 @@ mod test { .collect(); Ok(package) } + + fn workspace_builder() -> cargo_metadata::MetadataBuilder { + use cargo_metadata::{MetadataBuilder, WorkspaceDefaultMembers}; + + MetadataBuilder::default() + .version(1u8) + .workspace_default_members(WorkspaceDefaultMembers::default()) + .resolve(None) + .workspace_root("") + .workspace_metadata(json!({})) + .target_directory("") + } }