Hacking on it
Cutting an ISO Release
The whole pipeline lives in archiso/. CI (.github/workflows/build.yml) only
compile-checks — it does not build or publish ISOs. Releases are cut by hand.
The steps
1. Pre-flight the tree. One script now answers what used to be five manual checks:
tools/preflight.sh # working tree + index, against HEAD
tools/preflight.sh --staged # only what `git commit` would take
tools/preflight.sh --self-test # prove the checks can still fail
It refuses a commit that cannot ship, and every finding is a fact about the repo rather than a style opinion:
| Check | The bug it catches |
|---|---|
| pkgrel | a source edit with no pkgrel bump. pacman compares pkgver-pkgrel, so nothing rebuilds and nothing installs, however different the file is — the edit is committed, pushed, applied, and absent |
| lists | a component in KNOWN= with no build rule, or in one of the six lists and not the others |
| iso / installer | a package built into the ISO's local repo but never installed onto the image, or on the image and delivered to no installed system |
| pkgver | a pkgver composed from another variable, which a scraper reads back literally and vercmp then ranks above every real version |
| order | a build order that does not respect an inter-component dependency — it only fails on a fresh host |
Run --self-test if a finding surprises you: a matcher that matches nothing
prints ok, so a broken check and a clean tree look identical.
Still worth an eye by hand: untracked source files, since git commit -a can
push a tree that does not build.
git status --short --untracked=all -- synui synapd synguard synsh synnet synapse_kmod
2. Bump the version. iso_version in archiso/profiledef.sh is what the
ISO's filename is built from — but the version is also written into the boot
menu text in five more files, and nothing checks that they agree. Miss one and
the image boots to a menu naming the previous release:
cd archiso
sed -i 's/0\.2\.9\.4/0.2.9.5/g' profiledef.sh grub/grub.cfg grub/loopback.cfg \
syslinux/archiso_head.cfg syslinux/archiso_pxe-linux.cfg syslinux/archiso_sys-linux.cfg
grep -rn '0\.2\.9\.4' --include='*.sh' --include='*.cfg' . | grep -v '^./work/'
The grep should print nothing. (work/ is the previous build's tree and is
regenerated.)
Do not touch SYNAPSEOS_VERSION in build.sh; see
the version model.
Bump the pkgrel of any component whose code changed.
3. Build (~20–30 min: compiles llama.cpp, downloads a ~4 GB GGUF, runs mkarchiso):
sudo ./archiso/build.sh --no-clean
The ISO is signed by default. Which key is named by
archiso/release-key.fingerprint, so there is nothing to type and nothing to
remember; the key's passphrase is prompted for near the end, so this is not
unattended. build.sh proves the key is usable in its preflight, not at the
end — a host without the key stops in the first ten seconds rather than after
half an hour of building.
Use --no-sign for a throwaway image. And if an ISO ends up unsigned anyway,
that never costs a rebuild — the signature is detached, so the image is
already the right one:
./archiso/sign-iso.sh 0.3.0 # or no argument for the newest in out/
See Signing below for the key itself.
publish-release.sh warns about an unsigned release rather than refusing.
| Flag | Effect |
|---|---|
| (none) | CPU-only llama.cpp — this is the release default |
--gpu=vulkan |
AMD/Intel; portable, safe to ship broadly (needs glslc) |
--gpu=cuda |
NVIDIA; will not start on a machine without the driver |
--no-gpu |
Same as the default; kept for compatibility |
--with-model |
embed a ~4.1 GB gguf. Off by default since 0.2.8 — a release ships no model |
--no-model |
the default; kept for compatibility, does nothing |
--llama-only |
build and stage llama.cpp, then stop — no packages, no ISO |
--no-clean |
reuse the previous llama.cpp build — safe, and saves the most time |
--no-sign |
do not sign — signing is the default; use for a throwaway image |
--sign |
the default; kept so old invocations still work |
Output: archiso/out/SynapseOS-<ver>-x86_64.iso.
The ISO ships a CPU llama build on purpose, and the default flags give you that. A CUDA or ROCm build links the shipped
libggmlagainst the build host's driver stack, sosynapdexits 127 on any machine without it — including every VM.--gpu=vulkanis the exception: it links only the Vulkan loader and runs on any AMD/Intel card.Regardless of the ISO's own backend, the build also packages
synapse-llama-cudainto the repo ifnvccis on the host, andsynapse-llama-vulkanifglslcis — so an installed machine can switch onto its GPU later. Those go in the repo, never intopackages.x86_64.
4. Verify the right packages actually made it in. This is the step that catches a silently stale build:
grep '^synui ' archiso/work/iso/arch/pkglist.x86_64.txt # → synui 0.1.0-<pkgrel>
If that pkgrel isn't the one you just bumped, the ISO is stale — do not ship it.
5. Publish. GitHub caps release assets at 2 GiB, so the ISO is split:
./archiso/publish-release.sh <ver>
This splits the image into 1900 MiB .part* files and runs gh release create
v<ver>. Recent ISOs are ~6.5 GB → 4 parts.
It uploads <iso>.asc alongside them when the build was signed, and verifies
that signature before publishing — a signature that does not check out stops
the release rather than shipping. With no .asc it prints a loud warning and
publishes anyway, because a test build is a legitimate thing to release.
6. Mirror the whole image. The split is GitHub's constraint alone. The download people are pointed at is a single file, and it lives on Cloudflare R2 with the Internet Archive as the mirror:
./archiso/publish-mirrors.sh <ver>
This uploads the ISO and its .sha256, .b2sum and .asc to both, then
checks what each host actually serves — the size, and the hash of the last
mebibyte by range request — before reporting either as published. An upload
tool's exit status says the transfer returned, not that the object is servable:
a bucket whose public access was never switched on, or a custom domain pointing
somewhere else, exits 0 and serves nothing.
Re-running it is cheap. Anything already uploaded at the right size is skipped,
so an interrupted 4.6 GB transfer is resumed rather than restarted, and
--skip-r2 / --skip-ia publish to one mirror alone. The Archive's torrent is
built by its derive queue some time after the upload, so it is announced as
pending rather than printed as a finished link — check it exists before the
download page links it.
Setup for both tools is under Mirrors in archiso/README.md.
7. Update the download page. publish-release.sh generates the release
body with the right links on its own, but the site's download section carries
the version in the URL and is edited by hand — it is the one place per release
that is. The wiki points at the site rather than at a versioned file, so it does
not need touching.
Signing
Releases from 0.2.9.5 carry a detached GPG signature. The checksum published in the same release proves the download is not corrupt and nothing more: whatever could alter the ISO could alter the checksum beside it. The signature is the part that says who built it, and the public key is published on soslinux.org rather than in the release, so the two reach people by different roads.
The signing key is a dedicated release key — not a personal identity, so it can be rotated or revoked without touching anything else, and a compromised build host costs the release key rather than your name.
gpg --quick-generate-key "SynapseOS Release Signing <releases@soslinux.org>" \
ed25519 sign 3y
gpg --list-secret-keys --keyid-format=long # the fingerprint
gpg --armor --export <fingerprint> > synapseos-release-key.asc
Give it a passphrase; keep the revocation certificate gpg writes to
~/.gnupg/openpgp-revocs.d/ somewhere that is not the build machine. The
exported public half belongs in the website repo as
public/synapseos-release-key.asc, with its fingerprint on the download page.
The fingerprint goes in archiso/release-key.fingerprint, which is committed:
a fingerprint is public, and the file is what lets the key be named without
anyone remembering to name it. SYNAPSE_SIGNING_KEY=<fpr> sudo -E ./archiso/build.sh
still overrides it for a build that is not signing with the project key.
Signing lives in one script, archiso/sign-iso.sh — build.sh calls it, and you
run it directly to sign an image built without a signature:
./archiso/sign-iso.sh # newest ISO in archiso/out/
./archiso/sign-iso.sh 0.3.0 # by version
./archiso/sign-iso.sh --check-key # prove the key is usable; sign nothing
It signs as the invoking user rather than as root — the build runs under sudo
and root's keyring holds no release key — and names the key explicitly, because
gpg --detach-sign with no -u signs with whatever secret key comes first. It
verifies the signature it just wrote, against the file that will actually ship;
publish-release.sh verifies again before uploading, and refuses an .asc that
does not check out.
0.3.0 shipped unsigned, which is why signing is now the default rather than a
flag: --sign and sudo -E and an environment variable were three things to
remember at the end of a long build, and remembering is not a mechanism.
Reproducible builds
A build of one commit is stamped with that commit's time, not the clock:
tools/source-date-epoch.sh gives SOURCE_DATE_EPOCH — the component's last
commit for a package, HEAD for the image — and build.sh and build-all.sh
both use it. Each package is built at the same path every time, because
.BUILDINFO records the build directory.
Check the packages — builds each component twice from a clean clone of HEAD and compares them file by file:
tools/repro-check.sh --all --skip synapse-llama
tools/repro-check.sh synui synguard # or just these
Check an image — build the same commit twice, keeping the first ISO aside, then:
tools/iso-repro-diff.sh first.iso second.iso
It prints every differing path under the cause that explains it, and anything unexplained on its own. Exit 0 means identical, 1 means only listed causes, 2 means something unexplained. Neither script needs root.
--no-clean is safe (it wasn't always)
Two stale-artifact bugs used to make builds silently "succeed" on old output.
Both are fixed (3b6b747), but they're worth knowing because they're the shape
of failure to expect here:
-
--no-cleanused to preserve mkarchiso'swork/too, not just the expensivebuild/(llama.cpp). mkarchiso's per-stage stamp files then made it skip straight to "Done!" in about two minutes — reusing the previous ISO and its old pacstrapped packages, including an unbumped synui. A false-positive exit 0. Nowbuild.shalways wipeswork/and gates onlybuild/on--clean. -
Git-sourced packages left a stale
src/<pkg>that broke the next build. See the src collision hazard.
Two earlier ones (ec361d2, bf5d733): create_source_tarball omitted data/
so synui's install_data(data/wallpaper.png) failed; and build.sh chowned the
ISO files but not the out/ directory (mkarchiso creates it root-owned), so
publish-release.sh — which runs as your user — couldn't write the split parts.
The collector drift, and why it is now deduped
create_source_tarball existed in two places — build-all.sh and
archiso/build.sh — and every single fix to it landed in one and not the other.
Four times out of four. The symptom was always the same shape: a file missing
from the tarball, a build that fails later and somewhere else, and a fix that
"was already applied".
Both collectors now delegate to the component's own <pkg>/mktarball.sh when
one exists (8629c32), and build.sh treats a failing mktarball.sh as fatal
rather than falling through to a stale tarball. When you add a package that needs
particular files, write it a mktarball.sh — do not teach the two collectors
about it.
Testing without hardware
QEMU_RAM=8G ./archiso/build_scripts/qemu-test.sh # auto-detects the newest ISO
Uses KVM when available, boots UEFI via OVMF (falling back to BIOS), and attaches a persistent 20 GB test disk. Give it 8 GB+ of RAM when the model is embedded. Kernel and boot output are mirrored to the serial console — View → serial0 in the QEMU window.
The default CPU build is already the right one for QEMU — and for release.
Release history
| Version | Date | Notes |
|---|---|---|
| (unreleased) | — | wlroots 0.20 / scenefx 0.5 port, glass halo + shadow_spread + CSD clipping, HDR10 output, desktop icon persistence, event sounds, the widget manager. Effects off by default |
| 0.1.9 | 2026-07-24 | The desktop release: quickshell bar replaces waybar, start button and menu move into it, desktop widgets, cursor theme manager, six riced themes, LUKS2 full-disk encryption + syn-crypt, per-app window geometry |
| 0.1.8 | 2026-07-19 | AMD/Vulkan + glass. Portable Vulkan llama.cpp backend, dual-boot install, scenefx glass migration, theme manager |
| 0.1.7 | 2026-07-18 | Greeter, clock/calendar panel, Software Manager, Samsung M2020W driver |
| 0.1.6 | 2026-07-17 | Native lock screen, night light, clipboard history, screen recording, laptop support, Bluetooth, CUPS, Alt+Tab MRU |
| 0.1.5 | 2026-07-13 | Titlebars with min/max/close, drag-to-edge snapping, window animations, the synctl control socket, virtual desktops |
| 0.1.4 | 2026-07-12 | Task manager, control panel, tray, game mode, worm/keylogger detection, Secure Boot enroll fix |
| 0.1.3 | 2026-07-11 | synui pkgrel 59 — control panel (Super+C), Super-tap start menu, waybar tray. 6.5 GB / 4 parts |
| 0.1.2 | 2026-07-10 | synui pkgrel 42 — lock screen (Super+L). 5.7 GB / 4 parts |
| 0.1.1 | 2026-07-08 | First release with the wallpaper selector + dock. 5.5 GB / 3 parts |
| 0.1.0 | — | Initial |
See also: Building and Packaging.