- docs/spec/release-packaging.md: decision-complete spec — compat matrix, Gitea 1.27.1 registry channel, nfpm toolchain, signing/key policy, release mechanics, package layout/ownership, maintainer-script contracts, initial config, make-install migration runbook. - docs/adr/0007: package delivery amends ADR 0004 (delivery/ownership only; runtime semantics inherited verbatim). 0004 status updated. - docs/research/: toolchain, gitea-registry, obs findings merged from research branches (assets of map tickets #34/#35/#36). - CONTEXT.md: Release + Rollback glossary terms (ticket #43).
13 KiB
Research: deb + rpm packaging toolchain for bundled-venv builds
Issue: #34 (parent plan: #33) — branch research/toolchain
Date: 2026-09-03 · target: Fenris 0.3.0, x86_64, Debian 12 / Ubuntu 22.04+24.04 / Fedora 40+
TL;DR
Recommended: nfpm with a build script that stages a --copies venv at
/opt/fenris. One nfpm.yaml is the single source of truth for both formats;
nfpm pkg -p deb && nfpm pkg -p rpm (one invocation per format — -p takes a
single string, verified in internal/cmd/package.go). Actively maintained
(releases v2.47.0, 2026-06-20; repo pushed 2026-08-31). Runner-up: fpm (active,
v1.18.0 gem 2026-08-26), but its "config" is a long CLI invocation per format —
the single source of truth degrades into a shell script. dh-virtualenv is
deb-only and its last upstream release is 2020-10 (effectively dormant);
rpmbuild spec is rpm-only and cannot share file lists with a deb build without
external generation.
Constraint evidence: distro textual is unusable (mostly)
| Distro | python3-textual | Source |
|---|---|---|
| Debian 12 (bookworm) | 0.1.13-1 | https://packages.debian.org/bookworm/python3-textual |
| Ubuntu 22.04 (jammy) | 0.1.13-1 | https://packages.ubuntu.com/jammy/python3-textual |
| Ubuntu 24.04 (noble) | 0.1.13-1 | https://packages.ubuntu.com/noble/python3-textual |
| Fedora 40 | 0.48.1 | https://src.fedoraproject.org/rpms/python-textual (f40 spec) |
| Fedora 41 / 42 / 43 | 0.69.0 / 1.0.0 / 4.0.0 | same spec, f41–f43 branches |
Fenris declares textual>=0.40.0 (pyproject) but pins textual==8.2.8
(requirements.txt). Debian 12 + Ubuntu 22.04/24.04 are ~1 major era behind even
the floor; Fedora 40 technically meets >=0.40 but not the pin. Verdict
unchanged: vendor deps inside the package for all targets; per-format
depends: only on python3 (>= 3.9), smartmontools, systemd.
Tool-by-tool
1. nfpm (goreleaser) — RECOMMENDED
- Route: Makefile target builds staging tree → one
nfpm.yaml→nfpm package -p deb+nfpm package -p rpm. (Goreleaser release pipeline can wrap both later.) - Shared assets: version, description, maintainer,
depends,contents:file list,scripts:all live once innfpm.yaml; per-format deltas viaoverrides: { deb: ..., rpm: ... }andpackager:-scoped content entries (https://nfpm.goreleaser.com/configuration/, sourcewww/content/docs/configuration.md). - Prerequisites: single static Go binary (
go install github.com/goreleaser/nfpm/v2/cmd/nfpm@latest, Homebrew, or release tarball — https://nfpm.goreleaser.com/install/). No toolchain per distro, no root, no containers required (build same tree for both formats). - Venv → file list: stage with
python3 -m venv --copies staging/opt/fenris && staging/opt/fenris/bin/pip install dist/fenris-*.whl; map in one entry:contents: [{ src: staging/opt/fenris/, dst: /opt/fenris, type: tree }]. Shebangs point at fixed absolute/opt/fenris/bin/python→ no relocation issues.--copiesavoids symlink-to-/usr breakage. Config file →type: config|noreplace(=%config(noreplace) on rpm, conffile semantics on deb)./var/lib/fenrisstore →type: ghost(rpm: owned-but-not-packed; deb: ignored → create inpostinstallscript instead). - Systemd/polkit/libexec: plain
contents:entries —/usr/lib/systemd/system/fenris-collect.{service,timer}(or/etc/systemd/systemto match current Makefile), polkit action at/usr/share/polkit-1/actions/, helpers under/usr/libexec/fenris/.scripts:supportspostinstall(deb maintainer script / rpm scriptlet) — runsystemctl daemon-reload, create/var/lib/fenrisroot:fenris 2750, group creation. - Upgrade/removal: deb — dpkg replaces all non-conffile files, conffile
prompts/preserves (
.dpkg-new) per Debian Policy ch-files (https://www.debian.org/doc/debian-policy/ch-files.html); removal keeps conffiles + unowned store; purge cleans. rpm —rpm -Ureplaces,%config(noreplace)keeps local edits as.rpmnew; only owned dirs are removed on erase (nfpmtype: direxists precisely to claim ownership — docs warn not to claim distro-owned dirs). - Maintenance: very active. goreleaser/nfpm, 2.6k stars, last push 2026-08-31, v2.47.0 released 2026-06-20 (GitHub API).
2. fpm — viable, weaker single-source-of-truth
- Route: staging tree (same as above) then
fpm -s dir -t deb ... staging/=/ ; fpm -s dir -t rpm .... - Shared assets: none declarative — everything is CLI flags
(
-n,-v,--config-files,--deb-systemd,--directories,--after-install,--rpm-posttrans, …). Flag list: https://fpm.readthedocs.io/en/latest/cli-reference.html. The two invocations will drift unless wrapped in a Makefile that shares variables; the "single source" is then a shell script, not a checked declarative file. (--deb-systemdexists; no rpm-native unit macro — you hand it the unit file plus--rpm-posttransfor daemon-reload.) - Prerequisites: Ruby + gem (
gem install fpm) or distro package; building rpm side needsrpmbuildpresent for some features. - Venv → file list:
-s dirmaps a directory into the package verbatim — same staging-tree trick as nfpm.--config-files /etc/fenrismarks conffiles (deb) / %config (rpm). - Upgrade/removal: identical downstream semantics to nfpm (native dpkg/rpm behavior); differences are only in how metadata/scripts land in the package.
- Maintenance: active — releases v1.16.0 (2024-12), v1.17.0 (2025-10), v1.18.0 (2026-08-26); gem 1.18.0 on rubygems; ~11.5k stars. But docs are openly "work in progress" (https://fpm.readthedocs.io/en/latest/).
3. dh-virtualenv (Spotify) — deb-only, dorms
- Route: debhelper add-on:
debian/ruleswithdh $@ --with python-virtualenv --buildsystem=python_distutils; produces a .deb containing venv at/opt/venvs/<package>(DH_VIRTUALENV_INSTALL_ROOToverridable,--builtin-venvforpython -m venv). Docs: repodoc/usage.rst,doc/tutorial.rst(https://github.com/spotify/dh-virtualenv). - Shared assets: none with rpm — it cannot emit .rpm at all. Would still need a second toolchain for Fedora → fails the criterion outright.
- Prerequisites:
build-essential debhelper devscripts equivs+dh-virtualenv(tutorial.rst); Debian 12 still ships it asdh-virtualenv 1.2.2-1.3(https://packages.debian.org/bookworm/dh-virtualenv). - Venv → file list: automatic — it builds the venv during the debhelper sequence and rewrites shebangs; the .deb owns the whole venv tree. Least manual work of all four, for deb alone.
- Upgrade/removal: standard dpkg; whole venv tree is package-owned, so
apt removedeletes it cleanly;--pypi-url/requirements handled by tool. - Maintenance: last upstream release 1.2.2, 2020-10-22 (GitHub tag); repo last pushed 2024-04-27, RTD docs 404. Effectively dormant upstream — fine via Debian's own packaging, but risky as strategic dependency.
- Bonus fact: PyPI
dh-virtualenvproject now returns 404 — install only from Debian repo / git.
4. rpmbuild spec + vendored venv — rpm-native, no deb
- Route: hand-written
fenris.spec:%installstage builds venv into%{buildroot}/opt/fenris,%fileslists it plus units/polkit/libexec,%ghost %attr(2750,root,fenris) /var/lib/fenris,%config(noreplace)for/etc/fenris,systemd_post/preunmacros for the timer. Reference style: https://docs.fedoraproject.org/en-US/packaging-guidelines/. - Shared assets: the spec is a second, parallel description of the same file list — nothing is shared with any deb build without generating one side from the other (e.g. generate spec + debian/control from a manifest). Worst single-source-of-truth score.
- Prerequisites:
rpm-build, mock/koji for cleanroots; Fedora toolchain knowledge; per-distroRelease:/dist tag handling. - Venv → file list:
%filesline%{buildroot}/opt/fenris/...— venv becomes ordinary payload; shebangs already absolute. - Upgrade/removal: canonical rpm semantics (same as above) plus real systemd scriptlet macros — the best-behaved rpm integration of the four, at the cost of hand-maintained spec.
- Maintenance: rpmbuild itself is maintained forever (part of RPM), but your spec is 100% hand-maintained duplication.
Comparison matrix
| Criterion | nfpm | fpm | dh-virtualenv | rpmbuild spec |
|---|---|---|---|---|
| deb + rpm from one config | ✅ one YAML (2 invocations) | ⚠️ flags per invocation | ❌ deb only | ❌ rpm only |
| File list shared across formats | ✅ contents: |
⚠️ per-invocation args | n/a | ❌ |
| Vendored venv supported | ✅ staging type: tree |
✅ -s dir |
✅✅ automatic (deb) | ✅ %files |
| conffile / %config(noreplace) | ✅ type: config|noreplace |
✅ --config-files |
✅ (debhelper) | ✅ %config(noreplace) |
| ghost store dir | ✅ type: ghost |
⚠️ --rpm-ghost? (no deb equiv) |
❌ | ✅ %ghost |
| systemd scriptlets | ✅ scripts: + macros? (plain scripts) |
✅ --deb-systemd, --rpm-posttrans |
✅ (deb) | ✅✅ native macros |
| Prereqs on build host | Go binary (or brew/apt tarball) | Ruby gem | debhelper stack | rpm-build + mock |
| Maintenance (2026) | 🟢 active (v2.47.0) | 🟢 active (v1.18.0) | 🔴 dormant since 2020 (Debian carries it) | 🟢 tool yes / 🔴 your spec |
| Risk | young-ish config schema churn | docs thin | dead upstream | duplication forever |
Proposed pipeline (sketch)
# Makefile additions (build only — install target stays for source installs)
stage: dist/fenris-*.whl
rm -rf build/stage
python3 -m venv --copies build/stage/opt/fenris
build/stage/opt/fenris/bin/pip install --no-compile dist/fenris-*.whl
install -D -m 0755 scripts/fenris build/stage/usr/bin/fenris
install -D -m 0755 src/fenris/monitor.py build/stage/usr/libexec/fenris/fenris-monitor
install -D -m 0755 src/fenris/collect.py build/stage/usr/libexec/fenris/fenris-collect
install -D -m 0644 units/fenris-collect.timer build/stage/usr/lib/systemd/system/fenris-collect.timer
install -D -m 0644 units/fenris-collect.service build/stage/usr/lib/systemd/system/fenris-collect.service
install -D -m 0644 polkit/com.bongbetic.fenris.monitor.policy \
build/stage/usr/share/polkit-1/actions/com.bongbetic.fenris.monitor.policy
package-deb package-rpm: stage
nfpm pkg -f packaging/nfpm.yaml -p deb -t dist/
nfpm pkg -f packaging/nfpm.yaml -p rpm -t dist/
# packaging/nfpm.yaml (excerpt)
name: fenris
arch: amd64
platform: linux
version: ${VERSION} # env expansion, documented feature
maintainer: Fenris Maintainers <ops@bongbetic.com>
description: SMART drive observation daemon with persistent TUI
homepage: https://git.bongbetic.com/xavierk/Fenris
depends: [smartmontools]
contents:
- src: build/stage/ # everything above
dst: /
type: tree
- dst: /etc/fenris # config dir; ship fenris.conf as config|noreplace
type: dir
- src: packaging/fenris.conf
dst: /etc/fenris/fenris.conf
type: config|noreplace
- dst: /var/lib/fenris # rpm: %ghost ownership; deb: create in postinst
type: ghost
scripts:
postinstall: packaging/postinst.sh # groupadd fenris; install -d -o root -g fenris -m 2750 /var/lib/fenris; systemctl daemon-reload (units shipped dormant)
preremove: packaging/prerm.sh # stop timer if running
overrides:
deb:
depends: [python3 (>= 3.9), smartmontools]
rpm:
depends: [python3 >= 3.9, smartmontools]
Recommendation
Adopt nfpm + staged --copies venv: closest to single source of truth
(one YAML for both formats), smallest prerequisite surface (one static binary),
actively maintained, and every Fenris constraint (units, polkit, libexec,
/etc/fenris conffile, /var/lib/fenris ghost/store) has a first-class
mapping. Keep fpm as documented fallback (identical staging tree, works
anywhere Ruby exists). Do not build the release pipeline on dh-virtualenv
(dormant, deb-only) or on a hand-maintained spec file (duplication, deb side
unaddressed).
Sources
- nfpm config reference: https://nfpm.goreleaser.com/configuration/ (source:
goreleaser/nfpm
www/content/docs/configuration.md, accessed 2026-09-03) - nfpm CLI (single
-p): goreleaser/nfpminternal/cmd/package.go - nfpm releases/status: GitHub API, repo pushed 2026-08-31, v2.47.0 2026-06-20
- fpm README + CLI reference: https://github.com/jordansissel/fpm, https://fpm.readthedocs.io/en/latest/cli-reference.html; releases v1.18.0 (2026-08-26), gem 1.18.0
- dh-virtualenv docs:
doc/usage.rst,doc/tutorial.rst@ master; tag 1.2.2 dated 2020-10-22 (GitHub commits API); PyPI project 404; Debian 12 package 1.2.2-1.3 (packages.debian.org) - Distro textual versions: packages.debian.org, packages.ubuntu.com,
src.fedoraproject.org
python-textual.specf40–f43 - Upgrade semantics: Debian Policy ch-files (https://www.debian.org/doc/debian-policy/ch-files.html); Fedora packaging guidelines (https://docs.fedoraproject.org/en-US/packaging-guidelines/); RPM directive behavior quoted in nfpm config docs (%ghost, %config(noreplace))