pub mod cfg_eval;
pub mod cli;
pub mod config;
pub mod package;
pub mod runner;
pub mod target;
pub mod tee;
pub mod workspace;
pub use cli::{ArgumentParser, Command, Options, parse_arguments};
pub use package::{FeatureCombinationError, Package};
pub use runner::{
ExitCode, color_spec, error_counts, print_feature_matrix, print_feature_matrix_for_target,
print_summary, run_cargo_command, run_cargo_command_for_target, warning_counts,
};
pub use workspace::Workspace;
use cfg_eval::RustcCfgEvaluator;
use cli::cargo_subcommand;
use runner::print_feature_combination_error;
use target::{RustcTargetDetector, TargetDetector};
use color_eyre::eyre;
use std::process;
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 mut packages = metadata.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()));
}
let cargo_args_owned = cargo_args;
let cargo_args: Vec<&str> = cargo_args_owned.iter().map(String::as_str).collect();
let detector = RustcTargetDetector::default();
let target = detector.detect_target(&cargo_args_owned)?;
let mut evaluator = RustcCfgEvaluator::default();
let result = match options.command {
Some(Command::Help | Command::Version) => Ok(None),
Some(Command::FeatureMatrix { pretty }) => print_feature_matrix_for_target(
&packages,
pretty,
options.packages_only,
&target,
&mut evaluator,
),
None => {
if cargo_subcommand(cargo_args.as_slice()) == cli::CargoSubcommand::Other {
eprintln!(
"warning: `cargo {bin_name}` only supports cargo's `build`, `test`, `run`, `check`, `doc`, and `clippy` subcommands",
);
}
run_cargo_command_for_target(&packages, cargo_args, &options, &target, &mut evaluator)
}
};
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)
}
}
}
#[cfg(test)]
mod test {
use super::*;
use color_eyre::eyre;
use serde_json::json;
#[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]");
}
}