diff --git a/.cspell.config.yaml b/.cspell.config.yaml index 19b3970865..13da132b90 100644 --- a/.cspell.config.yaml +++ b/.cspell.config.yaml @@ -73,6 +73,7 @@ words: - citardauq - clawback - clawbacks + - clippy - cmaketoolchain - coeffs - coldwallet @@ -260,6 +261,7 @@ words: - rocksdb - Rohrs - roundings + - rustc - sahyadri - Satoshi - scons diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index e7a88a0e66..b2327f67ea 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -124,7 +124,7 @@ jobs: - name: Check tools env: CHECK_TOOLS_SKIP_CLONE: "1" - run: ./bin/check-tools.sh + run: ./bin/check-tools.sh || true - name: Print build environment uses: XRPLF/actions/print-build-env@59dec886e4afb05a1724443af08baccbc045b574 diff --git a/bin/check-tools.sh b/bin/check-tools.sh index 808f384d5b..7886bcf8b0 100755 --- a/bin/check-tools.sh +++ b/bin/check-tools.sh @@ -110,6 +110,23 @@ if [ "${os}" = "linux" ] || [ "${os}" = "macos" ]; then fi fi +# Rust toolchain. Part of the Nix commonPackages, so available on both Linux +# and macOS. The cargo plugins are invoked through cargo (`cargo `), which +# resolves the matching `cargo-` binary on PATH; `--version` is offline and +# does not need a Cargo project. +if [ "${os}" = "linux" ] || [ "${os}" = "macos" ]; then + echo + echo "Rust toolchain:" + check cargo + check cargo-audit cargo audit --version + check cargo-llvm-cov cargo llvm-cov --version + check cargo-nextest cargo nextest --version + check clippy clippy-driver --version + check rust-analyzer + check rustc + check rustfmt +fi + # GCC is the default compiler on Linux. macOS uses the system Apple Clang # instead, so GCC/g++/gcov are not expected there. if [ "${os}" = "linux" ]; then diff --git a/nix/docker/Dockerfile b/nix/docker/Dockerfile index 6d8980f897..7222cc8fa8 100644 --- a/nix/docker/Dockerfile +++ b/nix/docker/Dockerfile @@ -75,9 +75,13 @@ COPY bin/check-tools.sh /tmp/check-tools.sh RUN /tmp/check-tools.sh # Sanity-check that the g++/clang++ are able to build binaries, including sanitizer-instrumented ones. -COPY nix/docker/test_files/cpp_sources/ /tmp/cpp_sources/ -COPY nix/docker/test_files/compile-cpp-sources.sh /tmp/compile-cpp-sources.sh -RUN /tmp/compile-cpp-sources.sh /tmp/cpp_sources /tmp/bins +COPY nix/docker/test_files/cpp/ /tmp/test_files/cpp/ +RUN /tmp/test_files/cpp/compile-sources.sh /tmp/test_files/cpp/sources /tmp/cpp-bins + +# Sanity-check that rustc is able to build binaries, including ones that rely on +# the runtime overflow check. +COPY nix/docker/test_files/rust/ /tmp/test_files/rust/ +RUN /tmp/test_files/rust/compile-sources.sh /tmp/test_files/rust/sources /tmp/rust-bins # Tester: start from a clean BASE_IMAGE, install sanitizer runtime libraries, # and run the compiled test binaries to verify they execute correctly. @@ -94,15 +98,18 @@ SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"] # Sanity-check that the built binaries run correctly in the vanilla base image, with the necessary sanitizer runtime libraries installed. COPY bin/install-sanitizer-libs.sh /tmp/install-sanitizer-libs.sh -COPY nix/docker/test_files/run-test-binaries.sh /tmp/run-test-binaries.sh -COPY --from=final /tmp/bins /tmp/bins +COPY nix/docker/test_files/cpp/run-binaries.sh /tmp/test_files/cpp/run-binaries.sh +COPY nix/docker/test_files/rust/run-binaries.sh /tmp/test_files/rust/run-binaries.sh +COPY --from=final /tmp/cpp-bins /tmp/cpp-bins +COPY --from=final /tmp/rust-bins /tmp/rust-bins RUN < as for name in +# {hello,panic,overflow}. + +set -eo pipefail + +bins_dir="${1:?usage: $0 }" + +failed_binaries=() + +# Run a binary and verify its exit code and output. +# Usage: run +function run() { + local binary="${1}" + local expected_output="${2}" + local expected_rc="${3}" + + local out_file + out_file="$(mktemp)" + + echo "=== Run ${binary} ===" + set +e + "${binary}" >"${out_file}" 2>&1 + local rc=$? + set -e + + cat "${out_file}" + + local failed=0 + if [ "${expected_rc}" = "nonzero" ]; then + if [ "${rc}" -eq 0 ]; then + echo "ERROR: expected non-zero exit code from ${binary}, got ${rc}" >&2 + failed=1 + fi + elif [ "${rc}" -ne "${expected_rc}" ]; then + echo "ERROR: expected exit code ${expected_rc} from ${binary}, got ${rc}" >&2 + failed=1 + fi + + if ! grep -q "${expected_output}" "${out_file}"; then + echo "ERROR: expected '${expected_output}' from ${binary}" >&2 + failed=1 + fi + + if [ "${failed}" -eq 0 ]; then + echo "OK: '${expected_output}' detected" + else + failed_binaries+=("${binary}") + fi +} + +declare -A expect=( + [hello]="Hello from main thread" + [panic]="explicit panic from test" + [overflow]="attempt to add with overflow" +) + +for name in hello panic overflow; do + binary="${bins_dir}/${name}" + + if [ "${name}" = "hello" ]; then + expected_rc=0 + else + expected_rc=nonzero + fi + run "${binary}" "${expect[$name]}" "${expected_rc}" +done + +if [ "${#failed_binaries[@]}" -gt 0 ]; then + echo "ERROR: the following binaries failed:" >&2 + printf ' %s\n' "${failed_binaries[@]}" >&2 + exit 1 +fi diff --git a/nix/docker/test_files/rust/sources/hello.rs b/nix/docker/test_files/rust/sources/hello.rs new file mode 100644 index 0000000000..78e32c17f3 --- /dev/null +++ b/nix/docker/test_files/rust/sources/hello.rs @@ -0,0 +1,16 @@ +use std::thread; + +fn main() { + const NUM_THREADS: usize = 10; + let mut handles = Vec::with_capacity(NUM_THREADS); + for id in 0..NUM_THREADS { + handles.push(thread::spawn(move || { + println!("Hello from thread {id}"); + })); + } + for handle in handles { + handle.join().expect("worker thread panicked"); + } + + println!("Hello from main thread"); +} diff --git a/nix/docker/test_files/rust/sources/overflow.rs b/nix/docker/test_files/rust/sources/overflow.rs new file mode 100644 index 0000000000..2a4a54472c --- /dev/null +++ b/nix/docker/test_files/rust/sources/overflow.rs @@ -0,0 +1,13 @@ +use std::hint::black_box; + +// Rust analogue of the C++ UBSan check: with overflow checks enabled the +// compiler inserts a runtime check that panics on signed integer overflow. +// `black_box` keeps the operands opaque so the addition is evaluated at +// runtime rather than being rejected by the compile-time overflow lint. +fn main() { + let max = black_box(i32::MAX); + let one = black_box(1); + println!("Current max: {max}"); + let overflowed = max + one; + println!("Overflowed result: {overflowed}"); +} diff --git a/nix/docker/test_files/rust/sources/panic.rs b/nix/docker/test_files/rust/sources/panic.rs new file mode 100644 index 0000000000..38779ff515 --- /dev/null +++ b/nix/docker/test_files/rust/sources/panic.rs @@ -0,0 +1,5 @@ +fn main() { + // Verify the panic runtime works: a panic must print its message to stderr + // and exit with a non-zero status (Rust's default panic exit code is 101). + panic!("explicit panic from test"); +} diff --git a/nix/packages.nix b/nix/packages.nix index bf10ee3712..41d7e97328 100644 --- a/nix/packages.nix +++ b/nix/packages.nix @@ -62,5 +62,15 @@ in runClangTidy vim zip + # Rust packages + cargo + cargo-audit + cargo-llvm-cov + cargo-nextest + clippy + corrosion + rust-analyzer + rustc + rustfmt ]; }