Repository navigation
feat(brew): Brew — Homebrew formulae and casks as a v2 resource (ENG-3738) - #33
Merged
Ryan Zhu (underthestars-zhy) merged 2 commits intoNov 1, 2026
Conversation
…3738) v2 could install CLT and Homebrew itself, but not a single formula or cask, and most farm setup is Brew(...). Brew ports v1's Brew onto @resource, @monitor, and the Homebrew facts and lock: CommandLineTools() Homebrew(owner: "ops") Brew("wget") Brew("firefox", type: .cask) Brew("tailscale", version: .atLeast("1.102.0")) - Brew is a Component that declares BrewPackage (astrolabe.brew) once \.homebrew.isInstalled reads true. Gated on isInstalled, never on one owner: remove really uninstalls, and mixed owners (another admin's brew install) would otherwise hide, and uninstall, every package. - The id is brew:<type>:<short name>, so tap-qualifying a name keeps the id and never uninstalls. The hash covers name, type and minimum: a new .atLeast floor upgrades in place. - rollout: check the Mac without the lock (satisfied → no lock, no brew); else under HomebrewContext.lock, check again, resolve the one owner from system.owners(), brew install|upgrade --formula|--cask <name> as the owner, and verify. An uncomparable version throws and never upgrades. [] owners → homebrewMissing; mixed, root or #uid → homebrewNotReady, retried once Homebrew re-owns. - remove: brew uninstall --formula|--cask <short name>, only if it's there. Uses only what the id is built from. - \.homebrew.packages scans Cellar, Caskroom and opt every 60 s with readdir and readlink, no brew and no lock. Throws only on real I/O errors. Each BrewPackage monitors \.homebrew[satisfies:], its own Bool, so a hand-run brew uninstall or a downgrade is undone within a minute. This replaces v1's 15 s loop. - .latest(checkEvery:) is dropped: time passing never changes a hash. A follow-up adds an available fact that body pins into .atLeast. - HomebrewSystem gains packages() and brew(_:as:). BrewVersion is v1's, verbatim. - Examples/BrewEngine reads packages from /etc/astrolabe-brew-packages. - Tests: ids, hashes and the snapshot; the scan on a temp prefix; v1's version cases; check and the subscript; ensure and uninstall over the fake (call order, no lock when satisfied, upgrade, floors, owners, failures, casks, taps, cancellation, one brew at a time); and the engine: CLT then Homebrew then Brew with no failures, no Homebrew(), drift, dropped from body and across a restart, a new floor, tap qualification, mixed owners, a deleted Homebrew, a superseded rollout waiting on the lock, and the dry run. FakeHomebrewSystem keeps one package map and runs brew. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the
|
…e' into ryan/eng-3738-astrolabe-brew-as-a-v2-resource
Ryan Zhu (underthestars-zhy)
merged commit Nov 1, 2026
6b21452
into
ryan/eng-3749-astrolabe-homebrew-itself-as-a-v2-resource
1 check passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements ENG-3738. Stacked on #32 (ENG-3749), which is stacked on #31, #30, #29, #28, #26 and #25 (
next).What
v2 could install CLT (ENG-3739) and Homebrew itself (ENG-3749), but not a single formula or cask, and most farm setup is
Brew(...). This ports v1'sBrewonto@Resource,@Monitor, and the Homebrew facts and lock from #32:Brew("wget")brew install --formula wgetas Homebrew's owner, once Homebrew is on the Macbodybrew uninstall --formula wget, also after a daemon restart (fromrecords.json).installed→.atLeast("1.25")brew upgradeonly if below 1.25. Never uninstalls"cloudflared"→"cloudflare/cloudflare/cloudflared".formula→.cask(same name)brew uninstall wgetbrew installagainbrew install jqHomebrewre-owns the prefix. Nothing is uninstalledHomebrew(owner:)Brewwaits silentlyHomebrewreinstalls, then everyBrewinstalls again\.homebrew.isInstalled, never on one owner.removereally uninstalls, and anything not declared is removed. Gating onowners.count == 1would uninstall every package whenever another admin runsbrew install.isInstalledgoes false only whenbin/brewis gone, and then the Cellar usually is too, soremovefinds nothing.@Monitor. A new fact,\.homebrew.packages, scansCellar,Caskroomandoptevery 60 s withreaddir/readlink(nobrew, no lock). EachBrewPackagemonitors only its ownBool(\.homebrew[satisfies: …], internal), so installing one package never reruns the others. This replaces v1's 15 sloop()..latest(checkEvery:)is dropped. Time passing never changes a hash. A follow-up adds anavailablefact thatbodypins into.atLeast.\.homebrew.packagesis public, so app code can@Factit, e.g. to declare something only once a binary is installed.How it runs
brewrollout(BrewInstaller.ensure):brew.HEAD-…,latest, av1.2floor) throws and never upgrades, as in v1.HomebrewContext.lock:Task.checkCancellation(), check again, then resolve the owner fromsystem.owners()(neverHomebrewReads.owners(), which takes the same lock).brew install|upgrade --formula|--cask <declared name>as the owner, streamed to the log, then verify against the Cellar.remove(BrewInstaller.uninstall): same shape, withbrew uninstall --formula|--cask <short name>, only if it's installed. It uses only what the id is built from, since the snapshot it gets isn't necessarily the last success.[]throwshomebrewMissing. Several owners, root, or a uid with no account throwhomebrewNotReady, retried (60 s → 1 h) onceHomebrewre-owns.install.sh, re-owns and theownersread, sobrewnever runs during an install and never two at once. Brew never touches the CLT lock. The timeout is 4 h, mostly for waiting on the lock: on first boot every package queues behind the others.Changes
BuiltIn/Brew/Brew.swift: the publicBrewcomponent (PackageType,Version) and the internalBrewPackage(astrolabe.brew, idbrew:<type>:<short name>; the hash covers name, type and minimum).BrewFact.swift:\.homebrew.packages, the[satisfies:]subscript, and the publicBrewPackages.BrewInstaller.swift:BrewName,BrewCheck,ensure,uninstall,BrewError.BrewSystem.swift:BrewScan, pluspackages()andbrew(_:as:)onInstallScriptHomebrew.Cellar/Caskroomreads as empty. Any other I/O error throws, so a hiccup never reads as "nothing installed".optlink target, otherwise the highest keg.BrewVersion.swift: v1's, verbatim.HomebrewSystemgainspackages()andbrew(_:as:). AlsoHomebrewReads.packages(), and comment updates on the lock and onHomebrewFacts.Examples/BrewEnginereads packages from/etc/astrolabe-brew-packages(wget,cask firefox,tailscale >= 1.102.0).Tests
swift teston this Mac: 335 tests. Everything passes exceptstorageClientConcurrentWritesPreserveAllKeys(.helperNotFound), which also fails onmain.BrewTestspassed in 8 runs in a row.BrewTests(63), overFakeHomebrewSystem(now one package map, plusbrewthat suspends, so overlapping runs would show) andFakeCLTSystem:packages.BrewVersion: v1's cases, verbatim.check: every case, and the subscript agrees with it.ensure/uninstall: exact call order; no lock when satisfied or absent; upgrade; still below the floor; uncomparable; each owner case; brew failure and a brew that didn't take; casks; tap names; cancelled while waiting;maxRunning == 1.operation.failed;Homebrew(), on a clean Mac and on an existing one;body, and across a restart;brew;brew;Removing the lock from
ensuremakes three of these fail (maxRunning, cancelled-while-waiting, superseded), so they do check serialization.Also checked:
BrewScanon this Mac's real/opt/homebrew(read-only) agrees withbrew liston all 227 formulae and 9 casks, in about 50 ms.Not yet verified
The farm-Mac steps need root: every Astrolabe binary refuses to start without it,
--dry-runincluded. On a farm Mac or a clean VM, afterswift build -c release:/etc/astrolabe-homebrew-ownerand the list to/etc/astrolabe-brew-packages(temp file, thensudo mv).sudo .build/release/BrewEngine --dry-run, thensudo .build/release/BrewEngine.brew as-brew-user uninstall wget(back within 60 s), removing a line, addingtailscale >= 1.102.0, and another admin'sbrew install jq.Known gap (accepted)
If
bin/brewis gone while the Cellar stays (a partial deletion, or a read in the instantbrew updaterewritesbin/brew),isInstalledreads false and everyBrewPackageis removed, then reinstalled afterHomebrewrepairs it. It's rare and limited to one Mac. AHeld(until:)structural freeze would close it if it ever shows up.ENG-3738's description predates
@MonitorandHomebrew(owner:); I'll update it when this lands.🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need.