name: Build and test configuration on: workflow_call: inputs: build_only: description: 'Whether to only build or to build and test the code ("true", "false").' required: true type: boolean build_type: description: 'The build type to use ("Debug", "Release").' required: true type: string ccache_enabled: description: "Whether to enable ccache." required: false type: boolean default: false cmake_args: description: "Additional arguments to pass to CMake." required: false type: string default: "" cmake_target: description: "The CMake target to build." required: true type: string runs_on: description: Runner to run the job on as a JSON string required: true type: string image: description: "The image to run in (leave empty to run natively)" required: true type: string config_name: description: "The configuration string (used for naming artifacts and such)." required: true type: string nproc_subtract: description: "The number of processors to subtract when calculating parallelism." required: false type: number default: 2 sanitizers: description: "The sanitizers to enable." required: false type: string default: "" compiler: description: 'The compiler to use ("gcc" or "clang"). Leave empty for macOS/Windows (uses system default).' required: false type: string default: "" secrets: CODECOV_TOKEN: description: "The Codecov token to use for uploading coverage reports." required: true defaults: run: shell: bash env: # Conan installs the generators in the build/generators directory, see the # layout() method in conanfile.py. We then run CMake from the build directory. BUILD_DIR: build jobs: build-and-test: name: ${{ inputs.config_name }} runs-on: ${{ fromJSON(inputs.runs_on) }} container: ${{ inputs.image != '' && inputs.image || null }} timeout-minutes: ${{ inputs.sanitizers != '' && 360 || 180 }} env: # Use a namespace to keep the objects separate for each configuration. CCACHE_NAMESPACE: ${{ inputs.config_name }} # Ccache supports both Redis and HTTP endpoints. # * For Redis, use the following format: redis://ip:port, see # https://github.com/ccache/ccache/wiki/Redis-storage. Note that TLS is # not directly supported by ccache, and requires use of a proxy. # * For HTTP use the following format: http://ip:port/cache when using # nginx as backend or http://ip:port|layout=bazel when using Bazel # Remote Cache, see https://github.com/ccache/ccache/wiki/HTTP-storage. # Note that HTTPS is not directly supported by ccache. CCACHE_REMOTE_ONLY: true CCACHE_REMOTE_STORAGE: http://cache.dev.ripplex.io:8080|layout=bazel # Ignore the creation and modification timestamps on files, since the # header files are copied into separate directories by CMake, which will # otherwise result in cache misses. CCACHE_SLOPPINESS: include_file_ctime,include_file_mtime # Determine if coverage and voidstar should be enabled. COVERAGE_ENABLED: ${{ contains(inputs.cmake_args, '-Dcoverage=ON') }} VOIDSTAR_ENABLED: ${{ contains(inputs.cmake_args, '-Dvoidstar=ON') }} SANITIZERS_ENABLED: ${{ inputs.sanitizers != '' }} steps: - name: Cleanup workspace (macOS and Windows) if: ${{ runner.os == 'macOS' || runner.os == 'Windows' }} uses: XRPLF/actions/cleanup-workspace@c7d9ce5ebb03c752a354889ecd870cadfc2b1cd4 - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Prepare runner uses: XRPLF/actions/prepare-runner@64ec3cf3b152b4444638f470bbd6df7a7a30c81c with: enable_ccache: ${{ inputs.ccache_enabled }} - name: Set ccache log file if: ${{ inputs.ccache_enabled && runner.debug == '1' }} run: echo "CCACHE_LOGFILE=${{ runner.temp }}/ccache.log" >>"${GITHUB_ENV}" - name: Check tools env: CHECK_TOOLS_SKIP_CLONE: "1" run: ./bin/check-tools.sh - name: Print build environment uses: XRPLF/actions/print-build-env@59dec886e4afb05a1724443af08baccbc045b574 - name: Get number of processors uses: XRPLF/actions/get-nproc@cf0433aa74563aead044a1e395610c96d65a37cf id: nproc with: subtract: ${{ inputs.nproc_subtract }} - name: Set compiler environment (Linux) if: ${{ runner.os == 'Linux' }} uses: ./.github/actions/set-compiler-env with: compiler: ${{ inputs.compiler }} - name: Setup Conan env: SANITIZERS: ${{ inputs.sanitizers }} uses: ./.github/actions/setup-conan - name: Build dependencies uses: ./.github/actions/build-deps with: build_nproc: ${{ steps.nproc.outputs.nproc }} build_type: ${{ inputs.build_type }} # Set the verbosity to "quiet" for Windows to avoid an excessive # amount of logs. For other OSes, the "verbose" logs are more useful. log_verbosity: ${{ runner.os == 'Windows' && 'quiet' || 'verbose' }} sanitizers: ${{ inputs.sanitizers }} - name: Configure CMake working-directory: ${{ env.BUILD_DIR }} env: BUILD_TYPE: ${{ inputs.build_type }} CMAKE_ARGS: ${{ inputs.cmake_args }} run: | cmake \ -G '${{ runner.os == 'Windows' && 'Visual Studio 18 2026' || 'Ninja' }}' \ -DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake \ -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \ ${CMAKE_ARGS} \ .. # Export the sanitizer options before any instrumented binary runs. The # protocol code-gen and build steps below invoke instrumented dependency # tools (protoc, grpc), so setting UBSAN_OPTIONS here lets the UBSan # suppression list silence their diagnostics too, not just at test time. # GITHUB_WORKSPACE (not the github.workspace context) is used so the path # resolves correctly inside the container job. - name: Set sanitizer options if: ${{ !inputs.build_only && env.SANITIZERS_ENABLED == 'true' }} env: CONFIG_NAME: ${{ inputs.config_name }} run: | SUPP="${GITHUB_WORKSPACE}/sanitizers/suppressions" ASAN_OPTS="include=${SUPP}/runtime-asan-options.txt:suppressions=${SUPP}/asan.supp" if [[ "${CONFIG_NAME}" == *gcc* ]]; then ASAN_OPTS="${ASAN_OPTS}:alloc_dealloc_mismatch=0" fi echo "ASAN_OPTIONS=${ASAN_OPTS}" >>${GITHUB_ENV} echo "TSAN_OPTIONS=include=${SUPP}/runtime-tsan-options.txt:suppressions=${SUPP}/tsan.supp" >>${GITHUB_ENV} echo "UBSAN_OPTIONS=include=${SUPP}/runtime-ubsan-options.txt:suppressions=${SUPP}/ubsan.supp" >>${GITHUB_ENV} echo "LSAN_OPTIONS=include=${SUPP}/runtime-lsan-options.txt:suppressions=${SUPP}/lsan.supp" >>${GITHUB_ENV} - name: Check protocol autogen files are up-to-date working-directory: ${{ env.BUILD_DIR }} env: MESSAGE: | The generated protocol wrapper classes are out of date. This typically happens when the macro files or generator scripts have changed but the generated files were not regenerated. To fix this: 1. Run: cmake --build . --target setup_code_gen 2. Run: cmake --build . --target code_gen 3. Commit and push the regenerated files run: | set -e cmake --build . --target setup_code_gen cmake --build . --target code_gen DIFF=$(git -C .. status --porcelain -- include/xrpl/protocol_autogen src/tests/libxrpl/protocol_autogen) if [ -n "${DIFF}" ]; then echo "::error::Generated protocol files are out of date" git -C .. diff -- include/xrpl/protocol_autogen src/tests/libxrpl/protocol_autogen echo "${MESSAGE}" exit 1 fi - name: Build the binary working-directory: ${{ env.BUILD_DIR }} env: BUILD_NPROC: ${{ steps.nproc.outputs.nproc }} BUILD_TYPE: ${{ inputs.build_type }} CMAKE_TARGET: ${{ inputs.cmake_target }} run: | cmake \ --build . \ --config "${BUILD_TYPE}" \ --parallel "${BUILD_NPROC}" \ --target "${CMAKE_TARGET}" - name: Show ccache statistics if: ${{ inputs.ccache_enabled }} run: | ccache --show-stats -vv if [ '${{ runner.debug }}' = '1' ]; then cat "${CCACHE_LOGFILE}" curl ${CCACHE_REMOTE_STORAGE%|*}/status || true fi - name: Upload the binary (Linux) if: ${{ github.event.repository.visibility == 'public' && runner.os == 'Linux' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: xrpld-${{ inputs.config_name }} path: ${{ env.BUILD_DIR }}/xrpld retention-days: 3 if-no-files-found: error - name: Upload the test binary (Linux) if: ${{ github.event.repository.visibility == 'public' && runner.os == 'Linux' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: xrpl_tests-${{ inputs.config_name }} path: ${{ env.BUILD_DIR }}/xrpl_tests retention-days: 3 if-no-files-found: error - name: Export server definitions if: ${{ runner.os != 'Windows' && !inputs.build_only && env.VOIDSTAR_ENABLED != 'true' }} working-directory: ${{ env.BUILD_DIR }} run: | set -o pipefail ./xrpld --definitions | python3 -m json.tool >server_definitions.json - name: Upload server definitions if: ${{ github.event.repository.visibility == 'public' && inputs.config_name == 'debian-gcc-release-amd64' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: server-definitions path: ${{ env.BUILD_DIR }}/server_definitions.json retention-days: 3 if-no-files-found: error - name: Check linking (Linux) if: ${{ runner.os == 'Linux' && env.SANITIZERS_ENABLED == 'false' }} working-directory: ${{ env.BUILD_DIR }} run: | ldd ./xrpld if [ "$(ldd ./xrpld | grep -E '(libstdc\+\+|libgcc)' | wc -l)" -eq 0 ]; then echo 'The binary is statically linked.' else echo 'The binary is dynamically linked.' exit 1 fi - name: Verify presence of instrumentation (Linux) if: ${{ runner.os == 'Linux' && env.VOIDSTAR_ENABLED == 'true' }} working-directory: ${{ env.BUILD_DIR }} run: | ./xrpld --version | grep libvoidstar - name: Run the separate tests if: ${{ !inputs.build_only }} working-directory: ${{ runner.os == 'Windows' && format('{0}/{1}', env.BUILD_DIR, inputs.build_type) || env.BUILD_DIR }} run: ./xrpl_tests - name: Run the embedded tests if: ${{ !inputs.build_only }} working-directory: ${{ runner.os == 'Windows' && format('{0}/{1}', env.BUILD_DIR, inputs.build_type) || env.BUILD_DIR }} env: BUILD_NPROC: ${{ steps.nproc.outputs.nproc }} run: | set -o pipefail # Coverage builds are slower due to instrumentation; use fewer parallel jobs to avoid flakiness [ "$COVERAGE_ENABLED" = "true" ] && BUILD_NPROC=$((BUILD_NPROC - 2)) # The resolver/preload workaround is only correct for the ASan build: # a regular build doesn't hit the __dn_expand interceptor bug, and must # NOT have libasan injected. So only preload when xrpld is ASan-built. # # libresolv hosts getaddrinfo's resolver helpers (dn_expand, res_*). Under ASan # these are intercepted via dlsym(RTLD_NEXT, ...), which yields a NULL pointer # and crashes DNS resolution if libresolv isn't loaded. Linking it guarantees # the symbols are present; it's a harmless no-op on glibc >= 2.34 (merged into # libc) and is what the compiler driver already does for sanitizer builds. # https://github.com/llvm/llvm-project/issues/59007 # https://github.com/google/sanitizers/issues/1592 if ldd ./xrpld | grep -q libasan; then PRELOAD="$(gcc -print-file-name=libasan.so):/usr/lib/x86_64-linux-gnu/libresolv.so.2" else PRELOAD="" fi LD_PRELOAD="$PRELOAD" ./xrpld --unittest --unittest-jobs "${BUILD_NPROC}" 2>&1 | tee unittest.log - name: Show test failure summary if: ${{ failure() && !inputs.build_only }} env: WORKING_DIR: ${{ runner.os == 'Windows' && format('{0}\{1}', env.BUILD_DIR, inputs.build_type) || env.BUILD_DIR }} run: | if [ ! -d "${WORKING_DIR}" ]; then echo "Working directory '${WORKING_DIR}' does not exist." exit 0 fi cd "${WORKING_DIR}" if [ ! -f unittest.log ]; then echo "unittest.log not found; embedded tests may not have run." exit 0 fi if ! grep -E "failed" unittest.log; then echo "Log present but no failure lines found in unittest.log." fi - name: Debug failure (Linux) if: ${{ failure() && runner.os == 'Linux' && !inputs.build_only }} run: | echo "IPv4 local port range:" cat /proc/sys/net/ipv4/ip_local_port_range echo "Netstat:" netstat -an - name: Prepare coverage report if: ${{ !inputs.build_only && env.COVERAGE_ENABLED == 'true' }} working-directory: ${{ env.BUILD_DIR }} env: BUILD_NPROC: ${{ steps.nproc.outputs.nproc }} BUILD_TYPE: ${{ inputs.build_type }} run: | cmake \ --build . \ --config "${BUILD_TYPE}" \ --parallel "${BUILD_NPROC}" \ --target coverage - name: Upload coverage report if: ${{ github.repository == 'XRPLF/rippled' && !inputs.build_only && env.COVERAGE_ENABLED == 'true' }} uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: disable_search: true disable_telem: true fail_ci_if_error: true files: ${{ env.BUILD_DIR }}/coverage.xml plugins: noop token: ${{ secrets.CODECOV_TOKEN }} verbose: true