Derive Your Own Distro
Arch Silverblue is built so you can fork it into your own atomic Arch-based distro by editing
one file — config/distro.conf — and rebuilding. You do not need to
hunt through the scripts for the string silverblue; the build reads everything it needs to
rebrand from that config.
Reference implementation: SilverDeck — a gaming-console distribution built on Arch Silverblue — is a real-world derivative and the reference example of this fork-and-own model.
How it works (fork-and-own model)
Section titled “How it works (fork-and-own model)”Your derivative is a full copy of this repository that you own and maintain. You edit
config/distro.conf, run the build, and ship your ISO. To pick up later upstream fixes you
git merge (or cherry-pick) from upstream like any other fork.
The build performs two strictly separated transformations, which is why a default (unedited) build is identical to upstream:
- The source tree is never rewritten.
src/keeps itssilverbluedefaults so the unit tests keep working. Branding in the engine is just env-var indirection whose defaults equal today’s values. - Renaming/branding happens at build & install time.
iso/build.shreadsconfig/distro.confand renders/renames copies of the tools into the ISO; the autoinstaller (which gets the same config copied into the ISO) lays those down on the target and writes/etc/os-release, hostname, timezone, locale, and any extra repos.
config/distro.conf ──► iso/build.sh ──► ISO (renamed binary, units, paths, os-release.in, config) │ └──► autoinstaller (in the ISO) ──► installed target (os-release, hostname, tz, locale, repos, tools)Prerequisites
Section titled “Prerequisites”Same as upstream (see README.md):
make test(lint + unit tests + unit verification): just needsbash;shellcheck/batsare pulled on demand vianix shell.make build-iso: Docker with--privilegedand network (all Arch tooling runs in the container).make test-qemu: QEMU.
The one file you edit: config/distro.conf
Section titled “The one file you edit: config/distro.conf”It is plain bash (KEY="value" and arr=(...)), sourced by the build. The defaults reproduce
stock Arch Silverblue. Variables, grouped:
Identity / branding (also fills /etc/os-release)
Section titled “Identity / branding (also fills /etc/os-release)”| Variable | Purpose |
|---|---|
DISTRO_ID |
lowercase machine id → os-release ID, LOGO, and the Docker image tag |
DISTRO_NAME |
pretty name → boot-entry titles and os-release NAME/PRETTY_NAME |
DISTRO_VERSION / DISTRO_VERSION_ID |
os-release BUILD_ID / VERSION_ID |
DISTRO_ANSI_COLOR |
os-release ANSI_COLOR |
DISTRO_HOME_URL |
os-release HOME_URL and the Documentation= URL in the systemd units |
DISTRO_DOC_URL / DISTRO_SUPPORT_URL / DISTRO_BUG_URL |
the matching os-release URLs |
Names & paths (rendered into the image only)
Section titled “Names & paths (rendered into the image only)”| Variable | Purpose | Required for a rename? |
|---|---|---|
BIN_PREFIX |
the CLIs become ${BIN_PREFIX}-update and ${BIN_PREFIX}-install (/usr/bin/silverblue-update / -install by default) |
yes |
UNIT_PREFIX |
the units become ${UNIT_PREFIX}-mark-good.service / -rollback.service / -rollback.target |
yes |
LIB_DIR |
helper/library dir (/usr/lib/silverblue) |
yes |
ESP_SUBDIR |
per-snapshot kernel dir on the ESP (/efi/silverblue/<snap>, systemd-boot) |
cosmetic |
SORT_KEY |
systemd-boot sort-key grouping value |
cosmetic |
TOPLEVEL_MNT |
transient Btrfs subvolid=5 mountpoint |
cosmetic |
BIN_PREFIX and UNIT_PREFIX are usually set to the same value as DISTRO_ID.
System defaults (written into the installed target)
Section titled “System defaults (written into the installed target)”| Variable | Purpose |
|---|---|
HOSTNAME |
/etc/hostname |
TIMEZONE |
zoneinfo path (e.g. Europe/Amsterdam) |
LOCALE |
locale.gen entry + /etc/locale.conf LANG |
KEYMAP |
vconsole keymap ("" = leave default) |
These are what the unattended test install writes verbatim, and what the interactive
installer offers as prompt defaults (BOOTLOADER below likewise seeds its bootloader menu).
Init system
Section titled “Init system”| Variable | Purpose |
|---|---|
INIT_SYSTEM |
default init system of the installed target: systemd (default), openrc, or dinit; seeds the installer’s init menu and the autoinstaller |
INIT_CHOICES |
choices the interactive installer offers (systemd openrc dinit by default) |
The installer is init-aware: systemd targets pacstrap the stock Arch PKGS_BASE and keep
the Arch repos; openrc/dinit targets swap the installed system to the Artix
repositories and use the Artix base (PKGS_BASE_OPENRC/PKGS_BASE_DINIT). Stock Arch’s
core repo drags in systemd, and the Artix base package requires the virtual
init-logind that the per-init elogind-<init> package provides — so the swap installs
base <init> elogind-<init>, the target’s /etc/pacman.conf gets the Artix repo sections
([system] [world] [galaxy] [lib32], ordered from ARTIX_REPOS), and
/etc/pacman.d/mirrorlist is rendered from ARTIX_MIRRORS.
Notes for non-systemd derivatives (Artix-style):
- Set
PKGS_BASE_OPENRC/PKGS_BASE_DINITto whatever those targets should boot into — the default Artix sets arebase <init> elogind-<init> linux mkinitcpio btrfs-progs dosfstools gptfdisk efibootmgr. The systemdPKGS_BASEis left untouched. The engine only wires up the health check; the init itself comes from these package sets. - The health-check/rollback machinery works natively on all three inits (see
src/init/init-backends.shfor the per-init health semantics). On OpenRC/dinit the*-boot-check.shwrapper self-manages the 120s timeout and failure→rollback dispatch, since those inits have noOnFailure=equivalent. - The systemd watchdog drop-in has no OpenRC/dinit analog: hang protection on those inits
comes from systemd-boot boot-counting / GRUB
recordfailonly (optionally add awatchdogdto your package set). enable_network_stackis init-aware:networkdstays systemd-only, butNetworkManager/dhcpcdenable their service on all three inits (systemctl /rc-update add … default/ aboot.dsymlink), and the installer adds the matching per-init package (networkmanager-<init>,dhcpcd-<init>) on non-systemd targets.- Guest-agent packages are init-aware too:
qemu-guest-agent/spice-vdagent(plusopen-vm-tools,virtualbox-guest-utilson systemd) are offered when a virtual machine is detected, and the per-init service packages are used on openrc/dinit. - The live ISO always runs systemd regardless of
INIT_SYSTEM(archiso is systemd-based).
Bootloader / filesystem
Section titled “Bootloader / filesystem”| Variable | Purpose |
|---|---|
BOOTLOADER |
default bootloader when none is forced (systemd-boot or grub) |
FS_LABEL |
Btrfs root label + GPT partition name |
ESP_LABEL |
FAT32 ESP label |
EFI_DIR |
ESP mountpoint on the installed system — advanced; leave at /efi (the autoinstaller’s grub-install/mount paths assume /efi) |
KEEP_SNAPSHOTS |
max snapshots retained by the engine (SB_KEEP) |
BOOT_TRIES |
systemd-boot boot-counting tries (SB_TRIES) |
Snapshot integrity
Section titled “Snapshot integrity”| Variable | Purpose |
|---|---|
MANIFEST_PATHS |
snapshot paths hashed into the HMAC-signed integrity manifest (SB_MANIFEST_PATHS; default /usr /boot — keep /etc//var out, they mutate at runtime) |
VERIFY_ON_ROLLBACK |
what a failed manifest check does to a rollback: warn (default, proceed loudly), strict (refuse; try older snapshots first), off (SB_VERIFY_ON_ROLLBACK) |
Packages & repos
Section titled “Packages & repos”| Variable | Purpose |
|---|---|
PKGS_BASE |
array of packages pacstrapped onto a systemd target |
PKGS_BASE_OPENRC |
base packages for an openrc target (Artix-style; includes openrc elogind-openrc) |
PKGS_BASE_DINIT |
base packages for a dinit target (Artix-style; includes dinit elogind-dinit) |
PKGS_ISO |
array of extra packages added to the live ISO so the installer can run |
EXTRA_REPOS |
array of complete pacman.conf section blocks appended to the target (networked installs only — see below) |
ARTIX_REPOS |
repo names ([system] [world] [galaxy] [lib32]) written into a non-systemd target’s pacman.conf |
ARTIX_MIRRORS |
mirror URLs (with $repo/$arch placeholders) rendered into a non-systemd target’s /etc/pacman.d/mirrorlist |
What gets rendered where
Section titled “What gets rendered where”| You set… | …and it lands in |
|---|---|
DISTRO_ID |
the ISO file name (<id>-YYYY.MM.DD-x86_64.iso) and the Docker image tag |
BIN_PREFIX |
/usr/bin/<prefix>-update and /usr/bin/<prefix>-install; the baked engine defaults; file_permissions in the ISO profile |
UNIT_PREFIX |
the three unit files + the *-mark-good.sh/*-rollback.sh/*-boot-check.sh scripts; the enable symlink; the watchdog drop-in; the OpenRC/dinit service names |
INIT_SYSTEM |
which health-check integration the installer enables on the target (systemd units, an OpenRC init script, or a dinit service) |
LIB_DIR |
where the engine finds its helpers/scripts (SB_LIB_DIR baked in) |
DISTRO_NAME |
systemd-boot/GRUB entry titles; unit Description=; os-release NAME/PRETTY_NAME |
DISTRO_* URLs/ids |
/etc/os-release (rendered from config/os-release.in) and unit Documentation= |
HOSTNAME / TIMEZONE / LOCALE / KEYMAP |
the installed /etc/hostname, /etc/localtime, /etc/locale.conf, /etc/vconsole.conf |
FS_LABEL / ESP_LABEL |
the Btrfs/ESP labels and the GPT partition name |
PKGS_ISO |
packages.x86_64 in the archiso profile |
PKGS_BASE |
the pacstrap package list of both installers for systemd targets (the interactive one adds the user’s init/microcode/firmware/network/guest-agent/sudo choices on top) |
PKGS_BASE_OPENRC / PKGS_BASE_DINIT |
the pacstrap package list for openrc/dinit targets |
EXTRA_REPOS |
the target’s /etc/pacman.conf (appended after the Artix sections on non-systemd targets) |
ARTIX_REPOS / ARTIX_MIRRORS |
the target’s /etc/pacman.conf repo sections + /etc/pacman.d/mirrorlist on openrc/dinit targets |
What deliberately stays generic: the whole src/ tree on disk, the upstream unit tests,
tools/verify-units.sh, the uppercase SILVERBLUE-* progress markers (the QEMU harness greps
them), and the test-only silverblue-autoinstall.sh / synthetic [silverblue-local] repo.
The installer library and interactive frontend are installed verbatim (they read your
distro.conf at runtime); only the frontend’s file name is derived from BIN_PREFIX.
Branding
Section titled “Branding”- Set
DISTRO_NAMEfor the pretty name shown in the boot menu,systemctl status, and/etc/os-release. - Tweak
config/os-release.indirectly if you want extra fields (e.g. a differentLOGOorANSI_COLOR). It’s a template with@TOKEN@placeholders that the autoinstaller fills in from the config.
Packages & repos
Section titled “Packages & repos”- Edit
PKGS_BASEto change what’s installed on a systemd target (addlinux-firmwarefor real hardware; add a desktop, NetworkManager, etc.). - Edit
PKGS_BASE_OPENRC/PKGS_BASE_DINITif your openrc/dinit targets need a different Artix-style base (rememberelogind-<init>, which providesbase’sinit-logind). - Edit
PKGS_ISOif your installer needs more tools in the live environment. - Add
EXTRA_REPOSto point the installed system at your own pacman repo, e.g.:Caveat:Terminal window EXTRA_REPOS=($'[mydistro]\nSigLevel = Optional TrustAll\nServer = https://repo.mydistro.org/$arch')EXTRA_REPOSis applied by the interactive installer (always networked) and by the unattended test install’s networked path (net=1). The offline/hermetic test path leaves the target with just the bundledfile://repo so the self-contained update test still works.
Build & verify
Section titled “Build & verify”# 1. Sanity-check the engine/units (independent of your config; must stay green):make test
# 2. Build your ISO:make build-iso # -> iso/output/*.iso
# 3. (optional) Boot + install + update + rollback in QEMU:make test-qemuAfter make build-iso, confirm your rename took (replace mydistro with your BIN_PREFIX):
# Inspect the assembled airootfs staging (or extract the squashfs from the ISO):grep -R "mydistro-update" iso/output/ 2>/dev/null # or look in the build profile
# Inside the image you should find:# /usr/bin/mydistro-update# /usr/lib/mydistro/{sdboot-helpers.sh,grub-helpers.sh,mydistro-mark-good.sh,mydistro-rollback.sh}# /usr/lib/systemd/system/mydistro-mark-good.service (ExecStart=/usr/lib/mydistro/...,# OnFailure=mydistro-rollback.target)# the baked engine header: SB_DISTRO_NAME, SB_LIB_DIR, SB_VERIFY_UNIT set to your valuesOn the installed target, cat /etc/os-release shows your ID/PRETTY_NAME, hostnamectl
shows your hostname, the boot menu title shows your DISTRO_NAME, and mydistro-update --dry-run
runs.
Constraints & gotchas
Section titled “Constraints & gotchas”- Keep the source defaults if you reuse the upstream unit tests. The bats tests pin the
defaults (
Arch Silverblue,sort-key silverblue, ESP path/silverblue/...). They test the source tree, which is never rewritten, so they keep passing regardless of your config. If you rename and want a greenmake test-qemu, pointtests/qemu/harness.py/tests/qemu/run.shat yourBIN_PREFIX-named binary and units (the harness uses the literal names). - sed-safe values.
DISTRO_NAMEand the URLs are substituted withsed; the build escapes&,|, and\, but avoid embedding a literal|inDISTRO_NAMEto be safe. ESP_SUBDIRmust agree everywhere. Both the engine (baked) and the autoinstaller read it from this one config, so they stay in lockstep — don’t hand-edit it in only one place.EFI_DIRis wired into the engine but the autoinstaller assumes/efi; leave it unless you’re also willing to adjust the autoinstaller’sgrub-install/mount paths.
FAQ / troubleshooting
Section titled “FAQ / troubleshooting”mark-good.servicefails to start / “permission denied”. The executables must be 0755 in the ISO.iso/build.shinjectsfile_permissionsentries for your renamed paths; if you changed the injection, double-check those entries matchBIN_PREFIX/LIB_DIR/UNIT_PREFIX./etc/os-releasedidn’t change. Arch ships/etc/os-releaseas a symlink to/usr/lib/os-release; the autoinstaller writes a regular file that overrides it. If you overrideOSRELEASE_IN, make sure the template still has the@TOKEN@placeholders.- Bootloader “not detected” after install. Make sure
BOOTLOADERissystemd-bootorgrub, and thatESP_SUBDIRis consistent (see above). - Build uses the wrong image tag.
IMAGEin theMakefileis derived fromDISTRO_ID; if the config can’t be sourced it falls back toarch-silverblue-iso.
Acknowledgments
Section titled “Acknowledgments”Multi-init support, brought to you by @c-ludenberg, creator of Antergos-NeXT — the guy who saw the previous multi-init attempt and said “hold my beer”. Full-screen TUI, Artix repo swap for openrc/dinit, guest-agent detection, per-init services. All of it actually works. You’re welcome.