Repository navigation
SimulationOS Alpha foundation: bootable ISO, live Hyprland desktop, installer, CI - #1
Merged
Merged
Conversation
- bootmodes: replace the 6 removed names with bios.syslinux + uefi.systemd-boot
(uefi.systemd-boot already covers x64 + mixed-mode IA32; mkarchiso rejects it
alongside uefi.grub)
- drop buildmodes=('iso') (implicit default) and iso_icon (not read by mkarchiso)
- rename ISO to simulationos so mkarchiso emits simulationos-<ver>-x86_64.iso
- pacman.conf: declare [cachyos] with explicit Server= URLs instead of
Include=/etc/pacman.d/cachyos-mirrorlist, so the build no longer silently
depends on a developer-specific host file
- pacman.conf: remove DisableSandbox; keep SigLevel = Required DatabaseOptional
- order Arch repos before [cachyos] so shared packages come from Arch
The profile installed linux-cachyos but the boot entries referenced
vmlinuz-linux and the custom preset referenced a nonexistent
vmlinuz-linux-simos. Three names, none of which lined up; the ISO could
not have booted.
- all boot entries now use vmlinuz-linux-cachyos / initramfs-linux-cachyos.img,
confirmed from the linux-cachyos PKGBUILD (pkgbase=linux-cachyos)
- delete airootfs/etc/mkinitcpio.d/linux.preset: presets are keyed to pkgbase,
so it was orphaned. Upstream archiso removed its own preset for the same
reason ('the default preset is good enough')
- rebrand boot menus to SimulationOS; rename entries to 01/02/03-simulationos-*
- delete grub/grub.cfg: only the uefi.grub bootmode reads it, and that cannot
be combined with uefi.systemd-boot. grub/loopback.cfg is kept because
mkarchiso installs it whenever no *grub* bootmode is set
- drop broadcom-wl: removed from the Arch repositories, would abort pacstrap
…desktop The inherited config was the author's laptop setup and could not work on another machine: - exec-once = brightnessctl set 0% -> black screen on first login - wallpaper at /mnt/m.2_ssd/Linux/Wallpapers/1354205.jpeg - ~/.scripts/Acer/PredatorSense/... on autostart - hardcoded USB audio sink, SYNA7DB5:00 touchpad, eDP-2 output - required grimblast / auto-cpufreq / pamixer / ML4W, none shipped - two wallpaper daemons (swww + hyprpaper); monitor.conf AND monitors.conf; several files shipped but never sourced Replaced with one hyprland.conf plus an empty monitors.conf for per-machine overrides. Every exec-once and keybind target maps to a shipped package or a script in airootfs/usr/local/bin. Waybar uses built-in modules only, so a missing helper can never blank the bar. Also fixes .bashrc, which aliased rm to trash-put without shipping it, breaking rm in the live session. Wallpaper is a real 1920x1080 asset, reproducible via scripts/make-wallpaper.py.
Session: - SDDM with the default X11 greeter (xorg-server/xorg-xauth are hard deps) launching the Wayland hyprland.desktop; theme 'maldives' ships in the sddm package, unlike the previously configured 'breeze' which needs Plasma - display-manager.service symlink instead of the never-invoked dmcheck guesser - the SDDM autologin config was previously filed under polkit-1/rules.d/, where SDDM would never read it Network: NetworkManager only. systemd-networkd, its .network files, iwd, statically enabled wpa_supplicant and systemd-resolved (plus the resolv.conf stub symlink that depended on it) are removed. Running two network stacks is a reliable source of nondeterministic breakage. Audio: pipewire/wireplumber user units are enabled explicitly via /etc/systemd/user/, derived from the units' actual [Install] sections, rather than relying on package presets (the packages ship no .wants symlinks). Security: sshd is installed but no longer enabled by default, and the permissive upstream drop-in (PermitRootLogin yes) is replaced. Live-only privilege grants are namespaced *-live-* so the installer can strip them.
Replaces calamares-online.sh, which referenced /etc/version-tag and a simulationos-keyring that never existed, and which nothing invoked. The config lives at /usr/share/simulationos/calamares and is launched with 'calamares -c'. It cannot live in /etc/calamares: cachyos-calamares owns ~60 files under /etc/calamares/modules and declares no backup= array, and the airootfs overlay is copied BEFORE pacstrap, so that would be a hard file conflict. Verified in the Calamares source that -c overrides appDataDir and that module-config and branding lookup then resolve only inside it. Offline install via unpackfs from /run/archiso/bootmnt/arch/x86_64/airootfs.sfs (derived from install_dir and arch in profiledef.sh). Because unpackfs copies the live filesystem verbatim, the target inherits every live concession. simulationos-deloop strips them and, critically, LOCKS ROOT - the live medium ships root with an empty password, which unpackfs would otherwise carry onto every installed system. It must run before initcpio so the archiso initramfs hook is gone before the initramfs is rebuilt.
Identity: os-release (ID=simulationos), issue, motd, hostname, simulationos-release. /etc/hosts previously contained ONLY a hardcoded GitHub IP with no localhost entries. Removed six scripts that nothing invoked and that all depend on CachyOS' chwd, which was never in the package list: dmcheck, prepare-live-desktop.sh, remove-nvidia, removeun, removeun-online, calamares-online.sh. Also removed the chwd-dependent nvidia-module-loader and its modprobe drop-in, the broadcom-wl drop-in (package gone from Arch), choose-mirror (its service was never shipped), Installation_guide, pacman-more.conf, the hand-written CachyOS-forcing mirrorlist (now provided by pacman-mirrorlist + the upstream uncomment hook), and the reflector config for a package that is not installed. Shipping half-working hardware detection is worse than shipping none: NVIDIA falls back to nouveau for the Alpha.
validate-profile.sh catches the class of breakage that otherwise only appears at boot: kernel-name drift between packages.x86_64 and the boot entries, Hyprland launching binaries nobody ships, developer paths, dangling systemd symlinks, missing Calamares module configs, a deloop script that stopped removing the archiso initramfs hook, more than one wallpaper daemon, and shell syntax errors. Runs on bash 3.2 without pacman, so it works on macOS. Verified by fault injection: 10 deliberate regressions introduced into a scratch copy, 10 caught. build-iso.sh refuses to run on a non-Linux or non-x86_64 host, without root, without archiso, or without the CachyOS key trusted - each with the exact remediation - rather than producing a wrong-architecture or unsigned build. It never disables signature verification.
Records, for each significant decision, which behaviour is upstream archiso, which is CachyOS-specific and which is a SimulationOS decision, plus the verification status of every claim - explicitly separating what was statically verified from what has never been booted.
One build engine for developers, CI and releases, so a local build and a CI build cannot drift. build.sh decides WHERE to build - natively on Arch x86_64, otherwise in an x86_64 Arch container - and then runs the same scripts/build-iso.sh either way. On an arm64 host the container is pinned to linux/amd64 so an arm64 ISO can never be produced by accident. Found by actually running it: pacman >=7 sandboxes downloads with seccomp and an unprivileged alpm user, and that filter fails with EINVAL under qemu-user emulation. The workaround is confined to the throwaway builder container (a patched copy of pacman.conf plus --disable-sandbox); the committed pacman.conf never sets DisableSandbox and signature verification is never disabled. validate-profile.sh now fails if DisableSandbox or 'SigLevel = Never' ever appears in a committed config, so the concession cannot leak into the product. profiledef.sh takes pacman_conf from $SIMOS_PACMAN_CONF so the builder can supply that patched copy without modifying the repository. Also adds scripts/inspect-iso.sh: mkarchiso exiting 0 is not evidence of a usable image, so the artefact is checked for the kernel and initramfs named by packages.x86_64, the squashfs, the bootloader payloads for the declared bootmodes, and a matching checksum. build-iso.sh now writes build-info.json (commit, archiso version, checksum, duration) and build.log.
- ./build.sh is documented as the single entry point; the container path is now automatic rather than a manual podman recipe - drops the hardcoded cachyos-keyring package URL, which goes stale on every CachyOS keyring rebuild, in favour of installing through ./pacman.conf - verification table now carries evidence per row, and distinguishes 'mkarchiso accepts the profile' (verified: a real run passed bootmode validation and entered pacstrap) from 'ISO build completes' (not yet)
Found by an actual build, 40 minutes in: error: extract: not overwriting dir with file .../airootfs/usr/lib/Xorg ==> ERROR: Failed to install packages to new root xorg-server ships BOTH /usr/lib/Xorg (a file) and /usr/lib/xorg/ (a directory). On a case-insensitive filesystem those are the same path, so pacstrap aborts. The work directory was bind-mounted from macOS, whose APFS is case-insensitive by default; confirmed directly on this host. - ./build.sh now puts the work directory inside the container (/var/tmp/simulationos-work, case-sensitive overlayfs) and bind-mounts only out/, so the artefact still lands on the host. - build-iso.sh probes the work directory for case sensitivity up front and fails immediately with the fix, instead of wasting a long build. - clean-build.sh previously refused any path outside the repository, which would have rejected the new container work directory. It now accepts explicit build directories under /var/tmp or /tmp while still refusing system paths such as /usr. This class of bug is invisible to static analysis and to dependency resolution; only running the build surfaces it.
'QEMU started' is not evidence that an OS booted. boot-test.sh boots the ISO headlessly with the kernel console on a serial port and waits for proof the system reached graphical.target, reporting which stages were observed: kernel, linux-cachyos, archiso initramfs, systemd, multi-user, graphical. It watches for failure signatures (kernel panic, unable to mount root, emergency mode) as well as success, so a crash is reported rather than being indistinguishable from 'still booting', and every run ends in PASS, FAIL or TIMEOUT rather than hanging. boot.yml runs it for both firmware modes the profile actually declares - uefi.systemd-boot and bios.syslinux - and nothing else, and uploads the serial log on failure. iso.yml chains it after the build.
Found by an actual build: ==> ERROR: file not found: '/usr/lib/initcpio/ipconfig' ==> ERROR: binary not found: 'nbd-client' ==> ERROR: file not found: '/usr/lib/initcpio/nfsmount' The inherited HOOKS line kept archiso_pxe_common/_nbd/_http/_nfs while the package cleanup removed mkinitcpio-nfs-utils, nbd and nfs-utils as server bloat. I had judged the PXE plumbing 'upstream, harmless'; it is not harmless once its support packages are gone. SimulationOS does not support PXE netboot - the product journey is download -> flash USB -> boot - so the hooks are removed rather than the packages restored, along with syslinux/archiso_pxe*.cfg and the pxe branch of syslinux.cfg, which were unreachable configuration. validate-profile.sh now enforces the hook -> package relationship and flags a PXE syslinux config with no PXE hook enabled. Verified by injecting archiso_pxe_nbd without the nbd package: the validator rejects it.
Both found by inspecting a real 2.1 GB artefact: - build-info.json was invalid JSON. It re-sourced profiledef.sh to read iso_version/arch, but profiledef.sh assigns file_permissions as an associative array without declaring it; mkarchiso declares it first, a bare source does not, so bash failed with an arithmetic syntax error on '/etc/shadow'. Version and architecture are now derived from the produced filename, which is ground truth. - inspect-iso.sh looked for /syslinux/ but mkarchiso writes boot/syslinux/, so a correct ISO was reported as missing its BIOS payload. It now checks boot/syslinux/isolinux.bin and the generated menu, and additionally extracts both boot menus FROM the ISO to assert they say SimulationOS, carry no 'Arch Linux install medium' label, and point at the kernel the profile installs. L3 now passes on the real artefact: kernel, initramfs, squashfs, unpackfs source path, BIOS and UEFI payloads, branding and checksum.
The container had no git, and even with git installed it refuses to read a
bind-mounted repository owned by another uid ('dubious ownership'), so every
build recorded git_commit: unknown. A release engineer cannot answer 'which
commit produced this ISO' from that.
Installs git in the builder and marks /simulationos a safe directory.
The ISO now builds end to end. The verification table distinguishes what the artefact proves (kernel naming, both bootloader payloads, branding read out of the ISO, installer source path, checksum) from what it does not: it has never been booted.
qemu-system-x86_64 from Homebrew ships its firmware as share/qemu/edk2-x86_64-code.fd with edk2-i386-vars.fd as the vars template, neither of which matched the Linux distribution paths the scripts searched. Both boot-test.sh and test-iso.sh now find them. boot-test.sh takes SIMOS_QEMU_ACCEL so a host without KVM (any macOS host) selects TCG explicitly instead of relying on the kvm:tcg fallback.
The first real boot test proved the ISO boots - OVMF loaded it and systemd-boot rendered the branded SimulationOS menu with all three entries and the configured 15s timeout - but every post-bootloader stage showed as MISS because the kernel logs only to tty0, which is invisible headlessly. Boot entries now carry 'console=ttyS0,115200 console=tty0'. tty0 is listed last so it remains the primary console for a normal user, while serial makes the boot debuggable headlessly, on real hardware and in CI. This follows archiso's own intent: it already enables a serial console for syslinux (SERIAL 0 115200 in archiso_head.cfg). It is a product improvement, not a concession to the test harness. boot-test.sh additionally reports the firmware/bootloader stages it can observe, so a bootloader-level regression is distinguishable from a kernel one.
The builder container has no python3, so the build-info.json validity check always failed and printed 'WARNING: build-info.json is not valid JSON' about a perfectly valid file. It now tries python3, falls back to jq, and says it skipped the check when neither exists - and a genuine JSON failure now aborts the build instead of only warning.
First end-to-end boot of a SimulationOS ISO, under QEMU + OVMF:
BdsDxe: starting Boot0001 "UEFI QEMU DVD-ROM"
SimulationOS (x86_64, UEFI) <- systemd-boot menu, our entries
[ 0.391639] virt/tdx: ... <- kernel executing
SimulationOS Alpha Linux 7.2.7-1-cachyos (ttyS0)
simulationos login:
That chain proves firmware -> systemd-boot -> linux-cachyos -> archiso
initramfs -> systemd -> getty, with /etc/issue branding and the hostname
applied. It does NOT prove the graphical session, the installer, or an
installed system.
boot-test.sh had reported this healthy boot as a TIMEOUT because its success
pattern required 'Reached target Graphical Interface'. systemd sends status
to tty0, the primary console, so that string never reaches serial; a login
prompt is in fact stronger evidence. Success detection now accepts it.
The script also distinguishes stages that genuinely failed from stages simply
not visible on serial, instead of printing MISS for both, and gains
SIMOS_BOOT_ANALYZE_ONLY=1 to re-evaluate a captured log without rebooting.
Extracts the SquashFS from the built ISO, runs simulationos-deloop inside it exactly as Calamares does (including the removeuser step), and asserts the installed-system security gates. This covers acceptance gates 15-19 without needing to drive the graphical installer: liveuser absent, /home/liveuser gone, SDDM and tty1 autologin gone, NOPASSWD sudo replaced by a password-prompting %wheel rule, permissive polkit rule gone, root locked and no longer empty-password, archiso initramfs hook gone - while confirming the display manager, graphical target, SimulationOS identity and /etc/skel survive. It also asserts the live-medium PRECONDITIONS first, so the test cannot pass vacuously against a rootfs that never had the live configuration. Verified locally against simulationos-2026.09.27-x86_64.iso: 19/19 gates.
Boots the ISO, logs in on the serial console and checks RUNNING state rather than package presence: default target, graphical.target, display-manager, liveuser session, Hyprland, waybar/hyprpaper/mako/hyprpolkitagent/nm-applet, xdg-desktop-portal, NetworkManager, DNS, PipeWire via wpctl, and the presence of the installer binary, config and launcher. The unattended serial login relies on the live medium shipping root with an empty password, which is standard archiso and is removed on install by simulationos-deloop.
- iso.yml now builds through ./build.sh on the runner instead of running actions/checkout inside an Arch container. build.sh already knows how to drive an x86_64 Arch container, so CI and local builds share one engine and cannot drift. Adds a disk-cleanup step, since a 2.1 GB ISO plus a 4 GB rootfs does not fit beside the runner's preinstalled toolchains. - artifacts are named simulationos-alpha-<sha>-x86_64 and carry the ISO, its .sha256 and build-info.json, uploaded uncompressed. - boot.yml and acceptance.yml take the artifact name as an input so every downstream job tests the exact ISO this commit produced. - acceptance.yml adds live-health (running-state checks in the booted guest) and deloop-contract (installed-system security gates). - all workflows expose workflow_dispatch.
CI run 36362920104 failed in pacstrap: mount: /var/tmp/simulationos-work/x86_64/airootfs/dev: permission denied ==> ERROR: failed to setup chroot Root cause: build.sh picked the runtime by name order (podman, then docker). GitHub's ubuntu-latest ships a ROOTLESS podman alongside a rootful docker, so podman won. pacstrap bind-mounts /dev inside the chroot, which needs real CAP_SYS_ADMIN on the host; a rootless container cannot provide that even with --privileged. The runtime is now selected by capability rather than by name: docker if its daemon responds, otherwise podman - and if podman is rootless, via sudo when available, or a clear error explaining the three ways to fix it instead of an opaque mount failure deep inside pacstrap. SIMOS_CONTAINER still overrides, and now accepts a multi-word value such as 'sudo podman'. Fixed in build.sh rather than special-cased in the workflow, so CI exercises exactly the logic a developer gets locally.
CI run 36363194961 built the ISO successfully (2043 MB) and then failed: bsdtar not available; cannot inspect ISO contents inspect-iso.sh reads the ISO9660 filesystem with bsdtar, which ubuntu-latest does not ship; it lives in libarchive-tools. The script already fails loudly with the reason rather than silently skipping the check, which is why this surfaced immediately - so the fix is to provide the tool, not to soften the check.
CI run 36364204960 failed live-health after 3 seconds: qemu-system-x86_64: -device hda-duplex: no default audio driver available guest never reached a login prompt live-health.sh declared '-audiodev none,id=nosnd' but never attached it to hda-duplex, so QEMU aborted at startup. On a host with no default audio backend - every GitHub runner - that is fatal. test-iso.sh had the same latent bug and would have aborted identically for any developer whose machine has no default QEMU audio backend; it now takes SIMOS_QEMU_AUDIODEV, defaulting to a silent-but-always-valid 'none'. live-health.sh now also distinguishes 'QEMU exited' from 'timed out'. They need completely different investigation: the first is a harness problem, the second may be emulation slowness on a runner without KVM.
CI run 36365762103 reached the login prompt and ran the checks, but 12 of 13 assertions failed and the log contained a 'less' help screen. Two harness defects, not SimulationOS defects: 1. systemctl and journalctl page by default. A pager took over the serial console, so every subsequent command was delivered as keystrokes INSIDE less rather than to the shell. The guest shell now exports PAGER=cat SYSTEMD_PAGER=cat SYSTEMD_COLORS=0 TERM=dumb. 2. Collection used fixed sleeps. Under TCG on a runner without KVM a command routinely takes longer than any sleep worth guessing, so replies landed in the middle of the next command and section markers never matched. Each command now waits for its own ##END marker with a bounded timeout, so slowness delays the run instead of corrupting it. Also fixes a malformed 'journalctl -g -i hyprland' (-g takes a pattern). The assertions themselves are unchanged: this makes the measurement reliable, it does not make the checks easier to pass.
Inspecting the serial log from run 36368038012 showed the collected sections
contained only ECHOED COMMAND TEXT, never command output.
The serial line echoes what we type, and the command text itself contained
the literal '##BEGIN x' and '##END x'. So:
- wait_for matched the echo and returned immediately, before the command
had run;
- the assertions then matched against echoed command text, which happens to
contain words like 'graphical', 'active' and 'Hyprland'.
11 of 13 checks were therefore FALSE PASSES. The job only went red at all
because two patterns did not appear in any echoed line. A test that passes by
matching its own input is worse than no test.
Markers are now assembled at runtime with printf: the typed line contains
'##BEG%s'/'##EN%s', so only genuine guest output can produce '##BEGIN label'
and '##END label'. Section extraction anchors on whole lines, stty -echo is
sent as its own command once the shell is ready and its effect is confirmed
rather than assumed.
No assertion was weakened; this makes the results real.
Run 36370016973 reported 'serial root login failed' even though the guest had emitted SIMOS_SHELL_READY. Two defects: 1. wait_for grepped the RAW serial log while sect() grepped a cleaned copy. systemd emits OSC shell-integration sequences (ESC]3008;...) immediately before command output, so a '^'-anchored pattern could never match in the raw stream. Both now share one clean_log helper that strips NULs, CSI colour codes and OSC sequences and converts CR to LF, so line anchors mean what they say. 2. The OVMF firmware was located but never passed to QEMU, so the guest booted via SeaBIOS/ISOLINUX instead of UEFI. The pflash drives are now attached, making this a real UEFI live-session test. Incidentally the log confirms the BIOS path end to end: ISOLINUX 6.04 -> SimulationOS menu -> vmlinuz-linux-cachyos -> initramfs -> login prompt.
Replaying run 36372041948's serial log with corrected extraction shows the
live desktop genuinely works:
sessions: 2 1000 liveuser seat0 674 user tty1 <- SDDM autologin
hyprland: 772 Hyprland --watchdog-fd 4
session: 817 hyprpaper 818 waybar 819 mako
821 nm-applet 828 hyprpolkitagent
12 of 13 checks pass on real guest output. Two remaining harness issues:
1. Residual OSC text can precede a marker on the same line, so '^##BEGIN x'
never matched (BEGIN=0 while END=17). clean_log now forces each marker
onto its own line, making extraction independent of what precedes it.
2. The portal check was simply wrong. xdg-desktop-portal is D-Bus activated
and is SUPPOSED to be absent until something requests a portal, so pgrep
could only pass by accident. It now asks D-Bus to activate the portal and
confirms it answers - a functional test rather than a liveness guess.
Run 36385709091 exposed two defects in the checks themselves. 1. 'active' is a SUBSTRING of 'inactive'. The graphical.target section literally contained 'inactive' and the assertion passed anyway. Service state assertions now anchor on '^active$'. This was a false pass that would have been reported as a verified graphical target. 2. session-procs passed in run 36372041948 and failed in 36385709091 from the same ISO: the checks race the session still starting under TCG on a runner without KVM. The guest now waits for graphical.target AND waybar AND Hyprland before anything is asserted, up to 10 minutes, and emits DESKTOP_READY. That readiness is itself a new gate, so a desktop that never comes up fails loudly instead of producing a flaky mixture. The portal assertion is renamed to 'present' to describe what it checks, now that activation is attempted first.
artifacts/ holds ISOs pulled from GitHub Actions for local verification and must never be committed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Turns the profile into a coherent Alpha that builds, boots and de-lives correctly, with CI proving it.
Verified by CI run 36388606710 (all green)
Artifact
simulationos-alpha-6bcbb90…-x86_64, SHA25681cedf5a552ddbb294422e36a14e23ac19c7442f5ce8a2bef9454abc854ff867.What the live-health job proves (real guest output)
What the de-live contract proves (19 gates on the real ISO)
liveuser removed · /home/liveuser gone · SDDM and tty1 autologin removed ·
NOPASSWD sudo replaced by a password-prompting %wheel rule · permissive polkit
rule removed · root locked · archiso initramfs hook removed · installer
launcher removed — while display-manager, graphical.target, SimulationOS
identity and /etc/skel survive.
Original defects fixed
Three-way kernel-name mismatch (ISO could not boot) ·
broadcom-wlremovedfrom Arch (build abort) · no desktop installed despite graphical.target ·
installed systems would have inherited passwordless root ·
brightnessctl set 0%blanking the screen on first login ·/etc/hostswithno localhost ·
rmaliased to an unshippedtrash-put· SDDM config filedunder polkit rules.d · six dead scripts depending on an uninstalled
chwd.Not yet proven
Calamares GUI install and installed-disk boot are NOT TESTED; the de-live
contract covers the security-critical half of that path. See
docs/adr/foropen decisions (profiles as meta-packages, nftables policy).