mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-23 03:27:43 +09:00
- Harness: bootstrap find/awk in distro-compatibility.py (AL2023, Tumbleweed, SLES BCI minimal images lack them; the source-level suites legitimately use both) - Python gate 3.11-3.14: Arch/Manjaro rolling ship only python 3.14 with no versioned fallback packages (same trap as Debian 13/#99); align scripts/update.sh, which had drifted to a 3.11-3.12 gate - Gentoo: add emerge as a supported installer package-manager path (install.sh, scripts/lib/pkg.sh, distro.py family, deps manifest); provision via slotted dev-lang/python:3.13 -> 3.12 - Tests: move out-of-range stubs above the new ceiling, shadow all locate_python candidate names (real python3.14/3.13 in Manjaro/ Gentoo images were winning over fixtures), cover the 7th manager - Catalog: Gentoo is no longer a documented expected failure Verified: test_lib 32/32, test_install 124/124, test_update 76/76, pytest test_serverkit_installer 46/46, and all 6 previously failing images now pass both quick and provision container probes (12/12).
189 lines
8.0 KiB
Bash
189 lines
8.0 KiB
Bash
# shellcheck shell=bash
|
|
#
|
|
# scripts/lib/pkg.sh — sourceable cross-distro package-management abstraction.
|
|
#
|
|
# The ServerKit installer/updater needs to refresh the package index, install a
|
|
# handful of OS packages, and check whether something is already present — and it
|
|
# has to do this identically on Debian/Ubuntu (apt), RHEL/Rocky/Alma/Fedora
|
|
# (dnf, or yum on older boxes), openSUSE (zypper), Arch (pacman), Alpine (apk)
|
|
# and Gentoo (emerge). Sprinkling per-distro `if command -v apt` branches through every script
|
|
# rots fast and is impossible to unit-test, so we centralise the dispatch here.
|
|
#
|
|
# This bash helper mirrors the semantics of the Python PackageManager class in
|
|
# backend/app/utils/system.py (same detect → install/is_installed contract,
|
|
# same sudo-only-when-needed rule) so the host and the running panel agree on
|
|
# what "installed" means. It covers the three managers the Python side knows
|
|
# (apt/dnf/yum) plus the four install-time-only ones (zypper/pacman/apk/emerge)
|
|
# that the shell installer can hit before the panel is even up.
|
|
#
|
|
# Contract (these names/behaviours are depended on by callers and unit tests —
|
|
# do not rename them or add extra public functions):
|
|
# pkg_detect → echoes: apt|dnf|yum|zypper|pacman|apk|emerge|"" (none)
|
|
# pkg_refresh → refresh the package index (best-effort)
|
|
# pkg_install <pkg> [pkg...] → install packages non-interactively
|
|
# pkg_is_installed <pkg> → return 0 if installed, 1 otherwise
|
|
#
|
|
# Honors:
|
|
# PKG_DRY_RUN=1 print intended commands instead of running them
|
|
# PKG_MGR_OVERRIDE=... force the detected manager (used by unit tests)
|
|
#
|
|
# Everything mutating is best-effort and privilege-aware: a refresh/install we
|
|
# can't run cleanly degrades to a non-zero return rather than aborting the
|
|
# caller, and sudo is prepended only when we are not already root.
|
|
|
|
# Prepend sudo for a mutating command when we are not root AND sudo exists;
|
|
# otherwise run the command as-is. Mirrors _needs_sudo() in system.py — minimal
|
|
# containers run as root without sudo installed, and Windows dev boxes have no
|
|
# sudo at all, so blindly prefixing it would break those.
|
|
_pkg_sudo() {
|
|
if [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then
|
|
sudo "$@"
|
|
return $?
|
|
fi
|
|
"$@"
|
|
}
|
|
|
|
# Run a (privileged) mutating package command, or just print it under dry-run.
|
|
# Kept tiny so the dry-run output is exactly the command that would otherwise
|
|
# execute — which is what the installer unit tests assert against.
|
|
_pkg_run() {
|
|
if [ "${PKG_DRY_RUN:-0}" = "1" ]; then
|
|
printf ' [pkg] would run: %s\n' "$*"
|
|
return 0
|
|
fi
|
|
# Discard the tool's own chatter (progress bars, "Nothing to do" noise) here
|
|
# so callers don't redirect — that redirection would also swallow the
|
|
# dry-run line above, which the installer unit tests assert on.
|
|
_pkg_sudo "$@" >/dev/null 2>&1
|
|
}
|
|
|
|
# Detect the active package manager. Order matters and is fixed (apt first):
|
|
# a box can have several of these binaries installed, so we pick the first that
|
|
# is present in the documented precedence. An explicit override short-circuits
|
|
# detection entirely — that is how the unit tests force a given manager.
|
|
pkg_detect() {
|
|
if [ -n "${PKG_MGR_OVERRIDE:-}" ]; then
|
|
printf '%s\n' "$PKG_MGR_OVERRIDE"
|
|
return 0
|
|
fi
|
|
local mgr
|
|
for mgr in apt dnf yum zypper pacman apk emerge; do
|
|
if command -v "$mgr" >/dev/null 2>&1; then
|
|
printf '%s\n' "$mgr"
|
|
return 0
|
|
fi
|
|
done
|
|
# No supported manager found: echo nothing (empty string).
|
|
printf '\n'
|
|
return 0
|
|
}
|
|
|
|
# Refresh the package index for the detected manager. Best-effort: index
|
|
# refreshes routinely fail on a flaky mirror and that should not abort an
|
|
# install, so we never propagate the manager's exit here — every arm is
|
|
# guarded and the function always returns 0.
|
|
pkg_refresh() {
|
|
local mgr
|
|
mgr="$(pkg_detect)"
|
|
case "$mgr" in
|
|
apt) _pkg_run apt-get update || true ;;
|
|
dnf) _pkg_run dnf makecache --refresh || true ;;
|
|
yum) _pkg_run yum makecache || true ;;
|
|
zypper) _pkg_run zypper --non-interactive refresh || true ;;
|
|
pacman) _pkg_run pacman -Sy --noconfirm || true ;;
|
|
apk) _pkg_run apk update || true ;;
|
|
emerge) _pkg_run emerge --sync || true ;;
|
|
*) : ;;
|
|
esac
|
|
return 0
|
|
}
|
|
|
|
# Install one or more packages non-interactively for the detected manager.
|
|
# Propagates the manager's non-zero exit on failure (callers want to know an
|
|
# install failed); returns 1 with a stderr note when no manager was detected.
|
|
pkg_install() {
|
|
if [ "$#" -eq 0 ]; then
|
|
return 0
|
|
fi
|
|
local mgr
|
|
mgr="$(pkg_detect)"
|
|
case "$mgr" in
|
|
# Deliberate word-splitting of the package list via "$@" — each argument
|
|
# is already a separate, individually-quoted package name.
|
|
# `env VAR=…` rather than an exported variable: _pkg_sudo runs plain
|
|
# `sudo "$@"`, and sudo's env_reset would drop an exported
|
|
# DEBIAN_FRONTEND before apt ever saw it. Putting it in argv survives.
|
|
# Without it, debconf (tzdata et al) opens a dialog and waits forever —
|
|
# the docstring above promised "non-interactively" and only apt did not
|
|
# deliver it. --force-conf* covers modified-conffile prompts, which
|
|
# DEBIAN_FRONTEND does not suppress.
|
|
apt) _pkg_run env DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
|
-o Dpkg::Options::=--force-confold \
|
|
-o Dpkg::Options::=--force-confdef "$@" ;;
|
|
dnf) _pkg_run dnf install -y "$@" ;;
|
|
yum) _pkg_run yum install -y "$@" ;;
|
|
zypper) _pkg_run zypper --non-interactive install "$@" ;;
|
|
pacman) _pkg_run pacman -S --noconfirm "$@" ;;
|
|
apk) _pkg_run apk add "$@" ;;
|
|
# --ask=n pins emerge's explicit-prompt flag off; emerge is already
|
|
# non-interactive by default, this just keeps that true under MAKEOPTS
|
|
# or config drift (mirrors the non-interactivity every other arm gets).
|
|
emerge) _pkg_run emerge --noreplace --ask=n "$@" ;;
|
|
*)
|
|
printf ' [pkg] no supported package manager found — cannot install: %s\n' "$*" >&2
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Return 0 if <pkg> is installed, 1 otherwise. Query-only (no sudo): we use the
|
|
# distro's low-level package database rather than the high-level manager so the
|
|
# answer matches what an install would see. Each branch tolerates the query tool
|
|
# being absent (it returns non-zero / 1 rather than aborting the caller).
|
|
pkg_is_installed() {
|
|
local pkg="$1"
|
|
local mgr
|
|
mgr="$(pkg_detect)"
|
|
case "$mgr" in
|
|
apt)
|
|
# dpkg can report a removed-but-configured package with returncode 0,
|
|
# so additionally require the "install ok installed" status line —
|
|
# matching PackageManager.is_installed() in system.py.
|
|
local out
|
|
if ! command -v dpkg >/dev/null 2>&1; then
|
|
return 1
|
|
fi
|
|
out="$(dpkg -s "$pkg" 2>/dev/null)" || return 1
|
|
case "$out" in
|
|
*'install ok installed'*) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
;;
|
|
dnf|yum|zypper)
|
|
command -v rpm >/dev/null 2>&1 || return 1
|
|
rpm -q "$pkg" >/dev/null 2>&1
|
|
;;
|
|
pacman)
|
|
command -v pacman >/dev/null 2>&1 || return 1
|
|
pacman -Q "$pkg" >/dev/null 2>&1
|
|
;;
|
|
apk)
|
|
command -v apk >/dev/null 2>&1 || return 1
|
|
apk info -e "$pkg" >/dev/null 2>&1
|
|
;;
|
|
emerge)
|
|
# Portage's installed database is /var/db/pkg/<category>/<name-ver>;
|
|
# strip any :slot suffix and glob for a matching install dir.
|
|
local q="${pkg%%:*}"
|
|
[ -d /var/db/pkg ] || return 1
|
|
case "$q" in
|
|
*/*) compgen -G "/var/db/pkg/$q-*" >/dev/null ;;
|
|
*) compgen -G "/var/db/pkg/*/$q-*" >/dev/null ;;
|
|
esac
|
|
;;
|
|
*)
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|