Development
How the os repository builds the image and the installer medium, installs them, and what the result does. To run the commands, see its README. How it is tested is in Testing.
Build
make image (scripts/image.sh) builds in two phases. It
signs with the keys of SIGNING_KEY: by default the development keys that
scripts/keys.sh generates in build/keys/.
dev-secureboot.key and dev-secureboot.crt (named ShapeBit Development) sign
systemd-boot and the UKI; dev-pcr.key signs the UKI’s TPM2 PCR 11 values.
make release-key KEYS_DIR=<directory> generates the release keys once
(release-secureboot.key, release-secureboot.crt, named ShapeBit, and
release-pcr.key), which a build uses with SIGNING_KEY=release; builds never
generate them. The Candidate and Installer
workflows restore them from the RELEASE_SECUREBOOT_KEY and RELEASE_PCR_KEY
secrets, so that installed systems update across builds; the checks of pushes
and pull requests sign with throwaway development keys
(Signing Keys).
- Containerfile builds the system: signed systemd-boot, the
certificate to enroll, systemd-homed login support, the
login screen, the desktop, and Files (Nautilus). The desktop’s files come
from its source tree (
DESKTOP_SRC, by defaultdesktop/), installed by its Makefile, and so do the packages it runs on, which it lists inpackages. dnf installs no weak dependencies (image/rootfs/usr/share/dnf5/libdnf.conf.d), so every package is in the image because a list names it or a named package requires it. Fedora has no Hyprland, so the build installs it from the third-party sdegler/hyprland COPR for now; the repository is not left in the image. The image also carries Flatpak with Flathub’s remote, Distrobox, the speech recognition of dictation (sherpa-onnx), and llama.cpp’sllama-serverfor the assistant, each built or downloaded at a pinned version and checked against its hash. The image ships no AI model: the models are extras, downloaded on the computer when the person chooses. Fedora’s logos are replaced bygeneric-logos, as Fedora’s trademark guidelines ask of systems made from it. - Containerfile.seal adds a signed UKI. Its
command line pins the system’s composefs digest, and it carries signed
PCR 11 values for TPM2 unlock. The digest is computed from the built
image, as
bootc installcomputes it.
The sealed image is saved as build/image.tar.
Installer medium
make installer builds the medium in the same two phases.
Containerfile.installer adds, to the sealed
system (IMAGE), the installer, the
cage kiosk compositor that shows it, and the same sealed system, as an OCI layout, as
the payload; sealing then replaces the system’s UKI with the medium’s own, whose
composefs digest covers all of it. So the Installer workflow builds from the
released os:<version> alone, and no unsealed image is kept anywhere. The
medium’s record, build/installer.img.json, names the system it installs: its
version, which is also the installer’s, and its image digest.
scripts/medium.sh writes the result to
build/installer.img: an EFI System Partition and one unencrypted, compressed
Btrfs filesystem, 7 GiB in all, so that it fits a USB drive sold as 8 GB. The
medium holds no secrets.
Disk
make disk (scripts/disk.sh) installs the way a user does:
it boots the medium in QEMU with an empty disk of DISK_SIZE attached, checks
the medium (tests/medium/), and runs the installer
through installerctl, the command-line client of the same D-Bus interface
that the UI uses. make installer-vm opens the same VM in a window instead.
The installer lays out the disk and installs with bootc’s composefs backend.
| Partition | Content |
|---|---|
esp |
EFI System Partition: systemd-boot, UKIs, Secure Boot keys. |
shapebit |
LUKS2 volume with one Btrfs filesystem, split into subvolumes. |
| Subvolume | Mounted at | Content |
|---|---|---|
@base |
/sysroot |
composefs images and each deployment’s /etc. |
@machine |
/var |
This device’s state. |
@people |
/var/home |
systemd-homed homes (/home links to /var/home). |
@recovery |
not mounted | Reserved for recovery data. |
The LUKS2 partition has the x86-64 root partition type, so systemd finds and unlocks it without machine-specific kernel arguments, which a UKI cannot carry.
Boot and unlock
The chain starts with Fedora’s shim, which Microsoft signed, so a PC keeps its factory Secure Boot keys:
firmware → Fedora's shim → signed systemd-boot → signed UKI → sealed system
Shim trusts our key only after a person enrolls its certificate, once per machine, in shim’s MOK manager. The first boot of the medium stops there; it needs no password. scripts/mok-enroll.sh does the same on the VM, whose firmware starts with Microsoft’s keys. esp-shim places shim on the medium and on the installed disk; updates do not replace shim yet.
The installer gives the LUKS2 volume two key slots:
| Key slot | Purpose |
|---|---|
| TPM2 | Unlocks every normal boot, without a prompt. |
| Recovery key | Shown once by the installer; saved to build/recovery-key by make disk. |
The TPM2 slot is bound to PCR 7 (the Secure Boot policy) and to PCR 11 values signed by the PCR key. The medium and the installed system are signed with the same keys, so the slot that the installer enrolls unlocks the installed system, and a new UKI signed with the same keys unlocks without re-enrollment. If the TPM ever refuses, the console asks for a passphrase, and the recovery key works there. The random key that opens the volume during the installation never leaves memory and is removed before the installer finishes.
The installer marks the machine ID uninitialized, so the first boot generates
it and saves it once that boot completes.
The VM’s EFI variables (build/efivars.fd) and TPM state (build/tpm/)
belong to the machine, which the medium and the installed disk share;
make disk resets them.
Updates
Each build records IMAGE_VERSION in /usr/lib/os-release and in the
image’s org.opencontainers.image.version label: in CI, the time the build
started, in UTC, such as 202610301530; dev otherwise. Installed systems update
from UPDATE_REF, which the installer records:
ghcr.io/shapebit-software/os:stable.
os-update.timer runs os-update
(image/rootfs/usr/libexec/os-update) soon after
boot and every 6 hours, except on metered connections; Settings’ Check
now starts [email protected], which a polkit rule lets the person at
the computer start. It downloads and stages a new build with
bootc upgrade, never restarts, and writes its state for the shell to
/run/os-update/status.json.
To update a VM from a local build, make publish
(scripts/publish.sh) pushes the image to a local
registry container on the host, and the VM switches to it as
DEV_UPDATE_REF, through the host’s 10.0.2.2; bootc switches only to a
build other than the one running. That registry serves plain HTTP, so the VM
must mark it insecure first:
printf '[[registry]]\nlocation = "10.0.2.2:5000"\ninsecure = true\n' \
>/etc/containers/registries.conf.d/50-dev-registry.conf
bootc switch 10.0.2.2:5000/shapebit-os:dev
A staged build boots after a restart; bootc rollback returns to the
previous one. The TPM unlocks the new build
without re-enrollment, because its UKI carries PCR 11 values signed by the
same PCR key, and each deployment’s /etc carries the machine’s own changes.
tests/update/ checks all of this.
Homes
Users are systemd-homed homes. Each home
is a LUKS2-encrypted image, /home/<user>.home in @people, with Btrfs
inside, unlocked by its owner’s password at login. The installer creates the
first owner, an administrator (wheel), directly on the installed disk.
systemd-homework leaves that Btrfs without SELinux labels, which keeps the
login screen out of the home; homed mounts it with defcontext= so it reads
as a home directory
(systemd-homed.service.d).
Boot splash
Plymouth shows the logo and
a spinner from the initramfs on, instead of boot messages
(kargs.d/30-splash.toml);
it also asks for the recovery key when the TPM cannot unlock. The theme,
image/rootfs/usr/share/plymouth/themes/boot,
is Fedora’s spinner theme with the logo. Plymouth shows only messages when
the kernel console includes a serial port, as it does for development, so
plymouth.ignore-serial-consoles tells it to draw on the screen anyway.
Errors still reach the console; Esc shows the messages during boot.
Every step up to the desktop shares the splash’s night color, #1f1a3c, so
none shows a black frame: the login screen and the installer take the screen
from Plymouth, which leaves its last picture until they draw
(sddm.service.d);
the login screen fades in from the color and back to it after signing in;
the text console, briefly shown between them, has it as its background; and
Hyprland paints it until the shell’s wallpaper fades in.
Login screen
SDDM shows the login screen on the first virtual terminal; its greeter runs in Hyprland, the desktop’s compositor, with its own configuration (image/rootfs/usr/lib/sddm/compositor.lua). Weston, SDDM’s usual compositor, shows it without a cursor. The theme, in image/rootfs/usr/share/sddm/themes/login, draws the screen: the date and time, the accessibility and power menus, and the person who signs in with their password. It lists systemd-homed users, uses the Instrument Sans font (SIL Open Font License, in image/rootfs/usr/share/fonts/instrument-sans, since Fedora has no package of it), and draws its backdrop itself.
Signing in starts the desktop session: Hyprland with the ShapeBit shell, run
by UWSM. The login screen offers no other session, since Hyprland’s own
entries are removed; without one it says so. Consoles are on the other
virtual terminals (Ctrl+Alt+F3). The
installer medium masks sddm.service, because the installer takes the screen
there.
To change the theme, run it in a window on a machine with SDDM:
sddm-greeter-qt6 --test-mode --theme image/rootfs/usr/share/sddm/themes/login
Real hardware
Installing ShapeBit is the guide for anyone. A medium built
locally is signed with the development keys, so a machine that installs it
enrolls those instead, and later updates from stable would need the
release keys enrolled too.
-
Build the medium, and write it to a USB drive of at least 8 GB, which erases it; check its name with
lsblkfirst:make installer ENGINE=docker sudo dd if=build/installer.img of=/dev/sdX bs=4M conv=fsync status=progress - Install as the guide says.
-
To run the system checks on it, switch to a console (Ctrl+Alt+F3), log in, connect to the network (
nmcli device wifi connect NAME --askfor Wi-Fi), allow root to log in withbuild/ssh/id_ed25519.pub, and run them from the development machine, with the settings given to the installer:make test-guest GUEST=ADDRESS IMAGE_DIGEST=sha256:... OWNER_NAME=... LOCALE=... KEYBOARD_LAYOUT=... TIME_ZONE=...
If something fails, journalctl -b -u installer-backend.service on the medium
shows the installation steps.
The first owner’s home takes most of the free space, as systemd-homed sizes it by default until the home storage policy exists; that can leave too little room for updates.
Development access
The images and disks contain no credentials. make disk, make vm, and
make test generate an SSH key in build/ssh/ and pass its public key to the
VM as the systemd credential ssh.authorized_keys.root. make disk gives the
installer the first owner, OWNER_NAME (default dev) with the password in
build/owner-password, and the settings LOCALE, KEYBOARD_LAYOUT, and
TIME_ZONE. There is no root password.
Developing on ShapeBit
The immutable host ships with rootless Podman and Distrobox, so you can develop and install tools without touching the base system.
Containers, not sandboxes
Distrobox exists for convenience and compatibility. It is not a security sandbox.
Default container image
New containers start from the Fedora image matching the Fedora major version of your OS base.
What’s next
A dedicated ShapeBit OS development image is planned for a future release.
Not yet implemented
- Trial boots with automatic rollback (systemd’s Automatic Boot Assessment)
and the boot health check. They wait on upstream: bootc’s composefs backend
does not yet add boot counters to its entries (planned through
/etc/kernel/tries), and Fedora 44’s SELinux policy does not yet letsystemd-bless-bootrename entries on the ESP. - NvPCR measurements. The same policy does not let
systemd-tpm2-setupsave its anchor credential to the ESP, so that unit fails on the installer medium. The TPM2 unlock policy (PCR 7 and PCR 11) does not use NvPCRs. - A minimal signed recovery UKI.
- The home storage policy: guaranteed minimums, the machine reserve, and low-space states. systemd-homed’s defaults size homes for now.