Merge pull request #6405 from basecamp/recover-pacman-file-conflicts
Recover from package file conflicts instead of predicting them
This commit is contained in:
@@ -7,42 +7,25 @@ set -e
|
||||
|
||||
echo -e "\e[32m\nUpdate system packages\e[0m"
|
||||
|
||||
# Transition --overwrite: previous script-installed Omarchy wrote these paths
|
||||
# as unowned files; pacman would refuse the omarchy-settings upgrade on first
|
||||
# encounter. Drop each entry once the transition release is the baseline.
|
||||
errors=$(mktemp)
|
||||
trap 'rm -f "$errors"' EXIT
|
||||
|
||||
# /usr/share/omarchy is wholly Omarchy's; files can land there unowned and would
|
||||
# otherwise abort the upgrade. Packaged content wins there.
|
||||
#
|
||||
# An entry has to ship at least one release before the package starts owning the
|
||||
# path. pacman checks file conflicts during transaction prepare, so the copy of
|
||||
# this script running the upgrade is the one already on disk.
|
||||
#
|
||||
# The /usr/share/omarchy/* entry is permanent: that tree is wholly owned by
|
||||
# the omarchy packages, but files can land there unowned (in-place extension
|
||||
# work, script-written files), which would abort the whole upgrade. Packaged
|
||||
# content always wins there.
|
||||
sudo env OMARCHY_UPDATE_PACMAN=1 pacman -Syu --noconfirm \
|
||||
--overwrite '/etc/docker/daemon.json' \
|
||||
--overwrite '/etc/gnupg/dirmngr.conf' \
|
||||
--overwrite '/etc/mkinitcpio.conf.d/omarchy_hooks.conf' \
|
||||
--overwrite '/etc/mkinitcpio.conf.d/thunderbolt_module.conf' \
|
||||
--overwrite '/etc/modprobe.d/omarchy-usb-autosuspend.conf' \
|
||||
--overwrite '/etc/sddm.conf.d/10-theme.conf' \
|
||||
--overwrite '/etc/sddm.conf.d/10-wayland.conf' \
|
||||
--overwrite '/etc/sudoers.d/omarchy-asdcontrol' \
|
||||
--overwrite '/etc/sudoers.d/omarchy-passwd-tries' \
|
||||
--overwrite '/etc/sudoers.d/omarchy-tzupdate' \
|
||||
--overwrite '/etc/sysctl.d/90-omarchy-file-watchers.conf' \
|
||||
--overwrite '/etc/sysctl.d/99-omarchy-sysctl.conf' \
|
||||
--overwrite '/etc/systemd/logind.conf.d/10-ignore-power-button.conf' \
|
||||
--overwrite '/etc/systemd/resolved.conf.d/10-disable-multicast.conf' \
|
||||
--overwrite '/etc/systemd/resolved.conf.d/20-docker-dns.conf' \
|
||||
--overwrite '/etc/systemd/system.conf.d/10-faster-shutdown.conf' \
|
||||
--overwrite '/etc/systemd/system/user@.service.d/10-faster-shutdown.conf' \
|
||||
--overwrite '/etc/systemd/system/docker.service.d/no-block-boot.conf' \
|
||||
--overwrite '/etc/systemd/system/plocate-updatedb.service.d/ac-only.conf' \
|
||||
--overwrite '/etc/systemd/system.conf.d/20-omarchy-nofile.conf' \
|
||||
--overwrite '/etc/systemd/user.conf.d/20-omarchy-nofile.conf' \
|
||||
--overwrite '/usr/lib/systemd/system-sleep/unmount-fuse' \
|
||||
--overwrite '/usr/share/plymouth/themes/omarchy/*' \
|
||||
--overwrite '/usr/share/sddm/hyprland.lua' \
|
||||
--overwrite '/usr/share/sddm/themes/omarchy/*' \
|
||||
--overwrite '/usr/share/omarchy/*'
|
||||
# Progress bars stay on stdout. Errors are on stderr, kept for the conflict
|
||||
# handler below; LC_ALL=C is what keeps them parseable in any locale.
|
||||
if sudo env LC_ALL=C OMARCHY_UPDATE_PACMAN=1 pacman -Syu --noconfirm \
|
||||
--overwrite '/usr/share/omarchy/*' 2>"$errors"; then
|
||||
cat "$errors" >&2
|
||||
exit 0
|
||||
fi
|
||||
cat "$errors" >&2
|
||||
|
||||
# An upgrade blocked only by files pacman doesn't own yet is the one failure
|
||||
# worth retrying: the handler clears them and runs this again. Anything else,
|
||||
# including a second failure, is for a human.
|
||||
[[ ${OMARCHY_UPDATE_RETRY:-} != 1 ]] || exit 1
|
||||
# exec, so the EXIT trap above does not fire and the handler can still read the
|
||||
# report. It takes over deleting it.
|
||||
exec env OMARCHY_UPDATE_CONFLICT=1 omarchy-update-system-pkgs-when-conflicted "$errors"
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 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.
|
||||
|
||||
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 <pacman-error-report>}
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
Executable
+134
@@ -0,0 +1,134 @@
|
||||
#!/bin/bash
|
||||
|
||||
# A file Omarchy writes into /usr belongs to nobody, and the
|
||||
# day a package starts shipping that same path, pacman refuses the upgrade for
|
||||
# everyone who has the file. omarchy-update-system-pkgs-when-conflicted recovers from
|
||||
# that, but the cheaper answer is to ship the file in the package instead.
|
||||
#
|
||||
# This flags a script writing such a path unless a PKGBUILD installs it, or it
|
||||
# is recorded below with the reason it cannot be packaged.
|
||||
#
|
||||
# It is a net, not a proof: it reads the destination off the command, so a path
|
||||
# assembled from variables passes through. The two known cases are recorded
|
||||
# below, and a new one is caught only if it names the path where it writes it.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
|
||||
python3 - "$ROOT" <<'PYTHON'
|
||||
import os, re, sys
|
||||
from pathlib import Path
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
|
||||
# Paths Omarchy writes into /usr that no package owns, with the reason.
|
||||
allowed = {
|
||||
# Symlinks into another package's icon theme; owning them would mean owning
|
||||
# paths inside Yaru.
|
||||
"/usr/share/icons/Yaru/scalable/actions",
|
||||
# Hardware-conditional sleep hooks, installed only on the machines that need
|
||||
# them so the hook does not exist where it does not apply.
|
||||
"/usr/lib/systemd/system-sleep",
|
||||
# Written through a variable, so the scan below cannot see them at the point
|
||||
# they are written. Both drop configuration into another project's tree rather
|
||||
# than Omarchy's, which is why neither is a candidate for omarchy-settings.
|
||||
"/usr/share/chromium/extensions",
|
||||
"/usr/lib/firefox/distribution",
|
||||
# Static content that belongs in omarchy-settings. It cannot move there in the
|
||||
# same release that first ships omarchy-update-system-pkgs-when-conflicted: the
|
||||
# upgrade carrying the handler is the one that would hit the conflict, and the
|
||||
# handler only helps once it is on disk. Package it the release after.
|
||||
"/usr/lib/chromium/initial_preferences",
|
||||
}
|
||||
|
||||
# One-time 3.x upgrade. It runs before this rule existed and cannot be made to
|
||||
# retroactively matter for machines that already ran it.
|
||||
skip_scripts = {"bin/omarchy-upgrade-to-quattro"}
|
||||
|
||||
pkgs_candidates = [
|
||||
root.parent / "omarchy-pkgs/pkgbuilds",
|
||||
root.parent.parent / "omarchy-pkgs/pkgbuilds",
|
||||
root.parent / "omacom/omarchy-pkgs/pkgbuilds",
|
||||
Path.home() / "Work/omacom/omarchy-pkgs/pkgbuilds",
|
||||
]
|
||||
override = os.environ.get("OMARCHY_PKGS_PATH")
|
||||
if override:
|
||||
pkgs_candidates = [Path(override) / "pkgbuilds", Path(override)] + pkgs_candidates
|
||||
pkgs_root = next((p for p in pkgs_candidates if p.exists()), None)
|
||||
if pkgs_root is None:
|
||||
print("not ok - omarchy-pkgs checkout found for package ownership check", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
packaged = "\n".join(p.read_text() for p in pkgs_root.glob("*/PKGBUILD"))
|
||||
|
||||
# Commands that put a file somewhere, as opposed to reading one.
|
||||
# /etc is administrator territory that Omarchy legitimately edits. /usr is
|
||||
# package territory, where writing anything is the thing worth catching.
|
||||
writer = re.compile(r"\b(tee|cp|install|ln)\b|>\s*/usr/")
|
||||
target = re.compile(r"/usr/[A-Za-z0-9._@/+-]+")
|
||||
|
||||
problems = []
|
||||
for base in ("bin", "install", "migrations"):
|
||||
for path in sorted((root / base).rglob("*")):
|
||||
if not path.is_file():
|
||||
continue
|
||||
rel = str(path.relative_to(root))
|
||||
if rel in skip_scripts:
|
||||
continue
|
||||
# Join continuations first, so a command split across lines is judged whole
|
||||
# rather than as a source path on one line and a destination on the next.
|
||||
joined, buf, start = [], "", 1
|
||||
for lineno, line in enumerate(path.read_text(errors="ignore").splitlines(), 1):
|
||||
if not buf:
|
||||
start = lineno
|
||||
if line.rstrip().endswith("\\"):
|
||||
buf += line.rstrip()[:-1] + " "
|
||||
continue
|
||||
joined.append((start, buf + line))
|
||||
buf = ""
|
||||
if buf:
|
||||
joined.append((start, buf))
|
||||
|
||||
for lineno, line in joined:
|
||||
code = line.split("#", 1)[0]
|
||||
if not writer.search(code):
|
||||
continue
|
||||
# A redirection names its destination directly. Otherwise cp/install/ln
|
||||
# put the destination last, and anything earlier is a source being read.
|
||||
redirect = re.search(r">\s*\"?(/usr/[^\s\"]+)", code)
|
||||
if redirect:
|
||||
hit = redirect.group(1).rstrip("/")
|
||||
else:
|
||||
tokens = re.sub(r"[12]?>\s*\S+", "", code).split()
|
||||
if not tokens or not tokens[-1].startswith("/usr/"):
|
||||
continue
|
||||
hit = tokens[-1].rstrip("/")
|
||||
# Omarchy's own tree and its binaries are covered elsewhere.
|
||||
if hit.startswith(("/usr/share/omarchy", "/usr/bin")) or hit.count("/") < 3:
|
||||
continue
|
||||
# Directory or file form of a recorded path both count as recorded, but
|
||||
# only on a path boundary: system-sleeping is not system-sleep.
|
||||
def covers(a, b):
|
||||
return a == b or b.startswith(a + "/")
|
||||
if any(covers(a, hit) or covers(hit, a) for a in allowed):
|
||||
continue
|
||||
# Likewise in the PKGBUILD text, where the path is a destination rather
|
||||
# than a prefix of a longer one.
|
||||
if re.search(re.escape(hit) + r'(?=["\'\s]|$)', packaged, re.M):
|
||||
continue
|
||||
problems.append(f"{rel}:{lineno}: {hit}")
|
||||
|
||||
if problems:
|
||||
print("not ok - Omarchy writes paths under /usr that no package owns", file=sys.stderr)
|
||||
for p in problems:
|
||||
print(f" {p}", file=sys.stderr)
|
||||
print(
|
||||
"\nShip it from a PKGBUILD so pacman owns it, or record it in this test\n"
|
||||
"with the reason it cannot be packaged.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
PYTHON
|
||||
|
||||
pass "no Omarchy script writes a path under /usr that no package owns"
|
||||
Executable
+276
@@ -0,0 +1,276 @@
|
||||
#!/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"
|
||||
|
||||
cat >"$stub_bin/sudo" <<'STUB'
|
||||
#!/bin/bash
|
||||
exec "$@"
|
||||
STUB
|
||||
|
||||
# Fails the first -Syu with the report under test, then succeeds unless the case
|
||||
# asked for the retry to fail too.
|
||||
cat >"$stub_bin/pacman" <<'STUB'
|
||||
#!/bin/bash
|
||||
if [[ $1 == -Qo ]]; then
|
||||
# Anything in OWNED_PATHS has a package behind it; everything else is unowned.
|
||||
[[ " $OWNED_PATHS " == *" $2 "* ]]
|
||||
exit $?
|
||||
fi
|
||||
|
||||
attempt=$(($(cat "$PACMAN_ATTEMPTS") + 1))
|
||||
echo "$attempt" >"$PACMAN_ATTEMPTS"
|
||||
if ((attempt == 1)); then
|
||||
cat "$CONFLICT_REPORT" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -n ${RETRY_FAILS:-} ]]; then
|
||||
# Optionally commit the file first, as a partial transaction would.
|
||||
[[ -n ${RETRY_INSTALLS:-} ]] && echo "packaged" >"$RETRY_INSTALLS"
|
||||
echo "error: failed to retrieve some files" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "upgrade complete"
|
||||
STUB
|
||||
|
||||
chmod +x "$stub_bin/sudo" "$stub_bin/pacman"
|
||||
|
||||
replaced="$test_tmp/replaced"
|
||||
|
||||
run_update() {
|
||||
OMARCHY_REPLACED_DIR="$replaced" \
|
||||
RETRY_FAILS="${RETRY_FAILS:-}" \
|
||||
RETRY_INSTALLS="${RETRY_INSTALLS:-}" \
|
||||
PACMAN_ATTEMPTS="$test_tmp/attempts" \
|
||||
CONFLICT_REPORT="$test_tmp/report" \
|
||||
OWNED_PATHS="${OWNED_PATHS:-}" \
|
||||
PATH="$stub_bin:$ROOT/bin:$PATH" \
|
||||
bash "$ROOT/bin/omarchy-update-system-pkgs"
|
||||
}
|
||||
|
||||
# $1 blamed package, $2 path, $3 optional owning package.
|
||||
write_report() {
|
||||
echo 0 >"$test_tmp/attempts"
|
||||
{
|
||||
echo "error: failed to commit transaction (conflicting files)"
|
||||
echo "$1: $2 exists in filesystem${3:+ (owned by $3)}"
|
||||
} >"$test_tmp/report"
|
||||
}
|
||||
|
||||
# Raw conflict lines, for reports the recovery must refuse wholesale.
|
||||
write_raw_report() {
|
||||
echo 0 >"$test_tmp/attempts"
|
||||
{
|
||||
echo "error: failed to commit transaction (conflicting files)"
|
||||
printf '%s\n' "$@"
|
||||
} >"$test_tmp/report"
|
||||
}
|
||||
|
||||
work="$test_tmp/work"
|
||||
fresh_work() {
|
||||
rm -rf "$work" "$replaced"
|
||||
mkdir -p "$work"
|
||||
}
|
||||
|
||||
# An unowned path one of the packages is taking over.
|
||||
fresh_work
|
||||
stray="$work/omarchy-fcitx5.service"
|
||||
echo "stray content" >"$stray"
|
||||
write_report omarchy-settings-dev "$stray"
|
||||
run_update >"$test_tmp/out" 2>"$test_tmp/err" ||
|
||||
fail "an unowned file conflict is not resolved"
|
||||
[[ ! -e $stray ]] ||
|
||||
fail "the file is left in pacman's way"
|
||||
pass "a file pacman is taking over is moved out of its way"
|
||||
|
||||
# Kept out of the directory it came from, where SDDM and systemd-sleep read
|
||||
# every file and every executable respectively.
|
||||
[[ -z $(ls -A "$work") ]] ||
|
||||
fail "something is left in the directory the replaced file came from"
|
||||
grep -qx "stray content" "$replaced$stray" ||
|
||||
fail "the replaced file is destroyed rather than kept out of the way"
|
||||
pass "the replaced file is quarantined outside the directory it came from"
|
||||
|
||||
# A real fight between packages, not Omarchy's leftovers. pacman appends
|
||||
# "(owned by ...)" here.
|
||||
fresh_work
|
||||
echo "theirs" >"$stray"
|
||||
write_report omarchy-settings-dev "$stray" someone-else
|
||||
if run_update >"$test_tmp/out" 2>"$test_tmp/err"; then
|
||||
fail "a file owned by another package is silently taken"
|
||||
fi
|
||||
[[ -e $stray && ! -e $replaced$stray ]] ||
|
||||
fail "a file owned by another package is silently taken"
|
||||
pass "a conflict owned by another package stops the upgrade instead of being taken"
|
||||
|
||||
# Report reads unowned, database disagrees: the parse is never the only thing
|
||||
# between a retry and another package's file.
|
||||
fresh_work
|
||||
echo "theirs" >"$stray"
|
||||
write_report omarchy-settings-dev "$stray"
|
||||
if OWNED_PATHS="$stray" run_update >"$test_tmp/out" 2>"$test_tmp/err"; then
|
||||
fail "pacman -Qo is not consulted before moving a file"
|
||||
fi
|
||||
[[ -e $stray && ! -e $replaced$stray ]] ||
|
||||
fail "pacman -Qo is not consulted before moving a file"
|
||||
pass "an owned path is left alone even when the report reads as unowned"
|
||||
|
||||
# A name prefix is not a namespace; only the packages that own system paths.
|
||||
fresh_work
|
||||
echo "stray" >"$stray"
|
||||
write_report omarchy-chromium-bin "$stray"
|
||||
if run_update >"$test_tmp/out" 2>"$test_tmp/err"; then
|
||||
fail "an optional omarchy-prefixed package gets its conflicts auto-resolved"
|
||||
fi
|
||||
pass "only the packages that own system paths get their conflicts resolved"
|
||||
|
||||
# Not Omarchy's conflict to resolve.
|
||||
fresh_work
|
||||
echo "stray" >"$stray"
|
||||
write_report some-other-pkg "$stray"
|
||||
if run_update >"$test_tmp/out" 2>"$test_tmp/err"; then
|
||||
fail "a conflict from an unrelated package is auto-resolved"
|
||||
fi
|
||||
pass "a conflict from a non-Omarchy package is left for a human"
|
||||
|
||||
# The path is used literally, so glob characters in a name mean nothing.
|
||||
fresh_work
|
||||
globby="$work/omarchy-[1].conf"
|
||||
echo "globby" >"$globby"
|
||||
write_report omarchy-settings-dev "$globby"
|
||||
run_update >"$test_tmp/out" 2>"$test_tmp/err" ||
|
||||
fail "a path whose name contains glob characters is not resolved"
|
||||
[[ -f "$replaced$globby" && ! -e "$globby" ]] ||
|
||||
fail "a path whose name contains glob characters is treated as a pattern"
|
||||
pass "a path whose name would act as a glob is moved literally"
|
||||
|
||||
# A leftover directory is cleared the same way a file is.
|
||||
fresh_work
|
||||
conflict_dir="$work/omarchy-dir"
|
||||
mkdir -p "$conflict_dir"
|
||||
write_report omarchy-settings-dev "$conflict_dir"
|
||||
run_update >"$test_tmp/out" 2>"$test_tmp/err" ||
|
||||
fail "a conflicting directory is not cleared out of pacman's way"
|
||||
[[ -d "$replaced$conflict_dir" && ! -e $conflict_dir ]] ||
|
||||
fail "a conflicting directory is left in place"
|
||||
pass "a conflicting directory is moved away"
|
||||
|
||||
# A space is legal in a package path; the parse must not truncate it.
|
||||
fresh_work
|
||||
spaced="$work/omarchy theme.conf"
|
||||
echo "spaced" >"$spaced"
|
||||
write_report omarchy-settings-dev "$spaced"
|
||||
run_update >"$test_tmp/out" 2>"$test_tmp/err" ||
|
||||
fail "a conflicting path containing a space is not resolved"
|
||||
[[ -f "$replaced$spaced" && ! -e "$spaced" ]] ||
|
||||
fail "a conflicting path containing a space is truncated"
|
||||
pass "a conflicting path containing a space is parsed whole"
|
||||
|
||||
# An earlier quarantined copy is the more original one, and might not be ours.
|
||||
fresh_work
|
||||
echo "current" >"$stray"
|
||||
mkdir -p "$replaced$work"
|
||||
echo "from an earlier run" >"$replaced$stray"
|
||||
write_report omarchy-settings-dev "$stray"
|
||||
run_update >"$test_tmp/out" 2>"$test_tmp/err" ||
|
||||
fail "a conflict with an existing quarantined copy is not resolved"
|
||||
grep -qrx "from an earlier run" "$replaced" ||
|
||||
fail "an existing quarantined copy is destroyed to make room for a new one"
|
||||
pass "an existing quarantined copy survives a later run needing the same name"
|
||||
|
||||
# mv without -T would move the source inside an existing destination directory.
|
||||
fresh_work
|
||||
echo "ours" >"$stray"
|
||||
mkdir -p "$replaced$stray"
|
||||
write_report omarchy-settings-dev "$stray"
|
||||
run_update >"$test_tmp/out" 2>"$test_tmp/err" ||
|
||||
fail "a conflict whose destination is a directory is not resolved"
|
||||
[[ -f "$replaced$stray" ]] ||
|
||||
fail "the leftover was moved inside the existing destination directory"
|
||||
pass "an existing destination directory is replaced, not moved into"
|
||||
|
||||
# One healable conflict beside one that is not. Moving only the first leaves the
|
||||
# retry blocked by the second, and that config inactive for nothing.
|
||||
fresh_work
|
||||
echo "ours" >"$stray"
|
||||
write_raw_report \
|
||||
"omarchy-settings-dev: $stray exists in filesystem" \
|
||||
"some-package: $work/theirs exists in filesystem (owned by other-package)"
|
||||
if run_update >"$test_tmp/out" 2>"$test_tmp/err"; then
|
||||
fail "an upgrade with an unhealable conflict reports success"
|
||||
fi
|
||||
[[ -e $stray && ! -e $replaced$stray ]] ||
|
||||
fail "a healable conflict is moved even though another conflict dooms the retry"
|
||||
pass "nothing moves unless every reported conflict is healable"
|
||||
|
||||
# The retry can still fail for an unrelated reason. Leave nothing inactive.
|
||||
fresh_work
|
||||
echo "ours" >"$stray"
|
||||
write_report omarchy-settings-dev "$stray"
|
||||
if RETRY_FAILS=1 run_update >"$test_tmp/out" 2>"$test_tmp/err"; then
|
||||
fail "a failed retry reports success"
|
||||
fi
|
||||
[[ -f $stray ]] ||
|
||||
fail "a failed retry leaves the file moved away and inactive"
|
||||
grep -qx "ours" "$stray" ||
|
||||
fail "the restored file is not the original content"
|
||||
[[ $(cat "$test_tmp/attempts") == 2 ]] ||
|
||||
fail "the handler and the upgrade re-invoke each other instead of stopping"
|
||||
pass "a failed retry puts the files back, without re-invoking the handler"
|
||||
|
||||
# A retry that failed after committing the files has nothing to restore, and
|
||||
# should not announce a restore it is not doing.
|
||||
fresh_work
|
||||
echo "ours" >"$stray"
|
||||
write_report omarchy-settings-dev "$stray"
|
||||
if RETRY_FAILS=1 RETRY_INSTALLS="$stray" run_update >"$test_tmp/out" 2>"$test_tmp/err"; then
|
||||
fail "a failed retry reports success"
|
||||
fi
|
||||
grep -qi "restoring" "$test_tmp/out" "$test_tmp/err" &&
|
||||
fail "a restore is announced when no file is put back"
|
||||
grep -qx "packaged" "$stray" ||
|
||||
fail "the file pacman installed was overwritten by the restore"
|
||||
pass "no restore is announced when there is nothing to put back"
|
||||
|
||||
# A dangling symlink reads as absent to -e, so a relative link that no longer
|
||||
# resolves from inside the quarantine must still be recognised and put back.
|
||||
fresh_work
|
||||
ln -s ./neighbour "$stray"
|
||||
write_report omarchy-settings-dev "$stray"
|
||||
if RETRY_FAILS=1 run_update >"$test_tmp/out" 2>"$test_tmp/err"; then
|
||||
fail "a failed retry reports success"
|
||||
fi
|
||||
[[ -L $stray ]] ||
|
||||
fail "a symlink that dangles from the quarantine is never restored"
|
||||
pass "a dangling symlink is restored rather than stranded in the quarantine"
|
||||
|
||||
# The handler acts on a pacman report; an old or hand-written one would move
|
||||
# live files aside for an upgrade that is not happening.
|
||||
fresh_work
|
||||
echo "ours" >"$stray"
|
||||
write_report omarchy-settings-dev "$stray"
|
||||
if PATH="$stub_bin:$ROOT/bin:$PATH" OMARCHY_REPLACED_DIR="$replaced" \
|
||||
bash "$ROOT/bin/omarchy-update-system-pkgs-when-conflicted" "$test_tmp/report" \
|
||||
>"$test_tmp/out" 2>"$test_tmp/err"; then
|
||||
fail "the handler acts on a report handed to it outside an update"
|
||||
fi
|
||||
[[ -f $stray ]] ||
|
||||
fail "the handler moved a live file when run outside an update"
|
||||
pass "the handler refuses a report handed to it outside an update"
|
||||
|
||||
# The happy path must not pay for any of this.
|
||||
fresh_work
|
||||
: >"$test_tmp/report"
|
||||
echo 1 >"$test_tmp/attempts"
|
||||
run_update >"$test_tmp/out" 2>"$test_tmp/err" ||
|
||||
fail "a clean upgrade fails"
|
||||
[[ $(cat "$test_tmp/attempts") == 2 ]] ||
|
||||
fail "a clean upgrade runs more than one pacman transaction"
|
||||
pass "a clean upgrade runs a single pacman transaction"
|
||||
Reference in New Issue
Block a user