Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
}

#[derive(Serialize, Deserialize, Default, Debug)]
pub struct DeprecatedConfig {
#[serde(default)]
Expand Down
106 changes: 90 additions & 16 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<ColorSpec> = LazyLock::new(|| color_spec(Color::Cyan, true));
static RED: LazyLock<ColorSpec> = LazyLock::new(|| color_spec(Color::Red, true));
static YELLOW: LazyLock<ColorSpec> = LazyLock::new(|| color_spec(Color::Yellow, true));
Expand Down Expand Up @@ -91,6 +93,40 @@ impl ArgumentParser for Vec<String> {
}
}

pub trait Workspace {
/// Returns the workspace configuration section for feature combinations.
fn workspace_config(&self) -> eyre::Result<WorkspaceConfig>;

/// Returns the packages relevant for feature combinations.
fn packages_for_fc(&self) -> eyre::Result<Vec<&cargo_metadata::Package>>;
}

impl Workspace for cargo_metadata::Metadata {
fn workspace_config(&self) -> eyre::Result<WorkspaceConfig> {
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<Vec<&cargo_metadata::Package>> {
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.
///
Expand All @@ -110,12 +146,9 @@ pub trait Package {

impl Package for cargo_metadata::Package {
fn config(&self) -> eyre::Result<Config> {
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
Expand Down Expand Up @@ -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()));
Expand All @@ -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()));
Expand All @@ -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;

Expand Down Expand Up @@ -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<cargo_metadata::Package> {
use cargo_metadata::{PackageBuilder, PackageId};
use cargo_util_schemas::manifest::PackageName;
Expand All @@ -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("")
}
}