ADR-0007 — macOS as a first-class platform
- Status: Accepted (2026-07-25)
- Related: ROADMAP v0.9, ADR-0001 (platform abstraction), ADR-0004 (plugin contract), ADR-0005 (cross-distro package identity), COMPATIBILITY.md
- Supersedes in part: ADR-0001’s assumption that a “platform” is a Linux distribution, and ROADMAP’s placement of macOS in the post-1.0 1.x line.
Context
Section titled “Context”ADR-0001 introduced Platform and PackageManager precisely so that macOS and
Windows would later be “just new impls”. That claim is now being tested. Two
years of Linux-only implementation left assumptions that are correct on Linux
and unmarked as Linux-specific:
MachineMetadata.distro_familyis a closed serde enum. Every host is assumed to belong to a Linux distribution family; there is noosaxis at all.rivet_platform::discover_machinereads/etc/os-releaseunconditionally.resolve_window_managerprobesXDG_CURRENT_DESKTOPand Wayland compositor sockets.registry::detectfails with “unsupported Linux distribution”.
None of these are deep. The genuinely hard part of macOS is not detection or
package management — it is that ~/Library is simultaneously where every
application’s configuration lives and where its credentials, cookies, and
caches live, on a filesystem that is case-insensitive by default, behind a
permission system (TCC) that returns EPERM for paths a stat says exist.
A second forcing function: v0.9 is the format-freeze milestone. Any change to the on-disk model is dramatically cheaper now than after v1.0 publishes a compatibility guarantee.
Decision
Section titled “Decision”1. Split the OS axis from the distro axis.
MachineMetadata gains an os_family field. distro_family keeps its exact
meaning — which Linux distribution family — and reads Unknown on macOS.
pub enum OsFamily { Linux, MacOs, Unknown }Modelling macOS as another DistroFamily variant was the cheaper edit and is
wrong: it would make DistroFamily::MacOs flow into the cross-distro package
map, the /etc/os-release parser, and every match that reasons about Linux
package managers. Two orthogonal facts get two fields.
2. Both enums become forward-compatible.
OsFamily and DistroFamily both gain #[serde(other)] Unknown. Without this,
a v1.0 binary reading a snapshot from a later Rivet that added a variant
(Windows, or a new distro family) fails deserialization outright rather
than degrading. This is the single most time-critical item in the macOS track:
it must land before the format freeze, because after v1.0 the failure mode is
permanent for every binary already in the wild.
3. Parsers are portable; only execution is cfg-gated.
Every macOS module splits into a pure parsing function and a thin
command-running caller. The parsers compile and unit-test on all hosts; only
the exec:: calls sit behind #[cfg(target_os = "macos")]. This matches how
pacman.rs/flatpak.rs are already structured, and it means Linux CI catches
a brew info --json=v2 parsing regression without a mac runner.
4. Homebrew is the native manager; MAS and MacPorts are secondary.
MacOsPlatform::package_managers() returns HomebrewManager, MasManager,
and MacPortsManager. Homebrew’s native_manager id is "brew", which becomes
a new column in the ADR-0005 curated package map — ADR-0005 already anticipated
this (“the same logical concept later absorbs Homebrew”). Casks and taps are
additive PackageManifest sections, not overloaded onto native: a cask is not
a formula and restoring it needs a different command.
Restore issues explicit per-package brew install calls rather than generating
a Brewfile and shelling out to brew bundle. Brewfile would be less code,
but it collapses plan() into a single opaque action and Rivet’s whole restore
UX is built on a reviewable action list.
5. ~/Library is opt-in per path, and the Keychain is permanently out of
scope.
ADR-0004’s rule — plugins declare small reviewable roots, never scan $HOME —
is what makes macOS support safe at all, and it is enforced harder here:
- Every new root under
Library/is audited individually against a deny list (Library/Keychains,Library/Cookies,Library/Containers, known token blobs). - Rivet never invokes
security(1)and never exports Keychain material, under any flag. Secrets belong in the ADR-0003 vault, placed there deliberately by the user.
6. macOS-specific failure modes get first-class diagnostics, not raw errno.
- A TCC denial (
EPERMon a readable-looking~/Librarypath) is translated into an instruction to grant Full Disk Access to the terminal. - A case-insensitive destination volume that would collapse two distinct
snapshot entries (
Foo.confandfoo.conf) refuses the restore rather than silently writing one over the other. This has no Linux analogue and is a genuine data-loss path. - Extended attributes and resource forks are not preserved; this is stated on the restore page rather than discovered.
7. macOS ships in v0.9, and v0.9 is rescoped.
v0.9 was “feature-frozen Linux RC”. It becomes “macOS parity, then RC”. The
alternative — hold macOS for v1.1 — keeps the existing freeze intact but forces
either a MachineMetadata format break in a 1.x minor or a permanently closed
enum. Doing it before the freeze is the cheaper order of operations.
Alternatives considered
Section titled “Alternatives considered”DistroFamily::MacOs. One-line change; conflates two orthogonal facts and leaks macOS into every Linux package-managermatch. Rejected.- A separate
rivet-macoscrate. Cleaner isolation on paper, but the managers are 80–150 lines each and sharepackage_linesand thePlatformregistry with their Linux peers. A crate boundary here buys nothing and splits the registry. Rejected; modules inrivet-platforminstead. brew bundlefor capture and restore. Fewer moving parts, but an opaque single-action restore plan. Rejected — see decision 4.- Blanket
defaults export -gfor system preferences. Captures the entire global domain including window positions, recent-document lists, and hardware-specific state, which then restores onto different hardware. Rejected in favour of per-domain exports. - Capture the Keychain via
security export. Requires interactive authorization per item, produces material Rivet’s threat model says it must never hold, and duplicates the vault. Rejected permanently, not deferred. - Ship macOS as x86_64-only and rely on Rosetta. Works, but a Rosetta
Homebrew reports the
/usr/localprefix and the wrong architecture, which poisons the captured metadata. Native builds for both architectures, fused into a universal binary. Rejected.
Consequences
Section titled “Consequences”- Positive: the OS axis exists before the format freezes, so Windows later is additive rather than breaking.
- Positive: ADR-0001’s central bet is validated — macOS is genuinely impl + data, with no redesign of the trait seams.
- Positive:
brewin the package map makes Linux→macOS migration real, which is the most-requested direction for a laptop-refresh workflow. - Cost: the curated package map roughly doubles in maintenance surface.
- Cost: CI grows two macOS runners; release builds grow a
lipostep. - Risk:
~/Librarycoverage is a long tail. The deny list is a safety net, not a guarantee — each new plugin root remains a review item, and the docs must say that capture is deliberately incomplete rather than imply parity with a full-disk backup tool. - Risk: without an Apple Developer ID, downloaded binaries are quarantined by Gatekeeper. Mitigated by making a Homebrew tap the primary install channel (tap-installed binaries are not quarantined); notarization stays optional.