Cross-distro and macOS support
Rivet v0.3 generalised the Arch-only v0.1/v0.2 foundation to six Linux distro
families, and v0.9 added macOS on both Apple Silicon and Intel. Both follow
ADR-0001 (the Platform/PackageManager
traits), ADR-0005 (cross-distro package
identity), and ADR-0007 (macOS as a first-class
platform).
This is the reference document for what is supported, how detection works, and how a restore across families — or across operating systems — stays honest rather than guessing.
Two independent axes describe a host, and rivet scan reports both:
| Field | Values | Meaning |
|---|---|---|
os_family |
linux, mac_os, unknown |
Which operating system |
distro_family |
arch, debian, fedora, suse, nix, void, unknown |
Which Linux distribution — always unknown on macOS |
An os_family this binary does not recognise reads as unknown rather than
failing to load; see COMPATIBILITY.md.
Supported distro families
Section titled “Supported distro families”| Family | distro_family |
Native manager | Detected via /etc/os-release |
|---|---|---|---|
| Arch and derivatives (CachyOS, EndeavourOS, Manjaro) | arch |
Pacman (+ AUR via paru/yay) |
ID/ID_LIKE contains arch, cachyos, endeavouros, or manjaro |
| Debian and derivatives (Ubuntu, Linux Mint, Pop!_OS) | debian |
APT | ID/ID_LIKE contains debian, ubuntu, linuxmint, or pop |
| Fedora and derivatives (RHEL, CentOS, Rocky, AlmaLinux) | fedora |
DNF | ID/ID_LIKE contains fedora, rhel, centos, rocky, or almalinux |
| openSUSE (Tumbleweed, Leap) and SLES | suse |
Zypper | ID starts with opensuse, or ID/ID_LIKE contains suse/sles |
| NixOS | nix |
nix profile |
ID/ID_LIKE contains nixos |
| Void | void |
XBPS | ID/ID_LIKE contains void |
rivet scan (or scan --json) reports the detected family, native manager,
distro version, and window manager without requiring any package manager to
already be installed — detection reads only /etc/os-release. Every native
manager also gets Snap and Flatpak alongside it, since those are
cross-cutting sources available on any distro (rivet-platform/src/*.rs,
crates/rivet-platform/src/registry.rs).
An unrecognised distro is not silently guessed at: rivet returns a clear
error asking you to run rivet scan --json and file an issue.
macOS is detected at compile time — a binary built for Darwin is running on
Darwin — so there is no /etc/os-release equivalent to parse. Host facts come
from sw_vers, and native_manager is always brew.
| Manager | Discovery command | Restored with |
|---|---|---|
| Homebrew formulae | brew leaves --installed-on-request |
brew install |
| Homebrew casks | brew list --cask |
brew install --cask |
| Homebrew taps | brew tap, minus the built-in homebrew/core and homebrew/cask |
brew tap, before any formula |
| Mac App Store | mas list |
mas install |
| MacPorts | port -q installed requested |
sudo port install |
Homebrew runs without sudo — it refuses to run as root — unlike every
Linux native manager here.
A mas install needs a signed-in App Store account, which Rivet will not
automate. An unavailable mas, a signed-out account, or an app not tied to
this Apple ID each produce a warning naming the app rather than aborting the
restore.
Apple Silicon and Intel
Section titled “Apple Silicon and Intel”Both are supported natively; the release ships one universal binary
containing both slices. The only substantive difference is the Homebrew
prefix (/opt/homebrew on Apple Silicon, /usr/local on Intel), which Rivet
resolves from the filesystem rather than inferring from the architecture.
What macOS captures, and what it deliberately does not
Section titled “What macOS captures, and what it deliberately does not”~/Library is both the configuration directory and the credential store, so
every macOS plugin root is chosen individually rather than by taking an
application’s whole directory. On top of that, a deny list refuses these
subtrees to any plugin, including one you write yourself:
Library/Keychains, Library/Cookies, Library/Containers,
Library/Group Containers, Library/Caches, Library/Mail,
Library/Messages, Library/Safari, Library/Accounts, and known session
token stores (Slack, Discord, Raycast’s clipboard history).
Rivet never exports the Keychain, under any flag. Secrets belong in the vault, placed there deliberately.
System preferences are captured with per-domain defaults export, never
defaults export -g: the global domain holds window positions and
recent-document lists, which is another machine’s screen geometry restored
onto yours.
macOS-specific failure modes
Section titled “macOS-specific failure modes”| Symptom | Cause | Fix |
|---|---|---|
macOS denied access to Library/… |
TCC, not file permissions | Grant Full Disk Access to your terminal in System Settings → Privacy & Security |
| Restore refuses, naming two paths that differ only in case | The destination volume is case-insensitive (the APFS default) and the snapshot holds both | Restore onto a case-sensitive volume, or drop one of the pair |
Extended attributes, resource forks, and Finder metadata are not preserved: restore writes file contents and permissions only. A restored download carries no quarantine flag.
Native package discovery
Section titled “Native package discovery”Each native manager records explicit, user-installed packages only —
never dependencies, which the destination host’s manager resolves on its
own — mirroring Pacman’s -Qqe semantics:
| Manager | Discovery command | Notes |
|---|---|---|
| Pacman | pacman -Qqe |
Foreign (AUR) packages via pacman -Qqm, restored with paru/yay |
| APT | apt-mark showmanual, intersected with dpkg-query’s installed set |
showmanual alone can list packages removed but never unmarked |
| DNF | dnf repoquery --userinstalled --qf '%{name}', falling back to dnf history userinstalled on dnf5 |
|
| Zypper | zypper search --installed-only --type package |
Zypper has no explicit-vs-dependency distinction, so this includes dependencies too |
| XBPS | xbps-query -m, reduced from <name>-<version>_<revision> to the bare name |
|
| Nix | nix profile list, reassembled as <flake url>#<attribute> |
Stored in its own manifest field (nix), not native — usable on any distro, not just NixOS |
An unavailable tool degrades to a warning, never a hard error (rivet scan
still succeeds and reports what it could).
Desktop environments and window managers
Section titled “Desktop environments and window managers”| Plugin | Roots | Hook |
|---|---|---|
kde-plasma |
Plasma/KWin/Dolphin/Konsole config + local themes | — |
gnome |
.config/gtk-3.0, .config/gtk-4.0 |
dconf dump / / dconf load / → gnome-dconf.ini |
cinnamon |
.config/cinnamon |
dconf dump /org/cinnamon/ / dconf load /org/cinnamon/ → cinnamon-dconf.ini |
xfce |
.config/xfce4 |
— (Xfce already stores per-channel settings as XML there) |
hyprland |
.config/hypr |
— |
sway |
.config/sway |
— |
niri |
.config/niri |
— |
i3 |
.config/i3, .i3 |
— |
GNOME and Cinnamon store most preferences in dconf, not on-disk files, so
those two run an explicit hook: an argv command (never a shell string)
whose captured stdout is secret-scanned like any tracked file and stored at
hooks/<artifact> in the snapshot repository. On restore, the matching
import command replays it via stdin, only under --apply --yes, and only
when the tool is available — an unavailable dconf degrades to a skipped
hook, never a failed restore. See rivet-core/src/hooks.rs and
docs/v0.3-implementation-spec.md §6 for the full mechanism; declarative
user-defined hooks reuse the same machinery in v0.6.
Cross-distro restore
Section titled “Cross-distro restore”A snapshot always records which native manager it came from
(manifest.packages.native_manager). When you restore on a host whose
native manager differs, Rivet does not install the source packages’ names
verbatim — it resolves each one through PackageMap
(rivet-core/src/packagemap.rs):
- At snapshot time, every native package is looked up against the
package map and — when known — recorded as a
LogicalPackage(a distro-agnostic identity plus which source produced the match). - At restore time, if the target host’s native manager differs, each
recorded
LogicalPackageis resolved to that manager’s name:- Found → a normal install action, noting when the match came from the lower-confidence generated list.
- Not found → a warning, never an attempted install.
- Same-family restores never touch the package map at all — native names are used verbatim, exactly as in v0.2.
Foreign (AUR) packages are Arch-specific by construction; a cross-family restore reports how many were skipped rather than attempting anything with them.
Pass --strict-packages to rivet restore to refuse lower-confidence
matches and install only exact, hand-curated ones:
rivet restore --repo ~/rivet-snapshots --strict-packagesThe package map itself
Section titled “The package map itself”rivet-core/packagemap/ ships two embedded TOML assets, both keyed by a
logical package id with one row per native manager:
-
curated.toml— hand-verified by a Rivet maintainer (Confidence::Curated), and always wins over the generated map on conflict. This is where a wrong or missing mapping gets fixed. Add an entry whenever a cross-distro restore reports an unmapped package you have verified by hand. -
generated.toml— produced offline from a Repology bulk export by therivet-pkgmap-genworkspace member (Confidence::Generated), not a Rivet runtime dependency. Ships empty until a maintainer runs:Terminal window cargo run -p rivetsnap-pkgmap-gen -- <repology-export.json>from the workspace root, which regenerates
crates/rivet-core/packagemap/generated.toml. Repology repository ids are mapped to Rivet’s five native managers via prefix match (arch*→ Pacman,debian*/ubuntu*→ APT,fedora*→ DNF,opensuse*→ Zypper,void*→ XBPS); any other repository is ignored.
CI coverage
Section titled “CI coverage”.github/workflows/ci.yml’s distro-matrix job runs the full test suite and a
release build inside archlinux:latest, debian:stable, fedora:latest,
opensuse/tumbleweed:latest, and ghcr.io/void-linux/void-glibc-full:latest
containers (5 of the 6 supported native families — see below for NixOS), plus
asserts rivet scan --json reports the matching distro_family. No package is
actually installed in CI: the round-trip tests disable every package source,
and the cross-distro test only asserts the dry-run plan. A real migration
dry-run against a live host per family remains a manual maintainer
verification step ahead of tagging a release.
NixOS is not in the container matrix. There is no actively maintained
Docker image that is genuinely the NixOS distribution (a real
/etc/os-release with ID=nixos) while also fitting GitHub Actions’
container: step model the same way the other five do. The commonly reached-for
nixos/nix image is the Nix package manager on a non-NixOS base — using it
would only validate a fabricated os-release file, not the real detection
path. rivet-platform’s distro_family_covers_every_documented_id unit test
(runs on every push, in the primary test job) fully covers the nixos
ID/ID_LIKE parsing logic; a real NixOS container is a candidate follow-up
if a suitable image appears or Rivet builds one itself.
What is not multi-distro yet
Section titled “What is not multi-distro yet”- Repository/PPA enablement. v0.3 installs only from already-configured repositories; a package that needs a new repo or PPA enabled first is out of scope (deferred — see the v0.3 spec’s escalate-to-Opus triggers). Version pinning is out of scope for the same reason.
- Interactive per-manager selection.
checklist --select’s toggles still only cover Pacman/AUR/AppImage/Snap/Flatpak; the other native managers are captured by default with no interactive opt-out yet. - Windows remains out of scope until v1.1+ (winget/Scoop and
registry-aware plugins) per
docs/ROADMAP.md. - Cross-platform configuration restore does not translate paths. A
macOS snapshot restored onto Linux installs packages through the map and
restores portable dotfiles, but its
Library/paths are skipped and reported, not rewritten into.config/equivalents. Guessing an application’s layout on a platform it was not captured from is precisely the silent inference ADR-0005 exists to prevent. - Casks have no Linux equivalent. A Homebrew cask is a macOS application bundle, so it is restored macOS→macOS only; a Linux snapshot landing on a Mac reports its GUI applications as unmapped rather than guessing a cask name.