1. Refuse to run if CI is not green on `master` HEAD.
2. Compute the new version, finalize `CHANGES`, propagate version strings into all source locations, commit, and tag — all without human edits.
-3. Produce the source tarball, the Windows MSVC binaries, RPM packages (AlmaLinux), and DEB packages (Ubuntu / Debian) — the latter two targeting `/opt/rrdtool` so they coexist with distribution-maintained `rrdtool` packages — all attached to a single GitHub Release with extracted release notes.
+3. Produce the source tarball, the Windows MSVC binaries, RPM packages (AlmaLinux), and DEB packages (Ubuntu / Debian) — the latter two installing into `/opt/rrdtool` (binaries, library, and Perl/Python/Tcl/Lua/Ruby bindings, all rooted under that prefix) so they coexist with distribution-maintained `rrdtool` packages without touching the system. All artifacts attach to a single GitHub Release with extracted release notes.
## Constraints
- **CI is the gate.** A release must not happen if `Linux Build` or `Windows CI` failed on the commit at master HEAD.
- **One Release, all artifacts.** Source tarball, MSVC x64/x86 zips, distro-tagged `.rpm` and `.deb` files, all attached to the same GitHub Release.
- **Binary packages run in distro containers.** rrdtool's dependency tree (cairo, pango, libdbi, etc.) is large; per-distro containers isolate it.
-- **`/opt/rrdtool` install prefix.** Our packages install into `/opt/rrdtool/` and do not touch `/usr/...`. They coexist with the distribution-maintained `rrdtool` packages — users can have both installed simultaneously. Neither the RPM `Conflicts:` header nor the DEB `Conflicts:` field is set, intentionally — both packages can be installed alongside the distro version.
-- **C-only build, no language bindings in packages.** Distros' rrdtool packages split bindings into language-native packages (`python3-rrdtool`, `librrds-perl`, `lua-rrd`, etc.) installed into FHS-canonical paths (`/usr/lib/python3/dist-packages/`, `@INC`). Our `/opt/rrdtool/lib/...` paths would not be on language search paths without per-user env setup, which makes shipping bindings under `/opt` more confusing than useful. The /opt build is `rrdtool`, `rrdupdate`, `rrdcgi`, `rrdcached`, `librrd.so`, headers, manpages — the C tooling, nothing else. Users wanting bindings install them from their distro or from CPAN/PyPI/gems.
+- **`/opt/rrdtool` install prefix, nothing outside.** Our packages install entirely under `/opt/rrdtool/` — no `/etc/ld.so.conf.d/` entries, no `/etc/profile.d/`, no `/usr/...`. They coexist with the distribution-maintained `rrdtool` packages — users can have both installed simultaneously. Neither the RPM `Conflicts:` header nor the DEB `Conflicts:` field is set, intentionally.
+- **Bindings included, installed under `/opt`.** Perl, Python, Tcl, Lua, and Ruby bindings are built and installed under `/opt/rrdtool/lib/<lang>/...` — rrdtool's `configure` was designed for exactly this. By **not** passing `--enable-perl-site-install`, `--enable-ruby-site-install`, `--enable-lua-site-install`, or `--enable-tcl-site`, the bindings land in `$prefix/lib/perl`, `$prefix/lib/ruby`, `$prefix/lib/lua/$lua_version`, etc. — never in the system's `@INC`, `sys.path`, or equivalents. The distribution's bindings are untouched.
+- **`librrd.so` discoverability without `ld.so.conf`.** rrdtool's own binaries (`rrdtool`, `rrdupdate`, `rrdcgi`, `rrdcached`) get `RPATH=/opt/rrdtool/lib` baked in by libtool during the build (we do **not** pass `--disable-rpath`). The library knows where to find itself. For third-party consumers, the installed `librrd.pc` is post-processed to add `-Wl,-rpath,${libdir}` to its `Libs:` line — anyone compiling against `/opt/rrdtool` via `pkg-config` gets the rpath embedded in their binary automatically. No system-wide linker config needed.
## Non-goals
--datarootdir=/opt/rrdtool/share \
--mandir=/opt/rrdtool/share/man \
--disable-static \
- --disable-rpath \
- --disable-perl \
- --disable-python \
- --disable-ruby \
- --disable-lua \
- --disable-tcl \
--with-pic
make
make install DESTDIR="$PWD/stage"
```
-After `make install` the staged tree contains only `stage/opt/rrdtool/...`. We then add an `ld.so.conf.d` snippet so the runtime linker finds `librrd.so`:
+Critical defaults (chosen by **not** passing flags):
-```bash
-mkdir -p stage/etc/ld.so.conf.d
-echo "/opt/rrdtool/lib" > stage/etc/ld.so.conf.d/rrdtool-opt.conf
-```
+| Flag we deliberately omit | Effect |
+|---|---|
+| `--enable-perl-site-install` | Perl modules go to `$prefix/lib/perl`, **not** the system `@INC` |
+| `--enable-ruby-site-install` | Ruby modules go to `$prefix/lib/ruby`, **not** the system gem path |
+| `--enable-lua-site-install` | Lua modules go to `$prefix/lib/lua/$lua_version`, **not** the system Lua paths |
+| `--enable-tcl-site` | Tcl bindings install under `$prefix`, **not** in the Tcl auto-load tree |
+| `--disable-rpath` | libtool bakes `RPATH=/opt/rrdtool/lib` into our binaries, so they find `librrd.so` without any system linker config |
+
+For Python: `AM_PATH_PYTHON` honours `--prefix`, so the module lands at `/opt/rrdtool/lib/python3.X/site-packages/rrdtool.so` (or similar; the exact subpath includes the Python version found by configure).
+
+After `make install` the staged tree is entirely under `stage/opt/rrdtool/`. **Two post-install touches:**
+
+1. **Add `-Wl,-rpath,${libdir}` to the installed `librrd.pc`** so consumers' binaries link with the rpath baked in:
+
+ ```bash
+ sed -i 's|^Libs: -L\${libdir} -lrrd$|Libs: -L${libdir} -lrrd -Wl,-rpath,${libdir}|' \
+ stage/opt/rrdtool/lib/pkgconfig/librrd.pc
+ ```
-That's the only file outside `/opt/rrdtool/` we install. (Adding `/etc/profile.d/rrdtool-opt.sh` for `PATH` is tempting but invasive — users who want it can `ln -s /opt/rrdtool/bin/rrdtool /usr/local/bin/rrdtool` themselves. Skipping by default.)
+ This is intentionally only done in the `/opt`-packaged build — the upstream `src/librrd.pc.in` stays unchanged so FHS-canonical builds (the existing Debian/Fedora packaging) aren't affected.
+
+2. **Install a sourceable environment-setup helper** at `stage/opt/rrdtool/bin/rrdtool-env.sh`:
+
+ ```sh
+ #!/bin/sh
+ # Source me to use /opt/rrdtool from your shell:
+ # . /opt/rrdtool/bin/rrdtool-env.sh
+ ROOT=/opt/rrdtool
+ export PATH="$ROOT/bin${PATH:+:$PATH}"
+ export MANPATH="$ROOT/share/man${MANPATH:+:$MANPATH}"
+ export PKG_CONFIG_PATH="$ROOT/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"
+ export PERL5LIB="$ROOT/lib/perl${PERL5LIB:+:$PERL5LIB}"
+ PY_SP=$(ls -d "$ROOT"/lib/python*/site-packages 2>/dev/null | head -1)
+ [ -n "$PY_SP" ] && export PYTHONPATH="$PY_SP${PYTHONPATH:+:$PYTHONPATH}"
+ RB_ARCH=$(ls -d "$ROOT"/lib/ruby/*-* 2>/dev/null | head -1)
+ export RUBYLIB="$ROOT/lib/ruby${RB_ARCH:+:$RB_ARCH}${RUBYLIB:+:$RUBYLIB}"
+ LUA_VDIR=$(ls -d "$ROOT"/lib/lua/5.* 2>/dev/null | head -1)
+ [ -n "$LUA_VDIR" ] && export LUA_CPATH="$LUA_VDIR/?.so;${LUA_CPATH:-;;}"
+ export TCLLIBPATH="$ROOT/lib${TCLLIBPATH:+ $TCLLIBPATH}"
+ ```
+
+ Users opt in with `. /opt/rrdtool/bin/rrdtool-env.sh` (or add it to `~/.bashrc`). One file, no system mutation.
+
+**Documentation in the package description** explains the usage pattern: source the env script, or set `PKG_CONFIG_PATH` directly and compile with rpath baked in via `pkg-config --libs librrd`. The GitHub Release body gets a short "Using the /opt packages" section too.
### Job: `build-rpm`
image: ${{ matrix.image }}
```
-A new spec file `conftools/rrdtool-opt.spec` is added to the repo. It's deliberately minimal — none of the FHS gymnastics of the existing `rrdtool.spec`, no subpackage split, no PHP4. Sketch:
+A new spec file `conftools/rrdtool-opt.spec` is added to the repo. It's deliberately minimal — no FHS gymnastics, no subpackage split, no PHP4. Sketch:
```spec
-%global _prefix /opt/rrdtool
-%global _sysconfdir /opt/rrdtool/etc
-%global _localstatedir /opt/rrdtool/var
-%global _datarootdir /opt/rrdtool/share
-%global _mandir /opt/rrdtool/share/man
-
Name: rrdtool
Version: @VERSION@
Release: 1%{?dist}
Source0: rrdtool-%{version}.tar.gz
Prefix: /opt/rrdtool
-AutoReq: yes
+AutoReqProv: no
BuildRequires: gcc, make, autoconf, automake, libtool, pkgconfig
BuildRequires: groff, gettext-devel, intltool
BuildRequires: cairo-devel >= 1.2, pango-devel >= 1.14
BuildRequires: freetype-devel, libpng-devel, zlib-devel, libxml2-devel
BuildRequires: glib2-devel, libdbi-devel
+# binding build-deps
+BuildRequires: perl-devel, perl-ExtUtils-MakeMaker
+BuildRequires: python3-devel
+BuildRequires: tcl-devel
+BuildRequires: lua-devel
+BuildRequires: ruby, ruby-devel
%description
RRDtool is the OpenSource industry standard high performance data
logging and graphing system for time series data.
-This package installs RRDtool under /opt/rrdtool so it does not
-conflict with the distribution-provided rrdtool package. Add
-/opt/rrdtool/bin to PATH (or symlink the binaries from /usr/local/bin)
-to use it. The shared library is registered via /etc/ld.so.conf.d.
+This package installs RRDtool entirely under /opt/rrdtool so it does
+not conflict with the distribution-provided rrdtool package. Source
+/opt/rrdtool/bin/rrdtool-env.sh in your shell to put it on PATH and
+make its language bindings (Perl, Python, Tcl, Lua, Ruby) discoverable.
+
+For C/C++ consumers, set PKG_CONFIG_PATH=/opt/rrdtool/lib/pkgconfig
+and compile with `pkg-config --cflags --libs librrd`; the resulting
+binary has /opt/rrdtool/lib baked in via -Wl,-rpath.
%prep
%setup -q -n rrdtool-%{version}
--datarootdir=/opt/rrdtool/share \
--mandir=/opt/rrdtool/share/man \
--disable-static \
- --disable-rpath \
- --disable-perl --disable-python --disable-ruby --disable-lua --disable-tcl \
--with-pic
make %{?_smp_mflags}
%install
make install DESTDIR=%{buildroot}
-mkdir -p %{buildroot}/etc/ld.so.conf.d
-echo "/opt/rrdtool/lib" > %{buildroot}/etc/ld.so.conf.d/rrdtool-opt.conf
-
-%post -p /sbin/ldconfig
-%postun -p /sbin/ldconfig
+# add rpath to consumer pkg-config args
+sed -i 's|^Libs: -L\${libdir} -lrrd$|Libs: -L${libdir} -lrrd -Wl,-rpath,${libdir}|' \
+ %{buildroot}/opt/rrdtool/lib/pkgconfig/librrd.pc
+# install env-setup helper (content created by the workflow before rpmbuild)
+install -m 0755 %{_sourcedir}/rrdtool-env.sh \
+ %{buildroot}/opt/rrdtool/bin/rrdtool-env.sh
%files
/opt/rrdtool
-/etc/ld.so.conf.d/rrdtool-opt.conf
```
-`@VERSION@` is substituted at workflow time using `sed`. The spec lives in `conftools/` so `make dist` doesn't ship it inside the tarball (Fedora maintainers maintain their own spec; ours is for the /opt build only).
+`@VERSION@` is substituted at workflow time using `sed`. `AutoReqProv: no` is important — without it, rpmbuild would scan our binaries' RPATH and emit dependencies like `librrd.so.X()(64bit)` which only the /opt-installed librrd can satisfy, breaking install on hosts that have the distro rrdtool installed. We explicitly list runtime dependencies on the system libraries we link against (cairo, pango, libxml2, libpng, freetype, libdbi, zlib) via `Requires:` lines (omitted from the sketch for brevity; included in the actual spec).
+
+The spec lives in `conftools/` so `make dist` doesn't ship it inside the tarball.
Steps in the job:
rpm-build rpmdevtools gcc make autoconf automake libtool pkgconfig \
groff gettext gettext-devel intltool \
cairo-devel pango-devel freetype-devel libpng-devel zlib-devel \
- libxml2-devel glib2-devel libdbi-devel
+ libxml2-devel glib2-devel libdbi-devel \
+ perl-devel perl-ExtUtils-MakeMaker \
+ python3-devel \
+ tcl-devel \
+ lua-devel \
+ ruby ruby-devel
```
-2. **Set up rpmbuild tree** (`rpmdev-setuptree`), substitute version into the spec, copy spec to `~/rpmbuild/SPECS/`, copy `source-tarball` to `~/rpmbuild/SOURCES/`.
-3. **`rpmbuild -bb ~/rpmbuild/SPECS/rrdtool-opt.spec`** — builds the binary RPM(s).
-4. **Collect** `.rpm` files from `~/rpmbuild/RPMS/x86_64/`. Filename has the dist tag (e.g. `rrdtool-1.9.1-1.el9.x86_64.rpm`) so multiple matrix entries don't collide.
+2. **Set up rpmbuild tree** (`rpmdev-setuptree`), substitute version into the spec, write the `rrdtool-env.sh` helper to `~/rpmbuild/SOURCES/`, copy the spec to `~/rpmbuild/SPECS/`, copy `source-tarball` to `~/rpmbuild/SOURCES/`.
+3. **`rpmbuild -bb ~/rpmbuild/SPECS/rrdtool-opt.spec`** — builds the binary RPM.
+4. **Collect** the `.rpm` from `~/rpmbuild/RPMS/x86_64/`. Filename has the dist tag (e.g. `rrdtool-1.9.1-1.el9.x86_64.rpm`) so multiple matrix entries don't collide.
5. **Upload** as artifact `rpm-${{ matrix.image }}` (slashes stripped).
### Job: `build-deb`
gettext intltool groff dc \
libcairo2-dev libpango1.0-dev libxml2-dev libglib2.0-dev libdbi-dev \
libfreetype6-dev libpng-dev zlib1g-dev \
+ libperl-dev \
+ python3-dev python3-setuptools \
+ tcl-dev \
+ liblua5.1-0-dev \
ruby ruby-dev ruby-rubygems
gem install --no-document fpm
```
- (Build-dep list mirrors Debian's working set: `libpango1.0-dev` transitively pulls cairo, freetype, glib, png.)
+ (Build-dep list mirrors Debian's working set: `libpango1.0-dev` transitively pulls cairo, freetype, glib, png. Adding language `-dev` packages enables binding builds.)
2. **Download `source-tarball` artifact.**
-3. **Extract and build** with the shared configure invocation (above), `make`, `make install DESTDIR=$PWD/stage`, plus the `ld.so.conf.d` snippet.
+3. **Extract and build** with the shared configure invocation (above), `make`, `make install DESTDIR=$PWD/stage`, plus the two post-install touches (rpath in `librrd.pc`, write `rrdtool-env.sh`).
4. **Run `fpm` to produce the `.deb`**:
```
fpm -s dir -t deb -n rrdtool -v X.Y.Z \
--maintainer "Tobias Oetiker <tobi@oetiker.ch>" \
--vendor "oss.oetiker.ch" \
--url "https://oss.oetiker.ch/rrdtool/" \
- --description "Round Robin Database Tool (upstream /opt build)" \
+ --description "Round Robin Database Tool (upstream /opt build). \
+Source /opt/rrdtool/bin/rrdtool-env.sh to use." \
--depends "libcairo2" --depends "libpango-1.0-0" \
--depends "libxml2" --depends "libpng16-16" \
- --depends "libfreetype6" --depends "libdbi1" \
- --after-install /tmp/ldconfig.sh \
- --after-remove /tmp/ldconfig.sh \
- -C stage opt etc
+ --depends "libfreetype6" --depends "libdbi1" --depends "zlib1g" \
+ --deb-no-default-config-files \
+ -C stage opt
```
- `DISTRO_TAG` is one of `ubuntu22.04`, `ubuntu24.04`, `debian12` (derived from `${{ matrix.image }}`).
+ - `DISTRO_TAG` is one of `ubuntu22.04`, `ubuntu24.04`, `debian12` (derived from `${{ matrix.image }}`).
+ - `-C stage opt` packages only the `opt/` subtree — no system files.
+ - **No `--after-install` ldconfig hook** — `librrd.so` is found via the binaries' baked-in RPATH and via `librrd.pc`'s rpath args for consumers. ldconfig is not needed.
+ - **Dependencies on language bindings are intentionally omitted from the package's `Depends:`.** A user who installs the `.deb` gets `rrdtool` and the C library; the bindings are present on disk but only loaded by a Perl/Python/etc. interpreter that has been pointed at `/opt/rrdtool/lib/<lang>` (via `rrdtool-env.sh` or manually). This is intentional: it means the package installs on a host without Perl/Ruby/Tcl/Lua installed.
- `/tmp/ldconfig.sh` is a one-liner created earlier in the job (`#!/bin/sh` + `/sbin/ldconfig`). It runs after install and after removal so the linker cache picks up `/opt/rrdtool/lib`.
-
- **Note**: this `.deb` is intentionally non-canonical (single package, doesn't match Debian's split). It's an upstream/opt package. Users wanting the FHS-compliant split layout use the Debian-maintained packages.
+ **Note**: this `.deb` is intentionally non-canonical (single package, `/opt`-rooted, no subpackage split). It's an upstream package, not Debian's. Users wanting the FHS-compliant split layout use the Debian-maintained packages from `apt`.
5. **Upload** as artifact `deb-${{ matrix.image }}` (slashes stripped). Resulting filename: `rrdtool_X.Y.Z-1~ubuntu22.04_amd64.deb`, etc.
### Failure-mode policy for binary packages
-`build-rpm` and `build-deb` use **`continue-on-error: true`** at the job level, plus `fail-fast: false` in their matrices. Rationale: the source tarball is the canonical release; an outdated `rrdtool.spec`, missing apt mirror, or single-distro container hiccup should not prevent a release that the maintainer has explicitly green-lit. When an `.rpm` or `.deb` job fails, the Release still publishes with whatever binary packages succeeded plus the source tarball and Windows artifacts. The failed job is visible in the workflow run, giving a clean signal for follow-up cleanup without blocking the release.
+`build-rpm` and `build-deb` use **`continue-on-error: true`** at the job level, plus `fail-fast: false` in their matrices. Rationale: the source tarball is the canonical release; a missing apt mirror, an unexpected distro-specific binding-build failure (e.g. a Ruby `mkmf` quirk on Alma 9, a Tcl version mismatch on Ubuntu 24.04), or a transient container pull issue should not block a release the maintainer has explicitly green-lit. When an `.rpm` or `.deb` job fails, the Release still publishes with whatever binary packages succeeded, plus the source tarball and Windows artifacts. The failed job is visible in the workflow run, giving a clean signal for follow-up cleanup without blocking the release.
`build-source` and `build-windows` do **not** get `continue-on-error` — those are the established artifacts users depend on. Their failure aborts the release.
| `.github/workflows/release-windows.yml` | **delete** — folded into `release.yml`; the `push: tags` trigger likewise disappears. The CI smoke build for MSVC stays in `ci-workflow.yml` |
| `conftools/bump-version.sh` | **new** — version-propagation logic extracted from `rrdtool-release` |
| `conftools/rrdtool-opt.spec` | **new** — RPM spec for the `/opt/rrdtool` build (separate from the legacy `rrdtool.spec` which is FHS-targeted and unused by the new workflow) |
+| `conftools/rrdtool-env.sh.in` | **new** — sourceable environment-setup helper template (rendered to `/opt/rrdtool/bin/rrdtool-env.sh` at install time; used by both the RPM `%install` step and the DEB build's staging step) |
| `rrdtool-release` | **refactor** — call `conftools/bump-version.sh` for the propagation step; SCP-to-james and local sanity build stay intact for the maintainer's local workflow |
| `docs/superpowers/specs/2026-05-13-release-workflow-design.md` | **new** — this document |
- **The new `rrdtool-opt.spec` is minimal but untested in production.** First-run failures will surface real issues (missing BuildRequires, configure flag typos, RPM macro quirks). `continue-on-error: true` on `build-rpm` means these don't block the release. Same applies to `build-deb` (configure on a fresh Ubuntu/Debian container may surface a missing dep we didn't anticipate).
-- **`/opt`-install packages are non-canonical by design.** They're a single combined package, don't follow FHS, don't conflict with the distribution rrdtool. Distro-package users won't find them via `apt show rrdtool` or `dnf info rrdtool` because they're not the same package — different upstream channel. Documenting this in the GitHub Release description is worthwhile (future work, low priority).
+- **`/opt`-install packages are non-canonical by design.** They're a single combined package, don't follow FHS, don't conflict with the distribution rrdtool. Distro-package users won't find them via `apt show rrdtool` or `dnf info rrdtool` because they're not the same package — different upstream channel. The GitHub Release body will include a short "Using the /opt packages" section explaining the env script.
+
+- **Bindings need user opt-in to be discoverable.** The `.rpm` and `.deb` install Perl/Python/Tcl/Lua/Ruby bindings under `/opt/rrdtool/lib/<lang>` but the system Perl, Python, etc. don't look there by default. Users source `/opt/rrdtool/bin/rrdtool-env.sh` (or add it to their shell rc, or set `PERL5LIB`/`PYTHONPATH`/etc. themselves). The distribution's bindings remain untouched, so a user using the system `python3 -c 'import rrdtool'` still gets the distro version unless they've sourced our env script. This is the intended behavior.
-- **No language bindings in `/opt` packages.** Users wanting Perl/Python/Tcl/Lua/Ruby bindings install them from their distro packages or from CPAN/PyPI/gems. Those bindings link against the distro's `librrd`, not ours — which is fine because the C library ABI is stable across these point versions. If someone needs upstream bindings against the upstream library, they build from source. Adding binding packages later is a feasible extension but explicitly out of scope here.
+- **Python binding directory varies by distro.** `AM_PATH_PYTHON` installs the module into `$prefix/lib/python$PYTHON_VERSION/site-packages`, where `$PYTHON_VERSION` is whatever Python the build container shipped (3.9 on Alma 9, 3.10/3.12 on Ubuntu 22.04/24.04, 3.11 on Debian 12). This is intentional: each package is built for a specific Python and the env script auto-discovers the versioned directory at use time.
- **No rollback.** If `create-release` fails after `prepare` has pushed the tag, the tag stays. The maintainer deletes the tag (`git push origin :v$NEW`) and re-dispatches. The bumped commit on master stays — that's harmless; the version is what it is.