Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
650d058
fix(desktop): port the Hyprland session to the Lua config and make st…
haiderbassem-swibit Oct 3, 2026
4a50ac2
feat(branding): SimOS binary logo, fastfetch logo and three wallpapers
haiderbassem-swibit Oct 3, 2026
dde018d
fix(installer): make the Calamares golden path produce a bootable system
haiderbassem-swibit Oct 3, 2026
999db80
fix(build): let pacman's download sandbox reach the compat cache dire…
haiderbassem-swibit Oct 3, 2026
e6ee8ed
test: real graphical installation and installed-disk boot
haiderbassem-swibit Oct 3, 2026
05bdca2
docs: describe the install flow, VM graphics requirement and install …
haiderbassem-swibit Oct 3, 2026
8bddd5b
docs: update the desktop design notes for the Lua config, swaybg and …
haiderbassem-swibit Oct 3, 2026
dae2a5f
fix(test): safe chroot cleanup, in-image qml check, wait for drawn su…
haiderbassem-swibit Oct 3, 2026
85d6056
fix: system-wide fastfetch config; look up the Hyprland instance per …
haiderbassem-swibit Oct 3, 2026
6721df4
ci: run the installation acceptance test, with KVM
haiderbassem-swibit Oct 3, 2026
9983a93
fix(test): make the install harness survive a serial line that repeat…
haiderbassem-swibit Oct 6, 2026
d8f6a5f
fix(test): compress serial transfers; fix the installed-system checks…
haiderbassem-swibit Oct 6, 2026
c6ef761
fix(install): do not wait for network time on the installed system
haiderbassem-swibit Oct 6, 2026
6767fdc
fix(test): wait for the new boot after a reboot; trim the log transfer
haiderbassem-swibit Oct 6, 2026
ab2697c
test: time the installed system's shutdown and report what held it up
haiderbassem-swibit Oct 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/enable-kvm.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
#
# enable-kvm.sh - let the runner user open /dev/kvm.
#
# GitHub-hosted x86_64 Linux runners support nested virtualisation, but the
# device node is not accessible to the unprivileged runner user by default.
# Without this every VM test silently falls back to TCG emulation, which is
# ~10-20x slower and turns timeouts into false failures.
#
# This is a CI-host setting only; it does not change SimulationOS. If the
# runner has no KVM at all the tests still run under TCG (scripts use
# "kvm:tcg"), so this never fails the job.
set -u

if [ ! -e /dev/kvm ]; then
echo "::notice::no /dev/kvm on this runner - VM tests will use TCG emulation (slow)"
exit 0
fi
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm4all.rules >/dev/null
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
sleep 1
ls -l /dev/kvm
if [ -r /dev/kvm ] && [ -w /dev/kvm ]; then
echo "KVM available to $(id -un)"
else
echo "::notice::/dev/kvm exists but is not accessible - VM tests will use TCG emulation (slow)"
fi
93 changes: 76 additions & 17 deletions .github/workflows/acceptance.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
# L4b/L5 - runtime acceptance of the built ISO on an x86_64 runner.
#
# Two jobs, deliberately separate so a live-desktop regression is
# distinguishable from a security-cleanup regression:
# Three jobs, deliberately separate so each regression class is
# distinguishable at a glance:
#
# live-health boots the ISO, logs in on serial and asserts on RUNNING
# state: graphical.target, SDDM, Hyprland, session
# components, NetworkManager, DNS, PipeWire, portals.
# deloop-contract extracts the SquashFS, runs simulationos-deloop exactly as
# Calamares does, and asserts the installed-system security
# gates (liveuser gone, autologin gone, NOPASSWD gone, root
# locked, archiso initramfs hook gone).
# state: graphical.target, SDDM, Hyprland (including its own
# config-error list), session components, NetworkManager,
# DNS, PipeWire, portals, and that the installer can load.
# deloop-contract extracts the SquashFS and checks the image-level
# contracts: Calamares libraries resolve, Hyprland's
# verifier accepts the shipped config, simulationos-deloop
# removes every live concession, and mkinitcpio produces a
# normal (non-archiso) initramfs.
# install the real thing: drives the graphical installer on the live
# desktop, installs to a blank disk (UEFI + GPT + ext4 +
# GRUB), powers off, boots the DISK ALONE twice and asserts
# on the installed system.
#
# Manual runs: `gh workflow run acceptance.yml -f run_id=<iso run id>` tests
# the ISO built by an earlier run, so harness changes do not need a rebuild.
name: acceptance

on:
workflow_dispatch:
inputs:
run_id:
description: Run ID of the iso workflow whose ISO artifact should be tested
type: string
required: true
workflow_call:
inputs:
artifact:
Expand All @@ -23,6 +37,7 @@ on:

permissions:
contents: read
actions: read

jobs:
live-health:
Expand All @@ -31,20 +46,20 @@ jobs:
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- name: Enable KVM
run: ./.github/enable-kvm.sh
- name: Install QEMU and OVMF
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq qemu-system-x86 ovmf
- name: Download the ISO
uses: actions/download-artifact@v4
with:
name: ${{ inputs.artifact }}
pattern: ${{ inputs.artifact || 'simulationos-alpha-*' }}
path: out
- name: KVM availability
run: |
# Hosted runners have no nested virtualisation; TCG is correct but
# slow. This is recorded so a timeout can be attributed correctly.
if [ -e /dev/kvm ]; then echo "KVM available"; else echo "no /dev/kvm -> TCG"; fi
merge-multiple: true
run-id: ${{ inputs.run_id || github.run_id }}
github-token: ${{ github.token }}
- name: Live health checks
env:
SIMOS_LIVE_TIMEOUT: '2100'
Expand All @@ -58,7 +73,7 @@ jobs:
if-no-files-found: ignore

deloop-contract:
name: De-live security contract
name: Image contracts (installer, Hyprland, de-live, initramfs)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
Expand All @@ -70,7 +85,51 @@ jobs:
- name: Download the ISO
uses: actions/download-artifact@v4
with:
name: ${{ inputs.artifact }}
pattern: ${{ inputs.artifact || 'simulationos-alpha-*' }}
path: out
- name: Verify the de-live transformation
merge-multiple: true
run-id: ${{ inputs.run_id || github.run_id }}
github-token: ${{ github.token }}
- name: Verify the image contracts
run: sudo ./scripts/deloop-test.sh

install:
name: Install and boot (UEFI, GPT, ext4, GRUB)
runs-on: ubuntu-latest
timeout-minutes: 150
steps:
- uses: actions/checkout@v4
- name: Enable KVM
run: ./.github/enable-kvm.sh
- name: Install QEMU, OVMF and OCR
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq qemu-system-x86 qemu-utils ovmf tesseract-ocr tesseract-ocr-eng
- name: Download the ISO
uses: actions/download-artifact@v4
with:
pattern: ${{ inputs.artifact || 'simulationos-alpha-*' }}
path: out
merge-multiple: true
run-id: ${{ inputs.run_id || github.run_id }}
github-token: ${{ github.token }}
- name: Install SimulationOS and boot the installed disk
run: ./scripts/install-test.py
- name: Acceptance matrix
if: always()
run: |
if [ -f out/install-test/summary.md ]; then
echo "### Installation acceptance" >> "$GITHUB_STEP_SUMMARY"
cat out/install-test/summary.md >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload installation evidence (screenshots, logs, matrix)
if: always()
uses: actions/upload-artifact@v4
with:
name: installation-test-${{ github.sha }}
path: |
out/install-test/
!out/install-test/*.qcow2
!out/install-test/OVMF_VARS.fd
retention-days: 14
if-no-files-found: warn
7 changes: 2 additions & 5 deletions .github/workflows/boot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,8 @@ jobs:
name: ${{ inputs.artifact }}
path: out

- name: Enable KVM if the runner provides it
run: |
# Hosted runners usually have no nested virtualisation; TCG is the
# fallback and is slow but correct.
[ -e /dev/kvm ] && echo "KVM available" || echo "no /dev/kvm; using TCG"
- name: Enable KVM
run: ./.github/enable-kvm.sh

- name: Boot test
env:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/iso.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ on:

permissions:
contents: read
# acceptance.yml downloads the ISO artifact through the API.
actions: read

jobs:
validate:
Expand Down
33 changes: 31 additions & 2 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ jobs:
shellcheck -S warning build.sh scripts/*.sh \
airootfs/usr/local/bin/simos-* \
airootfs/usr/local/bin/simulationos-install \
airootfs/usr/share/simulationos/calamares/scripts/simulationos-deloop
airootfs/usr/share/simulationos/calamares/scripts/simulationos-deloop \
airootfs/usr/share/simulationos/calamares/scripts/simulationos-verify-target \
.github/enable-kvm.sh

- name: Python tooling compiles
run: python3 -m py_compile scripts/install-test.py scripts/make-branding.py

- name: Installer config is valid YAML
run: |
Expand Down Expand Up @@ -82,4 +87,28 @@ jobs:
if ./scripts/validate-profile.sh >/dev/null 2>&1; then
echo "::error::validator did NOT catch an injected kernel-name regression"; exit 1
fi
echo "validator correctly rejected the injected regression"
echo "validator correctly rejected the injected kernel-name regression"

# Generating the initramfs BEFORE de-living bakes the archiso hooks
# into the installed system. The validator must refuse that order.
git checkout -q -- .
python3 - <<'PY'
import re
p = "airootfs/usr/share/simulationos/calamares/settings.conf"
s = open(p).read()
s = s.replace(" - shellprocess@deloop\n - initcpiocfg\n - initcpio\n",
" - initcpiocfg\n - initcpio\n - shellprocess@deloop\n")
open(p, "w").write(s)
PY
if ./scripts/validate-profile.sh >/dev/null 2>&1; then
echo "::error::validator did NOT catch initramfs-before-deloop ordering"; exit 1
fi
echo "validator correctly rejected initramfs generation before de-living"

# A legacy hyprland.conf means config errors on every login.
git checkout -q -- .
printf 'windowrulev2 = float, class:^(x)$\n' > airootfs/etc/skel/.config/hypr/hyprland.conf
if ./scripts/validate-profile.sh >/dev/null 2>&1; then
echo "::error::validator did NOT catch a legacy hyprland.conf"; exit 1
fi
echo "validator correctly rejected a legacy hyprland.conf"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@
/.cache/
/artifacts/
.DS_Store
# Installer compatibility libraries staged at build time by
# scripts/calamares-compat.sh (never committed: they track upstream packages).
/airootfs/usr/lib/simulationos/calamares-compat/
74 changes: 61 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,14 @@ SimulationOS/
│ ├── validate-profile.sh static consistency checks
│ ├── build-iso.sh environment checks + mkarchiso + checksum
│ ├── clean-build.sh remove work/ and out/
│ ├── calamares-compat.sh build step: make sure the installer can load its libraries
│ ├── inspect-iso.sh L3 artefact inspection
│ ├── test-iso.sh boot it in QEMU (UEFI or BIOS)
│ └── make-wallpaper.py regenerate the wallpaper asset
│ ├── boot-test.sh L4 headless boot (UEFI + BIOS)
│ ├── live-health.sh L4b live desktop health (renders, no config errors, installer loads)
│ ├── deloop-test.sh L4b image contracts (de-live, initramfs, Hyprland verifier)
│ ├── install-test.py L5 real graphical install + boot of the installed disk
│ ├── test-iso.sh boot it in QEMU yourself (UEFI or BIOS)
│ └── make-branding.py regenerate wallpapers, logos and the fastfetch logo
│
├── .github/workflows/
│ ├── validate.yml L1 static validation (seconds, no container)
Expand Down Expand Up @@ -208,8 +213,29 @@ build.log
./scripts/test-iso.sh --boot-disk # boot ONLY that disk, no ISO
```

The last two are the real acceptance test: install to the disk, then boot the
disk with no medium attached.
The last two are the manual acceptance test: install to the disk, then boot
the disk with no medium attached.

**Graphics in a VM.** Hyprland needs a virtual GPU. `test-iso.sh` uses
virtio-gpu (`virtio-vga-gl` with 3D on Linux, plain `virtio-vga` with software
rendering on macOS). With a plain VGA adapter (`-vga std`, or a hypervisor's
"standard VGA") Hyprland starts but has no renderer and the screen stays
black. In VirtualBox/VMware enable 3D acceleration; in virt-manager use
"Virtio" video.

### Automated installation test

```bash
./scripts/install-test.py # needs qemu, OVMF and tesseract
```

Drives the whole journey unattended, the way CachyOS tests its installer
(keyboard and mouse through QEMU, OCR to read the screen): boot the ISO, open
"Install SimulationOS" from the desktop, click through Calamares, install to a
blank disk (UEFI + GPT + ext4 + GRUB), power off, **boot the disk without the
ISO**, log in through SDDM, assert on the running system, reboot, and power
off. Screenshots of every step, logs and the result matrix land in
`out/install-test/`. CI runs this on every push (`acceptance.yml`).

---

Expand All @@ -228,21 +254,35 @@ Firmware → syslinux (BIOS) / systemd-boot (UEFI)
**Installation**

```
Live session → "Install SimulationOS" → Calamares
Live session → "Install SimulationOS" (bar button / application menu)
→ simulationos-install: preflight (install source, libraries, display)
→ pkexec → Calamares
→ partition → mount → unpackfs (copy the live SquashFS)
→ users → networkcfg → services-systemd → packages (drop the installer)
→ removeuser (liveuser) → simulationos-deloop (strip live config)
→ initcpiocfg → initcpio (rebuild a NON-archiso initramfs)
→ grubcfg → bootloader (GRUB) → umount
→ reboot into the installed system
→ machineid → fstab → locale → keyboard → localecfg
→ removeuser (liveuser)
→ simulationos-deloop (strip live config, install the kernel into /boot,
drop the archiso mkinitcpio config, pacman keyring)
→ initcpiocfg → initcpio (a NORMAL, non-archiso initramfs)
→ users → networkcfg → displaymanager → hwclock → services-systemd
→ packages (remove installer-only packages)
→ grubcfg → bootloader (GRUB on the target ESP)
→ simulationos-verify-target (refuse to report success for an unbootable
or still-live system)
→ umount → reboot into the installed system
```

Because the offline installer copies the live filesystem verbatim, the target
initially inherits every live concession. `simulationos-deloop` removes them:
SDDM autologin, tty autologin, NOPASSWD sudo (replaced by a password-prompting
`%wheel` rule), the permissive polkit rule, the archiso initramfs hook,
archiso-only units, `/home/liveuser` — and it **locks the root account**, which
the live medium leaves passwordless.
`%wheel` rule), the permissive polkit rule, the archiso initramfs
configuration, archiso-only units, `/home/liveuser` — and it **locks the root
account**, which the live medium leaves passwordless. It runs *before* the
initramfs is generated; the reverse order would bake the live-medium hooks
into the installed system. It exits non-zero if a security- or boot-critical
step cannot be completed, which fails the installation visibly.

The supported ("golden") path for Alpha is deliberately narrow:
**x86_64, UEFI, GPT, ext4, GRUB, offline install.**

---

Expand All @@ -255,6 +295,14 @@ the live medium leaves passwordless.
SquashFS; there is no online package selection.
- **Bootloaders:** the installed system always gets GRUB. No systemd-boot,
rEFInd or Limine choice.
- **Filesystems:** the installer offers ext4 only until that path has a clean
record; btrfs/xfs/f2fs are not offered.
- **Virtual machines need a virtual GPU** (virtio-gpu, or 3D acceleration
enabled). This is a Hyprland requirement; see "Test in a VM".
- **Installer library compatibility:** `cachyos-calamares` can lag behind an
Arch Boost update. The build detects this and stages the matching,
signature-verified Boost libraries for the installer only
(`scripts/calamares-compat.sh`).
- **No disk encryption flow** has been exercised, though the LUKS-capable
Calamares modules and `cryptsetup` are present.
- **Secure Boot** is not supported.
Expand Down
Loading
Loading