Compare commits

..
Author SHA1 Message Date
Omarchybot da40ed270d Diagnose the retry that follows a deleted package
Every attempt after the first fails on the open rather than on a checksum: pacstrap runs with --noconfirm, so pacman answers yes to "delete it?" and unlinks the package through the bind mount that makes the ISO's offline mirror the target's package cache. The failure screen then carried no diagnosis at all, because the only line it matched -- "is corrupted" under the target cache -- was written by the attempt that already scrolled away. A retry is the state most people are in by the time they read the screen, and it was the one case with nothing on it.

pacman names the repository it read from in that error, not the cache it writes to, so the match keys on the mirror path rather than the target cache path. Where a log holds both attempts the checksum failure still wins: it is the one that names the cause, and the open failures after it are its wake.

The verdict sends the reader to reboot instead of offering a remedy for the medium. Nothing can be weighed once the bytes are gone, and the deletion landed in the live session's RAM overlay rather than on the stick, so a reboot brings the ISO's copy back and the first error after it is the one that says what actually failed.
2026-08-28 05:45:47 -07:00
20 changed files with 138 additions and 797 deletions

No files matched your search

-8
View File
@@ -143,11 +143,3 @@ 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.
+1 -9
View File
@@ -158,15 +158,7 @@ else
DOCKER_ARGS+=(--pull=always)
fi
# 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
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,10 +1,5 @@
#!/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=""
@@ -42,14 +37,6 @@ 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"
@@ -83,9 +70,3 @@ 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"
+40 -62
View File
@@ -18,10 +18,9 @@
# 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 suite into the guest before running
# --sync-omarchy DIR Push DIR's test/acceptance 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)
@@ -202,11 +201,6 @@ 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)" \
@@ -217,7 +211,6 @@ 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 \
@@ -407,7 +400,7 @@ guest_layer_absent() {
}
cleanup_shortcut_state() {
ssh_session 'for plugin in omarchy.menu omarchy.emojis omarchy.clipboard omarchy.audio; do
ssh_session 'for plugin in omarchy.launcher 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
@@ -584,8 +577,8 @@ shortcut_smoke_phase() {
local status=0
local name chord namespace plugin
local shortcuts='menu|meta_l-spc|omarchy-menu|omarchy.menu
apps-menu|meta_l-alt-spc|omarchy-menu|omarchy.menu
local shortcuts='launcher|meta_l-spc|omarchy-launcher|omarchy.launcher
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'
@@ -638,11 +631,7 @@ drive_configurator() {
capture_console "success-installer-04-prepare-disk-warning-encrypted"
if ! $ENCRYPT; then
press ctrl-c
# 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
wait_for_screen "without encryption" 30
capture_console "success-installer-05-prepare-disk-warning-unencrypted"
fi
press ret
@@ -706,11 +695,7 @@ drive_configurator() {
capture_console "success-installer-12-disk-warning-encrypted"
if ! $ENCRYPT; then
press ctrl-c # toggles the confirm to the unencrypted flow
# 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
wait_for_screen "without encryption" 30
capture_console "success-installer-13-disk-warning-unencrypted"
fi
press ret
@@ -890,39 +875,40 @@ EOF
(cd "$BASE_DIR/www" && exec python3 -m http.server "$HTTP_PORT" --bind 127.0.0.1 >/dev/null 2>&1) &
HTTP_PID=$!
# 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
local waited=0
while true; 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
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
ocr_screen | grep -qi "login:" && break
((waited += 8))
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
if ((waited >= 300)); then
capture_console "failure-first-boot-console-timeout"
echo "Timed out waiting for a console login prompt" >&2
return 1
fi
sleep 4
done
echo "Console bootstrap did not produce SSH access after 3 attempts" >&2
return 1
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
}
# Get a freshly booted system to a running Hyprland session, typing at the
@@ -1072,14 +1058,17 @@ 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 into the guest"
tar -C "$SYNC_DIR" -cf - test | ssh_guest "mkdir -p $target && tar -C $target -xf -"
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 -"
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"
@@ -1089,19 +1078,8 @@ 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=/usr/share/omarchy OMARCHY_ACCEPTANCE_DIR=/tmp/omarchy-acceptance \
OMARCHY_ACCEPTANCE_SUDO_PASSWORD=$GUEST_PASSWORD bash .local/share/omarchy/test/acceptance" || status=$?
ssh_guest "OMARCHY_PATH=$guest_omarchy_path OMARCHY_ACCEPTANCE_DIR=/tmp/omarchy-acceptance 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
@@ -1,101 +0,0 @@
#!/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
@@ -1,53 +0,0 @@
#!/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
@@ -1,66 +0,0 @@
#!/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
@@ -1,205 +0,0 @@
#!/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
@@ -1,4 +0,0 @@
# 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
@@ -1,26 +0,0 @@
# 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
@@ -1,15 +0,0 @@
# 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
@@ -1,18 +0,0 @@
# 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
@@ -1,134 +0,0 @@
<?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
@@ -1,3 +0,0 @@
-- 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
@@ -1,8 +0,0 @@
#!/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, Fun & Agentic Linux by DHH"
tagline="Beautiful, Modern & Opinionated Linux by DHH"
tpad=$(((cols - ${#tagline}) / 2)); (( tpad < 0 )) && tpad=0
printf '\033[%d;%dH%s' "$tagline_row" "$((tpad + 1))" "$tagline"
@@ -47,6 +47,21 @@ 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
@@ -77,7 +92,14 @@ 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
@@ -87,6 +109,20 @@ 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/rc/$arch
Server = https://pkgs.omarchy.org/edge/$arch
[arch-mact2]
Server = https://mirror.funami.tech/arch-mact2/os/x86_64
+59
View File
@@ -142,6 +142,65 @@ 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
@@ -1,64 +0,0 @@
#!/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"