Wren Linux

wren-linux / tools / test-apt-markers.sh

#!/bin/bash
# Checks lib/apt.sh (the marker packages behind apt_install and apt_purge)
# without touching the system: apt-get, apt-mark and dpkg-query are replaced
# by small fakes that keep their state in a temporary folder. Real tools are
# used for what is only file work (dpkg-deb), plus apt-get -s to see that
# apt can resolve a marker. Run by test.sh; or: bash tools/test-apt-markers.sh
. "$(dirname -- "${BASH_SOURCE[0]}")/../lib.sh"
set +e +o pipefail

ST=$(mktemp -d) || { echo "  FAIL  couldn't make a temp folder (is /tmp full?)"; exit 1; }
trap 'rm -rf "$ST"' EXIT
mkdir -p "$ST/pkg"
LOG=$ST/log
: >"$LOG"
fails=0
check() { # DESCRIPTION CONDITION-COMMAND...
    local what=$1
    shift
    if "$@" >/dev/null 2>&1; then printf '  ok    %s\n' "$what"; else printf '  FAIL  %s\n' "$what"; fails=$((fails + 1)); fi
}
logged() { grep -q -- "$1" "$LOG"; }
count() { grep -c -- "$1" "$LOG"; }
installed() { [ -f "$ST/pkg/$1" ]; }
mark() { cat "$ST/pkg/$1" 2>/dev/null; }

# ---- fakes: a package is a file holding "auto" or "manual", and
# $ST/pkg/NAME.deps holds what it depends on
SUDO=
have_internet() { return 0; }
fake_install() { # PKG MARK [DEPS]
    echo "$2" >"$ST/pkg/$1"
    echo "$3" >"$ST/pkg/$1.deps"
}
apt-get() {
    echo "apt-get $*" >>"$LOG"
    local cmd=$1 a d deps changed
    shift
    case $cmd in
        install)
            for a in "$@"; do
                case $a in
                    -*) ;;
                    *.deb)
                        deps=$(dpkg-deb -f "$a" Depends)
                        fake_install "$(dpkg-deb -f "$a" Package)" manual "$deps"
                        dpkg-deb -f "$a" Wren-Brought >"$ST/pkg/$(dpkg-deb -f "$a" Package).brought"
                        for d in $(tr -d ',' <<<"$deps"); do installed "$d" || fake_install "$d" auto ""; done ;;
                    *) installed "$a" || fake_install "$a" manual "" ;;
                esac
            done ;;
        purge)
            for a in "$@"; do
                case $a in -*) ;; *) rm -f "$ST/pkg/$a" "$ST/pkg/$a.deps" "$ST/pkg/$a.brought" ;; esac
            done ;;
        autoremove)
            changed=true
            while $changed; do
                changed=false
                for a in "$ST"/pkg/*; do
                    case $a in *.deps | *.brought | *.kept | *.prio | *.recs) continue ;; esac
                    [ -f "$a" ] || continue
                    [ "$(cat "$a")" = auto ] || continue
                    # (NAME.kept: an installed package recommends or suggests it, which
                    # autoremove keeps, as apt kept gpgv and zstd)
                    [ ! -e "$a.kept" ] || continue
                    if ! cat "$ST"/pkg/*.deps | tr -d ',' | tr ' ' '\n' | grep -qxF -- "$(basename -- "$a")"; then
                        rm -f "$a" "$a.deps"
                        changed=true
                    fi
                done
            done ;;
    esac
}
apt-mark() {
    echo "apt-mark $*" >>"$LOG"
    local a
    case $1 in
        showauto | showmanual)
            for a in "${@:2}"; do [ "$(cat "$ST/pkg/$a" 2>/dev/null)" = "${1#show}" ] && echo "$a"; done
            return 0 ;;
    esac
    for a in "${@:2}"; do [ -f "$ST/pkg/$a" ] && echo "$1" >"$ST/pkg/$a"; done
    return 0
}
# Reverse dependencies: the packages whose .deps name it, in apt-cache's layout
apt-cache() {
    local pkg=${*: -1} a
    echo "$pkg"
    echo "Reverse Depends:"
    # (--no-depends: what recommends it, from $ST/pkg/NAME.recs, a list of who does)
    if [[ " $* " == *" --no-depends "* ]]; then
        [ ! -f "$ST/pkg/$pkg.recs" ] || sed 's/^/  /' "$ST/pkg/$pkg.recs"
        return 0
    fi
    for a in "$ST"/pkg/*.deps; do
        tr -d ',' <"$a" | tr ' ' '\n' | grep -qxF -- "$pkg" && echo "  $(basename -- "$a" .deps)"
    done
    return 0
}
# shellcheck disable=SC2016  # the ${...} are dpkg-query field names
dpkg-query() {
    local fmt pkg a
    for a in "$@"; do
        case $a in -f=*) fmt=${a#-f=} ;; -W) ;; *) pkg=$a ;; esac
    done
    case $fmt in
        *'${Status}'*) installed "$pkg" && echo "install ok installed" ;;
        *'${Depends}'*) installed "$pkg" && sed 's/ *$//' "$ST/pkg/$pkg.deps" ;;
        *'${Wren-Brought}'*) installed "$pkg" && cat "$ST/pkg/$pkg.brought" 2>/dev/null ;;
        *'${Priority}'*) installed "$pkg" && { cat "$ST/pkg/$pkg.prio" 2>/dev/null || echo optional; } ;;
        *Status-Abbrev*)
            for a in "$ST"/pkg/wren-*; do
                case $a in *.deps | *.brought | *.kept | *.prio | *.recs) continue ;; esac
                [ -f "$a" ] && echo "ii  $(basename -- "$a")"
            done ;;
    esac
    return 0
}
unset WREN_APT_UPDATED

echo "Marker package names"
check "named after the script" test "$(bash -c '. "$1/lib.sh"; apt_marker' "parts/Foo_Bar.sh" "$WREN_DIR")" = wren-foo-bar
check "WREN_OWNER wins" test "$(WREN_OWNER=Whonix bash -c '. "$1/lib.sh"; apt_marker' x "$WREN_DIR")" = wren-whonix

echo "Building a marker"
mkdir "$ST/b"
# with the private umask parts/home-private.sh sets (077), as in a real login
(umask 077; marker_build wren-demo "$ST/b" mat2 mpv mat2)
check "it is a valid package, also with umask 077" dpkg-deb -I "$ST/b/wren-demo.deb"
check "...readable by apt's own user (_apt)" test "$(stat -c %a "$ST/b/wren-demo.deb")" = 644
check "it depends on the sorted, unique packages" test "$(dpkg-deb -f "$ST/b/wren-demo.deb" Depends)" = "mat2, mpv"
if [ -x /usr/bin/apt-get ]; then
    check "apt can resolve it" /usr/bin/apt-get -s -o Debug::NoLocking=1 install --no-install-recommends "$ST/b/wren-demo.deb"
fi

echo "apt_install"
export WREN_OWNER=alpha
apt_install aaa bbb >/dev/null
check "the marker is installed" installed wren-alpha
check "its packages came in as dependencies" bash -c "[ \"$(mark aaa)\" = auto ] && [ \"$(mark bbb)\" = auto ]"
check "the marker itself is manual" test "$(mark wren-alpha)" = manual
n=$(count '^apt-get install')
apt_install aaa >/dev/null
check "asking for recorded packages again runs no apt" test "$(count '^apt-get install')" = "$n"
apt_install ccc >/dev/null
check "a second call merges with the first" test "$(tr -d ',\n' <"$ST/pkg/wren-alpha.deps")" = "aaa bbb ccc"
check "apt-get update ran only once" test "$(count '^apt-get update')" = 1

echo "Shared packages"
export WREN_OWNER=beta
apt_install bbb ddd >/dev/null
export WREN_OWNER=alpha
apt_purge aaa bbb ccc >/dev/null
check "purging removes the marker" bash -c "! [ -f '$ST/pkg/wren-alpha' ]"
check "packages only alpha needed are gone" bash -c "! [ -f '$ST/pkg/aaa' ] && ! [ -f '$ST/pkg/ccc' ]"
check "a package beta still needs stays" installed bbb
check "autoremove ran" logged '^apt-get autoremove --purge'
export WREN_OWNER=beta
apt_purge >/dev/null
check "beta's purge removes the rest" bash -c "! [ -f '$ST/pkg/bbb' ] && ! [ -f '$ST/pkg/ddd' ]"

echo "Packages from a .deb file"
mkdir "$ST/f"
printf 'Package: fakeapp\nVersion: 1\nArchitecture: all\nMaintainer: x\nDescription: x\n' >/dev/null
mkdir -p "$ST/f/root/DEBIAN"
chmod 0755 "$ST/f/root" "$ST/f/root/DEBIAN"
printf 'Package: fakeapp\nVersion: 1\nArchitecture: all\nMaintainer: x <x@localhost>\nDescription: test\n' >"$ST/f/root/DEBIAN/control"
dpkg-deb --root-owner-group --build "$ST/f/root" "$ST/f/fakeapp.deb" >/dev/null
export WREN_OWNER=gamma
apt_install "$ST/f/fakeapp.deb" libfoo >/dev/null
check "the marker depends on the package in the file" bash -c "grep -q fakeapp '$ST/pkg/wren-gamma.deps' && grep -q libfoo '$ST/pkg/wren-gamma.deps'"
check "the file's package was made automatic" test "$(mark fakeapp)" = auto
apt_purge fakeapp >/dev/null
check "removing takes it away" bash -c "! [ -f '$ST/pkg/fakeapp' ]"

echo "Packages that were there before a script asked for them"
# keepme: installed by Debian's installer, automatic, kept by autoremove (something recommends
# it), as gpgv and zstd were; newone: not there before
fake_install keepme auto ""
: >"$ST/pkg/keepme.kept"
export WREN_OWNER=gamma
apt_install keepme newone >/dev/null
check "the marker records what the script brought in, and only that" test "$(cat "$ST/pkg/wren-gamma.brought")" = newone
apt_install third >/dev/null
check "...and keeps it through a second call" test "$(cat "$ST/pkg/wren-gamma.brought")" = "newone, third"
apt_purge >/dev/null
check "removing it takes out what it brought" bash -c "! [ -f '$ST/pkg/newone' ] && ! [ -f '$ST/pkg/third' ]"
check "...but not what was there before, automatic or not (it took gpgv: tried for real)" test "$(mark keepme)" = auto
# a marker from before the record existed (every install so far): what it brought isn't known.
# Its packages autoremove keeps go, as they always did (the ultra review: otherwise --remove
# left them all); but not one Debian counts as its base system, or one something recommends
fake_install olddep auto ""; : >"$ST/pkg/olddep.kept"
fake_install basepkg auto ""; : >"$ST/pkg/basepkg.kept"; echo important >"$ST/pkg/basepkg.prio"
fake_install recpkg auto ""; : >"$ST/pkg/recpkg.kept"; echo initramfs-tools-core >"$ST/pkg/recpkg.recs"
fake_install wren-delta manual "olddep, basepkg, recpkg"
export WREN_OWNER=delta
apt_purge >/dev/null
check "a marker with no record: what it depends on that autoremove keeps is taken out, as before" bash -c "! [ -f '$ST/pkg/olddep' ]"
check "...but not a package of Debian's base system (priority important: gpgv)" test "$(mark basepkg)" = auto
check "...nor one an installed package recommends (zstd, for initramfs-tools)" test "$(mark recpkg)" = auto
check "...and the marker itself is removed" bash -c "! [ -f '$ST/pkg/wren-delta' ]"
# a marker of today's that brought nothing: its record says so ("-"), so nothing is taken out
fake_install already auto ""; : >"$ST/pkg/already.kept"
export WREN_OWNER=epsilon
apt_install already >/dev/null
check "a script that brought nothing: its marker says so, and isn't taken for one from before" \
    test "$(cat "$ST/pkg/wren-epsilon.brought")" = "-"
apt_purge >/dev/null
check "...so removing it takes out nothing that was there before" test "$(mark already)" = auto
# shared: zeta brings shared1; eta asks for it too, after: it records it as well, so whichever
# goes last takes it away (the ultra review: only the first recorded it, and it stayed forever)
export WREN_OWNER=zeta
apt_install shared1 >/dev/null
: >"$ST/pkg/shared1.kept"
export WREN_OWNER=eta
apt_install shared1 >/dev/null
check "a package another script's marker brought: this one records it too" grep -qx shared1 "$ST/pkg/wren-eta.brought"
export WREN_OWNER=zeta
apt_purge >/dev/null
check "...the first one's removal keeps it, the other still needs it" installed shared1
export WREN_OWNER=eta
apt_purge >/dev/null
check "...and the last one's removal takes it away" bash -c "! [ -f '$ST/pkg/shared1' ]"
rm -f "$ST"/pkg/keepme* "$ST"/pkg/olddep* "$ST"/pkg/basepkg* "$ST"/pkg/recpkg* "$ST"/pkg/already* "$ST"/pkg/shared1*

echo "Packages installed before markers existed"
fake_install oldpkg manual ""
fake_install sharedpkg manual ""
fake_install wren-other manual "sharedpkg"
export WREN_OWNER=delta
apt_purge oldpkg sharedpkg >/dev/null
check "an old package is purged by name" bash -c "! [ -f '$ST/pkg/oldpkg' ]"
check "unless another script's marker needs it" installed sharedpkg
# a --remove cut off after its marker was purged: the next run has only the
# names, and openssl among them must not take ca-certificates with it
fake_install openssl manual ""
fake_install ca-certificates manual "openssl"
fake_install tor manual ""
fake_install torsocks manual "tor"
apt_purge tor torsocks openssl >/dev/null
check "no marker: a named package something else installed depends on stays (openssl for ca-certificates)" installed openssl
check "...and what depends on it too" installed ca-certificates
check "...one only the other named packages depend on goes (tor, for torsocks)" bash -c "! [ -f '$ST/pkg/tor' ] && ! [ -f '$ST/pkg/torsocks' ]"
fake_install untouched manual ""
apt_purge >/dev/null
check "with nothing recorded and no names, nothing is removed" installed untouched

echo "Switching to a package that conflicts with the old one"
export WREN_OWNER=epsilon
apt_install oldname extra >/dev/null
apt_drop oldname
apt_install newname >/dev/null
check "the dropped package leaves the marker" test "$(tr -d ',\n' <"$ST/pkg/wren-epsilon.deps")" = "extra newname"
APT_DROP=()
apt_drop extra
n=$(count '^apt-get install')
apt_install newname >/dev/null
check "dropping works even when nothing new is asked for" test "$(tr -d ',\n' <"$ST/pkg/wren-epsilon.deps")" = newname
check "and the marker was rebuilt for it" test "$(count '^apt-get install')" -gt "$n"

# need_tools: only the packages of what is missing, each once, through apt_install
(
    # shellcheck disable=SC2317,SC2329  # called by need_tools
    apt_install() { printf '%s\n' "$*" >"$ST/need"; }
    : >"$ST/need"
    need_tools bash:bash "$ST:nothing" >/dev/null 2>&1
    [ ! -s "$ST/need" ] || exit 1
    need_tools no-such-cmd-a:pkg-a no-such-cmd-b:pkg-a "$ST/no-such-file:pkg-b" bash:bash >/dev/null 2>&1
    [ "$(cat "$ST/need")" = "pkg-a pkg-b" ]
)
check "need_tools installs only what is missing (commands and files), each package once" test $? -eq 0

[ "$fails" -eq 0 ]