diff --git a/bin/omarchy-upgrade-to-quattro b/bin/omarchy-upgrade-to-quattro index 56707457..4b39148b 100755 --- a/bin/omarchy-upgrade-to-quattro +++ b/bin/omarchy-upgrade-to-quattro @@ -322,6 +322,8 @@ fi sudo_keepalive_pid="" hyprland_config_reload_suppressed=0 +upgrade_started=0 +upgrade_completed=0 cleanup_on_exit() { local exit_status=$? @@ -334,6 +336,14 @@ cleanup_on_exit() { kill "$sudo_keepalive_pid" 2>/dev/null || true fi + # set -e aborts without a word, and the last thing on screen is a green + # progress line, so a half-finished upgrade otherwise reads as a finished one. + if (( exit_status != 0 && upgrade_started && ! upgrade_completed )); then + printf '\n\033[31mUpgrade incomplete - do NOT reboot.\033[0m\n' >&2 + printf 'The system is part Omarchy 3 and part Omarchy quattro; rebooting now can leave it without a working network or desktop.\n' >&2 + printf 'Fix the error reported above and run this script again. Re-running is safe and resumes the remaining steps.\n' >&2 + fi + exit "$exit_status" } trap cleanup_on_exit EXIT @@ -1040,11 +1050,9 @@ USER_UNITS cleanup_retired_services() { log "Disabling services retired by Omarchy quattro" - # Do not stop iwd or systemd-networkd mid-upgrade; disabling is enough and - # avoids dropping a running Wi-Fi connection before the user can reboot into - # NetworkManager. - as_root systemctl disable iwd.service >/dev/null 2>&1 || true - + # iwd is retired alongside the NetworkManager enable it depends on. Do not + # stop systemd-networkd mid-upgrade; disabling is enough and avoids dropping a + # running connection before the user can reboot into NetworkManager. local networkd_unit for networkd_unit in \ systemd-networkd.service \ @@ -1339,7 +1347,12 @@ EOF enable_system_service linux-modules-cleanup.service enable_system_service docker.socket enable_system_service systemd-resolved.service + # NetworkManager replaces iwd, and both halves of that swap belong in one + # step: a machine that boots with both enabled has two daemons fighting over + # the Wi-Fi adapter. iwd is disabled but never stopped, so the running + # connection survives until the reboot. enable_system_service NetworkManager.service + as_root systemctl disable iwd.service >/dev/null 2>&1 || true enable_system_service bluetooth.service enable_system_service power-profiles-daemon.service enable_system_service sddm.service @@ -2256,11 +2269,15 @@ if command -v xdg-mime >/dev/null 2>&1; then xdg-mime default HEY.desktop x-scheme-handler/mailto || true fi -"$root/bin/omarchy-done" mark first-run-user -rm -f "$state_dir/first-run-user.done" - -"$root/bin/omarchy-done" mark finalize-user -rm -f "$state_dir/finalize-user.done" +# This script is fetched from the branch and runs against whatever packaged +# tree the channel currently serves, so the packaged tree can be older than the +# script. The completion markers are written directly rather than through +# omarchy-done, because a command missing from an older build would abort the +# upgrade two thirds of the way through. +done_dir="$state_dir/done" +mkdir -p "$done_dir" +touch "$done_dir/first-run-user" "$done_dir/finalize-user" +rm -f "$state_dir/first-run-user.done" "$state_dir/finalize-user.done" USER_SETUP } @@ -2360,6 +2377,10 @@ refresh_current_theme_after_upgrade() { run_as_user_omarchy omarchy-restart-terminal >/dev/null 2>&1 || true } +# Everything below mutates the system, so a non-zero exit from here on leaves a +# half-upgraded machine that must not be rebooted. +upgrade_started=1 + # Suppress live Hyprland reloads before anything touches the legacy config # trees; the swap windows otherwise surface transient "source= globbing error" # popups in the running session. @@ -2381,8 +2402,10 @@ cleanup_legacy_user_paths apply_system_transition apply_user_transition apply_user_hardware_transition -run_as_user_omarchy omarchy-refresh-applications -run_as_user_omarchy omarchy-bar defaults +run_as_user_omarchy omarchy-refresh-applications || + warn "Could not refresh application launchers; run 'omarchy refresh applications' after reboot." +run_as_user_omarchy omarchy-bar defaults || + warn "Could not restore the default bar widgets; run 'omarchy bar defaults' after reboot." cleanup_retired_services ensure_sleep_lock_service remove_retired_default_packages @@ -2401,6 +2424,8 @@ else warn "Leaving retired Omarchy 3 session processes running until reboot." fi +upgrade_completed=1 + cat </dev/null fi pass "Omarchy 4 upgrade completes first-run as one lifecycle" -grep -F '"$root/bin/omarchy-done" mark first-run-user' "$upgrade_to_quattro" >/dev/null -grep -F 'rm -f "$state_dir/first-run-user.done"' "$upgrade_to_quattro" >/dev/null -grep -F '"$root/bin/omarchy-done" mark finalize-user' "$upgrade_to_quattro" >/dev/null -grep -F 'rm -f "$state_dir/finalize-user.done"' "$upgrade_to_quattro" >/dev/null +grep -F 'touch "$done_dir/first-run-user" "$done_dir/finalize-user"' "$upgrade_to_quattro" >/dev/null +grep -F 'rm -f "$state_dir/first-run-user.done" "$state_dir/finalize-user.done"' "$upgrade_to_quattro" >/dev/null pass "Omarchy 4 upgrade completes first-run and migrates legacy completion markers" +# The script runs from the branch against whatever packaged tree the channel +# serves, so a packaged command missing from an older build must never be able +# to abort the upgrade partway through. +if grep -F '"$root/bin/omarchy-done"' "$upgrade_to_quattro" >/dev/null; then + fail "Omarchy 4 upgrade writes completion markers without the packaged omarchy-done" +fi +pass "Omarchy 4 upgrade writes completion markers without the packaged omarchy-done" + +for guarded_step in omarchy-refresh-applications 'omarchy-bar defaults'; do + grep -F "run_as_user_omarchy $guarded_step ||" "$upgrade_to_quattro" >/dev/null || + fail "Omarchy 4 upgrade survives a packaged tree without $guarded_step" +done +pass "Omarchy 4 upgrade survives a packaged tree missing top-level commands" + grep -F 'configure_snapper_policy' "$upgrade_to_quattro" >/dev/null grep -F '/usr/share/omarchy/install/config/snapper.sh' "$upgrade_to_quattro" >/dev/null grep -F 'bash -euo pipefail "$snapper_config_script"' "$upgrade_to_quattro" >/dev/null @@ -61,6 +73,44 @@ grep -F 'systemd-networkd.socket' "$upgrade_to_quattro" >/dev/null grep -F 'systemd-networkd-resolve-hook.socket' "$upgrade_to_quattro" >/dev/null pass "Omarchy 4 upgrade retires systemd-networkd for NetworkManager" +# Booting with both managers enabled leaves them fighting over the Wi-Fi +# adapter, so enabling NetworkManager and disabling iwd cannot be separated by +# any step that might abort in between. +function_body() { + awk -v name="$1" '$0 == name "() {" { inside = 1; next } inside && $0 == "}" { exit } inside' "$upgrade_to_quattro" +} + +if function_body cleanup_retired_services | grep -F 'systemctl disable iwd' >/dev/null; then + fail "Omarchy 4 upgrade does not retire iwd in a step separate from the NetworkManager enable" +fi +grep -A1 -F ' enable_system_service NetworkManager.service' "$upgrade_to_quattro" | + grep -F 'as_root systemctl disable iwd.service' >/dev/null || + fail "Omarchy 4 upgrade retires iwd in the step that enables NetworkManager" +pass "Omarchy 4 upgrade switches from iwd to NetworkManager atomically" + +# set -e aborts silently, so only an explicit banner distinguishes a +# half-upgraded system from a finished one. +grep -Fx 'trap cleanup_on_exit EXIT' "$upgrade_to_quattro" >/dev/null || + fail "Omarchy 4 upgrade reports an aborted run instead of exiting silently" +cleanup_body=$(function_body cleanup_on_exit) +grep -F 'upgrade_started && ! upgrade_completed' <<<"$cleanup_body" >/dev/null || + fail "Omarchy 4 upgrade reports an aborted run instead of exiting silently" +grep -F 'Upgrade incomplete - do NOT reboot.' <<<"$cleanup_body" >/dev/null || + fail "Omarchy 4 upgrade reports an aborted run instead of exiting silently" +grep -F 'exit "$exit_status"' <<<"$cleanup_body" >/dev/null || + fail "Omarchy 4 upgrade preserves the failing exit status" +grep -F '>&2' <<<"$cleanup_body" >/dev/null || + fail "Omarchy 4 upgrade reports an aborted run on stderr" +started_line=$(grep -n '^upgrade_started=1$' "$upgrade_to_quattro" | cut -d: -f1) +completed_line=$(grep -n '^upgrade_completed=1$' "$upgrade_to_quattro" | cut -d: -f1) +suppress_line=$(grep -n '^suppress_hyprland_config_reload$' "$upgrade_to_quattro" | cut -d: -f1) +shell_line=$(grep -n '^if start_omarchy_shell_session; then$' "$upgrade_to_quattro" | cut -d: -f1) +[[ -n $started_line && -n $completed_line && -n $suppress_line && -n $shell_line ]] || + fail "upgrade progress markers and the mutating step range exist" +(( started_line < suppress_line )) || fail "the upgrade is marked started before the first mutation" +(( completed_line > shell_line )) || fail "the upgrade is marked complete only after the last step" +pass "Omarchy 4 upgrade reports an aborted run instead of exiting silently" + grep -F 'omarchy-bar defaults' "$upgrade_to_quattro" >/dev/null pass "Omarchy 4 upgrade restores service-aware bar defaults"