Skip to content
This repository was archived by the owner on Jun 8, 2026. It is now read-only.

Latest commit

 

History

History
95 lines (77 loc) · 14.9 KB

File metadata and controls

95 lines (77 loc) · 14.9 KB

Rust Frontend Notes

Ruby code reference

  • Run script/bootstrap to install Brewfile dependencies and clone Homebrew/brew into homebrew/ for Ruby code reference.
  • A shallow Homebrew/brew clone lives at homebrew/ (gitignored) and can be created by running script/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? in Library/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.

Naming and structure conventions

  • Mirror Homebrew Ruby and Bash filenames when porting commands (e.g., fetch.rbfetch.rs).
  • Keep Rust command entrypoints in src/cmd/ for Library/Homebrew/cmd/*.rb and src/dev_cmd/ for Library/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.rsLibrary/Homebrew/fetch.rb, src/formula_installer.rsLibrary/Homebrew/formula_installer.rb, src/download_queue.rsLibrary/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 plan and planner names; prefer Ruby-aligned terms such as resolution, resolver, and installer, 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.rbsrc/keg.rs and utils/link.rbsrc/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/core and Homebrew/homebrew-core.

Cask install structure

  • Before changing native cask install behaviour, trace the Ruby flow from Library/Homebrew/cmd/install.rb: it partitions formulae and casks, requires cask/installer, prefetches casks through Cask::Installer#enqueue_downloads, then installs new casks with Cask::Installer#install.
  • Mirror the cask file layout instead of collapsing behaviour into a single catch-all install module. Keep src/cask/mod.rs aligned with Library/Homebrew/cask.rb, src/cask/cask.rs with cask/cask.rb, src/cask/cask_loader.rs with cask/cask_loader.rb, src/cask/download.rs with cask/download.rb, src/cask/metadata.rs with cask/metadata.rb, src/cask/staged.rs with cask/staged.rb and src/cask/tab.rs with cask/tab.rb.
  • Keep artifact code under src/cask/artifact/ aligned with Homebrew's artifact hierarchy: relocated owns source/target resolution, moved owns move-style artifacts such as app, symlinked owns link-style artifacts such as binary, and concrete artifact files implement their Ruby install_phase behaviour.
  • Keep src/cask/install.rs as the Rust orchestration layer for the supported native path and preserve Ruby method names where practical: fetch, stage, install_artifacts, summary and metadata saving should remain easy to compare with Cask::Installer.
  • When a cask path remains unsupported natively, delegate before partial installation work. In particular, unsupported artifacts, dependencies, installed casks, non-homebrew/cask taps and Ruby-only requirement logic should hand back to Ruby with an explicit reason.

Architecture

  • brew.sh should 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 help output aligned with the real current output; list desired future Rust commands outside those snippets.
  • Do not shell out to brew ruby from Rust code, tests, or helper scripts. If a supported Rust happy path still needs Ruby-only behavior, leave a TODO and delegate the whole command back through the normal backend handoff instead of embedding a brew ruby compatibility shim.
  • Keep the Rust fetch command bottle-only for simple named homebrew/core formulae; 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 with Library/Homebrew/cmd/fetch.rb, Library/Homebrew/fetch.rb, Library/Homebrew/bottle.rb, Library/Homebrew/download_queue.rb, Library/Homebrew/retryable_download.rb, and Library/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-enabled in Library/Homebrew/brew.sh as well or brew will 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_PREFIX gate in rust-frontend-enabled unless that behavior is intentionally being reverted.
  • Keep run-brew-rs-experimental.sh free of brew ruby; if Rust needs more API compatibility, fix that in brew-rs itself instead of synthesizing cache entries in Bash.

Building

  • From the repository root, build and install brew-rs through ./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/bin to PATH and install rake there if it is missing.
  • Use ./run-brew-rs-experimental.sh ... from Library/Homebrew/rust/brew-rs when you want a self-locating helper that skips vendor-install entirely when the vendored brew-rs binary is already up-to-date, rebuilds only when Cargo.toml, Cargo.lock, or src/ changed, runs brew update when the Rust-backed API cache is missing, and then runs brew with HOMEBREW_EXPERIMENTAL_RUST_FRONTEND=1.

Testing

  • Run script/test before handing off or committing. It runs cargo fmt --check, cargo clippy --all-targets --locked -- -D warnings, coverage-instrumented cargo 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 nested Library/Homebrew/rust/brew-rs path inside a Homebrew checkout. If you make any later edits, including review fixes or commit amends, rerun script/test on the final tree; do not rely on earlier green runs.
  • Integration tests in tests/cli.rs need a working Homebrew installation. The helper homebrew_root() first checks the nested four-levels-up path (Library/Homebrew/rust/brew-rs → Homebrew root) for CI; when that path does not contain bin/brew it falls back to brew --repository so the tests work from a standalone checkout.
  • Search parity tests require the system Homebrew API cache (formula_names.txt and cask_names.txt under brew --cache). Run brew update to populate it locally; in CI, setup-homebrew with core: true handles 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.rs that runs both ruby_command() and rust_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.sh for shell fast paths and gate/dispatch behavior so you target invocations that actually reach brew-rs.
  • Add a concise comment explaining every lint disable such as rubocop:disable, #[allow(...)], eslint-disable, shellcheck disable or similar, including why the lint does not apply.
  • Avoid unsafe whenever 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 that env::set_var/env::remove_var can be used in tests (required by Rust 2024 edition). Use std::sync::Mutex to serialize env-var-mutating tests and prevent flaky failures from parallel test execution.
  • Do not introduce _impl wrappers, tty: bool parameters, or other indirection purely to make TTY/env branches reachable in unit tests. This was tried (e.g., splitting info_formula into info_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 mockito crate is available as a dev dependency for HTTP mock testing. Use it to test download_formula_json, download_http_url, and other functions that take &Client as a parameter; point HOMEBREW_API_DEFAULT_DOMAIN at the mockito server URL.

Coverage

  • The CI enforces 90% line coverage via script/test and 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 in tty::width(), delegation dispatch, and cmd/install.rs::run() glue code. These are verified by the output-parity integration tests and smoke tests instead.
  • To check coverage locally, run script/bootstrap first if cargo-llvm-cov or llvm is missing, then run script/test. It creates coverage-target/ and lcov.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/test and confirm it passes. Do not rely only on non-instrumented cargo test runs or on cargo llvm-cov report without --fail-under-lines 90.

Benchmarks

  • Use rake benchmark for Ruby vs Rust benchmarks.
  • Use rake benchmark:check for the CI benchmark gate.
  • Use BREW_RS_STAGE_REPOSITORY=/path/to/Homebrew rake stage to 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 Rakefile benchmark 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, currently hello and ripgrep, 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_FRONTEND because the brew-rs workflow exports it globally.
  • Outside the default prefix, benchmarks should cover read commands and skip mutating commands.

CI notes

  • CI symlinks the checkout into the Homebrew tree (Library/Homebrew/rust/brew-rs → $GITHUB_WORKSPACE). Both Rakefile and homebrew_root() handle this by falling back to brew --repository when walking up from __dir__/CARGO_MANIFEST_DIR does not find bin/brew (because realpath resolves through the symlink to the workspace). Brewfile dependencies (including cargo-llvm-cov) are installed via cache-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.txt lists macOS-only casks that Ruby filters out on Linux).
  • Both ruby_command() and rust_command() strip GITHUB_ACTIONS and HOMEBREW_EXPERIMENTAL_RUST_FRONTEND so 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 typecheck and ./bin/brew lgtm for repo-wide verification.