Compare commits

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

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

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

No files matched your search

+1 -9
View File
@@ -158,15 +158,7 @@ else
DOCKER_ARGS+=(--pull=always)
fi
# The docker group is root-equivalent, so Omarchy operators stay out of it on
# purpose. When the socket refuses the calling user, run the client through
# sudo instead of requiring group membership.
DOCKER=(docker)
if ! docker version &>/dev/null; then
DOCKER=(sudo docker)
fi
"${DOCKER[@]}" run "${DOCKER_ARGS[@]}" archlinux/archlinux:latest /$BUILD_SCRIPT
docker run "${DOCKER_ARGS[@]}" archlinux/archlinux:latest /$BUILD_SCRIPT
latest_iso=$(\ls -t "$BUILD_RELEASE_PATH"/*.iso | head -n1)
iso_ref="${latest_iso%.*}-$OMARCHY_ISO_REF.iso"
-19
View File
@@ -1,10 +1,5 @@
#!/bin/bash
# A failed step must stop the release: without this, a make that dies (say,
# docker permission denied) falls through to signing and uploading whatever
# stale ISO release/ already holds, under the new release's name.
set -e
MAKE_ARGS=()
ISO_REF=quattro
RELEASE_SUFFIX=""
@@ -42,14 +37,6 @@ if [[ -z "${BUILD_VERSION-}" ]]; then
exit 1
fi
# A version that already numbers its candidate (4.0.2rc2) is the complete
# artifact name; the bare -rc suffix exists only for an unnumbered candidate.
# Without this, every candidate of a train uploads over the same
# omarchy-X.Y.Z-rc.iso and testers cannot tell which rc they are holding.
if [[ $BUILD_VERSION == *rc* ]]; then
RELEASE_SUFFIX=""
fi
BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..")
BUILD_RELEASE_PATH="$BUILD_ROOT/release"
@@ -83,9 +70,3 @@ fi
echo -e "\e[32mSHA256 for $ISO_REF ISO: $iso_sha\e[0m"
omarchy-iso-upload "$release_iso"
# The whole point of a release is handing someone this pair.
echo -e "\e[32mReleased $ISO_REF ISO $BUILD_VERSION\e[0m"
echo " URL: https://iso.omarchy.org/${release_iso##*/}"
echo " SHA256: $iso_sha"
echo " Local: $release_iso"
+40 -62
View File
@@ -18,10 +18,9 @@
# first boot runs omarchy-provision-owner and creates it there
# --reuse-base Skip the install phase if a base image already exists
# --install-only Stop after producing the installed base image
# --sync-omarchy DIR Push DIR's test suite into the guest before running
# --sync-omarchy DIR Push DIR's test/acceptance into the guest before running
# (defaults to OMARCHY_PATH when the suite is present)
# --sync-all DIR Push all of DIR (bin/config/shell/test) into the guest
# and also run the CLI and shell suites there
# --port PORT Host port forwarded to guest SSH (default 2222)
# --memory MB VM memory (default 8192)
# --timeout SECS Install timeout (default 2400)
@@ -202,11 +201,6 @@ start_vm() {
local disk="$1" serial="$2"
shift 2
# A viewer that resizes the guest display breaks the console driving: OCR
# cannot read the shrunken font, and a resize during early boot can wedge
# virtio-gpu before the system comes up.
log "Watch (do not resize): vncviewer -RemoteResize=0 127.0.0.1:5905"
qemu-system-x86_64 \
-cpu host -enable-kvm -machine q35,accel=kvm \
-smp "$(nproc)" \
@@ -217,7 +211,6 @@ start_vm() {
-device virtio-blk-pci,drive=drive0,bootindex=1 \
-device virtio-vga \
-display none \
-vnc 127.0.0.1:5 \
-usb -device usb-tablet \
-netdev user,id=net0,hostfwd=tcp:127.0.0.1:$SSH_PORT-:22 \
-device virtio-net-pci,netdev=net0 \
@@ -407,7 +400,7 @@ guest_layer_absent() {
}
cleanup_shortcut_state() {
ssh_session 'for plugin in omarchy.menu omarchy.emojis omarchy.clipboard omarchy.audio; do
ssh_session 'for plugin in omarchy.launcher omarchy.menu omarchy.emojis omarchy.clipboard omarchy.audio; do
omarchy-shell shell hide "$plugin" >/dev/null 2>&1 || true
done
while read -r address; do
@@ -584,8 +577,8 @@ shortcut_smoke_phase() {
local status=0
local name chord namespace plugin
local shortcuts='menu|meta_l-spc|omarchy-menu|omarchy.menu
apps-menu|meta_l-alt-spc|omarchy-menu|omarchy.menu
local shortcuts='launcher|meta_l-spc|omarchy-launcher|omarchy.launcher
menu|meta_l-alt-spc|omarchy-menu|omarchy.menu
emojis|meta_l-ctrl-e|omarchy-emojis|omarchy.emojis
clipboard|meta_l-ctrl-v|omarchy-clipboard|omarchy.clipboard
audio-panel|meta_l-ctrl-a|omarchy-keyboard-panel|omarchy.audio'
@@ -638,11 +631,7 @@ drive_configurator() {
capture_console "success-installer-04-prepare-disk-warning-encrypted"
if ! $ENCRYPT; then
press ctrl-c
# The toggled confirm ("Yes, install without encryption") is a highlighted
# button OCR cannot read, so don't wait for it. A failed toggle still
# surfaces: the encrypted install stalls at the LUKS prompt on first boot
# and SSH never comes up.
sleep 2
wait_for_screen "without encryption" 30
capture_console "success-installer-05-prepare-disk-warning-unencrypted"
fi
press ret
@@ -706,11 +695,7 @@ drive_configurator() {
capture_console "success-installer-12-disk-warning-encrypted"
if ! $ENCRYPT; then
press ctrl-c # toggles the confirm to the unencrypted flow
# The toggled confirm ("Yes, install without encryption") is a highlighted
# button OCR cannot read, so don't wait for it. A failed toggle still
# surfaces: the encrypted install stalls at the LUKS prompt on first boot
# and SSH never comes up.
sleep 2
wait_for_screen "without encryption" 30
capture_console "success-installer-13-disk-warning-unencrypted"
fi
press ret
@@ -890,39 +875,40 @@ EOF
(cd "$BASE_DIR/www" && exec python3 -m http.server "$HTTP_PORT" --bind 127.0.0.1 >/dev/null 2>&1) &
HTTP_PID=$!
# Type the login blind: a VNC viewer that resizes the framebuffer makes the
# console font unreadable to OCR, and the real gate is SSH coming up anyway.
# Getty re-prompts after a failed login, so a whole-sequence retry recovers
# from typing before the prompt was ready.
sleep 15 # give first boot time to reach getty
local attempt
for attempt in 1 2 3; do
local waited=0
while true; do
press ctrl-alt-f3
sleep 8
press ret # settle a half-typed prompt from a previous attempt
sleep 2
type_text "$GUEST_USER"
capture_console "success-first-boot-03-console-username"
press ret
sleep 3
type_text "$GUEST_PASSWORD"
press ret
sleep 4
type_text "curl -fsS http://10.0.2.2:$HTTP_PORT/bootstrap -o /tmp/bs && bash /tmp/bs"
capture_console "success-first-boot-05-bootstrap-command"
press ret
ocr_screen | grep -qi "login:" && break
((waited += 8))
if wait_for_ssh 120 "failure-first-boot-ssh-timeout-$attempt"; then
capture_console "success-first-boot-06-bootstrap-complete"
kill "$HTTP_PID" 2>/dev/null || true
HTTP_PID=""
press ctrl-alt-f1
return 0
if ((waited >= 300)); then
capture_console "failure-first-boot-console-timeout"
echo "Timed out waiting for a console login prompt" >&2
return 1
fi
sleep 4
done
echo "Console bootstrap did not produce SSH access after 3 attempts" >&2
return 1
type_text "$GUEST_USER"
capture_console "success-first-boot-03-console-username"
press ret
wait_for_screen "Password" 60
type_text "$GUEST_PASSWORD"
capture_console "success-first-boot-04-console-password"
press ret
sleep 3
type_text "curl -fsS http://10.0.2.2:$HTTP_PORT/bootstrap -o /tmp/bs && bash /tmp/bs"
capture_console "success-first-boot-05-bootstrap-command"
press ret
wait_for_ssh 360 "failure-first-boot-ssh-timeout"
capture_console "success-first-boot-06-bootstrap-complete"
kill "$HTTP_PID" 2>/dev/null || true
HTTP_PID=""
press ctrl-alt-f1
}
# Get a freshly booted system to a running Hyprland session, typing at the
@@ -1072,14 +1058,17 @@ sync_omarchy() {
log "Syncing $SYNC_DIR into the guest"
tar -C "$SYNC_DIR" --exclude .git -cf - . | ssh_guest "mkdir -p $target && tar -C $target -xf -"
else
log "Syncing $SYNC_DIR/test into the guest"
tar -C "$SYNC_DIR" -cf - test | ssh_guest "mkdir -p $target && tar -C $target -xf -"
log "Syncing $SYNC_DIR/test/acceptance into the guest"
tar -C "$SYNC_DIR/test" -cf - acceptance acceptance.d | ssh_guest "mkdir -p $target/test && tar -C $target/test -xf -"
fi
}
acceptance_phase() {
log "Booting acceptance VM from base image overlay"
local guest_omarchy_path="/usr/share/omarchy"
$SYNC_ALL && guest_omarchy_path=".local/share/omarchy"
qemu-img create -f qcow2 -b "$BASE_DISK" -F qcow2 "$RUN_DIR/run.qcow2" >/dev/null
start_vm "$RUN_DIR/run.qcow2" "$RUN_DIR/serial.log"
@@ -1089,19 +1078,8 @@ acceptance_phase() {
local status=0
shortcut_smoke_phase || status=1
# The synced tree is a full checkout under --sync-all; run the host-style
# CLI and shell suites in the guest too before the graphical acceptance
# run. They derive their root from their own location in the synced tree.
if $SYNC_ALL; then
log "Running CLI and shell suites in the guest"
ssh_guest "bash .local/share/omarchy/test/all" || status=$?
fi
# The product under test is always the installed tree — VM runs verify the
# finished product, never a dev-linked checkout.
log "Running acceptance suite"
ssh_guest "OMARCHY_PATH=/usr/share/omarchy OMARCHY_ACCEPTANCE_DIR=/tmp/omarchy-acceptance \
OMARCHY_ACCEPTANCE_SUDO_PASSWORD=$GUEST_PASSWORD bash .local/share/omarchy/test/acceptance" || status=$?
ssh_guest "OMARCHY_PATH=$guest_omarchy_path OMARCHY_ACCEPTANCE_DIR=/tmp/omarchy-acceptance bash .local/share/omarchy/test/acceptance" || status=$?
log "Collecting artifacts into $RUN_DIR"
ssh_guest "tar -C /tmp -cf - omarchy-acceptance 2>/dev/null" | tar -C "$RUN_DIR" -xf - || true
-101
View File
@@ -1,101 +0,0 @@
#!/bin/bash
#
# Stop the VM a `omarchy-iso-test --keep-running` run left behind: graceful
# guest poweroff over SSH first, escalating to TERM then KILL. The base image
# is untouched either way; only the throwaway run overlay is affected.
#
# Usage: omarchy-iso-test-stop [--kill] [--port PORT]
#
# --kill Skip the graceful poweroff and kill the VM immediately
# --port PORT Host port forwarded to guest SSH (default 2222)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_DIR="$(dirname "$SCRIPT_DIR")"
GUEST_USER="omarchy"
GUEST_PASSWORD="omarchy"
SSH_PORT=2222
GRACEFUL=true
while (($#)); do
case "$1" in
--kill) GRACEFUL=false ;;
--port) SSH_PORT="$2"; shift ;;
-h | --help)
sed -n '3,10p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
exit 0
;;
*) echo "Unknown option: $1" >&2; exit 1 ;;
esac
shift
done
# The newest run dir whose qemu is still alive is the VM to stop.
PID=""
PIDFILE=""
while IFS= read -r candidate; do
pid=$(<"$candidate")
if kill -0 "$pid" 2>/dev/null; then
PID="$pid"
PIDFILE="$candidate"
break
fi
done < <(ls -t "$REPO_DIR"/test-runs/*/runs/*/qemu.pid 2>/dev/null)
if [[ -z $PID ]]; then
echo "No running omarchy-iso-test VM found." >&2
exit 1
fi
BASE_DIR="$(dirname "$(dirname "$(dirname "$PIDFILE")")")"
SSH_KEY="$BASE_DIR/id_ed25519"
# A harness that is still driving this VM dies with it; make sure that is
# what the caller wants before pulling anything down.
if [[ -t 0 ]]; then
if pgrep -f "bin/omarchy-iso-test .*\.iso" >/dev/null; then
echo "Warning: an omarchy-iso-test run appears to be in progress."
fi
if ! gum confirm "Stop test VM (pid $PID, ${PIDFILE#$REPO_DIR/})?"; then
exit 0
fi
fi
echo "Stopping test VM (pid $PID, ${PIDFILE#$REPO_DIR/})"
if $GRACEFUL && [[ -f $SSH_KEY ]]; then
ssh -i "$SSH_KEY" -p "$SSH_PORT" \
-o BatchMode=yes \
-o IdentitiesOnly=yes \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o ConnectTimeout=5 \
-o LogLevel=ERROR \
"$GUEST_USER@127.0.0.1" \
"echo $GUEST_PASSWORD | sudo -S systemctl poweroff" >/dev/null 2>&1 || true
waited=0
while kill -0 "$PID" 2>/dev/null && ((waited < 15)); do
sleep 1
((waited += 1))
done
fi
if kill -0 "$PID" 2>/dev/null; then
kill "$PID" 2>/dev/null || true
sleep 1
fi
if kill -0 "$PID" 2>/dev/null; then
kill -9 "$PID" 2>/dev/null || true
sleep 1
fi
if kill -0 "$PID" 2>/dev/null; then
echo "VM (pid $PID) refused to die." >&2
exit 1
fi
echo "VM stopped; port $SSH_PORT is free."
+1 -1
View File
@@ -122,7 +122,7 @@ greeter() {
printf '\033[%d;1H' "$logo_row"
gum style --foreground 2 --padding "0 0 0 $PADDING_LEFT" "$(<"$LOGO_PATH")"
tagline="Beautiful, Fun & Agentic Linux by DHH"
tagline="Beautiful, Modern & Opinionated Linux by DHH"
tpad=$(((cols - ${#tagline}) / 2)); (( tpad < 0 )) && tpad=0
printf '\033[%d;%dH%s' "$tagline_row" "$((tpad + 1))" "$tagline"
@@ -47,6 +47,21 @@ corrupt_package() {
printf '%s' "$package"
}
# A retry in the same boot never gets as far as a checksum. The first failure
# unlinked the package through the bind mount, so pacman now fails opening it,
# and it names the mirror it read from rather than the cache it writes to.
deleted_package() {
local log match package=""
for log in "$@"; do
[[ -f $log ]] || continue
match=$(sed -n "s|.*Could not open file ${OFFLINE_MIRROR}/\([^ ]*\).*|\1|p" "$log" | tail -n 1)
[[ -n $match ]] && package="$match"
done
printf '%s' "$package"
}
expected_checksum() {
local package="$1" reader
@@ -77,7 +92,14 @@ actual_checksum() {
printf '%s' "${sum%% *}"
}
# The checksum failure is read first wherever both are in the log: it is the
# attempt that names the cause, and the open failures after it are its wake.
package=$(corrupt_package "$@")
deleted=""
if [[ -z $package ]]; then
package=$(deleted_package "$@")
deleted=1
fi
if [[ -z $package ]]; then
exit 1
fi
@@ -87,6 +109,20 @@ fi
# filename has no business containing before printing it.
display_package="${package//[^A-Za-z0-9._+:@%-]/}"
# Nothing can be weighed here: the bytes that failed are gone, and the copy in
# the mirror went with them. Say which attempt this is, because the screen
# otherwise repeats the same unreadable error for every retry and the one that
# explains it scrolled away with the first.
if [[ -n $deleted ]]; then
echo "An earlier attempt deleted this package"
echo "pacman rejected it off this ISO and unlinked it from the mirror:"
echo "$display_package"
echo
echo "Reboot before retrying: the deletion is in RAM, so the ISO's copy returns"
echo "The first error after that reboot is the one that names the fault"
exit 0
fi
expected=$(expected_checksum "$package")
actual=$(actual_checksum "$package")
+1 -1
View File
@@ -27,7 +27,7 @@ Include = /etc/pacman.d/mirrorlist
[omarchy]
SigLevel = Optional TrustAll
Server = https://pkgs.omarchy.org/rc/$arch
Server = https://pkgs.omarchy.org/edge/$arch
[arch-mact2]
Server = https://mirror.funami.tech/arch-mact2/os/x86_64
+59
View File
@@ -142,6 +142,65 @@ if grep -qF "no longer matches the checksum" <<<"$output"; then
fi
pass "a deleted package names the medium without claiming which way it failed"
# Every retry in the same boot fails on the open rather than the checksum, and
# pacman names the mirror it read from, not the cache it writes to.
write_retry_log() {
local log="$1" path="${2:-$mirror/$PACKAGE}"
{
echo "[dashboard] :: Retrieving packages..."
echo "[dashboard] error: failed retrieving file '${path##*/}' from disk : Could not open file $path"
echo "[dashboard] error: failed to commit transaction (download library error)"
echo "[dashboard] ==> ERROR: Failed to install packages to new root"
} >"$log"
}
write_retry_log "$work/retry.log"
set +e
output=$(diagnose "$work/retry.log")
status=$?
set -e
(( status == 0 )) || fail "a retry after the deletion is diagnosed" "exit status $status"
[[ $(head -n 1 <<<"$output") == "An earlier attempt deleted this package" ]] ||
fail "a retry is diagnosed as a retry" "$output"
grep -qF "$PACKAGE" <<<"$output" || fail "a retry names the package" "$output"
grep -qF "Reboot before retrying" <<<"$output" ||
fail "a retry says how to get the package back" "$output"
pass "a retry after the deletion says so instead of going undiagnosed"
# A log holding both attempts must answer with the one that names the cause.
# The retries come after it and would otherwise win on recency.
printf 'a package, but damaged\n' >"$mirror/$PACKAGE"
write_mirror_db "$(printf '0%.0s' {1..64})"
{
cat "$log"
cat "$work/retry.log"
} >"$work/both.log"
set +e
output=$(diagnose "$work/both.log")
set -e
[[ $(head -n 1 <<<"$output") == "The install medium is damaged" ]] ||
fail "the checksum failure outranks the retries that followed it" "$output"
pass "the checksum failure outranks the retries that followed it"
# An open failure somewhere other than the mirror was not read off this ISO.
write_retry_log "$work/retry-elsewhere.log" "/var/tmp/build/$PACKAGE"
set +e
output=$(diagnose "$work/retry-elsewhere.log")
status=$?
set -e
(( status == 1 )) || fail "an open failure outside the mirror is not diagnosed" "exit $status: $output"
[[ -z $output ]] || fail "an open failure outside the mirror prints nothing" "$output"
pass "an open failure outside the mirror is not blamed on the medium"
rm "$mirror/$PACKAGE"
# A record with no %SHA256SUM% must not borrow the next record's, which would
# compare a real file against another package's checksum.
printf 'a package, but damaged\n' >"$mirror/$PACKAGE"