turborepo
712e0e4a - refactor: Migrate CLI parser to usage-rs (#13890)

Commit
56 days ago
refactor: Migrate CLI parser to usage-rs (#13890) ### Description Migrate the primary Turborepo CLI parser from `clap` to `usage-rs` 6.5.0. - Refactor parser-facing command structures where `usage-rs` requires owned, unboxed argument groups. - Preserve implicit `turbo <task>` routing, global flags, aliases, optional values, conflicts, requirements, `--` pass-through arguments, `--single-package`, and repeated-scalar rejection. - Replace clap help, diagnostics, and completion generation with usage-rs equivalents. - Add deterministic arbitrary-argv property coverage and an ASan/libFuzzer target, including non-UTF-8 `OsString` inputs on Unix. - Update CLI snapshots for the expected cosmetic help and diagnostic changes. #### Benchmarks Measured on Apple Silicon macOS using the `release-turborepo` profile: | Measurement | clap | usage-rs | Difference | | --- | ---: | ---: | ---: | | Cold build wall time | 285.66s | 284.24s | 0.5% faster | | Build max RSS | 3.48 GB | 3.37 GB | 3.2% lower | | Binary size | 45,090,512 B | 45,479,680 B | 0.86% larger | | `--help` | 10.9 ± 2.0 ms | 10.5 ± 0.5 ms | ~4% faster | | Invalid argument | 11.4 ± 3.0 ms | 10.7 ± 2.2 ms | ~9% faster | The runtime differences are close to process startup noise. `clap` also remains in the final dependency graph through other crates, so this migration alone does not remove it from the binary. #### Differential fuzzing I preserved a release build of the clap implementation from the migration base and ran it beside the usage-rs build. The differential corpus covered explicit and implicit `run`, other subcommands, aliases, global flags in different positions, optional values, invalid values, conflicting flags, repeated scalar and repeatable flags, Unicode values, unknown flags, and `--` pass-through arguments. Both binaries received the same argv. I compared parser acceptance/rejection and exit behavior, then compared focused stdout/stderr for stable cases such as help, unknown arguments, conflicts, and duplicate scalar values; diagnostic prose and formatting were intentionally treated as cosmetic. This differential work found that usage-rs accepted a repeated nested scalar such as two `--concurrency` values where clap rejected it. The migration now rejects repeated scalar flags explicitly and retains a regression test. Grammar-guided sentinel runs were also used as a discovery tool for differences in error precedence and command-local flag placement; those findings were checked against the existing CLI regression suite rather than treating first-error wording as semantic equivalence. This sat alongside, rather than replacing, the arbitrary-byte property test and ASan/libFuzzer campaigns described below. #### Patch-level compatibility escape hatch A missed parser incompatibility can be repaired inside the usage-rs path in a normal patch release, without user configuration, a second binary, or reverting the migration. `Args::parse_args` owns the complete `Vec<OsString>` immediately before `Args::try_parse_from`, and it already uses this boundary for compatibility behavior such as stripping and restoring `--single-package`, rejecting clap-incompatible duplicate scalar flags, and rejecting values attached to boolean switches. A targeted hotfix can therefore normalize a legacy spelling or argv shape before parsing, or adjust the typed `Args` immediately afterward, before any command handler runs. For example, if usage-rs stopped accepting the existing `--dry` alias, the compatibility patch can live at the pre-parse boundary: ```rust let words: Vec<OsString> = single_package_free .map(|word| match word.to_str() { Some("--dry") => OsString::from("--dry-run"), Some(value) if value.starts_with("--dry=") => { OsString::from(value.replacen("--dry=", "--dry-run=", 1)) } _ => word, }) .collect(); let mut args = Args::try_parse_from(&refs) .map_err(|error| Args::render_failure(&refs[1..], &error))?; ``` This is transparent to users: the same command line continues to work after installing the patch release. The same boundary can handle renamed flags, changed optional-value syntax, argument ordering, legacy aliases, and other token-level differences. If the parser accepts the tokens but binds them differently, the patch can instead mutate the typed `args` directly after `try_parse_from` and before returning from `parse_args`; command handlers only receive the corrected representation. I proved this locally and removed it without committing: I temporarily removed the declared `--dry` alias from usage-rs, added the normalization above, and verified both `turbo run build --dry` and `turbo run build --dry=json` still parsed successfully. The existing focused typed parser tests passed (2 passed, 0 failed), and both end-to-end binary invocations exited 0. #### Expected cosmetic changes - Help uses usage-rs layout, wrapping, headings, and metavariable names. - Diagnostics are shorter and use usage-rs wording. - Conflict diagnostics can name flags in a different order. - Shell completions use usage-rs's smaller dynamic completion protocol rather than clap's large static script. ### Testing Instructions - Exercise representative explicit and implicit runs, such as `turbo run build` and `turbo build`. - Verify global flags before and after subcommands. - Verify unknown flags and repeated scalar options fail. - Verify arguments after `--` reach the underlying task unchanged. - Generate completions for each supported shell. Fuzz validation performed: - 100,000 deterministic property cases with arbitrary byte-backed arguments, including non-UTF-8 Unix arguments. - 1,000,000 ASan/libFuzzer executions with no crashes or hangs. - An additional 100,000 ASan/libFuzzer executions after adding repeated-scalar enforcement, also with no crashes or hangs.
Author
Parents
Loading