Put a blocked package upgrade back to whoever is updating (#6830)
Pacman answers its own conflict question with No under --noconfirm, so one retired package can stop every update after it. Which package to drop is a decision rather than a cleanup, so run the upgrade again with pacman asking when there is a terminal to answer on, and report instead when -y promised not to ask. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
625c66301d
commit
5ca3030c5a
@@ -21,6 +21,10 @@ trap 'omarchy-update-stay-awake stop' EXIT
|
|||||||
|
|
||||||
omarchy-update-requires-free-space
|
omarchy-update-requires-free-space
|
||||||
|
|
||||||
|
# -y is a promise not to ask anything. Steps that would need an answer report
|
||||||
|
# and move on instead of waiting on a prompt nobody is here to give.
|
||||||
|
[[ ${1:-} != "-y" ]] || export OMARCHY_UPDATE_UNATTENDED=1
|
||||||
|
|
||||||
if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
|
if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
|
||||||
# Before the snapshot: the cache is on the snapshotted subvolume, so pruning
|
# Before the snapshot: the cache is on the snapshotted subvolume, so pruning
|
||||||
# after it frees nothing until that snapshot ages out.
|
# after it frees nothing until that snapshot ages out.
|
||||||
@@ -36,6 +40,10 @@ if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
|
|||||||
|
|
||||||
omarchy-update-dev
|
omarchy-update-dev
|
||||||
omarchy-update-keyring
|
omarchy-update-keyring
|
||||||
|
|
||||||
|
# Migrations ship with the packages installed here and are written against
|
||||||
|
# them, so everything below waits on this finishing. An upgrade that stopped
|
||||||
|
# takes the update with it rather than migrating against what is still on disk.
|
||||||
omarchy-update-system-pkgs
|
omarchy-update-system-pkgs
|
||||||
omarchy-migrate
|
omarchy-migrate
|
||||||
omarchy-hook post-update
|
omarchy-hook post-update
|
||||||
|
|||||||
@@ -5,6 +5,14 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
# The conflict handler's last resort: pacman puts its questions to the person
|
||||||
|
# running the update instead of answering them itself. Nothing here may capture
|
||||||
|
# a stream, because an upgrade without --noconfirm prompts on stderr. The
|
||||||
|
# handler has already said why, so no heading here either.
|
||||||
|
if [[ ${OMARCHY_UPDATE_CONFLICT:-} == 1 && ${OMARCHY_UPDATE_INTERACTIVE:-} == 1 ]]; then
|
||||||
|
exec sudo env OMARCHY_UPDATE_PACMAN=1 pacman -Syu --overwrite '/usr/share/omarchy/*'
|
||||||
|
fi
|
||||||
|
|
||||||
echo -e "\e[32m\nUpdate system packages\e[0m"
|
echo -e "\e[32m\nUpdate system packages\e[0m"
|
||||||
|
|
||||||
errors=$(mktemp)
|
errors=$(mktemp)
|
||||||
@@ -22,9 +30,9 @@ if sudo env LC_ALL=C OMARCHY_UPDATE_PACMAN=1 pacman -Syu --noconfirm \
|
|||||||
fi
|
fi
|
||||||
cat "$errors" >&2
|
cat "$errors" >&2
|
||||||
|
|
||||||
# An upgrade blocked only by files pacman doesn't own yet is the one failure
|
# The handler takes it from here: it clears files pacman doesn't own yet and
|
||||||
# worth retrying: the handler clears them and runs this again. Anything else,
|
# runs this again, and puts a package conflict to whoever started the update.
|
||||||
# including a second failure, is for a human.
|
# Anything else, including a second failure, is for a human.
|
||||||
[[ ${OMARCHY_UPDATE_RETRY:-} != 1 ]] || exit 1
|
[[ ${OMARCHY_UPDATE_RETRY:-} != 1 ]] || exit 1
|
||||||
# exec, so the EXIT trap above does not fire and the handler can still read the
|
# exec, so the EXIT trap above does not fire and the handler can still read the
|
||||||
# report. It takes over deleting it.
|
# report. It takes over deleting it.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# omarchy:summary=Retry a system package update after resolving file conflicts
|
# omarchy:summary=Retry a system package update that hit a conflict
|
||||||
# omarchy:hidden=true
|
# omarchy:hidden=true
|
||||||
|
|
||||||
# Internal to omarchy-update-system-pkgs. Not a command to run by hand: it acts
|
# Internal to omarchy-update-system-pkgs. Not a command to run by hand: it acts
|
||||||
@@ -11,6 +11,10 @@
|
|||||||
# release's installer, an in-place edit -- belong to nobody, and pacman refuses
|
# release's installer, an in-place edit -- belong to nobody, and pacman refuses
|
||||||
# to install over a file it doesn't own. This clears them and runs the upgrade
|
# to install over a file it doesn't own. This clears them and runs the upgrade
|
||||||
# again.
|
# again.
|
||||||
|
#
|
||||||
|
# Packages that conflict with each other are the other blocked upgrade, and that
|
||||||
|
# one is a decision rather than a cleanup, so it goes back to the person
|
||||||
|
# watching the update.
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
@@ -57,6 +61,26 @@ restore_moved() {
|
|||||||
trap 'restore_moved; rm -f "$errors"' EXIT
|
trap 'restore_moved; rm -f "$errors"' EXIT
|
||||||
trap 'exit 1' INT TERM
|
trap 'exit 1' INT TERM
|
||||||
|
|
||||||
|
# Pacman answers its own conflict question with No under --noconfirm, so one
|
||||||
|
# retired package can stop every upgrade after it. Which package to drop is a
|
||||||
|
# decision -- an upstream split retires one, but so does a package the user
|
||||||
|
# picked on purpose -- so put the question to whoever started the update rather
|
||||||
|
# than guess at it. Pacman printed what conflicts just above.
|
||||||
|
if grep -q '^error: unresolvable package conflicts detected$' "$errors"; then
|
||||||
|
# -y promised not to ask anything, and without a terminal there is nowhere to
|
||||||
|
# ask; either way pacman would sit on a prompt nobody answers. An upgrade that
|
||||||
|
# isn't running --noconfirm puts its questions on stderr and reads the answers
|
||||||
|
# from stdin, so those are the two that have to be a terminal.
|
||||||
|
if [[ ${OMARCHY_UPDATE_UNATTENDED:-} == 1 || ! -t 0 || ! -t 2 ]]; then
|
||||||
|
echo -e "\e[33m\nThis upgrade needs an answer. Run omarchy update interactively to give it.\e[0m" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\e[33m\nA package conflict stopped this upgrade. Running it again so you can answer:\e[0m"
|
||||||
|
OMARCHY_UPDATE_INTERACTIVE=1 omarchy-update-system-pkgs
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
# Paths one of these packages installs that pacman doesn't own. Moving them out
|
# Paths one of these packages installs that pacman doesn't own. Moving them out
|
||||||
# of the way is what lets the upgrade through, and works on a leftover directory
|
# of the way is what lets the upgrade through, and works on a leftover directory
|
||||||
# as well as a file.
|
# as well as a file.
|
||||||
|
|||||||
Executable
+155
@@ -0,0 +1,155 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
source "$(dirname "$0")/base-test.sh"
|
||||||
|
|
||||||
|
require_command script
|
||||||
|
|
||||||
|
test_tmp=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$test_tmp"' EXIT
|
||||||
|
|
||||||
|
stub_bin="$test_tmp/bin"
|
||||||
|
mkdir -p "$stub_bin"
|
||||||
|
|
||||||
|
cat >"$stub_bin/sudo" <<'STUB'
|
||||||
|
#!/bin/bash
|
||||||
|
exec "$@"
|
||||||
|
STUB
|
||||||
|
|
||||||
|
# Fails the first -Syu with the report under test, then succeeds. Every call
|
||||||
|
# records its arguments and which of its streams reached a terminal: pacman puts
|
||||||
|
# its questions on stderr once it is not running --noconfirm, so a retry meant
|
||||||
|
# for a person has to keep that stream.
|
||||||
|
cat >"$stub_bin/pacman" <<'STUB'
|
||||||
|
#!/bin/bash
|
||||||
|
attempt=$(($(cat "$PACMAN_ATTEMPTS") + 1))
|
||||||
|
echo "$attempt" >"$PACMAN_ATTEMPTS"
|
||||||
|
{
|
||||||
|
printf 'args %s\n' "$*"
|
||||||
|
for fd in 0 1 2; do
|
||||||
|
if [[ -t $fd ]]; then printf 'tty%s yes\n' "$fd"; else printf 'tty%s no\n' "$fd"; fi
|
||||||
|
done
|
||||||
|
} >>"$PACMAN_CALLS"
|
||||||
|
|
||||||
|
if ((attempt == 1)); then
|
||||||
|
cat "$CONFLICT_REPORT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "upgrade complete"
|
||||||
|
STUB
|
||||||
|
|
||||||
|
chmod +x "$stub_bin/sudo" "$stub_bin/pacman"
|
||||||
|
|
||||||
|
# Everything a blocked qemu-common upgrade leaves on stderr, and no more. The
|
||||||
|
# ":: ... Remove qemu-block-gluster? [y/N]" pacman asked is deliberately absent:
|
||||||
|
# under --noconfirm it goes to stdout, so nothing downstream of the report can
|
||||||
|
# be built on having read it.
|
||||||
|
write_conflict_report() {
|
||||||
|
echo 0 >"$test_tmp/attempts"
|
||||||
|
: >"$test_tmp/calls"
|
||||||
|
{
|
||||||
|
echo "error: unresolvable package conflicts detected"
|
||||||
|
echo "error: failed to prepare transaction (conflicting dependencies)"
|
||||||
|
} >"$test_tmp/report"
|
||||||
|
}
|
||||||
|
|
||||||
|
update_env() {
|
||||||
|
printf '%s\n' \
|
||||||
|
"OMARCHY_REPLACED_DIR=$test_tmp/replaced" \
|
||||||
|
"PACMAN_ATTEMPTS=$test_tmp/attempts" \
|
||||||
|
"PACMAN_CALLS=$test_tmp/calls" \
|
||||||
|
"CONFLICT_REPORT=$test_tmp/report" \
|
||||||
|
"OWNED_PATHS=" \
|
||||||
|
"OMARCHY_UPDATE_UNATTENDED=${OMARCHY_UPDATE_UNATTENDED:-}" \
|
||||||
|
"OMARCHY_UPDATE_INTERACTIVE=${OMARCHY_UPDATE_INTERACTIVE:-}" \
|
||||||
|
"PATH=$stub_bin:$ROOT/bin:$PATH"
|
||||||
|
}
|
||||||
|
|
||||||
|
# No terminal on any stream, the way a cron or ssh caller arrives.
|
||||||
|
run_headless() {
|
||||||
|
mapfile -t environment < <(update_env)
|
||||||
|
env "${environment[@]}" bash "$ROOT/bin/omarchy-update-system-pkgs" \
|
||||||
|
</dev/null >"$test_tmp/out" 2>"$test_tmp/err"
|
||||||
|
}
|
||||||
|
|
||||||
|
# script gives the update the pty that omarchy-update always runs it on, so the
|
||||||
|
# terminal checks see what a person at the keyboard would give them. Its
|
||||||
|
# transcript is stdout and stderr together, which is also what that person sees.
|
||||||
|
# $1 optionally takes one stream back off the pty.
|
||||||
|
run_on_terminal() {
|
||||||
|
mapfile -t environment < <(update_env)
|
||||||
|
env "${environment[@]}" \
|
||||||
|
script -qec "bash '$ROOT/bin/omarchy-update-system-pkgs' ${1:-}" "$test_tmp/out" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
call_line() {
|
||||||
|
awk -v call="$1" -v key="$2" \
|
||||||
|
'$1 == "args" { n++ } n == call && $1 == key { sub(/^[^ ]+ /, ""); print }' "$test_tmp/calls"
|
||||||
|
}
|
||||||
|
|
||||||
|
write_conflict_report
|
||||||
|
run_on_terminal || fail "a package conflict is not resolved on a terminal"
|
||||||
|
(($(cat "$test_tmp/attempts") == 2)) ||
|
||||||
|
fail "a package conflict does not get an interactive retry"
|
||||||
|
[[ $(call_line 2 args) == *"-Syu"* ]] ||
|
||||||
|
fail "the interactive retry does not upgrade"
|
||||||
|
[[ $(call_line 2 args) != *"--noconfirm"* ]] ||
|
||||||
|
fail "the interactive retry still answers pacman's questions itself"
|
||||||
|
[[ $(call_line 2 args) != *"--ask"* ]] ||
|
||||||
|
fail "the interactive retry answers pacman's questions from a bitmask instead"
|
||||||
|
pass "a package conflict is put back to the person running the update"
|
||||||
|
|
||||||
|
[[ $(call_line 2 tty0) == "yes" && $(call_line 2 tty2) == "yes" ]] ||
|
||||||
|
fail "the interactive retry cannot be answered: pacman has no terminal left"
|
||||||
|
pass "the interactive retry keeps the streams pacman asks and listens on"
|
||||||
|
|
||||||
|
# Which streams have to be a terminal follows from where pacman asks: stderr
|
||||||
|
# carries the question once --noconfirm is gone, stdin carries the answer, and
|
||||||
|
# stdout carries progress bars nobody has to see to answer.
|
||||||
|
write_conflict_report
|
||||||
|
run_on_terminal '>/dev/null' ||
|
||||||
|
fail "a redirected progress stream is mistaken for an unattended update"
|
||||||
|
(($(cat "$test_tmp/attempts") == 2)) ||
|
||||||
|
fail "a conflict goes unasked when only stdout is redirected"
|
||||||
|
pass "an answerable session is not turned away over its progress output"
|
||||||
|
|
||||||
|
write_conflict_report
|
||||||
|
if run_on_terminal '2>/dev/null'; then
|
||||||
|
fail "a conflict is asked about on a stream nobody is reading"
|
||||||
|
fi
|
||||||
|
(($(cat "$test_tmp/attempts") == 1)) ||
|
||||||
|
fail "pacman is left prompting where the question cannot be seen"
|
||||||
|
pass "a session that cannot show the question is not asked one"
|
||||||
|
|
||||||
|
[[ $(call_line 1 tty2) == "no" ]] ||
|
||||||
|
fail "the first upgrade no longer captures the error report"
|
||||||
|
pass "the first upgrade still captures its errors for the handler"
|
||||||
|
|
||||||
|
write_conflict_report
|
||||||
|
if run_headless; then
|
||||||
|
fail "a package conflict passes for a completed update without a terminal"
|
||||||
|
fi
|
||||||
|
(($(cat "$test_tmp/attempts") == 1)) ||
|
||||||
|
fail "a package conflict is retried with no terminal to answer on"
|
||||||
|
grep -q 'omarchy update' "$test_tmp/err" ||
|
||||||
|
fail "a package conflict with no terminal does not say how to answer it"
|
||||||
|
pass "a package conflict with no terminal reports instead of hanging"
|
||||||
|
|
||||||
|
write_conflict_report
|
||||||
|
if OMARCHY_UPDATE_UNATTENDED=1 run_on_terminal; then
|
||||||
|
fail "an unattended update stops on a prompt nobody answers"
|
||||||
|
fi
|
||||||
|
(($(cat "$test_tmp/attempts") == 1)) ||
|
||||||
|
fail "an unattended update prompts anyway"
|
||||||
|
pass "-y is kept: an unattended update never waits on an answer"
|
||||||
|
|
||||||
|
# The interactive upgrade skips the error capture the handler depends on, so
|
||||||
|
# reaching it any other way would lose the report that drives every recovery.
|
||||||
|
write_conflict_report
|
||||||
|
if OMARCHY_UPDATE_INTERACTIVE=1 run_headless; then
|
||||||
|
fail "a caller reaches the interactive upgrade on its own"
|
||||||
|
fi
|
||||||
|
[[ $(call_line 1 args) == *"--noconfirm"* ]] ||
|
||||||
|
fail "a caller can ask for an interactive upgrade directly"
|
||||||
|
pass "only the conflict handler can hand the upgrade to a person"
|
||||||
Executable
+108
@@ -0,0 +1,108 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
source "$(dirname "$0")/base-test.sh"
|
||||||
|
|
||||||
|
test_tmp=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$test_tmp"' EXIT
|
||||||
|
|
||||||
|
stub_bin="$test_tmp/bin"
|
||||||
|
mkdir -p "$stub_bin"
|
||||||
|
|
||||||
|
# Every step omarchy-update runs, recorded in order with the unattended flag it
|
||||||
|
# was handed. One of them can be told to fail.
|
||||||
|
steps=(
|
||||||
|
omarchy-update-lock
|
||||||
|
omarchy-update-requires-free-space
|
||||||
|
omarchy-update-confirm
|
||||||
|
omarchy-update-pkg-prune
|
||||||
|
omarchy-snapshot
|
||||||
|
omarchy-update-stay-awake
|
||||||
|
omarchy-update-dev
|
||||||
|
omarchy-update-keyring
|
||||||
|
omarchy-update-system-pkgs
|
||||||
|
omarchy-migrate
|
||||||
|
omarchy-hook
|
||||||
|
omarchy-update-aur-pkgs
|
||||||
|
omarchy-update-mise
|
||||||
|
omarchy-update-orphan-pkgs
|
||||||
|
omarchy-update-analyze-logs
|
||||||
|
omarchy-update-status
|
||||||
|
omarchy-update-restart
|
||||||
|
)
|
||||||
|
|
||||||
|
for step in "${steps[@]}"; do
|
||||||
|
cat >"$stub_bin/$step" <<'STUB'
|
||||||
|
#!/bin/bash
|
||||||
|
printf '%s unattended=%s\n' "${0##*/}" "${OMARCHY_UPDATE_UNATTENDED:-}" >>"$STEP_LOG"
|
||||||
|
[[ ${FAILING_STEP:-} != "${0##*/}" ]] || exit 1
|
||||||
|
STUB
|
||||||
|
chmod +x "$stub_bin/$step"
|
||||||
|
done
|
||||||
|
|
||||||
|
# OMARCHY_UPDATE_LOGGED stands in for the script(1) wrapper the update re-execs
|
||||||
|
# itself under; the stubbed lock reports itself already held.
|
||||||
|
run_update() {
|
||||||
|
: >"$test_tmp/steps"
|
||||||
|
STEP_LOG="$test_tmp/steps" \
|
||||||
|
FAILING_STEP="${FAILING_STEP:-}" \
|
||||||
|
OMARCHY_UPDATE_LOGGED=1 \
|
||||||
|
PATH="$stub_bin:$PATH" \
|
||||||
|
bash "$ROOT/bin/omarchy-update" "$@" >"$test_tmp/out" 2>"$test_tmp/err"
|
||||||
|
}
|
||||||
|
|
||||||
|
steps_run() {
|
||||||
|
cut -d' ' -f1 "$test_tmp/steps"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Every step of a whole update, in order. $1 asks for the one a person confirms.
|
||||||
|
# Stay Awake bookends the work, so it is here twice.
|
||||||
|
expected_steps() {
|
||||||
|
printf '%s\n' \
|
||||||
|
omarchy-update-lock \
|
||||||
|
omarchy-update-requires-free-space \
|
||||||
|
${1:+omarchy-update-confirm} \
|
||||||
|
omarchy-update-pkg-prune \
|
||||||
|
omarchy-snapshot \
|
||||||
|
omarchy-update-stay-awake \
|
||||||
|
omarchy-update-dev \
|
||||||
|
omarchy-update-keyring \
|
||||||
|
omarchy-update-system-pkgs \
|
||||||
|
omarchy-migrate \
|
||||||
|
omarchy-hook \
|
||||||
|
omarchy-update-aur-pkgs \
|
||||||
|
omarchy-update-mise \
|
||||||
|
omarchy-update-orphan-pkgs \
|
||||||
|
omarchy-update-analyze-logs \
|
||||||
|
omarchy-update-status \
|
||||||
|
omarchy-update-stay-awake \
|
||||||
|
omarchy-update-restart
|
||||||
|
}
|
||||||
|
|
||||||
|
run_update -y || fail "an update where everything works reports a failure"
|
||||||
|
diff <(expected_steps) <(steps_run) >"$test_tmp/order" ||
|
||||||
|
fail "an update where everything works does not run every step in order" "$(cat "$test_tmp/order")"
|
||||||
|
pass "an update where every step works runs all of them, in order"
|
||||||
|
|
||||||
|
grep -q '^omarchy-update-system-pkgs unattended=1$' "$test_tmp/steps" ||
|
||||||
|
fail "-y does not mark the update unattended"
|
||||||
|
run_update </dev/null || fail "a confirmed update reports a failure"
|
||||||
|
diff <(expected_steps confirmed) <(steps_run) >"$test_tmp/order" ||
|
||||||
|
fail "a confirmed update runs a different set of steps" "$(cat "$test_tmp/order")"
|
||||||
|
grep -q '^omarchy-update-system-pkgs unattended=$' "$test_tmp/steps" ||
|
||||||
|
fail "an update a person confirmed is treated as unattended"
|
||||||
|
pass "-y is what marks an update unattended, not the update itself"
|
||||||
|
|
||||||
|
# Migrations ship with the packages the upgrade installs and are written against
|
||||||
|
# them. Running them against what is still on disk is the failure this ordering
|
||||||
|
# exists to prevent, so the update stops where the packages did.
|
||||||
|
if FAILING_STEP=omarchy-update-system-pkgs run_update -y; then
|
||||||
|
fail "an update whose packages did not upgrade passes for a whole one"
|
||||||
|
fi
|
||||||
|
for step in omarchy-migrate omarchy-hook omarchy-update-aur-pkgs omarchy-update-restart; do
|
||||||
|
if grep -q "^$step " "$test_tmp/steps"; then
|
||||||
|
fail "a blocked package upgrade still runs $step"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
pass "a blocked package upgrade stops the update before it migrates"
|
||||||
Reference in New Issue
Block a user