mod cli;
mod execute;
use self::cli::Cargo;
use crate::{
config::Config,
feature::FeatureMatrix,
runtime::execute::{Task, TaskResult},
};
use anyhow::{Result, anyhow};
use cargo_metadata::{Metadata, MetadataCommand, Package};
use clap::Parser;
use figment::{
Figment,
providers::{Format, Json},
};
use std::{ffi::OsString, path::PathBuf};
use yansi::Paint;
pub(crate) fn run<I, T>(args: Option<I>) -> Result<()>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
let cli = if let Some(args) = args {
Cargo::try_parse_from(args)?
} else {
Cargo::try_parse()?
};
match cli {
Cargo::Matrix(matrix_args) => {
let manifest_path = matrix_args.manifest_path();
let metadata = load_metadata(manifest_path.as_ref())?;
let channel = matrix_args.channel().as_deref().unwrap_or("default");
let matricies: Vec<(&Package, FeatureMatrix)> = get_workspace_members(&metadata)
.map(generate_config)
.filter_map(Result::ok)
.filter(|(_, config)| !config.skip_package())
.map(|(package, config)| generate_matrix(package, &config, channel))
.filter_map(Result::ok)
.collect();
println!();
println!(
"{} Using channel config '{channel}'",
Paint::cyan(" Channel").bold()
);
println!();
let command = matrix_args.command();
let matricies = if let Some(package) = matrix_args.package() {
matricies
.iter()
.filter(|(pkg, _)| pkg.name == *package)
.cloned()
.collect()
} else {
let num_chunks = matrix_args.num_chunks();
let chunk = matrix_args.chunk();
if *chunk == 0 {
return Err(anyhow!("chunk argument cannot be 0"));
}
if *num_chunks == 0 {
return Err(anyhow!("num_chunks argument cannot be 0"));
}
if chunk > num_chunks {
return Err(anyhow!("chunk must be less than or equal to num_chunks"));
}
if matricies.is_empty() {
return Ok(());
}
let mut chunk_size = matricies.len() / num_chunks;
if !matricies.len().is_multiple_of(*num_chunks) {
chunk_size += 1;
}
let Some(matrix_chunk) = matricies.chunks(chunk_size).nth(chunk - 1) else {
println!(
"Chunk is empty (did you ask for more chunks than there are packages?"
);
return Ok(());
};
if *num_chunks != 1 {
let len = matrix_chunk.len();
let packages: String = matrix_chunk
.iter()
.flat_map(|(p, _)| [&p.name, ","])
.collect();
let packages = packages.trim_end_matches(',');
print!("{}", Paint::cyan(" Chunking ").bold());
println!(
"Running on chunk {chunk} out of {num_chunks} ({len} package(s): {packages})"
);
}
matrix_chunk.to_vec()
};
for (package, matrix) in matricies {
let task_result = Task::new(
command.clone(),
package.name.to_string(),
matrix,
manifest_path.clone(),
*matrix_args.dry_run(),
)
.execute()?;
match task_result {
TaskResult::Success => {}
TaskResult::Fail(code) => return Err(anyhow!("task failed: {code}")),
}
}
}
}
Ok(())
}
fn get_workspace_members(metadata: &Metadata) -> impl Iterator<Item = &Package> + '_ {
metadata
.packages
.iter()
.filter(|package| metadata.workspace_members.contains(&package.id))
}
fn load_metadata(manifest_path: Option<&PathBuf>) -> Result<Metadata> {
let mut cmd = MetadataCommand::new();
if let Some(manifest_path) = manifest_path {
let _ = cmd.manifest_path(manifest_path);
}
Ok(cmd.exec()?)
}
fn generate_config(package: &Package) -> Result<(&Package, Config)> {
let figment = if let Some(package_config) = package.metadata.get("cargo-matrix") {
Figment::from(Config::default())
.merge(Figment::from(Json::string(&package_config.to_string())))
} else {
Figment::from(Config::default())
};
Ok((package, Config::from(figment)?))
}
fn generate_matrix<'a>(
package: &'a Package,
config: &Config,
channel: &str,
) -> Result<(&'a Package, FeatureMatrix)> {
Ok((package, FeatureMatrix::new(package, config, channel)?))
}
#[cfg(test)]
mod tests {
use super::*;
fn deny_manifest() -> String {
format!(
"{}/tests/testdata/deny/Cargo.toml",
env!("CARGO_MANIFEST_DIR")
)
}
fn run_dry(subcommand: &str, matrix_args: &[&str]) -> Result<()> {
let manifest = deny_manifest();
let mut args: Vec<&str> =
vec!["cargo", "matrix", "--dry-run", "--manifest-path", &manifest];
args.extend_from_slice(matrix_args);
args.push(subcommand);
run(Some(args))
}
fn run_nodry(subcommand: &str, matrix_args: &[&str]) -> Result<()> {
let manifest = deny_manifest();
let mut args: Vec<&str> = vec!["cargo", "matrix", "--manifest-path", &manifest];
args.extend_from_slice(matrix_args);
args.push(subcommand);
run(Some(args))
}
#[test]
fn run_build_dry_run() {
assert!(run_dry("build", &[]).is_ok());
}
#[test]
fn run_check_dry_run() {
assert!(run_dry("check", &[]).is_ok());
}
#[test]
fn run_clippy_dry_run() {
assert!(run_dry("clippy", &[]).is_ok());
}
#[test]
fn run_test_dry_run() {
assert!(run_dry("test", &[]).is_ok());
}
#[test]
fn run_nextest_dry_run() {
assert!(run_dry("nextest", &[]).is_ok());
}
#[test]
fn run_llvm_cov_dry_run() {
assert!(run_dry("llvm-cov", &[]).is_ok());
}
#[test]
fn run_with_package_filter() {
assert!(run_dry("build", &["--package", "sample"]).is_ok());
}
#[test]
fn run_nonexistent_package_returns_ok() {
assert!(run_dry("build", &["--package", "does-not-exist"]).is_ok());
}
#[test]
fn run_chunk_zero_errors() {
let result = run_nodry("build", &["--num-chunks", "2", "--chunk", "0"]);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("chunk argument cannot be 0")
);
}
#[test]
fn run_chunk_exceeds_num_chunks_errors() {
let result = run_nodry("build", &["--num-chunks", "2", "--chunk", "3"]);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("chunk must be less than or equal to num_chunks")
);
}
#[test]
fn run_valid_chunk() {
assert!(run_dry("build", &["--num-chunks", "2", "--chunk", "1"]).is_ok());
}
#[test]
fn run_with_channel() {
assert!(run_dry("build", &["--channel", "nightly"]).is_ok());
}
#[test]
fn run_multi_token_command_dry_run() {
let manifest = deny_manifest();
let args = vec![
"cargo",
"matrix",
"--dry-run",
"--manifest-path",
&manifest,
"nextest",
"run",
];
assert!(run(Some(args)).is_ok());
}
#[test]
fn run_with_passthrough_args_dry_run() {
let manifest = deny_manifest();
let args = vec![
"cargo",
"matrix",
"--dry-run",
"--manifest-path",
&manifest,
"clippy",
"--",
"-D",
"warnings",
];
assert!(run(Some(args)).is_ok());
}
#[test]
fn run_missing_command_errors() {
let manifest = deny_manifest();
let args = vec!["cargo", "matrix", "--manifest-path", &manifest];
assert!(run(Some(args)).is_err());
}
fn skip_package_manifest() -> String {
format!(
"{}/tests/testdata/skip_package/Cargo.toml",
env!("CARGO_MANIFEST_DIR")
)
}
#[test]
fn run_skip_package_returns_ok() {
let manifest = skip_package_manifest();
let args = vec![
"cargo",
"matrix",
"--dry-run",
"--manifest-path",
&manifest,
"build",
];
assert!(run(Some(args)).is_ok());
}
}