# 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/` (`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 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))