feat(info): show the install reason and drop the indentation

N? and `info PKG` now print three lines, none indented:

    PKG: <description>
    reason: explicitly installed | installed as a dependency | unknown | not installed
    required by: <installed dependents, comma separated> | nothing

The reason comes from a new <be>_reason backend function: apt-mark
showauto / showmanual (dpkg decides for a package in neither list), the
Install Reason of pacman -Qi, dnf repoquery --installed %{reason} (dnf4
and dnf5 words) and the Installed line of zypper info (rpm plus libzypp's
AutoInstalled when zypper is locked). A row whose old version is "-" is
not installed and the backend is not asked. The "(installed)" qualifier
moves to --help, README and DESIGN.

The rpm E2E fixture and e2e_reset now also mark su-dummy-a and su-dummy-c
as user-installed on dnf, which records plain rpm installs as "unknown",
so 1? and 2? show both reasons on all 12 distros. The verified commands
are recorded in discovery/{apt,pacman,rpm}.md.
This commit is contained in:
bryanpedini committed 2026-10-10 13:32:42 +02:00
1 parent 1354db6c39
commit 71d7f87aa3
21 files changed
+589 -48

No files matched your search

+26
View File
@@ -7,6 +7,10 @@
# FAKE_APT_MARK_UNHOLD_SIGNAL=SIG: send SIG to the applet before an unhold (a second Ctrl-C).
# FAKE_APT_MARK_UNHOLD_RC=N: unhold fails (nothing released). FAKE_APT_MARK_FAIL_MSG: the message a
# failing hold prints. Every unhold logs whether $STATE_DIR/holds.lock is held by someone.
# showauto/showmanual: installed = the "?i" rows of data/apt/dpkg-status.tsv (not purged), auto = those
# listed in data/apt/auto.txt, manual = the others; FAKE_APT_MARK_NEITHER="pkg ..." are in neither
# list, FAKE_APT_MARK_SHOW_RC=N makes both fail. Like the real ones they print only the named
# packages (all of them without a name), native packages without their ":arch" qualifier.
# shellcheck source=../fakelib.bash
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
fake_log "$@"
@@ -27,8 +31,30 @@ case $verb in hold | unhold)
fi
;;
esac
# show_marked auto|manual NAME...
show_marked() {
local want=$1 n st name mark
shift
local -A pick=()
for n in "$@"; do pick[${n%:all}]=1; done
if [[ -n ${FAKE_APT_MARK_SHOW_RC:-} ]]; then
echo "E: Unable to determine the package states" >&2
exit "$FAKE_APT_MARK_SHOW_RC"
fi
while IFS=$'\t' read -r st name _; do
[[ $st == ?i* ]] || continue
fake_purged "$name" && continue
[[ " ${FAKE_APT_MARK_NEITHER:-} " == *" $name "* ]] && continue
if grep -qxF -- "$name" "$FAKE_DATA/apt/auto.txt"; then mark=auto; else mark=manual; fi
[[ $mark == "$want" ]] || continue
if (($# == 0)) || [[ -n ${pick[$name]:-} || -n ${pick[$name:amd64]:-} ]]; then printf '%s\n' "$name"; fi
done <"$FAKE_DATA/apt/dpkg-status.tsv" | sort
}
case $verb in
showhold) sort "$h" ;;
showauto) show_marked auto "$@" ;;
showmanual) show_marked manual "$@" ;;
hold)
for p in "$@"; do
if grep -qxF -- "$p" "$h"; then echo "$p was already set on hold."; else
+26 -2
View File
@@ -2,7 +2,8 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2026 Bryan Joshua Pedini
# fake dpkg-query (unit tests): the dpkg database dump used for dependency closures
# fake dpkg-query (unit tests): the dpkg database dump used for dependency closures, and
# `-W -f '${db:Status-Abbrev}' NAME` (the status of one package; purged = unknown to dpkg)
# shellcheck source=../fakelib.bash
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
fake_log "$@"
@@ -10,7 +11,30 @@ fake_log "$@"
st=${SKEPTICAL_UPDATER_STATE_DIR:-}/trusted.tsv
if [[ -n ${FAKE_STATE_APPEND:-} ]]; then printf '%s\n' "$FAKE_STATE_APPEND" >>"$st"; fi
if [[ -n ${FAKE_STATE_SED:-} ]]; then sed -i "$FAKE_STATE_SED" "$st"; fi
case " $* " in
args=" $* "
names=()
while (($#)); do
case $1 in
-f) shift ;;
-*) ;;
*) names+=("$1") ;;
esac
shift
done
if ((${#names[@]})); then
rc=0
for n in "${names[@]}"; do
st=$(awk -F'\t' -v n="${n%:amd64}" '$2 == n { print $1; exit }' "$FAKE_DATA/apt/dpkg-status.tsv")
if [[ -z $st ]] || fake_purged "${n%:amd64}"; then
echo "dpkg-query: no packages found matching $n" >&2
rc=1
continue
fi
printf '%s' "$st"
done
exit "$rc"
fi
case $args in
*' -W '*) cat "$FAKE_DATA/apt/dpkg-status.tsv" ;;
*)
echo "fake dpkg-query: unhandled: $*" >&2
+24 -1
View File
@@ -5,7 +5,9 @@
# fake dnf (unit tests). FAKE_DNF_MAJOR=4|5 picks the output dialect; FAKE_DNF_CHECK_RC=0|1 simulates
# "no updates" / an error; FAKE_DNF_CHECK=obsoletes replays a check-update with only an
# "Obsoleting Packages" section; FAKE_DNF_REMOVE_REFUSE=1 makes `--assumeno remove` fail;
# removal tables: see the --assumeno remove branch.
# removal tables: see the --assumeno remove branch. `repoquery --installed --qf %{reason}`: REASON of
# data/rpm/db.bash (dnf5's words with FAKE_DNF_MAJOR=5), FAKE_DNF_REASON="pkg=raw value" overrides
# it, FAKE_DNF_REPOQUERY_RC=N makes repoquery fail.
# shellcheck source=../fakelib.bash
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
# shellcheck source=../../data/rpm/db.bash
@@ -41,7 +43,28 @@ case " $* " in
;;
*' repoquery '*)
p=${pos[-1]}
rc=$(fake_rc FAKE_DNF_REPOQUERY_RC 0)
if ((rc)); then
echo "Error: rpmdb open failed" >&2
exit "$rc"
fi
case " $* " in
*' --installed '*'%{reason}'*)
installed "$p" || exit 0
r=${REASON[$p]:-unknown}
if ((major == 5)); then
case $r in
user) r=User ;;
dependency) r=Dependency ;;
weak-dependency) r='Weak Dependency' ;;
group) r=Group ;;
unknown) r='External User' ;;
esac
fi
[[ ${FAKE_DNF_REASON:-} != "$p="* ]] || r=${FAKE_DNF_REASON#*=}
printf '%s\n' "$r"
((major == 5)) || echo
;;
*' --upgrades '*) case $p in su-dummy-[abc]) printf 'skeptical-updater dummy %s (v2.0)\n' "$p"; ((major == 5)) || echo ;; esac ;;
*) case $p in su-dummy-new) printf 'skeptical-updater dummy su-dummy-new (v1.0)\n' ;; esac ;;
esac
+14 -1
View File
@@ -10,6 +10,7 @@
# FAKE_PACMAN_STDERR=TEXT: -Sl and -Qu also print TEXT on stderr (an unreadable database).
# -Rs --print: FAKE_PACMAN_RS_REFUSE=1 makes pacman refuse ("breaks dependency");
# FAKE_PACMAN_RS_EXTRA="pkg:other" adds other to the plan of pkg.
# FAKE_PACMAN_REASON="pkg=text" replaces the "Install Reason" of pkg in -Qi (empty text: no such line).
# FAKE_PACMAN_QU=fixture|real|none selects the -Qu sample,
# FAKE_PACMAN_SL=empty makes -Sl print nothing, FAKE_PACMAN_SUP_EXTRA="a b" adds packages that
# only -Sup (the real transaction) shows. Every -Q*/-S[il] call records its COLUMNS value, every
@@ -107,7 +108,19 @@ case $op in
[[ -z ${FAKE_PACMAN_STDERR:-} ]] || echo "$FAKE_PACMAN_STDERR" >&2
[[ ${FAKE_PACMAN_SL:-} == empty ]] || cat "$D/Sl.txt"
;;
-Qi) blocks "$D/Qi.txt" "${targets[@]}" ;;
-Qi)
if [[ -z ${FAKE_PACMAN_REASON:-} ]]; then
blocks "$D/Qi.txt" "${targets[@]}"
exit
fi
rc=0
out=$(blocks "$D/Qi.txt" "${targets[@]}") || rc=$?
awk -v n="${FAKE_PACMAN_REASON%%=*}" -v r="${FAKE_PACMAN_REASON#*=}" '
/^Name +: / { cur = $0; sub(/^Name +: /, "", cur) }
/^Install Reason +: / && cur == n { if (r != "") print "Install Reason : " r; next }
{ print }' <<<"$out"
exit "$rc"
;;
-Si)
t=()
for a in "${targets[@]}"; do t+=("${a#*/}"); done
+7
View File
@@ -6,6 +6,8 @@
# FAKE_ZYPPER_DUP=fixture|empty|removes the `dist-upgrade --dry-run` plan (data/zypper/dup-*.xml).
# addlock writes real zypp lock entries into the locksfile.path of $ZYPP_CONF (default
# /etc/zypp/locks is never touched: the fake refuses); update/dist-upgrade log that file.
# info replays data/zypper/info-NAME.txt (exit 104 without one); FAKE_ZYPPER_INFO_RC=7 makes it fail
# like a zypp lock held by another process.
# shellcheck source=../fakelib.bash
. "${BASH_SOURCE[0]%/*}/../fakelib.bash"
fake_log "${ZYPP_CONF:+ZYPP_CONF=$ZYPP_CONF }$*"
@@ -43,6 +45,11 @@ refresh)
list-updates) cat "$D/lu-${FAKE_ZYPPER_LU:-fixture}.xml" ;;
info)
p=${args[-1]}
if [[ -n ${FAKE_ZYPPER_INFO_RC:-} ]]; then
echo "System management is locked by the application with pid 4242 (zypper)."
echo "Close this application before trying again."
exit "$FAKE_ZYPPER_INFO_RC"
fi
if [[ -f $D/info-$p.txt ]]; then cat "$D/info-$p.txt"; else
echo "package '$p' not found."
exit 104