Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6cb58c4c3
|
||
|
|
5742a2841f
|
||
|
|
70cbcb558c
|
||
|
|
2673c613d9 | ||
|
|
d6e4060d56 | ||
|
|
100a41f7fd | ||
|
|
f9be60b24b | ||
|
|
222a742daf | ||
|
|
9900e63bd4 | ||
|
|
3559b2bc9d |
No files matched your search
@@ -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.
|
||||
@@ -42,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"
|
||||
|
||||
|
||||
+32
-28
@@ -202,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)" \
|
||||
@@ -885,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
|
||||
|
||||
Executable
+101
@@ -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."
|
||||
Executable
+53
@@ -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
|
||||
Executable
+66
@@ -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"
|
||||
Executable
+205
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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")
|
||||
Executable
+8
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
|
||||
Executable
+64
@@ -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"
|
||||
Reference in new issue
Block a user