Files
Fenris/docs/research/deb-rpm-toolchain.md
T
xavierk b005049733 docs: release & packaging spec + ADR 0007 amending 0004 (map #33, task #42)
- 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).
2026-09-03 01:44:37 +05:30

13 KiB
Raw Blame History

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

  • 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 in nfpm.yaml; per-format deltas via overrides: { deb: ..., rpm: ... } and packager:-scoped content entries (https://nfpm.goreleaser.com/configuration/, source www/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. --copies avoids symlink-to-/usr breakage. Config file → type: config|noreplace (=%config(noreplace) on rpm, conffile semantics on deb). /var/lib/fenris store → type: ghost (rpm: owned-but-not-packed; deb: ignored → create in postinstall script instead).
  • Systemd/polkit/libexec: plain contents: entries — /usr/lib/systemd/system/fenris-collect.{service,timer} (or /etc/systemd/system to match current Makefile), polkit action at /usr/share/polkit-1/actions/, helpers under /usr/libexec/fenris/. scripts: supports postinstall (deb maintainer script / rpm scriptlet) — run systemctl daemon-reload, create /var/lib/fenris root: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 -U replaces, %config(noreplace) keeps local edits as .rpmnew; only owned dirs are removed on erase (nfpm type: dir exists 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-systemd exists; no rpm-native unit macro — you hand it the unit file plus --rpm-posttrans for daemon-reload.)
  • Prerequisites: Ruby + gem (gem install fpm) or distro package; building rpm side needs rpmbuild present for some features.
  • Venv → file list: -s dir maps a directory into the package verbatim — same staging-tree trick as nfpm. --config-files /etc/fenris marks 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/rules with dh $@ --with python-virtualenv --buildsystem=python_distutils; produces a .deb containing venv at /opt/venvs/<package> (DH_VIRTUALENV_INSTALL_ROOT overridable, --builtin-venv for python -m venv). Docs: repo doc/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 as dh-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 remove deletes 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-virtualenv project now returns 404 — install only from Debian repo / git.

4. rpmbuild spec + vendored venv — rpm-native, no deb

  • Route: hand-written fenris.spec: %install stage builds venv into %{buildroot}/opt/fenris, %files lists it plus units/polkit/libexec, %ghost %attr(2750,root,fenris) /var/lib/fenris, %config(noreplace) for /etc/fenris, systemd_post/preun macros 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-distro Release:/dist tag handling.
  • Venv → file list: %files line %{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