From c90ad3a77b10c9692f29d3eda62379c80ff8a2e8 Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 24 Aug 2026 20:33:26 +0800 Subject: [PATCH 01/11] builder: build for the host architecture instead of hardcoding x86_64 builder/build-iso.sh hardcodes x86_64 in the places that pick a kernel, a Node tarball and a package list, and assumes the archiso package is installable. Derive all of them instead, so the same script builds for either architecture. Architecture selection: - one case statement sets ISO_ARCH / ISO_NODE_ARCH / ISO_KERNEL from uname -m; everything downstream uses those rather than a literal - packages.$ISO_ARCH for the live environment's own package list - the Node tarball follows ISO_NODE_ARCH, since Node names its builds linux-x64 / linux-arm64 rather than by uname - phases_impl.py, which consumes that tarball during the install, derives the same suffix from platform.machine(). Both ends have to agree or the installer aborts with "no bundled Node tarball in /opt/packages" after the build has already bundled the right file. - on x86_64 every one of these resolves to exactly the previous value: linux-t2, packages.x86_64, linux-x64.tar.gz Host portability, not architecture-specific: - prefer the archiso package, and fall back to the vendored submodule when the repository has no archiso (Arch Linux ARM does not). Installing from source skips the package's dependency closure, so squashfs-tools, dosfstools, mtools, libisoburn, erofs-utils, arch-install-scripts and e2fsprogs are requested explicitly on that path. Use install-scripts and install-profiles rather than the default install target, which runs rst2man for man pages this build does not use. - match both .pkg.tar.zst and .pkg.tar.xz when indexing the offline mirror. Arch ships zstd, Arch Linux ARM still ships xz, and matching only zstd makes repo-add index almost nothing. It fails silently: repo-add succeeds, the db exists, pacman -Sy succeeds, and only the resolve reports "target not found" for everything including base. Both extensions are enumerated rather than globbed so .sig files are not swept in. aarch64 package selection, gated on uname -m: - OMARCHY_ARCH_DROP plus a filter, since pacman -Syw aborts the whole transaction on the first missing target. Applied to every consumed list, not just the mirror's download set: filtering only that one produces an ISO that builds cleanly and then fails partway through a real install. - quickshell-git and mise substituted for their packaged equivalents. Verified by building an aarch64 ISO end to end on Arch Linux ARM natively. --- builder/build-iso.sh | 156 ++++++++++++++++-- builder/build-omarchy-packages.sh | 5 +- configs/airootfs/root/.automated_script.sh | 2 +- .../omarchy-iso/orchestrator/phases_impl.py | 7 +- 4 files changed, 155 insertions(+), 15 deletions(-) diff --git a/builder/build-iso.sh b/builder/build-iso.sh index db8daa2c..344298c2 100755 --- a/builder/build-iso.sh +++ b/builder/build-iso.sh @@ -5,6 +5,21 @@ set -e OMARCHY_ISO_REF="${OMARCHY_ISO_REF:-quattro}" OMARCHY_MIRROR="${OMARCHY_MIRROR:-stable}" +# Architecture-dependent selections. The build targets the host architecture; +# everything below derives from these rather than hardcoding one of them. +case "$(uname -m)" in + aarch64) + ISO_ARCH=aarch64 + ISO_NODE_ARCH=arm64 + ISO_KERNEL=linux-aarch64 + ;; + *) + ISO_ARCH=x86_64 + ISO_NODE_ARCH=x64 + ISO_KERNEL=linux-t2 + ;; +esac + # Edge, dev, and local-source ISOs install the dev packages explicitly. Those # package recipes track the quattro branch. This avoids relying on pacman's # provides=omarchy resolution and shows the real package names being tested in @@ -30,7 +45,24 @@ pacman --noconfirm -Sy archlinux-keyring # so this container can be months behind the mirror it installs from. A plain # -Sy install is then a partial upgrade — new packages linked against a glibc # the container doesn't have yet. -pacman --noconfirm -Syu archiso git sudo base-devel jq grub imagemagick neovim nodejs npm tree-sitter-cli +pacman --noconfirm -Syu git sudo base-devel jq grub imagemagick neovim nodejs npm tree-sitter-cli + +# Prefer the archiso package. Not every repository carries it -- Arch Linux ARM +# has no archiso -- so fall back to the vendored submodule, which is the same +# source the package is built from. +if ! pacman --noconfirm -S --needed archiso; then + echo "archiso package unavailable; installing from the vendored submodule" + # The package would have pulled these in; installing from source does not: + # mksquashfs, mkfs.fat, mmd/mcopy, xorriso, mkfs.erofs, pacstrap. + pacman --noconfirm -S --needed \ + squashfs-tools dosfstools mtools libisoburn erofs-utils arch-install-scripts e2fsprogs + # /archiso is mounted read-only, so build out of a copy. scripts+profiles only: + # the default install target runs rst2man (python-docutils) for man pages, + # which this build has no use for. + cp -r /archiso /tmp/archiso-src + make -C /tmp/archiso-src PREFIX=/usr install-scripts install-profiles +fi +command -v mkarchiso # Pre-import the omarchy signing key (so pacman trusts our [omarchy] repo # during the build without keyserver lookups). @@ -50,6 +82,44 @@ fi # Build locations build_cache_dir=/var/cache + +# ---------------------------------------------------------------- aarch64 +# Packages in the lists above that do not exist for aarch64. pacman -Syw aborts +# the entire transaction on the first "target not found", so these must be +# removed rather than tolerated. +# +# Grouped by *why*, because "not on ARM" covers three very different cases and +# only the third is a gap worth closing. +OMARCHY_ARCH_DROP=( + # 1. x86/x86_64 platform hardware. Meaningless on ARM, not a gap. + amd-ucode intel-ucode + apple-bcm-firmware apple-t2-audio-config t2fanrd linux-t2 linux-t2-headers + macbook12-spi-driver-dkms macbook8-spi-pxa2xx-nodma-dkms + asusctl supergfxctl rog-control-center + dell-xps-touchpad-haptics tuxedo-drivers-nocompatcheck-dkms + intel-ipu7-camera intel-lpmd intel-media-driver libva-intel-driver + thermald linux-ptl linux-ptl-headers vpl-gpu-rt libvpl + vulkan-intel vulkan-radeon + nvidia-dkms nvidia-open-dkms nvidia-utils nvidia-580xx-dkms nvidia-580xx-utils + libva-nvidia-driver lib32-nvidia-utils lib32-nvidia-580xx-utils + broadcom-wl yt6801-dkms qmk-hid xpadneo-dkms + edk2-shell memtest86+ memtest86+-efi syslinux refind + + # 2. x86-only virtualisation guest tooling. + hyperv open-vm-tools virtualbox-guest-utils-nox qemu-user-static-binfmt + + # 3. Software with no aarch64 build. Unlike the categories above, these are + # real gaps rather than things that are meaningless on ARM. + obs-studio obsidian pinta dotnet-runtime asdcontrol + + # 4. Build artefacts / not carried by ALARM. + yay-debug reflector +) + +# quickshell-git is a git snapshot of 0.3.0; ALARM ships a tagged quickshell +# 0.3.1, which is newer and satisfies the same dependency. +OMARCHY_ARCH_SUBST_FROM=(quickshell-git mise) +OMARCHY_ARCH_SUBST_TO=(quickshell mise-bin) offline_mirror_dir="$build_cache_dir/airootfs/var/cache/omarchy/mirror/offline" mkdir -p "$build_cache_dir" "$offline_mirror_dir" @@ -107,8 +177,8 @@ fi # Node.js binary for offline mise install. NODE_DIST_URL="https://nodejs.org/dist/latest" NODE_SHASUMS=$(curl -fsSL "$NODE_DIST_URL/SHASUMS256.txt") -NODE_FILENAME=$(echo "$NODE_SHASUMS" | grep "linux-x64.tar.gz" | awk '{print $2}') -NODE_SHA=$(echo "$NODE_SHASUMS" | grep "linux-x64.tar.gz" | awk '{print $1}') +NODE_FILENAME=$(echo "$NODE_SHASUMS" | grep "linux-${ISO_NODE_ARCH}.tar.gz" | awk '{print $2}') +NODE_SHA=$(echo "$NODE_SHASUMS" | grep "linux-${ISO_NODE_ARCH}.tar.gz" | awk '{print $1}') curl -fsSL "$NODE_DIST_URL/$NODE_FILENAME" -o "/tmp/$NODE_FILENAME" echo "$NODE_SHA /tmp/$NODE_FILENAME" | sha256sum -c - mkdir -p "$build_cache_dir/airootfs/opt/packages/" @@ -118,8 +188,8 @@ cp "/tmp/$NODE_FILENAME" "$build_cache_dir/airootfs/opt/packages/" # The selected omarchy-settings package is needed here so its post_install hook # drops Omarchy's plymouthd.conf into /etc/plymouth before mkarchiso builds the # live initramfs. -arch_packages=(linux-t2 git gum jq openssl plymouth ttfx tzupdate omarchy-keyring "$OMARCHY_SETTINGS_PACKAGE" lvm2 cryptsetup parted) -printf '%s\n' "${arch_packages[@]}" >> "$build_cache_dir/packages.x86_64" +arch_packages=("$ISO_KERNEL" git gum jq openssl plymouth ttfx tzupdate omarchy-keyring "$OMARCHY_SETTINGS_PACKAGE" lvm2 cryptsetup parted) +printf '%s\n' "${arch_packages[@]}" >> "$build_cache_dir/packages.$ISO_ARCH" # The live ISO boots linux-t2 (see airootfs/etc/mkinitcpio.d/linux-t2.preset), so # stock linux is a second kernel nobody boots: ~147MB of ISO, plus its own archiso @@ -132,7 +202,10 @@ printf '%s\n' "${arch_packages[@]}" >> "$build_cache_dir/packages.x86_64" # install is entirely offline and the live environment needs no Wi-Fi driver. # # Anchored so linux-t2 and linux-firmware are untouched. -sed -i -E '/^(linux|broadcom-wl)$/d' "$build_cache_dir/packages.x86_64" +_drop_re='^(linux|broadcom-wl)$' +# ALARM's releng list also carries linux-firmware-marvell, which stock Arch does not. +[[ $ISO_ARCH == aarch64 ]] && _drop_re='^(linux|linux-firmware-marvell|broadcom-wl)$' +sed -i -E "/$_drop_re/d" "$build_cache_dir/packages.$ISO_ARCH" # Build the offline mirror: everything pacstrap might want during the target # install. With --local-source, the omarchy* packages we just built are @@ -148,7 +221,7 @@ else rm -rf "$bootstrap_cache_dir" /tmp/offlinedb-bootstrap /tmp/omarchy-pkglists mkdir -p "$bootstrap_cache_dir" /tmp/offlinedb-bootstrap pacman --config /configs/pacman-online-${OMARCHY_MIRROR}.conf --noconfirm -Syw "$OMARCHY_RUNTIME_PACKAGE" --cachedir "$bootstrap_cache_dir" --dbpath /tmp/offlinedb-bootstrap >/dev/null - omarchy_pkg=$(find "$bootstrap_cache_dir" -maxdepth 1 -type f -name "$OMARCHY_RUNTIME_PACKAGE-*.pkg.tar.zst" | sort | head -1) + omarchy_pkg=$(find "$bootstrap_cache_dir" -maxdepth 1 -type f \( -name "$OMARCHY_RUNTIME_PACKAGE-*.pkg.tar.zst" -o -name "$OMARCHY_RUNTIME_PACKAGE-*.pkg.tar.xz" \) | sort | head -1) if [[ -z $omarchy_pkg ]]; then echo "ERROR: downloaded package for $OMARCHY_RUNTIME_PACKAGE not found in $bootstrap_cache_dir" >&2 exit 1 @@ -187,19 +260,79 @@ if [[ ! -f $setup_form ]]; then fi cp "$setup_form" "$build_cache_dir/airootfs/usr/share/omarchy-iso/setup-form.sh" +# ---------------------------------------------------------------- aarch64 +# Apply the same architecture filter to the lists this ISO ships. These are read +# both by resolve_expected_packages() below and by the installer at runtime, so +# leaving them unfiltered yields an ISO that builds cleanly and then fails +# partway through a real install. +filter_shipped_package_list() { + local file="$1" tmp + [[ -f $file ]] || return 0 + tmp="$(mktemp)" + local line kept=0 dropped=0 + while IFS= read -r line || [[ -n $line ]]; do + if [[ -z $line || $line == \#* ]]; then printf '%s\n' "$line" >>"$tmp"; continue; fi + local skip="" + for d in "${OMARCHY_ARCH_DROP[@]}"; do [[ $line == "$d" ]] && { skip=1; break; }; done + if [[ -n $skip ]]; then dropped=$((dropped+1)); continue; fi + for i in "${!OMARCHY_ARCH_SUBST_FROM[@]}"; do + [[ $line == "${OMARCHY_ARCH_SUBST_FROM[$i]}" ]] && line="${OMARCHY_ARCH_SUBST_TO[$i]}" + done + printf '%s\n' "$line" >>"$tmp"; kept=$((kept+1)) + done <"$file" + mv "$tmp" "$file" + echo "aarch64: $(basename "$file"): kept $kept, dropped $dropped" +} + +if [[ $(uname -m) == aarch64 ]]; then + ARCHINSTALL_PACKAGES="$build_cache_dir/builder/archinstall.packages" + # The live environment's own list, which mkarchiso pacstraps directly. Missing + # this one fails the build late, at "Failed to install packages to new root". + filter_shipped_package_list "$build_cache_dir/packages.$ISO_ARCH" + filter_shipped_package_list "$build_cache_dir/airootfs/usr/share/omarchy-iso/omarchy-base.packages" + filter_shipped_package_list "$build_cache_dir/airootfs/usr/share/omarchy-iso/omarchy-other.packages" + # archinstall.packages is read from /builder (read-only mount); stage a copy + # so the filtered version is what gets resolved. + mkdir -p "$build_cache_dir/builder" + cp /builder/archinstall.packages "$build_cache_dir/builder/archinstall.packages" + filter_shipped_package_list "$build_cache_dir/builder/archinstall.packages" +fi + # Collect every package we want available in the offline mirror. declare -a all_packages mapfile -t all_packages < <( { - cat "$build_cache_dir/packages.x86_64" + cat "$build_cache_dir/packages.$ISO_ARCH" grep -hv '^#\|^$' "${base_pkg_lists[@]}" - grep -hv '^#\|^$' /builder/archinstall.packages + grep -hv '^#\|^$' "${ARCHINSTALL_PACKAGES:-/builder/archinstall.packages}" # Always include the selected Omarchy packages so the target install can # find the runtime and companion packages in the offline mirror. printf '%s\n' "$OMARCHY_RUNTIME_PACKAGE" "$OMARCHY_SETTINGS_PACKAGE" "$OMARCHY_NVIM_PACKAGE" } | sort -u ) +if [[ $(uname -m) == aarch64 ]]; then + declare -a _kept=() _dropped=() + for _p in "${all_packages[@]}"; do + _skip="" + for _d in "${OMARCHY_ARCH_DROP[@]}"; do + [[ $_p == "$_d" ]] && { _skip=1; break; } + done + if [[ -n $_skip ]]; then _dropped+=("$_p"); continue; fi + for _i in "${!OMARCHY_ARCH_SUBST_FROM[@]}"; do + if [[ $_p == "${OMARCHY_ARCH_SUBST_FROM[$_i]}" ]]; then + _p="${OMARCHY_ARCH_SUBST_TO[$_i]}" + echo "aarch64: substituting ${OMARCHY_ARCH_SUBST_FROM[$_i]} -> $_p" + fi + done + _kept+=("$_p") + done + mapfile -t all_packages < <(printf '%s\n' "${_kept[@]}" | sort -u) + echo "aarch64: dropped ${#_dropped[@]} package(s) with no aarch64 build:" + printf ' %s\n' "${_dropped[@]}" + echo "aarch64: ${#all_packages[@]} package(s) remain for the offline mirror" +fi + # With --local-source we already built these omarchy* packages directly into # the mirror; strip them from the pacman -Syw list so it doesn't try to fetch # the published versions on top. @@ -272,7 +405,8 @@ printf '%s\n' "${required_package_files[@]}" | # Rebuild the offline repo db from scratch so size/checksum/depends entries # always reflect only the package files selected for this build. rm -f "$offline_mirror_dir"/offline.db* "$offline_mirror_dir"/offline.files* -repo-add "$offline_mirror_dir/offline.db.tar.gz" "$offline_mirror_dir/"*.pkg.tar.zst +repo-add "$offline_mirror_dir/offline.db.tar.gz" \ + "$offline_mirror_dir/"*.pkg.tar.zst "$offline_mirror_dir/"*.pkg.tar.xz # mkarchiso expects the mirror at /var/cache/omarchy/mirror/offline inside the # container (the airootfs path); symlink rather than duplicate. @@ -299,7 +433,7 @@ resolve_expected_packages() { mapfile -t targets < <( { - grep -hv '^#\|^$' /builder/archinstall.packages + grep -hv '^#\|^$' "${ARCHINSTALL_PACKAGES:-/builder/archinstall.packages}" # Read the shipped copy, which is what _runtime_package_list reads at # install time, not the build-time source it came from. grep -hv '^#\|^$' \ diff --git a/builder/build-omarchy-packages.sh b/builder/build-omarchy-packages.sh index 5f7173fb..2f878a82 100755 --- a/builder/build-omarchy-packages.sh +++ b/builder/build-omarchy-packages.sh @@ -70,7 +70,8 @@ for pkg in "${packages[@]}"; do done mkdir -p "$offline_mirror_dir" -for package_file in "$work_dir"/*.pkg.tar.zst; do +for package_file in "$work_dir"/*.pkg.tar.zst "$work_dir"/*.pkg.tar.xz; do + [[ -e $package_file ]] || continue destination="$offline_mirror_dir/$(basename "$package_file")" # A cached signature belongs to the previously downloaded or locally built @@ -82,4 +83,4 @@ done echo echo "Built Omarchy packages, placed in $offline_mirror_dir:" -ls "$offline_mirror_dir"/omarchy*.pkg.tar.zst | sed 's|^| |' +ls "$offline_mirror_dir"/omarchy*.pkg.tar.{zst,xz} 2>/dev/null | sed 's|^| |' diff --git a/configs/airootfs/root/.automated_script.sh b/configs/airootfs/root/.automated_script.sh index 16904f87..b8823b80 100644 --- a/configs/airootfs/root/.automated_script.sh +++ b/configs/airootfs/root/.automated_script.sh @@ -83,7 +83,7 @@ warm_offline_mirror() { ((spent_kb + size_kb > budget_kb)) && continue cat -- "$path" >/dev/null 2>&1 || true spent_kb=$((spent_kb + size_kb)) - done < <(du -k "$mirror"/*.pkg.tar.zst 2>/dev/null | sort -rn) + done < <(du -k "$mirror"/*.pkg.tar.zst "$mirror"/*.pkg.tar.xz 2>/dev/null | sort -rn) } warm_offline_mirror & diff --git a/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py b/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py index 6d486245..7b031f3a 100644 --- a/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py +++ b/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py @@ -24,6 +24,7 @@ import hashlib import os +import platform import re import shutil import subprocess @@ -1160,6 +1161,10 @@ def run_system_finalizer(ctx: InstallContext) -> None: PROVISION_STATE_DIR = "var/lib/omarchy/provisioning" PROVISION_KEYFILE = "etc/omarchy/provisioning.key" NODE_PACKAGES_DIR = Path("/opt/packages") +# Node names its builds x64 / arm64 rather than by uname, and the ISO bundles the +# one matching the architecture it was built for. +_NODE_ARCH = {"x86_64": "x64", "aarch64": "arm64"}.get(platform.machine(), "x64") +NODE_TARBALL_GLOB = f"node-v*-linux-{_NODE_ARCH}.tar.gz" def stage_provisioning_state(ctx: InstallContext) -> None: @@ -1200,7 +1205,7 @@ def stage_provisioning_state(ctx: InstallContext) -> None: def _stage_node_tarball(ctx: InstallContext, provisioning_dir) -> None: - tarballs = sorted(NODE_PACKAGES_DIR.glob("node-v*-linux-x64.tar.gz")) + tarballs = sorted(NODE_PACKAGES_DIR.glob(NODE_TARBALL_GLOB)) if not tarballs: # Hard error on every install, not just deferred-provisioning installs: the stash is what lets a # later factory reset finalize the next owner offline, and an ISO From d1fd5d24da8edf87dc49a14c87b98974af9100c1 Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 24 Aug 2026 21:44:26 +0800 Subject: [PATCH 02/11] configs: filter x86-only repositories out of the pacman config on aarch64 --- builder/build-iso.sh | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/builder/build-iso.sh b/builder/build-iso.sh index 344298c2..ea646b39 100755 --- a/builder/build-iso.sh +++ b/builder/build-iso.sh @@ -70,14 +70,39 @@ pacman-key --add /builder/omarchy.gpg pacman-key --lsign-key 40DFB630FF42BCFFB047046CF0134EE680CAC571 # omarchy-keyring is needed inside the offline mirror too. -pacman --config /configs/pacman-online-${OMARCHY_MIRROR}.conf --noconfirm -Sy omarchy-keyring +# The checked-in pacman configs describe an x86_64 build. Rather than edit them, +# stage a filtered copy for aarch64 and use that; x86_64 keeps reading the +# originals untouched. +# +# [multilib] 32-bit x86 libraries. No ARM mirror carries it. +# [arch-mact2] an Apple T2 repository, x86 by definition. +# core/extra the explicit Server line points at Omarchy's mirror of Arch, +# which is x86_64-only. Both sections already Include the +# mirrorlist, so dropping the line is enough. Substituting +# another URL would not work: Arch Linux ARM lays its tree out +# as $arch/$repo rather than Arch's $repo/os/$arch. +PACMAN_ONLINE_CONF="/configs/pacman-online-${OMARCHY_MIRROR}.conf" +if [[ $ISO_ARCH == aarch64 ]]; then + PACMAN_ONLINE_CONF="/tmp/pacman-online-${OMARCHY_MIRROR}.conf" + awk ' + /^\[multilib\]$/ { skip = 1; next } + /^\[arch-mact2\]$/ { skip = 1; next } + /^\[/ { skip = 0; section = $0 } + skip { next } + (section == "[core]" || section == "[extra]") && /^Server[[:space:]]*=/ { next } + { print } + ' "/configs/pacman-online-${OMARCHY_MIRROR}.conf" > "$PACMAN_ONLINE_CONF" + echo "aarch64: staged $PACMAN_ONLINE_CONF without [multilib]/[arch-mact2]" +fi + +pacman --config $PACMAN_ONLINE_CONF --noconfirm -Sy omarchy-keyring pacman-key --populate omarchy # Append the [omarchy] repo to the container's /etc/pacman.conf so subsequent # tools (notably makepkg in build-omarchy-packages.sh) can resolve omarchy- # only build deps like limine-snapper-sync and limine-mkinitcpio-hook. if ! grep -q '^\[omarchy\]' /etc/pacman.conf; then - awk '/^\[omarchy\]/,/^$/' /configs/pacman-online-${OMARCHY_MIRROR}.conf >> /etc/pacman.conf + awk '/^\[omarchy\]/,/^$/' $PACMAN_ONLINE_CONF >> /etc/pacman.conf fi # Build locations @@ -220,7 +245,7 @@ else bootstrap_cache_dir=/tmp/omarchy-pkg-bootstrap rm -rf "$bootstrap_cache_dir" /tmp/offlinedb-bootstrap /tmp/omarchy-pkglists mkdir -p "$bootstrap_cache_dir" /tmp/offlinedb-bootstrap - pacman --config /configs/pacman-online-${OMARCHY_MIRROR}.conf --noconfirm -Syw "$OMARCHY_RUNTIME_PACKAGE" --cachedir "$bootstrap_cache_dir" --dbpath /tmp/offlinedb-bootstrap >/dev/null + pacman --config $PACMAN_ONLINE_CONF --noconfirm -Syw "$OMARCHY_RUNTIME_PACKAGE" --cachedir "$bootstrap_cache_dir" --dbpath /tmp/offlinedb-bootstrap >/dev/null omarchy_pkg=$(find "$bootstrap_cache_dir" -maxdepth 1 -type f \( -name "$OMARCHY_RUNTIME_PACKAGE-*.pkg.tar.zst" -o -name "$OMARCHY_RUNTIME_PACKAGE-*.pkg.tar.xz" \) | sort | head -1) if [[ -z $omarchy_pkg ]]; then echo "ERROR: downloaded package for $OMARCHY_RUNTIME_PACKAGE not found in $bootstrap_cache_dir" >&2 @@ -348,7 +373,7 @@ fi mkdir -p /tmp/offlinedb download_offline_packages() { - pacman --config /configs/pacman-online-${OMARCHY_MIRROR}.conf --noconfirm -Syw \ + pacman --config $PACMAN_ONLINE_CONF --noconfirm -Syw \ "${all_packages[@]}" --cachedir "$offline_mirror_dir/" --dbpath /tmp/offlinedb --needed } @@ -366,7 +391,7 @@ fi # newest version of every cached package name) removes packages that have left # the lists or dependency closure, such as an old Electron major version. if ! resolved_package_files="$( - pacman --config "/configs/pacman-online-${OMARCHY_MIRROR}.conf" --noconfirm \ + pacman --config "$PACMAN_ONLINE_CONF" --noconfirm \ --dbpath /tmp/offlinedb -S --print --print-format '%f' "${all_packages[@]}" )"; then echo "ERROR: could not resolve the package files required by the offline mirror" >&2 From 98bba10332004ad876198777f5c9bbd68b672a02 Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 24 Aug 2026 22:18:29 +0800 Subject: [PATCH 03/11] boot: make the live ISO bootable on aarch64 --- builder/build-iso.sh | 34 ++++++++++++++++ configs/aarch64/customize_airootfs.sh | 57 +++++++++++++++++++++++++++ configs/aarch64/linux.preset | 5 +++ configs/aarch64/zz-aarch64-live.conf | 32 +++++++++++++++ configs/profiledef.sh | 35 ++++++++++++++-- 5 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 configs/aarch64/customize_airootfs.sh create mode 100644 configs/aarch64/linux.preset create mode 100644 configs/aarch64/zz-aarch64-live.conf diff --git a/builder/build-iso.sh b/builder/build-iso.sh index ea646b39..3f3c7814 100755 --- a/builder/build-iso.sh +++ b/builder/build-iso.sh @@ -159,6 +159,40 @@ rm -rf "$build_cache_dir/airootfs/etc/xdg/reflector" # Bring in our archiso profile additions. cp -r /configs/* "$build_cache_dir/" + +# The GRUB configs name the kernel and initramfs they boot. That name follows the +# live kernel package, so substitute it here rather than keeping a second copy of +# each file. Both grub.cfg and loopback.cfg carry those names -- loopback.cfg is +# what boots the ISO from a file on disk, and missing it leaves that path +# requesting a kernel the ISO does not contain. On x86_64 ISO_KERNEL is linux-t2 +# and this is a no-op. +for _grub_cfg in "$build_cache_dir"/grub/*.cfg; do + [[ -e $_grub_cfg ]] || continue + sed -i \ + -e "s|vmlinuz-linux-t2|vmlinuz-${ISO_KERNEL}|g" \ + -e "s|initramfs-linux-t2\\.img|initramfs-${ISO_KERNEL}.img|g" \ + "$_grub_cfg" +done + +# Live-ISO mkinitcpio overrides that only make sense on aarch64. They are kept +# out of configs/airootfs/ so an x86_64 build never ships them, and staged into +# the airootfs here instead. They must exist before pacstrap runs mkinitcpio's +# pacman hook, which is why customize_airootfs.sh cannot handle those two. +if [[ $ISO_ARCH == aarch64 ]]; then + install -Dm644 /configs/aarch64/zz-aarch64-live.conf \ + "$build_cache_dir/airootfs/etc/mkinitcpio.conf.d/zz-aarch64-live.conf" + install -Dm644 /configs/aarch64/linux.preset \ + "$build_cache_dir/airootfs/etc/mkinitcpio.d/linux.preset" + install -Dm755 /configs/aarch64/customize_airootfs.sh \ + "$build_cache_dir/airootfs/root/customize_airootfs.sh" + # The T2 preset has no kernel image on this architecture, and pacstrap's + # mkinitcpio hook would fail on it before customize_airootfs.sh can run: + # ERROR: Invalid option -k -- '/boot/vmlinuz-linux-t2' must be readable + rm -f "$build_cache_dir/airootfs/etc/mkinitcpio.d/linux-t2.preset" + echo "aarch64: staged live-ISO mkinitcpio overrides" +fi +# configs/aarch64/ is a staging directory, not part of the airootfs. +rm -rf "$build_cache_dir/aarch64" mkdir -p "$build_cache_dir/airootfs/usr/share/omarchy-iso" echo "$OMARCHY_MIRROR" > "$build_cache_dir/airootfs/root/omarchy_mirror" echo "$OMARCHY_ISO_REF" > "$build_cache_dir/airootfs/root/omarchy_iso_ref" diff --git a/configs/aarch64/customize_airootfs.sh b/configs/aarch64/customize_airootfs.sh new file mode 100644 index 00000000..c1a9311b --- /dev/null +++ b/configs/aarch64/customize_airootfs.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Runs inside the live-ISO chroot after packages are installed. +set -uo pipefail + +# Everything here addresses differences in Arch Linux ARM's kernel packaging. +# On any other architecture this script is a no-op. +if [[ $(uname -m) != aarch64 ]]; then + exit 0 +fi + +# Omarchy forces the thunderbolt module; ALARM's aarch64 kernel does not build +# it, and mkinitcpio treats an unresolvable MODULES entry as an error. +if [[ -f /etc/mkinitcpio.conf.d/thunderbolt_module.conf ]]; then + sed -i 's/^MODULES+=(thunderbolt)/#MODULES+=(thunderbolt) # not built for aarch64/' \ + /etc/mkinitcpio.conf.d/thunderbolt_module.conf +fi + +# ALARM's linux-aarch64 installs /boot/Image rather than /boot/vmlinuz-. +# Give the preset the filename it expects. +if [[ -e /boot/Image && ! -e /boot/vmlinuz-linux-aarch64 ]]; then + cp -a /boot/Image /boot/vmlinuz-linux-aarch64 +fi + +# Drop presets whose kernel image is absent: `mkinitcpio -P` aborts on them. +for preset in /etc/mkinitcpio.d/*.preset; do + [[ -e $preset ]] || continue + kname="$(basename "$preset" .preset)" + if [[ $kname != linux-aarch64 && ! -e /boot/vmlinuz-$kname ]]; then + echo "customize_airootfs: dropping $kname.preset (no kernel image)" + rm -f "$preset" + fi +done + +# The equivalent of x86's linux-t2.preset: archiso_config makes mkinitcpio read +# archiso.conf as its configuration, bypassing /etc/mkinitcpio.conf.d entirely. +# That is what stops omarchy_hooks.conf -- which describes an *installed* +# system -- from replacing the archiso hooks in the *live* initramfs. +cat > /etc/mkinitcpio.d/linux-aarch64.preset <<'PRESET' +# Live-ISO preset for Arch Linux ARM's linux-aarch64. +PRESETS=('archiso') + +ALL_kver='/boot/vmlinuz-linux-aarch64' +archiso_config='/etc/mkinitcpio.conf.d/archiso.conf' + +archiso_image="/boot/initramfs-linux-aarch64.img" +PRESET + +echo "customize_airootfs: building the live initramfs from archiso.conf" +rm -f /boot/initramfs-linux*.img +mkinitcpio -p linux-aarch64 || echo "customize_airootfs: WARNING mkinitcpio failed" +if compgen -G "/boot/initramfs-*.img" >/dev/null; then + echo "customize_airootfs: initramfs present" +else + echo "customize_airootfs: ERROR no initramfs produced" +fi + +exit 0 diff --git a/configs/aarch64/linux.preset b/configs/aarch64/linux.preset new file mode 100644 index 00000000..188d28ec --- /dev/null +++ b/configs/aarch64/linux.preset @@ -0,0 +1,5 @@ +# The live ISO boots linux-aarch64 (/boot/Image). A stock linux.preset points at +# /boot/vmlinuz-linux, which does not exist here, and `mkinitcpio -P` fails with +# ERROR: Invalid option -k -- '/boot/vmlinuz-linux' must be readable +# An empty PRESETS array makes this preset a no-op. +PRESETS=() diff --git a/configs/aarch64/zz-aarch64-live.conf b/configs/aarch64/zz-aarch64-live.conf new file mode 100644 index 00000000..1933f073 --- /dev/null +++ b/configs/aarch64/zz-aarch64-live.conf @@ -0,0 +1,32 @@ +# Live-ISO overrides. Sorts last in /etc/mkinitcpio.conf.d, so it has the final +# word, and is unowned by any package so pacman cannot clobber it. +# +# WHY THIS EXISTS +# --------------- +# archiso.conf sets the HOOKS needed to boot a live ISO: +# base udev microcode modconf kms memdisk archiso archiso_loop_mnt +# archiso_pxe_* block filesystems keyboard +# omarchy-settings then ships omarchy_hooks.conf, which sets the HOOKS for an +# *installed* system (autodetect, encrypt, fsck, btrfs-overlayfs...). +# +# "omarchy_hooks.conf" sorts after "archiso.conf", so it wins -- and the live +# initramfs ends up with no `archiso` hook. The ISO then boots the kernel and +# panics in the initramfs: +# ERROR: Failed to mount '' on real root +# Restore the live hooks, keeping Omarchy's plymouth (the ISO installs +# omarchy-settings specifically so its plymouthd.conf lands before mkinitcpio). +# +# `microcode` is omitted: on aarch64 that hook only emits +# WARNING: architecture 'aarch64' not supported, skipping hook +HOOKS=(base udev plymouth modconf kms memdisk archiso archiso_loop_mnt + archiso_pxe_common archiso_pxe_nbd archiso_pxe_http archiso_pxe_nfs + block filesystems keyboard) + +# thunderbolt is x86-oriented hardware; ALARM's aarch64 kernel does not build +# the module, and a missing MODULES entry is fatal to mkinitcpio. +_keep=() +for _m in "${MODULES[@]:-}"; do + [[ $_m == "thunderbolt" || -z $_m ]] || _keep+=("$_m") +done +MODULES=("${_keep[@]}") +unset _keep _m diff --git a/configs/profiledef.sh b/configs/profiledef.sh index d7d483cf..ae24da16 100644 --- a/configs/profiledef.sh +++ b/configs/profiledef.sh @@ -8,8 +8,17 @@ iso_application="Omarchy Installer" iso_version="$(date --date="@${SOURCE_DATE_EPOCH:-$(date +%s)}" +%Y.%m.%d)" install_dir="arch" buildmodes=('iso') -bootmodes=('bios.syslinux' 'uefi.grub') -arch="x86_64" +case "$(uname -m)" in + aarch64) + arch="aarch64" + # ARM has no legacy BIOS; syslinux is x86-only. + bootmodes=('uefi.grub') + ;; + *) + arch="x86_64" + bootmodes=('bios.syslinux' 'uefi.grub') + ;; +esac pacman_conf="pacman-offline.conf" airootfs_image_type="squashfs" # Package archives in the offline mirror are already zstd-compressed. Storing @@ -22,9 +31,20 @@ airootfs_image_type="squashfs" # cold on every boot: kernel, plymouth, systemd, python, archinstall, gum. The # whole ISO grows well under a percent for it, and dropping the x86 BCJ filter # also removes one of the blockers listed in plans/aarch64-support.md. +# Arch Linux ARM builds its kernel without CONFIG_SQUASHFS_ZSTD (ZLIB, LZ4 and +# XZ only), so a zstd airootfs builds correctly and then cannot be mounted by +# the very kernel on the ISO: +# mount: /run/archiso/airootfs: fsconfig() failed: +# Filesystem uses "zstd" compression. This is not supported. +# Arch's kernel does enable it, so zstd remains the default there and the +# measured rationale above is unaffected. +if [[ $arch == aarch64 ]]; then + _airootfs_comp=('-comp' 'xz' '-Xbcj' 'arm') +else + _airootfs_comp=('-comp' 'zstd' '-Xcompression-level' '19') +fi airootfs_image_tool_options=( - '-comp' 'zstd' - '-Xcompression-level' '19' + "${_airootfs_comp[@]}" '-b' '1M' '-action' 'uncompressed@subpathname(var/cache/omarchy/mirror/offline)' ) @@ -44,3 +64,10 @@ file_permissions=( ["/usr/local/bin/omarchy-upload-log"]="0:0:755" ["/var/cache/omarchy/mirror/offline/"]="0:0:775" ) + +# Staged into the airootfs by build-iso.sh on aarch64 only. +if [[ $arch == aarch64 ]]; then + file_permissions["/etc/mkinitcpio.conf.d/zz-aarch64-live.conf"]="0:0:644" + file_permissions["/etc/mkinitcpio.d/linux.preset"]="0:0:644" + file_permissions["/root/customize_airootfs.sh"]="0:0:755" +fi From ca64a862cf4569c34b49e3111931bba3cb4eaf1e Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 24 Aug 2026 22:18:29 +0800 Subject: [PATCH 04/11] installer: fix the phases that assume an x86 target --- configs/airootfs/root/configurator | 22 ++++++++++++-- .../share/omarchy-iso/orchestrator/context.py | 17 ++++++++++- .../omarchy-iso/orchestrator/phases_impl.py | 30 ++++++++++++++----- 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/configs/airootfs/root/configurator b/configs/airootfs/root/configurator index 4cd3db3d..fb949142 100644 --- a/configs/airootfs/root/configurator +++ b/configs/airootfs/root/configurator @@ -9,6 +9,18 @@ OMARCHY_SETTINGS_PACKAGE="${OMARCHY_SETTINGS_PACKAGE:-omarchy-settings}" LOGO_PATH="$OMARCHY_PATH/logo.txt" +# Limine installs BOOT.EFI as limine_.efi. The Arch package ships +# every architecture's binary, so the x64 name resolves on ARM too and the +# install silently registers an unbootable x86-64 loader. Derive it instead. +case "$(uname -m)" in + x86_64) limine_efi_binary="limine_x64.efi" ;; + i?86) limine_efi_binary="limine_ia32.efi" ;; + aarch64) limine_efi_binary="limine_aa64.efi" ;; + riscv64) limine_efi_binary="limine_riscv64.efi" ;; + loongarch64) limine_efi_binary="limine_loongarch64.efi" ;; + *) limine_efi_binary="limine_x64.efi" ;; +esac + # The setup form — keyboard, account, hostname, and timezone questions, plus the # rules their answers are checked against — shared verbatim with the first-boot # owner setup that finishes a deferred install. build-iso.sh vendors it out of @@ -412,6 +424,12 @@ to_gb() { # T2 Macs need their own kernel for keyboard/wifi drivers. detect_kernel() { + # Arch Linux ARM ships no bare `linux` package -- its kernel is + # `linux-aarch64`, and that is also the pkgbase every bootloader hook and + # validate_boot() looks for. T2 detection is x86-only by construction. + case "$(uname -m)" in + aarch64) echo "linux-aarch64"; return ;; + esac if lspci -nn 2>/dev/null | grep -q "106b:180[12]"; then echo "linux-t2" else @@ -805,7 +823,7 @@ run_partition_execute() { "boot": { "esp_mount": "$esp_mount_in_target", "esp_path": "/EFI/limine", - "efi_binary": "limine_x64.efi", + "efi_binary": "$limine_efi_binary", "enable_fallback": false }, "storage": { @@ -1144,7 +1162,7 @@ cat <<-_EOF_ >user_configuration.json "boot": { "esp_mount": "/boot", "esp_path": "/EFI/limine", - "efi_binary": "limine_x64.efi", + "efi_binary": "$limine_efi_binary", "enable_fallback": true }, "storage": { diff --git a/configs/airootfs/usr/share/omarchy-iso/orchestrator/context.py b/configs/airootfs/usr/share/omarchy-iso/orchestrator/context.py index 9b5c50ff..2e0b7ff7 100644 --- a/configs/airootfs/usr/share/omarchy-iso/orchestrator/context.py +++ b/configs/airootfs/usr/share/omarchy-iso/orchestrator/context.py @@ -6,11 +6,26 @@ import json import os +import platform import secrets from dataclasses import dataclass, field from pathlib import Path from typing import Any +# Limine ships BOOT{X64,IA32,AA64,RISCV64,LOONGARCH64}.EFI and the Arch package +# installs *all* of them, so hardcoding the x64 name does not fail on ARM -- it +# silently installs an x86-64 binary the firmware cannot execute. Derive the +# name instead, matching limine-common-functions' limine_efi_arch(). +_LIMINE_EFI_ARCH = { + "x86_64": "X64", + "i686": "IA32", + "aarch64": "AA64", + "riscv64": "RISCV64", + "loongarch64": "LOONGARCH64", +}.get(platform.machine(), "X64") +_LIMINE_SOURCE_EFI = f"BOOT{_LIMINE_EFI_ARCH}.EFI" +_LIMINE_EFI_BINARY = f"limine_{_LIMINE_EFI_ARCH.lower()}.efi" + @dataclass class InstallContext: @@ -184,7 +199,7 @@ def _default_omarchy_install(user_configuration: dict) -> dict[str, Any]: "boot": { "esp_mount": "/boot", "esp_path": "/EFI/limine", - "efi_binary": "limine_x64.efi", + "efi_binary": _LIMINE_EFI_BINARY, "enable_fallback": mode == "full_disk", }, "storage": {}, diff --git a/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py b/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py index 7b031f3a..16c7a06f 100644 --- a/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py +++ b/configs/airootfs/usr/share/omarchy-iso/orchestrator/phases_impl.py @@ -39,6 +39,20 @@ from .keyboard import configure_keyboard from .ui import error, info +# Limine ships BOOT{X64,IA32,AA64,RISCV64,LOONGARCH64}.EFI and the Arch package +# installs *all* of them, so hardcoding the x64 name does not fail on ARM -- it +# silently installs an x86-64 binary the firmware cannot execute. Derive the +# name instead, matching limine-common-functions' limine_efi_arch(). +_LIMINE_EFI_ARCH = { + "x86_64": "X64", + "i686": "IA32", + "aarch64": "AA64", + "riscv64": "RISCV64", + "loongarch64": "LOONGARCH64", +}.get(platform.machine(), "X64") +_LIMINE_SOURCE_EFI = f"BOOT{_LIMINE_EFI_ARCH}.EFI" +_LIMINE_EFI_BINARY = f"limine_{_LIMINE_EFI_ARCH.lower()}.efi" + # Package targets are written by builder/build-iso.sh. Stable ISOs use the # stable package names, while dev/local-source ISOs install the dev package @@ -388,7 +402,7 @@ def _install_pre_mounted_limine(ctx: InstallContext) -> None: disk=Path(disk), part=part, esp_path=boot.get("esp_path", "/EFI/limine"), - efi_binary=boot.get("efi_binary", "limine_x64.efi"), + efi_binary=boot.get("efi_binary", _LIMINE_EFI_BINARY), pre_state=pre_state, ) @@ -406,15 +420,15 @@ def _install_limine_efi( part: int, removable: bool = False, esp_path: str = "/EFI/limine", - efi_binary: str = "limine_x64.efi", + efi_binary: str = _LIMINE_EFI_BINARY, pre_state: dict | None = None, ) -> None: if removable: esp_path = "/EFI/BOOT" - efi_binary = "BOOTX64.EFI" + efi_binary = _LIMINE_SOURCE_EFI limine_path = ctx.target / "usr" / "share" / "limine" - source_name = "BOOTX64.EFI" + source_name = _LIMINE_SOURCE_EFI target_dir = Path(esp_mount) / esp_path.lstrip("/") target_path = target_dir / efi_binary _copy_required(limine_path / source_name, ctx.target / target_path.relative_to("/")) @@ -756,7 +770,7 @@ def _boot_intent(ctx: InstallContext) -> dict: boot = dict(ctx.omarchy_install.get("boot") or {}) boot.setdefault("esp_mount", "/boot") boot.setdefault("esp_path", "/EFI/limine") - boot.setdefault("efi_binary", "limine_x64.efi") + boot.setdefault("efi_binary", _LIMINE_EFI_BINARY) boot.setdefault("enable_fallback", not ctx.is_protected) return boot @@ -1161,8 +1175,8 @@ def run_system_finalizer(ctx: InstallContext) -> None: PROVISION_STATE_DIR = "var/lib/omarchy/provisioning" PROVISION_KEYFILE = "etc/omarchy/provisioning.key" NODE_PACKAGES_DIR = Path("/opt/packages") -# Node names its builds x64 / arm64 rather than by uname, and the ISO bundles the -# one matching the architecture it was built for. +# Node ships x64/arm64 builds under different filenames; the ISO bundles the +# one matching the target architecture. _NODE_ARCH = {"x86_64": "x64", "aarch64": "arm64"}.get(platform.machine(), "x64") NODE_TARBALL_GLOB = f"node-v*-linux-{_NODE_ARCH}.tar.gz" @@ -1674,7 +1688,7 @@ def validate_boot(ctx: InstallContext) -> None: kernel = storage.get("kernel") or (ctx.user_configuration.get("kernels") or ["linux"])[0] if arch.has_uefi(): - limine_binary = esp_mount / boot.get("esp_path", "/EFI/limine").lstrip("/") / boot.get("efi_binary", "limine_x64.efi") + limine_binary = esp_mount / boot.get("esp_path", "/EFI/limine").lstrip("/") / boot.get("efi_binary", _LIMINE_EFI_BINARY) if not limine_binary.exists() or limine_binary.stat().st_size == 0: raise RuntimeError(f"{limine_binary} missing or empty") From ff59434a0dfb37080007ec00823824d4bf7bf9fb Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 24 Aug 2026 22:20:44 +0800 Subject: [PATCH 05/11] configs: add an aarch64 package list for the live environment mkarchiso reads packages.$arch from the profile directory. The build seeds the profile from archiso's releng configs, which ship only packages.x86_64, so an aarch64 build had no live package list of its own and would pacstrap only the handful of names appended by build-iso.sh. Derived from releng's packages.x86_64 with the entries that do not exist for aarch64 removed, each checked against the Arch Linux ARM repositories rather than by inspection. --- builder/build-iso.sh | 5 +- configs/packages.aarch64 | 128 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 configs/packages.aarch64 diff --git a/builder/build-iso.sh b/builder/build-iso.sh index 3f3c7814..5f82adea 100755 --- a/builder/build-iso.sh +++ b/builder/build-iso.sh @@ -361,7 +361,10 @@ fi declare -a all_packages mapfile -t all_packages < <( { - cat "$build_cache_dir/packages.$ISO_ARCH" + # mkarchiso allows comments and blank lines in packages.$arch, so strip them + # the same way the lists below are read. releng's own list carries none, + # which is why a bare cat worked until this profile shipped a commented one. + grep -hv '^#\|^$' "$build_cache_dir/packages.$ISO_ARCH" grep -hv '^#\|^$' "${base_pkg_lists[@]}" grep -hv '^#\|^$' "${ARCHINSTALL_PACKAGES:-/builder/archinstall.packages}" # Always include the selected Omarchy packages so the target install can diff --git a/configs/packages.aarch64 b/configs/packages.aarch64 new file mode 100644 index 00000000..68f401ac --- /dev/null +++ b/configs/packages.aarch64 @@ -0,0 +1,128 @@ +# Package list for the aarch64 live environment. +# +# Derived from archiso's releng packages.x86_64, with entries that do not exist +# for aarch64 removed: x86 microcode and boot tooling (intel-ucode, syslinux, +# refind, memtest86+, edk2-shell), x86-only guest tooling (hyperv, open-vm-tools, +# virtualbox-guest-utils-nox), broadcom-wl (a prebuilt module for the stock +# kernel), and packages Arch Linux ARM does not carry (reflector, +# ghostty-terminfo). The kernel itself comes from ISO_KERNEL in build-iso.sh. +# +# mkarchiso reads packages.$arch from the profile directory, so this overrides +# the releng copy seeded earlier in the build. +alsa-utils +amd-ucode +arch-install-scripts +archinstall +b43-fwcutter +base +bcachefs-tools +bind +bolt +brltty +btrfs-progs +clonezilla +cloud-init +cryptsetup +darkhttpd +ddrescue +dhcpcd +diffutils +dmidecode +dmraid +dnsmasq +dosfstools +e2fsprogs +efibootmgr +espeakup +ethtool +exfatprogs +f2fs-tools +fatresize +foot-terminfo +fsarchiver +gpart +gpm +gptfdisk +grml-zsh-config +grub +hdparm +irssi +iw +iwd +jfsutils +kitty-terminfo +kmscon-terminfo +ldns +less +lftp +libfido2 +libusb-compat +linux-atm +linux-firmware +linux-firmware-marvell +livecd-sounds +lsscsi +lvm2 +lynx +man-db +man-pages +mc +mdadm +mkinitcpio +mkinitcpio-archiso +mkinitcpio-nfs-utils +mmc-utils +modemmanager +mtools +nano +nbd +ndisc6 +nfs-utils +nilfs-utils +nmap +ntfsprogs +nvme-cli +open-iscsi +openconnect +openpgp-card-tools +openssh +openvpn +partclone +parted +partimage +pcsclite +ppp +pptpclient +pv +qemu-guest-agent +rsync +rxvt-unicode-terminfo +screen +sdparm +sequoia-sq +sg3_utils +smartmontools +sof-firmware +squashfs-tools +sudo +systemd-resolvconf +tcpdump +terminus-font +testdisk +tmux +tpm2-tools +tpm2-tss +udftools +usb_modeswitch +usbmuxd +usbutils +vim +vpnc +wireless-regdb +wireless_tools +wpa_supplicant +wvdial +xdg-utils +xfsprogs +xl2tpd +zsh From fd8dd8e3dab2c4c48274b7267ce8cabd1eb458bd Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 24 Aug 2026 23:28:40 +0800 Subject: [PATCH 06/11] boot: fail the build when the live initramfs is not produced customize_airootfs.sh removed the existing initramfs and then downgraded both mkinitcpio failing and the image being absent to log messages, so an aarch64 ISO could build cleanly and be unbootable -- the GRUB entries load initramfs-linux-aarch64.img by name. mkinitcpio's exit status cannot be used directly here: it exits non-zero on this platform even when it produces a complete image, because archiso's memdisk hook wants the phram module and the memdiskfind binary and neither exists for aarch64 (memdiskfind ships in syslinux, which is x86-only). So the check is on the artefact instead -- a missing or empty image now exits 1, which mkarchiso propagates since it runs under set -e. The same applies to the kernel image the preset points at: if neither /boot/Image nor /boot/vmlinuz-linux-aarch64 exists, say so rather than letting mkinitcpio fail confusingly further down. --- configs/aarch64/customize_airootfs.sh | 31 ++++++++++++++++++++------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/configs/aarch64/customize_airootfs.sh b/configs/aarch64/customize_airootfs.sh index c1a9311b..4f61f5c9 100644 --- a/configs/aarch64/customize_airootfs.sh +++ b/configs/aarch64/customize_airootfs.sh @@ -16,9 +16,15 @@ if [[ -f /etc/mkinitcpio.conf.d/thunderbolt_module.conf ]]; then fi # ALARM's linux-aarch64 installs /boot/Image rather than /boot/vmlinuz-. -# Give the preset the filename it expects. -if [[ -e /boot/Image && ! -e /boot/vmlinuz-linux-aarch64 ]]; then - cp -a /boot/Image /boot/vmlinuz-linux-aarch64 +# Give the preset the filename it expects. Everything below depends on this, so +# say so plainly rather than letting mkinitcpio fail confusingly later. +if [[ ! -e /boot/vmlinuz-linux-aarch64 ]]; then + if [[ -e /boot/Image ]]; then + cp -a /boot/Image /boot/vmlinuz-linux-aarch64 + else + echo "customize_airootfs: no kernel image at /boot/Image or /boot/vmlinuz-linux-aarch64" >&2 + exit 1 + fi fi # Drop presets whose kernel image is absent: `mkinitcpio -P` aborts on them. @@ -47,11 +53,20 @@ PRESET echo "customize_airootfs: building the live initramfs from archiso.conf" rm -f /boot/initramfs-linux*.img -mkinitcpio -p linux-aarch64 || echo "customize_airootfs: WARNING mkinitcpio failed" -if compgen -G "/boot/initramfs-*.img" >/dev/null; then - echo "customize_airootfs: initramfs present" -else - echo "customize_airootfs: ERROR no initramfs produced" + +# mkinitcpio exits non-zero on this platform even when it produces a complete +# image: archiso's memdisk hook wants the phram module and the memdiskfind +# binary, and neither exists for aarch64 (memdiskfind ships in syslinux, which +# is x86-only). Judge the result on the artefact rather than the exit status. +mkinitcpio -p linux-aarch64 || \ + echo "customize_airootfs: mkinitcpio reported errors; checking for the image" + +# The GRUB entries boot this file by name. If it is missing the ISO builds +# cleanly and then fails to boot, so fail the build here instead. +if [[ ! -s /boot/initramfs-linux-aarch64.img ]]; then + echo "customize_airootfs: no live initramfs was produced; the ISO would not boot" >&2 + exit 1 fi +echo "customize_airootfs: initramfs present ($(stat -c %s /boot/initramfs-linux-aarch64.img) bytes)" exit 0 From adf5a1f2bf2f11a963a8ec669ccbd872091a98d9 Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 24 Aug 2026 23:52:06 +0800 Subject: [PATCH 07/11] boot: document what customize_airootfs.sh is actually responsible for The script read as though it hand-rolls initramfs generation for aarch64. It does not, and should not: on Arch that is automatic, because the kernel package installs usr/lib/modules//{vmlinuz,pkgbase}, 90-mkinitcpio-install.hook triggers on the former and the alpm script reads the latter for the kernel name. Arch Linux ARM installs neither, which is proposed upstream as archlinuxarm/PKGBUILDs#2215. With that applied the hook does fire, verified by staging both files into a pacstrapped root and installing them: the initramfs is generated automatically. Two naming gaps remain, and closing those is this script's actual job: - ALARM's preset writes /boot/initramfs-linux.img while the GRUB entries load initramfs-linux-aarch64.img - nothing creates /boot/vmlinuz-*, which is the glob mkarchiso copies the kernel from Also records why the deprecated customize_airootfs.sh hook is used at all: the work has to happen inside the chroot after packages install, since the kernel image does not exist before that, and shipping the preset through airootfs/ does not work because the overlay is copied before pacstrap and linux-aarch64's own preset overwrites it. Comments only; no behaviour change. --- configs/aarch64/customize_airootfs.sh | 42 ++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/configs/aarch64/customize_airootfs.sh b/configs/aarch64/customize_airootfs.sh index 4f61f5c9..ff0765de 100644 --- a/configs/aarch64/customize_airootfs.sh +++ b/configs/aarch64/customize_airootfs.sh @@ -1,5 +1,32 @@ #!/usr/bin/env bash # Runs inside the live-ISO chroot after packages are installed. +# +# WHAT THIS IS FOR +# ---------------- +# Arch Linux ARM's kernel packages name things differently from Arch's, and +# archiso and GRUB both look for the Arch names. This reconciles the two. +# +# It is deliberately *not* responsible for generating the initramfs. On Arch +# that happens by itself: the kernel package installs +# usr/lib/modules//{vmlinuz,pkgbase}, 90-mkinitcpio-install.hook triggers +# on the former, and /usr/share/libalpm/scripts/mkinitcpio reads the latter to +# learn the kernel's name. ALARM installs neither file, so the hook never fires +# -- proposed upstream as archlinuxarm/PKGBUILDs#2215. With that applied the +# hook does run, but two naming gaps remain and are what this script closes: +# +# * ALARM's preset is PRESETS=('default') writing /boot/initramfs-linux.img, +# while the GRUB entries load initramfs-linux-aarch64.img. +# * Nothing creates /boot/vmlinuz-*, and mkarchiso copies the kernel with +# `install -- "${pacstrap_dir}/boot/vmlinuz-"*`, which would match nothing. +# +# It also sets archiso_config in the preset, which is how x86's linux-t2.preset +# keeps an installed system's HOOKS out of the live initramfs. +# +# NOTE: mkarchiso prints a deprecation warning for customize_airootfs.sh. There +# is currently no replacement hook that runs inside the chroot after packages +# install, which is required here because the kernel image only exists then. +# Shipping the preset in airootfs/ does not work either: the overlay is copied +# before pacstrap, so linux-aarch64's own preset overwrites it. set -uo pipefail # Everything here addresses differences in Arch Linux ARM's kernel packaging. @@ -15,9 +42,8 @@ if [[ -f /etc/mkinitcpio.conf.d/thunderbolt_module.conf ]]; then /etc/mkinitcpio.conf.d/thunderbolt_module.conf fi -# ALARM's linux-aarch64 installs /boot/Image rather than /boot/vmlinuz-. -# Give the preset the filename it expects. Everything below depends on this, so -# say so plainly rather than letting mkinitcpio fail confusingly later. +# ALARM installs the kernel as /boot/Image. mkarchiso copies /boot/vmlinuz-* +# into the ISO, so without this name the kernel never reaches the image. if [[ ! -e /boot/vmlinuz-linux-aarch64 ]]; then if [[ -e /boot/Image ]]; then cp -a /boot/Image /boot/vmlinuz-linux-aarch64 @@ -37,10 +63,12 @@ for preset in /etc/mkinitcpio.d/*.preset; do fi done -# The equivalent of x86's linux-t2.preset: archiso_config makes mkinitcpio read -# archiso.conf as its configuration, bypassing /etc/mkinitcpio.conf.d entirely. -# That is what stops omarchy_hooks.conf -- which describes an *installed* -# system -- from replacing the archiso hooks in the *live* initramfs. +# Replace ALARM's preset, which writes /boot/initramfs-linux.img, with one that +# writes the name the GRUB entries load. archiso_config additionally makes +# mkinitcpio read archiso.conf as its configuration, bypassing +# /etc/mkinitcpio.conf.d entirely -- the same trick x86's linux-t2.preset uses +# to keep omarchy_hooks.conf, which describes an *installed* system, out of the +# *live* initramfs. cat > /etc/mkinitcpio.d/linux-aarch64.preset <<'PRESET' # Live-ISO preset for Arch Linux ARM's linux-aarch64. PRESETS=('archiso') From b69ef016482fb536ad6281b6d1fcd5ed4b23f0af Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 26 Aug 2026 01:58:31 +0800 Subject: [PATCH 08/11] builder: filter GRUB modules not built for the target platform archiso hardcodes a GRUB module list taken from an x86 bug report. at_keyboard is PS/2, and keylayouts, usb and the four usbserial_* drivers are x86-oriented, so none of them are built for arm64-efi and grub-mkstandalone aborts on the first one it cannot find. Without this an aarch64 ISO cannot be built at all. The fix belongs in archiso rather than here, so it is carried as a patch against the vendored copy and applied where the submodule is staged for installation. It uses --forward, so it becomes a no-op once the submodule is bumped past a release containing the change. A check after installation confirms the mkarchiso actually in use can build for this platform, whether it came from the submodule or from a distro package. Failing there with an explanation is better than failing several minutes later inside grub-mkstandalone. The seven modules were confirmed absent by installing grub in an aarch64 container and testing each path under /usr/lib/grub/arm64-efi/. --- builder/build-iso.sh | 22 +++++++++++++++ builder/patches/archiso-grubmodules.patch | 33 +++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 builder/patches/archiso-grubmodules.patch diff --git a/builder/build-iso.sh b/builder/build-iso.sh index 5f82adea..aa23a784 100755 --- a/builder/build-iso.sh +++ b/builder/build-iso.sh @@ -60,10 +60,32 @@ if ! pacman --noconfirm -S --needed archiso; then # the default install target runs rst2man (python-docutils) for man pages, # which this build has no use for. cp -r /archiso /tmp/archiso-src + + # archiso hardcodes a GRUB module list taken from an x86 bug report, and + # at_keyboard (PS/2), keylayouts, usb and the usbserial_* drivers are not + # built for arm64-efi, so grub-mkstandalone aborts on the first one it cannot + # find. Filter the list to what the target platform actually provides. + # + # This belongs upstream in archiso rather than here; carried against the + # vendored copy until it lands there, and applied with --forward so it becomes + # a no-op once the submodule is bumped past it. + patch -d /tmp/archiso-src -p1 --forward --batch \ + &2 + echo "built for arm64-efi (at_keyboard, keylayouts, usb, usbserial_*), so" >&2 + echo "grub-mkstandalone would abort. Apply builder/patches/archiso-grubmodules.patch" >&2 + echo "or use an archiso that already filters the list." >&2 + exit 1 +fi + # Pre-import the omarchy signing key (so pacman trusts our [omarchy] repo # during the build without keyserver lookups). pacman-key --add /builder/omarchy.gpg diff --git a/builder/patches/archiso-grubmodules.patch b/builder/patches/archiso-grubmodules.patch new file mode 100644 index 00000000..52255ef2 --- /dev/null +++ b/builder/patches/archiso-grubmodules.patch @@ -0,0 +1,33 @@ +# Filter archiso's hardcoded GRUB module list to what the target platform +# provides. at_keyboard (PS/2), keylayouts, usb and the usbserial_* drivers +# are not built for arm64-efi, so grub-mkstandalone aborts without this. +# +# Generated against the vendored archiso (v87). Belongs upstream in archiso; +# carried here until it lands there. build-iso.sh applies it with --forward, +# so it becomes a no-op once the submodule is bumped past the fix. +--- a/archiso/mkarchiso ++++ b/archiso/mkarchiso +@@ -691,6 +691,23 @@ + minicmd normal ntfs ntfscomp part_apple part_gpt part_msdos png read reboot regexp search \ + search_fs_file search_fs_uuid search_label serial sleep tpm udf usb usbserial_common usbserial_ftdi \ + usbserial_pl2303 usbserial_usbdebug video xfs zstd) ++ # Not every module in the list above is built for every platform: at_keyboard ++ # is PS/2 and the usbserial_* drivers are x86-oriented, so none of them exist ++ # for arm64-efi and grub-mkstandalone aborts on the first one it cannot find. ++ # Keep only what the target platform actually provides. ++ local _m _available=() _missing=() ++ for _m in "${grubmodules[@]}"; do ++ if [[ -e "/usr/lib/grub/${grub_target}/${_m}.mod" ]]; then ++ _available+=("$_m") ++ else ++ _missing+=("$_m") ++ fi ++ done ++ if (( ${#_missing[@]} )); then ++ _msg_info "Skipping GRUB module(s) not built for ${grub_target}: ${_missing[*]}" ++ fi ++ grubmodules=("${_available[@]}") ++ + grub-mkstandalone -O "$grub_target" \ + --modules="${grubmodules[*]}" \ + --locales="en@quot" \ From 1ce48df49ae80afc85945036855d2863822bf21a Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 26 Aug 2026 15:40:23 +0800 Subject: [PATCH 09/11] builder: define _filter_grubmodules in the carried archiso patch build-iso.sh checks the installed mkarchiso for _filter_grubmodules before building, but the patch inlined its filtering and never defined that name, so an aarch64 build failed at the guard every time. Move the filtering into a _filter_grubmodules helper that build-iso.sh can find. Its diagnostic goes to stderr, since stdout carries the module list. --- builder/patches/archiso-grubmodules.patch | 46 +++++++++++++++-------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/builder/patches/archiso-grubmodules.patch b/builder/patches/archiso-grubmodules.patch index 52255ef2..2c838cb7 100644 --- a/builder/patches/archiso-grubmodules.patch +++ b/builder/patches/archiso-grubmodules.patch @@ -7,27 +7,43 @@ # so it becomes a no-op once the submodule is bumped past the fix. --- a/archiso/mkarchiso +++ b/archiso/mkarchiso -@@ -691,6 +691,23 @@ - minicmd normal ntfs ntfscomp part_apple part_gpt part_msdos png read reboot regexp search \ - search_fs_file search_fs_uuid search_label serial sleep tpm udf usb usbserial_common usbserial_ftdi \ - usbserial_pl2303 usbserial_usbdebug video xfs zstd) -+ # Not every module in the list above is built for every platform: at_keyboard -+ # is PS/2 and the usbserial_* drivers are x86-oriented, so none of them exist -+ # for arm64-efi and grub-mkstandalone aborts on the first one it cannot find. -+ # Keep only what the target platform actually provides. -+ local _m _available=() _missing=() -+ for _m in "${grubmodules[@]}"; do -+ if [[ -e "/usr/lib/grub/${grub_target}/${_m}.mod" ]]; then -+ _available+=("$_m") +@@ -669,6 +669,31 @@ + fi + } + ++# Filter a GRUB module list down to what a platform actually provides. ++# Not every module archiso asks for is built everywhere: at_keyboard is PS/2 and ++# the usbserial_* drivers are x86-oriented, so none of them exist for arm64-efi ++# and grub-mkstandalone aborts on the first one it cannot find. ++# $1: GRUB target (e.g. arm64-efi) ++# $@: module names ++# Prints the available modules, one per line. ++_filter_grubmodules() { ++ local _target="${1}" ++ shift ++ local _module _available=() _missing=() ++ for _module in "$@"; do ++ if [[ -e "/usr/lib/grub/${_target}/${_module}.mod" ]]; then ++ _available+=("${_module}") + else -+ _missing+=("$_m") ++ _missing+=("${_module}") + fi + done + if (( ${#_missing[@]} )); then -+ _msg_info "Skipping GRUB module(s) not built for ${grub_target}: ${_missing[*]}" ++ # stdout carries the module list, so this has to go to stderr. ++ _msg_info "Skipping GRUB module(s) not built for ${_target}: ${_missing[*]}" >&2 + fi -+ grubmodules=("${_available[@]}") ++ printf '%s\n' "${_available[@]}" ++} + + # Prepare GRUB + _make_bootmode_uefi.grub() { + local grub_target grubmodules=() files_to_copy=() +@@ -691,6 +716,7 @@ + minicmd normal ntfs ntfscomp part_apple part_gpt part_msdos png read reboot regexp search \ + search_fs_file search_fs_uuid search_label serial sleep tpm udf usb usbserial_common usbserial_ftdi \ + usbserial_pl2303 usbserial_usbdebug video xfs zstd) ++ mapfile -t grubmodules < <(_filter_grubmodules "$grub_target" "${grubmodules[@]}") grub-mkstandalone -O "$grub_target" \ --modules="${grubmodules[*]}" \ --locales="en@quot" \ From 019297c347ea8bfe98d95702f18059beafa795a1 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 26 Aug 2026 17:00:00 +0800 Subject: [PATCH 10/11] installer: keep the distribution's mirrorlist on aarch64 The configurator hands archinstall three custom mirror servers, which it prepends to the target's /etc/pacman.d/mirrorlist. All three are Arch x86_64 mirrors laid out as $repo/os/$arch. Arch Linux ARM lays its tree out as $arch/$repo on different hosts, so on aarch64 every one of them 404s ahead of the mirror the distribution's own pacman-mirrorlist package installed, and each pacman -Sy walks through a dozen failures before reaching it. Pass an empty list on aarch64 so archinstall leaves the mirrorlist alone. The x86_64 rendering is byte-identical to before. --- configs/airootfs/root/configurator | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/configs/airootfs/root/configurator b/configs/airootfs/root/configurator index fb949142..3bb2d202 100644 --- a/configs/airootfs/root/configurator +++ b/configs/airootfs/root/configurator @@ -21,6 +21,20 @@ case "$(uname -m)" in *) limine_efi_binary="limine_x64.efi" ;; esac +# archinstall prepends these to the target's mirrorlist. They are Arch x86_64 +# mirrors laid out as $repo/os/$arch; Arch Linux ARM uses $arch/$repo on +# different hosts, so on aarch64 every one of them 404s ahead of the mirror the +# distribution's own pacman-mirrorlist package installed. Leave that list alone +# there. +case "$(uname -m)" in + aarch64) mirror_custom_servers='[]' ;; + *) mirror_custom_servers='[ + {"url": "https://mirror.omarchy.org/$repo/os/$arch"}, + {"url": "https://mirror.rackspace.com/archlinux/$repo/os/$arch"}, + {"url": "https://geo.mirror.pkgbuild.com/$repo/os/$arch"} + ]' ;; +esac + # The setup form — keyboard, account, hostname, and timezone questions, plus the # rules their answers are checked against — shared verbatim with the first-boot # owner setup that finishes a deferred install. build-iso.sh vendors it out of @@ -854,11 +868,7 @@ run_partition_execute() { }, "mirror_config": { "custom_repositories": [], - "custom_servers": [ - {"url": "https://mirror.omarchy.org/\$repo/os/\$arch"}, - {"url": "https://mirror.rackspace.com/archlinux/\$repo/os/\$arch"}, - {"url": "https://geo.mirror.pkgbuild.com/\$repo/os/\$arch"} - ], + "custom_servers": $mirror_custom_servers, "mirror_regions": {}, "optional_repositories": [] }, @@ -1243,11 +1253,7 @@ cat <<-_EOF_ >user_configuration.json }, "mirror_config": { "custom_repositories": [], - "custom_servers": [ - {"url": "https://mirror.omarchy.org/\$repo/os/\$arch"}, - {"url": "https://mirror.rackspace.com/archlinux/\$repo/os/\$arch"}, - {"url": "https://geo.mirror.pkgbuild.com/\$repo/os/\$arch"} - ], + "custom_servers": $mirror_custom_servers, "mirror_regions": {}, "optional_repositories": [] }, From 7f003b3cc9483317930e1ff2241e44262c784dd4 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 26 Aug 2026 17:41:56 +0800 Subject: [PATCH 11/11] builder: enumerate offline mirror packages before repo-add repo-add was handed both *.pkg.tar.zst and *.pkg.tar.xz as inline globs. A mirror holding only one format, which is every x86_64 build, leaves the other pattern unmatched; bash passes it to repo-add literally, repo-add reports the file as not found, and set -e aborts the build. Collect the existing files first, as build-omarchy-packages.sh already does, and fail with a clear message if there are none. --- builder/build-iso.sh | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/builder/build-iso.sh b/builder/build-iso.sh index aa23a784..41e9e567 100755 --- a/builder/build-iso.sh +++ b/builder/build-iso.sh @@ -489,8 +489,19 @@ printf '%s\n' "${required_package_files[@]}" | # Rebuild the offline repo db from scratch so size/checksum/depends entries # always reflect only the package files selected for this build. rm -f "$offline_mirror_dir"/offline.db* "$offline_mirror_dir"/offline.files* -repo-add "$offline_mirror_dir/offline.db.tar.gz" \ - "$offline_mirror_dir/"*.pkg.tar.zst "$offline_mirror_dir/"*.pkg.tar.xz +# Enumerate rather than glob inline: a mirror holding only one of the two +# formats leaves the other pattern unmatched, and bash would hand it to +# repo-add literally, which fails the build under set -e. +offline_repo_packages=() +for package_file in "$offline_mirror_dir/"*.pkg.tar.zst "$offline_mirror_dir/"*.pkg.tar.xz; do + [[ -e $package_file ]] || continue + offline_repo_packages+=("$package_file") +done +if (( ${#offline_repo_packages[@]} == 0 )); then + echo "ERROR: no package files found in $offline_mirror_dir" >&2 + exit 1 +fi +repo-add "$offline_mirror_dir/offline.db.tar.gz" "${offline_repo_packages[@]}" # mkarchiso expects the mirror at /var/cache/omarchy/mirror/offline inside the # container (the airootfs path); symlink rather than duplicate.