mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-29 18:10:34 +00:00
Merge remote-tracking branch 'origin/develop' into a1q123456/split-loan-set-and-loan-accept-implementation
This commit is contained in:
2
.github/scripts/strategy-matrix/linux.json
vendored
2
.github/scripts/strategy-matrix/linux.json
vendored
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"image_tag": "sha-3122de8",
|
||||
"image_tag": "sha-40cdf49",
|
||||
"configs": {
|
||||
"ubuntu": [
|
||||
{
|
||||
|
||||
2
.github/workflows/build-nix-images.yml
vendored
2
.github/workflows/build-nix-images.yml
vendored
@@ -13,6 +13,7 @@ on:
|
||||
- "!nix/docker/README.md"
|
||||
- "!nix/devshell.nix"
|
||||
- "bin/check-tools.sh"
|
||||
- "bin/default-loader-path.sh"
|
||||
- "bin/install-sanitizer-libs.sh"
|
||||
pull_request:
|
||||
paths:
|
||||
@@ -24,6 +25,7 @@ on:
|
||||
- "!nix/docker/README.md"
|
||||
- "!nix/devshell.nix"
|
||||
- "bin/check-tools.sh"
|
||||
- "bin/default-loader-path.sh"
|
||||
- "bin/install-sanitizer-libs.sh"
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
2
.github/workflows/check-pr-description.yml
vendored
2
.github/workflows/check-pr-description.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Write PR body to file
|
||||
env:
|
||||
|
||||
114
.github/workflows/check-tools.yml
vendored
Normal file
114
.github/workflows/check-tools.yml
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
# Verifies the committed snapshots of `bin/check-tools.sh` output for each Nix
|
||||
# environment (see nix/check-tools/). If the environment changes — a new image
|
||||
# tag, an updated flake.lock, a different tool list — without the matching
|
||||
# snapshot being regenerated and committed, this workflow fails so the drift is
|
||||
# caught in review.
|
||||
#
|
||||
# To regenerate the snapshots, see nix/check-tools/README.md.
|
||||
name: Check tools
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/check-tools.yml"
|
||||
- ".github/scripts/strategy-matrix/linux.json"
|
||||
- "bin/check-tools.sh"
|
||||
- "nix/**"
|
||||
- "flake.nix"
|
||||
- "flake.lock"
|
||||
- "rust-toolchain.toml"
|
||||
push:
|
||||
branches:
|
||||
- "develop"
|
||||
paths:
|
||||
- ".github/workflows/check-tools.yml"
|
||||
- ".github/scripts/strategy-matrix/linux.json"
|
||||
- "bin/check-tools.sh"
|
||||
- "nix/**"
|
||||
- "flake.nix"
|
||||
- "flake.lock"
|
||||
- "rust-toolchain.toml"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
# The nix-nixos image tag is pinned alongside the build matrix in linux.json,
|
||||
# so snapshots are checked against the exact image CI builds against.
|
||||
linux-image-tag:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
tag: ${{ steps.tag.outputs.tag }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Read nix image tag
|
||||
id: tag
|
||||
run: echo "tag=$(jq -r .image_tag .github/scripts/strategy-matrix/linux.json)" >>"${GITHUB_OUTPUT}"
|
||||
|
||||
# One job for all environments; they differ only in whether the tools come
|
||||
# from the nix-nixos container (Linux) or `nix develop` (macOS).
|
||||
check-tools:
|
||||
needs: linux-image-tag
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-latest
|
||||
snapshot: nix/check-tools/nix-ubuntu-amd64.txt
|
||||
nix_develop: false
|
||||
- runner: ubuntu-24.04-arm
|
||||
snapshot: nix/check-tools/nix-ubuntu-arm64.txt
|
||||
nix_develop: false
|
||||
- runner: macos-26-apple-clang-21
|
||||
snapshot: nix/check-tools/macos.txt
|
||||
nix_develop: true
|
||||
runs-on: ${{ matrix.runner }}
|
||||
# Linux runs inside the pinned nix-nixos image; macOS runs natively and uses
|
||||
# the flake's dev shell instead (see the run step below).
|
||||
container: ${{ !matrix.nix_develop && format('ghcr.io/xrplf/xrpld/nix-ubuntu:{0}', needs.linux-image-tag.outputs.tag) || null }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@e4b6449d55a61c002d7c3fdfa6c20f721ede0606
|
||||
with:
|
||||
enable_ccache: false
|
||||
|
||||
- name: Regenerate snapshot
|
||||
env:
|
||||
CHECK_TOOLS_SKIP_CLONE: "1"
|
||||
# check-tools.sh skips some macOS tools when CI is set; the snapshots
|
||||
# capture the full `nix develop` environment, so unset it here.
|
||||
CI: ""
|
||||
run: |
|
||||
if [ "${{ matrix.nix_develop }}" = "true" ]; then
|
||||
# `nix develop` prints the dev-shell greeting first; keep only the
|
||||
# check-tools.sh output (from the "Detected OS:" line onward).
|
||||
nix --extra-experimental-features "nix-command flakes" develop \
|
||||
-c bash bin/check-tools.sh | sed -n '/^Detected OS:/,$p' >"${{ matrix.snapshot }}"
|
||||
else
|
||||
bash bin/check-tools.sh >"${{ matrix.snapshot }}"
|
||||
fi
|
||||
|
||||
- name: Verify snapshot is up to date
|
||||
run: |
|
||||
if ! git diff --exit-code -- "${{ matrix.snapshot }}"; then
|
||||
echo "::error::${{ matrix.snapshot }} is out of date. Regenerate it (see nix/check-tools/README.md) and commit the result."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload regenerated snapshot
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: check-tools-${{ runner.os }}-${{ runner.arch }}
|
||||
path: ${{ matrix.snapshot }}
|
||||
3
.github/workflows/on-pr.yml
vendored
3
.github/workflows/on-pr.yml
vendored
@@ -52,7 +52,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Determine changed files
|
||||
# This step checks whether any files have changed that should
|
||||
# cause the next jobs to run. We do it this way rather than
|
||||
@@ -90,6 +90,7 @@ jobs:
|
||||
.clang-tidy
|
||||
.codecov.yml
|
||||
bin/check-tools.sh
|
||||
bin/default-loader-path.sh
|
||||
cfg/**
|
||||
cmake/**
|
||||
conan/**
|
||||
|
||||
1
.github/workflows/on-trigger.yml
vendored
1
.github/workflows/on-trigger.yml
vendored
@@ -28,6 +28,7 @@ on:
|
||||
- ".clang-tidy"
|
||||
- ".codecov.yml"
|
||||
- "bin/check-tools.sh"
|
||||
- "bin/default-loader-path.sh"
|
||||
- "cfg/**"
|
||||
- "cmake/**"
|
||||
- "conan/**"
|
||||
|
||||
4
.github/workflows/publish-docs.yml
vendored
4
.github/workflows/publish-docs.yml
vendored
@@ -41,10 +41,10 @@ env:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-3122de8
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-40cdf49
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@e4b6449d55a61c002d7c3fdfa6c20f721ede0606
|
||||
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
uses: XRPLF/actions/cleanup-workspace@c7d9ce5ebb03c752a354889ecd870cadfc2b1cd4
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@e4b6449d55a61c002d7c3fdfa6c20f721ede0606
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Check levelization
|
||||
run: python .github/scripts/levelization/generate.py
|
||||
- name: Check for differences
|
||||
|
||||
2
.github/workflows/reusable-check-rename.yml
vendored
2
.github/workflows/reusable-check-rename.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Check definitions
|
||||
run: .github/scripts/rename/definitions.sh .
|
||||
- name: Check copyright notices
|
||||
|
||||
4
.github/workflows/reusable-clang-tidy.yml
vendored
4
.github/workflows/reusable-clang-tidy.yml
vendored
@@ -34,13 +34,13 @@ jobs:
|
||||
needs: [determine-files]
|
||||
if: ${{ needs.determine-files.outputs.cpp_changed_files != '' || needs.determine-files.outputs.need_full_run == 'true' }}
|
||||
runs-on: ["self-hosted", "Linux", "X64", "heavy"]
|
||||
container: "ghcr.io/xrplf/xrpld/nix-debian:sha-3122de8"
|
||||
container: "ghcr.io/xrplf/xrpld/nix-debian:sha-40cdf49"
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@e4b6449d55a61c002d7c3fdfa6c20f721ede0606
|
||||
|
||||
4
.github/workflows/reusable-package.yml
vendored
4
.github/workflows/reusable-package.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
matrix: ${{ steps.generate.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Download pre-built binary
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
matrix: ${{ steps.generate.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
|
||||
4
.github/workflows/reusable-upload-recipe.yml
vendored
4
.github/workflows/reusable-upload-recipe.yml
vendored
@@ -40,14 +40,14 @@ defaults:
|
||||
jobs:
|
||||
upload:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-3122de8
|
||||
container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-40cdf49
|
||||
env:
|
||||
REMOTE_NAME: ${{ inputs.remote_name }}
|
||||
CONAN_LOGIN_USERNAME_XRPLF: ${{ secrets.remote_username }}
|
||||
CONAN_PASSWORD_XRPLF: ${{ secrets.remote_password }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Generate build version number
|
||||
id: version
|
||||
|
||||
2
.github/workflows/upload-conan-deps.yml
vendored
2
.github/workflows/upload-conan-deps.yml
vendored
@@ -65,7 +65,7 @@ jobs:
|
||||
uses: XRPLF/actions/cleanup-workspace@c7d9ce5ebb03c752a354889ecd870cadfc2b1cd4
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Prepare runner
|
||||
uses: XRPLF/actions/prepare-runner@e4b6449d55a61c002d7c3fdfa6c20f721ede0606
|
||||
|
||||
@@ -30,8 +30,10 @@ missing=()
|
||||
checked=0
|
||||
|
||||
# check <name> [probe-command...]
|
||||
# Runs the probe (default: "<name> --version") quietly. Records <name> as
|
||||
# missing if the command is not found or exits non-zero.
|
||||
# Runs the probe (default: "<name> --version"), capturing both stdout and
|
||||
# stderr, and prints one aligned line: the status, the name, and the first
|
||||
# non-blank line of the probe output (its version). Records <name> as missing
|
||||
# if the command is not found or exits non-zero.
|
||||
check() {
|
||||
local name="$1"
|
||||
shift
|
||||
@@ -40,10 +42,11 @@ check() {
|
||||
probe=("${name}" --version)
|
||||
fi
|
||||
|
||||
echo "Checking ${name}..."
|
||||
checked=$((checked + 1))
|
||||
if "${probe[@]}" | head -n 1; then
|
||||
printf ' [ ok ] %s\n' "${name}"
|
||||
local output version
|
||||
if output="$("${probe[@]}" 2>&1)"; then
|
||||
version="$(printf '%s\n' "${output}" | grep -m1 '[^[:space:]]' || true)"
|
||||
printf ' [ ok ] %-20s %s\n' "${name}" "${version}"
|
||||
else
|
||||
printf ' [MISS] %s\n' "${name}"
|
||||
missing+=("${name}")
|
||||
@@ -85,12 +88,14 @@ if [ "${os}" = "linux" ] || [ "${os}" = "macos" ]; then
|
||||
check file
|
||||
check less
|
||||
check make
|
||||
check netstat which netstat
|
||||
# net-tools netstat reports "net-tools X.Y"; macOS ships BSD netstat with no
|
||||
# version flag, so fall back to a presence marker there.
|
||||
check netstat sh -c 'command -v netstat >/dev/null && { netstat --version 2>&1 | grep -m1 -oE "net-tools [0-9.]+" || echo present; }'
|
||||
check ninja
|
||||
check perl
|
||||
check perl perl -e 'print "$^V\n"'
|
||||
check pkg-config
|
||||
check vim
|
||||
check zip
|
||||
check zip bash -c 'zip --version 2>&1 | grep -m1 -oE "Zip [0-9.]+"'
|
||||
|
||||
# These tools are present in our Linux CI images and in local development
|
||||
# setups, but not in the macOS CI environment. So check them everywhere
|
||||
|
||||
@@ -29,6 +29,27 @@ if(CMAKE_GENERATOR STREQUAL "Xcode")
|
||||
set(is_xcode TRUE)
|
||||
endif()
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Nix toolchain detection
|
||||
# --------------------------------------------------------------------
|
||||
# True when the C++ compiler resolves into the Nix store. CMAKE_CXX_COMPILER may
|
||||
# be referenced through a symlink outside the store (a Nix profile, a /usr/bin
|
||||
# alternative, ...), so resolve the real path before matching.
|
||||
set(is_nix_compiler FALSE)
|
||||
get_filename_component(_cxx_real "${CMAKE_CXX_COMPILER}" REALPATH)
|
||||
if(_cxx_real MATCHES "^/nix/store/")
|
||||
set(is_nix_compiler TRUE)
|
||||
endif()
|
||||
unset(_cxx_real)
|
||||
|
||||
# True inside the Nix CI Docker image, identified by the /nix/ci-env tree it
|
||||
# ships (see nix/docker/Dockerfile). The dev shell and bare systems don't have
|
||||
# it, so it distinguishes the CI image from other Nix-compiler environments.
|
||||
set(is_ci_image FALSE)
|
||||
if(EXISTS "/nix/ci-env/bin")
|
||||
set(is_ci_image TRUE)
|
||||
endif()
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Operating system detection
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
@@ -1,26 +1,37 @@
|
||||
#[===================================================================[
|
||||
Patch executables to run in non-Nix environments.
|
||||
|
||||
The Nix-based CI image links binaries against an ELF interpreter (loader)
|
||||
that lives in the Nix store, so the resulting binaries don't run elsewhere
|
||||
(including once installed from the .deb package). `patch_nix_binary` adds a
|
||||
POST_BUILD step that resets the interpreter to the system default loader and
|
||||
drops the rpath.
|
||||
The Nix toolchain links binaries against an ELF interpreter (loader)
|
||||
that lives in the Nix store, so the resulting binaries don't run elsewhere.
|
||||
`patch_nix_binary` adds a POST_BUILD step that resets the interpreter
|
||||
to the system default loader and drops the rpath.
|
||||
|
||||
This is only active inside the Nix-based image, detected by the presence of
|
||||
/tmp/loader-path.sh (shipped by that image, resolves the default loader). It
|
||||
is skipped for sanitizer builds, whose runtime libraries are resolved through
|
||||
the rpath. Everywhere else `patch_nix_binary` is a no-op.
|
||||
This runs by default for Nix-toolchain builds (determined by whether the compiler resolves under /nix/store/).
|
||||
Those builds are where binaries get a Nix-store loader.
|
||||
It is opted out of by setting the XRPLD_NO_PATCH_NIX_BINARY environment variable —
|
||||
the plain Nix dev shells set it, since their binaries link a newer glibc
|
||||
and must not be retargeted to the system loader.
|
||||
|
||||
Non-Nix builds (a system compiler, already using the system loader) and sanitizer builds
|
||||
(runtime libraries resolved through the rpath) are skipped too.
|
||||
Everywhere else `patch_nix_binary` is a no-op.
|
||||
|
||||
The default loader is resolved by bin/default-loader-path.sh.
|
||||
#]===================================================================]
|
||||
|
||||
include_guard(GLOBAL)
|
||||
|
||||
include(CompilationEnv)
|
||||
|
||||
# Provided by the Nix-based CI image; prints the system default ELF loader path.
|
||||
set(_loader_path_script "/tmp/loader-path.sh")
|
||||
# Resolves the system default ELF loader path for the current architecture.
|
||||
set(_loader_path_script "${CMAKE_SOURCE_DIR}/bin/default-loader-path.sh")
|
||||
|
||||
if(is_linux AND NOT SANITIZERS_ENABLED AND EXISTS "${_loader_path_script}")
|
||||
if(
|
||||
is_linux
|
||||
AND NOT SANITIZERS_ENABLED
|
||||
AND is_nix_compiler
|
||||
AND NOT DEFINED ENV{XRPLD_NO_PATCH_NIX_BINARY}
|
||||
)
|
||||
execute_process(
|
||||
COMMAND "${_loader_path_script}"
|
||||
OUTPUT_VARIABLE DEFAULT_LOADER_PATH
|
||||
|
||||
@@ -171,9 +171,8 @@ else()
|
||||
# Clang wrapper supplies those paths itself (via -nostdinc++), so at compile time the
|
||||
# flag is unused -> Clang errors under our -Werror. At link time the flag IS consumed
|
||||
# (it selects the C++ runtime), so we move it there instead of dropping it entirely.
|
||||
get_filename_component(_cxx_real "${CMAKE_CXX_COMPILER}" REALPATH)
|
||||
if(
|
||||
_cxx_real MATCHES "^/nix/store/"
|
||||
is_nix_compiler
|
||||
AND is_linux
|
||||
AND is_clang
|
||||
AND CMAKE_CXX_FLAGS MATCHES "stdlib=libstdc"
|
||||
|
||||
@@ -36,6 +36,19 @@ elseif(is_gcc)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# A Nix compiler is only meant to be used from a managed environment: the xrpld
|
||||
# dev shell (which exports XRPL_DEVSHELL) or the CI image. Using one from a bare
|
||||
# shell usually means a leaked toolchain (picked up via PATH or a Conan profile)
|
||||
# and leads to confusing breakage, so fail early with guidance.
|
||||
if(is_nix_compiler AND NOT is_ci_image AND NOT DEFINED ENV{XRPL_DEVSHELL})
|
||||
message(
|
||||
FATAL_ERROR
|
||||
"A Nix compiler (${CMAKE_CXX_COMPILER}) is being used outside the xrpld "
|
||||
"dev shell. Enter it with `nix develop` (see docs/build/nix.md) before "
|
||||
"configuring the build."
|
||||
)
|
||||
endif()
|
||||
|
||||
# check for in-source build and fail
|
||||
if("${CMAKE_CURRENT_SOURCE_DIR}" STREQUAL "${CMAKE_BINARY_DIR}")
|
||||
message(
|
||||
|
||||
8
docs/build/nix.md
vendored
8
docs/build/nix.md
vendored
@@ -154,6 +154,14 @@ conan install .. --output-folder . --build '*' --settings build_type=Release
|
||||
|
||||
To update `flake.lock` to the latest revision use `nix flake update` command.
|
||||
|
||||
## Tooling snapshots
|
||||
|
||||
The tool versions in each Nix environment are recorded in
|
||||
[`nix/check-tools/`](../../nix/check-tools) and verified by CI. If you change the
|
||||
environment (bump the CI image tag, update `flake.lock`, or edit the tool list in
|
||||
`bin/check-tools.sh`), CI fails until you regenerate and commit the affected
|
||||
snapshot — see [`nix/check-tools/README.md`](../../nix/check-tools/README.md).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
See [Troubleshooting Nix problems](./nix_troubleshooting.md) for common issues,
|
||||
|
||||
49
nix/check-tools/README.md
Normal file
49
nix/check-tools/README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# check-tools snapshots
|
||||
|
||||
These files capture the output of [`bin/check-tools.sh`](../../bin/check-tools.sh)
|
||||
— the versions of the development tooling — in each Nix environment:
|
||||
|
||||
| File | Environment |
|
||||
| --------------------- | ----------------------------------- |
|
||||
| `nix-nixos-amd64.txt` | `nix-nixos` CI image, `linux/amd64` |
|
||||
| `nix-nixos-arm64.txt` | `nix-nixos` CI image, `linux/arm64` |
|
||||
| `macos.txt` | macOS, inside `nix develop` |
|
||||
|
||||
The [`check-tools`](../../.github/workflows/check-tools.yml) workflow regenerates
|
||||
each snapshot in its environment and fails if it differs from the committed file.
|
||||
So if you change the environment (bump the image tag in
|
||||
[`linux.json`](../../.github/scripts/strategy-matrix/linux.json), update
|
||||
`flake.lock`, change the tool list in `check-tools.sh`, …) you must regenerate
|
||||
and commit the affected snapshots.
|
||||
|
||||
Each snapshot is `check-tools.sh` stdout with the git-clone connectivity check
|
||||
skipped (`CHECK_TOOLS_SKIP_CLONE=1`), so it contains only deterministic version
|
||||
data. On macOS the dev-shell greeting that `nix develop` prints first is dropped
|
||||
with `sed -n '/^Detected OS:/,$p'`.
|
||||
|
||||
## Regenerating
|
||||
|
||||
The two Linux snapshots come from the `nix-nixos` image (Docker or a compatible
|
||||
runtime such as Apple `container`). The image tag is pinned in `linux.json`:
|
||||
|
||||
```bash
|
||||
img="ghcr.io/xrplf/xrpld/nix-nixos:$(jq -r .image_tag .github/scripts/strategy-matrix/linux.json)"
|
||||
|
||||
for arch in amd64 arm64; do
|
||||
container run --rm -i -e CHECK_TOOLS_SKIP_CLONE=1 -a "${arch}" --entrypoint bash "${img}" -s \
|
||||
<bin/check-tools.sh >"nix/check-tools/nix-nixos-${arch}.txt"
|
||||
done
|
||||
```
|
||||
|
||||
(With Docker, replace `container run … -a "${arch}"` with
|
||||
`docker run … --platform "linux/${arch}"`.)
|
||||
|
||||
The macOS snapshot is generated locally. `CI=` is unset so `check-tools.sh`
|
||||
checks the full dev-shell tool set (it otherwise skips some tools when `CI` is
|
||||
set):
|
||||
|
||||
```bash
|
||||
CI= nix develop -c bash -c 'CHECK_TOOLS_SKIP_CLONE=1 bash bin/check-tools.sh' |
|
||||
sed -n '/^Detected OS:/,$p' \
|
||||
>nix/check-tools/macos.txt
|
||||
```
|
||||
47
nix/check-tools/macos.txt
Normal file
47
nix/check-tools/macos.txt
Normal file
@@ -0,0 +1,47 @@
|
||||
Detected OS: macos (Darwin arm64)
|
||||
|
||||
Core build tools:
|
||||
[ ok ] cmake cmake version 4.1.2
|
||||
[ ok ] conan Conan version 2.28.1
|
||||
[ ok ] git git version 2.54.0
|
||||
[ ok ] python3 Python 3.13.13
|
||||
|
||||
Development tooling:
|
||||
[ ok ] ccache ccache version 4.13.6
|
||||
[ ok ] clang clang version 22.1.7
|
||||
[ ok ] clang++ clang version 22.1.7
|
||||
[ ok ] ClangBuildAnalyzer ClangBuildAnalyzer 1.6.0
|
||||
[ ok ] curl curl 8.20.0 (aarch64-apple-darwin25.3.0) libcurl/8.20.0 OpenSSL/3.6.2 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.69.0 mit-krb5/1.22.1
|
||||
[ ok ] file file-5.47
|
||||
[ ok ] less less 692 (PCRE2 regular expressions)
|
||||
[ ok ] make GNU Make 4.4.1
|
||||
[ ok ] netstat present
|
||||
[ ok ] ninja 1.13.2
|
||||
[ ok ] perl v5.42.0
|
||||
[ ok ] pkg-config 0.29.2
|
||||
[ ok ] vim VIM - Vi IMproved 9.2 (2026 Feb 14, compiled Jan 01 1980 00:00:00)
|
||||
[ ok ] zip Zip 3.0
|
||||
[ ok ] clang-format clang-format version 22.1.7
|
||||
[ ok ] dot dot - graphviz version 12.2.1 (0)
|
||||
[ ok ] doxygen 1.16.1
|
||||
[ ok ] gcovr gcovr 8.4
|
||||
[ ok ] gh gh version 2.94.0 (nixpkgs)
|
||||
[ ok ] git-cliff git-cliff 2.13.1
|
||||
[ ok ] git-lfs git-lfs/3.7.1 (3.7.1; darwin arm64; go 1.26.3)
|
||||
[ ok ] gpg gpg (GnuPG) 2.4.9
|
||||
[ ok ] pre-commit pre-commit 4.5.1
|
||||
[ ok ] run-clang-tidy usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
|
||||
|
||||
Rust toolchain:
|
||||
[ ok ] cargo cargo 1.95.0 (f2d3ce0bd 2026-03-21)
|
||||
[ ok ] cargo-audit cargo-audit-audit 0.22.1
|
||||
[ ok ] cargo-llvm-cov cargo-llvm-cov 0.8.5
|
||||
[ ok ] cargo-nextest cargo-nextest 0.9.137
|
||||
[ ok ] clippy clippy 0.1.95 (59807616e1 2026-04-14)
|
||||
[ ok ] rust-analyzer rust-analyzer 1.95.0 (59807616 2026-04-14)
|
||||
[ ok ] rustc rustc 1.95.0 (59807616e 2026-04-14)
|
||||
[ ok ] rustfmt rustfmt 1.9.0-stable (59807616e1 2026-04-14)
|
||||
|
||||
Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set).
|
||||
|
||||
All 36 checked tools are present and runnable.
|
||||
55
nix/check-tools/nix-ubuntu-amd64.txt
Normal file
55
nix/check-tools/nix-ubuntu-amd64.txt
Normal file
@@ -0,0 +1,55 @@
|
||||
Detected OS: linux (Linux x86_64)
|
||||
|
||||
Core build tools:
|
||||
[ ok ] cmake cmake version 4.1.2
|
||||
[ ok ] conan Conan version 2.28.1
|
||||
[ ok ] git git version 2.54.0
|
||||
[ ok ] python3 Python 3.13.13
|
||||
|
||||
Development tooling:
|
||||
[ ok ] ccache ccache version 4.13.6
|
||||
[ ok ] clang clang version 22.1.7
|
||||
[ ok ] clang++ clang version 22.1.7
|
||||
[ ok ] ClangBuildAnalyzer ClangBuildAnalyzer 1.6.0
|
||||
[ ok ] curl curl 8.20.0 (x86_64-pc-linux-gnu) libcurl/8.20.0 OpenSSL/3.6.2 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.69.0 mit-krb5/1.22.1
|
||||
[ ok ] file file-5.47
|
||||
[ ok ] less less 692 (PCRE2 regular expressions)
|
||||
[ ok ] make GNU Make 4.4.1
|
||||
[ ok ] netstat net-tools 2.10
|
||||
[ ok ] ninja 1.13.2
|
||||
[ ok ] perl v5.42.0
|
||||
[ ok ] pkg-config 0.29.2
|
||||
[ ok ] vim VIM - Vi IMproved 9.2 (2026 Feb 14, compiled Jan 01 1980 00:00:00)
|
||||
[ ok ] zip Zip 3.0
|
||||
[ ok ] clang-format clang-format version 22.1.7
|
||||
[ ok ] dot dot - graphviz version 12.2.1 (0)
|
||||
[ ok ] doxygen 1.16.1
|
||||
[ ok ] gcovr gcovr 8.4
|
||||
[ ok ] gh gh version 2.94.0 (nixpkgs)
|
||||
[ ok ] git-cliff git-cliff 2.13.1
|
||||
[ ok ] git-lfs git-lfs/3.7.1 (3.7.1; linux amd64; go 1.26.3)
|
||||
[ ok ] gpg gpg (GnuPG) 2.4.9
|
||||
[ ok ] pre-commit pre-commit 4.5.1
|
||||
[ ok ] run-clang-tidy usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
|
||||
|
||||
Rust toolchain:
|
||||
[ ok ] cargo cargo 1.95.0 (f2d3ce0bd 2026-03-21)
|
||||
[ ok ] cargo-audit cargo-audit-audit 0.22.1
|
||||
[ ok ] cargo-llvm-cov cargo-llvm-cov 0.8.5
|
||||
[ ok ] cargo-nextest cargo-nextest 0.9.137
|
||||
[ ok ] clippy clippy 0.1.95 (59807616e1 2026-04-14)
|
||||
[ ok ] rust-analyzer rust-analyzer 1.95.0 (5980761 2026-04-14)
|
||||
[ ok ] rustc rustc 1.95.0 (59807616e 2026-04-14)
|
||||
[ ok ] rustfmt rustfmt 1.9.0-stable (59807616e1 2026-04-14)
|
||||
|
||||
GCC toolchain:
|
||||
[ ok ] gcc gcc (GCC) 15.2.0
|
||||
[ ok ] g++ g++ (GCC) 15.2.0
|
||||
[ ok ] gcov gcov (GCC) 15.2.0
|
||||
|
||||
Mold:
|
||||
[ ok ] mold mold 2.41.0 (compatible with GNU ld)
|
||||
|
||||
Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set).
|
||||
|
||||
All 40 checked tools are present and runnable.
|
||||
55
nix/check-tools/nix-ubuntu-arm64.txt
Normal file
55
nix/check-tools/nix-ubuntu-arm64.txt
Normal file
@@ -0,0 +1,55 @@
|
||||
Detected OS: linux (Linux aarch64)
|
||||
|
||||
Core build tools:
|
||||
[ ok ] cmake cmake version 4.1.2
|
||||
[ ok ] conan Conan version 2.28.1
|
||||
[ ok ] git git version 2.54.0
|
||||
[ ok ] python3 Python 3.13.13
|
||||
|
||||
Development tooling:
|
||||
[ ok ] ccache ccache version 4.13.6
|
||||
[ ok ] clang clang version 22.1.7
|
||||
[ ok ] clang++ clang version 22.1.7
|
||||
[ ok ] ClangBuildAnalyzer ClangBuildAnalyzer 1.6.0
|
||||
[ ok ] curl curl 8.20.0 (aarch64-unknown-linux-gnu) libcurl/8.20.0 OpenSSL/3.6.2 zlib/1.3.2 libssh2/1.11.1 nghttp2/1.69.0 mit-krb5/1.22.1
|
||||
[ ok ] file file-5.47
|
||||
[ ok ] less less 692 (PCRE2 regular expressions)
|
||||
[ ok ] make GNU Make 4.4.1
|
||||
[ ok ] netstat net-tools 2.10
|
||||
[ ok ] ninja 1.13.2
|
||||
[ ok ] perl v5.42.0
|
||||
[ ok ] pkg-config 0.29.2
|
||||
[ ok ] vim VIM - Vi IMproved 9.2 (2026 Feb 14, compiled Jan 01 1980 00:00:00)
|
||||
[ ok ] zip Zip 3.0
|
||||
[ ok ] clang-format clang-format version 22.1.7
|
||||
[ ok ] dot dot - graphviz version 12.2.1 (0)
|
||||
[ ok ] doxygen 1.16.1
|
||||
[ ok ] gcovr gcovr 8.4
|
||||
[ ok ] gh gh version 2.94.0 (nixpkgs)
|
||||
[ ok ] git-cliff git-cliff 2.13.1
|
||||
[ ok ] git-lfs git-lfs/3.7.1 (3.7.1; linux arm64; go 1.26.3)
|
||||
[ ok ] gpg gpg (GnuPG) 2.4.9
|
||||
[ ok ] pre-commit pre-commit 4.5.1
|
||||
[ ok ] run-clang-tidy usage: run-clang-tidy [-h] [-allow-enabling-alpha-checkers]
|
||||
|
||||
Rust toolchain:
|
||||
[ ok ] cargo cargo 1.95.0 (f2d3ce0bd 2026-03-21)
|
||||
[ ok ] cargo-audit cargo-audit-audit 0.22.1
|
||||
[ ok ] cargo-llvm-cov cargo-llvm-cov 0.8.5
|
||||
[ ok ] cargo-nextest cargo-nextest 0.9.137
|
||||
[ ok ] clippy clippy 0.1.95 (59807616e1 2026-04-14)
|
||||
[ ok ] rust-analyzer rust-analyzer 1.95.0 (5980761 2026-04-14)
|
||||
[ ok ] rustc rustc 1.95.0 (59807616e 2026-04-14)
|
||||
[ ok ] rustfmt rustfmt 1.9.0-stable (59807616e1 2026-04-14)
|
||||
|
||||
GCC toolchain:
|
||||
[ ok ] gcc gcc (GCC) 15.2.0
|
||||
[ ok ] g++ g++ (GCC) 15.2.0
|
||||
[ ok ] gcov gcov (GCC) 15.2.0
|
||||
|
||||
Mold:
|
||||
[ ok ] mold mold 2.41.0 (compatible with GNU ld)
|
||||
|
||||
Skipping git-over-HTTPS check (CHECK_TOOLS_SKIP_CLONE is set).
|
||||
|
||||
All 40 checked tools are present and runnable.
|
||||
@@ -50,12 +50,17 @@ let
|
||||
# compilerName is the command used to print the version, or null for none.
|
||||
makeShell =
|
||||
{
|
||||
shellName,
|
||||
stdenv,
|
||||
compilerName,
|
||||
version ? null,
|
||||
versionedTools ? [ ],
|
||||
extraPackages ? [ ],
|
||||
warningHook ? "",
|
||||
# Opt out of PatchNixBinary.cmake retargeting binaries to the system
|
||||
# loader. The plain toolchain links a newer glibc, so it must not be
|
||||
# patched; the custom toolchain patches by default.
|
||||
noPatchNixBinary ? false,
|
||||
}:
|
||||
let
|
||||
compilerVersionHook =
|
||||
@@ -73,14 +78,20 @@ let
|
||||
tools = versionedTools;
|
||||
});
|
||||
in
|
||||
(pkgs.mkShell.override { inherit stdenv; }) {
|
||||
packages = commonPackages ++ versionedLinks ++ extraPackages;
|
||||
shellHook = ''
|
||||
echo "Welcome to xrpld development shell";
|
||||
${compilerVersionHook}
|
||||
${warningHook}
|
||||
'';
|
||||
};
|
||||
(pkgs.mkShell.override { inherit stdenv; }) (
|
||||
{
|
||||
packages = commonPackages ++ versionedLinks ++ extraPackages;
|
||||
# Marks a managed dev shell, so the build (XrplSanity.cmake) can tell an
|
||||
# intentional Nix toolchain from one leaked into a bare shell.
|
||||
XRPL_DEVSHELL = shellName;
|
||||
shellHook = ''
|
||||
echo "Welcome to xrpld development shell";
|
||||
${compilerVersionHook}
|
||||
${warningHook}
|
||||
'';
|
||||
}
|
||||
// pkgs.lib.optionalAttrs noPatchNixBinary { XRPLD_NO_PATCH_NIX_BINARY = "1"; }
|
||||
);
|
||||
in
|
||||
rec {
|
||||
# macOS: Nix Clang. Linux: Nix GCC.
|
||||
@@ -89,6 +100,7 @@ rec {
|
||||
# gcc/clang use the custom-glibc toolchain, matching CI. On darwin there is no
|
||||
# custom glibc, so they fall back to the plain nixpkgs toolchain.
|
||||
gcc = makeShell {
|
||||
shellName = "gcc";
|
||||
stdenv = customGccStdenv;
|
||||
compilerName = "gcc";
|
||||
version = gccVersion;
|
||||
@@ -97,6 +109,7 @@ rec {
|
||||
};
|
||||
|
||||
clang = makeShell {
|
||||
shellName = "clang";
|
||||
stdenv = customClangStdenv;
|
||||
compilerName = "clang";
|
||||
version = llvmVersion;
|
||||
@@ -105,6 +118,7 @@ rec {
|
||||
|
||||
# Nix provides no compiler; use the one from your system (e.g. Apple Clang).
|
||||
no-compiler = makeShell {
|
||||
shellName = "no-compiler";
|
||||
stdenv = pkgs.stdenvNoCC;
|
||||
compilerName = null;
|
||||
};
|
||||
@@ -115,19 +129,23 @@ rec {
|
||||
# makes `nix develop .#gcc-plain` fail there rather than silently aliasing gcc.
|
||||
// pkgs.lib.optionalAttrs pkgs.stdenv.isLinux {
|
||||
gcc-plain = makeShell {
|
||||
shellName = "gcc-plain";
|
||||
stdenv = plainGccStdenv;
|
||||
compilerName = "gcc";
|
||||
version = gccVersion;
|
||||
versionedTools = gccVersionedTools;
|
||||
extraPackages = [ plainGcov ];
|
||||
warningHook = plainWarningHook;
|
||||
noPatchNixBinary = true;
|
||||
};
|
||||
|
||||
clang-plain = makeShell {
|
||||
shellName = "clang-plain";
|
||||
stdenv = plainClangStdenv;
|
||||
compilerName = "clang";
|
||||
version = llvmVersion;
|
||||
versionedTools = clangVersionedTools;
|
||||
warningHook = plainWarningHook;
|
||||
noPatchNixBinary = true;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ ENV GIT_SSL_CAINFO="/nix/ci-env/etc/ssl/certs/ca-bundle.crt"
|
||||
# Externally-built dynamically-linked ELF binaries hard-code the loader path
|
||||
# (e.g. /lib64/ld-linux-x86-64.so.2) in their PT_INTERP header. Install it
|
||||
# from the Nix store when the base image doesn't already provide one.
|
||||
COPY nix/docker/loader-path.sh /tmp/loader-path.sh
|
||||
COPY bin/default-loader-path.sh /tmp/loader-path.sh
|
||||
|
||||
RUN <<EOF
|
||||
target="$(/tmp/loader-path.sh)"
|
||||
|
||||
@@ -75,8 +75,9 @@ toolchain being present at runtime. Two pieces make that work:
|
||||
|
||||
- **An expected dynamic linker in the image.**
|
||||
Binaries built in Nix environments reference a dynamic linker from Nix store paths, which won't be present in the base image. However,
|
||||
[`loader-path.sh`](./loader-path.sh) reports the expected loader path for the
|
||||
current architecture, so we can patch the binaries to use the correct loader.
|
||||
[`bin/default-loader-path.sh`](../../bin/default-loader-path.sh) reports the
|
||||
expected loader path for the current architecture, so we can patch the binaries
|
||||
to use the correct loader.
|
||||
|
||||
The build then verifies all of this end to end, and the C++ and Rust programs
|
||||
go through the same pipeline: each is compiled in `final`, has its `PT_INTERP`
|
||||
@@ -93,8 +94,8 @@ whose resulting binary is patched and run like the others.
|
||||
| File | Purpose |
|
||||
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
||||
| [`./Dockerfile`](./Dockerfile) | Multi-stage build described above. |
|
||||
| [`./loader-path.sh`](./loader-path.sh) | Print the dynamic-linker (`PT_INTERP`) path for the current architecture. |
|
||||
| [`./test_files/cpp/`](./test_files/cpp) | C++ sanitizer smoke test: sources + compile/run scripts. |
|
||||
| [`./test_files/rust/`](./test_files/rust) | Rust smoke test: rustc sources + a cargo proc-macro workspace + compile/run scripts. |
|
||||
| [`/bin/check-tools.sh`](../../bin/check-tools.sh) | Verify every expected tools are present and runnable. |
|
||||
| [`/bin/default-loader-path.sh`](../../bin/default-loader-path.sh) | Print the dynamic-linker (`PT_INTERP`) path for the current architecture. |
|
||||
| [`/bin/install-sanitizer-libs.sh`](../../bin/install-sanitizer-libs.sh) | Install `libasan`/`libtsan`/`libubsan` runtimes on the supported base images. |
|
||||
|
||||
@@ -16,7 +16,23 @@ let
|
||||
exec ${pkgs.python3}/bin/python3 ${llvmPackages.clang-unwrapped}/bin/run-clang-tidy "$@"
|
||||
'';
|
||||
|
||||
rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ../rust-toolchain.toml;
|
||||
# rust-overlay's toolchain propagates the *default* stdenv.cc onto the PATH (so
|
||||
# cargo has a linker). That default may be different from the clang we pin here,
|
||||
# so it shadows our clang and the build can silently use a different compiler
|
||||
# version. Drop that cc from every propagation channel instead of pinning a
|
||||
# replacement: the toolchain then carries no compiler and cargo just uses the
|
||||
# active shell's stdenv cc. Must cover all channels — rust-overlay uses both
|
||||
# propagatedBuildInputs and depsHostHostPropagated.
|
||||
rustToolchainBase = pkgs.rust-bin.fromRustupToolchainFile ../rust-toolchain.toml;
|
||||
rustToolchain =
|
||||
let
|
||||
defaultCc = pkgs.stdenv.cc; # default compiler from nixpkgs stdenv
|
||||
withoutDefaultCc = builtins.filter (dep: (dep.outPath or "") != defaultCc.outPath);
|
||||
in
|
||||
rustToolchainBase.overrideAttrs (old: {
|
||||
propagatedBuildInputs = withoutDefaultCc (old.propagatedBuildInputs or [ ]);
|
||||
depsHostHostPropagated = withoutDefaultCc (old.depsHostHostPropagated or [ ]);
|
||||
});
|
||||
|
||||
# Nix wraps its toolchain so that binaries are exposed only under unsuffixed
|
||||
# names (gcc, g++, clang-tidy, ...). Several tools probe for a
|
||||
|
||||
@@ -40,6 +40,17 @@ public:
|
||||
*/
|
||||
[[nodiscard]] virtual unsigned
|
||||
version() const = 0;
|
||||
|
||||
/**
|
||||
* Close the client's connection to the server.
|
||||
*
|
||||
* Releases the connection the client holds against the server's per-port
|
||||
* connection limit. After this call the client must not be used to
|
||||
* invoke() again. Tests use this to deterministically free the slot
|
||||
* rather than waiting for the server's idle timeout to drop it.
|
||||
*/
|
||||
virtual void
|
||||
disconnect() = 0;
|
||||
};
|
||||
|
||||
} // namespace xrpl::test
|
||||
|
||||
@@ -482,11 +482,52 @@ public:
|
||||
app().getNumberOfThreads() == 1,
|
||||
"syncClose() is only useful on an application with a single thread");
|
||||
auto const result = close();
|
||||
auto serverBarrier = std::make_shared<std::promise<void>>();
|
||||
auto future = serverBarrier->get_future();
|
||||
boost::asio::post(app().getIOContext(), [serverBarrier]() { serverBarrier->set_value(); });
|
||||
auto const status = future.wait_for(timeout);
|
||||
return result && status == std::future_status::ready;
|
||||
return result && drainServerIo(timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect the Env's built-in client and wait for the server to
|
||||
* register the dropped connection.
|
||||
*
|
||||
* Env holds one persistent client connection to the server's RPC port for
|
||||
* its whole lifetime (see client()), and that connection counts against
|
||||
* the port's connection limit. Tests that need a known starting occupancy
|
||||
* can call this to deterministically release that slot instead of waiting
|
||||
* out the server's localhost idle timeout.
|
||||
*
|
||||
* The server decrements its per-port connection count in the peer's
|
||||
* destructor, which runs when the io_context processes the end-of-stream
|
||||
* on the closed socket. After closing the client this drains the server's
|
||||
* io_context twice: the first barrier guarantees the reactor has reaped
|
||||
* the closed socket and queued the peer's teardown, and the second
|
||||
* guarantees that teardown (and therefore the count decrement) has run.
|
||||
*
|
||||
* This is only sound when the server uses a single io_context thread, so
|
||||
* that draining establishes ordering against the teardown - configure the
|
||||
* Env with singleThreadIo() (as syncClose() also requires). Like
|
||||
* syncClose(), it relies on loopback teardown latency being negligible.
|
||||
*
|
||||
* @param timeout Maximum time to wait for each barrier task to execute
|
||||
* @return true if both barriers executed within timeout, false otherwise
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
disconnectClient(std::chrono::steady_clock::duration timeout = std::chrono::seconds{1})
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
app().getNumberOfThreads() == 1,
|
||||
"disconnectClient() is only useful on an application with a single "
|
||||
"thread");
|
||||
|
||||
bundle_.client->disconnect();
|
||||
|
||||
// Drain the server's single io thread twice: the first barrier flushes
|
||||
// the reactor's reap of the closed socket (queuing the peer teardown),
|
||||
// the second flushes that teardown - and therefore the connection-count
|
||||
// decrement. Both run unconditionally so a timed-out first drain does
|
||||
// not short-circuit the second.
|
||||
bool const reaped = drainServerIo(timeout);
|
||||
bool const toreDown = drainServerIo(timeout);
|
||||
return reaped && toreDown;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -846,6 +887,25 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
/**
|
||||
* Drain the (single) server io_context thread once.
|
||||
*
|
||||
* Posts a barrier task to the server's io_context and blocks until it
|
||||
* runs, so every task queued before it has been processed. Only meaningful
|
||||
* with a single io thread (see syncClose()/disconnectClient()).
|
||||
*
|
||||
* @param timeout Maximum time to wait for the barrier task to execute
|
||||
* @return true if the barrier ran within timeout, false otherwise
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
drainServerIo(std::chrono::steady_clock::duration timeout)
|
||||
{
|
||||
auto barrier = std::make_shared<std::promise<void>>();
|
||||
auto future = barrier->get_future();
|
||||
boost::asio::post(app().getIOContext(), [barrier]() { barrier->set_value(); });
|
||||
return future.wait_for(timeout) == std::future_status::ready;
|
||||
}
|
||||
|
||||
void
|
||||
fund(bool setDefaultRipple, STAmount const& amount, Account const& account);
|
||||
|
||||
|
||||
@@ -13,8 +13,9 @@ class WSClient_test : public beast::unit_test::Suite
|
||||
{
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
testSmoke()
|
||||
{
|
||||
testcase("smoke");
|
||||
using namespace jtx;
|
||||
Env env(*this);
|
||||
auto wsc = makeWSClient(env.app().config());
|
||||
@@ -28,6 +29,47 @@ public:
|
||||
auto jv = wsc->getMsg(std::chrono::seconds(1));
|
||||
pass();
|
||||
}
|
||||
|
||||
void
|
||||
testGracefulDisconnect()
|
||||
{
|
||||
testcase("graceful disconnect");
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
Env env(*this);
|
||||
auto wsc = makeWSClient(env.app().config());
|
||||
|
||||
// Put real traffic on the connection before closing it.
|
||||
json::Value stream;
|
||||
stream["streams"] = json::ValueType::Array;
|
||||
stream["streams"].append("ledger");
|
||||
auto const sub = wsc->invoke("subscribe", stream);
|
||||
BEAST_EXPECT(sub.isMember("result") || sub.isMember("status"));
|
||||
|
||||
// disconnect() performs a graceful WebSocket closing handshake and
|
||||
// blocks until the server acknowledges. On loopback that completes in
|
||||
// well under its internal 1s timeout; only a broken async_close/ack
|
||||
// coordination would fall through to the force-close path at ~1s. A
|
||||
// generous bound keeps this from flaking under load while still
|
||||
// catching that regression.
|
||||
auto const start = steady_clock::now();
|
||||
wsc->disconnect();
|
||||
auto const elapsed = duration_cast<milliseconds>(steady_clock::now() - start);
|
||||
BEAST_EXPECT(elapsed < milliseconds{750});
|
||||
|
||||
// disconnect() must be idempotent: a second call (and the subsequent
|
||||
// destructor) must not hang, double-close, or crash.
|
||||
wsc->disconnect();
|
||||
pass();
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testSmoke();
|
||||
testGracefulDisconnect();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(WSClient, jtx, xrpl);
|
||||
|
||||
@@ -14,18 +14,23 @@
|
||||
#include <xrpl/server/Port.h>
|
||||
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/address_v4.hpp>
|
||||
#include <boost/asio/ip/address_v6.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/beast/core/multi_buffer.hpp>
|
||||
#include <boost/beast/http/dynamic_body.hpp>
|
||||
#include <boost/beast/http/error.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/read.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <boost/beast/http/verb.hpp>
|
||||
#include <boost/beast/http/write.hpp>
|
||||
#include <boost/system/system_error.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
@@ -84,6 +89,40 @@ class JSONRPCClient : public AbstractClient
|
||||
boost::beast::multi_buffer bout_;
|
||||
unsigned rpcVersion_;
|
||||
|
||||
bool disconnected_ = false;
|
||||
|
||||
// Errors that mean the persistent keep-alive connection was dropped by the
|
||||
// server (rather than a genuine protocol failure), so the request can be
|
||||
// safely retried on a fresh connection.
|
||||
static bool
|
||||
droppedConnection(boost::system::error_code const& ec)
|
||||
{
|
||||
namespace error = boost::asio::error;
|
||||
static auto const kDroppedConnectionErrors = std::to_array<boost::system::error_code>({
|
||||
boost::beast::http::error::end_of_stream,
|
||||
error::eof,
|
||||
error::connection_reset,
|
||||
error::connection_aborted,
|
||||
error::broken_pipe,
|
||||
error::not_connected,
|
||||
});
|
||||
|
||||
return std::ranges::any_of(
|
||||
kDroppedConnectionErrors,
|
||||
[&ec](boost::system::error_code const& e) { return ec == e; });
|
||||
}
|
||||
|
||||
// Tear down and re-establish the socket to ep_, discarding any buffered
|
||||
// bytes left over from the dropped connection.
|
||||
void
|
||||
reconnect()
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
stream_.close(ec);
|
||||
bin_.clear();
|
||||
stream_.connect(ep_);
|
||||
}
|
||||
|
||||
public:
|
||||
explicit JSONRPCClient(Config const& cfg, unsigned rpcVersion)
|
||||
: ep_(getEndpoint(cfg)), stream_(ios_), rpcVersion_(rpcVersion)
|
||||
@@ -91,12 +130,10 @@ public:
|
||||
stream_.connect(ep_);
|
||||
}
|
||||
|
||||
/*
|
||||
Return value is an Object type with up to three keys:
|
||||
status
|
||||
error
|
||||
result
|
||||
*/
|
||||
// Return value is an Object type with up to three keys:
|
||||
// status
|
||||
// error
|
||||
// result
|
||||
json::Value
|
||||
invoke(std::string const& cmd, json::Value const& params) override
|
||||
{
|
||||
@@ -104,6 +141,13 @@ public:
|
||||
using namespace boost::asio;
|
||||
using namespace std::string_literals;
|
||||
|
||||
// Once disconnect() has released the slot, the client must not be
|
||||
// reused (see AbstractClient::disconnect). Refuse rather than let the
|
||||
// failed write/read below trip the reconnect path and silently
|
||||
// re-consume a connection slot, which would defeat disconnectClient().
|
||||
if (disconnected_)
|
||||
Throw<std::logic_error>("JSONRPCClient::invoke called after disconnect()");
|
||||
|
||||
request<string_body> req;
|
||||
req.method(boost::beast::http::verb::post);
|
||||
req.target("/");
|
||||
@@ -131,10 +175,29 @@ public:
|
||||
req.body() = to_string(jr);
|
||||
}
|
||||
req.prepare_payload();
|
||||
write(stream_, req);
|
||||
|
||||
// The client keeps a single keep-alive connection for its whole
|
||||
// lifetime, but the server drops idle localhost connections after a few
|
||||
// seconds (BaseHTTPPeer::kTimeoutSecondsLocal). If a slow gap between
|
||||
// requests let the server close the socket, the write/read here fails
|
||||
// with end_of_stream; reconnect and retry the request exactly once.
|
||||
response<dynamic_body> res;
|
||||
read(stream_, bin_, res);
|
||||
auto writeAndRead = [&] {
|
||||
write(stream_, req);
|
||||
read(stream_, bin_, res);
|
||||
};
|
||||
try
|
||||
{
|
||||
writeAndRead();
|
||||
}
|
||||
catch (boost::system::system_error const& e)
|
||||
{
|
||||
if (!droppedConnection(e.code()))
|
||||
throw;
|
||||
reconnect();
|
||||
res = {};
|
||||
writeAndRead();
|
||||
}
|
||||
|
||||
json::Reader jr;
|
||||
json::Value jv;
|
||||
@@ -151,6 +214,19 @@ public:
|
||||
{
|
||||
return rpcVersion_;
|
||||
}
|
||||
|
||||
void
|
||||
disconnect() override
|
||||
{
|
||||
if (disconnected_)
|
||||
return;
|
||||
|
||||
disconnected_ = true;
|
||||
|
||||
boost::system::error_code ec;
|
||||
stream_.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
|
||||
stream_.close(ec);
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<AbstractClient>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <xrpld/core/Config.h>
|
||||
|
||||
#include <xrpl/basics/Mutex.hpp>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/config/BasicConfig.h>
|
||||
#include <xrpl/config/Constants.h>
|
||||
@@ -112,10 +113,11 @@ class WSClientImpl : public WSClient
|
||||
|
||||
bool peerClosed_ = false;
|
||||
|
||||
// synchronize destructor
|
||||
bool b0_ = false;
|
||||
std::mutex m0_;
|
||||
std::condition_variable cv0_;
|
||||
// disconnect() waits on this until the read loop ends (for any reason:
|
||||
// the server acknowledged our close, or a timeout force-closed the socket).
|
||||
static constexpr auto kDisconnectTimeout = std::chrono::seconds{1};
|
||||
xrpl::Mutex<bool> readEnded_;
|
||||
std::condition_variable readEndCv_;
|
||||
|
||||
// synchronize message queue
|
||||
std::mutex m_;
|
||||
@@ -127,23 +129,26 @@ class WSClientImpl : public WSClient
|
||||
void
|
||||
cleanup()
|
||||
{
|
||||
boost::asio::post(ios_, boost::asio::bind_executor(strand_, [this] {
|
||||
if (!peerClosed_)
|
||||
{
|
||||
ws_.async_close(
|
||||
{}, boost::asio::bind_executor(strand_, [&](error_code) {
|
||||
try
|
||||
{
|
||||
stream_.cancel();
|
||||
}
|
||||
// NOLINTNEXTLINE(bugprone-empty-catch)
|
||||
catch (boost::system::system_error const&)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}));
|
||||
}
|
||||
}));
|
||||
boost::asio::post(
|
||||
ios_, //
|
||||
boost::asio::bind_executor(strand_, [this] {
|
||||
if (!peerClosed_)
|
||||
{
|
||||
ws_.async_close(
|
||||
{}, //
|
||||
boost::asio::bind_executor(strand_, [&](error_code) {
|
||||
try
|
||||
{
|
||||
stream_.cancel();
|
||||
}
|
||||
// NOLINTNEXTLINE(bugprone-empty-catch)
|
||||
catch (boost::system::system_error const&)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}));
|
||||
}
|
||||
}));
|
||||
work_ = std::nullopt;
|
||||
thread_.join();
|
||||
}
|
||||
@@ -289,6 +294,44 @@ public:
|
||||
return rpcVersion_;
|
||||
}
|
||||
|
||||
void
|
||||
disconnect() override
|
||||
{
|
||||
// Perform a graceful WebSocket closing handshake and block until the
|
||||
// read loop ends, so the server observes a clean close (not a RST) and
|
||||
// has finished tearing the connection down by the time we return.
|
||||
// If the server already closed, the wait below returns immediately.
|
||||
boost::asio::post(
|
||||
ios_,
|
||||
boost::asio::bind_executor(
|
||||
strand_, //
|
||||
[this] {
|
||||
if (!peerClosed_)
|
||||
{
|
||||
ws_.async_close(
|
||||
boost::beast::websocket::close_code::normal,
|
||||
boost::asio::bind_executor(strand_, [](error_code) {}));
|
||||
}
|
||||
}));
|
||||
|
||||
auto lock = readEnded_.lock<std::unique_lock>();
|
||||
readEndCv_.wait_for(lock, kDisconnectTimeout, [&lock] { return *lock; });
|
||||
|
||||
// On timeout (server gone or not replying) force the socket closed so
|
||||
// the outstanding read ends and the worker thread can later be joined.
|
||||
if (!*lock)
|
||||
{
|
||||
boost::asio::post(
|
||||
ios_,
|
||||
boost::asio::bind_executor(
|
||||
strand_, //
|
||||
[this] {
|
||||
boost::system::error_code ec;
|
||||
stream_.close(ec);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void
|
||||
onReadMsg(error_code const& ec)
|
||||
@@ -297,33 +340,31 @@ private:
|
||||
{
|
||||
if (ec == boost::beast::websocket::error::closed)
|
||||
peerClosed_ = true;
|
||||
|
||||
*readEnded_.lock() = true;
|
||||
readEndCv_.notify_all();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
json::Value jv;
|
||||
json::Reader jr;
|
||||
|
||||
jr.parse(bufferString(rb_.data()), jv);
|
||||
rb_.consume(rb_.size());
|
||||
|
||||
auto m = std::make_shared<Msg>(std::move(jv));
|
||||
{
|
||||
std::scoped_lock const lock(m_);
|
||||
msgs_.push_front(m);
|
||||
cv_.notify_all();
|
||||
}
|
||||
|
||||
ws_.async_read(
|
||||
rb_, boost::asio::bind_executor(strand_, [this](error_code const& ec, std::size_t) {
|
||||
onReadMsg(ec);
|
||||
}));
|
||||
}
|
||||
|
||||
// Called when the read op terminates
|
||||
void
|
||||
onReadDone()
|
||||
{
|
||||
std::scoped_lock const lock(m0_);
|
||||
b0_ = true;
|
||||
cv0_.notify_all();
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<WSClient>
|
||||
|
||||
@@ -558,10 +558,12 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
|
||||
using namespace test::jtx;
|
||||
using namespace boost::asio;
|
||||
using namespace boost::beast::http;
|
||||
Env env{*this, envconfig([&](std::unique_ptr<Config> cfg) {
|
||||
// Run the server with a single io thread so disconnectClient() below
|
||||
// can deterministically drain the server's io_context (see its docs).
|
||||
Env env{*this, singleThreadIo(envconfig([&](std::unique_ptr<Config> cfg) {
|
||||
(*cfg)[Sections::kPortRpc].set(Keys::kLimit, std::to_string(limit));
|
||||
return cfg;
|
||||
})};
|
||||
}))};
|
||||
|
||||
auto const section = env.app().config().section(Sections::kPortRpc);
|
||||
// NOLINTBEGIN(bugprone-unchecked-optional-access)
|
||||
@@ -580,16 +582,27 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
|
||||
BEAST_EXPECT(!ec);
|
||||
|
||||
std::vector<std::pair<ip::tcp::socket, boost::beast::multi_buffer>> clients;
|
||||
int connectionCount{1}; // starts at 1 because the Env already has one
|
||||
// for JSONRPCCLient
|
||||
|
||||
// for nonzero limits, go one past the limit, although failures happen
|
||||
// at the limit, so this really leads to the last two clients failing.
|
||||
// for zero limit, pick an arbitrary nonzero number of clients - all
|
||||
// should connect fine.
|
||||
// Env owns a persistent JSON-RPC HTTP client connection to port_rpc as
|
||||
// part of startup, which counts against this port's connection limit.
|
||||
// This test wants a known starting occupancy of zero, so for nonzero
|
||||
// limits it deterministically drops that hidden client and waits for
|
||||
// the server to register the disconnect before opening its own clients.
|
||||
//
|
||||
// Starting from zero is important because the port limit rejects once
|
||||
// the incremented connection count reaches the configured limit. With a
|
||||
// zero baseline and N = limit + 1 test-owned clients, exactly the last
|
||||
// two requests should be rejected.
|
||||
if (limit != 0)
|
||||
BEAST_EXPECT(env.disconnectClient());
|
||||
|
||||
// For nonzero limits, go one past the limit. The port rejects at the
|
||||
// limit, not only above it, so this yields the last two clients
|
||||
// failing. For zero limit, pick an arbitrary nonzero number of clients
|
||||
// and expect them all to succeed.
|
||||
|
||||
int const testTo = (limit == 0) ? 50 : limit + 1;
|
||||
while (connectionCount < testTo)
|
||||
while (static_cast<int>(clients.size()) < testTo)
|
||||
{
|
||||
clients.emplace_back(ip::tcp::socket{ios}, boost::beast::multi_buffer{});
|
||||
async_connect(clients.back().first, it, yield[ec]);
|
||||
@@ -597,19 +610,24 @@ class ServerStatus_test : public beast::unit_test::Suite, public beast::test::En
|
||||
auto req = makeHTTPRequest(ip, port, to_string(jr), {});
|
||||
async_write(clients.back().first, req, yield[ec]);
|
||||
BEAST_EXPECT(!ec);
|
||||
++connectionCount;
|
||||
}
|
||||
|
||||
int readCount = 0;
|
||||
int successfulReads = 0;
|
||||
for (auto& [soc, buf] : clients)
|
||||
{
|
||||
boost::beast::http::response<boost::beast::http::string_body> resp;
|
||||
async_read(soc, buf, resp, yield[ec]);
|
||||
++readCount;
|
||||
// expect the reads to fail for the clients that connected at or
|
||||
// above the limit. If limit is 0, all reads should succeed
|
||||
BEAST_EXPECT((limit == 0 || readCount < limit - 1) ? (!ec) : bool(ec));
|
||||
if (!ec)
|
||||
++successfulReads;
|
||||
}
|
||||
|
||||
// This test cares about the exact number of accepted requests, not which
|
||||
// specific client observed the rejection. With a zero baseline (the
|
||||
// hidden Env client dropped above), the server accepts until the
|
||||
// connection count reaches the limit: all clients for limit 0, else
|
||||
// limit - 1 of the limit + 1 clients (the last two are rejected).
|
||||
int const expectedReads = (limit == 0) ? static_cast<int>(clients.size()) : limit - 1;
|
||||
BEAST_EXPECT(successfulReads == expectedReads);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
Reference in New Issue
Block a user