- 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).
240 lines
13 KiB
Markdown
240 lines
13 KiB
Markdown
# 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 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)
|
||
|
||
```make
|
||
# 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/
|
||
```
|
||
|
||
```yaml
|
||
# 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/nfpm `internal/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.spec` f40–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))
|