Compare commits

..
Author SHA1 Message Date
雷电芽衣 e6cb58c4c3 Keep lazydocker: Super+Shift+D launches it
Claude-Session: https://claude.ai/code/session_019krxFwnSbEM2n2QNQ1rG1K
2026-09-03 06:27:42 -04:00
雷电芽衣 5742a2841f Fit the appliance under GitHub's 2 GiB release limit
The qcow2 is written with zstd and 2M clusters after trimming freed blocks,
and the appliance leaves out clang, llvm, the Docker plugins, lazydocker,
base-devel, aether, and yay, plus usr/share/doc through NoExtract: 2.78 GB
became 2.10 GB. sudo is installed explicitly now that base-devel no longer
pulls it in.

Claude-Session: https://claude.ai/code/session_019krxFwnSbEM2n2QNQ1rG1K
2026-09-03 06:09:55 -04:00
雷电芽衣 70cbcb558c Build the Omarchy appliance for UTM SE on iPad and iPhone
omarchy-utm-make runs builder/build-utm.sh in a privileged linux/arm64
container: a 32G sparse GPT disk with the ISO's Btrfs subvolume layout,
Arch Linux ARM plus the aarch64 Omarchy packages, Limine on the UEFI
fallback path, omarchy-apply-system --defer-provisioning in the chroot,
first-boot provisioning staged with the arm64 Node tarball, one
limine-update, then a sealed and checked disk converted to compressed
qcow2. The host wraps it as a .utm bundle (no serial device: iOS has no
Ptty mode), zips it, and writes a sha256. omarchy-utm-boot boots the
result under qemu's interpreter, the closest stand-in for UTM SE.

fstab is written from the partition UUIDs directly: genfstab named the
hand-made loop device nodes, which sent the first boot into emergency
mode.

Claude-Session: https://claude.ai/code/session_019krxFwnSbEM2n2QNQ1rG1K
2026-09-03 04:50:14 -04:00
David Heinemeier Hansson 2673c613d9 Update the tagline to Beautiful, Fun & Agentic (#144)
Matches omarchy.org and the omarchy repo. The centring math keys off the string length, so the shorter line stays centred.
2026-09-01 11:54:27 +02:00
Ryan Hughes d6e4060d56 Add omarchy-iso-test-stop to shut down the kept-running VM
Graceful guest poweroff over SSH first, escalating to TERM then KILL;
--kill skips straight to force. Finds the newest live run by its pidfile
and confirms before acting when run from a terminal, since a harness
still driving the VM dies with it.
2026-08-30 18:33:17 -04:00
Ryan Hughes 100a41f7fd Type the console bootstrap blind and gate it on SSH
A VNC viewer that resizes the guest framebuffer makes the console font
unreadable to OCR, which stranded the bootstrap at its login-prompt gate.
Getty is reliably on the spare TTY and SSH coming up is the real success
signal, so type the login blind and retry the whole sequence up to three
times; a failed pass just leaves getty re-prompting.

Also print the resize-safe viewer invocation whenever a VM starts: a
resize during early boot can wedge virtio-gpu before the system is up.
2026-08-30 18:33:17 -04:00
Ryan Hughes f9be60b24b Name the released ISO after its full candidate version
--rc appended a bare -rc suffix regardless of the version passed, so
every candidate of a train uploaded over the same omarchy-X.Y.Z-rc.iso
and nobody could tell rc1 from rc2 by name or URL. A version that
already numbers its candidate (4.0.2rc2) is the complete artifact name;
the suffix remains for unnumbered candidate builds.
2026-08-30 17:15:42 -04:00
Ryan Hughes 222a742daf Merge pull request #136 from omacom/tagline-beautiful-fun
Change the greeter tagline to "Beautiful, Fun & Opinionated"
2026-08-30 17:03:17 -04:00
Ryan Hughes 9900e63bd4 Merge pull request #139 from omacom/fix/iso-test-ocr-bindings-vnc
Fix omarchy-iso-test staleness against 4.0.2-rc and add VNC visibility
2026-08-30 16:57:08 -04:00
Ryan Hughes 37fd3c4305 Sync the whole test suite and run it in the guest
--sync-omarchy now pushes all of test/ (the security coverage lives in
shell.d as well as acceptance.d), and the acceptance run gets
OMARCHY_ACCEPTANCE_SUDO_PASSWORD so the suite's opt-in sshd hardening
exercise can run against the throwaway VM.

--sync-all additionally runs the CLI and shell suites inside the guest
from the synced tree. The acceptance suite always runs against
/usr/share/omarchy either way: VM runs verify the finished product, and
qs matches shell instances by config path, so pointing the suite at the
synced tree just reads the session shell as "not running".
2026-08-30 16:45:35 -04:00
Ryan Hughes e89c0796f7 Serve a localhost VNC display from test VMs
Runs stay headless, but a viewer on 127.0.0.1:5905 can watch the driven
install live or inspect the acceptance VM left up by --keep-running.
2026-08-30 15:30:37 -04:00
Ryan Hughes 0b2739a55d Match the shortcut smoke to the menu-first bindings
Omarchy now binds Super+Space to the Omarchy menu and Super+Alt+Space to
the apps menu, and the omarchy.launcher plugin is gone (the apps menu
replaced it). The smoke table still expected the old launcher binding, so
both chords failed against current builds. Both surfaces open under the
omarchy-menu layer namespace.
2026-08-30 15:30:31 -04:00
Ryan Hughes 9a7325e3a6 Stop OCR-waiting on the unencrypted install confirm
Tesseract cannot read the highlighted "Yes, install without encryption"
button under any preprocessing, so waiting for that text always timed out
and killed unencrypted installs at the disk warning. Capture the screen and
continue instead; a Ctrl+C toggle that failed to apply still surfaces when
the unexpectedly encrypted install stalls at the LUKS prompt on first boot.
2026-08-30 15:30:21 -04:00
Ryan Hughes bba81a6ec5 End the release by printing the URL, checksum, and local path
The artifacts a release exists to hand out — the download URL and the
checksum to verify it with — were buried in rclone transfer noise or
absent entirely. Print them together as the final output, after the
upload has actually succeeded (set -e means reaching this line is the
success path).
2026-08-30 14:20:08 -04:00
Ryan Hughes 6b5ce68f25 Run docker through sudo when the socket refuses the user
The docker group is root-equivalent, so Omarchy operators stay out of
it on purpose — which left omarchy-iso-make unable to reach the daemon
at all. Fall back to sudo docker when a plain client call cannot reach
the socket; the script already prompts for interactive sudo elsewhere.
DOCKER_ARGS (including HOST_UID/HOST_GID) are assembled before the
escalation, so output ownership is unchanged.
2026-08-30 14:19:21 -04:00
Ryan Hughes 02f8d1cf50 Stop the ISO release when a step fails
omarchy-iso-release ran every step unconditionally: when docker denied
the build, it fell through to signing and uploading the newest stale
*-rc.iso from release/ under the new release's name — a six-month-old
ISO shipped as omarchy-4.0.2-rc.iso. set -e ends the release at the
first failed step, and also aborts when release/ holds no ISO for the
ref at all.
2026-08-30 14:15:01 -04:00
David Heinemeier Hansson 3559b2bc9d Change the greeter tagline to "Beautiful, Fun & Opinionated"
The same frame opens omarchy's first boot from its own copy of the string, so both repositories change together or an install and the deferred first boot after it bookend the setup with different words. Nothing generates one copy from the other: the vendoring between the repositories covers the setup form, not the greeter.

The line is centred from its own length, so the three characters it loses move it one column and nothing else.
2026-08-30 14:33:48 +02:00
Ryan Hughes d2e764d317 RC ISO installs from the rc package channel
pacman-online-rc.conf's [omarchy] repo moves from pkgs.omarchy.org/edge to
pkgs.omarchy.org/rc: an RC ISO now bundles and installs the release-candidate
packages against the package set stable users will get, instead of edge
churn.
2026-08-26 01:37:17 -04:00
20 changed files with 797 additions and 138 deletions

No files matched your search

+8
View File
@@ -143,3 +143,11 @@ Run `./bin/omarchy-iso-upload [release/omarchy.iso]`. This requires rclone confi
## Full release of the ISO
Run `./bin/omarchy-iso-release VERSION` to create, test, sign, and upload the ISO in one flow. Add `--rc` to release an RC build instead.
## Building the UTM SE appliance
`./bin/omarchy-utm-make` builds Omarchy for the App Store version of UTM on iPad and iPhone: an aarch64 disk with Arch Linux ARM underneath, wrapped as a `.utm` bundle and zipped into `release/Omarchy-<version>-aarch64-utm-se.utm.zip` beside its `.sha256`. Everything Omarchy ships from its own repository comes from the aarch64 builds at [ZacharyZhang-NY/omarchy-pkgs-aarch64](https://github.com/ZacharyZhang-NY/omarchy-pkgs-aarch64), which `builder/pacman-utm-se.conf` names; the runtime is the `omarchy-dev` and `omarchy-settings-dev` pair built there from the `utm-se` branch of omarchy, as local-source ISOs do.
The build runs in a privileged `linux/arm64` container: it partitions a 32 GiB sparse disk, lays out the same Btrfs subvolumes the ISO does, installs `builder/utm-se.packages` and then the Omarchy base set minus `builder/utm-se-drop.packages`, puts Limine on the UEFI fallback path, runs `omarchy-apply-system --defer-provisioning`, stages first-boot provisioning with the arm64 Node tarball, builds the boot entries, and seals the disk with an empty machine-id. The host needs Docker with a kernel that offers btrfs and loop devices (with colima: `colima ssh -- sudo modprobe btrfs`), `qemu` for the UEFI variable store template, `zip`, and `shasum`.
The bundle boots with 2 cores, 2048 MiB, a `virtio-gpu-pci` display at 1280x800, VirtFS sharing, and the shared clipboard. The first boot asks for the keyboard, user, and password, the way a deferred-provisioning ISO install does.
+9 -1
View File
@@ -158,7 +158,15 @@ else
DOCKER_ARGS+=(--pull=always)
fi
docker run "${DOCKER_ARGS[@]}" archlinux/archlinux:latest /$BUILD_SCRIPT
# The docker group is root-equivalent, so Omarchy operators stay out of it on
# purpose. When the socket refuses the calling user, run the client through
# sudo instead of requiring group membership.
DOCKER=(docker)
if ! docker version &>/dev/null; then
DOCKER=(sudo docker)
fi
"${DOCKER[@]}" run "${DOCKER_ARGS[@]}" archlinux/archlinux:latest /$BUILD_SCRIPT
latest_iso=$(\ls -t "$BUILD_RELEASE_PATH"/*.iso | head -n1)
iso_ref="${latest_iso%.*}-$OMARCHY_ISO_REF.iso"
+19
View File
@@ -1,5 +1,10 @@
#!/bin/bash
# A failed step must stop the release: without this, a make that dies (say,
# docker permission denied) falls through to signing and uploading whatever
# stale ISO release/ already holds, under the new release's name.
set -e
MAKE_ARGS=()
ISO_REF=quattro
RELEASE_SUFFIX=""
@@ -37,6 +42,14 @@ if [[ -z "${BUILD_VERSION-}" ]]; then
exit 1
fi
# A version that already numbers its candidate (4.0.2rc2) is the complete
# artifact name; the bare -rc suffix exists only for an unnumbered candidate.
# Without this, every candidate of a train uploads over the same
# omarchy-X.Y.Z-rc.iso and testers cannot tell which rc they are holding.
if [[ $BUILD_VERSION == *rc* ]]; then
RELEASE_SUFFIX=""
fi
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
BUILD_RELEASE_PATH="$BUILD_ROOT/release"
@@ -70,3 +83,9 @@ fi
echo -e "\e[32mSHA256 for $ISO_REF ISO: $iso_sha\e[0m"
omarchy-iso-upload "$release_iso"
# The whole point of a release is handing someone this pair.
echo -e "\e[32mReleased $ISO_REF ISO $BUILD_VERSION\e[0m"
echo " URL: https://iso.omarchy.org/${release_iso##*/}"
echo " SHA256: $iso_sha"
echo " Local: $release_iso"
+62 -40
View File
@@ -18,9 +18,10 @@
# first boot runs omarchy-provision-owner and creates it there
# --reuse-base Skip the install phase if a base image already exists
# --install-only Stop after producing the installed base image
# --sync-omarchy DIR Push DIR's test/acceptance into the guest before running
# --sync-omarchy DIR Push DIR's test suite into the guest before running
# (defaults to OMARCHY_PATH when the suite is present)
# --sync-all DIR Push all of DIR (bin/config/shell/test) into the guest
# and also run the CLI and shell suites there
# --port PORT Host port forwarded to guest SSH (default 2222)
# --memory MB VM memory (default 8192)
# --timeout SECS Install timeout (default 2400)
@@ -201,6 +202,11 @@ start_vm() {
local disk="$1" serial="$2"
shift 2
# A viewer that resizes the guest display breaks the console driving: OCR
# cannot read the shrunken font, and a resize during early boot can wedge
# virtio-gpu before the system comes up.
log "Watch (do not resize): vncviewer -RemoteResize=0 127.0.0.1:5905"
qemu-system-x86_64 \
-cpu host -enable-kvm -machine q35,accel=kvm \
-smp "$(nproc)" \
@@ -211,6 +217,7 @@ start_vm() {
-device virtio-blk-pci,drive=drive0,bootindex=1 \
-device virtio-vga \
-display none \
-vnc 127.0.0.1:5 \
-usb -device usb-tablet \
-netdev user,id=net0,hostfwd=tcp:127.0.0.1:$SSH_PORT-:22 \
-device virtio-net-pci,netdev=net0 \
@@ -400,7 +407,7 @@ guest_layer_absent() {
}
cleanup_shortcut_state() {
ssh_session 'for plugin in omarchy.launcher omarchy.menu omarchy.emojis omarchy.clipboard omarchy.audio; do
ssh_session 'for plugin in omarchy.menu omarchy.emojis omarchy.clipboard omarchy.audio; do
omarchy-shell shell hide "$plugin" >/dev/null 2>&1 || true
done
while read -r address; do
@@ -577,8 +584,8 @@ shortcut_smoke_phase() {
local status=0
local name chord namespace plugin
local shortcuts='launcher|meta_l-spc|omarchy-launcher|omarchy.launcher
menu|meta_l-alt-spc|omarchy-menu|omarchy.menu
local shortcuts='menu|meta_l-spc|omarchy-menu|omarchy.menu
apps-menu|meta_l-alt-spc|omarchy-menu|omarchy.menu
emojis|meta_l-ctrl-e|omarchy-emojis|omarchy.emojis
clipboard|meta_l-ctrl-v|omarchy-clipboard|omarchy.clipboard
audio-panel|meta_l-ctrl-a|omarchy-keyboard-panel|omarchy.audio'
@@ -631,7 +638,11 @@ drive_configurator() {
capture_console "success-installer-04-prepare-disk-warning-encrypted"
if ! $ENCRYPT; then
press ctrl-c
wait_for_screen "without encryption" 30
# The toggled confirm ("Yes, install without encryption") is a highlighted
# button OCR cannot read, so don't wait for it. A failed toggle still
# surfaces: the encrypted install stalls at the LUKS prompt on first boot
# and SSH never comes up.
sleep 2
capture_console "success-installer-05-prepare-disk-warning-unencrypted"
fi
press ret
@@ -695,7 +706,11 @@ drive_configurator() {
capture_console "success-installer-12-disk-warning-encrypted"
if ! $ENCRYPT; then
press ctrl-c # toggles the confirm to the unencrypted flow
wait_for_screen "without encryption" 30
# The toggled confirm ("Yes, install without encryption") is a highlighted
# button OCR cannot read, so don't wait for it. A failed toggle still
# surfaces: the encrypted install stalls at the LUKS prompt on first boot
# and SSH never comes up.
sleep 2
capture_console "success-installer-13-disk-warning-unencrypted"
fi
press ret
@@ -875,40 +890,39 @@ EOF
(cd "$BASE_DIR/www" && exec python3 -m http.server "$HTTP_PORT" --bind 127.0.0.1 >/dev/null 2>&1) &
HTTP_PID=$!
local waited=0
while true; do
# Type the login blind: a VNC viewer that resizes the framebuffer makes the
# console font unreadable to OCR, and the real gate is SSH coming up anyway.
# Getty re-prompts after a failed login, so a whole-sequence retry recovers
# from typing before the prompt was ready.
sleep 15 # give first boot time to reach getty
local attempt
for attempt in 1 2 3; do
press ctrl-alt-f3
sleep 8
press ret # settle a half-typed prompt from a previous attempt
sleep 2
type_text "$GUEST_USER"
capture_console "success-first-boot-03-console-username"
press ret
sleep 3
type_text "$GUEST_PASSWORD"
press ret
sleep 4
ocr_screen | grep -qi "login:" && break
((waited += 8))
type_text "curl -fsS http://10.0.2.2:$HTTP_PORT/bootstrap -o /tmp/bs && bash /tmp/bs"
capture_console "success-first-boot-05-bootstrap-command"
press ret
if ((waited >= 300)); then
capture_console "failure-first-boot-console-timeout"
echo "Timed out waiting for a console login prompt" >&2
return 1
if wait_for_ssh 120 "failure-first-boot-ssh-timeout-$attempt"; then
capture_console "success-first-boot-06-bootstrap-complete"
kill "$HTTP_PID" 2>/dev/null || true
HTTP_PID=""
press ctrl-alt-f1
return 0
fi
sleep 4
done
type_text "$GUEST_USER"
capture_console "success-first-boot-03-console-username"
press ret
wait_for_screen "Password" 60
type_text "$GUEST_PASSWORD"
capture_console "success-first-boot-04-console-password"
press ret
sleep 3
type_text "curl -fsS http://10.0.2.2:$HTTP_PORT/bootstrap -o /tmp/bs && bash /tmp/bs"
capture_console "success-first-boot-05-bootstrap-command"
press ret
wait_for_ssh 360 "failure-first-boot-ssh-timeout"
capture_console "success-first-boot-06-bootstrap-complete"
kill "$HTTP_PID" 2>/dev/null || true
HTTP_PID=""
press ctrl-alt-f1
echo "Console bootstrap did not produce SSH access after 3 attempts" >&2
return 1
}
# Get a freshly booted system to a running Hyprland session, typing at the
@@ -1058,17 +1072,14 @@ sync_omarchy() {
log "Syncing $SYNC_DIR into the guest"
tar -C "$SYNC_DIR" --exclude .git -cf - . | ssh_guest "mkdir -p $target && tar -C $target -xf -"
else
log "Syncing $SYNC_DIR/test/acceptance into the guest"
tar -C "$SYNC_DIR/test" -cf - acceptance acceptance.d | ssh_guest "mkdir -p $target/test && tar -C $target/test -xf -"
log "Syncing $SYNC_DIR/test into the guest"
tar -C "$SYNC_DIR" -cf - test | ssh_guest "mkdir -p $target && tar -C $target -xf -"
fi
}
acceptance_phase() {
log "Booting acceptance VM from base image overlay"
local guest_omarchy_path="/usr/share/omarchy"
$SYNC_ALL && guest_omarchy_path=".local/share/omarchy"
qemu-img create -f qcow2 -b "$BASE_DISK" -F qcow2 "$RUN_DIR/run.qcow2" >/dev/null
start_vm "$RUN_DIR/run.qcow2" "$RUN_DIR/serial.log"
@@ -1078,8 +1089,19 @@ acceptance_phase() {
local status=0
shortcut_smoke_phase || status=1
# The synced tree is a full checkout under --sync-all; run the host-style
# CLI and shell suites in the guest too before the graphical acceptance
# run. They derive their root from their own location in the synced tree.
if $SYNC_ALL; then
log "Running CLI and shell suites in the guest"
ssh_guest "bash .local/share/omarchy/test/all" || status=$?
fi
# The product under test is always the installed tree — VM runs verify the
# finished product, never a dev-linked checkout.
log "Running acceptance suite"
ssh_guest "OMARCHY_PATH=$guest_omarchy_path OMARCHY_ACCEPTANCE_DIR=/tmp/omarchy-acceptance bash .local/share/omarchy/test/acceptance" || status=$?
ssh_guest "OMARCHY_PATH=/usr/share/omarchy OMARCHY_ACCEPTANCE_DIR=/tmp/omarchy-acceptance \
OMARCHY_ACCEPTANCE_SUDO_PASSWORD=$GUEST_PASSWORD bash .local/share/omarchy/test/acceptance" || status=$?
log "Collecting artifacts into $RUN_DIR"
ssh_guest "tar -C /tmp -cf - omarchy-acceptance 2>/dev/null" | tar -C "$RUN_DIR" -xf - || true
+101
View File
@@ -0,0 +1,101 @@
#!/bin/bash
#
# Stop the VM a `omarchy-iso-test --keep-running` run left behind: graceful
# guest poweroff over SSH first, escalating to TERM then KILL. The base image
# is untouched either way; only the throwaway run overlay is affected.
#
# Usage: omarchy-iso-test-stop [--kill] [--port PORT]
#
# --kill Skip the graceful poweroff and kill the VM immediately
# --port PORT Host port forwarded to guest SSH (default 2222)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_DIR="$(dirname "$SCRIPT_DIR")"
GUEST_USER="omarchy"
GUEST_PASSWORD="omarchy"
SSH_PORT=2222
GRACEFUL=true
while (($#)); do
case "$1" in
--kill) GRACEFUL=false ;;
--port) SSH_PORT="$2"; shift ;;
-h | --help)
sed -n '3,10p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit 0
;;
*) echo "Unknown option: $1" >&2; exit 1 ;;
esac
shift
done
# The newest run dir whose qemu is still alive is the VM to stop.
PID=""
PIDFILE=""
while IFS= read -r candidate; do
pid=$(<"$candidate")
if kill -0 "$pid" 2>/dev/null; then
PID="$pid"
PIDFILE="$candidate"
break
fi
done < <(ls -t "$REPO_DIR"/test-runs/*/runs/*/qemu.pid 2>/dev/null)
if [[ -z $PID ]]; then
echo "No running omarchy-iso-test VM found." >&2
exit 1
fi
BASE_DIR="$(dirname "$(dirname "$(dirname "$PIDFILE")")")"
SSH_KEY="$BASE_DIR/id_ed25519"
# A harness that is still driving this VM dies with it; make sure that is
# what the caller wants before pulling anything down.
if [[ -t 0 ]]; then
if pgrep -f "bin/omarchy-iso-test .*\.iso" >/dev/null; then
echo "Warning: an omarchy-iso-test run appears to be in progress."
fi
if ! gum confirm "Stop test VM (pid $PID, ${PIDFILE#$REPO_DIR/})?"; then
exit 0
fi
fi
echo "Stopping test VM (pid $PID, ${PIDFILE#$REPO_DIR/})"
if $GRACEFUL && [[ -f $SSH_KEY ]]; then
ssh -i "$SSH_KEY" -p "$SSH_PORT" \
-o BatchMode=yes \
-o IdentitiesOnly=yes \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o ConnectTimeout=5 \
-o LogLevel=ERROR \
"$GUEST_USER@127.0.0.1" \
"echo $GUEST_PASSWORD | sudo -S systemctl poweroff" >/dev/null 2>&1 || true
waited=0
while kill -0 "$PID" 2>/dev/null && ((waited < 15)); do
sleep 1
((waited += 1))
done
fi
if kill -0 "$PID" 2>/dev/null; then
kill "$PID" 2>/dev/null || true
sleep 1
fi
if kill -0 "$PID" 2>/dev/null; then
kill -9 "$PID" 2>/dev/null || true
sleep 1
fi
if kill -0 "$PID" 2>/dev/null; then
echo "VM (pid $PID) refused to die." >&2
exit 1
fi
echo "VM stopped; port $SSH_PORT is free."
+53
View File
@@ -0,0 +1,53 @@
#!/bin/bash
set -e
usage() {
echo "Usage: $0 <omarchy-utm-se.qcow2|Omarchy.utm> [--hvf]" >&2
echo "Boots the appliance disk under qemu-system-aarch64 with the devices the UTM bundle declares." >&2
echo "TCG emulation is the default, the closest stand-in for UTM SE; --hvf uses Apple's hypervisor instead." >&2
exit 1
}
(( $# >= 1 )) || usage
disk="$1"
shift
accel=tcg
cpu=cortex-a72
if (( $# > 0 )); then
[[ $1 == "--hvf" ]] || usage
accel=hvf
cpu=host
fi
if [[ -d $disk ]]; then
disk=$(ls "$disk"/Data/*.qcow2 | head -n1)
fi
[[ -f $disk ]] || { echo "Error: $disk is not a disk image" >&2; exit 1; }
firmware=""
for candidate in "$(brew --prefix 2>/dev/null)/share/qemu" /usr/share/qemu /usr/share/edk2/aarch64; do
if [[ -f $candidate/edk2-aarch64-code.fd ]]; then
firmware="$candidate"
break
fi
done
[[ -n $firmware ]] || { echo "Error: no edk2-aarch64-code.fd found; install qemu" >&2; exit 1; }
run_dir="${TMPDIR:-/tmp}/omarchy-utm-boot"
mkdir -p "$run_dir"
vars="$run_dir/efi_vars.fd"
[[ -f $vars ]] || cp "$firmware/edk2-arm-vars.fd" "$vars"
echo "QMP socket: $run_dir/qmp.sock serial log: $run_dir/serial.log ssh: localhost:2222"
exec qemu-system-aarch64 \
-machine virt -cpu "$cpu" -accel "$accel" -smp 2 -m 2048 \
-drive if=pflash,format=raw,readonly=on,file="$firmware/edk2-aarch64-code.fd" \
-drive if=pflash,format=raw,file="$vars" \
-drive file="$disk",format=qcow2,if=none,id=drive0 \
-device virtio-blk-pci,drive=drive0 \
-device virtio-gpu-pci -device virtio-keyboard-pci -device virtio-mouse-pci \
-netdev user,id=net0,hostfwd=tcp:127.0.0.1:2222-:22 -device virtio-net-pci,netdev=net0 \
-device virtio-rng-pci \
-serial file:"$run_dir/serial.log" \
-qmp unix:"$run_dir/qmp.sock",server,nowait \
-display default
+66
View File
@@ -0,0 +1,66 @@
#!/bin/bash
set -e
BUILD_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
RELEASE_DIR="$BUILD_ROOT/release"
CACHE_DIR="$HOME/.cache/omarchy/utm-se"
BUILDER_IMAGE="menci/archlinuxarm:base"
if (( $# > 0 )); then
echo "Usage: $0" >&2
echo "Builds the Omarchy appliance for UTM SE into $RELEASE_DIR: a zipped .utm bundle and its sha256." >&2
exit 1
fi
DOCKER=(docker)
if ! docker version &>/dev/null; then
DOCKER=(sudo docker)
fi
# The build partitions a loop device and mounts btrfs inside the container, so the engine's kernel must offer both.
if ! "${DOCKER[@]}" run --rm --privileged --platform linux/arm64 "$BUILDER_IMAGE" \
sh -c 'grep -q btrfs /proc/filesystems && losetup -f >/dev/null'; then
echo "Error: the Docker engine's kernel needs btrfs and loop devices (colima: colima ssh -- sudo modprobe btrfs)" >&2
exit 1
fi
efi_vars=""
for candidate in "$(brew --prefix 2>/dev/null)/share/qemu/edk2-arm-vars.fd" /usr/share/qemu/edk2-arm-vars.fd /usr/share/edk2/aarch64/QEMU_VARS.fd; do
if [[ -f $candidate ]]; then
efi_vars="$candidate"
break
fi
done
if [[ -z $efi_vars ]]; then
echo "Error: no UEFI variable store template found; install qemu" >&2
exit 1
fi
mkdir -p "$RELEASE_DIR" "$CACHE_DIR/pkg"
rm -f "$RELEASE_DIR/omarchy-utm-se.qcow2" "$RELEASE_DIR/omarchy-utm-se.version"
"${DOCKER[@]}" run --rm --privileged --platform linux/arm64 \
-v "$BUILD_ROOT/builder:/builder:ro" \
-v "$RELEASE_DIR:/out" \
-v "$CACHE_DIR/pkg:/var/cache/pacman/pkg" \
"$BUILDER_IMAGE" bash /builder/build-utm.sh
version=$(cat "$RELEASE_DIR/omarchy-utm-se.version")
name="Omarchy-$version-aarch64-utm-se"
bundle="$RELEASE_DIR/$name/Omarchy.utm"
vm_uuid=$(uuidgen | tr '[:lower:]' '[:upper:]')
disk_uuid=$(uuidgen | tr '[:lower:]' '[:upper:]')
mac=$(printf '02:%02X:%02X:%02X:%02X:%02X' $((RANDOM % 256)) $((RANDOM % 256)) $((RANDOM % 256)) $((RANDOM % 256)) $((RANDOM % 256)))
rm -rf "$RELEASE_DIR/$name"
mkdir -p "$bundle/Data"
mv "$RELEASE_DIR/omarchy-utm-se.qcow2" "$bundle/Data/$disk_uuid.qcow2"
cp "$efi_vars" "$bundle/Data/efi_vars.fd"
"$BUILD_ROOT/builder/utm/render" "$vm_uuid" "$disk_uuid" "$mac" "$version" >"$bundle/config.plist"
rm -f "$RELEASE_DIR/$name.utm.zip"
(cd "$RELEASE_DIR/$name" && zip -qr "../$name.utm.zip" Omarchy.utm)
(cd "$RELEASE_DIR" && shasum -a 256 "$name.utm.zip" >"$name.utm.zip.sha256")
rm -rf "$RELEASE_DIR/$name" "$RELEASE_DIR/omarchy-utm-se.version"
echo "Built $RELEASE_DIR/$name.utm.zip"
+205
View File
@@ -0,0 +1,205 @@
#!/bin/bash
# Builds the aarch64 Omarchy disk for UTM SE inside a privileged Arch Linux ARM container.
set -euo pipefail
BUILDER=/builder
OUT=/out
WORK=/work
TARGET=$WORK/target
IMAGE=$WORK/omarchy.img
DISK_SIZE=32G
NODE_DIST_URL="https://nodejs.org/dist/latest"
PROVISIONING_DIR=var/lib/omarchy/provisioning
loop=""
cleanup() {
umount -R "$TARGET" 2>/dev/null || true
[[ -n $loop ]] && losetup -d "$loop" 2>/dev/null || true
}
trap cleanup EXIT
packages() {
grep -hv '^#\|^$' "$1"
}
# Docker's /dev is static, so the partition nodes the kernel scanned have to be made by hand.
partition() {
local node="${loop}p$1"
[[ -b $node ]] || mknod "$node" b $(tr ':' ' ' <"/sys/block/${loop#/dev/}/${loop#/dev/}p$1/dev")
echo "$node"
}
# The ISO defers the boot-image hooks the same way: /dev/null masks on the side pacman reads them from.
deferred_hooks=(60-mkinitcpio-remove.hook 60-limine-mkinitcpio-remove-pre.hook 80-limine-efi-deploy.hook 90-limine-mkinitcpio-remove-post.hook 90-mkinitcpio-install.hook)
mask_hooks() {
local dir="$1" name path
shift
mkdir -p "$dir"
for name in "$@"; do
path="$dir/$name"
if [[ -e $path || -L $path ]]; then
mv "$path" "$path.omarchy-backup"
fi
ln -s /dev/null "$path"
done
}
unmask_hooks() {
local dir="$1" name path
shift
for name in "$@"; do
path="$dir/$name"
rm "$path"
if [[ -e $path.omarchy-backup ]]; then
mv "$path.omarchy-backup" "$path"
fi
done
}
chroot_env=(
env --unset=XDG_RUNTIME_DIR
OMARCHY_PATH=/usr/share/omarchy
OMARCHY_INSTALL=/usr/share/omarchy/install
OMARCHY_MIRROR=stable
OMARCHY_RUNTIME_PACKAGE=omarchy-dev
OMARCHY_SETTINGS_PACKAGE=omarchy-settings-dev
OMARCHY_NVIM_PACKAGE=omarchy-nvim
OMARCHY_INSTALL_LOG_FILE=/var/log/omarchy-install.log
OMARCHY_LOG_TO_STDOUT=1
)
echo "==> Preparing the build container"
pacman-key --init
pacman-key --populate archlinuxarm
install -m 0644 "$BUILDER/mirrorlist-utm-se" /etc/pacman.d/mirrorlist
pacman -Syu --noconfirm arch-install-scripts btrfs-progs dosfstools gptfdisk psmisc qemu-img
echo "==> Partitioning a $DISK_SIZE disk"
mkdir -p "$WORK" "$TARGET"
rm -f "$IMAGE"
truncate -s "$DISK_SIZE" "$IMAGE"
sgdisk --zap-all --new=1:0:+1G --typecode=1:ef00 --change-name=1:OMARCHY_ESP \
--new=2:0:0 --typecode=2:8305 --change-name=2:OMARCHY "$IMAGE" >/dev/null
loop=$(losetup --find --show --partscan "$IMAGE")
esp=$(partition 1)
root=$(partition 2)
mkfs.vfat -F32 -n OMARCHY_ESP "$esp" >/dev/null
mkfs.btrfs -f -L OMARCHY "$root" >/dev/null
mount "$root" "$TARGET"
for subvolume in @ @home @log @pkg; do
btrfs subvolume create "$TARGET/$subvolume" >/dev/null
done
umount "$TARGET"
mount -o noatime,compress=zstd,subvol=@ "$root" "$TARGET"
mkdir -p "$TARGET/home" "$TARGET/var/log" "$TARGET/var/cache/pacman/pkg" "$TARGET/boot"
mount -o noatime,compress=zstd,subvol=@home "$root" "$TARGET/home"
mount -o noatime,compress=zstd,subvol=@log "$root" "$TARGET/var/log"
mount -o noatime,compress=zstd,subvol=@pkg "$root" "$TARGET/var/cache/pacman/pkg"
mount "$esp" "$TARGET/boot"
echo "==> Installing the base system and Omarchy"
# pacstrap reads hooks from this container's /etc/pacman.d/hooks; the UKI is built once by limine-update at the end.
mask_hooks /etc/pacman.d/hooks "${deferred_hooks[@]}"
pacstrap -c -K -C "$BUILDER/pacman-utm-se.conf" "$TARGET" $(packages "$BUILDER/utm-se.packages")
base_list="$TARGET/usr/share/omarchy/install/omarchy-base.packages"
pacstrap -c -C "$BUILDER/pacman-utm-se.conf" "$TARGET" \
$(bash "$TARGET/usr/bin/omarchy-pkg-list" "$base_list" | grep -vxF -f <(packages "$BUILDER/utm-se-drop.packages"))
echo "==> Configuring the system"
# genfstab falls back to the hand-made loop device names here, so the table is written from the UUIDs directly.
root_uuid=$(blkid -s UUID -o value "$root")
esp_uuid=$(blkid -s UUID -o value "$esp")
cat >"$TARGET/etc/fstab" <<FSTAB
UUID=$root_uuid / btrfs rw,noatime,compress=zstd,subvol=/@ 0 0
UUID=$root_uuid /home btrfs rw,noatime,compress=zstd,subvol=/@home 0 0
UUID=$root_uuid /var/log btrfs rw,noatime,compress=zstd,subvol=/@log 0 0
UUID=$root_uuid /var/cache/pacman/pkg btrfs rw,noatime,compress=zstd,subvol=/@pkg 0 0
UUID=$esp_uuid /boot vfat rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro 0 2
FSTAB
install -m 0644 "$BUILDER/pacman-utm-se.conf" "$TARGET/etc/pacman.conf"
install -m 0644 "$BUILDER/mirrorlist-utm-se" "$TARGET/etc/pacman.d/mirrorlist"
echo "en_US.UTF-8 UTF-8" >"$TARGET/etc/locale.gen"
arch-chroot "$TARGET" locale-gen >/dev/null
echo "LANG=en_US.UTF-8" >"$TARGET/etc/locale.conf"
printf 'KEYMAP=us\nXKBLAYOUT=us\n' >"$TARGET/etc/vconsole.conf"
echo omarchy >"$TARGET/etc/hostname"
ln -sf /usr/share/zoneinfo/UTC "$TARGET/etc/localtime"
install -m 0644 "$BUILDER/utm/monitors.lua" "$TARGET/etc/skel/.config/hypr/monitors.lua"
echo "==> Installing Limine on the fallback boot path"
# The virt machine's firmware puts the console on the serial port; tty0 keeps boot messages on the display too.
cmdline="root=UUID=$root_uuid rootflags=subvol=@ rw console=ttyAMA0 console=tty0"
install -D -m 0644 "$TARGET/usr/share/limine/BOOTAA64.EFI" "$TARGET/boot/EFI/BOOT/BOOTAA64.EFI"
mkdir -p "$TARGET/etc/kernel"
cat >"$TARGET/etc/pacman.d/hooks/99-omarchy-limine.hook" <<'HOOK'
[Trigger]
Operation = Upgrade
Type = Package
Target = limine
[Action]
Description = Deploying Omarchy Limine after upgrade...
When = PostTransaction
Exec = /bin/sh -c "/usr/bin/cp /usr/share/limine/BOOTAA64.EFI /boot/EFI/BOOT/BOOTAA64.EFI"
HOOK
sed "s|@@CMDLINE@@|$cmdline|" "$TARGET/usr/share/omarchy/default/limine/default.conf" >"$TARGET/etc/default/limine"
echo "$cmdline" >"$TARGET/etc/kernel/cmdline"
cp "$TARGET/usr/share/omarchy/default/limine/limine.conf" "$TARGET/boot/limine.conf"
echo "==> Running Omarchy system setup"
mask_hooks "$TARGET/etc/pacman.d/hooks" 90-mkinitcpio-install.hook
arch-chroot "$TARGET" "${chroot_env[@]}" omarchy-apply-system --defer-provisioning --first-install
unmask_hooks "$TARGET/etc/pacman.d/hooks" 90-mkinitcpio-install.hook
echo "==> Staging first-boot provisioning"
provisioning="$TARGET/$PROVISIONING_DIR"
install -d -m 0755 "$provisioning" "$provisioning/packages"
shasums=$(curl -fsSL "$NODE_DIST_URL/SHASUMS256.txt")
node_file=$(awk '/linux-arm64\.tar\.gz$/ { print $2 }' <<<"$shasums")
curl -fsSL "$NODE_DIST_URL/$node_file" -o "$provisioning/packages/$node_file"
awk -v file="$provisioning/packages/$node_file" '/linux-arm64\.tar\.gz$/ { print $1 " " file }' <<<"$shasums" | sha256sum -c - >/dev/null
touch "$provisioning/pending"
install -m 0644 "$TARGET/usr/share/omarchy/install/provisioning/omarchy-provision-owner.service" \
"$TARGET/etc/systemd/system/omarchy-provision-owner.service"
mkdir -p "$TARGET/etc/systemd/system/multi-user.target.wants"
ln -sf /etc/systemd/system/omarchy-provision-owner.service \
"$TARGET/etc/systemd/system/multi-user.target.wants/omarchy-provision-owner.service"
echo "==> Building the boot entries"
arch-chroot "$TARGET" limine-update
arch-chroot "$TARGET" btrfs quota disable / >/dev/null 2>&1 || true
# limine-update swallows a kernel's failure, so the UKI and its entry are checked directly.
compgen -G "$TARGET/boot/EFI/Linux/*.efi" >/dev/null
grep -q 'EFI/Linux/' "$TARGET/boot/limine.conf"
echo "==> Sealing the image"
version=$(arch-chroot "$TARGET" pacman -Q omarchy-dev | cut -d' ' -f2)
: >"$TARGET/etc/machine-id"
rm -f "$TARGET"/etc/ssh/ssh_host_* "$TARGET/root/.bash_history"
rm -rf "$TARGET"/var/cache/pacman/pkg/*
# Freed blocks still hold old bytes; trimming zeroes them so they compress to nothing.
fstrim "$TARGET" && fstrim "$TARGET/boot"
echo "==> Checking the sealed image"
[[ ! -s $TARGET/etc/machine-id ]]
[[ -z $(ls -A "$TARGET/var/cache/pacman/pkg") ]]
[[ -f $provisioning/pending && -f $provisioning/packages/$node_file ]]
[[ -L $TARGET/etc/systemd/system/multi-user.target.wants/omarchy-provision-owner.service ]]
[[ -f $TARGET/boot/EFI/BOOT/BOOTAA64.EFI && -f $TARGET/etc/snapper/configs/root ]]
[[ -f $TARGET/etc/pacman.d/hooks/90-mkinitcpio-install.hook && ! -L $TARGET/etc/pacman.d/hooks/90-mkinitcpio-install.hook ]]
grep -q "root=UUID=$root_uuid" "$TARGET/etc/kernel/cmdline"
# Keyring setup leaves gpg-agents holding files on the target; their sockets died with the chroot's /run, so kill by holder.
fuser -km "$TARGET" >/dev/null 2>&1 || true
if ! umount -R "$TARGET"; then
fuser -vm "$TARGET" || true
exit 1
fi
losetup -d "$loop"
loop=""
echo "==> Writing the compressed disk"
qemu-img convert -c -O qcow2 -o compression_type=zstd,cluster_size=2M "$IMAGE" "$OUT/omarchy-utm-se.qcow2"
echo "$version" >"$OUT/omarchy-utm-se.version"
+4
View File
@@ -0,0 +1,4 @@
# Arch Linux ARM mirrors: the geo mirror first, then fallbacks pacman tries when one stalls.
Server = http://fl.us.mirror.archlinuxarm.org/$arch/$repo
Server = http://ca.us.mirror.archlinuxarm.org/$arch/$repo
Server = http://dk.mirror.archlinuxarm.org/$arch/$repo
+26
View File
@@ -0,0 +1,26 @@
# pacman configuration of the UTM SE appliance: Arch Linux ARM plus the aarch64 builds of Omarchy's own packages.
[options]
HoldPkg = pacman glibc
Architecture = auto
CheckSpace
ParallelDownloads = 5
SigLevel = Required DatabaseOptional
LocalFileSigLevel = Optional
# Developer documentation does not fit a bundle that GitHub caps at 2 GiB.
NoExtract = usr/share/doc/* usr/share/gtk-doc/*
[core]
Include = /etc/pacman.d/mirrorlist
[extra]
Include = /etc/pacman.d/mirrorlist
[alarm]
Include = /etc/pacman.d/mirrorlist
[aur]
Include = /etc/pacman.d/mirrorlist
[omarchy-aarch64]
SigLevel = Optional
Server = https://github.com/ZacharyZhang-NY/omarchy-pkgs-aarch64/releases/download/stable
+15
View File
@@ -0,0 +1,15 @@
# Base packages the appliance leaves out: apps and toolchains too heavy for an interpreted guest, tools with no aarch64 build, and helpers for hardware the guest has not.
aether
asdcontrol
clang
docker-buildx
docker-compose
gpu-screen-recorder
hyprland-preview-share-picker
kdenlive
libreoffice-fresh
llvm
localsend
moonlight-qt
xournalpp
yay
+18
View File
@@ -0,0 +1,18 @@
# Installed before the Omarchy base set: the aarch64 kernel, keyrings, boot stack, guest tools, and the dev packages built from the utm-se checkout.
archlinuxarm-keyring
base
efibootmgr
limine
linux-aarch64
lua51
luarocks
omarchy-dev
omarchy-keyring
omarchy-settings-dev
openssh
pipewire
qemu-guest-agent
snapper
spice-vdagent
sudo
zram-generator
+134
View File
@@ -0,0 +1,134 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Backend</key>
<string>QEMU</string>
<key>ConfigurationVersion</key>
<integer>4</integer>
<key>Information</key>
<dict>
<key>Name</key>
<string>Omarchy</string>
<key>UUID</key>
<string>@@VM_UUID@@</string>
<key>IconCustom</key>
<false/>
<key>Icon</key>
<string>arch-linux</string>
<key>Notes</key>
<string>Omarchy @@VERSION@@ for UTM SE (aarch64). The first boot asks for your keyboard, user, and password. Option acts as SUPER.</string>
</dict>
<key>System</key>
<dict>
<key>Architecture</key>
<string>aarch64</string>
<key>Target</key>
<string>virt</string>
<key>CPU</key>
<string>default</string>
<key>CPUFlagsAdd</key>
<array/>
<key>CPUFlagsRemove</key>
<array/>
<key>CPUCount</key>
<integer>2</integer>
<key>ForceMulticore</key>
<false/>
<key>MemorySize</key>
<integer>2048</integer>
<key>JITCacheSize</key>
<integer>0</integer>
</dict>
<key>QEMU</key>
<dict>
<key>DebugLog</key>
<false/>
<key>UEFIBoot</key>
<true/>
<key>RNGDevice</key>
<true/>
<key>BalloonDevice</key>
<false/>
<key>TPMDevice</key>
<false/>
<key>Hypervisor</key>
<false/>
<key>RTCLocalTime</key>
<false/>
<key>PS2Controller</key>
<false/>
<key>AdditionalArguments</key>
<array/>
</dict>
<key>Input</key>
<dict>
<key>UsbBusSupport</key>
<string>3.0</string>
<key>UsbSharing</key>
<false/>
<key>MaximumUsbShare</key>
<integer>3</integer>
</dict>
<key>Sharing</key>
<dict>
<key>DirectoryShareMode</key>
<string>VirtFS</string>
<key>DirectoryShareReadOnly</key>
<false/>
<key>ClipboardSharing</key>
<true/>
</dict>
<key>Display</key>
<array>
<dict>
<key>Hardware</key>
<string>virtio-gpu-pci</string>
<key>DynamicResolution</key>
<false/>
<key>NativeResolution</key>
<false/>
<key>UpscalingFilter</key>
<string>Linear</string>
<key>DownscalingFilter</key>
<string>Linear</string>
</dict>
</array>
<key>Drive</key>
<array>
<dict>
<key>Identifier</key>
<string>@@DISK_UUID@@</string>
<key>ImageName</key>
<string>@@DISK_UUID@@.qcow2</string>
<key>ImageType</key>
<string>Disk</string>
<key>Interface</key>
<string>VirtIO</string>
<key>InterfaceVersion</key>
<integer>1</integer>
<key>ReadOnly</key>
<false/>
</dict>
</array>
<key>Network</key>
<array>
<dict>
<key>Mode</key>
<string>Shared</string>
<key>Hardware</key>
<string>virtio-net-pci</string>
<key>MacAddress</key>
<string>@@MAC@@</string>
<key>IsolateFromHost</key>
<false/>
<key>PortForward</key>
<array/>
</dict>
</array>
<key>Serial</key>
<array/>
<key>Sound</key>
<array/>
</dict>
</plist>
+3
View File
@@ -0,0 +1,3 @@
-- UTM scales the guest's 1280x800 display to the device, so Hyprland and GTK draw at scale 1.
hl.monitor({ output = "", mode = "preferred", position = "auto", scale = 1 })
hl.env("GDK_SCALE", "1")
+8
View File
@@ -0,0 +1,8 @@
#!/bin/bash
# Renders the UTM config.plist template: render <vm-uuid> <disk-uuid> <mac> <version>
set -e
(( $# == 4 )) || { echo "Usage: $0 <vm-uuid> <disk-uuid> <mac> <version>" >&2; exit 1; }
sed -e "s/@@VM_UUID@@/$1/" -e "s/@@DISK_UUID@@/$2/g" -e "s/@@MAC@@/$3/" -e "s/@@VERSION@@/$4/" "$(dirname "$0")/config.plist"
+1 -1
View File
@@ -122,7 +122,7 @@ greeter() {
printf '\033[%d;1H' "$logo_row"
gum style --foreground 2 --padding "0 0 0 $PADDING_LEFT" "$(<"$LOGO_PATH")"
tagline="Beautiful, Modern & Opinionated Linux by DHH"
tagline="Beautiful, Fun & Agentic Linux by DHH"
tpad=$(((cols - ${#tagline}) / 2)); (( tpad < 0 )) && tpad=0
printf '\033[%d;%dH%s' "$tagline_row" "$((tpad + 1))" "$tagline"
@@ -47,21 +47,6 @@ corrupt_package() {
printf '%s' "$package"
}
# A retry in the same boot never gets as far as a checksum. The first failure
# unlinked the package through the bind mount, so pacman now fails opening it,
# and it names the mirror it read from rather than the cache it writes to.
deleted_package() {
local log match package=""
for log in "$@"; do
[[ -f $log ]] || continue
match=$(sed -n "s|.*Could not open file ${OFFLINE_MIRROR}/\([^ ]*\).*|\1|p" "$log" | tail -n 1)
[[ -n $match ]] && package="$match"
done
printf '%s' "$package"
}
expected_checksum() {
local package="$1" reader
@@ -92,14 +77,7 @@ actual_checksum() {
printf '%s' "${sum%% *}"
}
# The checksum failure is read first wherever both are in the log: it is the
# attempt that names the cause, and the open failures after it are its wake.
package=$(corrupt_package "$@")
deleted=""
if [[ -z $package ]]; then
package=$(deleted_package "$@")
deleted=1
fi
if [[ -z $package ]]; then
exit 1
fi
@@ -109,20 +87,6 @@ fi
# filename has no business containing before printing it.
display_package="${package//[^A-Za-z0-9._+:@%-]/}"
# Nothing can be weighed here: the bytes that failed are gone, and the copy in
# the mirror went with them. Say which attempt this is, because the screen
# otherwise repeats the same unreadable error for every retry and the one that
# explains it scrolled away with the first.
if [[ -n $deleted ]]; then
echo "An earlier attempt deleted this package"
echo "pacman rejected it off this ISO and unlinked it from the mirror:"
echo "$display_package"
echo
echo "Reboot before retrying: the deletion is in RAM, so the ISO's copy returns"
echo "The first error after that reboot is the one that names the fault"
exit 0
fi
expected=$(expected_checksum "$package")
actual=$(actual_checksum "$package")
+1 -1
View File
@@ -27,7 +27,7 @@ Include = /etc/pacman.d/mirrorlist
[omarchy]
SigLevel = Optional TrustAll
Server = https://pkgs.omarchy.org/edge/$arch
Server = https://pkgs.omarchy.org/rc/$arch
[arch-mact2]
Server = https://mirror.funami.tech/arch-mact2/os/x86_64
-59
View File
@@ -142,65 +142,6 @@ if grep -qF "no longer matches the checksum" <<<"$output"; then
fi
pass "a deleted package names the medium without claiming which way it failed"
# Every retry in the same boot fails on the open rather than the checksum, and
# pacman names the mirror it read from, not the cache it writes to.
write_retry_log() {
local log="$1" path="${2:-$mirror/$PACKAGE}"
{
echo "[dashboard] :: Retrieving packages..."
echo "[dashboard] error: failed retrieving file '${path##*/}' from disk : Could not open file $path"
echo "[dashboard] error: failed to commit transaction (download library error)"
echo "[dashboard] ==> ERROR: Failed to install packages to new root"
} >"$log"
}
write_retry_log "$work/retry.log"
set +e
output=$(diagnose "$work/retry.log")
status=$?
set -e
(( status == 0 )) || fail "a retry after the deletion is diagnosed" "exit status $status"
[[ $(head -n 1 <<<"$output") == "An earlier attempt deleted this package" ]] ||
fail "a retry is diagnosed as a retry" "$output"
grep -qF "$PACKAGE" <<<"$output" || fail "a retry names the package" "$output"
grep -qF "Reboot before retrying" <<<"$output" ||
fail "a retry says how to get the package back" "$output"
pass "a retry after the deletion says so instead of going undiagnosed"
# A log holding both attempts must answer with the one that names the cause.
# The retries come after it and would otherwise win on recency.
printf 'a package, but damaged\n' >"$mirror/$PACKAGE"
write_mirror_db "$(printf '0%.0s' {1..64})"
{
cat "$log"
cat "$work/retry.log"
} >"$work/both.log"
set +e
output=$(diagnose "$work/both.log")
set -e
[[ $(head -n 1 <<<"$output") == "The install medium is damaged" ]] ||
fail "the checksum failure outranks the retries that followed it" "$output"
pass "the checksum failure outranks the retries that followed it"
# An open failure somewhere other than the mirror was not read off this ISO.
write_retry_log "$work/retry-elsewhere.log" "/var/tmp/build/$PACKAGE"
set +e
output=$(diagnose "$work/retry-elsewhere.log")
status=$?
set -e
(( status == 1 )) || fail "an open failure outside the mirror is not diagnosed" "exit $status: $output"
[[ -z $output ]] || fail "an open failure outside the mirror prints nothing" "$output"
pass "an open failure outside the mirror is not blamed on the medium"
rm "$mirror/$PACKAGE"
# A record with no %SHA256SUM% must not borrow the next record's, which would
# compare a real file against another package's checksum.
printf 'a package, but damaged\n' >"$mirror/$PACKAGE"
+64
View File
@@ -0,0 +1,64 @@
#!/bin/bash
#
# Unit tests for the UTM SE appliance builder's static inputs: the config.plist
# template must be a valid plist once omarchy-utm-make substitutes its
# placeholders, and the package lists must name real packages.
set -euo pipefail
ROOT=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
pass() {
printf 'ok - %s\n' "$1"
}
fail() {
local description="$1"
local detail="${2:-}"
[[ -n $detail ]] && printf '%s\n' "$detail" >&2
printf 'not ok - %s\n' "$description" >&2
exit 1
}
template="$ROOT/builder/utm/config.plist"
render="$ROOT/builder/utm/render"
placeholders=$(grep -o '@@[A-Z_]*@@' "$template" | sort -u | tr '\n' ' ')
[[ $placeholders == "@@DISK_UUID@@ @@MAC@@ @@VERSION@@ @@VM_UUID@@ " ]] ||
fail "the template carries exactly the placeholders the build fills" "$placeholders"
pass "the template carries exactly the placeholders the build fills"
rendered=$("$render" 11111111-2222-3333-4444-555555555555 66666666-7777-8888-9999-000000000000 02:00:00:00:00:01 4.0.2)
python3 - "$rendered" <<'PY' || fail "the rendered config.plist parses, names the disk it ships, and declares nothing iOS rejects"
import plistlib, sys
config = plistlib.loads(sys.argv[1].encode())
assert "@@" not in sys.argv[1]
assert config["ConfigurationVersion"] == 4
assert config["Information"]["UUID"] == "11111111-2222-3333-4444-555555555555"
assert config["Drive"][0]["Identifier"] == "66666666-7777-8888-9999-000000000000"
assert config["Drive"][0]["ImageName"] == "66666666-7777-8888-9999-000000000000.qcow2"
assert config["Network"][0]["MacAddress"] == "02:00:00:00:00:01"
assert "4.0.2" in config["Information"]["Notes"]
assert config["System"]["Architecture"] == "aarch64"
assert config["QEMU"]["Hypervisor"] is False
assert config["System"]["MemorySize"] == 2048
# UTM on iOS has no Ptty serial mode and decodes strictly, so the bundle ships no serial device.
assert config["Serial"] == []
PY
pass "the rendered config.plist parses, names the disk it ships, and declares nothing iOS rejects"
base=""
for candidate in "$ROOT/../omarchy" "$ROOT/../omacom/omarchy"; do
if [[ -f $candidate/install/omarchy-base.packages ]]; then
base="$candidate/install/omarchy-base.packages"
break
fi
done
if [[ -z $base ]]; then
pass "no omarchy checkout beside this one; skipping the drop list check"
exit 0
fi
missing=$(grep -v '^#\|^$' "$ROOT/builder/utm-se-drop.packages" | grep -vxF -f <(sed -e 's/[[:space:]]*#.*$//' -e '/^$/d' "$base") || true)
[[ -z $missing ]] || fail "every dropped package is one the base list ships" "$missing"
pass "every dropped package is one the base list ships"