`println!` panics when its write fails, and the release profile builds
with `panic = "abort"`. When the reader of pnpm's stdout had exited, as
in `pnpm --version | true` or `pnpm config list | head -0`, pnpm raised
SIGABRT: exit code 134 and a core dump.
`pnpm-diagnostics` now exports `println!` and `print!` macros next to the
stderr ones from pnpm/pnpm#16916. A write whose reader closed the pipe is
dropped and the command finishes with its own exit status. Any other
failed write is kept, and `main` flushes stdout and fails the command
with it through `report_fatal_error`, so the NDJSON reporter still gets
a JSON record. Every `print!`/`println!` of the binary and the direct
stdout writes of `outdated`, `cat-file`, `cat-index`, `sbom`, `audit`
and `completion` go through them. clippy's `print_stdout` restriction
lint now rejects the std macros, with the same crate-level opt-outs as
`print_stderr`. The watch agent of `pnpm pipeline --watch` stops with
exit 0 once a write finds its reader gone, so `pnpm pipeline --watch |
head -1` does not hang. `run`, `exec`, `audit` and `sbom` print their
no-projects-matched notice through one helper, which keeps
`run_recursive` within perfectionist's length limit.
The tracking issue asked for pnpm to exit 0 at the first failed write,
as pnpm 11 does. Exiting is the wrong default:
- `outdated` and `audit` carry their verdict in the exit status. An
`exit(0)` turned `pnpm outdated | head` from 1 into 0, and would let
`pnpm audit | head` pass a CI job under `pipefail`.
- `clean` prints "Removing <path>" before each removal, and the prune
report of `audit` comes before its write to pnpm-workspace.yaml. An
`exit(0)` there ends the command with work undone and a success code.
- No command computes much after its first stdout write: `list`, `why`
and the registry queries render their whole output, then print it once.
pnpm 11.28.5 shows both failures: `outdated | true` exits 0 with an
outdated dependency, and `clean --lockfile | true` removes nothing and
exits 0.
Restoring the default SIGPIPE disposition is still not an option: pnpm
writes to its children's stdin and relies on the `BrokenPipe` error to
notice a child that stopped reading.
Closespnpm/tasks#132
`eprintln!` panics when its write fails, and the release profile builds
with `panic = "abort"`. When the reader of pnpm's stderr had exited, as
in `pnpm start 2>&1 | head -1` with a failing script, writing the
`[ELIFECYCLE]` line raised SIGABRT: exit code 134 and a core dump instead
of the script's exit code.
`pnpm-diagnostics` now exports `eprintln!` and `eprint!` macros that
write to stderr and drop a failed write, as the reporter already does.
Every `eprintln!`/`eprint!` in the CLI, `pnpm-store-dir`, and
`pnpm-diagnostics` goes through them. The macros keep the std names, so
perfectionist's `impure_macro_arguments` allow list, which matches the
final path segment, covers them like the std ones.
clippy's `print_stderr` restriction lint now rejects the std macros.
`allow-print-in-tests` exempts unit tests and `#[test]` functions. Code
that is never part of the pnpm binary opts out with a crate-level
`#![expect]`: the developer tools under `pnpm/tasks`, the test-only
binaries of `pnpm-fs`, and the CLI end-to-end suite.
Restoring the default SIGPIPE disposition is not an option: pnpm writes
to its children's stdin and relies on the `BrokenPipe` error to notice a
child that stopped reading.
Fixespnpm/pnpm#16904
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>