The keyboard layout list, the account and hostname validation rules, and the gum prompts that ask for them existed here and again in the runtime's first-boot owner setup, with nothing keeping the copies honest. They had already drifted: dropping two layouts from this list moved English (US) from the top of one gum choose page to the bottom of the previous one, leaving the preselected default pinned under a screenful of layouts. The form now lives in the runtime at install/provisioning/setup-form.sh, and build-iso.sh vendors it out of the very runtime package this ISO bundles — the same two-branch machinery already used for omarchy-base.packages, so a --local-source build takes it from the mounted checkout and everything else extracts it from the downloaded package. An ISO therefore ships the form belonging to the runtime it installs. That file is extracted on its own rather than alongside the package lists: bsdtar exits non-zero for a member it cannot find, so bundling the request would abort the build under set -e with a bare "Not found in archive" instead of the actionable error about publishing a runtime that carries the form. Cancel handling is unified with first-boot setup, which is what made the prompts shareable. Every prompt reports 0 (answered), 1 (Esc), or 130 (Ctrl+C). Esc now unwinds to the start of the form instead of ending the install — it previously hit the same `|| abort` as Ctrl+C. Ctrl+C keeps both of its hidden affordances: arming deferred provisioning on the first keyboard screen, and toggling unencrypted on the disk screens. test_keyboard.py reads the layout list from a runtime checkout now, and skips its localectl coverage rather than failing when there is none to read. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
350 lines
17 KiB
Bash
Executable File
350 lines
17 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
set -e
|
|
|
|
OMARCHY_ISO_REF="${OMARCHY_ISO_REF:-quattro}"
|
|
OMARCHY_MIRROR="${OMARCHY_MIRROR:-stable}"
|
|
|
|
# Quattro, 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 the offline mirror and target install.
|
|
case "$OMARCHY_ISO_REF" in
|
|
quattro|edge|dev|local)
|
|
: "${OMARCHY_RUNTIME_PACKAGE:=omarchy-dev}"
|
|
: "${OMARCHY_SETTINGS_PACKAGE:=omarchy-settings-dev}"
|
|
;;
|
|
*)
|
|
: "${OMARCHY_RUNTIME_PACKAGE:=omarchy}"
|
|
: "${OMARCHY_SETTINGS_PACKAGE:=omarchy-settings}"
|
|
;;
|
|
esac
|
|
: "${OMARCHY_NVIM_PACKAGE:=omarchy-nvim}"
|
|
export OMARCHY_RUNTIME_PACKAGE OMARCHY_SETTINGS_PACKAGE OMARCHY_NVIM_PACKAGE
|
|
|
|
# Packages installed into the Arch container used to build the ISO.
|
|
pacman-key --init
|
|
pacman --noconfirm -Sy archlinux-keyring
|
|
# Full upgrade, not just -Sy: docker never re-pulls :latest once it's cached,
|
|
# 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
|
|
|
|
# Pre-import the omarchy signing key (so pacman trusts our [omarchy] repo
|
|
# during the build without keyserver lookups).
|
|
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
|
|
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
|
|
fi
|
|
|
|
# Build locations
|
|
build_cache_dir=/var/cache
|
|
offline_mirror_dir="$build_cache_dir/airootfs/var/cache/omarchy/mirror/offline"
|
|
mkdir -p "$build_cache_dir" "$offline_mirror_dir"
|
|
|
|
# Seed from the official Arch releng profile.
|
|
cp -r /archiso/configs/releng/* "$build_cache_dir/"
|
|
rm "$build_cache_dir/airootfs/etc/motd"
|
|
|
|
# We rely on the global CDN; drop reflector.
|
|
rm -rf "$build_cache_dir/airootfs/etc/systemd/system/multi-user.target.wants/reflector.service"
|
|
rm -rf "$build_cache_dir/airootfs/etc/systemd/system/reflector.service.d"
|
|
rm -rf "$build_cache_dir/airootfs/etc/xdg/reflector"
|
|
|
|
# Bring in our archiso profile additions.
|
|
cp -r /configs/* "$build_cache_dir/"
|
|
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"
|
|
cat > "$build_cache_dir/airootfs/usr/share/omarchy-iso/package-targets" <<EOF
|
|
OMARCHY_RUNTIME_PACKAGE=$OMARCHY_RUNTIME_PACKAGE
|
|
OMARCHY_SETTINGS_PACKAGE=$OMARCHY_SETTINGS_PACKAGE
|
|
OMARCHY_NVIM_PACKAGE=$OMARCHY_NVIM_PACKAGE
|
|
EOF
|
|
|
|
if [[ ${OMARCHY_INSTALL_DEBUG:-} == "1" ]]; then
|
|
touch "$build_cache_dir/airootfs/usr/share/omarchy-iso/install-debug"
|
|
{
|
|
echo "debug=1"
|
|
echo "built_at=$(date -Is)"
|
|
echo "ref=$OMARCHY_ISO_REF"
|
|
echo "mirror=$OMARCHY_MIRROR"
|
|
echo "runtime_package=$OMARCHY_RUNTIME_PACKAGE"
|
|
echo "settings_package=$OMARCHY_SETTINGS_PACKAGE"
|
|
echo "nvim_package=$OMARCHY_NVIM_PACKAGE"
|
|
if [[ -d /omarchy-source ]]; then
|
|
echo "omarchy_source=/omarchy-source"
|
|
git -c safe.directory=/omarchy-source -C /omarchy-source rev-parse HEAD 2>/dev/null | sed 's/^/omarchy_commit=/' || true
|
|
git -c safe.directory=/omarchy-source -C /omarchy-source status --short 2>/dev/null | sed 's/^/omarchy_status=/' || true
|
|
fi
|
|
if [[ -d /omarchy-pkgs ]]; then
|
|
echo "omarchy_pkgs_source=/omarchy-pkgs"
|
|
git -c safe.directory=/omarchy-pkgs -C /omarchy-pkgs rev-parse HEAD 2>/dev/null | sed 's/^/omarchy_pkgs_commit=/' || true
|
|
git -c safe.directory=/omarchy-pkgs -C /omarchy-pkgs status --short 2>/dev/null | sed 's/^/omarchy_pkgs_status=/' || true
|
|
fi
|
|
} > "$build_cache_dir/airootfs/usr/share/omarchy-iso/build-info"
|
|
fi
|
|
|
|
# When --local-source is in effect, build omarchy* from the mounted source
|
|
# trees and drop them in the offline mirror. Otherwise pacman -Syw below
|
|
# downloads the published versions from the omarchy network mirror.
|
|
if [[ -d /omarchy-source && -d /omarchy-pkgs ]]; then
|
|
bash /builder/build-omarchy-packages.sh "$offline_mirror_dir"
|
|
LOCAL_OMARCHY_BUILD=1
|
|
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}')
|
|
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/"
|
|
cp "/tmp/$NODE_FILENAME" "$build_cache_dir/airootfs/opt/packages/"
|
|
|
|
# Packages installed into the live ISO environment itself (NOT the target system).
|
|
# 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 python-terminaltexteffects tzupdate omarchy-keyring "$OMARCHY_SETTINGS_PACKAGE" lvm2 cryptsetup parted)
|
|
printf '%s\n' "${arch_packages[@]}" >> "$build_cache_dir/packages.x86_64"
|
|
|
|
# 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
|
|
# initramfs, copied into both the ISO tree and the size-constrained FAT EFI image.
|
|
#
|
|
# It cannot just be deleted — releng's broadcom-wl hard-depends on it, and it is
|
|
# the only releng package that does, so pacman would drag the kernel straight back
|
|
# in. broadcom-wl is a prebuilt module for stock linux and cannot load on the
|
|
# kernel we boot, so it has done nothing since we started booting T2 anyway. The
|
|
# 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"
|
|
|
|
# Build the offline mirror: everything pacstrap might want during the target
|
|
# install. With --local-source, the omarchy* packages we just built are
|
|
# already in the mirror and we filter them out below. Without it, pacman -Syw
|
|
# pulls the published omarchy* from the network mirror like any other package.
|
|
if [[ -d /omarchy-source ]]; then
|
|
base_pkg_lists=(/omarchy-source/install/omarchy-base.packages /omarchy-source/install/omarchy-other.packages)
|
|
setup_form=/omarchy-source/install/provisioning/setup-form.sh
|
|
else
|
|
# Pull the same package lists out of the freshly-downloaded Omarchy runtime
|
|
# package so we don't need a local checkout in the non-local-source path.
|
|
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
|
|
omarchy_pkg=$(find "$bootstrap_cache_dir" -maxdepth 1 -type f -name "$OMARCHY_RUNTIME_PACKAGE-*.pkg.tar.zst" | 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
|
|
fi
|
|
mkdir -p /tmp/omarchy-pkglists
|
|
bsdtar -xf "$omarchy_pkg" -C /tmp/omarchy-pkglists usr/share/omarchy/install/omarchy-base.packages usr/share/omarchy/install/omarchy-other.packages
|
|
base_pkg_lists=(/tmp/omarchy-pkglists/usr/share/omarchy/install/omarchy-base.packages /tmp/omarchy-pkglists/usr/share/omarchy/install/omarchy-other.packages)
|
|
# Extracted on its own, tolerating a miss: bsdtar exits non-zero for a member
|
|
# it can't find, so asking for this alongside the package lists would abort the
|
|
# build here (set -e) with a bare "Not found in archive" instead of the
|
|
# actionable error below.
|
|
bsdtar -xf "$omarchy_pkg" -C /tmp/omarchy-pkglists usr/share/omarchy/install/provisioning/setup-form.sh 2>/dev/null || true
|
|
setup_form=/tmp/omarchy-pkglists/usr/share/omarchy/install/provisioning/setup-form.sh
|
|
fi
|
|
|
|
mkdir -p "$build_cache_dir/airootfs/usr/share/omarchy-iso"
|
|
cp "${base_pkg_lists[0]}" "$build_cache_dir/airootfs/usr/share/omarchy-iso/omarchy-base.packages"
|
|
cp "${base_pkg_lists[1]}" "$build_cache_dir/airootfs/usr/share/omarchy-iso/omarchy-other.packages"
|
|
|
|
# The configurator's setup form comes from the runtime this ISO bundles, so the
|
|
# installer and the first-boot setup that finishes a deferred install can never
|
|
# disagree. A runtime predating the split ships no such file, which would leave
|
|
# the configurator with no prompts at all.
|
|
if [[ ! -f $setup_form ]]; then
|
|
echo "ERROR: $OMARCHY_RUNTIME_PACKAGE does not ship install/provisioning/setup-form.sh" >&2
|
|
echo " The configurator sources its prompts from that file, so this ISO" >&2
|
|
echo " would boot into an installer with no questions to ask." >&2
|
|
echo " Publish a runtime carrying the shared setup form, or build with" >&2
|
|
echo " --local-source against a checkout that has it." >&2
|
|
exit 1
|
|
fi
|
|
cp "$setup_form" "$build_cache_dir/airootfs/usr/share/omarchy-iso/setup-form.sh"
|
|
|
|
# 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"
|
|
grep -hv '^#\|^$' "${base_pkg_lists[@]}"
|
|
grep -hv '^#\|^$' /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
|
|
)
|
|
|
|
# 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.
|
|
if [[ -n ${LOCAL_OMARCHY_BUILD:-} ]]; then
|
|
mapfile -t all_packages < <(
|
|
printf '%s\n' "${all_packages[@]}" |
|
|
grep -Fxv \
|
|
-e "$OMARCHY_RUNTIME_PACKAGE" \
|
|
-e "$OMARCHY_SETTINGS_PACKAGE" \
|
|
-e "$OMARCHY_NVIM_PACKAGE" || true
|
|
)
|
|
fi
|
|
|
|
mkdir -p /tmp/offlinedb
|
|
download_offline_packages() {
|
|
pacman --config /configs/pacman-online-${OMARCHY_MIRROR}.conf --noconfirm -Syw \
|
|
"${all_packages[@]}" --cachedir "$offline_mirror_dir/" --dbpath /tmp/offlinedb --needed
|
|
}
|
|
|
|
# A repository may occasionally republish a package without changing its
|
|
# filename. Pacman detects that the persistent cached copy no longer matches
|
|
# the refreshed repository checksum and deletes it, but still fails the
|
|
# transaction. Retry once so the now-missing package is downloaded.
|
|
if ! download_offline_packages; then
|
|
echo "Offline package download failed; retrying after pacman cleaned invalid cached files..." >&2
|
|
download_offline_packages
|
|
fi
|
|
|
|
# Resolve the exact filenames chosen by the same synced package databases used
|
|
# for the download. Pruning by this transaction (rather than merely keeping the
|
|
# 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 \
|
|
--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
|
|
exit 1
|
|
fi
|
|
mapfile -t required_package_files <<< "$resolved_package_files"
|
|
|
|
# The online transaction intentionally excludes packages built from the local
|
|
# checkouts. Add those exact artifacts back to the keep-set after verifying
|
|
# that the local build left exactly one file for each selected package name.
|
|
if [[ -n ${LOCAL_OMARCHY_BUILD:-} ]]; then
|
|
for local_package_name in \
|
|
"$OMARCHY_RUNTIME_PACKAGE" "$OMARCHY_SETTINGS_PACKAGE" "$OMARCHY_NVIM_PACKAGE"; do
|
|
local_package_file=""
|
|
for candidate in "$offline_mirror_dir/$local_package_name-"*.pkg.tar.*; do
|
|
[[ -f $candidate && $candidate != *.sig ]] || continue
|
|
read -r candidate_name _ < <(pacman -Qp "$candidate" 2>/dev/null) || continue
|
|
[[ $candidate_name == "$local_package_name" ]] || continue
|
|
if [[ -n $local_package_file ]]; then
|
|
echo "ERROR: multiple local builds found for $local_package_name" >&2
|
|
exit 1
|
|
fi
|
|
local_package_file="${candidate##*/}"
|
|
done
|
|
if [[ -z $local_package_file ]]; then
|
|
echo "ERROR: local build not found for $local_package_name" >&2
|
|
exit 1
|
|
fi
|
|
required_package_files+=("$local_package_file")
|
|
done
|
|
fi
|
|
|
|
printf '%s\n' "${required_package_files[@]}" |
|
|
bash /builder/prune-offline-mirror.sh "$offline_mirror_dir"
|
|
|
|
# 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
|
|
|
|
# mkarchiso expects the mirror at /var/cache/omarchy/mirror/offline inside the
|
|
# container (the airootfs path); symlink rather than duplicate.
|
|
mkdir -p /var/cache/omarchy/mirror
|
|
ln -sf "$offline_mirror_dir" /var/cache/omarchy/mirror/offline
|
|
|
|
# Denominator for the install dashboard's progress bar. Resolving the mirror's
|
|
# own package lists against the mirror we just indexed, with an empty local db,
|
|
# is the question pacstrap asks at install time — same resolver, same repo, same
|
|
# lists — so no hand-kept constant can drift.
|
|
#
|
|
# It over-counts by ~1 in 925: archinstall.packages lists both amd-ucode and
|
|
# intel-ucode because the mirror must contain either. phases.py records expected
|
|
# and actual in the timing JSON, so growing drift shows up in acceptance runs.
|
|
# The early-bootstrap set is already inside this closure, so restating it would
|
|
# only add a second list to drift.
|
|
resolve_expected_packages() {
|
|
local resolve_root=/tmp/omarchy-expected-packages
|
|
local resolved
|
|
local -a targets
|
|
|
|
rm -rf "$resolve_root"
|
|
mkdir -p "$resolve_root/var/lib/pacman"
|
|
|
|
mapfile -t targets < <(
|
|
{
|
|
grep -hv '^#\|^$' /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 '^#\|^$' \
|
|
"$build_cache_dir/airootfs/usr/share/omarchy-iso/omarchy-base.packages"
|
|
printf '%s\n' "$OMARCHY_RUNTIME_PACKAGE" "$OMARCHY_SETTINGS_PACKAGE" \
|
|
"$OMARCHY_NVIM_PACKAGE"
|
|
} | sort -u
|
|
)
|
|
|
|
pacman --config "$build_cache_dir/pacman-offline.conf" \
|
|
--root "$resolve_root" --dbpath "$resolve_root/var/lib/pacman" \
|
|
--noconfirm -Sy >/dev/null || return 1
|
|
|
|
# Capture before counting: no pipefail here, so a pacman failure inside a
|
|
# pipeline would become a plausible partial count, which never trips the
|
|
# dashboard's fallback.
|
|
resolved="$(pacman --config "$build_cache_dir/pacman-offline.conf" \
|
|
--root "$resolve_root" --dbpath "$resolve_root/var/lib/pacman" \
|
|
--noconfirm -S --print --print-format '%n' "${targets[@]}")" || return 1
|
|
|
|
printf '%s\n' "$resolved" | sort -u | grep -c .
|
|
}
|
|
|
|
# Worth failing the build over: -S --print only aborts when a target is missing
|
|
# from the offline repo, which would fail pacstrap the same way. A count that
|
|
# merely looks wrong is not — the dashboard falls back without the file.
|
|
if ! expected_packages="$(resolve_expected_packages)"; then
|
|
echo "ERROR: could not resolve the target package count from the offline mirror." >&2
|
|
echo " pacman -S --print aborts the whole transaction if any single target" >&2
|
|
echo " is missing, so this almost certainly means pacstrap would fail the" >&2
|
|
echo " same way at install time." >&2
|
|
exit 1
|
|
fi
|
|
if (( expected_packages < 600 || expected_packages > 2000 )); then
|
|
echo "WARNING: resolved target package count $expected_packages is outside the" >&2
|
|
echo " expected 600-2000 range; shipping no denominator so the install" >&2
|
|
echo " dashboard falls back to its time-based curve." >&2
|
|
else
|
|
printf '%s\n' "$expected_packages" \
|
|
>"$build_cache_dir/airootfs/usr/share/omarchy-iso/expected-packages"
|
|
echo "Target install resolves to $expected_packages packages."
|
|
fi
|
|
|
|
# Live ISO uses the same offline pacman.conf.
|
|
cp "$build_cache_dir/pacman-offline.conf" "$build_cache_dir/airootfs/etc/pacman.conf"
|
|
|
|
# Build the ISO.
|
|
mkarchiso -v -w "$build_cache_dir/work/" -o /out/ "$build_cache_dir/"
|
|
|
|
# Match host UID/GID on output.
|
|
if [[ -n $HOST_UID && -n $HOST_GID ]]; then
|
|
chown -R "$HOST_UID:$HOST_GID" /out/
|
|
fi
|