From 70cbcb558cce198cabd58e7411159a9b53c55f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 3 Sep 2026 04:50:14 -0400 Subject: [PATCH] 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 --- README.md | 8 ++ bin/omarchy-utm-boot | 53 +++++++++ bin/omarchy-utm-make | 66 ++++++++++++ builder/build-utm.sh | 203 +++++++++++++++++++++++++++++++++++ builder/mirrorlist-utm-se | 4 + builder/pacman-utm-se.conf | 24 +++++ builder/utm-se-drop.packages | 9 ++ builder/utm-se.packages | 18 ++++ builder/utm/config.plist | 134 +++++++++++++++++++++++ builder/utm/monitors.lua | 3 + builder/utm/render | 8 ++ test/unit/utm-bundle-test.sh | 64 +++++++++++ 12 files changed, 594 insertions(+) create mode 100755 bin/omarchy-utm-boot create mode 100755 bin/omarchy-utm-make create mode 100755 builder/build-utm.sh create mode 100644 builder/mirrorlist-utm-se create mode 100644 builder/pacman-utm-se.conf create mode 100644 builder/utm-se-drop.packages create mode 100644 builder/utm-se.packages create mode 100644 builder/utm/config.plist create mode 100644 builder/utm/monitors.lua create mode 100755 builder/utm/render create mode 100755 test/unit/utm-bundle-test.sh diff --git a/README.md b/README.md index 1ec23ea..0966f20 100644 --- a/README.md +++ b/README.md @@ -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--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. diff --git a/bin/omarchy-utm-boot b/bin/omarchy-utm-boot new file mode 100755 index 0000000..5bbda58 --- /dev/null +++ b/bin/omarchy-utm-boot @@ -0,0 +1,53 @@ +#!/bin/bash + +set -e + +usage() { + echo "Usage: $0 [--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 diff --git a/bin/omarchy-utm-make b/bin/omarchy-utm-make new file mode 100755 index 0000000..f2d9ce6 --- /dev/null +++ b/bin/omarchy-utm-make @@ -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" diff --git a/builder/build-utm.sh b/builder/build-utm.sh new file mode 100755 index 0000000..89a99c7 --- /dev/null +++ b/builder/build-utm.sh @@ -0,0 +1,203 @@ +#!/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" <"$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/* + +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 "$IMAGE" "$OUT/omarchy-utm-se.qcow2" +echo "$version" >"$OUT/omarchy-utm-se.version" diff --git a/builder/mirrorlist-utm-se b/builder/mirrorlist-utm-se new file mode 100644 index 0000000..e0224a1 --- /dev/null +++ b/builder/mirrorlist-utm-se @@ -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 diff --git a/builder/pacman-utm-se.conf b/builder/pacman-utm-se.conf new file mode 100644 index 0000000..85b978e --- /dev/null +++ b/builder/pacman-utm-se.conf @@ -0,0 +1,24 @@ +# 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 + +[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 diff --git a/builder/utm-se-drop.packages b/builder/utm-se-drop.packages new file mode 100644 index 0000000..c9bc98c --- /dev/null +++ b/builder/utm-se-drop.packages @@ -0,0 +1,9 @@ +# Base packages the appliance leaves out: desktop apps too heavy for an interpreted guest, tools with no aarch64 build, and hardware-only helpers. +asdcontrol +gpu-screen-recorder +hyprland-preview-share-picker +kdenlive +libreoffice-fresh +localsend +moonlight-qt +xournalpp diff --git a/builder/utm-se.packages b/builder/utm-se.packages new file mode 100644 index 0000000..f142e4f --- /dev/null +++ b/builder/utm-se.packages @@ -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 +base-devel +efibootmgr +limine +linux-aarch64 +lua51 +luarocks +omarchy-dev +omarchy-keyring +omarchy-settings-dev +openssh +pipewire +qemu-guest-agent +snapper +spice-vdagent +zram-generator diff --git a/builder/utm/config.plist b/builder/utm/config.plist new file mode 100644 index 0000000..55fcda1 --- /dev/null +++ b/builder/utm/config.plist @@ -0,0 +1,134 @@ + + + + + Backend + QEMU + ConfigurationVersion + 4 + Information + + Name + Omarchy + UUID + @@VM_UUID@@ + IconCustom + + Icon + arch-linux + Notes + Omarchy @@VERSION@@ for UTM SE (aarch64). The first boot asks for your keyboard, user, and password. Option acts as SUPER. + + System + + Architecture + aarch64 + Target + virt + CPU + default + CPUFlagsAdd + + CPUFlagsRemove + + CPUCount + 2 + ForceMulticore + + MemorySize + 2048 + JITCacheSize + 0 + + QEMU + + DebugLog + + UEFIBoot + + RNGDevice + + BalloonDevice + + TPMDevice + + Hypervisor + + RTCLocalTime + + PS2Controller + + AdditionalArguments + + + Input + + UsbBusSupport + 3.0 + UsbSharing + + MaximumUsbShare + 3 + + Sharing + + DirectoryShareMode + VirtFS + DirectoryShareReadOnly + + ClipboardSharing + + + Display + + + Hardware + virtio-gpu-pci + DynamicResolution + + NativeResolution + + UpscalingFilter + Linear + DownscalingFilter + Linear + + + Drive + + + Identifier + @@DISK_UUID@@ + ImageName + @@DISK_UUID@@.qcow2 + ImageType + Disk + Interface + VirtIO + InterfaceVersion + 1 + ReadOnly + + + + Network + + + Mode + Shared + Hardware + virtio-net-pci + MacAddress + @@MAC@@ + IsolateFromHost + + PortForward + + + + Serial + + Sound + + + diff --git a/builder/utm/monitors.lua b/builder/utm/monitors.lua new file mode 100644 index 0000000..30cb099 --- /dev/null +++ b/builder/utm/monitors.lua @@ -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") diff --git a/builder/utm/render b/builder/utm/render new file mode 100755 index 0000000..718ed65 --- /dev/null +++ b/builder/utm/render @@ -0,0 +1,8 @@ +#!/bin/bash +# Renders the UTM config.plist template: render + +set -e + +(( $# == 4 )) || { echo "Usage: $0 " >&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" diff --git a/test/unit/utm-bundle-test.sh b/test/unit/utm-bundle-test.sh new file mode 100755 index 0000000..11efe9b --- /dev/null +++ b/test/unit/utm-bundle-test.sh @@ -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"