Skip to content

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.

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.

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.

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.

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).

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.

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):

  1. 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).
  2. At restore time, if the target host’s native manager differs, each recorded LogicalPackage is 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.
  3. 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:

Terminal window
rivet restore --repo ~/rivet-snapshots --strict-packages

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 the rivet-pkgmap-gen workspace 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.

.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.

  • 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.