|
xrpld
|
This directory contains all files needed to build RPM and Debian packages for xrpld. The packages also ship the validator-keys tool, so packaging requires a build configured with -Dvalidator_keys=ON.
Packaging is declared on the build configs themselves, in .github/scripts/strategy-matrix/linux.json: a config that is also packaged carries a package map, so its binaries and its packaging job cannot drift apart. Today only linux/amd64 is emitted. The map pins the full container image in image — edit that field to move to a new image and both CI and local builds pick it up — and names the format that image builds in type, which CI passes to build_pkg.py as --package-type; the two have to stay in step. An optional variant names a flavour of the package (see Package variantsPackage variants), and CI passes it as --variant.
| Package type | Image (configs.<distro>[].package.image in linux.json) | Tools required |
|---|---|---|
| RPM | ghcr.io/xrplf/xrpld/packaging-rhel:sha-<sha> | rpmbuild, rpmsign |
| DEB | ghcr.io/xrplf/xrpld/packaging-debian:sha-<sha> | dpkg-buildpackage, debhelper with compat level 13, lintian |
To print the full packaging matrix (artifact names, images and package names) for the current linux.json:
A config whose binaries are not the plain release build cannot be packaged as xrpld: both would carry the same name and version, so whichever published last would win. It is packaged as a variant instead — variant: "assert" in its package map, which CI passes to build_pkg.py as --variant assert, producing xrpld-assert. What the build option itself does is a build concern, not a packaging one; see the options table in BUILD.md.
A variant ships the same paths as xrpld — /usr/bin/xrpld, /etc/xrpld, xrpld.service, /etc/logrotate.d/xrpld — differing only in the per-package documentation directory, so it declares itself a stand-in for the plain package rather than something installable next to it: Conflicts, Replaces and a versioned Provides: xrpld on Debian, Conflicts and Provides on RPM. Neither format declares Obsoletes, so apt upgrade and dnf upgrade keep an installed flavour on its own flavour, and switching is always explicit:
Only the DEB packages carry a variant today — xrpld-assert comes from the debian config alone, there being no call for an assert build on RHEL-based distributions — but the RPM side works the same way if one is added.
A switch is a removal plus an installation rather than an upgrade, so unlike a version upgrade it stops the service: Debian's scriptlets start it again, while on RPM the operator runs systemctl start xrpld. Configuration survives either way, being conffiles on Debian and config(noreplace) on RPM.
dnf installs the replacement before erasing the old flavour, whose preun would leave xrpld.service disabled, so postun re-applies the preset when the unit file outlives the erase — which, since rpm keeps a file another installed package owns, happens only during a swap. The cost is that a deliberate systemctl disable is not carried across an RPM switch.
The alternative is an xrpld-common package owning the unit, the sysusers and tmpfiles snippets and the configuration, required by both flavours at an exact version: nothing is erased mid-swap, so no scriptlet has to detect one. It is not worth it for a single variant — it moves files out of the production package, and a sanitizer flavour would likely need its own unit anyway, putting the lifecycle back where it is now.
Adding a variant is the flavour in VARIANTS in build_pkg.py, which is the list --variant accepts, plus a config in linux.json with the CMake arguments and a package map naming it, for one format or for both: generate.py --packaging emits the package names per format, and the test-install-deb and test-install-rpm jobs install what their own format produced.
Operators switch between the flavours as described in docs/install.md.
Caller workflows (on-pr.yml, on-tag.yml, on-trigger.yml) call reusable-package.yml, which runs in these stages:
from the tested DEB, and pushes them when publishing.
The packaging script derives the package version from the downloaded binary's xrpld --version output; no CMake configure or build step is needed inside the packaging job.
The binaries come from the debian and rhel build configs themselves — the ones carrying the package map — which pass -Dvalidator_keys=ON so that the build job produces validator-keys next to xrpld and uploads it as the validator-keys-<config name> artifact. The packaging matrix names both artifacts (xrpld_artifact_name and validator_keys_artifact_name) after that same config, so a packaged config must keep -Dvalidator_keys=ON. Those configs are not minimal, so on-pr.yml only packages once a PR runs the full matrix.
validator-keys is fetched from an exact commit pinned in cmake/XrplValidatorKeys.cmake, so a given xrpld version always packages the same tool; bump that commit deliberately.
With xrpld and validator-keys binaries already built at build/xrpld and build/validator-keys, run the packaging step inside the same container CI uses. The image tag is derived from linux.json so you don't need to hardcode a SHA.
Add --variant assert to package binaries built with -Dassert=ON; the package is then named xrpld-assert.
If you run CMake configure on a host that has rpmbuild or dpkg-buildpackage installed natively, you can use the CMake target directly — no container needed, but the host toolchain replaces the pinned CI image:
The cmake/XrplPackaging.cmake module defines the package target only if at least one of rpmbuild / dpkg-buildpackage is present and both the xrpld and validator-keys targets exist (-Dxrpld=ON -Dvalidator_keys=ON); the target builds both binaries before packaging, passing --package-type deb when dpkg-buildpackage is present and rpm otherwise, and --channel UNRELEASED. The packaging script installs to FHS-standard paths (/usr/bin, /etc/xrpld, etc.) regardless of CMAKE_INSTALL_PREFIX.
The package version is not a CMake input on this path: build_pkg.py derives it from the just-built xrpld binary's xrpld --version output. The package release defaults to 1 and is overridable with -Dpkg_release=N.
-Dassert=ON passes --variant assert, so such a build packages as xrpld-assert without anything else being asked for.
Packages are published to the XRPLF repositories on Sonatype Nexus through https://packages-upload.xrplf.org. Reads go through https://packages.xrplf.org, which Cloudflare proxies to cache them and which rejects request bodies over 100 MB, so uploads use the DNS-only host instead. The release-info action decides the channel from the event, and publish_pkg.py maps that channel to its repositories:
| Event | Version | Channel | DEB repository | RPM upload repository |
|---|---|---|---|---|
| tag | X.Y.Z | stable | deb-stable | rpm-stable-hosted |
| tag | X.Y.Z-rcN | rc | deb-rc | rpm-rc-hosted |
| tag | X.Y.Z-bN | beta | deb-beta | rpm-beta-hosted |
| tag, any other | xrpld --version | custom | deb-custom | rpm-custom-hosted |
| push to develop | 0.0.0-dev+<hash> | develop | deb-develop | rpm-develop-hosted |
| tag, non-public codebase | any but X.Y.Z | private | deb-private | rpm-private-hosted |
A variant is published to the same channel under its own name, so xrpld-assert never overwrites xrpld.
Only a tag picks a release channel. A final release, X.Y.Z, goes to stable even from a non-public codebase. Versions sort in row order, so moving to a more mature channel never downgrades. A tag matching none of the release patterns, such as X.Y.Z-hotfix1, publishes to custom, which sits outside that order.
Every untagged build reports 0.0.0-dev+<hash>, which the develop channel packages as 0.0.0~dev, below every release.
The action also picks the package release number:
Both reach the packaging scripts as arguments, so neither script derives anything itself.
Publishing is its own job, gated behind the install tests, uploading from the same image that built the packages with the publish_pkg.py shipped in it — the same copy other repositories run. Without publish: true the job is a --dry-run, listing the uploads it would make without needing credentials, so any run that builds packages also exercises the upload routing. on-trigger.yml passes publish: true for develop pushes in XRPLF/rippled and on-tag.yml for tags in any XRPLF repository, both authenticating with the NEXUS_REMOTE_USERNAME / NEXUS_REMOTE_PASSWORD secrets already used for the Conan remote; on-pr.yml never publishes.
Nexus owns the repository metadata; nothing here indexes anything. Worth knowing:
publish_pkg.py knows nothing about xrpld, so the packaging image installs it at /usr/local/bin/publish_pkg.py for other XRPLF repositories that build their packages elsewhere.
The docker job installs the tested xrpld DEB on ubuntu:26.04 using images/xrpld/Dockerfile, once per target, and checks that the server starts in each image. A tag's images are tagged with the tag name, a develop image as develop. With publish: true:
build_pkg.py derives the xrpld software version from ${BUILD_DIR}/xrpld --version in both package formats.
The binary's version is already SemVer-validated by BuildInfo. build_pkg.py converts pre-release versions such as 3.2.0-b1 or 3.2.0-rc1 from - to ~ for package metadata so pre-releases sort before the final release. If that normalized package version still contains -, packaging fails because RPM forbids - in Version, and Debian uses - as the upstream/revision separator.
pkg_version is the normalized package metadata version derived inside build_pkg.py from the binary-reported xrpld version (- pre-release separator converted to ~). It is not a separate user input.
PKG_RELEASE is a different value: the package release iteration for that xrpld version. RPM receives the normalized pkg_version and PKG_RELEASE as the pkg_version and pkg_release macros for its Version and Release values; DEB writes them as ${pkg_version}-${PKG_RELEASE} in debian/changelog.
With PKG_RELEASE=1, the package metadata becomes:
| Input version | RPM version/release | Debian version |
|---|---|---|
| 3.2.0 | 3.2.0-1%{?dist} | 3.2.0-1 |
| 3.2.0-b1 | 3.2.0~b1-1%{?dist} | 3.2.0~b1-1 |
| 3.2.0-rc1 | 3.2.0~rc1-1%{?dist} | 3.2.0~rc1-1 |
| 3.2.0-custom-1 | 3.2.0~custom.1-1%{?dist} | 3.2.0~custom.1-1 |
| 0.0.0-dev+abc1234 | 0.0.0~dev-1%{?dist} | 0.0.0~dev-1 |
build_pkg.py defines dist as .el9 rather than letting rpmbuild take it from the build host, so the RHEL image can track a newer release without changing what the packages claim to target.
The Debian changelog entry carries the channel passed as --channel: one of the channels in the table above, or UNRELEASED, the Debian convention for a build that targets no channel, which local and CMake builds pass. Each channel accepts:
The RPM path intentionally uses ~ in Version, matching the Debian pre-release ordering convention, so RPM filenames/NVRs begin with forms like xrpld-3.2.0~b1-... and xrpld-3.2.0~rc1-... instead of encoding pre-releases with an older 0.<release>.<suffix> RPM Release value.
--variant is the flavour of the package, empty by default and accepting only the flavours in VARIANTS; see Package variantsPackage variants. The RPM path passes it to the spec as the pkg_variant macro, which suffixes Name and adds the Conflicts/Provides pair. Debian control files have no conditionals, so the DEB path renders debian/control.in and debian/lintian-overrides.in instead, substituting @PKG@ with the package name and @VARIANT_FIELDS@ with the Conflicts/Replaces/Provides block, empty for the plain package; a token with no value fails the build rather than reaching dpkg. The files debhelper keys by package name (docs, links, and the units) are staged under that same name. The paths inside the package are unchanged either way, so debian/rules reads its package name from dh_listpackages and names the unit, sysusers, tmpfiles and logrotate files with --name xrpld.
The package format is --package-type, either deb or rpm. It is required, so a job never silently builds the wrong format for the image it runs in; the matching build tool still has to be on PATH.
Every input is a named argument, and every argument but --build-dir and --pkg-release is required. The repository root is not an argument at all: the script reads it from its own location. Only secrets stay in the environment, so they never reach the process list – PKG_SIGNING_KEY for sign_rpm.py, and NEXUS_USERNAME / NEXUS_PASSWORD for publish_pkg.py.
Signing is not part of this script. sign_rpm.py does it in a separate CI step that only runs when publishing, so a published RPM is always signed and a local build never needs a key.
It resolves the build directory to an absolute path, then calls stage_common() to copy the xrpld and validator-keys binaries, config files, and shared support files into the staging area, and invokes the platform build tool. Both binaries must be present in the build directory and must run in the packaging environment; a missing or non-runnable one fails early. That runtime check is what catches a binary still linked against the Nix store's ELF loader (see patch_nix_binary in cmake/PatchNixBinary.cmake).
RPM upgrades intentionally do not restart a running xrpld service. The spec uses systemd_postun, matching Debian's dh_installsystemd --no-stop-on-upgrade behavior; operators pick up the new binary on the next service restart.
Runs dpkg-buildpackage -b --no-sign -d (-d skips the build-dependency check, since the binary is already built). debian/rules uses manual install commands.
It also rewrites the libc6 bound to LIBC_MIN in debian/rules, the glibc the Nix toolchain builds against. dpkg-shlibdeps would otherwise derive it from the build host's symbols file — on trixie that yields libc6 (>= 2.34) because of sysconf, locking out distros the binaries run on. A check fails the build if either binary outgrows LIBC_MIN.
lintian still reports embedded-library zlib, no-manual-page and initial-upload-closes-no-bugs; only the /usr/local tags are overridden.
Both formats build reproducibly as they are: the same binaries at the same commit give byte-identical packages on a rebuild, and nothing has to be exported by hand.
build_pkg.py sets SOURCE_DATE_EPOCH from the latest git commit time. dpkg-buildpackage honours it on its own; the RPM spec sets three macros: