#!/bin/bash # omarchy:summary=Retry a system package update that hit a conflict # omarchy:hidden=true # Internal to omarchy-update-system-pkgs. Not a command to run by hand: it acts # on a pacman error report, and an old or hand-written one would move live files # out of the way for an upgrade that is not happening. # # Paths written by a script rather than installed by a package -- an earlier # 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 # 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 [[ ${OMARCHY_UPDATE_CONFLICT:-} == 1 ]] || { echo "omarchy-update-system-pkgs-when-conflicted runs as part of omarchy update" >&2 exit 1 } errors=${1:?usage: omarchy-update-system-pkgs-when-conflicted } # Where a file pacman is taking over gets moved, mirroring its full path. Kept # out of the directory it came from: SDDM reads every file in sddm.conf.d # whatever its extension, and systemd-sleep runs every executable in # system-sleep, so a copy left beside the original would still be live. replaced=${OMARCHY_REPLACED_DIR:-/var/lib/omarchy/replaced} # Anything moved that the upgrade didn't end up installing goes back, whether # the retry failed or the move loop itself was interrupted partway. Nothing here # should leave configuration inactive that was live when it started. # # -e follows symlinks, so a dangling one reads as absent. -L catches those: a # link pacman installed, and a relative link that no longer resolves from inside # the quarantine. moved=() restore_moved() { local path restorable=() for path in "${moved[@]}"; do if [[ ! -e $path && ! -L $path ]] && [[ -e $replaced$path || -L $replaced$path ]]; then restorable+=("$path") fi done ((${#restorable[@]})) || return 0 echo -e "\e[33m\nPutting back what the upgrade didn't take:\e[0m" for path in "${restorable[@]}"; do sudo mv -T "$replaced$path" "$path" echo " $path" done } # exec'd into, so the caller's EXIT trap never ran and the report is still here. # Cleaning it up is this script's job from now on. trap 'restore_moved; rm -f "$errors"' EXIT 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 '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 # of the way is what lets the upgrade through, and works on a leftover directory # as well as a file. # # "pkg: /path exists in filesystem" is unowned; the same line plus "(owned by # x)" is not, so the end anchor takes only ours. -Qo re-checks the live database # before anything moves. mapfile -t leftovers < <( grep -oP '^omarchy(-dev|-settings|-settings-dev)?: \K.+(?= exists in filesystem$)' "$errors" | while IFS= read -r path; do pacman -Qo "$path" &>/dev/null || echo "$path"; done ) ((${#leftovers[@]})) || exit 1 # All of them or none: moving a subset leaves the retry blocked by the rest, and # the moved files inactive for nothing. ((${#leftovers[@]} == $(grep -c ' exists in filesystem' "$errors"))) || exit 1 echo -e "\e[33m\nTaking over files pacman doesn't own yet:\e[0m" for path in "${leftovers[@]}"; do sudo mkdir -p "$replaced${path%/*}" # -T so an existing directory at the destination is replaced, not moved into. sudo mv -T --backup=numbered "$path" "$replaced$path" moved+=("$path") echo " $path -> $replaced$path" done echo if OMARCHY_UPDATE_RETRY=1 omarchy-update-system-pkgs; then moved=() exit 0 fi exit 1