use std::process::{Command, ExitCode};
use ohno::{AppError, IntoAppError};
use crate::cli::EachArgs;
use crate::filter::Predicate;
use crate::plan::{Mode, PackagesExpansion, Plan};
use crate::select::Selection;
use crate::workspace::{Member, Workspace};
pub(crate) fn run(args: &EachArgs) -> Result<ExitCode, AppError> {
let selection = build_selection(args);
let workspace = Workspace::load(args.manifest_path.as_deref()).into_app_err("failed to load workspace")?;
let mut members = selection.resolve(&workspace).into_app_err("failed to resolve package selection")?;
apply_filters(&mut members, args)?;
let packages = if selection.is_whole_workspace() && args.filters.is_empty() && args.exclude_filters.is_empty() {
PackagesExpansion::Workspace
} else {
PackagesExpansion::Explicit
};
let mode = if args.once { Mode::Once } else { Mode::PerPackage };
let plan = Plan::build(&members, mode, args.chdir, packages, &args.command).into_app_err("failed to build command plan")?;
if plan.invocations.is_empty() {
eprintln!("cargo each: selection resolved to no packages; nothing to do");
return Ok(ExitCode::SUCCESS);
}
if args.dry_run {
for inv in &plan.invocations {
match &inv.work_dir {
Some(dir) => println!("(cd {}) {}", dir.display(), shell_join(&inv.argv)),
None => println!("{}", shell_join(&inv.argv)),
}
}
return Ok(ExitCode::SUCCESS);
}
execute(&plan, args.keep_going)
}
fn build_selection(args: &EachArgs) -> Selection {
Selection {
packages: args.packages.clone(),
all: args.workspace,
exclude: args.exclude.clone(),
none: args.none,
}
}
fn apply_filters(members: &mut Vec<&Member>, args: &EachArgs) -> Result<(), AppError> {
let keep = parse_predicates(&args.filters)?;
let drop = parse_predicates(&args.exclude_filters)?;
members.retain(|m| keep.iter().all(|p| p.matches(m)) && !drop.iter().any(|p| p.matches(m)));
Ok(())
}
fn parse_predicates(specs: &[String]) -> Result<Vec<Predicate>, AppError> {
specs
.iter()
.map(|s| Predicate::parse(s).into_app_err("invalid filter predicate"))
.collect()
}
fn execute(plan: &Plan, keep_going: bool) -> Result<ExitCode, AppError> {
let mut any_failed = false;
for inv in &plan.invocations {
if let Some(label) = &inv.label {
eprintln!("cargo each: {label}");
}
let (program, rest) = inv.argv.split_first().expect("Plan::build never emits an empty argv");
let mut command = Command::new(program);
command.args(rest);
if let Some(dir) = &inv.work_dir {
command.current_dir(dir);
}
let status = match command.status() {
Ok(status) => status,
Err(err) if keep_going => {
eprintln!("cargo each: failed to spawn `{program}`: {err}");
any_failed = true;
continue;
}
other => other.into_app_err(format!("failed to spawn `{program}`"))?,
};
if !status.success() {
if !keep_going {
return Ok(ExitCode::from(exit_byte(status.code())));
}
any_failed = true;
}
}
Ok(if any_failed { ExitCode::from(1) } else { ExitCode::SUCCESS })
}
fn shell_join(argv: &[String]) -> String {
argv.iter()
.map(|a| {
if a.contains(char::is_whitespace) {
format!("\"{a}\"")
} else {
a.clone()
}
})
.collect::<Vec<_>>()
.join(" ")
}
fn exit_byte(raw: Option<i32>) -> u8 {
let Some(raw) = raw else { return 1 };
let byte = u8::try_from(raw & 0xFF).expect("`raw & 0xFF` masks to the low byte, always within 0..=255");
if byte == 0 { 1 } else { byte }
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::exit_byte;
#[test]
fn signal_terminated_child_maps_to_one() {
assert_eq!(exit_byte(None), 1);
}
#[test]
fn in_range_codes_pass_through() {
assert_eq!(exit_byte(Some(1)), 1);
assert_eq!(exit_byte(Some(2)), 2);
assert_eq!(exit_byte(Some(255)), 255);
}
#[test]
fn wide_codes_reduce_to_low_byte() {
assert_eq!(exit_byte(Some(259)), 3);
assert_eq!(exit_byte(Some(257)), 1);
}
#[test]
fn nonzero_code_with_zero_low_byte_maps_to_one() {
assert_eq!(exit_byte(Some(256)), 1);
assert_eq!(exit_byte(Some(512)), 1);
}
}