cargo-each 0.5.0

A cargo subcommand that runs a command over a cargo-style selection of workspace members
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! `cargo-each`: run a command over Cargo workspace members or JSON Lines
//! records.
//!
//! `cargo-each` resolves either a package selection expressed with the same
//! selectors as `cargo build` or a stream of JSON objects. It optionally
//! narrows Cargo selections with package predicates and runs a command once per
//! member, matching Cargo target, whole selected set, or JSON record. It
//! replaces hand-rolled shell loops with one cross-platform command.
//!
//! `cargo-each` ships as an executable only; it is a cargo subcommand, not a
//! library dependency.
#![doc(html_logo_url = "https://media.githubusercontent.com/media/microsoft/ox-tools/refs/heads/main/crates/cargo-each/logo.png")]
#![doc(html_favicon_url = "https://media.githubusercontent.com/media/microsoft/ox-tools/refs/heads/main/crates/cargo-each/favicon.ico")]
//!
//! # Installation
//!
//! ```text
//! cargo install cargo-each
//! ```
//!
//! # Usage
//!
//! ```text
//! cargo each [SELECTION | JSON INPUT] [FILTERS] [EXECUTION] -- <COMMAND> [ARG...]
//! ```
//!
//! Everything after `--` is the command template; `cargo-each` spawns it
//! directly (argv, not a shell string) after substituting placeholders.
//! Input is either a Cargo workspace selection or JSON Lines records; the two
//! sources are deliberately mutually exclusive.
//!
//! ## Selection (mirrors `cargo build`)
//!
//! - `-p` / `--package <SPEC>` — select a member. Repeatable. `SPEC` is a
//!   package name, a `name@version` spec, or a Unix glob (`tokio-*`).
//! - `--package-file <PATH>` — read package specs from a UTF-8 file, one per
//!   nonempty line. Repeatable; specs are unioned with `--package`. An empty
//!   file explicitly selects no members. One leading UTF-8 byte-order mark is
//!   ignored.
//! - `--workspace` / `--all` — select every workspace member.
//! - `--exclude <SPEC>` — drop a member (with `--workspace`). Repeatable.
//! - `--none` — explicitly select zero members (a no-op that exits 0).
//!
//! When nothing is named the default is cargo `default-members`, exactly
//! like `cargo build`; pass `--workspace` for every member. A selector that
//! matches no member is an error, so typos fail loudly. Package files contain
//! package specs only: comments, command-line tokens, malformed input, and
//! missing, unreadable, or non-UTF-8 files are errors.
//!
//! ## Filters
//!
//! `--filter` accepts Boolean expressions using `not`, `and`, `or`, and
//! parentheses, with conventional precedence. Repeated `--filter` expressions
//! are AND-combined; use `not` to exclude matching members. Metadata values
//! containing whitespace or Boolean syntax can be double-quoted. Expression
//! atoms:
//!
//! - `lib` / `bin` / `target-kind:<kind>` — target-kind membership.
//! - `doctest` — Cargo reports at least one target with doctests enabled.
//! - `publishable` — Cargo permits publishing the package.
//! - `feature:<name>` — the package declares the feature.
//! - `dep:<name>` — the member declares `<name>` as a dependency.
//! - `metadata:<dotted.key>` — `package.metadata.<dotted.key>` is present.
//! - `metadata:<dotted.key>=<value>` — that key equals `<value>` (numeric
//!   compare when both sides parse as a number, else string compare).
//!
//! ## Execution modes
//!
//! - *per-package* (default): run the command once per selected member, in
//!   name order, substituting the per-package placeholders below.
//! - `--once`: run the command exactly once when the set is non-empty (skip
//!   when empty), using the `{packages}` placeholder to inject the selection.
//! - `--json-lines` / `--json-lines-file`: bypass Cargo selection and run once
//!   per JSON object, substituting top-level string fields through
//!   `{json:key}`.
//! - `--each-target <KIND>`: run once per matching Cargo target, using
//!   `{target}` plus the package placeholders. Repeated kinds are OR-combined;
//!   `--target-required-feature` further narrows targets.
//!
//! `--keep-going` runs every invocation and exits non-zero if any failed
//! (default is fail-fast). `--jobs <N|auto>` bounds concurrent per-package,
//! per-target, or JSON-record work. Omitting it runs exactly one invocation at
//! a time; `auto` resolves once to the machine's available parallelism.
//! Detection failure is reported explicitly without falling back.
//! `--timeout <DURATION>` terminates
//! each invocation's Windows job object or Unix process group independently
//! (`250ms`, `30s`, or `2m`). Unix descendants can escape a process group by
//! starting a new session, so timeout cleanup is best-effort for those escaped
//! descendants.
//! `--chdir` runs each per-package or per-target command from that member crate
//! root; `--dry-run` prints commands without running them.
//!
//! ## Placeholders
//!
//! Substituted inside each command argument:
//!
//! - `{name}` — bare package name (per-package and per-target).
//! - `{spec}` — `name@version` (per-package and per-target).
//! - `{version}` — package version (per-package and per-target).
//! - `{manifest}` — absolute member `Cargo.toml` path (per-package and
//!   per-target).
//! - `{target}` — Cargo target name (per-target).
//! - `{packages}` — the cargo selection flags for the resolved set
//!   (`--workspace` for the whole workspace, else `--package name@version …`);
//!   valid only in `--once` mode and only as a standalone argument.
//! - `{workspace-rust-version}` — the root `[workspace.package].rust-version`,
//!   or root `[package].rust-version` in a single-package repository; valid in
//!   Cargo-backed per-package, per-target, and `--once` modes. Expands to an
//!   empty string when the root declaration is absent.
//! - `{json:key}` — the top-level string field named `key` in the current
//!   record; valid only in JSON-record mode.
//!
//! Using a placeholder in the wrong mode is a usage error. Every `{json:…}`
//! sequence is validated as a JSON placeholder. Other unrecognized `{…}`
//! sequences (a typo, or a literal brace an argument needs) pass through
//! verbatim to the spawned command — there is no brace-escape, so this
//! passthrough is part of the contract.
//!
//! # Behavior
//!
//! An empty resolved selection (via `--none`, or a filter that removes every
//! member) is a **successful no-op**: `cargo-each` prints a one-line note and
//! exits 0. This is what lets callers drop bespoke nothing-to-do guards.
//! Workspace Rust-version resolution is lazy: it runs only when the command
//! uses `{workspace-rust-version}` and the resolved plan has work. An absent
//! root declaration expands to an empty string. When a root floor exists,
//! every member's resolved minimum must be present and no newer than that
//! floor; malformed or inconsistent declarations remain errors. Substitution
//! is textual, so an empty value does not remove its surrounding argv element.
//! Placeholder mode validation still runs before an empty-plan no-op.
//!
//! JSON-record mode requires one JSON object per nonempty input line and does
//! not load Cargo metadata. Referenced `{json:key}` fields must exist and be
//! strings. Invalid input is rejected before any command is spawned, records
//! retain input order and duplicates, and an empty record set is a successful
//! no-op.
//!
//! The effective worker count is the requested `--jobs` value capped by plan
//! size. An effective count of one uses sequential
//! execution with inherited standard input, output, and error even when the
//! requested value was larger. A genuinely parallel count disconnects child input and
//! buffers stdout and stderr; complete blocks are emitted in deterministic
//! plan order. Fail-fast stops launching after the first
//! observed failure, waits for running work, and chooses the final failure by
//! plan order. `--keep-going` runs the complete plan. Worker panics and
//! unexpected worker-channel disconnections become infrastructure-failure
//! outcomes instead of blocking the scheduler. Worker launch failures retain
//! output already collected at earlier plan indices. Parallel work runs in
//! plan-contiguous waves capped by the effective worker count; each completed
//! wave is emitted and dropped before the next wave starts, bounding retained
//! temporary-file storage. Untimed effective-one execution uses an ordinary
//! child, preserving terminal foreground behavior and Ctrl-C delivery; a
//! post-spawn wait failure makes one best-effort child-termination request.
//! Timed and genuinely parallel commands use a job or process group. Without
//! `--timeout`, cargo-each observes only the leader and does not kill background
//! descendants. Every genuinely parallel invocation redirects stdout and
//! stderr directly to separate unique temporary files.
//! Child writers and parent readers are separately reopened so parent seeks
//! cannot move descendant write positions. Cargo-each records each file's
//! current length when the leader completes (or immediately after the timeout
//! termination request returns), then reads exactly that finite snapshot in
//! plan order without loading unbounded output into memory. Later writes by
//! background or escaped descendants are outside the snapshot. RAII removes
//! cargo-each's directory entry, but a preserved descendant can keep the
//! backing storage allocated and growing until its inherited writer closes.
//! Capture create, reopen, length, seek, and read failures are infrastructure
//! failures.
//!
//! At a timeout, cargo-each makes one termination request for the Windows job
//! or Unix process group and returns without waiting for operating-system
//! teardown to finish. A failed termination request is an infrastructure
//! failure; otherwise the invocation is reported as timed out. On Unix, an
//! uncollected timed-out leader may remain as a zombie until cargo-each exits,
//! consuming one temporary process-table entry per timed-out invocation.
//! Child commands inherit `PATH` explicitly. On Windows this makes relative
//! program lookup honor the inherited `PATH` order instead of preferring an
//! unrelated executable beside `cargo-each`.
//! Exit `0` means all work succeeded or there was no work. In fail-fast mode a
//! command failure returns its code, a timeout returns `1`, and usage,
//! configuration, spawn, or post-spawn infrastructure failures return `2`.
//! Under `--keep-going`, any failure maps the aggregate result to `1`.
//!
//! # Examples
//!
//! Run a per-manifest tool over every library crate:
//!
//! ```text
//! cargo each --workspace --filter lib -- \
//!     cargo check-external-types --manifest-path {manifest}
//! ```
//!
//! Run one clippy invocation over a computed subset, skipping when it is empty:
//!
//! ```text
//! cargo each -p crate-a -p crate-b --once -- \
//!     cargo clippy {packages} --all-targets -- -D warnings
//! ```
//!
//! Run every Cargo test target that requires the `loom` feature:
//!
//! ```text
//! cargo each --workspace --each-target test --target-required-feature loom -- \
//!     cargo test -p {name} --test {target} --features loom
//! ```

#![cfg_attr(coverage_nightly, feature(coverage_attribute))]

mod cli;
mod error;
mod filter;
mod json_lines;
mod plan;
mod run;
mod select;
mod substitute;
mod workspace;

use std::process::ExitCode;

use clap::Parser;

use crate::cli::CargoCli;

fn main() -> ExitCode {
    let CargoCli::Each(args) = CargoCli::parse();
    match run::run(&args) {
        Ok(code) => code,
        Err(err) => {
            eprintln!("error: {err}");
            // These are cargo-each's own usage / configuration errors (as
            // opposed to a command that ran but failed, which returns its own
            // code via `Ok`); map them all to exit 2.
            ExitCode::from(2)
        }
    }
}