build: Add assert-enabled builds and packages

This commit is contained in:
Ayaz Salikhov
2026-09-08 21:36:55 +01:00
committed by Bart
parent faa2bf583f
commit 295b74da1c
17 changed files with 504 additions and 182 deletions

View File

@@ -15,6 +15,14 @@ _BASE_CMAKE_ARGS = [
"-Drust=ON",
]
# The package formats a config can be packaged as, each with its own
# install-test job in reusable-package.yml.
PACKAGE_TYPES = ("deb", "rpm")
# The package name a variant suffixes, as build_pkg.py's BASE_NAME spells it:
# the two have to agree, or the artifact globs miss what was built.
BASE_NAME = "xrpld"
# Maps sanitizer names (as used in cmake) to short config-name suffixes.
_SANITIZER_SUFFIX: dict[str, str] = {
"address": "asan",
@@ -62,10 +70,20 @@ def get_cmake_args(build_type: str, extra_args: str) -> str:
class PackageConfig:
"""The 'package' map of a config whose binaries are also packaged."""
type: str # "deb" or "rpm"; has to match what the image provides
type: str # has to match what the image provides
# The packaging container image: a vanilla distro image, not the nix image
# the config itself builds in.
image: str
# A flavour of the package, named xrpld-<variant>, for a config whose
# binaries are not the plain release build. A variant needs no counterpart
# in the other format.
variant: str = ""
def __post_init__(self) -> None:
assert self.type in PACKAGE_TYPES, (
f"unsupported package type {self.type!r}: "
f"use one of {', '.join(PACKAGE_TYPES)}."
)
@dataclasses.dataclass
@@ -178,6 +196,8 @@ class PackagingEntry:
validator_keys_artifact_name: str
image: str
package_type: str # "deb" or "rpm"; drives the format-specific steps
package_variant: str # passed to build_pkg.py --variant; empty for xrpld
package_name: str # the name it builds under, which the artifact globs use
# ---------------------------------------------------------------------------
@@ -267,12 +287,32 @@ def expand_linux_packaging(linux: LinuxFile) -> list[PackagingEntry]:
validator_keys_artifact_name=f"validator-keys-{name}",
image=cfg.package.image,
package_type=cfg.package.type,
package_variant=cfg.package.variant,
package_name=(
f"{BASE_NAME}-{cfg.package.variant}"
if cfg.package.variant
else BASE_NAME
),
)
)
return entries
def package_names_by_type(entries: list[PackagingEntry]) -> dict[str, list[str]]:
"""The names of the packages in 'entries', keyed by format.
Derived from the packaging matrix rather than listed again, so the packages
the install-test jobs look for are the packages that were built.
"""
return {
package_type: sorted(
{e.package_name for e in entries if e.package_type == package_type}
)
for package_type in PACKAGE_TYPES
}
def expand_platform_matrix(pf: PlatformFile, minimal: bool) -> list[MatrixEntry]:
"""Expand a PlatformFile (macOS or Windows) into matrix entries.
@@ -341,6 +381,10 @@ if __name__ == "__main__":
if args.packaging:
matrix = expand_linux_packaging(LinuxFile.load(THIS_DIR / "linux.json"))
# One list per format, so each install-test job installs the packages its
# own format produced.
for package_type, names in package_names_by_type(matrix).items():
print(f"{package_type}_package_names={json.dumps(names)}")
else:
if args.config in ("linux", None):
matrix += expand_linux_matrix(

View File

@@ -76,6 +76,19 @@
"type": "deb",
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-49cdc10"
}
},
{
"compiler": ["gcc"],
"build_type": ["Release"],
"arch": ["amd64"],
"minimal": false,
"suffix": "assert",
"extra_cmake_args": "-Dvalidator_keys=ON -Dassert=ON",
"package": {
"type": "deb",
"image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-49cdc10",
"variant": "assert"
}
}
],

View File

@@ -85,6 +85,7 @@ jobs:
.github/workflows/reusable-build-test.yml
.github/workflows/reusable-check-autogen.yml
.github/workflows/reusable-clang-tidy.yml
.github/workflows/reusable-package-test-install.yml
.github/workflows/reusable-package.yml
.github/workflows/reusable-rust.yml
.github/workflows/reusable-strategy-matrix.yml

View File

@@ -23,6 +23,7 @@ on:
- ".github/workflows/reusable-build-test.yml"
- ".github/workflows/reusable-check-autogen.yml"
- ".github/workflows/reusable-clang-tidy.yml"
- ".github/workflows/reusable-package-test-install.yml"
- ".github/workflows/reusable-package.yml"
- ".github/workflows/reusable-rust.yml"
- ".github/workflows/reusable-strategy-matrix.yml"

View File

@@ -0,0 +1,103 @@
# Install one package format on every distro family it targets, one job per
# package name and image, and run the binaries there. Called once per format by
# reusable-package.yml, which owns the names and the image lists.
name: Install packages
on:
workflow_call:
inputs:
package_type:
description: 'The package format to install ("deb" or "rpm").'
required: true
type: string
package_names:
description: "JSON array of package names built for this format."
required: true
type: string
images:
description: "JSON array of container images to install in."
required: true
type: string
defaults:
run:
shell: bash
env:
PACKAGE_DIR: packages
jobs:
install:
strategy:
fail-fast: false
matrix:
package_name: ${{ fromJson(inputs.package_names) }}
image: ${{ fromJson(inputs.images) }}
name: "${{ matrix.package_name }} on ${{ matrix.image }}"
permissions:
contents: read
runs-on: ubuntu-latest
container: ${{ matrix.image }}
timeout-minutes: 5
steps:
# Every package lands in one directory; the step below picks its own,
# which keeps this independent of the artifact names.
- name: Download package artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: "*-pkg"
merge-multiple: true
path: ${{ env.PACKAGE_DIR }}
- name: Find the package
id: find
env:
PACKAGE_NAME: ${{ matrix.package_name }}
PACKAGE_TYPE: ${{ inputs.package_type }}
run: |
# The version follows the name, separated by '_' in a DEB and '-' in an
# RPM. Requiring a digit after it is what keeps 'xrpld' from picking up
# another package, such as 'xrpld-assert'.
pattern="${PACKAGE_NAME}[_-][0-9]*.${PACKAGE_TYPE}"
package="$(find "${PACKAGE_DIR}" -type f -name "${pattern}" -print -quit)"
test -n "${package}" || {
echo "no ${pattern} found in ${PACKAGE_DIR}" >&2
exit 1
}
echo "package=${package}" >>"${GITHUB_OUTPUT}"
- name: Install the DEB
if: ${{ inputs.package_type == 'deb' }}
env:
DEBIAN_FRONTEND: noninteractive
PACKAGE: ${{ steps.find.outputs.package }}
run: |
# Stock Debian and Ubuntu images carry no package lists, so apt has
# nothing to resolve the systemd dependency from until it fetches them.
apt-get update -qq
apt-get install -y "./${PACKAGE}"
- name: Install the RPM
if: ${{ inputs.package_type == 'rpm' }}
env:
PACKAGE: ${{ steps.find.outputs.package }}
run: dnf install -y "./${PACKAGE}"
- name: Run xrpld
run: xrpld --version
- name: Run validator-keys
run: validator-keys --version
- name: Run rippled, the legacy compatibility symlink
run: rippled --version
- name: Check the service account
run: id xrpld
- name: Check the state directory
run: test -d /var/lib/xrpld
- name: Check the log directory
run: test -d /var/log/xrpld

View File

@@ -3,8 +3,10 @@
#
# - 'package' builds and signs one format per config that carries a "package"
# map in linux.json; that map names the container image and the format
# - 'test-install' installs what was built on a range of distros and runs the
# binaries there, so a package that cannot be installed never reaches Nexus
# - 'test-install-deb' and 'test-install-rpm' call
# reusable-package-test-install.yml to install what was built on a range of
# distros and run the binaries there, so a package that cannot be installed
# never reaches Nexus
# - 'publish' uploads with the image's publish_pkg.py, doing a --dry-run
# unless 'publish: true'
#
@@ -49,6 +51,8 @@ jobs:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.generate.outputs.matrix }}
deb_package_names: ${{ steps.generate.outputs.deb_package_names }}
rpm_package_names: ${{ steps.generate.outputs.rpm_package_names }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -107,6 +111,7 @@ jobs:
- name: Build package
env:
PACKAGE_TYPE: ${{ matrix.package_type }}
PACKAGE_VARIANT: ${{ matrix.package_variant }}
PKG_RELEASE: ${{ steps.release_info.outputs.pkg_release }}
CHANNEL: ${{ steps.release_info.outputs.channel }}
run: |
@@ -114,6 +119,7 @@ jobs:
--package-type "${PACKAGE_TYPE}" \
--build-dir "${BUILD_DIR}" \
--pkg-release "${PKG_RELEASE}" \
--variant "${PACKAGE_VARIANT}" \
--channel "${CHANNEL}"
# Before the upload, so the artifact, the tested package and the published
@@ -125,14 +131,17 @@ jobs:
run: ./package/sign_rpm.py --package-dir "${BUILD_DIR}"
# Split from the debug symbols, which are an order of magnitude larger, so
# that test-install downloads only what it installs.
# that test-install downloads only what it installs. In the globs below the
# version follows the name, separated by '_' in a DEB and '-' in an RPM. A
# version starts with a digit and a longer name does not, so that one digit
# is what tells 'xrpld-3.4.1-...' from 'xrpld-assert-3.4.1-...'.
- name: Upload package artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ matrix.xrpld_artifact_name }}-pkg
path: |
${{ env.BUILD_DIR }}/debbuild/xrpld_*.deb
${{ env.BUILD_DIR }}/rpmbuild/RPMS/**/xrpld-[0-9]*.rpm
${{ env.BUILD_DIR }}/debbuild/${{ matrix.package_name }}_[0-9]*.deb
${{ env.BUILD_DIR }}/rpmbuild/RPMS/**/${{ matrix.package_name }}-[0-9]*.rpm
if-no-files-found: error
- name: Upload debug symbol artifact
@@ -140,129 +149,52 @@ jobs:
with:
name: ${{ matrix.xrpld_artifact_name }}-pkg-debug
path: |
${{ env.BUILD_DIR }}/debbuild/xrpld-dbgsym_*.deb
${{ env.BUILD_DIR }}/debbuild/xrpld-dbgsym_*.ddeb
${{ env.BUILD_DIR }}/rpmbuild/RPMS/**/xrpld-debuginfo-*.rpm
${{ env.BUILD_DIR }}/debbuild/${{ matrix.package_name }}-dbgsym_[0-9]*.deb
${{ env.BUILD_DIR }}/debbuild/${{ matrix.package_name }}-dbgsym_[0-9]*.ddeb
${{ env.BUILD_DIR }}/rpmbuild/RPMS/**/${{ matrix.package_name }}-debuginfo-[0-9]*.rpm
if-no-files-found: error
# Every distro family the packages target, oldest release first, so both ends
# of the dependency range they declare are exercised.
test-install:
needs: [package]
strategy:
fail-fast: false
matrix:
include:
- package_type: deb
image: debian:11
- package_type: deb
image: debian:12
- package_type: deb
image: debian:13
- package_type: deb
image: ubuntu:20.04
- package_type: deb
image: ubuntu:22.04
- package_type: deb
image: ubuntu:24.04
- package_type: deb
image: ubuntu:26.04
# One call per format, so a variant packaged for one format is installed for
# that format alone. The images are every distro family that format targets,
# oldest release first, so both ends of the dependency range the packages
# declare are exercised.
test-install-deb:
needs: [generate-matrix, package]
name: install deb
uses: ./.github/workflows/reusable-package-test-install.yml
with:
package_type: deb
package_names: ${{ needs.generate-matrix.outputs.deb_package_names }}
images: |
[
"debian:11",
"debian:12",
"debian:13",
"ubuntu:20.04",
"ubuntu:22.04",
"ubuntu:24.04",
"ubuntu:26.04"
]
- package_type: rpm
image: almalinux:9
- package_type: rpm
image: almalinux:10
- package_type: rpm
image: rockylinux/rockylinux:9
- package_type: rpm
image: rockylinux/rockylinux:10
- package_type: rpm
image: registry.access.redhat.com/ubi9/ubi
- package_type: rpm
image: registry.access.redhat.com/ubi10/ubi
name: "install ${{ matrix.package_type }} on ${{ matrix.image }}"
permissions:
contents: read
runs-on: ubuntu-latest
container: ${{ matrix.image }}
timeout-minutes: 5
steps:
# Both formats land in one directory; the step below picks its own by
# extension, so this stays independent of the artifact names.
- name: Download package artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: "*-pkg"
merge-multiple: true
path: ${{ env.PACKAGE_DIR }}
- name: Find the package
id: find
env:
PACKAGE_TYPE: ${{ matrix.package_type }}
run: |
package="$(find "${PACKAGE_DIR}" -type f -name "*.${PACKAGE_TYPE}" -print -quit)"
test -n "${package}" || {
echo "no .${PACKAGE_TYPE} found in ${PACKAGE_DIR}" >&2
exit 1
}
echo "package=${package}" >>"${GITHUB_OUTPUT}"
# Debian 11 went end-of-life on 2026-08-31
# (https://www.debian.org/News/2026/20260831) and its packages are
# already partly gone from deb.debian.org, so switch to the
# snapshot.debian.org entries the image ships commented out in its
# sources.list: they are pinned to the snapshot the image was built
# from, so they serve every version it needs and never go away.
# Snapshots keep their original, long-passed Valid-Until, hence the
# disabled check; the retries absorb snapshot.debian.org's throttling.
- name: Switch Debian 11 to snapshot.debian.org
if: ${{ matrix.image == 'debian:11' }}
run: |
sed -i 's|^deb |# deb |; s|^# deb http://snapshot|deb http://snapshot|' /etc/apt/sources.list
printf '%s\n' \
'Acquire::Check-Valid-Until "false";' \
'Acquire::Retries "3";' \
>/etc/apt/apt.conf.d/99snapshot
- name: Install the DEB
if: ${{ matrix.package_type == 'deb' }}
env:
DEBIAN_FRONTEND: noninteractive
PACKAGE: ${{ steps.find.outputs.package }}
run: |
# Stock Debian and Ubuntu images carry no package lists, so apt has
# nothing to resolve the systemd dependency from until it fetches them.
apt-get update -qq
apt-get install -y "./${PACKAGE}"
- name: Install the RPM
if: ${{ matrix.package_type == 'rpm' }}
env:
PACKAGE: ${{ steps.find.outputs.package }}
run: dnf install -y "./${PACKAGE}"
- name: Run xrpld
run: xrpld --version
- name: Run validator-keys
run: validator-keys --version
- name: Run rippled, the legacy compatibility symlink
run: rippled --version
- name: Check the service account
run: id xrpld
- name: Check the state directory
run: test -d /var/lib/xrpld
- name: Check the log directory
run: test -d /var/log/xrpld
test-install-rpm:
needs: [generate-matrix, package]
name: install rpm
uses: ./.github/workflows/reusable-package-test-install.yml
with:
package_type: rpm
package_names: ${{ needs.generate-matrix.outputs.rpm_package_names }}
images: |
[
"almalinux:9",
"almalinux:10",
"rockylinux/rockylinux:9",
"rockylinux/rockylinux:10",
"registry.access.redhat.com/ubi9/ubi",
"registry.access.redhat.com/ubi10/ubi"
]
publish:
needs: [generate-matrix, package, test-install]
needs: [generate-matrix, package, test-install-deb, test-install-rpm]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}

View File

@@ -44,12 +44,18 @@ else()
set(pkg_type rpm)
endif()
# Unquoted below, so an empty value adds no argument at all.
set(pkg_variant_option "")
if(assert)
set(pkg_variant_option --variant=assert)
endif()
add_custom_target(
package
COMMAND
${CMAKE_SOURCE_DIR}/package/build_pkg.py --package-type=${pkg_type}
--build-dir=${CMAKE_BINARY_DIR} --pkg-release=${pkg_release}
--channel=UNRELEASED
${pkg_variant_option} --channel=UNRELEASED
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
DEPENDS xrpld validator-keys
COMMENT "Building Linux ${pkg_type} package"

View File

@@ -6,7 +6,8 @@
`xrpld` is published as DEB and RPM packages for 64-bit x86 Linux.
Use APT on Debian-based distributions such as Debian and Ubuntu,
and YUM on Red Hat-based distributions such as RHEL, AlmaLinux, and Rocky Linux.
and DNF on Red Hat-based distributions such as RHEL, AlmaLinux, and Rocky Linux,
where `yum` is a symlink to `dnf`.
To build from source instead, see [BUILD.md](../BUILD.md).
## Release channels
@@ -81,7 +82,7 @@ wherever it appears in the repository configuration.
sudo apt -y install xrpld
```
### With the YUM package manager
### With the DNF package manager
1. Add the XRPL Foundation package-signing key:
@@ -109,9 +110,23 @@ wherever it appears in the repository configuration.
3. Install the `xrpld` package:
```bash
sudo yum install -y xrpld
sudo dnf install -y xrpld
```
### Optional: the assert-enabled build
Every channel also carries `xrpld-assert` as a DEB, the same build with assertions
enabled, for diagnosing a problem on a non-production server.
It installs the same files as `xrpld` and replaces it, so install one or the other:
```bash
sudo apt -y install xrpld-assert # APT removes xrpld itself
```
Switching stops the service, since it is a removal and an installation rather than an upgrade,
and APT starts it again.
Install `xrpld` the same way to switch back.
## The xrpld service
Both package managers install a systemd unit and enable it, so `xrpld` starts on boot.
@@ -121,7 +136,7 @@ Check whether it is already running:
systemctl status xrpld.service
```
The APT packages start it immediately as well; the YUM packages do not, so start it yourself:
The DEB packages start it immediately as well; the RPM packages do not, so start it yourself:
```bash
sudo systemctl start xrpld.service

View File

@@ -15,7 +15,8 @@ package/
publish_pkg.py Uploads built packages to the XRPLF Nexus repositories (called by CI, and shipped in that image)
rpm/
xrpld.spec RPM spec
debian/ Debian control files (control, rules, copyright, xrpld.docs, xrpld.links, xrpld.lintian-overrides, source/format)
debian/ Debian control files (control.in, lintian-overrides.in, rules, copyright, docs, links, source/format).
The `.in` files are templates rendered by `build_pkg.py`; `docs` and `links` are staged under the package name
shared/
xrpld.service systemd unit file (used by both RPM and DEB)
xrpld.sysusers sysusers.d config (used by both RPM and DEB)
@@ -32,20 +33,74 @@ 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.
have to stay in step. An optional `variant` names a flavour of the package (see
[Package variants](#package-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 and images) for the current
`linux.json`:
To print the full packaging matrix (artifact names, images and package names)
for the current `linux.json`:
```bash
./.github/scripts/strategy-matrix/generate.py --packaging
```
## Package variants
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`](../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:
```bash
apt-get install xrpld-assert # apt removes the plain package itself
dnf swap xrpld xrpld-VARIANT # 'dnf install' alone stops at the conflict
```
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`](../docs/install.md#optional-the-assert-enabled-build).
## Building packages
### Via CI
@@ -56,9 +111,11 @@ Caller workflows (`on-pr.yml`, `on-tag.yml`, `on-trigger.yml`) call
1. `package` fans out one job per config carrying a `package` map, building and
signing in that config's container, and uploading `<config>-pkg` alongside
`<config>-pkg-debug` for the much larger debug symbols.
2. `test-install` installs `<config>-pkg` in the container of every distro the
packages target and runs the binaries there, so one that cannot be installed
never reaches Nexus.
2. `test-install-deb` and `test-install-rpm` call
[`reusable-package-test-install.yml`](../.github/workflows/reusable-package-test-install.yml)
with their format's package names and distro images, installing each package
in the container of every distro that format targets and running the binaries
there, so one that cannot be installed never reaches Nexus.
3. `publish` uploads both artifacts, or lists what it would upload.
The packaging script derives the package version from the downloaded binary's
@@ -104,6 +161,9 @@ docker run --rm \
# build/rpmbuild/RPMS/x86_64/*.rpm
```
Add `--variant assert` to package binaries built with `-Dassert=ON`; the package
is then named `xrpld-assert`.
### Via CMake (host-side target)
If you run CMake configure on a host that has `rpmbuild` or `dpkg-buildpackage`
@@ -133,6 +193,9 @@ 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.
## Publishing packages
Packages are published to the XRPLF repositories on Sonatype Nexus at
@@ -147,6 +210,9 @@ the event, and `publish_pkg.py` maps that channel to its repositories:
| push to `develop` | `xrpld --version` | `develop` | `deb-develop` | `rpm-develop-hosted` |
| tag, non-public codebase | _any_ | `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 names a channel — do not extend that to `develop`, where
`BuildInfo.cpp`'s `versionString` moves through `-bN`, `-rcN` and even the final
version during a release cycle, which would send develop builds into `stable`.
@@ -160,7 +226,7 @@ the last, and the date and hash say which commit a package on
`packages.xrplf.org` came from. Both reach the packaging scripts as arguments,
so neither script derives anything itself.
Publishing is its own job, gated behind `test-install`, uploading from the same
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
@@ -175,7 +241,7 @@ Nexus owns the repository metadata; nothing here indexes anything. Worth knowing
- Each apt-hosted repository needs a distribution (ours use `any`) and a PGP
signing keypair configured in Nexus, which rejects one created without a
keypair. Nexus signs the apt metadata with it, never the packages.
- Hosted yum repositories cannot be signed by Nexus, so each `rpm-<channel>-hosted`
- yum-hosted repositories cannot be signed by Nexus, so each `rpm-<channel>-hosted`
repository sits behind a `rpm-<channel>` yum group repository whose metadata
Nexus signs. Uploads go to the hosted repository; clients point at the group
and verify the metadata with `repo_gpgcheck=1`. Nexus never signs the RPMs
@@ -244,6 +310,19 @@ 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 variants](#package-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.
@@ -286,8 +365,13 @@ service restart.
1. Creates a staging source tree at `debbuild/source/` inside the build directory.
2. Stages the binaries, configs, `README.md`, `LICENSE.md`, and
`validator-keys-LICENSE`.
3. Copies `package/debian/` control files into `debbuild/source/debian/`.
4. Copies shared service/sysusers/tmpfiles/logrotate into `debian/` where `dh_installsystemd`, `dh_installsysusers`, `dh_installtmpfiles` and `dh_installlogrotate` pick them up automatically.
3. Stages `package/debian/` into `debbuild/source/debian/`: the `.in` templates
are rendered, and the files debhelper keys by package name (`docs`, `links`,
`lintian-overrides`) are staged under the name being built.
4. Copies shared service/sysusers/tmpfiles/logrotate into `debian/` as
`<package>.xrpld.*`, which `dh_installsystemd`, `dh_installsysusers`,
`dh_installtmpfiles` and `dh_installlogrotate` read because `debian/rules`
passes them `--name xrpld`.
5. Generates a minimal `debian/changelog` using `${pkg_version}-${PKG_RELEASE}`,
where `pkg_version` is derived from the binary-reported `xrpld` version.
6. 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.

View File

@@ -21,6 +21,14 @@ SRC_DIR = Path(__file__).resolve().parents[1]
PRE_RELEASE = re.compile(r"^(b|rc)(0|[1-9][0-9]*)(\+.*)?$")
# The package name a variant suffixes, and the name every variant keeps for its
# on-disk paths (/usr/bin/xrpld, /etc/xrpld, xrpld.service).
BASE_NAME = "xrpld"
# The flavours that can be built, '' being the plain xrpld package. A variant
# needs a config in linux.json to be built by CI; see package/README.md.
VARIANTS = ("", "assert")
# Files both packaging systems consume, staged under the same names.
STAGED_FROM_BUILD = ("xrpld", "validator-keys", "validator-keys-LICENSE")
STAGED_FROM_SRC = {
@@ -31,6 +39,18 @@ STAGED_FROM_SRC = {
}
STAGED_UNITS = ("xrpld.service", "xrpld.sysusers", "xrpld.tmpfiles", "xrpld.logrotate")
# debian/ files debhelper keys by package name, staged as '<package>.<name>'.
DEBIAN_PKG_FILES = ("docs", "links")
# Debian control files have no conditionals, so what makes a variant replace the
# plain package is rendered into control.in rather than written there.
DEB_VARIANT_FIELDS = """\
Conflicts: xrpld
Replaces: xrpld
Provides: xrpld (= ${binary:Version})"""
TOKEN = re.compile(r"@[A-Z_]+@")
def run(*command: object, cwd: Path | None = None) -> None:
"""Echo a command and run it."""
@@ -75,6 +95,28 @@ def package_version(reported: str) -> str:
return version
def render(template: Path, dest: Path, values: dict[str, str]) -> None:
"""Write template to dest with its @TOKEN@ placeholders substituted.
A token left without a value fails the build rather than reaching dpkg.
"""
text = template.read_text()
for token, value in values.items():
text = text.replace(f"@{token}@", value)
missing = sorted(set(TOKEN.findall(text)))
assert not missing, f"{template}: no value for {', '.join(missing)}"
# An empty value at the end of a stanza would otherwise leave a blank line,
# which is what ends a stanza.
dest.write_text(text.rstrip("\n") + "\n")
def package_name(variant: str) -> str:
"""The binary package name for a variant: '' -> xrpld, 'assert' -> xrpld-assert."""
return f"{BASE_NAME}-{variant}" if variant else BASE_NAME
def read_version(xrpld: Path) -> str:
"""Read the version from the binary that is about to be packaged."""
fields = capture(xrpld, "--version").partition("\n")[0].split()
@@ -135,17 +177,18 @@ def stage_common(build_dir: Path, dest: Path) -> None:
shutil.copy2(SRC_DIR / source, dest / name)
def stage_units(dest: Path) -> None:
def stage_units(dest: Path, *, prefix: str = "") -> None:
"""Copy the systemd, sysusers, tmpfiles and logrotate files into dest.
Each format wants them somewhere else: rpmbuild reads them from SOURCES,
debhelper from debian/.
Each format wants them somewhere else: rpmbuild reads them from SOURCES by
path, debhelper from debian/ by package name -- hence 'prefix', which makes
the copies 'xrpld-assert.xrpld.service' and so on.
"""
for name in STAGED_UNITS:
shutil.copy2(SRC_DIR / "package" / "shared" / name, dest / name)
shutil.copy2(SRC_DIR / "package" / "shared" / name, dest / f"{prefix}{name}")
def build_rpm(build_dir: Path, *, version: str, pkg_release: str) -> None:
def build_rpm(build_dir: Path, *, version: str, pkg_release: str, variant: str) -> None:
"""Stage the spec and its sources, then build the binary RPMs."""
topdir = build_dir / "rpmbuild"
for name in ("BUILD", "BUILDROOT", "RPMS", "SOURCES", "SPECS", "SRPMS"):
@@ -156,6 +199,9 @@ def build_rpm(build_dir: Path, *, version: str, pkg_release: str) -> None:
stage_common(build_dir, topdir / "SOURCES")
stage_units(topdir / "SOURCES")
# The spec defaults it to nothing, so a plain build is unchanged.
variant_defines = ["--define", f"pkg_variant {variant}"] if variant else []
run(
"rpmbuild",
"-bb",
@@ -168,10 +214,29 @@ def build_rpm(build_dir: Path, *, version: str, pkg_release: str) -> None:
# The image tracks the newest distro, but the packages target el9.
"--define",
"dist .el9",
*variant_defines,
spec,
)
def stage_debian(dest: Path, name: str) -> None:
"""Stage the debian directory for the package name being built."""
source = SRC_DIR / "package" / "debian"
shutil.copytree(
source, dest, ignore=shutil.ignore_patterns("*.in", *DEBIAN_PKG_FILES)
)
values = {
"PKG": name,
"VARIANT_FIELDS": "" if name == BASE_NAME else DEB_VARIANT_FIELDS,
}
render(source / "control.in", dest / "control", values)
render(source / "lintian-overrides.in", dest / f"{name}.lintian-overrides", values)
for suffix in DEBIAN_PKG_FILES:
shutil.copy2(source / suffix, dest / f"{name}.{suffix}")
def build_deb(
build_dir: Path,
*,
@@ -180,21 +245,23 @@ def build_deb(
pkg_release: str,
channel: str,
epoch: int,
name: str,
) -> None:
"""Stage the debian directory and its sources, then build the binary DEBs."""
staging = build_dir / "debbuild" / "source"
stage_common(build_dir, staging)
shutil.copytree(SRC_DIR / "package" / "debian", staging / "debian")
stage_debian(staging / "debian", name)
# debhelper picks these up from debian/ automatically.
stage_units(staging / "debian")
# Prefixed whether it is a variant's name or not: debian/rules names them
# explicitly either way.
stage_units(staging / "debian", prefix=f"{name}.")
date = datetime.fromtimestamp(epoch, timezone.utc).strftime(
"%a, %d %b %Y %H:%M:%S %z"
)
# The leading spaces are significant to dpkg.
changelog = textwrap.dedent(f"""\
xrpld ({version}-{pkg_release}) {channel}; urgency=medium
{name} ({version}-{pkg_release}) {channel}; urgency=medium
* Release {reported}.
-- XRPL Foundation <contact@xrplf.org> {date}
@@ -223,6 +290,14 @@ def main() -> None:
default="1",
help="package release iteration (default: %(default)s)",
)
parser.add_argument(
"--variant",
default="",
choices=VARIANTS,
help="the flavour of the package to build: 'assert' produces "
"xrpld-assert, which ships the same paths as xrpld and replaces it "
"(default: the plain xrpld package)",
)
parser.add_argument(
"--channel",
required=True,
@@ -234,6 +309,8 @@ def main() -> None:
build_dir: Path = args.build_dir.resolve()
pkg_release: str = args.pkg_release
channel: str = args.channel
variant: str = args.variant
name = package_name(variant)
assert build_dir.is_dir(), (
f"build directory not found: {build_dir}. Build the binaries before "
@@ -253,6 +330,8 @@ def main() -> None:
for tree in ("debbuild", "rpmbuild"):
shutil.rmtree(build_dir / tree, ignore_errors=True)
print(f"Building {package_type} {name} {version}-{pkg_release}", flush=True)
if package_type == "deb":
build_deb(
build_dir,
@@ -261,9 +340,10 @@ def main() -> None:
pkg_release=pkg_release,
channel=channel,
epoch=epoch,
name=name,
)
else:
build_rpm(build_dir, version=version, pkg_release=pkg_release)
build_rpm(build_dir, version=version, pkg_release=pkg_release, variant=variant)
if __name__ == "__main__":

View File

@@ -1,4 +1,4 @@
Source: xrpld
Source: @PKG@
Section: net
Priority: optional
Maintainer: XRPL Foundation <contact@xrplf.org>
@@ -11,7 +11,7 @@ Homepage: https://github.com/XRPLF/rippled
Vcs-Git: https://github.com/XRPLF/rippled.git
Vcs-Browser: https://github.com/XRPLF/rippled
Package: xrpld
Package: @PKG@
Architecture: any
Depends:
${shlibs:Depends},
@@ -22,3 +22,4 @@ Description: XRP Ledger daemon
transactions, and maintains the ledger database.
This package also includes the validator-keys tool for validator key
management.
@VARIANT_FIELDS@

View File

@@ -0,0 +1,6 @@
# The /usr/local/bin/rippled symlink is deliberate compatibility for pre-FHS
# layouts, so the Policy 9.1.2 tags it raises are expected.
# TODO: remove alongside debian/links after rippled fully deprecated.
@PKG@: dir-in-usr-local [usr/local/bin/]
@PKG@: file-in-usr-local [usr/local/bin/rippled]
@PKG@: file-in-unusual-dir [usr/local/bin/rippled]

View File

@@ -8,33 +8,58 @@ export DH_VERBOSE = 1
# the binaries actually run on.
LIBC_MIN = 2.31
# The binary package's name, which a variant build changes to e.g. xrpld-assert,
# and the directory debhelper expects its files staged in.
PKG := $(firstword $(shell dh_listpackages))
PKG_DIR = debian/$(PKG)
# The base name, which every package ships under whatever it is called itself.
BASE_NAME = xrpld
# What build_pkg.py stages beside this directory, each installed under its own
# name. The binaries are also the ones checked against LIBC_MIN below.
BINARIES = $(BASE_NAME) validator-keys
CONFIGS = $(BASE_NAME).cfg validators.txt
%:
dh $@
override_dh_auto_configure override_dh_auto_build override_dh_auto_test:
@:
# The unit, sysusers, tmpfiles and logrotate files are named after the daemon
# rather than after the package, so a variant still ships xrpld.service and
# /etc/logrotate.d/xrpld. debhelper only reads debian/$(PKG).$(BASE_NAME).* when told
# the name.
override_dh_installsystemd:
dh_installsystemd --no-stop-on-upgrade xrpld.service
dh_installsystemd --no-stop-on-upgrade --name $(BASE_NAME)
# The tmpfiles snippet sets ownership to the xrpld user, so the sysusers snippet
# has to be emitted first: run it early and make its own sequence slot a no-op.
execute_before_dh_installtmpfiles:
dh_installsysusers
dh_installsysusers --name $(BASE_NAME)
override_dh_installsysusers:
override_dh_installtmpfiles:
dh_installtmpfiles --name $(BASE_NAME)
override_dh_installlogrotate:
dh_installlogrotate --name $(BASE_NAME)
override_dh_install:
install -D -m 0755 xrpld debian/xrpld/usr/bin/xrpld
install -D -m 0755 validator-keys debian/xrpld/usr/bin/validator-keys
install -D -m 0644 xrpld.cfg debian/xrpld/etc/xrpld/xrpld.cfg
install -D -m 0644 validators.txt debian/xrpld/etc/xrpld/validators.txt
for binary in $(BINARIES); do \
install -D -m 0755 "$$binary" "$(PKG_DIR)/usr/bin/$$binary"; \
done
for config in $(CONFIGS); do \
install -D -m 0644 "$$config" "$(PKG_DIR)/etc/$(BASE_NAME)/$$config"; \
done
override_dh_shlibdeps:
dh_shlibdeps
# Guards against the toolchain moving past LIBC_MIN and the packages then
# claiming a floor they do not meet.
for binary in xrpld validator-keys; do \
for binary in $(BINARIES); do \
needed=$$(readelf --dyn-syms --wide $$binary \
| grep -o 'GLIBC_[0-9.]*' | sed 's/GLIBC_//' | sort -uV | tail -1); \
if [ -z "$$needed" ]; then \
@@ -46,7 +71,7 @@ override_dh_shlibdeps:
exit 1; \
fi; \
done
sed -i 's/libc6 (>= [0-9.]*)/libc6 (>= $(LIBC_MIN))/' debian/xrpld.substvars
sed -i 's/libc6 (>= [0-9.]*)/libc6 (>= $(LIBC_MIN))/' debian/$(PKG).substvars
override_dh_dwz:
@:

View File

@@ -1,6 +0,0 @@
# The /usr/local/bin/rippled symlink is deliberate compatibility for pre-FHS
# layouts, so the Policy 9.1.2 tags it raises are expected.
# TODO: remove alongside debian/xrpld.links after rippled fully deprecated.
xrpld: dir-in-usr-local [usr/local/bin/]
xrpld: file-in-usr-local [usr/local/bin/rippled]
xrpld: file-in-unusual-dir [usr/local/bin/rippled]

View File

@@ -6,10 +6,14 @@
%{error:pkg_release must be defined}
%endif
Name: xrpld
# The base name, which every package ships under. A variant build
# (build_pkg.py --variant) only suffixes the package name, e.g. xrpld-assert.
%global base_name xrpld
Name: %{base_name}%{?pkg_variant:-%{pkg_variant}}
Version: %{pkg_version}
Release: %{pkg_release}%{?dist}
Summary: XRP Ledger daemon
Summary: XRP Ledger daemon%{?pkg_variant: (%{pkg_variant} build)}
License: ISC
URL: https://github.com/XRPLF/rippled
@@ -17,6 +21,12 @@ URL: https://github.com/XRPLF/rippled
ExclusiveArch: x86_64 aarch64
BuildRequires: systemd-rpm-macros
# A variant owns the same paths, so it stands in for the plain package.
%if "%{?pkg_variant}" != ""
Conflicts: %{base_name}
Provides: %{base_name} = %{version}-%{release}
%endif
# These have to precede %%debug_package: it opens the debuginfo subpackage, and
# any tag after it is silently dropped from the main package.
%{?systemd_requires}
@@ -52,22 +62,22 @@ management.
:
%install
install -Dm0755 %{_sourcedir}/xrpld %{buildroot}%{_bindir}/%{name}
install -Dm0755 %{_sourcedir}/xrpld %{buildroot}%{_bindir}/%{base_name}
install -Dm0755 %{_sourcedir}/validator-keys %{buildroot}%{_bindir}/validator-keys
install -Dm0644 %{_sourcedir}/xrpld.cfg %{buildroot}%{_sysconfdir}/%{name}/xrpld.cfg
install -Dm0644 %{_sourcedir}/validators.txt %{buildroot}%{_sysconfdir}/%{name}/validators.txt
install -Dm0644 %{_sourcedir}/xrpld.cfg %{buildroot}%{_sysconfdir}/%{base_name}/xrpld.cfg
install -Dm0644 %{_sourcedir}/validators.txt %{buildroot}%{_sysconfdir}/%{base_name}/validators.txt
# systemd units, sysusers, tmpfiles, preset
install -Dm0644 %{_sourcedir}/xrpld.service %{buildroot}%{_unitdir}/xrpld.service
install -Dm0644 %{_sourcedir}/xrpld.sysusers %{buildroot}%{_sysusersdir}/xrpld.conf
install -Dm0644 %{_sourcedir}/xrpld.tmpfiles %{buildroot}%{_tmpfilesdir}/xrpld.conf
install -d %{buildroot}%{_presetdir}
cat >%{buildroot}%{_presetdir}/50-xrpld.preset <<'EOF'
cat >%{buildroot}%{_presetdir}/50-%{base_name}.preset <<'EOF'
enable xrpld.service
EOF
# Logrotate config
install -Dm0644 %{_sourcedir}/xrpld.logrotate %{buildroot}%{_sysconfdir}/logrotate.d/%{name}
install -Dm0644 %{_sourcedir}/xrpld.logrotate %{buildroot}%{_sysconfdir}/logrotate.d/%{base_name}
# Docs
install -Dm0644 %{_sourcedir}/LICENSE.md %{buildroot}%{_docdir}/%{name}/LICENSE.md
@@ -78,13 +88,13 @@ install -Dm0644 %{_sourcedir}/validator-keys-LICENSE %{buildroot}%{_docdir}/%{na
# Legacy compatibility for pre-FHS package layouts.
# TODO: remove after rippled fully deprecated.
install -d %{buildroot}/usr/local/bin
ln -s %{_bindir}/%{name} %{buildroot}/usr/local/bin/rippled
ln -s %{_bindir}/%{base_name} %{buildroot}/usr/local/bin/rippled
%pre
%sysusers_create_package %{name} %{_sourcedir}/xrpld.sysusers
%sysusers_create_package %{base_name} %{_sourcedir}/xrpld.sysusers
%post
%tmpfiles_create_package %{name} %{_sourcedir}/xrpld.tmpfiles
%tmpfiles_create_package %{base_name} %{_sourcedir}/xrpld.tmpfiles
%systemd_post xrpld.service
%preun
@@ -92,6 +102,13 @@ ln -s %{_bindir}/%{name} %{buildroot}/usr/local/bin/rippled
%postun
%systemd_postun xrpld.service
# A flavour swap installs the replacement before erasing this package, so the
# %%preun above has just disabled a unit the replacement still owns. rpm keeps a
# file that another installed package owns, so the unit outliving our own erase
# means exactly that; a plain erase takes it with us and re-presets nothing.
if [ $1 -eq 0 ] && [ -f %{_unitdir}/xrpld.service ]; then
systemctl preset xrpld.service >/dev/null 2>&1 || :
fi
%files
%attr(0755,root,root) %dir %{_docdir}/%{name}
@@ -99,18 +116,18 @@ ln -s %{_bindir}/%{name} %{buildroot}/usr/local/bin/rippled
%license %{_docdir}/%{name}/validator-keys-LICENSE
%doc %{_docdir}/%{name}/README.md
%attr(0755,root,root) %dir %{_sysconfdir}/%{name}
%attr(0755,root,root) %dir %{_sysconfdir}/%{base_name}
%{_bindir}/%{name}
%{_bindir}/%{base_name}
%{_bindir}/validator-keys
%config(noreplace) %{_sysconfdir}/%{name}/xrpld.cfg
%config(noreplace) %{_sysconfdir}/%{name}/validators.txt
%config(noreplace) %{_sysconfdir}/logrotate.d/%{name}
%config(noreplace) %{_sysconfdir}/%{base_name}/xrpld.cfg
%config(noreplace) %{_sysconfdir}/%{base_name}/validators.txt
%config(noreplace) %{_sysconfdir}/logrotate.d/%{base_name}
%{_unitdir}/xrpld.service
%attr(0644,root,root) %{_presetdir}/50-xrpld.preset
%attr(0644,root,root) %{_presetdir}/50-%{base_name}.preset
%{_sysusersdir}/xrpld.conf
%{_tmpfilesdir}/xrpld.conf
%ghost %dir /var/lib/xrpld