Files
Fenris/docs/research/deb-rpm-toolchain.md
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

240 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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))