pub mod cfg_eval;
pub mod cli;
pub mod config;
pub mod diagnostics_only;
pub mod implication;
pub mod package;
pub mod runner;
pub mod target;
mod target_install;
pub mod target_plan;
pub mod tee;
pub mod workspace;
pub use cli::{ArgumentParser, Command, Options, parse_arguments};
pub use package::{FeatureCombinationError, Package};
pub use runner::{
ExitCode, MatrixOptions, color_spec, error_counts, print_feature_matrix_for_target,
print_summary, run_cargo_command_for_target, warning_counts,
};
pub use workspace::Workspace;
use cfg_eval::RustcCfgEvaluator;
use cli::cargo_subcommand;
use color_eyre::eyre;
use runner::print_feature_combination_error;
use std::process;
use target::RustcTargetEnvironment;
static WARNING_COLOR: std::sync::LazyLock<termcolor::ColorSpec> = std::sync::LazyLock::new(|| {
let mut spec = termcolor::ColorSpec::new();
spec.set_fg(Some(termcolor::Color::Yellow));
spec.set_bold(true);
spec
});
static NOTE_COLOR: std::sync::LazyLock<termcolor::ColorSpec> = std::sync::LazyLock::new(|| {
let mut spec = termcolor::ColorSpec::new();
spec.set_fg(Some(termcolor::Color::Cyan));
spec.set_bold(true);
spec
});
macro_rules! print_warning {
($($arg:tt)*) => {{
use std::io::Write as _;
use termcolor::WriteColor as _;
let mut stderr = termcolor::StandardStream::stderr(termcolor::ColorChoice::Auto);
let _ = stderr.set_color(&$crate::WARNING_COLOR);
let _ = write!(&mut stderr, "warning");
let _ = stderr.reset();
let _ = writeln!(&mut stderr, ": {}", format_args!($($arg)*));
}};
}
pub(crate) use print_warning;
macro_rules! print_note {
($($arg:tt)*) => {{
use std::io::Write as _;
use termcolor::WriteColor as _;
let mut stderr = termcolor::StandardStream::stderr(termcolor::ColorChoice::Auto);
let _ = stderr.set_color(&$crate::NOTE_COLOR);
let _ = write!(&mut stderr, "note");
let _ = stderr.reset();
let _ = writeln!(&mut stderr, ": {}", format_args!($($arg)*));
}};
}
pub(crate) use print_note;
const WARN_UNKNOWN_SUBCOMMAND: bool = false;
macro_rules! default_metadata_key {
() => {
"cargo-fc"
};
}
pub(crate) const METADATA_KEYS: &[&str] = &[
"cargo-feature-combinations",
"feature-combinations",
"cargo-fc",
"fc",
];
pub(crate) const DEFAULT_METADATA_KEY: &str = default_metadata_key!();
pub(crate) const DEFAULT_PKG_METADATA_SECTION: &str =
concat!("package.metadata.", default_metadata_key!());
pub(crate) fn find_metadata_value(
metadata: &serde_json::Value,
) -> Option<(&serde_json::Value, &'static str)> {
for &key in METADATA_KEYS {
if let Some(value) = metadata.get(key) {
return Some((value, key));
}
}
None
}
pub(crate) fn pkg_metadata_section(key: &str) -> String {
format!("package.metadata.{key}")
}
pub(crate) fn ws_metadata_section(key: &str) -> String {
format!("workspace.metadata.{key}")
}
pub fn run(bin_name: &str) -> eyre::Result<()> {
color_eyre::install()?;
let (options, cargo_args) = parse_arguments(bin_name)?;
if let Some(Command::Help) = options.command {
cli::print_help();
return Ok(());
}
if let Some(Command::Version) = options.command {
println!("cargo-{bin_name} v{}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
let mut cmd = cargo_metadata::MetadataCommand::new();
if let Some(ref manifest_path) = options.manifest_path {
cmd.manifest_path(manifest_path);
}
let metadata = cmd.exec()?;
let ws_config = metadata.workspace_config()?;
let packages = select_candidate_packages(&metadata, &options)?;
let configs: Vec<config::Config> = packages
.iter()
.map(|package| package.config())
.collect::<eyre::Result<Vec<_>>>()?;
let selected: Vec<target_plan::SelectedPackage<'_>> = packages
.iter()
.zip(&configs)
.map(|(package, config)| target_plan::SelectedPackage { package, config })
.collect();
let cargo_args_owned = cargo_args;
let cargo_args: Vec<&str> = cargo_args_owned.iter().map(String::as_str).collect();
let cli_target = target::parse_cli_target(&cargo_args_owned);
let ws_key = find_metadata_value(&metadata.workspace_metadata)
.map_or(DEFAULT_METADATA_KEY, |(_, key)| key);
let capability_allowed =
resolve_capability_and_warn(&options, &cargo_args, &ws_config, ws_key, &selected);
let env = RustcTargetEnvironment;
let mut evaluator = RustcCfgEvaluator::default();
let base_exclude = metadata.base_workspace_exclude_packages()?;
let target_plans = target_plan::build_target_plans(
&selected,
&ws_config,
&base_exclude,
cli_target.as_deref(),
capability_allowed,
&env,
&mut evaluator,
)?;
let result = match options.command {
Some(Command::Help | Command::Version) => Ok(None),
Some(Command::FeatureMatrix { pretty }) => {
let plan_set = runner::build_execution_plans(
&target_plans,
&options,
options.packages_only,
&mut evaluator,
)?;
if options.install_missing_targets {
print_note!(
"--install-missing-targets has no effect for matrix output; matrix only prints planned targets"
);
}
if options.aggregate_targets {
print_note!(
"--aggregate-targets has no effect for matrix output; matrix rows are always per target"
);
}
let matrix_opts = runner::MatrixOptions {
pretty,
packages_only: options.packages_only,
no_prune_implied: options.no_prune_implied,
};
runner::print_matrix_for_execution_plans(&plan_set, &matrix_opts)
}
None => {
if WARN_UNKNOWN_SUBCOMMAND
&& cargo_subcommand(cargo_args.as_slice()) == cli::CargoSubcommand::Other
{
print_warning!(
"`cargo {bin_name}` only supports cargo's `build`, `test`, `run`, `check`, `doc`, and `clippy` subcommands"
);
}
let plan_set =
runner::build_execution_plans(&target_plans, &options, false, &mut evaluator)?;
maybe_install_missing_targets(&options, &ws_config, &plan_set, &env, &cargo_args)?;
let mode = resolve_execution_mode(&options, &cargo_args, &plan_set);
runner::run_execution_plans(&plan_set, cargo_args, &options, mode)
}
};
match result {
Ok(Some(exit_code)) => process::exit(exit_code),
Ok(None) => Ok(()),
Err(err) => {
if let Some(e) = err.downcast_ref::<FeatureCombinationError>() {
print_feature_combination_error(e);
process::exit(2);
}
Err(err)
}
}
}
fn select_candidate_packages<'a>(
metadata: &'a cargo_metadata::Metadata,
options: &Options,
) -> eyre::Result<Vec<&'a cargo_metadata::Package>> {
let mut packages = metadata.candidate_packages_for_fc()?;
if options.manifest_path.is_some()
&& options.packages.is_empty()
&& let Some(root) = metadata.root_package()
{
packages.retain(|p| p.id == root.id);
}
packages.retain(|p| !options.exclude_packages.contains(p.name.as_str()));
if options.only_packages_with_lib_target {
packages.retain(|p| {
p.targets
.iter()
.any(|t| t.kind.contains(&cargo_metadata::TargetKind::Lib))
});
}
if !options.packages.is_empty() {
packages.retain(|p| options.packages.contains(p.name.as_str()));
}
Ok(packages)
}
fn maybe_install_missing_targets(
options: &Options,
ws_config: &config::WorkspaceConfig,
plan_set: &runner::ExecutionPlanSet<'_>,
env: &impl target::TargetEnvironment,
cargo_args: &[&str],
) -> eyre::Result<()> {
if options.install_missing_targets || ws_config.install_missing_targets {
let installer =
target_install::RustupTargetInstaller::new(cli::rustup_toolchain(cargo_args));
target_install::ensure_missing_targets_installed(plan_set, env, &installer)?;
}
Ok(())
}
fn resolve_capability_and_warn(
options: &Options,
cargo_args: &[&str],
ws_config: &config::WorkspaceConfig,
ws_key: &str,
selected: &[target_plan::SelectedPackage<'_>],
) -> bool {
if options.no_targets {
return false;
}
if matches!(options.command, Some(Command::FeatureMatrix { .. })) {
return true;
}
let token = cli::cargo_subcommand_token(cargo_args);
let policy = cli::configured_target_policy(token.as_deref(), &ws_config.subcommand_overrides);
if policy.enabled {
return true;
}
let has_raw_configured_targets = !ws_config.workspace_targets.is_empty()
|| selected.iter().any(|s| {
s.config
.package_targets
.as_ref()
.is_some_and(|t| !t.is_empty())
});
if has_raw_configured_targets
&& !policy.explicit
&& let Some(token) = token.as_deref().filter(|t| !t.is_empty())
{
print_warning!(
"not passing configured targets to cargo command `{token}` because it has no targets capability"
);
eprintln!(
"hint: add [{}.subcommands.{token}] targets = true if this command accepts --target",
ws_metadata_section(ws_key),
);
}
false
}
fn resolve_execution_mode(
options: &Options,
cargo_args: &[&str],
plan_set: &runner::ExecutionPlanSet<'_>,
) -> runner::TargetExecutionMode {
use runner::TargetExecutionMode;
if !options.aggregate_targets {
return TargetExecutionMode::SerialPerTarget;
}
if plan_set.plans.len() <= 1 {
print_note!("--aggregate-targets has no effect for a single target; running normally");
return TargetExecutionMode::SerialPerTarget;
}
if cargo_subcommand(cargo_args) == cli::CargoSubcommand::Run {
print_note!(
"--aggregate-targets does not apply to `run` (cargo runs one target at a time); running targets serially"
);
return TargetExecutionMode::SerialPerTarget;
}
if plan_set.show_pruned {
print_note!(
"--aggregate-targets is disabled because pruned summaries are target-specific; running targets serially"
);
return TargetExecutionMode::SerialPerTarget;
}
TargetExecutionMode::Aggregate
}
#[cfg(test)]
mod test {
use super::*;
use color_eyre::eyre;
use serde_json::json;
fn execution_plan_set(
targets: &[&str],
show_pruned: bool,
) -> runner::ExecutionPlanSet<'static> {
runner::ExecutionPlanSet {
plans: targets
.iter()
.map(|target| runner::ExecutionPlan {
target: target::TargetTriple((*target).to_string()),
package_plans: Vec::new(),
})
.collect(),
show_pruned,
show_target: targets.len() > 1,
}
}
#[test]
fn aggregate_execution_mode_selected_for_supported_multi_target_command() {
let options = Options {
aggregate_targets: true,
..Options::default()
};
let plan_set = execution_plan_set(&["t1", "t2"], false);
assert_eq!(
resolve_execution_mode(&options, &["check"], &plan_set),
runner::TargetExecutionMode::Aggregate
);
}
#[test]
fn aggregate_execution_mode_falls_back_for_run() {
let options = Options {
aggregate_targets: true,
..Options::default()
};
let plan_set = execution_plan_set(&["t1", "t2"], false);
assert_eq!(
resolve_execution_mode(&options, &["run"], &plan_set),
runner::TargetExecutionMode::SerialPerTarget
);
}
#[test]
fn aggregate_execution_mode_falls_back_for_pruned_summaries() {
let options = Options {
aggregate_targets: true,
..Options::default()
};
let plan_set = execution_plan_set(&["t1", "t2"], true);
assert_eq!(
resolve_execution_mode(&options, &["check"], &plan_set),
runner::TargetExecutionMode::SerialPerTarget
);
}
#[test]
fn aggregate_execution_mode_is_noop_for_single_target() {
let options = Options {
aggregate_targets: true,
..Options::default()
};
let plan_set = execution_plan_set(&["t1"], false);
assert_eq!(
resolve_execution_mode(&options, &["check"], &plan_set),
runner::TargetExecutionMode::SerialPerTarget
);
}
#[test]
fn no_targets_flag_denies_capability() {
let options = Options {
no_targets: true,
..Options::default()
};
let ws = config::WorkspaceConfig::default();
assert!(!resolve_capability_and_warn(
&options,
&["check"],
&ws,
DEFAULT_METADATA_KEY,
&[]
));
}
#[test]
fn builtin_command_allows_capability_without_no_targets() {
let options = Options::default();
let ws = config::WorkspaceConfig::default();
assert!(resolve_capability_and_warn(
&options,
&["check"],
&ws,
DEFAULT_METADATA_KEY,
&[]
));
}
#[test]
fn builtin_command_can_be_disabled_by_workspace_policy() {
let options = Options::default();
let mut ws = config::WorkspaceConfig::default();
ws.subcommand_overrides.insert(
"build".to_string(),
config::CommandTargetCapability { targets: false },
);
assert!(!resolve_capability_and_warn(
&options,
&["build"],
&ws,
DEFAULT_METADATA_KEY,
&[]
));
}
#[test]
fn no_targets_flag_denies_capability_for_matrix() {
let options = Options {
no_targets: true,
command: Some(Command::FeatureMatrix { pretty: false }),
..Options::default()
};
let ws = config::WorkspaceConfig::default();
let empty: [&str; 0] = [];
assert!(!resolve_capability_and_warn(
&options,
&empty,
&ws,
DEFAULT_METADATA_KEY,
&[]
));
}
#[test]
fn find_metadata_value_returns_none_for_empty_object() {
let meta = json!({});
assert!(find_metadata_value(&meta).is_none());
}
#[test]
fn find_metadata_value_returns_none_for_unrelated_keys() {
let meta = json!({ "other-tool": { "key": "value" } });
assert!(find_metadata_value(&meta).is_none());
}
#[test]
fn find_metadata_value_finds_each_alias() -> eyre::Result<()> {
for &alias in METADATA_KEYS {
let meta = json!({ alias: { "exclude_features": ["default"] } });
let (value, matched) =
find_metadata_value(&meta).ok_or_else(|| eyre::eyre!("no match for {alias}"))?;
assert_eq!(matched, alias);
assert!(value.get("exclude_features").is_some());
}
Ok(())
}
#[test]
fn find_metadata_value_prefers_longest_alias() -> eyre::Result<()> {
let meta = json!({
"cargo-feature-combinations": { "source": "long" },
"fc": { "source": "short" },
});
let (value, matched) = find_metadata_value(&meta).ok_or_else(|| eyre::eyre!("no match"))?;
assert_eq!(matched, "cargo-feature-combinations");
assert_eq!(value["source"], "long");
Ok(())
}
#[test]
fn find_metadata_value_prefers_cargo_fc_over_fc() -> eyre::Result<()> {
let meta = json!({
"cargo-fc": { "source": "cargo-fc" },
"fc": { "source": "fc" },
});
let (_, matched) = find_metadata_value(&meta).ok_or_else(|| eyre::eyre!("no match"))?;
assert_eq!(matched, "cargo-fc");
Ok(())
}
#[test]
fn pkg_metadata_section_formats_correctly() {
assert_eq!(
pkg_metadata_section("cargo-fc"),
"package.metadata.cargo-fc"
);
assert_eq!(pkg_metadata_section("fc"), "package.metadata.fc");
}
#[test]
fn ws_metadata_section_formats_correctly() {
assert_eq!(
ws_metadata_section("cargo-fc"),
"workspace.metadata.cargo-fc"
);
}
#[test]
fn default_metadata_key_is_cargo_fc() {
assert_eq!(DEFAULT_METADATA_KEY, "cargo-fc");
}
#[test]
fn default_pkg_metadata_section_uses_default_key() {
assert_eq!(DEFAULT_PKG_METADATA_SECTION, "package.metadata.cargo-fc");
}
}