Files
omarchy-iso/builder/build-utm.sh
T
雷电芽衣 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

206 lines
8.6 KiB
Bash
Executable File

#!/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"