mirror of
https://github.com/Homebrew/brew.git
synced 2026-08-12 22:29:27 +04:00
brew-rs: add an opt-in Rust frontend path
Homebrew's Ruby and Bash startup cost is most noticeable on common API-backed commands, so add a tightly gated Rust frontend experiment for search, info, list, and the essential mutating commands. The goal is to make iteration on a faster CLI possible without changing the default user experience or taking on the risk of reimplementing all of FormulaInstaller in Rust on day one. Keep the risk contained by only enabling the Rust path on supported Tier 1 default-prefix installs when both HOMEBREW_DEVELOPER and HOMEBREW_EXPERIMENTAL_RUST_FRONTEND are set, and by continuing to delegate complex install, upgrade, uninstall, and update behaviour back to the existing Homebrew frontend for v1 correctness. That lets us measure the frontend win first and only move more logic out of Ruby when profiling proves it is worth the complexity. Build brew-rs through vendor-install and run Rust subtree tasks through portable Ruby's rake so local development, first-run bootstrapping, and CI all use the same path instead of bespoke scripts. Add integration coverage, a dedicated workflow, hyperfine benchmarks, Dependabot support, and Rust-specific docs so the experiment is easy to validate, benchmark, and evolve.
This commit is contained in:
+15
-1
@@ -19,6 +19,21 @@ updates:
|
||||
- dependency-type: all
|
||||
cooldown:
|
||||
default-days: 7
|
||||
- package-ecosystem: cargo
|
||||
directory: "/Library/Homebrew/rust/brew-rs"
|
||||
schedule:
|
||||
interval: weekly
|
||||
day: friday
|
||||
time: '08:00'
|
||||
timezone: Etc/UTC
|
||||
groups:
|
||||
cargo:
|
||||
patterns:
|
||||
- "*"
|
||||
allow:
|
||||
- dependency-type: all
|
||||
cooldown:
|
||||
default-days: 7
|
||||
- package-ecosystem: devcontainers
|
||||
directory: "/"
|
||||
schedule:
|
||||
@@ -81,4 +96,3 @@ updates:
|
||||
- dependency-type: all
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
name: brew-rs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
merge_group:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
HOMEBREW_DEVELOPER: 1
|
||||
HOMEBREW_EXPERIMENTAL_RUST_FRONTEND: 1
|
||||
HOMEBREW_NO_AUTO_UPDATE: 1
|
||||
HOMEBREW_NO_ENV_HINTS: 1
|
||||
HOMEBREW_NO_INSTALL_CLEANUP: 1
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -xeuo pipefail {0}
|
||||
|
||||
jobs:
|
||||
brew-rs:
|
||||
name: ${{ matrix.name }}
|
||||
if: github.repository_owner == 'Homebrew'
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: brew-rs (Linux)
|
||||
runs-on: ubuntu-latest
|
||||
- name: brew-rs (macOS)
|
||||
runs-on: macos-26
|
||||
steps:
|
||||
- name: Set up Homebrew
|
||||
id: set-up-homebrew
|
||||
uses: Homebrew/actions/setup-homebrew@b7f2e4e4cd0fc6580ebf3ecee0d10ac27bc02ce6 # main
|
||||
with:
|
||||
core: true
|
||||
cask: false
|
||||
|
||||
- uses: Homebrew/actions/cache-homebrew-prefix@b7f2e4e4cd0fc6580ebf3ecee0d10ac27bc02ce6 # main
|
||||
env:
|
||||
HOMEBREW_EXPERIMENTAL_RUST_FRONTEND:
|
||||
with:
|
||||
install: hyperfine rust
|
||||
workflow-key: brew-rs
|
||||
uninstall: true
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Cache Bundler RubyGems
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ${{ steps.set-up-homebrew.outputs.gems-path }}
|
||||
key: ${{ matrix.runs-on }}-brew-rs-rubygems-${{ steps.set-up-homebrew.outputs.gems-hash }}
|
||||
restore-keys: ${{ matrix.runs-on }}-brew-rs-rubygems-
|
||||
|
||||
- name: Install Bundler RubyGems
|
||||
run: brew install-bundler-gems --groups=tests
|
||||
|
||||
- name: Add portable Ruby to PATH
|
||||
run: |
|
||||
portable_ruby_bindir="${GITHUB_WORKSPACE}/Library/Homebrew/vendor/portable-ruby/current/bin"
|
||||
[[ -x "${portable_ruby_bindir}/ruby" ]] || ./bin/brew vendor-install ruby
|
||||
if [[ ! -x "${portable_ruby_bindir}/rake" ]]
|
||||
then
|
||||
"${portable_ruby_bindir}/gem" install rake --no-document
|
||||
fi
|
||||
echo "${portable_ruby_bindir}" >> "${GITHUB_PATH}"
|
||||
|
||||
- name: Build brew-rs
|
||||
working-directory: Library/Homebrew/rust/brew-rs
|
||||
run: rake build
|
||||
|
||||
- name: Check brew-rs formatting
|
||||
working-directory: Library/Homebrew/rust/brew-rs
|
||||
run: rake fmt
|
||||
|
||||
- name: Lint brew-rs
|
||||
working-directory: Library/Homebrew/rust/brew-rs
|
||||
run: rake clippy
|
||||
|
||||
- name: Test brew-rs crate
|
||||
working-directory: Library/Homebrew/rust/brew-rs
|
||||
run: rake test
|
||||
|
||||
- name: Run brew-rs integration tests
|
||||
run: ./bin/brew tests --only=cmd/brew_rs --no-parallel
|
||||
|
||||
- name: Benchmark brew-rs
|
||||
working-directory: Library/Homebrew/rust/brew-rs
|
||||
run: rake benchmark
|
||||
|
||||
- name: Stage brew-rs into default-prefix Homebrew
|
||||
env:
|
||||
BREW_RS_STAGE_REPOSITORY: ${{ steps.set-up-homebrew.outputs.repository-path }}
|
||||
working-directory: Library/Homebrew/rust/brew-rs
|
||||
run: rake stage
|
||||
|
||||
- name: Prepare smoke test formula
|
||||
run: brew uninstall --force hello || true
|
||||
|
||||
- name: Smoke-test brew-rs install
|
||||
run: brew install hello
|
||||
|
||||
- name: Smoke-test brew-rs upgrade
|
||||
run: brew upgrade hello
|
||||
|
||||
- name: Smoke-test brew-rs uninstall
|
||||
run: brew uninstall hello
|
||||
|
||||
- name: Smoke-test brew-rs update
|
||||
run: brew update --quiet --force
|
||||
|
||||
- name: Upload brew-rs artifact
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: brew-rs-${{ runner.os }}-${{ runner.arch }}
|
||||
path: Library/Homebrew/vendor/brew-rs/brew-rs
|
||||
@@ -13,11 +13,14 @@
|
||||
/Library/Homebrew/doc
|
||||
/Library/Homebrew/prof
|
||||
/Library/Homebrew/test/.gem
|
||||
/Library/Homebrew/test/.cargo/
|
||||
/Library/Homebrew/test/.subversion
|
||||
/Library/Homebrew/test/coverage
|
||||
/Library/Homebrew/test/junit
|
||||
/Library/Homebrew/test/fs_leak_log
|
||||
/Library/Homebrew/vendor/portable-ruby
|
||||
/Library/Homebrew/vendor/brew-rs/
|
||||
/Library/Homebrew/rust/brew-rs/target
|
||||
/Library/Taps
|
||||
/Library/PinnedTaps
|
||||
/Library/Homebrew/.byebug_history
|
||||
|
||||
@@ -286,6 +286,49 @@ check-array-membership() {
|
||||
fi
|
||||
}
|
||||
|
||||
rust-frontend-enabled() {
|
||||
if [[ -z "${HOMEBREW_DEVELOPER:-}" ]]
|
||||
then
|
||||
return 1
|
||||
elif [[ -z "${HOMEBREW_EXPERIMENTAL_RUST_FRONTEND:-}" ]]
|
||||
then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -n "${HOMEBREW_NO_INSTALL_FROM_API:-}" ]]
|
||||
then
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "${HOMEBREW_COMMAND}" in
|
||||
search | info | list | install | reinstall | update | upgrade | uninstall) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
|
||||
if [[ -z "${HOMEBREW_INTEGRATION_TEST:-}" && "${HOMEBREW_PREFIX}" != "${HOMEBREW_DEFAULT_PREFIX}" ]]
|
||||
then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -n "${HOMEBREW_MACOS}" ]]
|
||||
then
|
||||
if [[ "${HOMEBREW_PROCESSOR}" != "arm64" ]]
|
||||
then
|
||||
return 1
|
||||
fi
|
||||
elif [[ -n "${HOMEBREW_LINUX}" ]]
|
||||
then
|
||||
if [[ "${HOMEBREW_PROCESSOR}" != "arm64" && "${HOMEBREW_PROCESSOR}" != "x86_64" ]]
|
||||
then
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# These variables are set from various Homebrew scripts.
|
||||
# shellcheck disable=SC2154
|
||||
auto-update() {
|
||||
@@ -1093,6 +1136,34 @@ unset SUDO
|
||||
# Remove internal variables
|
||||
unset HOMEBREW_INTERNAL_ALLOW_PACKAGES_FROM_PATHS
|
||||
|
||||
if rust-frontend-enabled
|
||||
then
|
||||
HOMEBREW_RUST_BREW_FILE="${HOMEBREW_LIBRARY}/Homebrew/vendor/brew-rs/brew-rs"
|
||||
source "${HOMEBREW_LIBRARY}/Homebrew/cmd/vendor-install.sh"
|
||||
if ! brew-rs-vendor-up-to-date
|
||||
then
|
||||
cargo_path="$(PATH="${HOMEBREW_PATH:-${PATH}}" command -v cargo)"
|
||||
if [[ ! -x "${cargo_path}" ]]
|
||||
then
|
||||
opoo "HOMEBREW_EXPERIMENTAL_RUST_FRONTEND is set but cargo from the rust formula was not found; falling back to the Ruby frontend."
|
||||
else
|
||||
homebrew-vendor-install brew-rs || exit $?
|
||||
# Close the vendor-install lock FD opened by the sourced helper.
|
||||
exec 200>&-
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -x "${HOMEBREW_RUST_BREW_FILE}" ]]
|
||||
then
|
||||
[[ "${HOMEBREW_ARG_COUNT}" -gt 0 ]] && set -- "${HOMEBREW_COMMAND}" "$@"
|
||||
|
||||
auto-update "$@"
|
||||
|
||||
opoo "using the experimental brew-rs Rust frontend."
|
||||
exec "${HOMEBREW_RUST_BREW_FILE}" "$@"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "${HOMEBREW_BASH_COMMAND}" ]]
|
||||
then
|
||||
# source rather than executing directly to ensure the entire file is read into
|
||||
|
||||
@@ -12,6 +12,24 @@ source "${HOMEBREW_LIBRARY}/Homebrew/utils/ruby.sh"
|
||||
|
||||
VENDOR_DIR="${HOMEBREW_LIBRARY}/Homebrew/vendor"
|
||||
|
||||
brew-rs-vendor-up-to-date() {
|
||||
local brew_rs_source_dir="${HOMEBREW_LIBRARY}/Homebrew/rust/brew-rs"
|
||||
local vendor_root="${VENDOR_DIR}/brew-rs"
|
||||
local vendor_binary="${vendor_root}/bin/brew-rs"
|
||||
local cargo_lock_path="${brew_rs_source_dir}/Cargo.lock"
|
||||
|
||||
if [[ -x "${vendor_root}/brew-rs" &&
|
||||
-x "${vendor_binary}" &&
|
||||
"${brew_rs_source_dir}/Cargo.toml" -ot "${vendor_binary}" &&
|
||||
(! -f "${cargo_lock_path}" || "${cargo_lock_path}" -ot "${vendor_binary}") &&
|
||||
-z "$(find "${brew_rs_source_dir}/src" -type f -newer "${vendor_binary}" -print -quit)" ]]
|
||||
then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
set_ruby_variables() {
|
||||
# Handle the case where /usr/local/bin/brew is run under arm64.
|
||||
# It's a x86_64 installation there (we refuse to install arm64 binaries) so
|
||||
@@ -291,6 +309,10 @@ homebrew-vendor-install() {
|
||||
local option
|
||||
local url_var
|
||||
local sha_var
|
||||
local cargo_lock_path
|
||||
local cargo_path
|
||||
local vendor_binary
|
||||
local vendor_root
|
||||
|
||||
unset VENDOR_PHYSICAL_PROCESSOR
|
||||
unset VENDOR_PROCESSOR
|
||||
@@ -336,6 +358,35 @@ homebrew-vendor-install() {
|
||||
[[ -z "${VENDOR_NAME}" ]] && odie "This command requires a vendor target!"
|
||||
[[ -n "${HOMEBREW_DEBUG}" ]] && set -x
|
||||
|
||||
if [[ "${VENDOR_NAME}" == "brew-rs" ]]
|
||||
then
|
||||
[[ -n "${HOMEBREW_DEVELOPER:-}" && -n "${HOMEBREW_EXPERIMENTAL_RUST_FRONTEND:-}" ]] ||
|
||||
odie "brew-rs vendor-install requires HOMEBREW_DEVELOPER=1 and HOMEBREW_EXPERIMENTAL_RUST_FRONTEND=1."
|
||||
|
||||
cargo_path="$(PATH="${HOMEBREW_PATH:-${PATH}}" command -v cargo)"
|
||||
[[ -x "${cargo_path}" ]] || odie "brew-rs vendor-install requires 'cargo' from the 'rust' formula."
|
||||
|
||||
brew_rs_source_dir="${HOMEBREW_LIBRARY}/Homebrew/rust/brew-rs"
|
||||
vendor_root="${VENDOR_DIR}/brew-rs"
|
||||
if brew-rs-vendor-up-to-date
|
||||
then
|
||||
return 0
|
||||
fi
|
||||
|
||||
lock "vendor-install ${VENDOR_NAME}"
|
||||
|
||||
if brew-rs-vendor-up-to-date
|
||||
then
|
||||
return 0
|
||||
fi
|
||||
|
||||
[[ -n "${HOMEBREW_QUIET}" ]] || ohai "Building brew-rs" >&2
|
||||
PATH="${HOMEBREW_PATH:-${PATH}}" "${cargo_path}" install --path "${brew_rs_source_dir}" --locked --force --root "${vendor_root}" \
|
||||
2> >(grep -Fv "be sure to add" >&2) || odie "Failed to build brew-rs!"
|
||||
ln -sfn "bin/brew-rs" "${vendor_root}/brew-rs"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -z "${VENDOR_PHYSICAL_PROCESSOR}" ]]
|
||||
then
|
||||
VENDOR_PHYSICAL_PROCESSOR="${HOMEBREW_PHYSICAL_PROCESSOR}"
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Rust Frontend Notes
|
||||
|
||||
- Build and install `brew-rs` through `./bin/brew vendor-install brew-rs`.
|
||||
- Keep the vendored binary at `Library/Homebrew/vendor/brew-rs/brew-rs`.
|
||||
- `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.
|
||||
- Respect existing Homebrew cache, Cellar, Caskroom, logs, temp, and metadata paths.
|
||||
- Keep Rust command entrypoints in `src/commands/` with one file per command where practical.
|
||||
- Before running `rake`, prepend `Library/Homebrew/vendor/portable-ruby/current/bin` to `PATH` and install `rake` there if it is missing.
|
||||
- Run tasks from `Library/Homebrew/rust/brew-rs` with `rake ...`.
|
||||
- Use `rake build` to vendor the binary locally.
|
||||
- Use `rake check` for Rust formatting, lint, and Rust tests.
|
||||
- Use `BREW_RS_STAGE_REPOSITORY=/path/to/Homebrew rake stage` to stage into another checkout.
|
||||
- Use `rake benchmark` for Ruby vs Rust benchmarks.
|
||||
- Run `./bin/brew typecheck` and `./bin/brew lgtm` for repo-wide verification.
|
||||
- Outside the default prefix, benchmarks should cover read commands and skip mutating commands.
|
||||
Generated
+204
@@ -0,0 +1,204 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "brew-rs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"regex",
|
||||
"rust-fuzzy-search",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "rust-fuzzy-search"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a157657054ffe556d8858504af8a672a054a6e0bd9e8ee531059100c0fa11bb2"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "brew-rs"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.85"
|
||||
|
||||
[dependencies]
|
||||
# Ergonomic error propagation for the CLI and command dispatch layers.
|
||||
anyhow = "1.0.100"
|
||||
# Supports `brew search /REGEX/` matching.
|
||||
regex = "1.12.2"
|
||||
# Adds fuzzy fallback suggestions for plain `brew search` queries.
|
||||
rust-fuzzy-search = "0.1.1"
|
||||
# Deserialize structured Homebrew API and metadata payloads.
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
# Read and write JSON API cache entries and command output.
|
||||
serde_json = "1.0.145"
|
||||
# Traverse installed formula and cask state under existing Homebrew paths.
|
||||
walkdir = "2.5.0"
|
||||
@@ -0,0 +1,122 @@
|
||||
# brew-rs
|
||||
|
||||
`brew-rs` is the opt-in Rust frontend for a small set of `brew` commands.
|
||||
Today the gated command set is `search`, `info`, `list`, `install`,
|
||||
`reinstall`, `update`, `upgrade`, and `uninstall`.
|
||||
|
||||
Meaningful Rust behavior currently exists for `search` and `list`.
|
||||
`info` and the mutating commands currently print a warning to `stderr` and
|
||||
delegate back to the existing Ruby frontend for correctness.
|
||||
|
||||
It is intentionally built through `brew vendor-install brew-rs`, which runs
|
||||
standard Cargo commands under the hood instead of a bespoke build wrapper.
|
||||
|
||||
## Pre-step
|
||||
|
||||
Export both Rust frontend gate variables before building or running `brew-rs`:
|
||||
|
||||
```bash
|
||||
export HOMEBREW_DEVELOPER=1
|
||||
export HOMEBREW_EXPERIMENTAL_RUST_FRONTEND=1
|
||||
```
|
||||
|
||||
Before running `rake`, add Homebrew's portable Ruby to `PATH` and install
|
||||
`rake` there if needed:
|
||||
|
||||
```bash
|
||||
portable_ruby_bindir="$PWD/Library/Homebrew/vendor/portable-ruby/current/bin"
|
||||
[[ -x "${portable_ruby_bindir}/ruby" ]] || ./bin/brew vendor-install ruby
|
||||
if [[ ! -x "${portable_ruby_bindir}/rake" ]]
|
||||
then
|
||||
"${portable_ruby_bindir}/gem" install rake --no-document
|
||||
fi
|
||||
export PATH="${portable_ruby_bindir}:${PATH}"
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
cd Library/Homebrew/rust/brew-rs
|
||||
rake build
|
||||
```
|
||||
|
||||
## Enable
|
||||
|
||||
```bash
|
||||
./bin/brew search jq
|
||||
```
|
||||
|
||||
The first supported Rust-backed command will run `brew vendor-install brew-rs`
|
||||
automatically and skip rebuilding when the vendored binary is already up-to-date.
|
||||
If `cargo` from the `rust` formula is not available yet, `brew` prints a warning
|
||||
to `stderr` and falls back to the existing Ruby frontend instead of failing.
|
||||
When `brew-rs` is used, `brew` prints an experimental warning to `stderr`.
|
||||
If a command is not meaningfully implemented in Rust yet, `brew-rs` also
|
||||
prints a handoff warning before delegating back to Ruby.
|
||||
|
||||
## Rust Checks
|
||||
|
||||
```bash
|
||||
cd Library/Homebrew/rust/brew-rs
|
||||
rake check
|
||||
```
|
||||
|
||||
## Homebrew Checks
|
||||
|
||||
```bash
|
||||
HOMEBREW_NO_AUTO_UPDATE=1 ./bin/brew tests --only=cmd/brew_rs --no-parallel
|
||||
HOMEBREW_NO_AUTO_UPDATE=1 ./bin/brew typecheck
|
||||
HOMEBREW_NO_AUTO_UPDATE=1 ./bin/brew lgtm
|
||||
```
|
||||
|
||||
The `cmd/brew_rs` integration spec is intentionally small because integration
|
||||
tests are slow. It currently covers the Rust-owned `search` and `list` flows.
|
||||
|
||||
## Benchmark
|
||||
|
||||
The `benchmark` task compares the Ruby and Rust frontends with `hyperfine` for
|
||||
the commands currently gated through `brew-rs`.
|
||||
|
||||
`rake` should be run with Homebrew's portable Ruby bin directory at the front
|
||||
of `PATH`. The benchmark prints the normal `hyperfine` output along with each
|
||||
command's stdout/stderr. Outside the default Homebrew prefix it benchmarks
|
||||
`search`, `info`, and `list`, then skips `install`, `reinstall`, `upgrade`,
|
||||
`uninstall`, and `update`. On a default-prefix Tier 1 install it runs the
|
||||
write benchmarks for real.
|
||||
|
||||
Right now only `search` and `list` do meaningful work in Rust. The `info`
|
||||
benchmark measures Rust dispatch plus an immediate handoff back to Ruby, and
|
||||
the mutating-command benchmarks do the same for the existing Ruby/Bash install
|
||||
path. If the vendored binary is missing, the benchmark task builds it first
|
||||
with `vendor-install`.
|
||||
|
||||
```bash
|
||||
brew install hyperfine
|
||||
cd Library/Homebrew/rust/brew-rs
|
||||
rake benchmark
|
||||
```
|
||||
|
||||
## Tier 1 Smoke Test
|
||||
|
||||
Run these on a default-prefix Tier 1 Homebrew install outside this repository checkout:
|
||||
|
||||
```bash
|
||||
brew install hello
|
||||
brew reinstall hello
|
||||
brew upgrade hello
|
||||
brew uninstall hello
|
||||
brew update --quiet --force
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
The current plan is to keep correctness-sensitive install lifecycle work in
|
||||
Ruby while moving the heavy bottle path into Rust in small slices.
|
||||
|
||||
- Keep `brew.sh` as a thin gate and dispatch layer.
|
||||
- Keep using existing Homebrew paths, caches, and metadata formats.
|
||||
- Move more read-only behavior into Rust when it produces clear wins.
|
||||
- For formula installs, move bottle fetch and pour into Rust first.
|
||||
- Keep `post_install` and the more involved finish/finalization steps in Ruby
|
||||
until narrower shared helpers exist and profiling shows a clear benefit to
|
||||
moving more of that logic.
|
||||
@@ -0,0 +1,282 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "fileutils"
|
||||
require "open3"
|
||||
require "pathname"
|
||||
require "rake"
|
||||
require "shellwords"
|
||||
|
||||
module BrewRsTasks
|
||||
module_function
|
||||
|
||||
ROOT = Pathname(__dir__).join("../../../..").realpath.freeze
|
||||
RUST_DIR = Pathname(__dir__).realpath.freeze
|
||||
BREW = (ROOT/"bin/brew").to_s.freeze
|
||||
|
||||
def executable_on_path?(command)
|
||||
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? do |directory|
|
||||
path = File.join(directory, command)
|
||||
File.file?(path) && File.executable?(path)
|
||||
end
|
||||
end
|
||||
|
||||
def run_command(*command, env: {}, chdir: ROOT)
|
||||
success = Dir.chdir(chdir) do
|
||||
system(env, *command, exception: false)
|
||||
end
|
||||
|
||||
return if success
|
||||
|
||||
raise "Command failed: #{Shellwords.join(command)}"
|
||||
end
|
||||
|
||||
def capture_command(*command, env: {}, chdir: ROOT)
|
||||
output, status = Dir.chdir(chdir) do
|
||||
Open3.capture2e(env, *command)
|
||||
end
|
||||
|
||||
return output.strip if status.success?
|
||||
|
||||
raise "Command failed: #{Shellwords.join(command)}\n#{output}"
|
||||
end
|
||||
|
||||
def shell_command(*command, env: {})
|
||||
Shellwords.join(["env"] + env.map { |key, value| "#{key}=#{value}" } + command)
|
||||
end
|
||||
|
||||
def frontend_env
|
||||
{
|
||||
"HOMEBREW_DEVELOPER" => "1",
|
||||
"HOMEBREW_EXPERIMENTAL_RUST_FRONTEND" => "1",
|
||||
}
|
||||
end
|
||||
|
||||
def benchmark_env(current_prefix:, default_prefix:)
|
||||
env = {
|
||||
"HOMEBREW_DEVELOPER" => "1",
|
||||
"HOMEBREW_NO_AUTO_UPDATE" => "1",
|
||||
"HOMEBREW_NO_INSTALL_CLEANUP" => "1",
|
||||
}
|
||||
env["HOMEBREW_INTEGRATION_TEST"] = "1" if current_prefix != default_prefix
|
||||
env
|
||||
end
|
||||
|
||||
def default_prefix
|
||||
system_name = capture_command("uname", "-s")
|
||||
machine = capture_command("uname", "-m")
|
||||
|
||||
if system_name == "Darwin" && machine == "arm64"
|
||||
"/opt/homebrew"
|
||||
elsif system_name == "Linux"
|
||||
"/home/linuxbrew/.linuxbrew"
|
||||
else
|
||||
"/usr/local"
|
||||
end
|
||||
end
|
||||
|
||||
def run_benchmark(title, ruby_command:, rust_command:, prepare: nil, cleanup: nil, runs: 3, warmup: 1)
|
||||
command = ["hyperfine", "--show-output", "--runs", runs.to_s, "--warmup", warmup.to_s]
|
||||
command += ["--prepare", prepare] if prepare
|
||||
command += ["--cleanup", cleanup] if cleanup
|
||||
command += [ruby_command, rust_command]
|
||||
|
||||
puts
|
||||
puts "==> #{title}"
|
||||
run_command(*command)
|
||||
end
|
||||
end
|
||||
|
||||
desc "Build and vendor brew-rs through Homebrew vendor-install"
|
||||
task :build do
|
||||
BrewRsTasks.run_command(BrewRsTasks::BREW, "vendor-install", "brew-rs", env: BrewRsTasks.frontend_env)
|
||||
end
|
||||
|
||||
desc "Check brew-rs formatting"
|
||||
task :fmt do
|
||||
BrewRsTasks.run_command(
|
||||
"cargo",
|
||||
"fmt",
|
||||
"--manifest-path",
|
||||
(BrewRsTasks::RUST_DIR/"Cargo.toml").to_s,
|
||||
"--check",
|
||||
)
|
||||
end
|
||||
|
||||
desc "Lint brew-rs"
|
||||
task :clippy do
|
||||
BrewRsTasks.run_command(
|
||||
"cargo",
|
||||
"clippy",
|
||||
"--manifest-path",
|
||||
(BrewRsTasks::RUST_DIR/"Cargo.toml").to_s,
|
||||
"--all-targets",
|
||||
"--locked",
|
||||
"--",
|
||||
"-D",
|
||||
"warnings",
|
||||
)
|
||||
end
|
||||
|
||||
desc "Run brew-rs Rust tests"
|
||||
task :test do
|
||||
BrewRsTasks.run_command(
|
||||
"cargo",
|
||||
"test",
|
||||
"--manifest-path",
|
||||
(BrewRsTasks::RUST_DIR/"Cargo.toml").to_s,
|
||||
"--locked",
|
||||
)
|
||||
end
|
||||
|
||||
desc "Run brew-rs build, formatting, lint, and Rust tests"
|
||||
task check: [:build, :fmt, :clippy, :test]
|
||||
|
||||
desc "Benchmark brew-rs against the existing frontend"
|
||||
task :benchmark do
|
||||
unless BrewRsTasks.executable_on_path?("hyperfine")
|
||||
abort "hyperfine is required. Install it with: brew install hyperfine"
|
||||
end
|
||||
|
||||
binary_path = BrewRsTasks::ROOT/"Library/Homebrew/vendor/brew-rs/brew-rs"
|
||||
Rake::Task["build"].invoke unless binary_path.executable?
|
||||
|
||||
abort "Rust frontend binary is missing at #{binary_path}" unless binary_path.executable?
|
||||
|
||||
current_prefix = BrewRsTasks.capture_command(BrewRsTasks::BREW, "--prefix")
|
||||
current_default_prefix = BrewRsTasks.default_prefix
|
||||
common_env = BrewRsTasks.benchmark_env(
|
||||
current_prefix: current_prefix,
|
||||
default_prefix: current_default_prefix,
|
||||
)
|
||||
rust_env = common_env.merge("HOMEBREW_EXPERIMENTAL_RUST_FRONTEND" => "1")
|
||||
formula_name = ENV.fetch("BREW_RS_BENCHMARK_FORMULA", "hello")
|
||||
|
||||
BrewRsTasks.run_benchmark(
|
||||
"brew search jq",
|
||||
ruby_command: BrewRsTasks.shell_command(BrewRsTasks::BREW, "search", "jq", env: common_env),
|
||||
rust_command: BrewRsTasks.shell_command(BrewRsTasks::BREW, "search", "jq", env: rust_env),
|
||||
runs: 5,
|
||||
warmup: 2,
|
||||
)
|
||||
|
||||
BrewRsTasks.run_benchmark(
|
||||
"brew info git",
|
||||
ruby_command: BrewRsTasks.shell_command(BrewRsTasks::BREW, "info", "git", env: common_env),
|
||||
rust_command: BrewRsTasks.shell_command(BrewRsTasks::BREW, "info", "git", env: rust_env),
|
||||
runs: 5,
|
||||
warmup: 2,
|
||||
)
|
||||
|
||||
BrewRsTasks.run_benchmark(
|
||||
"brew list",
|
||||
ruby_command: BrewRsTasks.shell_command(BrewRsTasks::BREW, "list", env: common_env),
|
||||
rust_command: BrewRsTasks.shell_command(BrewRsTasks::BREW, "list", env: rust_env),
|
||||
runs: 5,
|
||||
warmup: 2,
|
||||
)
|
||||
|
||||
if current_prefix != current_default_prefix
|
||||
puts
|
||||
puts "==> Skipping write benchmarks outside the default prefix (#{current_default_prefix})"
|
||||
next
|
||||
end
|
||||
|
||||
cleanup_command =
|
||||
"#{BrewRsTasks.shell_command(BrewRsTasks::BREW, "uninstall", "--force", formula_name,
|
||||
env: common_env)} &>/dev/null || true"
|
||||
ensure_installed_command =
|
||||
"#{BrewRsTasks.shell_command(BrewRsTasks::BREW, "list", "--versions", formula_name, env: common_env)} " \
|
||||
"&>/dev/null || " \
|
||||
"#{BrewRsTasks.shell_command(BrewRsTasks::BREW, "install", formula_name, env: common_env)} &>/dev/null"
|
||||
|
||||
BrewRsTasks.run_benchmark(
|
||||
"brew install #{formula_name}",
|
||||
ruby_command: BrewRsTasks.shell_command(BrewRsTasks::BREW, "install", formula_name, env: common_env),
|
||||
rust_command: BrewRsTasks.shell_command(BrewRsTasks::BREW, "install", formula_name, env: rust_env),
|
||||
prepare: cleanup_command,
|
||||
cleanup: cleanup_command,
|
||||
runs: 2,
|
||||
warmup: 0,
|
||||
)
|
||||
|
||||
BrewRsTasks.run_benchmark(
|
||||
"brew upgrade #{formula_name}",
|
||||
ruby_command: BrewRsTasks.shell_command(BrewRsTasks::BREW, "upgrade", formula_name, env: common_env),
|
||||
rust_command: BrewRsTasks.shell_command(BrewRsTasks::BREW, "upgrade", formula_name, env: rust_env),
|
||||
prepare: ensure_installed_command,
|
||||
runs: 2,
|
||||
warmup: 0,
|
||||
)
|
||||
|
||||
BrewRsTasks.run_benchmark(
|
||||
"brew reinstall #{formula_name}",
|
||||
ruby_command: BrewRsTasks.shell_command(BrewRsTasks::BREW, "reinstall", formula_name, env: common_env),
|
||||
rust_command: BrewRsTasks.shell_command(BrewRsTasks::BREW, "reinstall", formula_name, env: rust_env),
|
||||
prepare: ensure_installed_command,
|
||||
runs: 2,
|
||||
warmup: 0,
|
||||
)
|
||||
|
||||
BrewRsTasks.run_benchmark(
|
||||
"brew uninstall #{formula_name}",
|
||||
ruby_command: BrewRsTasks.shell_command(BrewRsTasks::BREW, "uninstall", formula_name, env: common_env),
|
||||
rust_command: BrewRsTasks.shell_command(BrewRsTasks::BREW, "uninstall", formula_name, env: rust_env),
|
||||
prepare: ensure_installed_command,
|
||||
runs: 2,
|
||||
warmup: 0,
|
||||
)
|
||||
|
||||
BrewRsTasks.run_benchmark(
|
||||
"brew update --quiet --force",
|
||||
ruby_command: BrewRsTasks.shell_command(
|
||||
BrewRsTasks::BREW,
|
||||
"update",
|
||||
"--quiet",
|
||||
"--force",
|
||||
env: common_env,
|
||||
),
|
||||
rust_command: BrewRsTasks.shell_command(
|
||||
BrewRsTasks::BREW,
|
||||
"update",
|
||||
"--quiet",
|
||||
"--force",
|
||||
env: rust_env,
|
||||
),
|
||||
runs: 1,
|
||||
warmup: 0,
|
||||
)
|
||||
end
|
||||
|
||||
desc "Stage brew-rs into another Homebrew checkout and build it there"
|
||||
task :stage do
|
||||
repository = Pathname(ENV.fetch("BREW_RS_STAGE_REPOSITORY")).realpath
|
||||
|
||||
FileUtils.install(
|
||||
BrewRsTasks::ROOT/"Library/Homebrew/brew.sh",
|
||||
repository/"Library/Homebrew/brew.sh",
|
||||
mode: 0644,
|
||||
)
|
||||
FileUtils.install(
|
||||
BrewRsTasks::ROOT/"Library/Homebrew/cmd/vendor-install.sh",
|
||||
repository/"Library/Homebrew/cmd/vendor-install.sh",
|
||||
mode: 0755,
|
||||
)
|
||||
FileUtils.mkdir_p(repository/"Library/Homebrew/rust")
|
||||
FileUtils.rm_rf(repository/"Library/Homebrew/rust/brew-rs")
|
||||
BrewRsTasks.run_command(
|
||||
"rsync",
|
||||
"-a",
|
||||
"--delete",
|
||||
"--exclude",
|
||||
"target",
|
||||
"#{BrewRsTasks::RUST_DIR}/",
|
||||
"#{repository}/Library/Homebrew/rust/brew-rs/",
|
||||
)
|
||||
BrewRsTasks.run_command(
|
||||
(repository/"bin/brew").to_s,
|
||||
"vendor-install",
|
||||
"brew-rs",
|
||||
env: BrewRsTasks.frontend_env,
|
||||
chdir: repository,
|
||||
)
|
||||
end
|
||||
@@ -0,0 +1,34 @@
|
||||
use crate::BrewResult;
|
||||
use crate::commands;
|
||||
use crate::delegate;
|
||||
use std::env;
|
||||
use std::process::ExitCode;
|
||||
|
||||
pub(crate) fn main() -> ExitCode {
|
||||
match run() {
|
||||
Ok(code) => code,
|
||||
Err(error) => {
|
||||
eprintln!("{error}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> BrewResult<ExitCode> {
|
||||
let args: Vec<String> = env::args().skip(1).collect();
|
||||
if args.is_empty() {
|
||||
return delegate::run(&args);
|
||||
}
|
||||
|
||||
match args[0].as_str() {
|
||||
"search" => commands::search::run(&args),
|
||||
"info" => commands::info::run(&args),
|
||||
"list" => commands::list::run(&args),
|
||||
"install" => commands::install::run(&args),
|
||||
"reinstall" => commands::reinstall::run(&args),
|
||||
"update" => commands::update::run(&args),
|
||||
"upgrade" => commands::upgrade::run(&args),
|
||||
"uninstall" => commands::uninstall::run(&args),
|
||||
_ => delegate::run(&args),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
use crate::BrewResult;
|
||||
use crate::delegate;
|
||||
use std::process::ExitCode;
|
||||
|
||||
pub fn run(args: &[String]) -> BrewResult<ExitCode> {
|
||||
delegate::run_with_warning(args, "info")
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
use crate::BrewResult;
|
||||
use crate::delegate;
|
||||
use std::process::ExitCode;
|
||||
|
||||
pub fn run(args: &[String]) -> BrewResult<ExitCode> {
|
||||
delegate::run_with_warning(args, "install")
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
use crate::BrewResult;
|
||||
use crate::delegate;
|
||||
use crate::homebrew;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitCode;
|
||||
|
||||
pub fn run(args: &[String]) -> BrewResult<ExitCode> {
|
||||
if args[1..]
|
||||
.iter()
|
||||
.any(|arg| arg.starts_with('-') || arg.contains('/'))
|
||||
{
|
||||
return delegate::run(args);
|
||||
}
|
||||
|
||||
let cellar = homebrew::cellar_path()?;
|
||||
let prefix = homebrew::prefix_path()?;
|
||||
let caskroom = homebrew::caskroom_path()?;
|
||||
|
||||
if args.len() == 1 {
|
||||
let formulae = homebrew::installed_names(&cellar)?;
|
||||
let casks = homebrew::installed_names(&caskroom)?;
|
||||
homebrew::print_sections(&formulae, &casks);
|
||||
return Ok(ExitCode::SUCCESS);
|
||||
}
|
||||
|
||||
let mut missing = Vec::new();
|
||||
let mut listed_any = false;
|
||||
|
||||
for name in &args[1..] {
|
||||
match list_formula_paths(&cellar, &prefix, name)? {
|
||||
FormulaPaths::Paths(paths) => {
|
||||
if listed_any {
|
||||
println!();
|
||||
}
|
||||
println!("{}", paths.join("\n"));
|
||||
listed_any = true;
|
||||
continue;
|
||||
}
|
||||
FormulaPaths::Delegate => return delegate::run(args),
|
||||
FormulaPaths::Missing => {}
|
||||
}
|
||||
|
||||
if let Some(paths) = list_cask_paths(&caskroom, name)? {
|
||||
if listed_any {
|
||||
println!();
|
||||
}
|
||||
println!("{}", paths.join("\n"));
|
||||
listed_any = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
missing.push(name.clone());
|
||||
}
|
||||
|
||||
if !missing.is_empty() {
|
||||
for name in missing {
|
||||
eprintln!("No such keg or cask: {name}");
|
||||
}
|
||||
return Ok(ExitCode::FAILURE);
|
||||
}
|
||||
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
||||
|
||||
enum FormulaPaths {
|
||||
Delegate,
|
||||
Missing,
|
||||
Paths(Vec<String>),
|
||||
}
|
||||
|
||||
fn list_formula_paths(cellar: &Path, prefix: &Path, name: &str) -> BrewResult<FormulaPaths> {
|
||||
let rack = cellar.join(name);
|
||||
let versions = homebrew::installed_versions(&rack)?;
|
||||
if versions.is_empty() {
|
||||
return Ok(FormulaPaths::Missing);
|
||||
}
|
||||
|
||||
if let Some(prefix) = current_keg_path(prefix, &rack, name)? {
|
||||
return list_formula_paths_in(&prefix);
|
||||
}
|
||||
|
||||
if versions.len() == 1 {
|
||||
return list_formula_paths_in(&rack.join(&versions[0]));
|
||||
}
|
||||
|
||||
Ok(FormulaPaths::Delegate)
|
||||
}
|
||||
|
||||
fn current_keg_path(prefix: &Path, rack: &Path, name: &str) -> BrewResult<Option<PathBuf>> {
|
||||
for path in [
|
||||
prefix.join("opt").join(name),
|
||||
prefix.join("var/homebrew/linked").join(name),
|
||||
] {
|
||||
let metadata = match fs::symlink_metadata(&path) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => continue,
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
|
||||
if metadata.file_type().is_symlink() && path.is_dir() {
|
||||
let resolved = fs::canonicalize(&path)?;
|
||||
if resolved.starts_with(rack) {
|
||||
return Ok(Some(resolved));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn list_formula_paths_in(prefix: &Path) -> BrewResult<FormulaPaths> {
|
||||
Ok(FormulaPaths::Paths(list_paths(prefix)?))
|
||||
}
|
||||
|
||||
fn list_cask_paths(caskroom: &Path, name: &str) -> BrewResult<Option<Vec<String>>> {
|
||||
let cask_directory = caskroom.join(name);
|
||||
if !cask_directory.is_dir() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(list_paths(&cask_directory)?))
|
||||
}
|
||||
|
||||
fn list_paths(path: &Path) -> BrewResult<Vec<String>> {
|
||||
Ok(homebrew::list_files(path)?
|
||||
.into_iter()
|
||||
.map(|path| path.display().to_string())
|
||||
.collect())
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
pub mod info;
|
||||
pub mod install;
|
||||
pub mod list;
|
||||
pub mod reinstall;
|
||||
pub mod search;
|
||||
pub mod uninstall;
|
||||
pub mod update;
|
||||
pub mod upgrade;
|
||||
@@ -0,0 +1,7 @@
|
||||
use crate::BrewResult;
|
||||
use crate::delegate;
|
||||
use std::process::ExitCode;
|
||||
|
||||
pub fn run(args: &[String]) -> BrewResult<ExitCode> {
|
||||
delegate::run_with_warning(args, "reinstall")
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use crate::BrewResult;
|
||||
use crate::delegate;
|
||||
use crate::homebrew;
|
||||
use crate::matcher::Matcher;
|
||||
use rust_fuzzy_search::fuzzy_search_threshold;
|
||||
use std::process::ExitCode;
|
||||
|
||||
pub fn run(args: &[String]) -> BrewResult<ExitCode> {
|
||||
if args.len() != 2 || args[1].starts_with('-') {
|
||||
return delegate::run(args);
|
||||
}
|
||||
|
||||
let api_cache = homebrew::cache_api_path()?;
|
||||
let formula_names = match homebrew::read_lines(&api_cache.join("formula_names.txt")) {
|
||||
Ok(names) if !names.is_empty() => names,
|
||||
_ => return delegate::run(args),
|
||||
};
|
||||
let cask_names = match homebrew::read_lines(&api_cache.join("cask_names.txt")) {
|
||||
Ok(names) => names,
|
||||
Err(_) => return delegate::run(args),
|
||||
};
|
||||
|
||||
let matcher = Matcher::try_from(args[1].as_str())?;
|
||||
let matched_formulae = matched_names(&formula_names, &matcher);
|
||||
let matched_casks = matched_names(&cask_names, &matcher);
|
||||
|
||||
if matched_formulae.is_empty() && matched_casks.is_empty() {
|
||||
eprintln!("No formulae or casks found for {:?}.", args[1]);
|
||||
return Ok(ExitCode::FAILURE);
|
||||
}
|
||||
|
||||
homebrew::print_sections(&matched_formulae, &matched_casks);
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
||||
|
||||
fn matched_names(names: &[String], matcher: &Matcher) -> Vec<String> {
|
||||
let matched_names = names
|
||||
.iter()
|
||||
.filter(|name| matcher.matches(name))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
if !matched_names.is_empty() {
|
||||
return matched_names;
|
||||
}
|
||||
|
||||
let Matcher::String(query) = matcher else {
|
||||
return matched_names;
|
||||
};
|
||||
if query.len() < 3 {
|
||||
return matched_names;
|
||||
}
|
||||
|
||||
let candidates = names.iter().map(String::as_str).collect::<Vec<_>>();
|
||||
let mut similar = fuzzy_search_threshold(query, &candidates, 0.5);
|
||||
similar.sort_by(|(_, left), (_, right)| right.total_cmp(left));
|
||||
similar
|
||||
.into_iter()
|
||||
.map(|(name, _)| name.to_string())
|
||||
.collect()
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
use crate::BrewResult;
|
||||
use crate::delegate;
|
||||
use std::process::ExitCode;
|
||||
|
||||
pub fn run(args: &[String]) -> BrewResult<ExitCode> {
|
||||
delegate::run_with_warning(args, "uninstall")
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
use crate::BrewResult;
|
||||
use crate::delegate;
|
||||
use std::process::ExitCode;
|
||||
|
||||
pub fn run(args: &[String]) -> BrewResult<ExitCode> {
|
||||
delegate::run_with_warning(args, "update")
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
use crate::BrewResult;
|
||||
use crate::delegate;
|
||||
use std::process::ExitCode;
|
||||
|
||||
pub fn run(args: &[String]) -> BrewResult<ExitCode> {
|
||||
delegate::run_with_warning(args, "upgrade")
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use crate::BrewResult;
|
||||
use crate::homebrew;
|
||||
use anyhow::Context;
|
||||
use std::process::ExitCode;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
pub(crate) fn run(args: &[String]) -> BrewResult<ExitCode> {
|
||||
run_command(args)
|
||||
}
|
||||
|
||||
pub(crate) fn run_with_warning(args: &[String], command_name: &str) -> BrewResult<ExitCode> {
|
||||
eprintln!("Warning: brew-rs is handing {command_name} back to the Ruby backend.");
|
||||
run_command(args)
|
||||
}
|
||||
|
||||
fn run_command(args: &[String]) -> BrewResult<ExitCode> {
|
||||
let status = Command::new(homebrew::brew_file()?)
|
||||
.args(args)
|
||||
.env_remove("HOMEBREW_EXPERIMENTAL_RUST_FRONTEND")
|
||||
.stdin(Stdio::inherit())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.status()
|
||||
.context("Failed to delegate to brew")?;
|
||||
|
||||
Ok(ExitCode::from(
|
||||
status.code().unwrap_or(1).clamp(0, 255) as u8
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
use crate::BrewResult;
|
||||
use anyhow::{Context, anyhow};
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::{self, IsTerminal};
|
||||
use std::path::{Path, PathBuf};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
pub(crate) fn cache_api_path() -> BrewResult<PathBuf> {
|
||||
Ok(env_path("HOMEBREW_CACHE")?.join("api"))
|
||||
}
|
||||
|
||||
pub(crate) fn cellar_path() -> BrewResult<PathBuf> {
|
||||
env_path("HOMEBREW_CELLAR")
|
||||
}
|
||||
|
||||
pub(crate) fn caskroom_path() -> BrewResult<PathBuf> {
|
||||
env_path("HOMEBREW_CASKROOM")
|
||||
}
|
||||
|
||||
pub(crate) fn prefix_path() -> BrewResult<PathBuf> {
|
||||
env_path("HOMEBREW_PREFIX")
|
||||
}
|
||||
|
||||
pub(crate) fn brew_file() -> BrewResult<PathBuf> {
|
||||
env_path("HOMEBREW_BREW_FILE")
|
||||
}
|
||||
|
||||
pub(crate) fn read_lines(path: &Path) -> BrewResult<Vec<String>> {
|
||||
let contents =
|
||||
fs::read_to_string(path).with_context(|| format!("Failed to read {}", path.display()))?;
|
||||
Ok(contents
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty())
|
||||
.map(ToString::to_string)
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub(crate) fn installed_names(path: &Path) -> BrewResult<Vec<String>> {
|
||||
list_directories(path)
|
||||
}
|
||||
|
||||
pub(crate) fn installed_versions(path: &Path) -> BrewResult<Vec<String>> {
|
||||
list_directories(path)
|
||||
}
|
||||
|
||||
pub(crate) fn list_files(path: &Path) -> BrewResult<Vec<PathBuf>> {
|
||||
if !path.exists() {
|
||||
return Err(anyhow!("Failed to list {}", path.display()));
|
||||
}
|
||||
|
||||
let mut files = WalkDir::new(path)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.filter(|entry| entry.file_type().is_file())
|
||||
.map(|entry| entry.into_path())
|
||||
.collect::<Vec<_>>();
|
||||
files.sort();
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
pub(crate) fn print_sections(formulae: &[String], casks: &[String]) {
|
||||
let stdout_is_tty = io::stdout().is_terminal();
|
||||
|
||||
if stdout_is_tty && !formulae.is_empty() && !casks.is_empty() {
|
||||
println!("Formulae");
|
||||
println!("{}", formulae.join("\n"));
|
||||
println!();
|
||||
println!("Casks");
|
||||
println!("{}", casks.join("\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
if !formulae.is_empty() {
|
||||
println!("{}", formulae.join("\n"));
|
||||
}
|
||||
if !formulae.is_empty() && !casks.is_empty() {
|
||||
println!();
|
||||
}
|
||||
if !casks.is_empty() {
|
||||
println!("{}", casks.join("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
fn env_path(name: &str) -> BrewResult<PathBuf> {
|
||||
env::var_os(name)
|
||||
.map(PathBuf::from)
|
||||
.ok_or_else(|| anyhow!("{name} is not set"))
|
||||
}
|
||||
|
||||
fn list_directories(path: &Path) -> BrewResult<Vec<String>> {
|
||||
if !path.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut entries = fs::read_dir(path)
|
||||
.with_context(|| format!("Failed to list {}", path.display()))?
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.with_context(|| format!("Failed to list {}", path.display()))?;
|
||||
entries.sort_by_key(|entry| entry.file_name());
|
||||
|
||||
Ok(entries
|
||||
.into_iter()
|
||||
.filter_map(|entry| {
|
||||
entry
|
||||
.file_type()
|
||||
.ok()
|
||||
.filter(|file_type| file_type.is_dir())
|
||||
.map(|_| entry)
|
||||
})
|
||||
.map(|entry| entry.file_name().to_string_lossy().into_owned())
|
||||
.collect())
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod app;
|
||||
mod commands;
|
||||
mod delegate;
|
||||
mod homebrew;
|
||||
mod matcher;
|
||||
|
||||
use anyhow::Result;
|
||||
use std::process::ExitCode;
|
||||
|
||||
pub fn main() -> ExitCode {
|
||||
app::main()
|
||||
}
|
||||
|
||||
type BrewResult<T> = Result<T>;
|
||||
@@ -0,0 +1,5 @@
|
||||
use std::process::ExitCode;
|
||||
|
||||
fn main() -> ExitCode {
|
||||
brew_rs::main()
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use crate::BrewResult;
|
||||
use regex::Regex;
|
||||
|
||||
pub(crate) enum Matcher {
|
||||
Regex(Regex),
|
||||
String(String),
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Matcher {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(query: &str) -> BrewResult<Self> {
|
||||
if query.len() > 2 && query.starts_with('/') && query.ends_with('/') {
|
||||
Regex::new(&query[1..query.len() - 1])
|
||||
.map(Self::Regex)
|
||||
.map_err(|error| anyhow::anyhow!("{query} is not a valid regex: {error}"))
|
||||
} else {
|
||||
Ok(Self::String(simplify_string(query)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Matcher {
|
||||
pub(crate) fn matches(&self, value: &str) -> bool {
|
||||
match self {
|
||||
Self::Regex(regex) => regex.is_match(value),
|
||||
Self::String(string) => simplify_string(value).contains(string),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn simplify_string(value: &str) -> String {
|
||||
value
|
||||
.chars()
|
||||
.filter_map(|character| {
|
||||
let lowered = character.to_ascii_lowercase();
|
||||
if lowered.is_ascii_alphanumeric() || lowered == '@' || lowered == '+' {
|
||||
Some(lowered)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# This spec exercises brew.sh dispatch rather than a Ruby class API.
|
||||
# rubocop:disable RSpec/DescribeClass
|
||||
RSpec.describe "brew-rs" do
|
||||
let(:brew_rs_bin) { HOMEBREW_LIBRARY_PATH/"vendor/brew-rs/brew-rs" }
|
||||
let(:brew_rs_cache) { Pathname(TEST_TMPDIR)/"brew-rs-cache" }
|
||||
let(:brew_rs_runtime_env) do
|
||||
{
|
||||
"HOMEBREW_BREW_SH" => (HOMEBREW_LIBRARY_PATH.parent.parent/"bin/brew").to_s,
|
||||
"HOMEBREW_NO_COLOR" => "1",
|
||||
"HOMEBREW_DEVELOPER" => "1",
|
||||
"HOMEBREW_EXPERIMENTAL_RUST_FRONTEND" => "1",
|
||||
}
|
||||
end
|
||||
let(:brew_rs_env) { brew_rs_runtime_env.merge("HOMEBREW_CACHE" => brew_rs_cache.to_s) }
|
||||
let(:api_cache) { brew_rs_cache/"api" }
|
||||
let(:runtime_prefix) { HOMEBREW_LIBRARY_PATH.parent.parent }
|
||||
|
||||
before do
|
||||
skip "brew-rs is not built." unless brew_rs_bin.executable?
|
||||
end
|
||||
|
||||
after do
|
||||
FileUtils.rm_rf brew_rs_cache
|
||||
end
|
||||
|
||||
it "uses the brew-rs search flow", :integration_test do
|
||||
api_cache.mkpath
|
||||
(api_cache/"formula_names.txt").write("testball\n")
|
||||
(api_cache/"cask_names.txt").write("local-caffeine\n")
|
||||
|
||||
expect do
|
||||
expect { brew_sh "search", "l", brew_rs_env }.to be_a_success
|
||||
end.to output(/testball\n\nlocal-caffeine\n/).to_stdout
|
||||
end
|
||||
|
||||
it "uses fuzzy search when plain-text search has no exact matches", :integration_test do
|
||||
api_cache.mkpath
|
||||
(api_cache/"formula_names.txt").write("testball\n")
|
||||
(api_cache/"cask_names.txt").write("")
|
||||
|
||||
expect do
|
||||
expect { brew_sh "search", "testbal", brew_rs_env }.to be_a_success
|
||||
end.to output("testball\n").to_stdout
|
||||
end
|
||||
|
||||
it "matches the Ruby info output", :integration_test do
|
||||
formula_path = setup_test_formula "testball"
|
||||
|
||||
ruby_env = brew_rs_runtime_env.dup.tap { |env| env.delete("HOMEBREW_EXPERIMENTAL_RUST_FRONTEND") }
|
||||
brew_file = brew_rs_runtime_env.fetch("HOMEBREW_BREW_SH")
|
||||
|
||||
ruby_stdout, _ruby_stderr, ruby_status = Open3.capture3(ruby_env, brew_file, "info", formula_path.to_s)
|
||||
rust_stdout, rust_stderr, rust_status = Open3.capture3(brew_rs_runtime_env, brew_file, "info", formula_path.to_s)
|
||||
|
||||
expect(ruby_status.success?).to be true
|
||||
expect(rust_status.success?).to be true
|
||||
expect(rust_stdout).to eq(ruby_stdout)
|
||||
expect(Tty.strip_ansi(rust_stderr)).to include("Warning: using the experimental brew-rs Rust frontend.")
|
||||
end
|
||||
|
||||
it "uses the brew-rs list flow", :integration_test do
|
||||
runtime_cellar = runtime_prefix/"Cellar"
|
||||
runtime_caskroom = runtime_prefix/"Caskroom"
|
||||
|
||||
begin
|
||||
FileUtils.rm_rf runtime_cellar/"foo"
|
||||
FileUtils.rm_rf runtime_caskroom/"local-caffeine"
|
||||
(runtime_cellar/"foo/1.0/bin").mkpath
|
||||
(runtime_cellar/"foo/1.0/bin/foo").write("foo")
|
||||
(runtime_caskroom/"local-caffeine/1.2.3").mkpath
|
||||
|
||||
expect do
|
||||
expect { brew_sh "list", brew_rs_runtime_env }.to be_a_success
|
||||
end.to output(/foo.*local-caffeine/m).to_stdout
|
||||
ensure
|
||||
FileUtils.rm_rf runtime_cellar/"foo"
|
||||
FileUtils.rm_rf runtime_caskroom/"local-caffeine"
|
||||
end
|
||||
end
|
||||
|
||||
it "uses the linked keg when listing formula files", :integration_test do
|
||||
runtime_cellar = runtime_prefix/"Cellar"
|
||||
linked_file = runtime_cellar/"linked-formula/1.0/bin/linked-formula"
|
||||
|
||||
begin
|
||||
FileUtils.rm_rf runtime_cellar/"linked-formula"
|
||||
FileUtils.rm_f runtime_prefix/"opt/linked-formula"
|
||||
(runtime_cellar/"linked-formula/1.0/bin").mkpath
|
||||
linked_file.write("foo")
|
||||
(runtime_cellar/"linked-formula/2.0/bin").mkpath
|
||||
(runtime_cellar/"linked-formula/2.0/bin/linked-formula-newer").write("foo")
|
||||
(runtime_prefix/"opt").mkpath
|
||||
FileUtils.ln_sf(runtime_cellar/"linked-formula/1.0", runtime_prefix/"opt/linked-formula")
|
||||
|
||||
expect do
|
||||
expect { brew_sh "list", "linked-formula", brew_rs_runtime_env }.to be_a_success
|
||||
end.to output(%r{linked-formula/1\.0/bin/linked-formula\n\z}).to_stdout
|
||||
ensure
|
||||
FileUtils.rm_rf runtime_cellar/"linked-formula"
|
||||
FileUtils.rm_f runtime_prefix/"opt/linked-formula"
|
||||
end
|
||||
end
|
||||
end
|
||||
# rubocop:enable RSpec/DescribeClass
|
||||
@@ -0,0 +1,53 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "open3"
|
||||
require "shellwords"
|
||||
|
||||
# This spec exercises a shell helper rather than a Ruby class API.
|
||||
# rubocop:disable RSpec/DescribeClass
|
||||
RSpec.describe "vendor-install.sh" do
|
||||
let(:vendor_dir) { mktmpdir("vendor-install")/"vendor" }
|
||||
let(:vendor_root) { vendor_dir/"brew-rs" }
|
||||
let(:vendor_binary) { vendor_root/"bin/brew-rs" }
|
||||
let(:vendor_symlink) { vendor_root/"brew-rs" }
|
||||
let(:vendor_install_sh) { HOMEBREW_LIBRARY_PATH/"cmd/vendor-install.sh" }
|
||||
let(:check_vendor_script) do
|
||||
<<~SH
|
||||
source #{Shellwords.escape(vendor_install_sh.to_s)}
|
||||
VENDOR_DIR=#{Shellwords.escape(vendor_dir.to_s)}
|
||||
brew-rs-vendor-up-to-date
|
||||
SH
|
||||
end
|
||||
|
||||
before do
|
||||
vendor_binary.dirname.mkpath
|
||||
vendor_binary.write("#!/bin/bash\n")
|
||||
vendor_binary.chmod(0755)
|
||||
File.utime(Time.at(2_000_000_000), Time.at(2_000_000_000), vendor_binary)
|
||||
end
|
||||
|
||||
it "does not recreate the brew-rs launcher symlink while checking freshness" do
|
||||
_stdout, _stderr, status = Open3.capture3(
|
||||
{ "HOMEBREW_LIBRARY" => HOMEBREW_LIBRARY_PATH.to_s },
|
||||
"/bin/bash",
|
||||
"-c",
|
||||
check_vendor_script,
|
||||
)
|
||||
|
||||
expect([status.success?, vendor_symlink.exist?]).to eq([false, false])
|
||||
end
|
||||
|
||||
it "treats the launcher symlink as part of the up-to-date vendor layout" do
|
||||
FileUtils.ln_s("bin/brew-rs", vendor_symlink)
|
||||
|
||||
_stdout, _stderr, status = Open3.capture3(
|
||||
{ "HOMEBREW_LIBRARY" => HOMEBREW_LIBRARY_PATH.to_s },
|
||||
"/bin/bash",
|
||||
"-c",
|
||||
check_vendor_script,
|
||||
)
|
||||
|
||||
expect(status.success?).to be true
|
||||
end
|
||||
end
|
||||
# rubocop:enable RSpec/DescribeClass
|
||||
@@ -58,7 +58,7 @@ RSpec.shared_context "integration test" do # rubocop:disable RSpec/ContextWordin
|
||||
# properly merge coverage results.
|
||||
def command_id
|
||||
Thread.current[:brew_integration_test_number] ||= 0
|
||||
"#{ENV.fetch("TEST_ENV_NUMBER", "")}:#{Thread.current[:brew_integration_test_number] += 1}"
|
||||
"#{Process.pid}:#{ENV.fetch("TEST_ENV_NUMBER", "")}:#{Thread.current[:brew_integration_test_number] += 1}"
|
||||
end
|
||||
|
||||
# Runs a `brew` command with the test configuration
|
||||
@@ -121,12 +121,19 @@ RSpec.shared_context "integration test" do # rubocop:disable RSpec/ContextWordin
|
||||
end
|
||||
|
||||
def brew_sh(*args)
|
||||
env = args.last.is_a?(Hash) ? args.pop : {}
|
||||
env = {
|
||||
"HOMEBREW_USE_RUBY_FROM_PATH" => ENV.fetch("HOMEBREW_USE_RUBY_FROM_PATH", nil),
|
||||
"HOMEBREW_CACHE" => HOMEBREW_CACHE.to_s,
|
||||
}
|
||||
"HOMEBREW_INTEGRATION_TEST" => command_id,
|
||||
}.merge(env)
|
||||
Bundler.with_unbundled_env do
|
||||
stdout, stderr, status = Open3.capture3(env, "#{ENV.fetch("HOMEBREW_PREFIX")}/bin/brew", *args)
|
||||
brew_sh_path = env.delete("HOMEBREW_BREW_SH") || "#{ENV.fetch("HOMEBREW_PREFIX")}/bin/brew"
|
||||
stdout, stderr, status = Open3.capture3(
|
||||
env,
|
||||
brew_sh_path,
|
||||
*args,
|
||||
)
|
||||
$stdout.print stdout
|
||||
$stderr.print stderr
|
||||
status
|
||||
|
||||
Reference in New Issue
Block a user