- Run
script/bootstrapto installBrewfiledependencies and clone Homebrew/brew intohomebrew/for Ruby code reference. - A shallow Homebrew/brew clone lives at
homebrew/(gitignored) and can be created by runningscript/bootstrap. Use it to compare Ruby implementations when porting or modifying Rust commands. - When review comments conflict with the Homebrew Ruby source, keep the Ruby behaviour and add a concise comment explaining the Ruby-aligned reason instead of changing behaviour to match the review.
- When Rust code references a specific Ruby function for behaviour, add a concise comment pointing to that Ruby function, e.g.
Cask#installed?inLibrary/Homebrew/cask/cask.rb. Agents and code review tools must validate the Rust behaviour against the Ruby source rather than assuming the comment or Rust code is correct.
- Mirror Homebrew Ruby and Bash filenames when porting commands (e.g.,
fetch.rb→fetch.rs). - Keep Rust command entrypoints in
src/cmd/forLibrary/Homebrew/cmd/*.rbandsrc/dev_cmd/forLibrary/Homebrew/dev-cmd/*.rb, with one file per command where practical. - Keep command files thin when possible and move shared logic into mirrored helper files such as
src/fetch.rs→Library/Homebrew/fetch.rb,src/formula_installer.rs→Library/Homebrew/formula_installer.rb,src/download_queue.rs→Library/Homebrew/download_queue.rb. - When a Homebrew Ruby module, class, or method already provides a clear name for the Rust port, prefer matching that Rust module, type, or function name too when it keeps the code natural. Check
homebrew/Library/Homebrew/for the canonical Ruby names before inventing new ones. - For install-related Rust code, avoid generic
planandplannernames; prefer Ruby-aligned terms such asresolution,resolver, andinstaller, whichever best matches the mirrored Homebrew class or method. - Before adding or moving non-command Rust logic, inspect the matching path under
homebrew/Library/Homebrew/first and prefer mirroring that file location too, e.g.keg.rb→src/keg.rsandutils/link.rb→src/utils/link.rs, instead of leaving the code in an unrelated module because it was convenient. - For API-backed Rust commands, accept the tap spelling the cached API data already uses, including both
homebrew/coreandHomebrew/homebrew-core.
- Before changing native cask install behaviour, trace the Ruby flow from
Library/Homebrew/cmd/install.rb: it partitions formulae and casks, requirescask/installer, prefetches casks throughCask::Installer#enqueue_downloads, then installs new casks withCask::Installer#install. - Mirror the cask file layout instead of collapsing behaviour into a single catch-all install module. Keep
src/cask/mod.rsaligned withLibrary/Homebrew/cask.rb,src/cask/cask.rswithcask/cask.rb,src/cask/cask_loader.rswithcask/cask_loader.rb,src/cask/download.rswithcask/download.rb,src/cask/metadata.rswithcask/metadata.rb,src/cask/staged.rswithcask/staged.rbandsrc/cask/tab.rswithcask/tab.rb. - Keep artifact code under
src/cask/artifact/aligned with Homebrew's artifact hierarchy:relocatedowns source/target resolution,movedowns move-style artifacts such asapp,symlinkedowns link-style artifacts such asbinary, and concrete artifact files implement their Rubyinstall_phasebehaviour. - Keep
src/cask/install.rsas the Rust orchestration layer for the supported native path and preserve Ruby method names where practical:fetch,stage,install_artifacts,summaryand metadata saving should remain easy to compare withCask::Installer. - When a cask path remains unsupported natively, delegate before partial installation work. In particular, unsupported artifacts, dependencies, installed casks, non-
homebrew/casktaps and Ruby-only requirement logic should hand back to Ruby with an explicit reason.
brew.shshould stay a thin gate and dispatch layer.- Prefer reusing existing Homebrew Ruby and Bash behavior for correctness in v1 instead of mirroring complex logic in Rust.
- The Ruby frontend is the permanent path for Ruby formulae and casks: source-only formulae, Ruby-only caskfile logic and non-API Ruby paths should delegate instead of gaining Rust support.
- Keep README snippets that claim to show
brew helpoutput aligned with the real current output; list desired future Rust commands outside those snippets. - Do not shell out to
brew rubyfrom Rust code, tests, or helper scripts. If a supported Rust happy path still needs Ruby-only behavior, leave aTODOand delegate the whole command back through the normal backend handoff instead of embedding abrew rubycompatibility shim. - Keep the Rust
fetchcommand bottle-only for simple namedhomebrew/coreformulae; anything that needs source fetching, cask support, flags, or more complex argument handling should delegate back to Ruby with an explicit reason. - When changing Rust
fetch, keep its bottle naming, cache paths, retry behavior, and fallback boundaries aligned withLibrary/Homebrew/cmd/fetch.rb,Library/Homebrew/fetch.rb,Library/Homebrew/bottle.rb,Library/Homebrew/download_queue.rb,Library/Homebrew/retryable_download.rb, andLibrary/Homebrew/download_strategy.rb. - Respect existing Homebrew cache, Cellar, Caskroom, logs, temp, and metadata paths.
- Adding a Rust command implementation is not enough on its own; update
rust-frontend-enabledinLibrary/Homebrew/brew.shas well orbrewwill keep routing that command to the Ruby frontend even when the experimental helper is used. - Keep local Rust frontend development working from this repository checkout too; do not reintroduce a
HOMEBREW_PREFIX == HOMEBREW_DEFAULT_PREFIXgate inrust-frontend-enabledunless that behavior is intentionally being reverted. - Keep
run-brew-rs-experimental.shfree ofbrew ruby; if Rust needs more API compatibility, fix that inbrew-rsitself instead of synthesizing cache entries in Bash.
- From the repository root, build and install
brew-rsthrough./bin/brew vendor-install brew-rs. - Keep the vendored binary at
Library/Homebrew/vendor/brew-rs/brew-rs. - From the repository root, prepend
Library/Homebrew/vendor/portable-ruby/current/bintoPATHand installrakethere if it is missing. - Use
./run-brew-rs-experimental.sh ...fromLibrary/Homebrew/rust/brew-rswhen you want a self-locating helper that skipsvendor-installentirely when the vendoredbrew-rsbinary is already up-to-date, rebuilds only whenCargo.toml,Cargo.lock, orsrc/changed, runsbrew updatewhen the Rust-backed API cache is missing, and then runsbrewwithHOMEBREW_EXPERIMENTAL_RUST_FRONTEND=1.
- Run
script/testbefore handing off or committing. It runscargo fmt --check,cargo clippy --all-targets --locked -- -D warnings, coverage-instrumentedcargo test --locked, and the same 90% line coverage gate as CI. The script works from either a standalone checkout of this repository or from the nestedLibrary/Homebrew/rust/brew-rspath inside a Homebrew checkout. If you make any later edits, including review fixes or commit amends, rerunscript/teston the final tree; do not rely on earlier green runs. - Integration tests in
tests/cli.rsneed a working Homebrew installation. The helperhomebrew_root()first checks the nested four-levels-up path (Library/Homebrew/rust/brew-rs→ Homebrew root) for CI; when that path does not containbin/brewit falls back tobrew --repositoryso the tests work from a standalone checkout. - Search parity tests require the system Homebrew API cache (
formula_names.txtandcask_names.txtunderbrew --cache). Runbrew updateto populate it locally; in CI,setup-homebrewwithcore: truehandles this. If the cache is missing the search parity tests skip gracefully. - During development and testing, compare Rust commands against their Ruby equivalents and keep stdout, stderr, exit status, and user-facing progress output as close to Ruby as practical.
- When selecting the latest installed version from keg directory names, use
crate::pkg_version::compare_versions; filesystem order is lexicographic and does not match Homebrew version ordering. - Every Rust command with a meaningful implementation (not just delegation) must have at least one integration test in
tests/cli.rsthat runs bothruby_command()andrust_command()with the same arguments and asserts that stdout, exit status, and (where applicable) stderr are identical. These parity tests are the primary gate for output correctness. - When parity tests hit known nondeterministic output such as temp paths or Ruby backtrace frames, normalise only those specific substrings in a shared helper and still assert full remaining stdout/stderr equality; do not weaken checks by comparing only the first few lines or prefixes.
- For user-facing status lines such as bottle success markers, match Ruby's output timing as well as its text; do not print a success line before later fallible steps that Ruby still treats as part of the same install path, or reviewers will flag the output as misleading even if the final exit status is correct.
- For TTY-heavy commands, match Ruby's status/message split and output policy from
Library/Homebrew/download_queue.rb: single downloads should still be able to render a live bar when they take long enough to matter, but fast paths should collapse to the final success line instead of flashing noisy placeholder output. - Before adding Rust parity tests or benchmarks for a command, inspect
Library/Homebrew/brew.shfor shell fast paths and gate/dispatch behavior so you target invocations that actually reachbrew-rs. - Add a concise comment explaining every lint disable such as
rubocop:disable,#[allow(...)],eslint-disable,shellcheck disableor similar, including why the lint does not apply. - Avoid
unsafewhenever possible; when it remains necessary, keep it narrowly scoped and document the invariant that makes it sound. - The crate uses
#![cfg_attr(not(test), forbid(unsafe_code))]so thatenv::set_var/env::remove_varcan be used in tests (required by Rust 2024 edition). Usestd::sync::Mutexto serialize env-var-mutating tests and prevent flaky failures from parallel test execution. - Do not introduce
_implwrappers,tty: boolparameters, or other indirection purely to make TTY/env branches reachable in unit tests. This was tried (e.g., splittinginfo_formulaintoinfo_formula+info_formula_impl(..., tty: bool)) and reverted because it adds indirection that diverges from the Ruby code structure, makes the Rust harder to compare with the Ruby counterpart, and the branches it targets are already verified by the output-parity integration tests that compare full Rust and Ruby command output. Keeping function signatures matching Ruby is more valuable than marginal coverage numbers. - The
mockitocrate is available as a dev dependency for HTTP mock testing. Use it to testdownload_formula_json,download_http_url, and other functions that take&Clientas a parameter; pointHOMEBREW_API_DEFAULT_DOMAINat the mockito server URL.
- The CI enforces 90% line coverage via
script/testand uploads to Codecov. Patch coverage is not enforced but Codecov will comment on PRs with inline annotations showing uncovered lines in changed files so reviewers can decide case-by-case. - Do not add integration tests purely for delegation code paths (e.g.,
reinstall_delegates_to_ruby_with_a_warning). Delegation just hands back to Ruby; testing it adds CI time without verifying Rust behaviour. - The uncoverable gap is in TTY-dependent branches (
io::stdout().is_terminal()),OnceLock-guarded fallbacks intty::width(), delegation dispatch, andcmd/install.rs::run()glue code. These are verified by the output-parity integration tests and smoke tests instead. - To check coverage locally, run
script/bootstrapfirst ifcargo-llvm-covorllvmis missing, then runscript/test. It createscoverage-target/andlcov.info, prints a text coverage report when the 90% line gate fails, and is the same entrypoint used by CI. - Before committing or saying work is done after changing tests or control flow, run
script/testand confirm it passes. Do not rely only on non-instrumentedcargo testruns or oncargo llvm-cov reportwithout--fail-under-lines 90.
- Use
rake benchmarkfor Ruby vs Rust benchmarks. - Use
rake benchmark:checkfor the CI benchmark gate. - Use
BREW_RS_STAGE_REPOSITORY=/path/to/Homebrew rake stageto stage into another checkout. - Benchmarks should cover only commands with meaningful Rust implementations, not commands that immediately hand back to the Ruby backend.
- When a command gains a meaningful Rust implementation, add comparable
Rakefilebenchmark coverage and replace any overlapping GitHub Actions smoke test for that native path. - Benchmarks for formula-specific commands should use the shared batch in
Rakefile, currentlyhelloandripgrep, so Rust proves it is faster on native multi-formula work. - Run local mutating benchmarks outside nested macOS sandboxes because Homebrew postinstall paths call
sandbox-exec. - Rust benchmark rows must not be slower than Ruby. If a native path cannot meet that, change the benchmark scope to representative multi-formula, dependent-heavy or cask work where appropriate, or add a TODO explaining why the Rust path is still slower.
- Benchmarks for simple cask install should use the same cross-platform binary cask, currently
1password-cli. - The benchmark's Ruby baseline must explicitly unset
HOMEBREW_EXPERIMENTAL_RUST_FRONTENDbecause thebrew-rsworkflow exports it globally. - Outside the default prefix, benchmarks should cover read commands and skip mutating commands.
- CI symlinks the checkout into the Homebrew tree (
Library/Homebrew/rust/brew-rs → $GITHUB_WORKSPACE). BothRakefileandhomebrew_root()handle this by falling back tobrew --repositorywhen walking up from__dir__/CARGO_MANIFEST_DIRdoes not findbin/brew(becauserealpathresolves through the symlink to the workspace).Brewfiledependencies (includingcargo-llvm-cov) are installed viacache-homebrew-prefix. - Parity tests that compare Rust and Ruby search output must use Ruby as the reference and skip when Ruby does not return results for both formulae and casks (e.g.,
cask_names.txtlists macOS-only casks that Ruby filters out on Linux). - Both
ruby_command()andrust_command()stripGITHUB_ACTIONSandHOMEBREW_EXPERIMENTAL_RUST_FRONTENDso parity tests run under consistent environments regardless of CI workflow-level env vars. - Prefer a single commit per PR in this repository unless multiple commits materially improve reviewability.
- Run
./bin/brew typecheckand./bin/brew lgtmfor repo-wide verification.