Compare commits

..

19 Commits

Author SHA1 Message Date
Michael Legleux
dc02510d03 package: refactor packaging; make builds reproducible; stop requiring git 2026-04-23 12:57:22 -07:00
Michael Legleux
3bdd891233 fix indentation 2026-04-23 12:57:22 -07:00
Michael Legleux
8ddd5de1ef rm rpm template; try to get version from binary 2026-04-23 12:57:22 -07:00
Michael Legleux
5ebe3edd46 f 2026-04-23 12:57:22 -07:00
Michael Legleux
ed77410100 update matrix to build packages 2026-04-23 12:57:21 -07:00
Michael Legleux
eb9da2b331 add a little more info to the result of the update check 2026-04-23 12:57:21 -07:00
Michael Legleux
f9789fa4df Migrate updater from cron to systemd timer; use journald + flock
- systemd oneshot service + timer replaces cron job
- randomized scheduling and persistent timers
- journald logging with SyslogIdentifier
- flock-based concurrency control
- remove legacy update.log file
2026-04-23 12:57:21 -07:00
Michael Legleux
d216ab4862 guard more 2026-04-23 12:57:21 -07:00
Michael Legleux
0f32bf8948 Pin packaging image versions 2026-04-23 12:57:21 -07:00
Michael Legleux
2ec02762ff pr comments 2026-04-23 12:57:20 -07:00
Michael Legleux
40e70a7bac PR comments and more tweaks 2026-04-23 12:57:20 -07:00
Michael Legleux
deba938eef pr comments 2026-04-23 12:57:20 -07:00
Michael Legleux
b2b04afd5f more tweaks 2026-04-23 12:57:20 -07:00
Michael Legleux
9314a5c5c1 cp lic/readme 2026-04-23 12:57:20 -07:00
Michael Legleux
5e93d57669 more fixes/updates 2026-04-23 12:57:20 -07:00
Michael Legleux
99ff7cdd53 update download-artifacts version 2026-04-23 12:57:19 -07:00
Michael Legleux
4c38067fda handle tmpfiles in rpm 2026-04-23 12:57:19 -07:00
Michael Legleux
9cbb7953e9 address review comments 2026-04-23 12:57:19 -07:00
Michael Legleux
88fbf718da feat: Build Linux packages in GitHub 2026-04-23 12:57:19 -07:00
35 changed files with 1326 additions and 69 deletions

View File

@@ -32,6 +32,52 @@ We will further set additional CMake arguments as follows:
"""
def build_config_name(os_entry: dict, architecture: dict, build_type: str) -> str:
name = os_entry["distro_name"]
if (n := os_entry["distro_version"]) != "":
name += f"-{n}"
if (n := os_entry["compiler_name"]) != "":
name += f"-{n}"
if (n := os_entry["compiler_version"]) != "":
name += f"-{n}"
platform = architecture["platform"]
name += f"-{platform[platform.find('/') + 1:]}"
name += f"-{build_type.lower()}"
return name
def build_container_image(os_entry: dict) -> str:
return (
f"ghcr.io/xrplf/ci/{os_entry['distro_name']}-{os_entry['distro_version']}:"
f"{os_entry['compiler_name']}-{os_entry['compiler_version']}-sha-{os_entry['image_sha']}"
)
def generate_packaging_matrix(config: Config) -> list:
"""Emit packaging entries for each os entry with a 'packaging' field.
Packaging always uses Release build on linux/amd64.
"""
architecture = next(
(a for a in config.architecture if a["platform"] == "linux/amd64"),
None,
)
if architecture is None:
raise Exception("linux/amd64 architecture required for packaging")
entries = []
for os_entry in config.os:
for pkg_type in os_entry.get("packaging", []):
config_name = build_config_name(os_entry, architecture, "Release")
entries.append(
{
"pkg_type": pkg_type,
"artifact_name": f"xrpld-{config_name}",
"container_image": build_container_image(os_entry),
}
)
return entries
def generate_strategy_matrix(all: bool, config: Config) -> list:
configurations = []
for architecture, os, build_type, cmake_args in itertools.product(
@@ -99,14 +145,15 @@ def generate_strategy_matrix(all: bool, config: Config) -> list:
continue
# RHEL:
# - 9 using GCC 12: Debug on linux/amd64.
# - 9 using GCC 12: Debug and Release on linux/amd64
# (Release is required for RPM packaging).
# - 10 using Clang: Release on linux/amd64.
if os["distro_name"] == "rhel":
skip = True
if os["distro_version"] == "9":
if (
f"{os['compiler_name']}-{os['compiler_version']}" == "gcc-12"
and build_type == "Debug"
and build_type in ["Debug", "Release"]
and architecture["platform"] == "linux/amd64"
):
skip = False
@@ -121,7 +168,8 @@ def generate_strategy_matrix(all: bool, config: Config) -> list:
continue
# Ubuntu:
# - Jammy using GCC 12: Debug on linux/arm64.
# - Jammy using GCC 12: Debug on linux/arm64, Release on
# linux/amd64 (Release is required for DEB packaging).
# - Noble using GCC 14: Release on linux/amd64.
# - Noble using Clang 18: Debug on linux/amd64.
# - Noble using Clang 19: Release on linux/arm64.
@@ -134,6 +182,12 @@ def generate_strategy_matrix(all: bool, config: Config) -> list:
and architecture["platform"] == "linux/arm64"
):
skip = False
if (
f"{os['compiler_name']}-{os['compiler_version']}" == "gcc-12"
and build_type == "Release"
and architecture["platform"] == "linux/amd64"
):
skip = False
elif os["distro_version"] == "noble":
if (
f"{os['compiler_name']}-{os['compiler_version']}" == "gcc-14"
@@ -215,17 +269,7 @@ def generate_strategy_matrix(all: bool, config: Config) -> list:
# Generate a unique name for the configuration, e.g. macos-arm64-debug
# or debian-bookworm-gcc-12-amd64-release.
config_name = os["distro_name"]
if (n := os["distro_version"]) != "":
config_name += f"-{n}"
if (n := os["compiler_name"]) != "":
config_name += f"-{n}"
if (n := os["compiler_version"]) != "":
config_name += f"-{n}"
config_name += (
f"-{architecture['platform'][architecture['platform'].find('/')+1:]}"
)
config_name += f"-{build_type.lower()}"
config_name = build_config_name(os, architecture, build_type)
if "-Dcoverage=ON" in cmake_args:
config_name += "-coverage"
if "-Dunity=ON" in cmake_args:
@@ -313,10 +357,19 @@ if __name__ == "__main__":
required=False,
type=Path,
)
parser.add_argument(
"-p",
"--packaging",
help="Emit the packaging matrix (derived from the 'packaging' field on os entries) instead of the build/test matrix.",
action="store_true",
)
args = parser.parse_args()
matrix = []
if args.config is None or args.config == "":
if args.packaging:
config_path = args.config if args.config else THIS_DIR / "linux.json"
matrix += generate_packaging_matrix(read_config(config_path))
elif args.config is None or args.config == "":
matrix += generate_strategy_matrix(
args.all, read_config(THIS_DIR / "linux.json")
)

View File

@@ -120,7 +120,8 @@
"distro_version": "9",
"compiler_name": "gcc",
"compiler_version": "12",
"image_sha": "ab4d1f0"
"image_sha": "ab4d1f0",
"packaging": ["rpm"]
},
{
"distro_name": "rhel",
@@ -162,7 +163,8 @@
"distro_version": "jammy",
"compiler_name": "gcc",
"compiler_version": "12",
"image_sha": "ab4d1f0"
"image_sha": "ab4d1f0",
"packaging": ["deb"]
},
{
"distro_name": "ubuntu",

View File

@@ -67,6 +67,7 @@ jobs:
.github/workflows/reusable-build-test.yml
.github/workflows/reusable-clang-tidy.yml
.github/workflows/reusable-clang-tidy-files.yml
.github/workflows/reusable-package.yml
.github/workflows/reusable-strategy-matrix.yml
.github/workflows/reusable-test.yml
.github/workflows/reusable-upload-recipe.yml
@@ -81,6 +82,8 @@ jobs:
CMakeLists.txt
conanfile.py
conan.lock
package/**
- name: Check whether to run
# This step determines whether the rest of the workflow should
# run. The rest of the workflow will run if this job runs AND at
@@ -137,6 +140,42 @@ jobs:
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
generate-version:
needs: should-run
if: ${{ needs.should-run.outputs.go == 'true' }}
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
sparse-checkout: |
.github/actions/generate-version
src/libxrpl/protocol/BuildInfo.cpp
- name: Generate version
id: version
uses: ./.github/actions/generate-version
generate-packaging-matrix:
needs: should-run
if: ${{ needs.should-run.outputs.go == 'true' }}
uses: ./.github/workflows/reusable-strategy-matrix.yml
with:
mode: packaging
package:
needs: [should-run, build-test, generate-version, generate-packaging-matrix]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.generate-packaging-matrix.outputs.matrix) }}
uses: ./.github/workflows/reusable-package.yml
with:
pkg_type: ${{ matrix.pkg_type }}
artifact_name: ${{ matrix.artifact_name }}
version: ${{ needs.generate-version.outputs.version }}
container_image: ${{ matrix.container_image }}
upload-recipe:
needs:
- should-run

View File

@@ -1,5 +1,5 @@
# This workflow uploads the libxrpl recipe to the Conan remote when a versioned
# tag is pushed.
# This workflow uploads the libxrpl recipe to the Conan remote and builds
# release packages when a versioned tag is pushed.
name: Tag
on:
@@ -22,3 +22,50 @@ jobs:
secrets:
remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }}
build-test:
if: ${{ github.repository == 'XRPLF/rippled' }}
uses: ./.github/workflows/reusable-build-test.yml
strategy:
fail-fast: true
matrix:
os: [linux]
with:
ccache_enabled: false
os: ${{ matrix.os }}
strategy_matrix: minimal
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
generate-version:
if: ${{ github.repository == 'XRPLF/rippled' }}
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
sparse-checkout: |
.github/actions/generate-version
src/libxrpl/protocol/BuildInfo.cpp
- name: Generate version
id: version
uses: ./.github/actions/generate-version
generate-packaging-matrix:
uses: ./.github/workflows/reusable-strategy-matrix.yml
with:
mode: packaging
package:
needs: [build-test, generate-version, generate-packaging-matrix]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.generate-packaging-matrix.outputs.matrix) }}
uses: ./.github/workflows/reusable-package.yml
with:
pkg_type: ${{ matrix.pkg_type }}
artifact_name: ${{ matrix.artifact_name }}
version: ${{ needs.generate-version.outputs.version }}
container_image: ${{ matrix.container_image }}

View File

@@ -24,6 +24,7 @@ on:
- ".github/workflows/reusable-build-test.yml"
- ".github/workflows/reusable-clang-tidy.yml"
- ".github/workflows/reusable-clang-tidy-files.yml"
- ".github/workflows/reusable-package.yml"
- ".github/workflows/reusable-strategy-matrix.yml"
- ".github/workflows/reusable-test.yml"
- ".github/workflows/reusable-upload-recipe.yml"
@@ -38,6 +39,7 @@ on:
- "CMakeLists.txt"
- "conanfile.py"
- "conan.lock"
- "package/**"
# Run at 06:32 UTC on every day of the week from Monday through Friday. This
# will force all dependencies to be rebuilt, which is useful to verify that
@@ -98,3 +100,35 @@ jobs:
secrets:
remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }}
generate-version:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
sparse-checkout: |
.github/actions/generate-version
src/libxrpl/protocol/BuildInfo.cpp
- name: Generate version
id: version
uses: ./.github/actions/generate-version
generate-packaging-matrix:
uses: ./.github/workflows/reusable-strategy-matrix.yml
with:
mode: packaging
package:
needs: [build-test, generate-version, generate-packaging-matrix]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.generate-packaging-matrix.outputs.matrix) }}
uses: ./.github/workflows/reusable-package.yml
with:
pkg_type: ${{ matrix.pkg_type }}
artifact_name: ${{ matrix.artifact_name }}
version: ${{ needs.generate-version.outputs.version }}
container_image: ${{ matrix.container_image }}

79
.github/workflows/reusable-package.yml vendored Normal file
View File

@@ -0,0 +1,79 @@
# Build a Linux package (DEB or RPM) from a pre-built binary artifact.
name: Package
on:
workflow_call:
inputs:
artifact_name:
description: "Name of the GitHub pre-built binary artifact to download (e.g. xrpld-ubuntu-jammy-gcc-12-amd64-release)."
required: true
type: string
container_image:
description: "Container image to use for packaging."
required: true
type: string
pkg_release:
description: "Package release number. Increment when repackaging the same executable."
required: false
type: string
default: "1"
pkg_type:
description: "Package type to build: deb or rpm."
required: true
type: string
version:
description: "Version string used for naming the output artifact."
required: true
type: string
defaults:
run:
shell: bash
env:
BUILD_DIR: build
jobs:
package:
name: "${{ inputs.pkg_type }} (${{ inputs.version }})"
permissions:
contents: read
runs-on: ["self-hosted", "Linux", "X64", "heavy"]
container: ${{ inputs.container_image }}
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set source date epoch
run: |
echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct "$GITHUB_SHA")" >> "$GITHUB_ENV"
- name: Download pre-built binary
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ inputs.artifact_name }}
path: ${{ env.BUILD_DIR }}
- name: Make binary executable
run: chmod +x "${BUILD_DIR}/xrpld"
- name: Build package
env:
PKG_TYPE: ${{ inputs.pkg_type }}
PKG_VERSION: ${{ inputs.version }}
PKG_RELEASE: ${{ inputs.pkg_release }}
SOURCE_DATE_EPOCH: ${{ env.SOURCE_DATE_EPOCH }}
run: |
./package/build_pkg.sh "$PKG_TYPE" . "$BUILD_DIR" "$PKG_VERSION" "$PKG_RELEASE"
- name: Upload package artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: xrpld-${{ inputs.pkg_type }}-${{ inputs.version }}
path: |
${{ env.BUILD_DIR }}/debbuild/*.deb
${{ env.BUILD_DIR }}/debbuild/*.ddeb
${{ env.BUILD_DIR }}/rpmbuild/RPMS/**/*.rpm
if-no-files-found: error

View File

@@ -3,6 +3,11 @@ name: Generate strategy matrix
on:
workflow_call:
inputs:
mode:
description: 'What matrix to emit: "build-test" (default) or "packaging".'
required: false
type: string
default: "build-test"
os:
description: 'The operating system to use for the build ("linux", "macos", "windows").'
required: false
@@ -41,5 +46,11 @@ jobs:
id: generate
env:
GENERATE_CONFIG: ${{ inputs.os != '' && format('--config={0}.json', inputs.os) || '' }}
GENERATE_MODE: ${{ inputs.mode == 'packaging' && '--packaging --config=linux.json' || '' }}
GENERATE_OPTION: ${{ inputs.strategy_matrix == 'all' && '--all' || '' }}
run: ./generate.py ${GENERATE_OPTION} ${GENERATE_CONFIG} >> "${GITHUB_OUTPUT}"
run: |
if [ -n "${GENERATE_MODE}" ]; then
./generate.py ${GENERATE_MODE} >> "${GITHUB_OUTPUT}"
else
./generate.py ${GENERATE_OPTION} ${GENERATE_CONFIG} >> "${GITHUB_OUTPUT}"
fi

View File

@@ -134,6 +134,7 @@ endif()
include(XrplCore)
include(XrplProtocolAutogen)
include(XrplInstall)
include(XrplPackaging)
include(XrplValidatorKeys)
if(tests)

View File

@@ -12,14 +12,14 @@ if(is_root_project AND TARGET xrpld)
install(
FILES "${CMAKE_CURRENT_SOURCE_DIR}/cfg/xrpld-example.cfg"
DESTINATION "${CMAKE_INSTALL_SYSCONFDIR}/xrpld"
DESTINATION "${CMAKE_INSTALL_SYSCONFDIR}"
RENAME xrpld.cfg
COMPONENT runtime
)
install(
FILES "${CMAKE_CURRENT_SOURCE_DIR}/cfg/validators-example.txt"
DESTINATION "${CMAKE_INSTALL_SYSCONFDIR}/xrpld"
DESTINATION "${CMAKE_INSTALL_SYSCONFDIR}"
RENAME validators.txt
COMPONENT runtime
)

144
cmake/XrplPackaging.cmake Normal file
View File

@@ -0,0 +1,144 @@
#[===================================================================[
Linux packaging support: RPM and Debian targets + install tests
#]===================================================================]
if(NOT CMAKE_INSTALL_PREFIX STREQUAL "/opt/xrpld")
message(
STATUS
"Packaging targets require -DCMAKE_INSTALL_PREFIX=/opt/xrpld "
"(current: '${CMAKE_INSTALL_PREFIX}'); skipping."
)
return()
endif()
if(NOT DEFINED pkg_release)
set(pkg_release 1)
endif()
find_program(RPMBUILD_EXECUTABLE rpmbuild)
if(RPMBUILD_EXECUTABLE)
add_custom_target(
package-rpm
COMMAND
${CMAKE_SOURCE_DIR}/package/build_pkg.sh rpm ${CMAKE_SOURCE_DIR}
${CMAKE_BINARY_DIR} "${xrpld_version}" ${pkg_release}
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
DEPENDS xrpld
COMMENT "Building RPM package"
VERBATIM
)
else()
message(STATUS "rpmbuild not found; 'package-rpm' target not available")
endif()
find_program(DPKG_BUILDPACKAGE_EXECUTABLE dpkg-buildpackage)
if(DPKG_BUILDPACKAGE_EXECUTABLE)
add_custom_target(
package-deb
COMMAND
${CMAKE_SOURCE_DIR}/package/build_pkg.sh deb ${CMAKE_SOURCE_DIR}
${CMAKE_BINARY_DIR} "${xrpld_version}" ${pkg_release}
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
DEPENDS xrpld
COMMENT "Building Debian package"
VERBATIM
)
else()
message(
STATUS
"dpkg-buildpackage not found; 'package-deb' target not available"
)
endif()
#[===================================================================[
CTest fixtures for package install verification (requires docker)
#]===================================================================]
find_program(DOCKER_EXECUTABLE docker)
if(NOT DOCKER_EXECUTABLE)
message(STATUS "docker not found; package install tests not available")
return()
endif()
set(DEB_TEST_IMAGE
"geerlingguy/docker-ubuntu2204-ansible@sha256:bbe4c56c16c57c902554b9a47833590926b7a7d4440aef3d9851473b9f7be9d4"
)
set(RPM_TEST_IMAGE
"geerlingguy/docker-rockylinux9-ansible@sha256:790c2db9add93c0daa903ace816f352c9c04abb046ecfa12c581e8d4c59f41d6"
)
foreach(PKG deb rpm)
if(PKG STREQUAL "deb")
set(IMAGE ${DEB_TEST_IMAGE})
else()
set(IMAGE ${RPM_TEST_IMAGE})
endif()
# This image runs systemd for full testing xrpld.service
add_test(
NAME ${PKG}_container_start
COMMAND
sh -c
"docker rm -f xrpld_${PKG}_install_test 2>/dev/null || true && \
docker run -d \
--name xrpld_${PKG}_install_test \
--cgroupns host \
--volume '${CMAKE_SOURCE_DIR}:/root:ro' \
--volume /sys/fs/cgroup:/sys/fs/cgroup:rw \
--tmpfs /tmp \
--tmpfs /run \
--tmpfs /run/lock \
${IMAGE} \
/usr/sbin/init"
)
set_tests_properties(
${PKG}_container_start
PROPERTIES FIXTURES_SETUP ${PKG}_container LABELS packaging
)
# On CI: always stop. Locally: leave running on failure for diagnosis.
add_test(
NAME ${PKG}_container_stop
COMMAND
sh -c
"if [ -n \"$CI\" ] || ! docker exec xrpld_${PKG}_install_test test -f /tmp/test_failed 2>/dev/null; then \
docker rm -f xrpld_${PKG}_install_test; \
else \
echo 'Tests failed — leaving xrpld_${PKG}_install_test running for diagnosis'; \
echo 'Clean up with: docker rm -f xrpld_${PKG}_install_test'; \
fi"
)
set_tests_properties(
${PKG}_container_stop
PROPERTIES FIXTURES_CLEANUP ${PKG}_container LABELS packaging
)
add_test(
NAME ${PKG}_install
COMMAND
docker exec -w /root xrpld_${PKG}_install_test bash
/root/package/test/smoketest.sh local
)
set_tests_properties(
${PKG}_install
PROPERTIES
FIXTURES_REQUIRED ${PKG}_container
FIXTURES_SETUP ${PKG}_installed
LABELS packaging
TIMEOUT 600
)
add_test(
NAME ${PKG}_install_paths
COMMAND
docker exec -w /root xrpld_${PKG}_install_test sh
/root/package/test/check_install_paths.sh
)
set_tests_properties(
${PKG}_install_paths
PROPERTIES
FIXTURES_REQUIRED "${PKG}_container;${PKG}_installed"
LABELS packaging
TIMEOUT 60
)
endforeach()

View File

@@ -97,12 +97,15 @@ words:
- desync
- desynced
- determ
- disablerepo
- distro
- doxyfile
- dxrpl
- enabled
- enablerepo
- endmacro
- exceptioned
- EXPECT_STREQ
- Falco
- fcontext
- finalizers
@@ -158,6 +161,7 @@ words:
- Merkle
- Metafuncton
- misprediction
- missingok
- mptbalance
- MPTDEX
- mptflags
@@ -189,7 +193,9 @@ words:
- NOLINT
- NOLINTNEXTLINE
- nonxrp
- noreplace
- noripple
- notifempty
- nudb
- nullptr
- nunl
@@ -209,6 +215,7 @@ words:
- preauthorize
- preauthorizes
- preclaim
- preun
- protobuf
- protos
- ptrs
@@ -243,12 +250,14 @@ words:
- sfields
- shamap
- shamapitem
- shlibs
- sidechain
- SIGGOOD
- sle
- sles
- soci
- socidb
- SRPMS
- sslws
- statsd
- STATSDCOLLECTOR
@@ -276,8 +285,8 @@ words:
- txn
- txns
- txs
- UBSAN
- ubsan
- UBSAN
- umant
- unacquired
- unambiguity
@@ -314,7 +323,6 @@ words:
- xbridge
- xchain
- ximinez
- EXPECT_STREQ
- XMACRO
- xrpkuwait
- xrpl

158
package/README.md Normal file
View File

@@ -0,0 +1,158 @@
# Linux Packaging
This directory contains all files needed to build RPM and Debian packages for `xrpld`.
## Directory layout
```
package/
build_pkg.sh Staging and build script (called by CMake targets and CI)
rpm/
xrpld.spec RPM spec (xrpld_version/pkg_release passed via rpmbuild --define)
deb/
debian/ Debian control files (control, rules, install, links, conffiles, ...)
shared/
xrpld.service systemd unit file (used by both RPM and DEB)
xrpld.sysusers sysusers.d config (used by both RPM and DEB)
xrpld.tmpfiles tmpfiles.d config (used by both RPM and DEB)
xrpld.logrotate logrotate config (installed to /opt/xrpld/bin/, user activates)
update-xrpld.sh auto-update script (installed to /opt/xrpld/bin/)
update-xrpld-cron cron entry for auto-update (installed to /opt/xrpld/bin/)
test/
smoketest.sh Package install smoke test
check_install_paths.sh Verify install paths and compat symlinks
```
## Prerequisites
Packaging targets and their container images are declared in
[`.github/scripts/strategy-matrix/linux.json`](../.github/scripts/strategy-matrix/linux.json)
via a `"packaging"` field on specific os entries. The image tag is composed
as `ghcr.io/xrplf/ci/{distro}-{version}:{compiler}-{cver}-sha-{image_sha}`
the same scheme used by `reusable-build-test.yml`. Bump `image_sha` in
`linux.json` and both CI and local builds pick up the new image with no
workflow edits.
| Package type | Image (derived from `linux.json`) | Tool required |
| ------------ | ---------------------------------------------------- | --------------------------------------------------------------- |
| RPM | `ghcr.io/xrplf/ci/rhel-9:gcc-12-sha-<git_sha>` | `rpmbuild` |
| DEB | `ghcr.io/xrplf/ci/ubuntu-jammy:gcc-12-sha-<git_sha>` | `dpkg-buildpackage`, `debhelper (>= 13)`, `dh-sequence-systemd` |
To print the exact image tags for the current `linux.json`:
```bash
./.github/scripts/strategy-matrix/generate.py --packaging --config=.github/scripts/strategy-matrix/linux.json
```
## Building packages
### Via CI
Caller workflows (`on-pr.yml`, `on-tag.yml`, `on-trigger.yml`) call
`reusable-strategy-matrix.yml` with `mode: packaging` to generate the matrix of
`{pkg_type, artifact_name, container_image}` entries, then fan out to
`reusable-package.yml` per entry. That workflow downloads the pre-built `xrpld`
binary artifact and calls `build_pkg.sh` directly — no CMake configure or
build step is needed inside the packaging job.
### Locally (mirrors CI)
With an `xrpld` binary already built at `build/xrpld`, 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.
```bash
# From the repo root.
PKG_TYPE=deb # or rpm
VERSION=2.4.0-local
PKG_RELEASE=1
# Derive the correct image for this package type from linux.json.
IMAGE=$(jq -r --arg pkg "$PKG_TYPE" '
.os[] | select((.packaging // []) | index($pkg)) |
"ghcr.io/xrplf/ci/\(.distro_name)-\(.distro_version):\(.compiler_name)-\(.compiler_version)-sha-\(.image_sha)"
' .github/scripts/strategy-matrix/linux.json)
# Run the packaging in the container.
docker run --rm \
-v "$(pwd):/src" \
-w /src \
"$IMAGE" \
./package/build_pkg.sh "$PKG_TYPE" . build "$VERSION" "$PKG_RELEASE"
# Output:
# build/debbuild/*.deb (DEB + dbgsym .ddeb)
# build/rpmbuild/RPMS/x86_64/*.rpm
```
### Via CMake (host-side target)
If you run CMake configure on a host that has `rpmbuild` or `dpkg-buildpackage`
installed natively, you can use the CMake targets directly — no container
needed, but the host toolchain replaces the pinned CI image:
```bash
cmake \
-DCMAKE_INSTALL_PREFIX=/opt/xrpld \
-Dxrpld=ON \
-Dxrpld_version=2.4.0-local \
-Dtests=OFF \
..
cmake --build . --target package-rpm # requires rpmbuild
cmake --build . --target package-deb # requires dpkg-buildpackage
```
The `cmake/XrplPackaging.cmake` module gates each target on whether the
required tool is present at configure time, so configuring on a host that
lacks one simply omits the corresponding target. `CMAKE_INSTALL_PREFIX` must
be `/opt/xrpld`; if it is not, both targets are skipped with a `STATUS`
message.
## How `build_pkg.sh` works
`build_pkg.sh <pkg_type> <src_dir> <build_dir> [version] [pkg_release]` stages
all files and invokes the platform build tool. It resolves `src_dir` and
`build_dir` to absolute paths, then calls `stage_common()` to copy the binary,
config files, and shared support files into the staging area.
### RPM
1. Creates the standard `rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}` tree inside the build directory.
2. Copies `xrpld.spec` and all source files (binary, configs, service files) into `SOURCES/`.
3. Runs `rpmbuild -bb --define "xrpld_version ..." --define "pkg_release ..."`. The spec uses manual `install` commands to place files.
4. Output: `rpmbuild/RPMS/x86_64/xrpld-*.rpm`
### DEB
1. Creates a staging source tree at `debbuild/source/` inside the build directory.
2. Stages the binary, configs, `README.md`, and `LICENSE.md`.
3. Copies `package/deb/debian/` control files into `debbuild/source/debian/`.
4. Copies shared service/sysusers/tmpfiles into `debian/` where `dh_installsystemd`, `dh_installsysusers`, and `dh_installtmpfiles` pick them up automatically.
5. Generates a minimal `debian/changelog` (pre-release versions use `~` instead of `-`).
6. Runs `dpkg-buildpackage -b --no-sign`. `debian/rules` uses manual `install` commands.
7. Output: `debbuild/*.deb` and `debbuild/*.ddeb` (dbgsym package)
## Post-build verification
```bash
# DEB
dpkg-deb -c debbuild/*.deb | grep -E 'systemd|sysusers|tmpfiles'
lintian -I debbuild/*.deb
# RPM
rpm -qlp rpmbuild/RPMS/x86_64/*.rpm
```
## Reproducibility
The following environment variables improve build reproducibility. They are not
set automatically by `build_pkg.sh`; set them manually if needed:
```bash
export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
export TZ=UTC
export LC_ALL=C.UTF-8
export GZIP=-n
export DEB_BUILD_OPTIONS="noautodbgsym reproducible=+fixfilepath"
```

129
package/build_pkg.sh Executable file
View File

@@ -0,0 +1,129 @@
#!/usr/bin/env bash
set -euo pipefail
# Build an RPM or Debian package from a pre-built xrpld binary.
#
# Usage: build_pkg.sh <pkg_type> <src_dir> <build_dir> [version] [pkg_release]
# pkg_type : rpm | deb
# src_dir : path to repository root
# build_dir : directory containing the pre-built xrpld binary
# version : package version string (e.g. 3.2.0-b1)
# pkg_release : package release number (default: 1)
PKG_TYPE="${1:?pkg_type required}"
SRC_DIR="$(cd "${2:?src_dir required}" && pwd)"
BUILD_DIR="$(cd "${3:?build_dir required}" && pwd)"
VERSION="${4:-$("${BUILD_DIR}/xrpld" --version | awk 'NR==1 {print $3}')}"
PKG_RELEASE="${5:-1}"
if [[ -z "${SOURCE_DATE_EPOCH:-}" ]]; then
if git -C "$SRC_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
SOURCE_DATE_EPOCH="$(git -C "$SRC_DIR" log -1 --format=%ct)"
else
SOURCE_DATE_EPOCH="$(date +%s)"
fi
fi
export SOURCE_DATE_EPOCH
CHANGELOG_DATE="$(date -u -R -d "@$SOURCE_DATE_EPOCH")"
# Split VERSION at the first '-' into base and optional pre-release suffix.
# Examples: "3.2.0" -> ("3.2.0", ""); "3.2.0-b1" -> ("3.2.0", "b1").
VER_BASE="${VERSION%%-*}"
VER_SUFFIX="${VERSION#*-}"
[[ "${VER_SUFFIX}" == "${VERSION}" ]] && VER_SUFFIX=""
SHARED="${SRC_DIR}/package/shared"
DEBIAN_DIR="${SRC_DIR}/package/debian"
# Stage files that both packaging systems consume using the same filenames.
stage_common() {
local dest="$1"
mkdir -p "${dest}"
cp "${BUILD_DIR}/xrpld" "${dest}/xrpld"
cp "${SRC_DIR}/cfg/xrpld-example.cfg" "${dest}/xrpld.cfg"
cp "${SRC_DIR}/cfg/validators-example.txt" "${dest}/validators.txt"
cp "${SRC_DIR}/LICENSE.md" "${dest}/LICENSE.md"
cp "${SRC_DIR}/README.md" "${dest}/README.md"
cp "${SHARED}/xrpld.service" "${dest}/xrpld.service"
cp "${SHARED}/xrpld.sysusers" "${dest}/xrpld.sysusers"
cp "${SHARED}/xrpld.tmpfiles" "${dest}/xrpld.tmpfiles"
cp "${SHARED}/xrpld.logrotate" "${dest}/xrpld.logrotate"
cp "${SHARED}/update-xrpld.sh" "${dest}/update-xrpld.sh"
cp "${SHARED}/update-xrpld-cron" "${dest}/update-xrpld-cron"
cp "${SHARED}/update-xrpld.service" "${dest}/update-xrpld.service"
cp "${SHARED}/update-xrpld.timer" "${dest}/update-xrpld.timer"
cp "${SHARED}/50-xrpld.preset" "${dest}/50-xrpld.preset"
}
build_rpm() {
local topdir="${BUILD_DIR}/rpmbuild"
rm -rf "${topdir}"
mkdir -p "${topdir}"/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
cp "${SRC_DIR}/package/rpm/xrpld.spec" "${topdir}/SPECS/xrpld.spec"
stage_common "${topdir}/SOURCES"
# RPM Version can't contain '-'. A pre-release goes in Release with a
# leading "0." so 3.2.0-b1 sorts before the final 3.2.0-<pkg_release>.
local rpm_release="${PKG_RELEASE}"
[[ -n "${VER_SUFFIX}" ]] && rpm_release="0.${VER_SUFFIX}.${PKG_RELEASE}"
set -x
rpmbuild -bb \
--define "_topdir ${topdir}" \
--define "xrpld_version ${VER_BASE}" \
--define "xrpld_release ${rpm_release}" \
"${topdir}/SPECS/xrpld.spec"
}
build_deb() {
local staging="${BUILD_DIR}/debbuild/source"
rm -rf "${staging}"
mkdir -p "${staging}"
stage_common "${staging}"
cp -r "${DEBIAN_DIR}" "${staging}/debian"
# Debhelper auto-discovers these only from debian/.
cp "${staging}/xrpld.service" "${staging}/debian/xrpld.service"
cp "${staging}/xrpld.sysusers" "${staging}/debian/xrpld.sysusers"
cp "${staging}/xrpld.tmpfiles" "${staging}/debian/xrpld.tmpfiles"
cp "${staging}/update-xrpld.service" "${staging}/debian/xrpld.update-xrpld.service"
cp "${staging}/update-xrpld.timer" "${staging}/debian/xrpld.update-xrpld.timer"
# Debian '~' marks a pre-release; 3.2.0~b1 sorts before 3.2.0.
local deb_full_version="${VER_BASE}${VER_SUFFIX:+~${VER_SUFFIX}}-${PKG_RELEASE}"
# Derive release channel from the version suffix:
# (none) -> stable (tagged release)
# b0 -> develop (develop-branch build)
# b<N>, rc<N> -> unstable (pre-release)
local deb_distribution
case "${VER_SUFFIX}" in
"") deb_distribution="stable" ;;
b0) deb_distribution="develop" ;;
*) deb_distribution="unstable" ;;
esac
cat > "${staging}/debian/changelog" <<EOF
xrpld (${deb_full_version}) ${deb_distribution}; urgency=medium
* Release ${VERSION}.
-- XRPL Foundation <contact@xrplf.org> ${CHANGELOG_DATE}
EOF
chmod +x "${staging}/debian/rules"
set -x
( cd "${staging}" && dpkg-buildpackage -b --no-sign -d )
}
case "${PKG_TYPE}" in
rpm) build_rpm ;;
deb) build_deb ;;
*)
echo "Unknown package type: ${PKG_TYPE}" >&2
exit 1
;;
esac

23
package/debian/control Normal file
View File

@@ -0,0 +1,23 @@
Source: xrpld
Section: net
Priority: optional
Maintainer: XRPL Foundation <contact@xrplf.org>
Rules-Requires-Root: no
Build-Depends:
debhelper-compat (= 13)
Standards-Version: 4.7.0
Homepage: https://github.com/XRPLF/rippled
Vcs-Git: https://github.com/XRPLF/rippled.git
Vcs-Browser: https://github.com/XRPLF/rippled
Package: xrpld
Section: net
Priority: optional
Architecture: any
Depends:
${shlibs:Depends},
${misc:Depends}
Description: XRP Ledger daemon
Reference implementation of the XRP Ledger protocol. Participates
in the peer-to-peer network, processes transactions, and maintains
a local ledger copy.

20
package/debian/copyright Normal file
View File

@@ -0,0 +1,20 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: rippled
Source: https://github.com/XRPLF/rippled
Files: *
Copyright: 2012-2025 Ripple Labs Inc.
License: ISC
License: ISC
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

34
package/debian/rules Normal file
View File

@@ -0,0 +1,34 @@
#!/usr/bin/make -f
export DH_VERBOSE = 1
export DH_OPTIONS = -v
%:
dh $@
override_dh_auto_configure override_dh_auto_build override_dh_auto_test:
@:
override_dh_auto_install:
install -Dm0755 xrpld debian/tmp/opt/xrpld/bin/xrpld
install -Dm0644 xrpld.cfg debian/tmp/opt/xrpld/etc/xrpld.cfg
install -Dm0644 validators.txt debian/tmp/opt/xrpld/etc/validators.txt
install -Dm0644 xrpld.logrotate debian/tmp/opt/xrpld/bin/xrpld.logrotate
install -Dm0755 update-xrpld.sh debian/tmp/opt/xrpld/bin/update-xrpld.sh
install -Dm0644 update-xrpld-cron debian/tmp/opt/xrpld/bin/update-xrpld-cron
install -Dm0644 README.md debian/tmp/usr/share/doc/xrpld/README.md
install -Dm0644 LICENSE.md debian/tmp/usr/share/doc/xrpld/LICENSE.md
# update-xrpld.service is a Type=oneshot fired by update-xrpld.timer; installing
# it without --no-start would run the update on package install.
override_dh_installsystemd:
dh_installsystemd xrpld.service
dh_installsystemd --name=update-xrpld --no-enable --no-start update-xrpld.service update-xrpld.timer
execute_before_dh_installtmpfiles:
dh_installsysusers
override_dh_installsysusers:
override_dh_dwz:
@:

View File

@@ -0,0 +1 @@
3.0 (quilt)

View File

@@ -0,0 +1,2 @@
/opt/xrpld/etc/xrpld.cfg
/opt/xrpld/etc/validators.txt

View File

@@ -0,0 +1,10 @@
opt/xrpld/bin/xrpld
opt/xrpld/bin/xrpld.logrotate
opt/xrpld/bin/update-xrpld.sh
opt/xrpld/bin/update-xrpld-cron
opt/xrpld/etc/xrpld.cfg
opt/xrpld/etc/validators.txt
usr/share/doc/xrpld/README.md
usr/share/doc/xrpld/LICENSE.md

View File

@@ -0,0 +1,10 @@
opt/xrpld/etc etc/opt/xrpld
opt/xrpld/bin/xrpld usr/bin/xrpld
## remove when "rippled" deprecated
opt/xrpld/bin/xrpld opt/xrpld/bin/rippled
opt/xrpld/bin/xrpld usr/local/bin/rippled
opt/xrpld/etc/xrpld.cfg opt/xrpld/etc/rippled.cfg
opt/xrpld opt/ripple
etc/opt/xrpld etc/opt/ripple

111
package/rpm/xrpld.spec Normal file
View File

@@ -0,0 +1,111 @@
Name: xrpld
Version: %{xrpld_version}
Release: %{xrpld_release}%{?dist}
Summary: XRP Ledger daemon
License: ISC
URL: https://github.com/XRPLF/rippled
ExclusiveArch: x86_64 aarch64
BuildRequires: systemd-rpm-macros
%undefine _debugsource_packages
%debug_package
%build_mtime_policy clamp_to_source_date_epoch
%{?systemd_requires}
%{?sysusers_requires_compat}
%description
xrpld is the reference implementation of the XRP Ledger protocol. It
participates in the peer-to-peer XRP Ledger network, processes
transactions, and maintains the ledger database.
%prep
:
%install
rm -rf %{buildroot}
SRC=%{_sourcedir}
install -Dm0755 ${SRC}/xrpld %{buildroot}/opt/xrpld/bin/xrpld
install -Dm0644 ${SRC}/xrpld.cfg %{buildroot}/opt/xrpld/etc/xrpld.cfg
install -Dm0644 ${SRC}/validators.txt %{buildroot}/opt/xrpld/etc/validators.txt
mkdir -p %{buildroot}/etc/opt %{buildroot}/usr/bin %{buildroot}/usr/local/bin
ln -s /opt/xrpld/etc %{buildroot}/etc/opt/xrpld
ln -s /opt/xrpld/bin/xrpld %{buildroot}/usr/bin/xrpld
# TODO: remove when rippled deprecated
ln -s xrpld %{buildroot}/opt/xrpld/bin/rippled
ln -s /opt/xrpld/bin/xrpld %{buildroot}/usr/local/bin/rippled
ln -s xrpld.cfg %{buildroot}/opt/xrpld/etc/rippled.cfg
ln -s /opt/xrpld %{buildroot}/opt/ripple
ln -s /etc/opt/xrpld %{buildroot}/etc/opt/ripple
install -Dm0644 ${SRC}/xrpld.service %{buildroot}%{_unitdir}/xrpld.service
install -Dm0644 ${SRC}/update-xrpld.service %{buildroot}%{_unitdir}/update-xrpld.service
install -Dm0644 ${SRC}/update-xrpld.timer %{buildroot}%{_unitdir}/update-xrpld.timer
install -Dm0644 ${SRC}/xrpld.sysusers %{buildroot}%{_sysusersdir}/xrpld.conf
install -Dm0644 ${SRC}/xrpld.tmpfiles %{buildroot}%{_tmpfilesdir}/xrpld.conf
install -Dm0644 ${SRC}/50-xrpld.preset %{buildroot}%{_presetdir}/50-xrpld.preset
install -Dm0755 ${SRC}/update-xrpld.sh %{buildroot}/opt/xrpld/bin/update-xrpld.sh
install -Dm0644 ${SRC}/update-xrpld-cron %{buildroot}/opt/xrpld/bin/update-xrpld-cron
install -Dm0644 ${SRC}/xrpld.logrotate %{buildroot}/opt/xrpld/bin/xrpld.logrotate
install -Dm0644 ${SRC}/LICENSE.md %{buildroot}/opt/xrpld/share/LICENSE.md
install -Dm0644 ${SRC}/README.md %{buildroot}/opt/xrpld/share/README.md
%pre
%sysusers_create_package xrpld %{_sourcedir}/xrpld.sysusers
%post
systemd-tmpfiles --create %{_tmpfilesdir}/xrpld.conf || :
%systemd_post xrpld.service update-xrpld.timer
%preun
%systemd_preun xrpld.service update-xrpld.timer
%postun
%systemd_postun_with_restart xrpld.service
%files
%license /opt/xrpld/share/LICENSE.md
%doc /opt/xrpld/share/README.md
%dir /opt/xrpld
%dir /opt/xrpld/bin
%dir /opt/xrpld/etc
/opt/xrpld/bin/xrpld
/opt/xrpld/bin/xrpld.logrotate
/opt/xrpld/bin/update-xrpld.sh
/opt/xrpld/bin/update-xrpld-cron
/usr/bin/xrpld
/etc/opt/xrpld
%config(noreplace) /opt/xrpld/etc/xrpld.cfg
%config(noreplace) /opt/xrpld/etc/validators.txt
%{_unitdir}/xrpld.service
%{_unitdir}/update-xrpld.service
%{_unitdir}/update-xrpld.timer
%{_presetdir}/50-xrpld.preset
%{_sysusersdir}/xrpld.conf
%{_tmpfilesdir}/xrpld.conf
%ghost %dir /var/lib/xrpld
%ghost %dir /var/log/xrpld
# TODO: remove when rippled deprecated
/opt/xrpld/bin/rippled
/usr/local/bin/rippled
/opt/xrpld/etc/rippled.cfg
/etc/opt/ripple
/opt/ripple

View File

@@ -0,0 +1,4 @@
# /usr/lib/systemd/system-preset/50-xrpld.preset
enable xrpld.service
# Don't enable automatic updates
disable update-xrpld.timer

View File

@@ -0,0 +1,9 @@
# For automatic updates, symlink this file to /etc/cron.d/
# Do not remove the newline at the end of this cron script
# bash required for use of RANDOM below.
SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
# invoke check/update script with random delay up to 59 mins
0 * * * * root sleep $((RANDOM*3540/32768)) && /opt/xrpld/bin/update-xrpld.sh

View File

@@ -0,0 +1,16 @@
[Unit]
Description=Check for and install xrpld package updates
Documentation=man:systemd.service(5)
Wants=network-online.target
After=network-online.target
ConditionPathExists=/opt/xrpld/bin/update-xrpld.sh
ConditionPathExists=/opt/xrpld/bin/xrpld
[Service]
Type=oneshot
ExecStart=/usr/bin/flock -n /run/lock/xrpld-update.lock /opt/xrpld/bin/update-xrpld.sh
StandardOutput=journal
StandardError=journal
SyslogIdentifier=update-xrpld
TimeoutStartSec=30min
PrivateTmp=true

145
package/shared/update-xrpld.sh Executable file
View File

@@ -0,0 +1,145 @@
#!/usr/bin/env bash
set -euo pipefail
# Optional: also write logs to a legacy file in addition to journald.
# By default, this script logs to systemd/journald, viewable via:
# journalctl -t update-xrpld
#
# Uncomment the line below if you need a flat file for compatibility with
# external tooling, manual inspection, or environments where journald logs
# are not persisted or easily accessible.
#
# Note: This duplicates all output (stdout/stderr) to both journald and the file.
# It is generally not needed on modern systems and may cause log file growth
# if left enabled long-term.
#
# Requires /var/log/xrpld/ to exist and be writable by the service (root).
#
# exec > >(tee -a /var/log/xrpld/update.log) 2>&1
PATH=/usr/sbin:/usr/bin:/sbin:/bin
PKG_NAME=${PKG_NAME:-xrpld}
log() {
# If running under systemd/journald, let it handle timestamps.
if [[ -n "${JOURNAL_STREAM:-}" ]]; then
printf '%s\n' "$*"
else
printf '%s %s\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" "$*"
fi
}
require_root() {
if [[ ${EUID:-$(id -u)} -ne 0 ]]; then
log "RESULT: failed reason=not-root"
exit 1
fi
}
get_installed_version() {
if command -v dpkg-query >/dev/null 2>&1; then
dpkg-query -W -f='${Version}' "$PKG_NAME" 2>/dev/null || printf 'unknown'
elif command -v rpm >/dev/null 2>&1; then
rpm -q --qf '%{VERSION}-%{RELEASE}' "$PKG_NAME" 2>/dev/null || printf 'unknown'
else
printf 'unknown'
fi
}
trap 'log "RESULT: failed reason=script-error exit_code=$?"' ERR
apt_can_update() {
apt-get update -qq
apt-get -s --only-upgrade install "$PKG_NAME" 2>/dev/null | grep -q "^Inst ${PKG_NAME}\b"
}
apt_apply_update() {
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
-o Dpkg::Options::="--force-confdef" \
-o Dpkg::Options::="--force-confold" \
"$PKG_NAME"
}
get_rpm_pm() {
if command -v dnf >/dev/null 2>&1; then
printf 'dnf\n'
elif command -v yum >/dev/null 2>&1; then
printf 'yum\n'
else
return 1
fi
}
rpm_refresh_metadata() {
local pm=$1
if [[ "$pm" == "dnf" ]]; then
dnf makecache --refresh -q >/dev/null
else
yum clean expire-cache -q >/dev/null
fi
}
rpm_can_update() {
local pm=$1
rpm_refresh_metadata "$pm"
local rc=0
set +e
"$pm" check-update -q "$PKG_NAME" >/dev/null 2>&1
rc=$?
set -e
if [[ $rc -eq 100 ]]; then
return 0
elif [[ $rc -eq 0 ]]; then
return 1
else
log "$pm check-update failed with exit code $rc"
exit 1
fi
}
rpm_apply_update() {
local pm=$1
"$pm" update -y "$PKG_NAME"
}
restart_service() {
systemctl restart "${PKG_NAME}.service"
log "${PKG_NAME} service restarted successfully"
}
main() {
require_root
if command -v apt-get >/dev/null 2>&1; then
log "Checking for ${PKG_NAME} updates via apt"
if apt_can_update; then
log "Update available; installing"
apt_apply_update
restart_service
log "RESULT: updated ${PKG_NAME}=$(get_installed_version)"
else
log "RESULT: no-update ${PKG_NAME}=$(get_installed_version)"
fi
return
fi
local rpm_pm=""
if rpm_pm="$(get_rpm_pm)"; then
log "Checking for ${PKG_NAME} updates via ${rpm_pm}"
if rpm_can_update "$rpm_pm"; then
log "Update available; installing"
rpm_apply_update "$rpm_pm"
restart_service
log "RESULT: updated ${PKG_NAME}=$(get_installed_version)"
else
log "RESULT: no-update ${PKG_NAME}=$(get_installed_version)"
fi
return
fi
log "RESULT: failed reason=no-package-manager"
exit 1
}
main "$@"

View File

@@ -0,0 +1,10 @@
[Unit]
Description=Daily xrpld update check
[Timer]
OnCalendar=*-*-* 00:00:00
RandomizedDelaySec=1h
Persistent=true
[Install]
WantedBy=timers.target

View File

@@ -0,0 +1,14 @@
/var/log/xrpld/*.log {
daily
minsize 200M
rotate 7
nocreate
missingok
notifempty
compress
compresscmd /usr/bin/gzip
compressext .gz
postrotate
/opt/xrpld/bin/xrpld --conf /etc/opt/xrpld/xrpld.cfg logrotate
endscript
}

View File

@@ -0,0 +1,22 @@
[Unit]
Description=XRP Ledger Daemon
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Type=simple
ExecStart=/opt/xrpld/bin/xrpld --net --silent --conf /etc/opt/xrpld/xrpld.cfg
Restart=always
RestartSec=5s
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=true
PrivateTmp=true
User=xrpld
Group=xrpld
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1 @@
u xrpld - "XRP Ledger daemon" /var/lib/xrpld /sbin/nologin

View File

@@ -0,0 +1,2 @@
d /var/lib/xrpld 0750 xrpld xrpld -
d /var/log/xrpld 0750 xrpld xrpld -

View File

@@ -0,0 +1,45 @@
#!/usr/bin/env sh
# Validate installed paths and compat symlinks for xrpld packages.
set -e
set -x
trap 'test $? -ne 0 && touch /tmp/test_failed' EXIT
check() { test $1 "$2" || { echo "FAIL: $1 $2"; exit 1; }; }
check_resolves_to() {
actual=$(readlink -f "$1")
[ "$actual" = "$2" ] || { echo "FAIL: $1 resolves to $actual, expected $2"; exit 1; }
}
# compat directory symlinks — existence and resolved target
check -L /opt/ripple
check_resolves_to /opt/ripple /opt/xrpld
check -L /etc/opt/xrpld
check_resolves_to /etc/opt/xrpld /opt/xrpld/etc
check -L /etc/opt/ripple
check_resolves_to /etc/opt/ripple /opt/xrpld/etc
# config accessible via all expected paths
check -f /opt/xrpld/etc/xrpld.cfg
check -f /opt/xrpld/etc/rippled.cfg
check -f /etc/opt/xrpld/xrpld.cfg
check -f /etc/opt/xrpld/rippled.cfg
check -f /etc/opt/ripple/xrpld.cfg
check -f /etc/opt/ripple/rippled.cfg
if systemctl is-system-running >/dev/null 2>&1; then
# service file sanity check
SERVICE=$(systemctl cat xrpld)
echo "$SERVICE" | grep -q 'ExecStart=/opt/xrpld/bin/xrpld' || { echo "FAIL: ExecStart wrong"; echo "$SERVICE"; exit 1; }
echo "$SERVICE" | grep -q 'User=xrpld' || { echo "FAIL: User not xrpld"; echo "$SERVICE"; exit 1; }
fi
# binary accessible via all expected paths
/opt/xrpld/bin/xrpld --version
/opt/xrpld/bin/rippled --version
/opt/ripple/bin/xrpld --version
/opt/ripple/bin/rippled --version
/usr/bin/xrpld --version
/usr/local/bin/rippled --version

88
package/test/smoketest.sh Executable file
View File

@@ -0,0 +1,88 @@
#!/usr/bin/env bash
# Install a locally-built package and run basic verification.
#
# Usage: smoketest.sh local
# Expects packages in build/{dpkg,rpm}/packages/ or build/debbuild/ / build/rpmbuild/RPMS/
set -o pipefail
set -x
rm -f /tmp/test_failed /tmp/unittest_results
trap 'test $? -ne 0 && touch /tmp/test_failed' EXIT
install_from=$1
. /etc/os-release
case ${ID} in
ubuntu|debian)
pkgtype="dpkg"
;;
fedora|centos|rhel|rocky|almalinux)
pkgtype="rpm"
;;
*)
echo "unrecognized distro!"
exit 1
;;
esac
if [ "${install_from}" != "local" ]; then
echo "only 'local' install mode is supported"
exit 1
fi
# Install the package
if [ "${pkgtype}" = "dpkg" ] ; then
apt-get -y update
# Find .deb files — check both possible output locations
mapfile -t debs < <(find build/debbuild/ build/dpkg/packages/ -name '*.deb' ! -name '*dbgsym*' 2>/dev/null)
if [ ${#debs[@]} -eq 0 ]; then
echo "No .deb files found"
exit 1
fi
dpkg --no-debsig -i "${debs[@]}" || apt-get -y install -f
elif [ "${pkgtype}" = "rpm" ] ; then
# Find .rpm files — check both possible output locations
mapfile -t rpms < <(find build/rpmbuild/RPMS/ build/rpm/packages/ -name '*.rpm' \
! -name '*debug*' ! -name '*devel*' ! -name '*.src.rpm' 2>/dev/null)
if [ ${#rpms[@]} -eq 0 ]; then
echo "No .rpm files found"
exit 1
fi
rpm -i "${rpms[@]}" || { echo "RPM install failed"; exit 1; }
fi
# Verify installed version
VERSION_OUTPUT=$(/opt/xrpld/bin/xrpld --version)
INSTALLED=$(echo "$VERSION_OUTPUT" | head -1 | awk '{print $NF}')
echo "Installed version: ${INSTALLED}"
# Run unit tests
if [ -n "${CI:-}" ]; then
unittest_jobs=$(nproc)
else
unittest_jobs=16
fi
(
cd /tmp
/opt/xrpld/bin/xrpld --unittest --unittest-jobs "${unittest_jobs}" > /tmp/unittest_results || true
)
if [ ! -s /tmp/unittest_results ]; then
echo "Unit test results file is empty — xrpld may have crashed"
exit 1
fi
num_failures=$(tail /tmp/unittest_results -n1 | grep -oP '\d+(?= failures)')
if [ -z "$num_failures" ]; then
echo "Could not parse unit test results — expected summary line not found"
exit 1
fi
if [ "${num_failures}" -ne 0 ]; then
echo "$num_failures unit test(s) failed:"
grep 'failed:' /tmp/unittest_results
exit 1
fi
# Compat path checks
"$(dirname "${BASH_SOURCE[0]}")/check_install_paths.sh"

View File

@@ -100,7 +100,7 @@ SField::SField(private_access_tag_t, int fc, char const* fn)
, fieldName(fn)
, fieldMeta(sMD_Never)
, fieldNum(++num)
, signingField(IsSigning::no)
, signingField(IsSigning::yes)
, jsonName(fieldName.c_str())
{
XRPL_ASSERT(

View File

@@ -8,9 +8,6 @@
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
#include <set>
#include <string>
namespace xrpl::test {
class ServerDefinitions_test : public beast::unit_test::suite
@@ -40,23 +37,20 @@ public:
{
auto const firstField = result[jss::result][jss::FIELDS][0u];
BEAST_EXPECT(firstField[0u].asString() == "Invalid");
BEAST_EXPECT(firstField[0u].asString() == "Generic");
BEAST_EXPECT(firstField[1][jss::isSerialized].asBool() == false);
BEAST_EXPECT(firstField[1][jss::isSigningField].asBool() == false);
BEAST_EXPECT(firstField[1][jss::isVLEncoded].asBool() == false);
BEAST_EXPECT(firstField[1][jss::nth].asInt() == -1);
BEAST_EXPECT(firstField[1][jss::nth].asUInt() == 0);
BEAST_EXPECT(firstField[1][jss::type].asString() == "Unknown");
}
{
auto const field = result[jss::result][jss::FIELDS][6u];
BEAST_EXPECT(field[0u].asString() == "LedgerEntryType");
BEAST_EXPECT(field[1][jss::isSerialized].asBool() == true);
BEAST_EXPECT(field[1][jss::isSigningField].asBool() == true);
BEAST_EXPECT(field[1][jss::isVLEncoded].asBool() == false);
BEAST_EXPECT(field[1][jss::nth].asUInt() == 1);
BEAST_EXPECT(field[1][jss::type].asString() == "UInt16");
}
BEAST_EXPECT(
result[jss::result][jss::LEDGER_ENTRY_TYPES]["AccountRoot"].asUInt() == 97);
BEAST_EXPECT(
result[jss::result][jss::TRANSACTION_RESULTS]["tecDIR_FULL"].asUInt() == 121);
BEAST_EXPECT(result[jss::result][jss::TRANSACTION_TYPES]["Payment"].asUInt() == 0);
BEAST_EXPECT(result[jss::result][jss::TYPES]["AccountID"].asUInt() == 8);
// check exception SFields
{
@@ -80,34 +74,17 @@ public:
BEAST_EXPECT(fieldExists("index"));
}
// verify no duplicate field names in FIELDS array
{
std::set<std::string> fieldNames;
for (auto const& field : result[jss::result][jss::FIELDS])
{
auto const name = field[0u].asString();
BEAST_EXPECT(fieldNames.insert(name).second);
}
}
// test that base_uint types are replaced with "Hash" prefix
{
auto const types = result[jss::result][jss::TYPES];
BEAST_EXPECT(types.isMember("Hash128") && types["Hash128"].asUInt() == 4);
BEAST_EXPECT(types.isMember("Hash160") && types["Hash160"].asUInt() == 17);
BEAST_EXPECT(types.isMember("Hash192") && types["Hash192"].asUInt() == 21);
BEAST_EXPECT(types.isMember("Hash256") && types["Hash256"].asUInt() == 5);
BEAST_EXPECT(types.isMember("Hash384") && types["Hash384"].asUInt() == 22);
BEAST_EXPECT(types.isMember("Hash512") && types["Hash512"].asUInt() == 23);
BEAST_EXPECT(types["Hash128"].asUInt() == 4);
BEAST_EXPECT(types["Hash160"].asUInt() == 17);
BEAST_EXPECT(types["Hash192"].asUInt() == 21);
BEAST_EXPECT(types["Hash256"].asUInt() == 5);
BEAST_EXPECT(types["Hash384"].asUInt() == 22);
BEAST_EXPECT(types["Hash512"].asUInt() == 23);
}
BEAST_EXPECT(
result[jss::result][jss::LEDGER_ENTRY_TYPES]["AccountRoot"].asUInt() == 97);
BEAST_EXPECT(
result[jss::result][jss::TRANSACTION_RESULTS]["tecDIR_FULL"].asUInt() == 121);
BEAST_EXPECT(result[jss::result][jss::TRANSACTION_TYPES]["Payment"].asUInt() == 0);
BEAST_EXPECT(result[jss::result][jss::TYPES]["AccountID"].asUInt() == 8);
// test the properties of the LEDGER_ENTRY_FLAGS section
{
BEAST_EXPECT(result[jss::result].isMember(jss::LEDGER_ENTRY_FLAGS));

View File

@@ -149,6 +149,18 @@ ServerDefinitions::ServerDefinitions() : defs_{Json::objectValue}
defs_[jss::FIELDS] = Json::arrayValue;
uint32_t i = 0;
{
Json::Value a = Json::arrayValue;
a[0U] = "Generic";
Json::Value v = Json::objectValue;
v[jss::nth] = 0;
v[jss::isVLEncoded] = false;
v[jss::isSerialized] = false;
v[jss::isSigningField] = false;
v[jss::type] = "Unknown";
a[1U] = v;
defs_[jss::FIELDS][i++] = a;
}
{
Json::Value a = Json::arrayValue;
@@ -215,25 +227,21 @@ ServerDefinitions::ServerDefinitions() : defs_{Json::objectValue}
defs_[jss::FIELDS][i++] = a;
}
// copy into a sorted map to ensure deterministic output order (sorted by fieldCode)
static std::map<int, SField const*> const sortedFields(
xrpl::SField::getKnownCodeToField().begin(), xrpl::SField::getKnownCodeToField().end());
for (auto const& [code, field] : sortedFields)
for (auto const& [code, field] : xrpl::SField::getKnownCodeToField())
{
if (field->fieldName.empty())
continue;
Json::Value innerObj = Json::objectValue;
uint32_t const type = field->fieldType;
uint32_t type = field->fieldType;
innerObj[jss::nth] = field->fieldValue;
// whether the field is variable-length encoded this means that the length is included
// before the content
innerObj[jss::isVLEncoded] =
(type == STI_VL || type == STI_ACCOUNT || type == STI_VECTOR256);
(type == 7U /* Blob */ || type == 8U /* AccountID */ || type == 19U /* Vector256 */);
// whether the field is included in serialization
innerObj[jss::isSerialized] =