diff --git a/.codecov.yml b/.codecov.yml index cd52e2604d..4268758e44 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,10 +1,32 @@ codecov: require_ci_to_pass: true + # The C++ and Rust uploads land minutes apart; without this gate Codecov + # publishes a near-zero total from whichever one arrives first. + notify: + after_n_builds: 2 + wait_for_ci: true comment: behavior: default layout: reach,diff,flags,tree,reach - show_carryforward_flags: false + show_carryforward_flags: true + after_n_builds: 2 + +# C++ and Rust coverage upload from independent workflows under the `cpp` and +# `rust` flags; carryforward keeps one language's total when only the other reran. +flag_management: + default_rules: + carryforward: true + individual_flags: + - name: cpp + carryforward: true + paths: + - include/ + - src/ + - name: rust + carryforward: true + paths: + - crates/ coverage: range: "70..85" diff --git a/.cspell.config.yaml b/.cspell.config.yaml index aa64a318fd..6220cfd60e 100644 --- a/.cspell.config.yaml +++ b/.cspell.config.yaml @@ -318,6 +318,7 @@ words: - summands - superpeer - superpeers + - Swatinem - takergets - takerpays - ters diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1ccbd61102..da37f79007 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -19,3 +19,19 @@ updates: github-actions: patterns: - "*" + + - package-ecosystem: cargo + directory: /crates + schedule: + interval: weekly + day: monday + time: "04:00" + timezone: Etc/GMT + commit-message: + prefix: "chore: [DEPENDABOT] " + target-branch: develop + open-pull-requests-limit: 10 + groups: + rust-dependencies: + patterns: + - "*" diff --git a/.github/scripts/strategy-matrix/generate.py b/.github/scripts/strategy-matrix/generate.py index fb37fb7691..7fef6643ff 100755 --- a/.github/scripts/strategy-matrix/generate.py +++ b/.github/scripts/strategy-matrix/generate.py @@ -7,7 +7,13 @@ from pathlib import Path THIS_DIR = Path(__file__).parent.resolve() -_BASE_CMAKE_ARGS = ["-Dtests=ON", "-Dwerr=ON", "-Dxrpld=ON", "-Dwextra=ON"] +_BASE_CMAKE_ARGS = [ + "-Dtests=ON", + "-Dwerr=ON", + "-Dxrpld=ON", + "-Dwextra=ON", + "-Drust=ON", +] # Maps sanitizer names (as used in cmake) to short config-name suffixes. _SANITIZER_SUFFIX: dict[str, str] = { diff --git a/.github/workflows/cargo-audit.yml b/.github/workflows/cargo-audit.yml new file mode 100644 index 0000000000..d167e52e61 --- /dev/null +++ b/.github/workflows/cargo-audit.yml @@ -0,0 +1,80 @@ +name: Cargo audit + +on: + schedule: + # 06:32 UTC every Monday. + - cron: "32 6 * * 1" + push: + branches: + - "develop" + - "release/*" + paths: + - "crates/**/Cargo.toml" + - "crates/Cargo.lock" + - ".github/workflows/cargo-audit.yml" + pull_request: + paths: + - "crates/**/Cargo.toml" + - "crates/Cargo.lock" + - ".github/workflows/cargo-audit.yml" + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + working-directory: crates + +permissions: + contents: read + +jobs: + audit: + runs-on: ubuntu-latest + container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-a0074f8 + permissions: + contents: read + # Needed to open an issue on scheduled failures. + issues: write + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Run cargo audit + id: audit + continue-on-error: true + run: | + set -o pipefail + cargo audit | tee /tmp/cargo-audit.txt + + - name: Prepare issue body + if: ${{ steps.audit.outcome != 'success' && github.event_name == 'schedule' }} + run: | + { + echo "## \`cargo audit\` found advisories" + echo + echo '```' + cat /tmp/cargo-audit.txt + echo '```' + echo + echo "---" + echo "*This issue was automatically created by the cargo-audit workflow.*" + } >/tmp/cargo-audit-issue.md + + - name: Create issue + if: ${{ steps.audit.outcome != 'success' && github.event_name == 'schedule' }} + uses: XRPLF/actions/create-issue@2b8bc36af85b88bca0dd7bfac2e2dc05f94ad712 + with: + title: "cargo audit found vulnerabilities" + body_file: /tmp/cargo-audit-issue.md + labels: "Bug,Security" + + - name: Fail if advisories were found + if: ${{ steps.audit.outcome != 'success' }} + run: | + echo "cargo audit found advisories!" + cat /tmp/cargo-audit.txt + exit 1 diff --git a/.github/workflows/check-tools.yml b/.github/workflows/check-tools.yml index 99dddd7d96..c7a00e8b49 100644 --- a/.github/workflows/check-tools.yml +++ b/.github/workflows/check-tools.yml @@ -79,7 +79,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Prepare runner - uses: XRPLF/actions/prepare-runner@c00c22ada3bd6bcda48fcb0d62fbbab49fec8a0f + uses: XRPLF/actions/prepare-runner@51af40f99ea91a08c3528ddf16d98132dcc7e63c with: enable_ccache: false diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml index a8209ac16f..933c7b8a54 100644 --- a/.github/workflows/on-pr.yml +++ b/.github/workflows/on-pr.yml @@ -86,6 +86,7 @@ jobs: .github/workflows/reusable-check-autogen.yml .github/workflows/reusable-clang-tidy.yml .github/workflows/reusable-package.yml + .github/workflows/reusable-rust.yml .github/workflows/reusable-strategy-matrix.yml .github/workflows/reusable-test.yml .github/workflows/reusable-upload-recipe.yml @@ -97,6 +98,7 @@ jobs: cfg/** cmake/** conan/** + crates/** external/** include/** src/** @@ -173,6 +175,13 @@ jobs: secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + rust: + needs: should-run + if: ${{ needs.should-run.outputs.go == 'true' }} + uses: ./.github/workflows/reusable-rust.yml + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + package: needs: [should-run, build-test] # Packaging consumes the debian/rhel release binaries, which are only built @@ -216,6 +225,7 @@ jobs: - check-rename - clang-tidy - build-test + - rust - package - upload-recipe - notify-clio diff --git a/.github/workflows/on-trigger.yml b/.github/workflows/on-trigger.yml index 1da0f47bc4..2099f5f739 100644 --- a/.github/workflows/on-trigger.yml +++ b/.github/workflows/on-trigger.yml @@ -24,6 +24,7 @@ on: - ".github/workflows/reusable-check-autogen.yml" - ".github/workflows/reusable-clang-tidy.yml" - ".github/workflows/reusable-package.yml" + - ".github/workflows/reusable-rust.yml" - ".github/workflows/reusable-strategy-matrix.yml" - ".github/workflows/reusable-test.yml" - ".github/workflows/reusable-upload-recipe.yml" @@ -35,6 +36,7 @@ on: - "cfg/**" - "cmake/**" - "conan/**" + - "crates/**" - "external/**" - "include/**" - "src/**" @@ -101,6 +103,11 @@ jobs: secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + rust: + uses: ./.github/workflows/reusable-rust.yml + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + upload-recipe: needs: build-test # Only run when pushing to the develop branch. diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 6e973a251d..3b863f2b33 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -47,7 +47,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Prepare runner - uses: XRPLF/actions/prepare-runner@c00c22ada3bd6bcda48fcb0d62fbbab49fec8a0f + uses: XRPLF/actions/prepare-runner@51af40f99ea91a08c3528ddf16d98132dcc7e63c with: enable_ccache: false diff --git a/.github/workflows/reusable-build-test-config.yml b/.github/workflows/reusable-build-test-config.yml index 94d0706e70..89bfc7463b 100644 --- a/.github/workflows/reusable-build-test-config.yml +++ b/.github/workflows/reusable-build-test-config.yml @@ -129,7 +129,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Prepare runner - uses: XRPLF/actions/prepare-runner@c00c22ada3bd6bcda48fcb0d62fbbab49fec8a0f + uses: XRPLF/actions/prepare-runner@51af40f99ea91a08c3528ddf16d98132dcc7e63c with: enable_ccache: ${{ inputs.ccache_enabled }} @@ -162,6 +162,19 @@ jobs: with: compiler: ${{ inputs.compiler }} + - name: Use cargo artifacts cache + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + cache-directories: ${{ env.BUILD_DIR }}/corrosion + key: ${{ inputs.config_name }} + save-if: ${{ github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/heads/release') }} + # two workspaces here because build artifacts are located in 2 places: + # - crates/target when cargo is called directly + # - build/cargo when cargo is called by cmake + workspaces: | + crates + crates -> ${{ runner.os == 'Windows' && format('../{0}/x64/{1}/cargo', env.BUILD_DIR, inputs.build_type) || format('../{0}/cargo', env.BUILD_DIR) }} + # `setup-nix-env` already did this for the Nix toolchain. - name: Setup Conan if: ${{ inputs.toolchain != 'nix' }} @@ -357,6 +370,11 @@ jobs: LD_PRELOAD="$PRELOAD" ./xrpld --unittest --unittest-jobs "${BUILD_NPROC}" 2>&1 | tee "${GITHUB_WORKSPACE}/unittest.log" + - name: Run Rust tests + if: ${{ !inputs.build_only }} + working-directory: crates + run: cargo nextest run --workspace --all-features --locked --no-tests=warn + # 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 there is nothing to gain from repeating it @@ -428,6 +446,7 @@ jobs: disable_telem: true fail_ci_if_error: true files: ${{ env.BUILD_DIR }}/coverage.xml + flags: cpp plugins: noop token: ${{ secrets.CODECOV_TOKEN }} verbose: true diff --git a/.github/workflows/reusable-clang-tidy.yml b/.github/workflows/reusable-clang-tidy.yml index 2049b1ce55..8dd1af9d99 100644 --- a/.github/workflows/reusable-clang-tidy.yml +++ b/.github/workflows/reusable-clang-tidy.yml @@ -43,7 +43,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Prepare runner - uses: XRPLF/actions/prepare-runner@c00c22ada3bd6bcda48fcb0d62fbbab49fec8a0f + uses: XRPLF/actions/prepare-runner@51af40f99ea91a08c3528ddf16d98132dcc7e63c with: enable_ccache: false @@ -59,6 +59,13 @@ jobs: with: compiler: ${{ env.COMPILER }} + - name: Use cargo artifacts cache + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + cache-directories: ${{ env.BUILD_DIR }}/corrosion + save-if: ${{ github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/heads/release') }} + workspaces: crates -> ../${{ env.BUILD_DIR }}/cargo + - name: Setup Conan uses: ./.github/actions/setup-conan @@ -80,13 +87,13 @@ jobs: -Dwerr=ON \ -Dxrpld=ON \ -Dverify_headers=ON \ + -Drust=ON \ .. - # clang-tidy needs headers generated from proto files - - name: Build libxrpl.libpb + - name: Build clang-tidy prerequisites working-directory: ${{ env.BUILD_DIR }} run: | - ninja -j ${{ steps.nproc.outputs.nproc }} xrpl.libpb + ninja -j ${{ steps.nproc.outputs.nproc }} tidy_prerequisites - name: Run clang tidy id: run_clang_tidy diff --git a/.github/workflows/reusable-rust.yml b/.github/workflows/reusable-rust.yml new file mode 100644 index 0000000000..e9d281c692 --- /dev/null +++ b/.github/workflows/reusable-rust.yml @@ -0,0 +1,86 @@ +# Clippy, coverage and documentation for the Rust crates in crates/. Each runs +# as an independent job on a GitHub-hosted runner, but inside the same container +# image used to build the crates in the C++/Corrosion path, so the toolchain +# (and therefore the lints, coverage instrumentation and the cargo cache) matches +# what production builds use. +# +# Rust unit tests are deliberately NOT run here. They run as part of the C++ +# build (reusable-build-test-config.yml), which already compiles the crates on a +# self-hosted runner, so there is no need to provision a toolchain again. +name: Rust + +on: + workflow_call: + secrets: + CODECOV_TOKEN: + description: "The Codecov token to use for uploading coverage reports." + required: true + +defaults: + run: + shell: bash + working-directory: crates + +permissions: + contents: read + +jobs: + clippy: + runs-on: ubuntu-latest + container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-a0074f8 + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Use cargo artifacts cache + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + workspaces: crates + + - name: Run clippy + run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings + + coverage: + runs-on: ubuntu-latest + container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-a0074f8 + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Use cargo artifacts cache + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + workspaces: crates + + - name: Generate coverage report + run: cargo llvm-cov nextest --workspace --all-features --locked --no-tests=warn --lcov --output-path lcov.info + + - name: Upload coverage report + if: ${{ github.repository == 'XRPLF/rippled' }} + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + with: + disable_search: true + disable_telem: true + fail_ci_if_error: true + files: crates/lcov.info + flags: rust + plugins: noop + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true + + doc: + runs-on: ubuntu-latest + container: ghcr.io/xrplf/xrpld/nix-ubuntu:sha-a0074f8 + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Use cargo artifacts cache + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + workspaces: crates + + - name: Build documentation + env: + RUSTDOCFLAGS: "-D warnings" + run: cargo doc --workspace --no-deps --all-features --locked diff --git a/.gitignore b/.gitignore index 13b59a7e2c..c5af8eb7b4 100644 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,6 @@ target/ # clangd cache /.cache + +# Rust build directory +crates/target diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d339cb29ed..e5e69759fd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -62,6 +62,15 @@ repos: types_or: [c++, c, proto] exclude: ^include/xrpl/protocol_autogen/(transactions|ledger_entries)/ + - repo: local + hooks: + - id: cargo-fmt + name: cargo fmt + entry: cargo fmt --manifest-path crates/Cargo.toml --all + language: system + types: [rust] + pass_filenames: false # rustfmt formats the whole workspace + - repo: https://github.com/BlankSpruce/gersemi-pre-commit rev: e98930bdc210d3387007f9252d8c1694ea7e410f # frozen: 0.27.7 hooks: diff --git a/BUILD.md b/BUILD.md index ae2e69bb97..e98d204d0b 100644 --- a/BUILD.md +++ b/BUILD.md @@ -304,6 +304,7 @@ See [Sanitizers docs](./docs/build/sanitizers.md) for more details. | ---------------- | ------------- | ----------------------------------------------------------------------------- | | `assert` | OFF | Force enabling assertions. | | `coverage` | OFF | Prepare the coverage report. | +| `rust` | OFF | Build the Rust crates and the C++ code that depends on them. | | `tests` | OFF | Build tests. | | `unity` | OFF | Configure a unity build. | | `verify_headers` | ON | Make the `verify-headers` target available to compile each header on its own. | @@ -316,6 +317,30 @@ memory) since they concatenate sources into fewer translation units. Non-unity builds may be faster for incremental builds, and can be helpful for detecting `#include` omissions. +### Rust crates + +The Rust crates in `crates/` are only part of the build when `rust` is ON. With +`-Drust=OFF` (the default) the `crates` directory is not added to the build, no +cxxbridge bindings are generated, and the C++ tests that exercise the Rust +interop are not compiled — so no Rust toolchain is needed. CI builds always pass +`-Drust=ON`. + +With `-Drust=ON` you need one extra dependency: a Rust toolchain (`cargo`, +`rustc`) matching the channel pinned in +[`rust-toolchain.toml`](./rust-toolchain.toml), which compiles the crates and +generates the cxxbridge bindings. It is provided by the +[Nix development shell](./docs/build/nix.md), so `-Drust=ON` works there without +any extra setup; otherwise install it as described in +[Rust](./docs/build/environment.md#rust). + +The crates also have their own Rust unit tests. Those are run with `cargo` and +need only the Rust toolchain, independently of CMake and of the `rust` option +(CI runs them with `cargo nextest`): + +```bash +cargo test --manifest-path crates/Cargo.toml --workspace +``` + ### Verifying headers The regular build only compiles `.cpp` files, so a header is only ever checked diff --git a/CMakeLists.txt b/CMakeLists.txt index efe7396661..a324cecedc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -158,7 +158,13 @@ if(coverage) include(XrplCov) endif() +add_custom_target(tidy_prerequisites) + +if(rust) + add_subdirectory(crates) +endif() include(XrplCore) + include(XrplProtocolAutogen) include(XrplInstall) include(XrplValidatorKeys) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fc385cf6ed..35309a9824 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -225,8 +225,9 @@ environment, so you don't need to install most of the individual tools yourself. The version of each hook sourced from an external repository (`clang-format`, `gersemi`, etc.) is pinned in that file, so running the hooks locally uses exactly the same versions as CI. A few `local` hooks — most notably -`clang-tidy` — run tools from your own environment; see -[Installing clang-tidy](#installing-clang-tidy) for how to get those. +`clang-tidy` and `cargo fmt` — run tools from your own environment; see +[Installing clang-tidy](#installing-clang-tidy) and +[Rust](./docs/build/environment.md#rust) for how to get those. To get started, install `pre-commit` and enable the git hook scripts: @@ -255,6 +256,7 @@ The hooks configured in this repository include, among others: - `clang-tidy` — C++ static analysis (see [Clang-tidy](#clang-tidy)); opt in with `TIDY=1` - `fix-include-style`, `fix-pragma-once`, `check-doxygen-style` — C++ hygiene - `gersemi` — CMake formatting +- `cargo fmt` — Rust formatting for the crates in `crates/` - `prettier`, `black`, `shfmt` — formatting for JavaScript/JSON/Markdown, Python, and shell - `cspell` — spell checking @@ -319,7 +321,11 @@ See the [environment setup guide](./docs/build/environment.md#clang-tidy) for ho ### Running clang-tidy locally -Before running clang-tidy, you must build the project to generate required files (particularly protobuf headers). Refer to [`BUILD.md`](./BUILD.md) for build instructions. +Before running clang-tidy, you must generate the files it depends on (protobuf headers, and, when the project is configured with `-Drust=ON`, the cxxbridge headers from the Rust crates). Configure the project as described in [`BUILD.md`](./BUILD.md), then build the `tidy_prerequisites` target, which generates all of them: + +```bash +cmake --build build --target tidy_prerequisites +``` #### Via pre-commit (recommended) diff --git a/README.md b/README.md index 88c7943ebb..a0d30ef68b 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ Here are some good places to start learning the source code: | `./docs` | Source documentation files and doxygen config. | | `./cfg` | Example configuration files. | | `./src` | Source code. | +| `./crates` | Rust source code. | Some of the directories under `src` are external repositories included using git-subtree. See those directories' README files for more details. diff --git a/cmake/XrplCore.cmake b/cmake/XrplCore.cmake index a3e08145d5..f3951d4eac 100644 --- a/cmake/XrplCore.cmake +++ b/cmake/XrplCore.cmake @@ -51,6 +51,8 @@ target_compile_options( target_link_libraries(xrpl.libpb PUBLIC protobuf::libprotobuf gRPC::grpc++) +add_dependencies(tidy_prerequisites xrpl.libpb) + # TODO: Clean up the number of library targets later. add_library(xrpl.imports.main INTERFACE) diff --git a/cmake/XrplSettings.cmake b/cmake/XrplSettings.cmake index be9bf1fda2..58b902baa1 100644 --- a/cmake/XrplSettings.cmake +++ b/cmake/XrplSettings.cmake @@ -32,6 +32,11 @@ endif() option(benchmark "Build benchmarks" ON) +# When OFF, the crates directory is not added to the build at all: no Rust +# toolchain is required, no cxxbridge bindings are generated, and the C++ tests +# that consume those bindings are left out of the build tree. +option(rust "Build the Rust crates and the C++ code that depends on them" OFF) + # Enabled by default so every header is compiled on its own as the main file of # its own compile_commands.json entry - this is what lets clang-tidy (and clangd # and IDEs) analyse a header's own includes directly. The per-header objects are diff --git a/conan.lock b/conan.lock index 5b01ffbf76..176f0b27cb 100644 --- a/conan.lock +++ b/conan.lock @@ -23,6 +23,7 @@ "fast_float/8.2.10#f6f28d6bb22112078e7dbda611caf681%1782494504.298", "ed25519/2015.03#ae761bdc52730a843f0809bdf6c1b1f6%1782307148.15562", "date/3.0.4#862e11e80030356b53c2c38599ceb32b%1782392402.538492", + "corrosion/0.6.1#bfa292df0a957bc70a450ff316cd9435%1786119416.131296", "c-ares/1.34.6#545240bb1c40e2cacd4362d6b8967650%1782392402.681654", "bzip2/1.0.8#c470882369c2d95c5c77e970c0c7e321%1782392402.296732", "boost/1.91.0#ea540ca2133d831b560036aa24dece3c%1782392419.475605", diff --git a/conanfile.py b/conanfile.py index 2742405b6c..0683a3779f 100644 --- a/conanfile.py +++ b/conanfile.py @@ -28,6 +28,7 @@ class Xrpl(ConanFile): } requires = [ + "corrosion/0.6.1", "ed25519/2015.03", "fast_float/8.2.10", "grpc/1.81.1", diff --git a/crates/.cargo/config.toml b/crates/.cargo/config.toml new file mode 100644 index 0000000000..fc29aa80f7 --- /dev/null +++ b/crates/.cargo/config.toml @@ -0,0 +1,17 @@ +# The Rust static libraries are linked into C++ targets, so the runtime linkage +# here has to match what the C++ build uses (see cmake/XrplCompiler.cmake). +# +# macOS needs nothing: AppleClang cannot link libgcc/libc++ statically, so the +# C++ build skips those flags on Apple as well. + +# Both amd64 and arm64 Linux builds link libgcc statically. This only affects +# links that rustc itself drives (`cargo test` binaries and the like) — the +# `staticlib` crates consumed by CMake are archived, not linked, so rustc +# silently ignores link args for them. Keeping libgcc_s.so.1 off the xrpld link +# line is handled in crates/CMakeLists.txt instead. +[target.'cfg(target_os = "linux")'] +rustflags = ["-C", "link-args=-static-libgcc"] + +# Windows builds use the static MSVC runtime. +[target.'cfg(windows)'] +rustflags = ["-C", "target-feature=+crt-static"] diff --git a/crates/CMakeLists.txt b/crates/CMakeLists.txt new file mode 100644 index 0000000000..3f83045cdb --- /dev/null +++ b/crates/CMakeLists.txt @@ -0,0 +1,104 @@ +find_package(Corrosion REQUIRED) + +corrosion_import_crate(MANIFEST_PATH ${CMAKE_CURRENT_SOURCE_DIR}/Cargo.toml) + +# The generated C++ lands in the build tree, so put a .clang-tidy next to it to +# keep clang-tidy from analyzing code we don't own. +configure_file( + generated.clang-tidy + "${CMAKE_CURRENT_BINARY_DIR}/.clang-tidy" + COPYONLY +) + +add_custom_target(xrpl_crates) +add_dependencies(tidy_prerequisites xrpl_crates) + +# On macOS, ld warns `ignoring duplicate libraries` when linking a crate. +# Corrosion is the source of both duplicates it names: +# +# * The crate archive and its cxxbridge archive, because +# `corrosion_add_cxxbridge` makes the two depend on each other, and CMake +# repeats a static library cycle on the link line so single-pass linkers can +# resolve it. (LINK_INTERFACE_MULTIPLICITY can only raise that count.) +# * `-lSystem`, which Corrosion copies from rustc's `native-static-libs` even +# though the compiler driver always links libSystem. +# +# ld needs neither: it resolves the cycle from one copy of each archive and +# links libSystem once. So silence the warning rather than rewrite Corrosion's +# link interface, which the cycle is also part of. The option itself is old — +# Xcode 15 is only where the warning became the default — and the check below +# leaves it out on a linker that does not know it. +if(is_macos) + include(CheckLinkerFlag) + check_linker_flag( + CXX + -Wl,-no_warn_duplicate_libraries + have_no_warn_duplicate_libraries + ) +endif() + +function(_unlink_libgcc_s crate) + if(NOT (is_linux AND static)) + return() + endif() + + # Corrosion exposes a crate's staticlib as an imported `-static` + # target and puts the native libs in its INTERFACE_LINK_LIBRARIES. If either + # of those changes, warn instead of silently letting libgcc_s.so.1 return. + set(imported "${crate}-static") + if(NOT TARGET ${imported}) + message( + FATAL_ERROR + "Corrosion did not create the imported target '${imported}', so " + "libgcc_s cannot be removed from the link interface of '${crate}'. " + "xrpld will link libgcc_s.so.1 dynamically. Check where Corrosion " + "${CORROSION_VERSION} now records `native-static-libs`." + ) + return() + endif() + + get_target_property(libs ${imported} INTERFACE_LINK_LIBRARIES) + if(NOT "gcc_s" IN_LIST libs) + message( + WARNING + "'gcc_s' was not in the link interface of '${imported}' as " + "expected. If the Rust toolchain stopped reporting it this " + "workaround is obsolete and can be deleted; otherwise xrpld may " + "link libgcc_s.so.1 dynamically. Verify with: " + "objdump -p xrpld | grep NEEDED" + ) + return() + endif() + + list(REMOVE_ITEM libs gcc_s) + set_property(TARGET ${imported} PROPERTY INTERFACE_LINK_LIBRARIES ${libs}) +endfunction() + +function(add_xrpl_crate name) + cmake_parse_arguments(ARG "" "CRATE" "FILES" ${ARGN}) + _unlink_libgcc_s(${ARG_CRATE}) + # `cc` picks its runtime flag from `crt-static` alone, so it compiles a + # crate's C++ with `-MT`; Debug needs `-MTd` (to match cmake/XrplCompiler.cmake). + if(is_msvc) + corrosion_set_env_vars( + ${ARG_CRATE} + "$<$:CXXFLAGS=-MTd>" + ) + endif() + corrosion_add_cxxbridge(${name}_cxxbridge CRATE ${ARG_CRATE} FILES + ${ARG_FILES} + ) + # Generated cxxbridge headers don't exist at configure time; CMake 3.28+ + # validates INTERFACE_SOURCES on consuming targets. Clear it to skip the + # existence check — build-time ordering is enforced by the custom commands. + set_target_properties(${name}_cxxbridge PROPERTIES INTERFACE_SOURCES "") + if(have_no_warn_duplicate_libraries) + target_link_options( + ${name}_cxxbridge + INTERFACE -Wl,-no_warn_duplicate_libraries + ) + endif() + add_dependencies(xrpl_crates ${name}_cxxbridge) +endfunction() + +add_xrpl_crate(rs_hello_world CRATE rs_hello_world FILES lib.rs) diff --git a/crates/Cargo.lock b/crates/Cargo.lock new file mode 100644 index 0000000000..bc38558c16 --- /dev/null +++ b/crates/Cargo.lock @@ -0,0 +1,301 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "cc" +version = "1.2.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "cxx" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe442a792c7c736eea18b32a7f8a3b63cf8aafabda6760042dc2fdeda456291" +dependencies = [ + "cc", + "cxx-build", + "cxxbridge-cmd", + "cxxbridge-flags", + "cxxbridge-macro", + "foldhash", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3184a94384c663718698311a78a51ac00c484c10b4eeac06fb0a068c5f64fa2" +dependencies = [ + "cc", + "codespan-reporting", + "indexmap", + "proc-macro2", + "quote", + "scratch", + "syn 3.0.3", +] + +[[package]] +name = "cxxbridge-cmd" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0148d8fd1199329ddf1d157a5e134e51ceff37c6a7ddd38615c399d81cb05d8d" +dependencies = [ + "clap", + "codespan-reporting", + "indexmap", + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52850339faed2eaadd24e286dc1d8268cc6f8a7bd9524d713adc9099566b4c89" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.198" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c77c856545d886c9bd5215409ebb63b925e262135248b50c79e5a5f194ee47c" +dependencies = [ + "indexmap", + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82" +dependencies = [ + "cc", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rs-hello_world" +version = "0.1.0" +dependencies = [ + "cxx", +] + +[[package]] +name = "scratch" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/crates/Cargo.toml b/crates/Cargo.toml new file mode 100644 index 0000000000..0bb0e9c550 --- /dev/null +++ b/crates/Cargo.toml @@ -0,0 +1,15 @@ +[workspace] +members = ["hello_world"] +resolver = "3" + +[workspace.dependencies] +cxx = { version = "1.0.198", features = ["c++20"] } + +[workspace.package] +edition = "2024" + +[profile.release] +opt-level = 3 +overflow-checks = true +lto = true +debug = true diff --git a/crates/generated.clang-tidy b/crates/generated.clang-tidy new file mode 100644 index 0000000000..8e2202d44a --- /dev/null +++ b/crates/generated.clang-tidy @@ -0,0 +1,10 @@ +--- +# Neutralizes clang-tidy for the corrosion/cxxbridge-generated C++. Copied into +# the crates build directory by crates/CMakeLists.txt, next to the generated +# sources, so clang-tidy picks it up instead of the top-level configuration. +# +# One check is kept enabled to avoid clang-tidy's "no checks enabled" error. +Checks: "-*,google-readability-todo" +WarningsAsErrors: "" +HeaderFilterRegex: "" +InheritParentConfig: false diff --git a/crates/hello_world/Cargo.toml b/crates/hello_world/Cargo.toml new file mode 100644 index 0000000000..2e5a329c9a --- /dev/null +++ b/crates/hello_world/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "rs-hello_world" +version = "0.1.0" +edition.workspace = true + +[lib] +crate-type = ["staticlib"] + +[dependencies] +cxx.workspace = true diff --git a/crates/hello_world/src/lib.rs b/crates/hello_world/src/lib.rs new file mode 100644 index 0000000000..b1cb121fa0 --- /dev/null +++ b/crates/hello_world/src/lib.rs @@ -0,0 +1,10 @@ +#[cxx::bridge(namespace = "rs::hello_world")] +mod ffi { + extern "Rust" { + fn hello_world() -> String; + } +} + +pub fn hello_world() -> String { + "hello_world".to_string() +} diff --git a/docs/build/environment.md b/docs/build/environment.md index 5616f32f37..51580b12a5 100644 --- a/docs/build/environment.md +++ b/docs/build/environment.md @@ -46,6 +46,9 @@ Besides a compiler, building `xrpld` requires: On Linux and macOS, the [Nix development shell](./nix.md) provides all of them (see below). On Windows they have to be installed manually. +Building with `-Drust=ON` additionally requires a Rust toolchain, see +[Rust](#rust). A default build does not, so it is not in the table above. + Once they are in place, verify that everything is installed and runnable with: ```bash @@ -121,6 +124,25 @@ manually: - [Git for Windows](https://git-scm.com/download/win) - Python, Conan, and CMake, at the versions listed in [Required tools](#required-tools). +- a [Rust toolchain](https://rustup.rs) — only needed to build with + `-Drust=ON`, see [Rust](#rust) + +## Rust + +The repository contains a Rust workspace in [`crates/`](../../crates), whose +crates are exposed to C++ through [cxx](https://cxx.rs) bindings. It is **not** +part of a default build: the CMake `rust` option is OFF by default, and with it +off no Rust toolchain is needed. It is only required when configuring with +`-Drust=ON` (which is what CI does), see [Options](../../BUILD.md#options). + +The toolchain (`cargo`, `rustc`) is pinned to the channel in +[`rust-toolchain.toml`](../../rust-toolchain.toml) at the repository root. If +you install Rust with [rustup](https://rustup.rs), that file is picked up +automatically, and `cargo`/`rustc` in the repository will use the pinned +version. + +Everything else the Rust build needs on the CMake side comes from Conan along +with the rest of the dependencies, so there is nothing further to install. ## Clang-tidy diff --git a/docs/build/nix.md b/docs/build/nix.md index 4c082afb28..0b701b39f3 100644 --- a/docs/build/nix.md +++ b/docs/build/nix.md @@ -128,6 +128,12 @@ Coverage builds (`-Dcoverage=ON`) work in the `gcc` shell (and `gcc-plain` on Li each ships a `gcov` matching its compiler, since Nix's cc-wrapper does not expose one. The `clang` shells do not include `llvm-cov`, so use a `gcc` shell for coverage. +Builds of the Rust crates (`-Drust=ON`) also work out of the box: every shell +provides the Rust toolchain pinned in +[`rust-toolchain.toml`](../../rust-toolchain.toml) (see +[Rust](./environment.md#rust)), plus the `cargo-audit`, `cargo-llvm-cov` and +`cargo-nextest` plugins. + ## Conan configuration The shell runs [`conan/init.sh`](../../conan/init.sh) on entry, so diff --git a/src/tests/libxrpl/CMakeLists.txt b/src/tests/libxrpl/CMakeLists.txt index 5e4cda243a..9cbfb8ca10 100644 --- a/src/tests/libxrpl/CMakeLists.txt +++ b/src/tests/libxrpl/CMakeLists.txt @@ -43,6 +43,9 @@ set(test_modules if(NOT WIN32) list(APPEND test_modules net) endif() +if(rust) + target_link_libraries(xrpl_tests PRIVATE rs_hello_world_cxxbridge) +endif() foreach(module IN LISTS test_modules) # Append the module's sources (${module}/*.cpp and ${module}.cpp, if any). @@ -52,6 +55,12 @@ foreach(module IN LISTS test_modules) "${CMAKE_CURRENT_SOURCE_DIR}/${module}/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/${module}.cpp" ) + if(NOT rust) + # Tests of the Rust interop include generated cxxbridge headers, which + # do not exist without the crates, so keep them out of the build tree + # entirely. They are named `Rust.cpp`. + list(FILTER sources EXCLUDE REGEX "/Rust[^/]*\\.cpp$") + endif() target_sources(xrpl_tests PRIVATE ${sources}) # Expose the module's private headers under their canonical include path. diff --git a/src/tests/libxrpl/basics/RustInterop.cpp b/src/tests/libxrpl/basics/RustInterop.cpp new file mode 100644 index 0000000000..8a6ad8a4ed --- /dev/null +++ b/src/tests/libxrpl/basics/RustInterop.cpp @@ -0,0 +1,9 @@ +#include +#include + +#include + +TEST(RustInteropTest, hello_world) +{ + EXPECT_EQ(std::string(rs::hello_world::hello_world()), "hello_world"); +}