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@e4b6449d55a61c002d7c3fdfa6c20f721ede0606 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 || true - 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: | set -o pipefail cmake \ --build . \ --config "${BUILD_TYPE}" \ --parallel "${BUILD_NPROC}" \ --target "${CMAKE_TARGET}" \ 2>&1 | tee "${GITHUB_WORKSPACE}/build.log" - 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 "${GITHUB_WORKSPACE}/unittest.log" # Smoke-run every benchmark module with a single repetition to confirm the # benchmarks still build and execute. This is a correctness check, not a # performance measurement, so it is skipped for instrumented builds # (sanitizers/coverage/voidstar), where it would be slow and meaningless, # and on Windows, where the `install` target does not build them. - name: Run the benchmarks if: ${{ !inputs.build_only && runner.os != 'Windows' && env.SANITIZERS_ENABLED == 'false' && env.COVERAGE_ENABLED != 'true' && env.VOIDSTAR_ENABLED != 'true' }} working-directory: ${{ env.BUILD_DIR }} run: | rc=0 while IFS= read -r bench; do echo "::group::${bench}" "./${bench}" --benchmark_repetitions=1 || rc=1 echo "::endgroup::" done < <(find src/benchmarks -type f -perm -u+x -name 'xrpl.bench.*') exit "${rc}" - name: Show build/test failure summary if: ${{ failure() }} run: | cd "${GITHUB_WORKSPACE}" if [ -f unittest.log ]; then if ! grep -E "failed" unittest.log | grep -vE "^I[0-9]|^[0-9]+> (ERR:|FTL:)"; then echo "unittest.log present but no failure lines found." fi elif [ -f build.log ]; then # GCC/Clang emit "error:" (covers "fatal error:"); MSVC emits # "error C####:", "error LNK####:", and "fatal error LNK####:". # -A6 prints the lines that follow each match (source line, caret, # notes, and the "N errors generated" tally) to capture the whole # diagnostic block. if ! grep -E -A6 "error:|error C[0-9]{4}|error LNK[0-9]{4}|fatal error" build.log; then echo "build.log present but no compile errors found." fi else echo "unittest.log/build.log not found; something went wrong." exit 1 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