feat(packaging): nfpm-based deb + rpm build infrastructure (spec §3-7, ADR 0007)
Implement the packaging configuration, staging script, maintainer scripts, and container test harness for building native deb and rpm packages. Core files: - packaging/nfpm.yaml: single source of truth for both formats - packaging/stage.sh: builds staged tree (venv, wrapper, helpers, units, polkit, sysusers, tmpfiles) - packaging/fenris.conf: placeholder-commented default configuration - packaging/postinst.sh, prerm.sh, postrm.sh: POSIX-compatible deb maintainer scripts - packaging/rpm/post.sh, preun.sh, postun.sh: RPM scriptlets - packaging/sysusers.d/fenris.conf, tmpfiles.d/fenris.conf: systemd fragments - packaging/fenris.repo: dnf consumer setup - packaging/keys/fenris-packaging.asc: public key placeholder Build targets added to Makefile: stage, package-deb, package-rpm, package, release, clean. Container test harness in tests/test_packaging.py covering dormant install, migration guard, upgrade semantics, and removal semantics across the compatibility matrix (Debian 12, Ubuntu 22.04/24.04, Fedora 40). Dormant CI workflow at .gitea/workflows/release.yml. All 289 existing tests pass without regression. Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
co-authored by
CommandCodeBot
parent
b005049733
commit
babc8eeeb1
@@ -0,0 +1,11 @@
|
||||
# Fenris configuration
|
||||
#
|
||||
# This file is managed by the fenris package. Local edits are preserved
|
||||
# across upgrades; changed defaults appear as .dpkg-new / .rpmnew.
|
||||
#
|
||||
# The device selector specifies which NVMe drive to monitor.
|
||||
# Uncomment and set exactly one device path:
|
||||
#
|
||||
# devices = /dev/disk/by-id/nvme-Samsung_SSD_980_PRO_500GB_S5PANS0T123456
|
||||
#
|
||||
# See https://git.bongbetic.com/xavierk/Fenris for documentation.
|
||||
@@ -0,0 +1,7 @@
|
||||
[fenris]
|
||||
name=Fenris NVMe Monitor
|
||||
baseurl=https://git.bongbetic.com/api/packages/xavierk/rpm/fenris
|
||||
enabled=1
|
||||
gpgcheck=1
|
||||
gpgkey=https://git.bongbetic.com/xavierk/Fenris/raw/branch/main/packaging/keys/fenris-packaging.asc
|
||||
repo_gpgcheck=0
|
||||
@@ -0,0 +1,15 @@
|
||||
# Fenris Packaging Key — placeholder
|
||||
#
|
||||
# The public half of the dedicated RSA-3072 packaging key used to sign rpm
|
||||
# payloads and clearsign SHA256SUMS manifests.
|
||||
#
|
||||
# The private half lives only in the password manager. Each release performs:
|
||||
# import → sign → delete. No machine permanently holds signing material.
|
||||
#
|
||||
# Key details (published with the first Release):
|
||||
# Algorithm: RSA 3072
|
||||
# UID: Fenris Packaging <packaging@bongbetic.com>
|
||||
# Expiry: 2 years from creation
|
||||
#
|
||||
# This file will be replaced with the real public key at the time of the
|
||||
# first Release. Its raw URL doubles as the dnf gpgkey target.
|
||||
@@ -0,0 +1,65 @@
|
||||
name: fenris
|
||||
arch: amd64
|
||||
platform: linux
|
||||
version: "${VERSION}"
|
||||
maintainer: Fenris Maintainers <ops@bongbetic.com>
|
||||
description: >
|
||||
NVMe wear monitor with persistent TUI — observes real-world drive use and
|
||||
translates it into an understandable endurance outlook.
|
||||
homepage: https://git.bongbetic.com/xavierk/Fenris
|
||||
license: Proprietary
|
||||
|
||||
depends:
|
||||
- python3 (>= 3.10)
|
||||
- smartmontools
|
||||
- systemd
|
||||
|
||||
contents:
|
||||
# Staged tree: venv, wrapper, helpers, units, polkit, sysusers, tmpfiles
|
||||
- src: build/stage/
|
||||
dst: /
|
||||
type: tree
|
||||
|
||||
# Configuration directory
|
||||
- dst: /etc/fenris
|
||||
type: dir
|
||||
file_info:
|
||||
mode: 0755
|
||||
|
||||
# Default placeholder-commented config (conffile for deb)
|
||||
- src: packaging/fenris.conf
|
||||
dst: /etc/fenris/fenris.conf
|
||||
type: config
|
||||
file_info:
|
||||
mode: 0644
|
||||
|
||||
# Observation store directory — owned by package, never packed
|
||||
# deb: created in postinst; rpm: %ghost
|
||||
- dst: /var/lib/fenris
|
||||
type: ghost
|
||||
file_info:
|
||||
mode: 2750
|
||||
|
||||
scripts:
|
||||
preinstall: packaging/preinst.sh
|
||||
postinstall: packaging/postinst.sh
|
||||
preremove: packaging/prerm.sh
|
||||
postremove: packaging/postrm.sh
|
||||
|
||||
overrides:
|
||||
deb:
|
||||
depends:
|
||||
- python3 (>= 3.10)
|
||||
- smartmontools
|
||||
- systemd
|
||||
|
||||
rpm:
|
||||
depends:
|
||||
- python3 >= 3.10
|
||||
- smartmontools
|
||||
- systemd
|
||||
scripts:
|
||||
preinstall: packaging/preinst.sh
|
||||
postinstall: packaging/rpm/post.sh
|
||||
preremove: packaging/rpm/preun.sh
|
||||
postremove: packaging/rpm/postun.sh
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/bin/sh
|
||||
# postinst — install and upgrade paths (spec §7).
|
||||
#
|
||||
# dpkg calls: postinst configure [most-recently-configured-version]
|
||||
# fresh install: $1 = "configure"
|
||||
# upgrade: $1 = "configure" (old version as $2 when available)
|
||||
#
|
||||
# rpm calls: %post $1 = 1 (fresh install) / 2 (upgrade)
|
||||
# (handled by packaging/rpm/post.sh)
|
||||
#
|
||||
# Install: sysusers, tmpfiles, daemon-reload — nothing else (dormant).
|
||||
# Upgrade: snapshot, migration, daemon-reload, conditional timer restart.
|
||||
set -eu
|
||||
|
||||
STORE_DIR="/var/lib/fenris"
|
||||
STORE_DB="${STORE_DIR}/observations.db"
|
||||
STORE_BAK="${STORE_DIR}/observations.db.bak"
|
||||
VENV_PYTHON="/opt/fenris/bin/python3"
|
||||
|
||||
case "${1:-}" in
|
||||
configure)
|
||||
if [ -n "${2:-}" ]; then
|
||||
# Upgrade — $2 is the old version
|
||||
# Snapshot observation store (one generation)
|
||||
if [ -f "${STORE_DB}" ]; then
|
||||
cp "${STORE_DB}" "${STORE_BAK}" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Forward-only schema migration
|
||||
if [ -x "${VENV_PYTHON}" ] && [ -f "${STORE_DB}" ]; then
|
||||
"${VENV_PYTHON}" -c "
|
||||
from fenris.store import migrate_to_latest
|
||||
from pathlib import Path
|
||||
n = migrate_to_latest(Path('${STORE_DB}'))
|
||||
print(f'Fenris migration: {n} step(s) applied') if n else None
|
||||
" 2>&1 || echo "Fenris: migration skipped (store not yet initialized)"
|
||||
fi
|
||||
|
||||
# Restart collect timer only if unit contents changed AND it is active
|
||||
for unit in fenris-collect.timer fenris-collect.service; do
|
||||
if systemctl is-active --quiet "${unit}" 2>/dev/null; then
|
||||
TMPFILE="$(mktemp)"
|
||||
systemctl cat "${unit}" > "${TMPFILE}" 2>/dev/null || true
|
||||
UNIT_PATH="/usr/lib/systemd/system/${unit}"
|
||||
if ! diff -q "${TMPFILE}" "${UNIT_PATH}" > /dev/null 2>&1; then
|
||||
systemctl restart "${unit}" 2>/dev/null || true
|
||||
fi
|
||||
rm -f "${TMPFILE}"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
# Always run on both fresh install and upgrade
|
||||
systemd-sysusers || true
|
||||
systemd-tmpfiles --create || true
|
||||
systemctl daemon-reload || true
|
||||
;;
|
||||
abort-upgrade|abort-install|disappear)
|
||||
;;
|
||||
esac
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/bin/sh
|
||||
# postrm — deb post-removal (spec §7, §9).
|
||||
#
|
||||
# dpkg calls: postrm remove (after package files removed)
|
||||
# postrm purge (after conffiles and config removed)
|
||||
# postrm upgrade (after new version installed)
|
||||
set -eu
|
||||
|
||||
case "${1:-}" in
|
||||
purge)
|
||||
rm -rf /etc/fenris
|
||||
rm -rf /var/lib/fenris
|
||||
# Remove the fenris group if it exists
|
||||
if getent group fenris > /dev/null 2>&1; then
|
||||
groupdel fenris 2>/dev/null || true
|
||||
fi
|
||||
;;
|
||||
remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
|
||||
;;
|
||||
esac
|
||||
|
||||
# Always daemon-reload after removal (units may be gone)
|
||||
systemctl daemon-reload 2>/dev/null || true
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
# preinst — abort if make-install remnants detected (spec §7, §9).
|
||||
# Dual marker: /var/lib/fenris/manifest.txt or /etc/systemd/system/fenris-collect.timer.
|
||||
set -eu
|
||||
|
||||
MARKER1="/var/lib/fenris/manifest.txt"
|
||||
MARKER2="/etc/systemd/system/fenris-collect.timer"
|
||||
|
||||
if [ -f "${MARKER1}" ] || [ -f "${MARKER2}" ]; then
|
||||
echo >&2
|
||||
echo >&2 "Fenris make-install remnants detected — refusing to install."
|
||||
echo >&2
|
||||
echo >&2 "Migrate to the package with:"
|
||||
echo >&2 " sudo make uninstall # removes make-install files, preserves store + config"
|
||||
echo >&2 " sudo apt install fenris # or: sudo dnf install fenris"
|
||||
echo >&2
|
||||
echo >&2 "See: https://git.bongbetic.com/xavierk/Fenris/blob/main/docs/spec/release-packaging.md#9-migration-from-make-install-systems"
|
||||
echo >&2
|
||||
exit 1
|
||||
fi
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
# prerm — deb pre-removal (spec §7).
|
||||
#
|
||||
# dpkg calls: prerm remove (package being removed)
|
||||
# prerm upgrade (old version about to be replaced)
|
||||
set -eu
|
||||
|
||||
case "${1:-}" in
|
||||
remove)
|
||||
# Sanctioned disable — close the monitoring period as user_disabled
|
||||
if [ -x /usr/libexec/fenris/fenris-monitor ]; then
|
||||
/usr/libexec/fenris/fenris-monitor disable --now 2>/dev/null || true
|
||||
fi
|
||||
systemctl stop fenris-collect.timer 2>/dev/null || true
|
||||
systemctl disable fenris-collect.timer 2>/dev/null || true
|
||||
;;
|
||||
upgrade)
|
||||
# Never interrupt monitoring on upgrade
|
||||
;;
|
||||
esac
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
#!/bin/sh
|
||||
# RPM %post — post-install/upgrade scriptlet (spec §7).
|
||||
#
|
||||
# Fresh install: sysusers, tmpfiles, daemon-reload — nothing else (dormant).
|
||||
# Upgrade: snapshot, migration, daemon-reload, conditional timer restart.
|
||||
set -eu
|
||||
|
||||
STORE_DIR="/var/lib/fenris"
|
||||
STORE_DB="${STORE_DIR}/observations.db"
|
||||
STORE_BAK="${STORE_DIR}/observations.db.bak"
|
||||
VENV_PYTHON="/opt/fenris/bin/python3"
|
||||
|
||||
if [ "$1" -eq 1 ]; then
|
||||
# Fresh install
|
||||
systemd-sysusers || true
|
||||
systemd-tmpfiles --create || true
|
||||
systemctl daemon-reload || true
|
||||
elif [ "$1" -ge 2 ]; then
|
||||
# Upgrade
|
||||
if [ -f "${STORE_DB}" ]; then
|
||||
cp "${STORE_DB}" "${STORE_BAK}" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [ -x "${VENV_PYTHON}" ] && [ -f "${STORE_DB}" ]; then
|
||||
"${VENV_PYTHON}" -c "
|
||||
from fenris.store import migrate_to_latest
|
||||
from pathlib import Path
|
||||
n = migrate_to_latest(Path('${STORE_DB}'))
|
||||
print(f'Fenris migration: {n} step(s) applied') if n else None
|
||||
" 2>&1 || echo "Fenris: migration skipped (store not yet initialized)"
|
||||
fi
|
||||
|
||||
systemd-sysusers || true
|
||||
systemd-tmpfiles --create || true
|
||||
systemctl daemon-reload || true
|
||||
|
||||
for unit in fenris-collect.timer fenris-collect.service; do
|
||||
if systemctl is-active --quiet "${unit}" 2>/dev/null; then
|
||||
TMPFILE="$(mktemp)"
|
||||
systemctl cat "${unit}" > "${TMPFILE}" 2>/dev/null || true
|
||||
UNIT_PATH="/usr/lib/systemd/system/${unit}"
|
||||
if ! diff -q "${TMPFILE}" "${UNIT_PATH}" > /dev/null 2>&1; then
|
||||
systemctl restart "${unit}" 2>/dev/null || true
|
||||
fi
|
||||
rm -f "${TMPFILE}"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
# RPM %postun — post-uninstall scriptlet (spec §7).
|
||||
#
|
||||
# On erase ($1 -eq 0): remove config (unmodified removed, modified as .rpmsave),
|
||||
# store, backups, and group.
|
||||
# On upgrade ($1 -ge 1): daemon-reload only.
|
||||
set -eu
|
||||
|
||||
if [ "$1" -eq 0 ]; then
|
||||
# Package fully erased
|
||||
rm -rf /etc/fenris
|
||||
rm -rf /var/lib/fenris
|
||||
# Remove the fenris group if it exists
|
||||
if getent group fenris > /dev/null 2>&1; then
|
||||
groupdel fenris 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Always daemon-reload (units may have been removed)
|
||||
systemctl daemon-reload 2>/dev/null || true
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
# RPM %preun — pre-uninstall scriptlet (spec §7).
|
||||
#
|
||||
# On erase ($1 -eq 0): sanctioned disable — close monitoring period.
|
||||
# On upgrade ($1 -ge 1): no-op — never interrupt monitoring.
|
||||
set -eu
|
||||
|
||||
if [ "$1" -eq 0 ]; then
|
||||
# Package is being erased (fully removed), not just upgraded
|
||||
if [ -x /usr/libexec/fenris/fenris-monitor ]; then
|
||||
/usr/libexec/fenris/fenris-monitor disable --now 2>/dev/null || true
|
||||
fi
|
||||
systemctl stop fenris-collect.timer 2>/dev/null || true
|
||||
systemctl disable fenris-collect.timer 2>/dev/null || true
|
||||
fi
|
||||
# On upgrade ($1 -ge 1): do nothing — monitoring continues uninterrupted
|
||||
Executable
+86
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
# Stage a packaging tree at build/stage/ for nfpm consumption.
|
||||
#
|
||||
# Usage: packaging/stage.sh [VERSION]
|
||||
#
|
||||
# VERSION defaults to the version in pyproject.toml.
|
||||
# The staged tree contains:
|
||||
# /opt/fenris/ — bundled venv with the built wheel
|
||||
# /usr/bin/fenris — unprivileged wrapper
|
||||
# /usr/libexec/fenris/ — fenris-monitor, fenris-collect
|
||||
# /usr/lib/systemd/system/ — fenris-collect.{timer,service}
|
||||
# /usr/share/polkit-1/actions/ — polkit policy
|
||||
# /usr/lib/sysusers.d/fenris.conf
|
||||
# /usr/lib/tmpfiles.d/fenris.conf
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
STAGE_DIR="${REPO_ROOT}/build/stage"
|
||||
|
||||
# --- Resolve version ---
|
||||
if [ -n "${1:-}" ]; then
|
||||
VERSION="$1"
|
||||
else
|
||||
VERSION="$(sed -n 's/^version = "\(.*\)"/\1/p' "${REPO_ROOT}/pyproject.toml")"
|
||||
fi
|
||||
|
||||
if [ -z "${VERSION}" ]; then
|
||||
echo "Error: could not determine version" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Staging fenris ${VERSION} ..."
|
||||
|
||||
# --- Clean previous stage ---
|
||||
rm -rf "${STAGE_DIR}"
|
||||
mkdir -p "${STAGE_DIR}"
|
||||
|
||||
# --- Use pre-built wheel from dist/ ---
|
||||
WHEEL=$(ls "${REPO_ROOT}"/dist/fenris-*.whl 2>/dev/null | head -1)
|
||||
if [ -z "${WHEEL}" ]; then
|
||||
echo "Error: no wheel found in dist/ — run 'make dist/fenris-*.whl' first" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " Using wheel: $(basename "${WHEEL}")"
|
||||
|
||||
# --- Create venv with --copies and install wheel ---
|
||||
echo " Creating bundled venv ..."
|
||||
python3 -m venv --copies "${STAGE_DIR}/opt/fenris"
|
||||
"${STAGE_DIR}/opt/fenris/bin/pip" install --upgrade pip --quiet 2>&1 | tail -1
|
||||
"${STAGE_DIR}/opt/fenris/bin/pip" install "${WHEEL}" --quiet 2>&1 | tail -1
|
||||
|
||||
# --- Inject version into wrapper from pyproject.toml ---
|
||||
# The wrapper has a hardcoded version string; patch it for packaging.
|
||||
WRAPPER_SRC="${REPO_ROOT}/scripts/fenris"
|
||||
WRAPPER_DST="${STAGE_DIR}/usr/bin/fenris"
|
||||
mkdir -p "$(dirname "${WRAPPER_DST}")"
|
||||
sed "s|version=\"%(prog)s [0-9.]*\"|version=\"%(prog)s ${VERSION}\"|g" \
|
||||
"${WRAPPER_SRC}" > "${WRAPPER_DST}"
|
||||
chmod 0755 "${WRAPPER_DST}"
|
||||
|
||||
# --- Privileged helpers ---
|
||||
echo " Installing helpers ..."
|
||||
mkdir -p "${STAGE_DIR}/usr/libexec/fenris"
|
||||
install -m 0755 "${REPO_ROOT}/src/fenris/monitor.py" "${STAGE_DIR}/usr/libexec/fenris/fenris-monitor"
|
||||
install -m 0755 "${REPO_ROOT}/src/fenris/collect.py" "${STAGE_DIR}/usr/libexec/fenris/fenris-collect"
|
||||
|
||||
# --- systemd units (vendor placement) ---
|
||||
echo " Installing systemd units ..."
|
||||
mkdir -p "${STAGE_DIR}/usr/lib/systemd/system"
|
||||
install -m 0644 "${REPO_ROOT}/units/fenris-collect.timer" "${STAGE_DIR}/usr/lib/systemd/system/"
|
||||
install -m 0644 "${REPO_ROOT}/units/fenris-collect.service" "${STAGE_DIR}/usr/lib/systemd/system/"
|
||||
|
||||
# --- polkit policy ---
|
||||
echo " Installing polkit policy ..."
|
||||
mkdir -p "${STAGE_DIR}/usr/share/polkit-1/actions"
|
||||
install -m 0644 "${REPO_ROOT}/polkit/com.bongbetic.fenris.monitor.policy" \
|
||||
"${STAGE_DIR}/usr/share/polkit-1/actions/"
|
||||
|
||||
# --- sysusers and tmpfiles fragments ---
|
||||
echo " Installing sysusers/tmpfiles fragments ..."
|
||||
mkdir -p "${STAGE_DIR}/usr/lib/sysusers.d"
|
||||
install -m 0644 "${REPO_ROOT}/packaging/sysusers.d/fenris.conf" "${STAGE_DIR}/usr/lib/sysusers.d/"
|
||||
|
||||
mkdir -p "${STAGE_DIR}/usr/lib/tmpfiles.d"
|
||||
install -m 0644 "${REPO_ROOT}/packaging/tmpfiles.d/fenris.conf" "${STAGE_DIR}/usr/lib/tmpfiles.d/"
|
||||
|
||||
echo "Stage complete: ${STAGE_DIR}"
|
||||
@@ -0,0 +1,3 @@
|
||||
# System user/group for Fenris observation store access
|
||||
# Created by systemd-sysusers during package install
|
||||
g fenris -
|
||||
@@ -0,0 +1,2 @@
|
||||
# Type Path Mode User Group Age Argument
|
||||
d /var/lib/fenris 2750 root fenris - -
|
||||
Reference in New Issue
Block a user