Skip to content

Derive Your Own Distro

Arch Silverblue is built so you can fork it into your own atomic Arch-based distro by editing one fileconfig/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.

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:

  1. The source tree is never rewritten. src/ keeps its silverblue defaults so the unit tests keep working. Branding in the engine is just env-var indirection whose defaults equal today’s values.
  2. Renaming/branding happens at build & install time. iso/build.sh reads config/distro.conf and 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)

Same as upstream (see README.md):

  • make test (lint + unit tests + unit verification): just needs bash; shellcheck/bats are pulled on demand via nix shell.
  • make build-iso: Docker with --privileged and network (all Arch tooling runs in the container).
  • make test-qemu: QEMU.

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

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_DINIT to whatever those targets should boot into — the default Artix sets are base <init> elogind-<init> linux mkinitcpio btrfs-progs dosfstools gptfdisk efibootmgr. The systemd PKGS_BASE is 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.sh for the per-init health semantics). On OpenRC/dinit the *-boot-check.sh wrapper self-manages the 120s timeout and failure→rollback dispatch, since those inits have no OnFailure= equivalent.
  • The systemd watchdog drop-in has no OpenRC/dinit analog: hang protection on those inits comes from systemd-boot boot-counting / GRUB recordfail only (optionally add a watchdogd to your package set).
  • enable_network_stack is init-aware: networkd stays systemd-only, but NetworkManager/dhcpcd enable their service on all three inits (systemctl / rc-update add … default / a boot.d symlink), 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 (plus open-vm-tools, virtualbox-guest-utils on 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).
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)
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)
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
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.

  • Set DISTRO_NAME for the pretty name shown in the boot menu, systemctl status, and /etc/os-release.
  • Tweak config/os-release.in directly if you want extra fields (e.g. a different LOGO or ANSI_COLOR). It’s a template with @TOKEN@ placeholders that the autoinstaller fills in from the config.
  • Edit PKGS_BASE to change what’s installed on a systemd target (add linux-firmware for real hardware; add a desktop, NetworkManager, etc.).
  • Edit PKGS_BASE_OPENRC/PKGS_BASE_DINIT if your openrc/dinit targets need a different Artix-style base (remember elogind-<init>, which provides base’s init-logind).
  • Edit PKGS_ISO if your installer needs more tools in the live environment.
  • Add EXTRA_REPOS to point the installed system at your own pacman repo, e.g.:
    Terminal window
    EXTRA_REPOS=(
    $'[mydistro]\nSigLevel = Optional TrustAll\nServer = https://repo.mydistro.org/$arch'
    )
    Caveat: EXTRA_REPOS is 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 bundled file:// repo so the self-contained update test still works.
Terminal window
# 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-qemu

After make build-iso, confirm your rename took (replace mydistro with your BIN_PREFIX):

Terminal window
# 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 values

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

  • 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 green make test-qemu, point tests/qemu/harness.py / tests/qemu/run.sh at your BIN_PREFIX-named binary and units (the harness uses the literal names).
  • sed-safe values. DISTRO_NAME and the URLs are substituted with sed; the build escapes &, |, and \, but avoid embedding a literal | in DISTRO_NAME to be safe.
  • ESP_SUBDIR must 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_DIR is wired into the engine but the autoinstaller assumes /efi; leave it unless you’re also willing to adjust the autoinstaller’s grub-install/mount paths.
  • mark-good.service fails to start / “permission denied”. The executables must be 0755 in the ISO. iso/build.sh injects file_permissions entries for your renamed paths; if you changed the injection, double-check those entries match BIN_PREFIX/LIB_DIR/UNIT_PREFIX.
  • /etc/os-release didn’t change. Arch ships /etc/os-release as a symlink to /usr/lib/os-release; the autoinstaller writes a regular file that overrides it. If you override OSRELEASE_IN, make sure the template still has the @TOKEN@ placeholders.
  • Bootloader “not detected” after install. Make sure BOOTLOADER is systemd-boot or grub, and that ESP_SUBDIR is consistent (see above).
  • Build uses the wrong image tag. IMAGE in the Makefile is derived from DISTRO_ID; if the config can’t be sourced it falls back to arch-silverblue-iso.

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.