diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 83185d9..806bea7 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -64,9 +64,24 @@ jobs: # Setup language-specific dependencies BEFORE CodeQL init for proper tracing setup - name: Setup Rust - if: matrix.language == 'rust' || matrix.language == 'c-cpp' uses: ./.github/actions/toolchains/rust + - name: Cache cargo + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + with: + shared-key: ${{ runner.os }}-regorus + + - name: Fetch workspace dependencies + run: cargo fetch --locked + + - name: Fetch FFI crate dependencies + if: matrix.language == 'c-cpp' || matrix.language == 'go' || matrix.language == 'csharp' + run: cargo fetch --locked --manifest-path bindings/ffi/Cargo.toml + + - name: Fetch Java crate dependencies + if: matrix.language == 'java-kotlin' + run: cargo fetch --locked --manifest-path bindings/java/Cargo.toml + - name: Setup Python if: matrix.language == 'python' uses: actions/setup-python@v5 @@ -92,6 +107,10 @@ jobs: with: global-json-file: ./bindings/csharp/global.json + - name: Invoke dotnet directly + if: matrix.language == 'csharp' + run: dotnet --info + - name: Setup Node.js if: matrix.language == 'javascript-typescript' uses: actions/setup-node@v4 @@ -132,79 +151,39 @@ jobs: cargo install wasm-pack # Manual build steps for different languages - - name: Build C/C++ FFI bindings + - name: Build C/C++ bindings via xtask if: matrix.language == 'c-cpp' - working-directory: ${{ matrix.working-directory }} run: | - # Build FFI library in no_std mode for embedded/constrained environments - cargo build --release --locked --features "ast,coverage,regorus/opa-no-std" --no-default-features + cargo xtask test-c --release --frozen + cargo xtask test-cpp --release --frozen --skip-ffi + cargo xtask test-c-no-std --release --frozen --skip-ffi - # Build the Rust FFI library that provides C-compatible interface - cargo build --release --locked - - # Build C bindings using CMake - cd ../c - mkdir -p build - cd build - cmake .. - make - - # Build C++ bindings using CMake - cd ../../cpp - mkdir -p build - cd build - cmake .. - make - - - name: Build Java bindings + - name: Build Java bindings via xtask if: matrix.language == 'java-kotlin' - working-directory: ${{ matrix.working-directory }} - run: | - # Build the Rust JNI library that provides Java-compatible interface - cargo fetch - cargo build --release --locked - # Compile Java source and create JAR package with Maven - mvn package + run: cargo xtask test-java --release --frozen - - name: Build Go bindings + - name: Build Go bindings via xtask if: matrix.language == 'go' - working-directory: ${{ matrix.working-directory }} - run: | - # Build the FFI library that Go bindings depend on via CGO - cd ../ffi - cargo fetch - cargo build --release --locked - cd ../go - # Download Go dependencies - go mod tidy - # Set up environment for CGO linking to Rust FFI library - export CGO_ENABLED=1 - export LD_LIBRARY_PATH="$(pwd)/../ffi/target/release:$LD_LIBRARY_PATH" - # Build Go packages with verbose output for CodeQL tracing - go build -v ./pkg/regorus - go build -v -o regorus_test . + run: cargo xtask test-go --release --frozen - - name: Build C# bindings + - name: Build C# bindings manually if: matrix.language == 'csharp' working-directory: ${{ matrix.working-directory }} run: | + # Temporary workaround: CodeQL's tracer replaces dotnet with a missing shim when cargo xtask test-csharp runs, + # so invoke dotnet directly here until the upstream fix lands. + # Ideal command once fixed: cargo xtask test-csharp --release # Build the FFI library that C# bindings access via P/Invoke cd ../ffi - cargo fetch cargo build --release --locked cd ../csharp # Restore NuGet packages and build .NET assemblies in release mode - # Build the main Regorus library project only (tests require packaged version) dotnet restore Regorus/Regorus.csproj dotnet build Regorus/Regorus.csproj --no-restore /p:Configuration=Release /p:IgnoreMissingArtifacts=true - - name: Build WASM bindings + - name: Build WASM bindings via xtask if: matrix.language == 'javascript-typescript' - working-directory: ${{ matrix.working-directory }} - run: | - # Build WebAssembly module with wasm-pack for Node.js target - cargo fetch - wasm-pack build --target nodejs --release + run: cargo xtask build-wasm --release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/pr-extensions.yml b/.github/workflows/pr-extensions.yml index c964d52..1c81fb2 100644 --- a/.github/workflows/pr-extensions.yml +++ b/.github/workflows/pr-extensions.yml @@ -21,18 +21,15 @@ jobs: - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - name: Setup Rust toolchain uses: ./.github/actions/toolchains/rust - - name: Build only std - run: cargo build -r --example regorus --no-default-features --features "std,rego-extensions" - - name: Doc Tests - run: cargo test -r --doc --features rego-extensions - - name: Run tests - run: cargo test -r --features rego-extensions - - name: Run example - run: cargo run --example regorus --features rego-extensions -- eval -d examples/server/allowed_server.rego -i examples/server/input.json data.example - - name: Run tests (ACI) - run: cargo test -r --test aci --features rego-extensions - - name: Run tests (KATA) - run: cargo test -r --test kata --features rego-extensions - - name: Run tests (OPA Conformance) + - name: Cache cargo + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + with: + shared-key: ${{ runner.os }}-regorus + - name: Fetch dependencies + run: cargo fetch --locked + - name: Run rego extensions CI suite run: >- - cargo test -r --test opa --features opa-testutil,serde_json/arbitrary_precision,rego-extensions -- $(tr '\n' ' ' < tests/opa.passing) + cargo xtask ci-release --frozen --features rego-extensions + --skip-all-features-build --skip-no-default-features-tests + --skip-azure-policy --skip-azure-rbac + --opa-features "opa-testutil,serde_json/arbitrary_precision,rego-extensions" diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 065fc56..e86cd48 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -21,33 +21,11 @@ jobs: - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - name: Setup Rust toolchain uses: ./.github/actions/toolchains/rust - - name: Format Check - run: cargo fmt --check - - name: Fetch - run: cargo fetch - - name: Build (all features) - run: cargo build -r --all-features --frozen - - name: Build - run: cargo build -r --frozen - - name: Test no_std - run: cargo test -r --no-default-features --frozen - - name: Build only std - run: cargo build -r --example regorus --no-default-features --features "std" --frozen - - name: Doc Tests - run: cargo test -r --doc --frozen - - name: Run tests - run: cargo test -r --frozen - - name: Run example - run: cargo run --example regorus --frozen -- eval -d examples/server/allowed_server.rego -i examples/server/input.json data.example - - name: Run tests (ACI) - run: cargo test -r --test aci --frozen - - name: Run tests (KATA) - run: cargo test -r --test kata --frozen - - name: Run tests (OPA Conformance) - run: >- - cargo test -r --test opa --frozen --features opa-testutil,serde_json/arbitrary_precision -- $(tr '\n' ' ' < tests/opa.passing) - - name: Run tests (Azure Policy) - run: >- - cargo test --frozen --features azure_policy - - name: Run tests (Azure RBAC) - run: cargo test -r --frozen --features azure-rbac + - name: Cache cargo + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + with: + shared-key: ${{ runner.os }}-regorus + - name: Fetch dependencies + run: cargo fetch --locked + - name: Run release CI suite + run: cargo xtask ci-release --frozen diff --git a/.github/workflows/publish-java.yml b/.github/workflows/publish-java.yml index b983302..53c88bc 100644 --- a/.github/workflows/publish-java.yml +++ b/.github/workflows/publish-java.yml @@ -48,7 +48,8 @@ jobs: python-version: "3.11" - if: ${{ matrix.build_cmd == 'zigbuild' }} run: pip install cargo-zigbuild - - run: cargo fetch + - run: cargo fetch --locked + - run: cargo fetch --locked --manifest-path bindings/java/Cargo.toml - run: cargo ${{ matrix.build_cmd || 'build' }} --release --frozen --target ${{ matrix.target }}${{ matrix.glibc && format('.{0}', matrix.glibc) || '' }} --manifest-path ./bindings/java/Cargo.toml - run: mkdir -p native/${{ matrix.target }} - run: mv target/${{ matrix.target }}/release/*.${{ matrix.extension }} ./native/${{ matrix.target }}/ diff --git a/.github/workflows/publish-python.yml b/.github/workflows/publish-python.yml index aa1fc5d..778348c 100644 --- a/.github/workflows/publish-python.yml +++ b/.github/workflows/publish-python.yml @@ -26,7 +26,7 @@ jobs: - name: Build Python extension run: | - cargo fetch + cargo fetch --locked cargo clippy --all-targets --no-deps -- -Dwarnings cargo build --release --target ${{ matrix.target }} --frozen working-directory: bindings/python @@ -59,7 +59,7 @@ jobs: - name: Build Python extension run: | - cargo fetch + cargo fetch --locked cargo clippy --all-targets --no-deps -- -Dwarnings cargo build --release --target ${{ matrix.host.target }} --frozen working-directory: bindings/python @@ -90,7 +90,7 @@ jobs: - name: Build Python extension run: | - cargo fetch + cargo fetch --locked cargo clippy --all-targets --no-deps -- -Dwarnings cargo build --release --target ${{ matrix.host.target }} --frozen working-directory: bindings/python diff --git a/.github/workflows/rust-clippy.yml b/.github/workflows/rust-clippy.yml index 3a4b73c..13ba0a9 100644 --- a/.github/workflows/rust-clippy.yml +++ b/.github/workflows/rust-clippy.yml @@ -34,20 +34,19 @@ jobs: - name: Setup Rust toolchain uses: ./.github/actions/toolchains/rust + - name: Cache cargo + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + with: + shared-key: ${{ runner.os }}-regorus - name: Install required cargo run: cargo install clippy-sarif sarif-fmt - name: Fetch - run: cargo fetch + run: cargo fetch --locked - name: Run rust-clippy - run: - cargo clippy - --all-features - --frozen - --message-format=json | clippy-sarif | tee rust-clippy-results.sarif | sarif-fmt - + run: cargo xtask clippy --sarif rust-clippy-results.sarif continue-on-error: true - name: Upload analysis results to GitHub diff --git a/.github/workflows/test-c-cpp.yml b/.github/workflows/test-c-cpp.yml index 5389ab3..d8ee86d 100644 --- a/.github/workflows/test-c-cpp.yml +++ b/.github/workflows/test-c-cpp.yml @@ -19,36 +19,24 @@ jobs: fetch-depth: 0 - uses: ./.github/actions/toolchains/rust + - name: Cache cargo + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + with: + shared-key: ${{ runner.os }}-regorus + - name: Fetch dependencies + run: cargo fetch --locked + + - name: Fetch FFI crate dependencies + run: cargo fetch --locked --manifest-path bindings/ffi/Cargo.toml - name: Setup gcc, g++, cmake, ninja run: sudo apt update && sudo apt install -y gcc g++ cmake ninja-build - - name: Workaround to ensure that regorus.h is generated - run: | - cargo fetch - cargo build -r --frozen - working-directory: ./bindings/ffi - - - name: Test c binding - run: | - mkdir bindings/c/build - cd bindings/c/build - cmake -G Ninja .. - ninja - ./regorus_test + - name: Test C binding via xtask + run: cargo xtask test-c --release --frozen - - name: Test c-nostd binding - run: | - mkdir bindings/c-nostd/build - cd bindings/c-nostd/build - cmake -G Ninja .. - ninja - ./regorus_test + - name: Test C (no-std) binding via xtask + run: cargo xtask test-c-nostd --release --frozen --skip-ffi - - name: Test cpp binding - run: | - mkdir bindings/cpp/build - cd bindings/cpp/build - cmake -G Ninja .. - ninja - ./regorus_test + - name: Test C++ binding via xtask + run: cargo xtask test-cpp --release --frozen --skip-ffi diff --git a/.github/workflows/test-csharp.yml b/.github/workflows/test-csharp.yml index 0aadf4c..34bb195 100644 --- a/.github/workflows/test-csharp.yml +++ b/.github/workflows/test-csharp.yml @@ -11,7 +11,7 @@ on: - cron: "0 8 * * *" env: - VersionSuffix: ${{ github.run_id }} + VersionSuffix: ${{ github.event_name == 'workflow_dispatch' && 'manualtrigger' || null }} jobs: build-ffi: @@ -42,22 +42,18 @@ jobs: with: fetch-depth: 0 - uses: ./.github/actions/toolchains/rust - - - name: Fetch crates - run: cargo fetch - working-directory: ./bindings/ffi + - name: Cache cargo + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + with: + shared-key: ${{ runner.os }}-regorus + - name: Fetch dependencies + run: cargo fetch --locked - - name: Check Regorus binding formatting - run: cargo fmt --check - working-directory: ./bindings/ffi + - name: Fetch FFI crate dependencies + run: cargo fetch --locked --manifest-path bindings/ffi/Cargo.toml --target ${{ matrix.runtime.target }} - - name: Check Clippy linting for Regorus binding - run: cargo clippy --frozen -- -D warnings - working-directory: ./bindings/ffi - - - name: Build Regorus binding - run: cargo build -r --target ${{ matrix.runtime.target }} --locked - working-directory: ./bindings/ffi + - name: Build Regorus FFI via xtask + run: cargo xtask build-ffi --release --target ${{ matrix.runtime.target }} - name: Upload regorus ffi shared library uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 @@ -68,7 +64,7 @@ jobs: if-no-files-found: error retention-days: 1 - build-nuget: + build-csharp: name: 'Build Regorus nuget' runs-on: ubuntu-latest needs: build-ffi @@ -77,12 +73,21 @@ jobs: uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 with: fetch-depth: 0 + - uses: ./.github/actions/toolchains/rust - uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 with: global-json-file: ./bindings/csharp/global.json - run: echo '${{ steps.stepid.outputs.dotnet-version }}' + - name: Cache cargo + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + with: + shared-key: ${{ runner.os }}-regorus + + - name: Fetch dependencies + run: cargo fetch --locked + - name: Download regorus ffi shared libraries uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: @@ -93,14 +98,8 @@ jobs: - name: Display regorus ffi artifacts run: ls -R ./bindings/csharp/Regorus/tmp - # Note that we need to supply the target folder within the folder where artifacts are downloaded. - - name: Build Regorus binding - run: dotnet build /p:Configuration=Release /p:RegorusFFIArtifactsDir=./tmp/bindings/ffi/target /p:VersionSuffix=${{ env.VersionSuffix }} - working-directory: ./bindings/csharp/Regorus - - - name: Pack - run: dotnet pack /p:RegorusFFIArtifactsDir=./tmp/bindings/ffi/target /p:VersionSuffix=${{ env.VersionSuffix }} - working-directory: ./bindings/csharp/Regorus + - name: Build Regorus nuget via xtask + run: cargo xtask build-csharp --release --clean --artifacts-dir ./bindings/csharp/Regorus/tmp/bindings/ffi/target --enforce-artifacts - name: Upload Regorus nuget uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 @@ -112,7 +111,7 @@ jobs: test-nuget: name: 'Test Regorus Nuget: (${{ matrix.runtime.target }})' - needs: build-nuget + needs: build-csharp runs-on: ${{ matrix.runtime.os }} strategy: # let us get failures from other jobs even if one fails @@ -131,46 +130,32 @@ jobs: with: fetch-depth: 0 + - uses: ./.github/actions/toolchains/rust + - uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0 with: global-json-file: ./bindings/csharp/global.json - run: echo '${{ steps.stepid.outputs.dotnet-version }}' + - name: Cache cargo + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + with: + shared-key: ${{ runner.os }}-regorus + + - name: Fetch dependencies + run: cargo fetch --locked + - name: Download regorus nuget uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: regorus-nuget - path: ./bindings/csharp/regorus-nuget/ + path: ./bindings/csharp/Regorus/bin/Release - - name: Restore Regorus.Tests - run: dotnet restore /p:UseLocalRegorus=false /p:VersionSuffix=${{ env.VersionSuffix }} /p:RestoreAdditionalProjectSources=../regorus-nuget - working-directory: ./bindings/csharp/Regorus.Tests + - name: Display regorus nuget + run: ls -R ./bindings/csharp/Regorus/bin/Release - - name: Run Regorus.Tests - run: dotnet test --no-restore /p:UseLocalRegorus=false /p:VersionSuffix=${{ env.VersionSuffix }} - working-directory: ./bindings/csharp/Regorus.Tests + - name: Run C# tests via xtask + run: cargo xtask test-csharp --release --clean --nuget-dir bindings/csharp/Regorus/bin/Release - - name: Restore TestApp - run: dotnet restore /p:VersionSuffix=${{ env.VersionSuffix }} /p:RestoreAdditionalProjectSources=../regorus-nuget - working-directory: ./bindings/csharp/TestApp - - - name: Build TestApp - run: dotnet build --no-restore - working-directory: ./bindings/csharp/TestApp - - - name: Run TestApp - run: dotnet run --no-build --framework net8.0 - working-directory: ./bindings/csharp/TestApp - - - name: Restore TargetExampleApp - run: dotnet restore /p:VersionSuffix=${{ env.VersionSuffix }} /p:RestoreAdditionalProjectSources=../regorus-nuget - working-directory: ./bindings/csharp/TargetExampleApp - - - name: Build TargetExampleApp - run: dotnet build --no-restore - working-directory: ./bindings/csharp/TargetExampleApp - - - name: Run TargetExampleApp - run: dotnet run --no-build --framework net8.0 - working-directory: ./bindings/csharp/TargetExampleApp + diff --git a/.github/workflows/test-ffi.yml b/.github/workflows/test-ffi.yml index 3d05de4..65755b6 100644 --- a/.github/workflows/test-ffi.yml +++ b/.github/workflows/test-ffi.yml @@ -18,11 +18,15 @@ jobs: with: fetch-depth: 0 - uses: ./.github/actions/toolchains/rust + - name: Cache cargo + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + with: + shared-key: ${{ runner.os }}-regorus + - name: Fetch dependencies + run: cargo fetch --locked + + - name: Fetch FFI crate dependencies + run: cargo fetch --locked --manifest-path bindings/ffi/Cargo.toml - name: Test FFI - run: | - cargo fetch - cargo build -r --frozen - cargo clippy --all-targets --no-deps -- -Dwarnings - cargo test --features contention_checks --frozen - working-directory: ./bindings/ffi + run: cargo xtask test-ffi --release --frozen diff --git a/.github/workflows/test-go.yml b/.github/workflows/test-go.yml index 98f19dd..fb65be3 100644 --- a/.github/workflows/test-go.yml +++ b/.github/workflows/test-go.yml @@ -18,19 +18,19 @@ jobs: with: fetch-depth: 0 - uses: ./.github/actions/toolchains/rust - - + - name: Cache cargo + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + with: + shared-key: ${{ runner.os }}-regorus + - name: Fetch dependencies + run: cargo fetch --locked + + - name: Fetch FFI crate dependencies + run: cargo fetch --locked --manifest-path bindings/ffi/Cargo.toml + - uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 with: architecture: x64 - - name: Build ffi - run: cargo build -r - working-directory: ./bindings/ffi - - - name: Test go - run: | - go mod tidy - go build - LD_LIBRARY_PATH=../ffi/target/release ./regorus_test - working-directory: ./bindings/go + - name: Test Go binding via xtask + run: cargo xtask test-go --release --frozen diff --git a/.github/workflows/test-java.yml b/.github/workflows/test-java.yml index a498d6d..4c64da3 100644 --- a/.github/workflows/test-java.yml +++ b/.github/workflows/test-java.yml @@ -23,25 +23,15 @@ jobs: java-version: 8 distribution: "corretto" - uses: ./.github/actions/toolchains/rust + - name: Cache cargo + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + with: + shared-key: ${{ runner.os }}-regorus + - name: Fetch dependencies + run: cargo fetch --locked - - name: Building binding - run: | - cargo clippy --all-targets --no-deps -- -Dwarnings - cargo build --release --manifest-path bindings/java/Cargo.toml --locked + - name: Fetch Java crate dependencies + run: cargo fetch --locked --manifest-path bindings/java/Cargo.toml - - name: Capture binding version - run: | - version=$(cargo metadata --manifest-path bindings/java/Cargo.toml --format-version 1 \ - | jq -r '.packages[] | select(.name == "regorus-java") | .version') - echo "REGORUS_JAVA_VERSION=$version" >> "$GITHUB_ENV" - - - name: Build jar - run: mvn package - working-directory: ./bindings/java - - - name: Test jar - run: | - jar="regorus-java-${REGORUS_JAVA_VERSION}.jar" - javac -cp "target/${jar}" Test.java - java -Djava.library.path=target/release -cp "target/${jar}:." Test - working-directory: ./bindings/java + - name: Run Java smoke tests via xtask + run: cargo xtask test-java --release --frozen diff --git a/.github/workflows/test-musl.yml b/.github/workflows/test-musl.yml index 1e30d10..be7c4a5 100644 --- a/.github/workflows/test-musl.yml +++ b/.github/workflows/test-musl.yml @@ -22,18 +22,15 @@ jobs: - uses: ./.github/actions/toolchains/rust with: targets: x86_64-unknown-linux-musl + - name: Cache cargo + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + with: + shared-key: ${{ runner.os }}-regorus + - name: Fetch dependencies + run: cargo fetch --locked + - name: Fetch MUSL target dependencies + run: cargo fetch --locked --target x86_64-unknown-linux-musl - name: Install musl-gcc run: sudo apt update && sudo apt install -y musl-tools - - name: Fetch - run: cargo fetch - - name: Build (MUSL) - run: cargo build --verbose --all-targets --target x86_64-unknown-linux-musl --frozen - - name: Run tests (MUSL) - run: cargo test -r --verbose --target x86_64-unknown-linux-musl --frozen - - name: Run tests (MUSL ACI) - run: cargo test -r --test aci --target x86_64-unknown-linux-musl --frozen - - name: Run tests (KATA ACI) - run: cargo test -r --test kata --target x86_64-unknown-linux-musl --frozen - - name: Run tests (MUSL OPA Conformance) - run: >- - cargo test -r --test opa --frozen --features opa-testutil,serde_json/arbitrary_precision --target x86_64-unknown-linux-musl -- $(tr '\n' ' ' < tests/opa.passing) + - name: Run MUSL suite via xtask + run: cargo xtask test-musl --release --frozen --target x86_64-unknown-linux-musl diff --git a/.github/workflows/test-no-std.yml b/.github/workflows/test-no-std.yml index 8e35b63..b6398fb 100644 --- a/.github/workflows/test-no-std.yml +++ b/.github/workflows/test-no-std.yml @@ -22,9 +22,14 @@ jobs: - uses: ./.github/actions/toolchains/rust with: targets: thumbv7m-none-eabi - - name: Fetch - run: cargo fetch - - name: Build - run: cargo build -r --target thumbv7m-none-eabi --frozen - working-directory: ./tests/ensure_no_std + - name: Cache cargo + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + with: + shared-key: ${{ runner.os }}-regorus + - name: Fetch dependencies + run: cargo fetch --locked + - name: Fetch ensure_no_std crate dependencies + run: cargo fetch --locked --manifest-path tests/ensure_no_std/Cargo.toml --target thumbv7m-none-eabi + - name: Test no-std + run: cargo xtask test-no-std --release --frozen diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index d459fe7..7c5e0f1 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -9,9 +9,6 @@ on: # Run at 8:00 AM every day - cron: "0 8 * * *" -env: - PYTHON_VERSION: "3.10" - jobs: build: strategy: @@ -28,66 +25,65 @@ jobs: with: fetch-depth: 0 - uses: ./.github/actions/toolchains/rust + with: + targets: ${{ matrix.host.target }} + - name: Cache cargo + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + with: + shared-key: ${{ runner.os }}-regorus + - name: Fetch dependencies + run: cargo fetch --locked + + - name: Fetch Python crate dependencies + run: cargo fetch --locked --manifest-path bindings/python/Cargo.toml --target ${{ matrix.host.target }} - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: - python-version: ${{ env.PYTHON_VERSION }} + python-version: "3.10" architecture: x64 - - name: Build Python extension - run: | - cargo fetch - cargo clippy --all-targets --no-deps -- -Dwarnings - cargo build --release --target ${{ matrix.host.target }} --frozen - working-directory: bindings/python + - name: Install maturin + run: python -m pip install maturin==1.5.1 - - name: Build Wheel - uses: PyO3/maturin-action@63b75c597b83e247fbf4fb7719801cc4220ae9f3 # v1.43.0 - with: - target: x86_64 - args: --release --out dist --manifest-path bindings/python/Cargo.toml --offline --strip - sccache: 'true' + - name: Build Python wheel via xtask + run: cargo xtask build-python --release --target ${{ matrix.host.target }} --target-dir bindings/python/dist --frozen - - name: Upload Wheel + - name: Upload wheel artefacts uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: regorus-wheel-${{ matrix.host.name }} - path: dist/regorus-*.whl + path: bindings/python/dist/regorus-*.whl test: + needs: build strategy: matrix: + host: [ubuntu-24.04, ubuntu-22.04, windows-latest] python-version: ["3.10", "3.11", "3.12", "3.13"] - host: - - name: ubuntu-24.04 - wheel: regorus-0.5.0-cp310-abi3-manylinux_2_34_x86_64.whl - - name: ubuntu-22.04 - wheel: regorus-0.5.0-cp310-abi3-manylinux_2_34_x86_64.whl - - name: windows-latest - wheel: regorus-0.5.0-cp310-abi3-win_amd64.whl - - needs: build - runs-on: ${{ matrix.host.name }} + runs-on: ${{ matrix.host }} steps: - name: Checkout repository uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 with: fetch-depth: 0 - - - name: Download Regorus wheel - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 + - uses: ./.github/actions/toolchains/rust + - name: Cache cargo + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 with: - path: wheels - pattern: regorus-wheel-* - merge-multiple: true + shared-key: ${{ runner.os }}-regorus + - name: Fetch dependencies + run: cargo fetch --locked + + - name: Fetch Python crate dependencies + run: cargo fetch --locked --manifest-path bindings/python/Cargo.toml - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ matrix.python-version }} architecture: x64 - - name: Test Wheel - run: | - pip3 install ../../wheels/${{ matrix.host.wheel }} - python3 test.py - working-directory: bindings/python + - name: Install maturin + run: python -m pip install maturin==1.5.1 + + - name: Run Python smoke tests via xtask + run: cargo xtask test-python --release --python python diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml index bde2c26..87f0f35 100644 --- a/.github/workflows/test-ruby.yml +++ b/.github/workflows/test-ruby.yml @@ -27,10 +27,16 @@ jobs: cargo-cache: true working-directory: "bindings/ruby" + - name: Cache cargo + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + with: + shared-key: ${{ runner.os }}-regorus + + - name: Fetch dependencies + run: cargo fetch --locked + + - name: Fetch Ruby crate dependencies + run: cargo fetch --locked --manifest-path bindings/ruby/Cargo.toml + - name: Run ruby tests - run: | - cd bindings/ruby - gem install bundler - bundle install - cargo clippy --all-targets --no-deps -- -Dwarnings - bundle exec rake + run: cargo xtask test-ruby --release --frozen diff --git a/.github/workflows/test-wasm.yml b/.github/workflows/test-wasm.yml index 3a27b0b..e70ebfa 100644 --- a/.github/workflows/test-wasm.yml +++ b/.github/workflows/test-wasm.yml @@ -20,6 +20,15 @@ jobs: - name: Setup Rust toolchain uses: ./.github/actions/toolchains/rust + - name: Cache cargo + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + with: + shared-key: ${{ runner.os }}-regorus + - name: Fetch dependencies + run: cargo fetch --locked + + - name: Fetch WASM crate dependencies + run: cargo fetch --locked --manifest-path bindings/wasm/Cargo.toml - name: Setup Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 @@ -29,11 +38,5 @@ jobs: - name: Install wasmlpack run: cargo install wasm-pack - - name: Test wasm binding - run: | - cd bindings/wasm - cargo fetch - cargo clippy --all-targets --no-deps -- -Dwarnings - wasm-pack build --target nodejs --release - wasm-pack test --release --node - node test.js + - name: Test wasm binding via xtask + run: cargo xtask test-wasm --release --frozen --node node diff --git a/.github/workflows/tests-debug.yml b/.github/workflows/tests-debug.yml index 889c511..8d1891c 100644 --- a/.github/workflows/tests-debug.yml +++ b/.github/workflows/tests-debug.yml @@ -21,26 +21,11 @@ jobs: - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 - name: Setup Rust toolchain uses: ./.github/actions/toolchains/rust - - name: Fetch - run: cargo fetch - - name: Build (all features) - run: cargo build --all-features --frozen - - name: Build - run: cargo build --frozen - - name: Test no_std - run: cargo test --no-default-features --frozen - - name: Build only std - run: cargo build --example regorus --no-default-features --features "std" --frozen - - name: Doc Tests - run: cargo test --doc --frozen - - name: Run tests - run: cargo test --frozen - - name: Run tests (ACI) - run: cargo test --test aci --frozen - - name: Run tests (KATA) - run: cargo test --test kata --frozen - - name: Run tests (OPA Conformance) - run: >- - cargo test --test opa --frozen --features opa-testutil,serde_json/arbitrary_precision -- $(tr '\n' ' ' < tests/opa.passing) - - name: Run tests (Azure RBAC) - run: cargo test --frozen --features azure-rbac + - name: Cache cargo + uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2 + with: + shared-key: ${{ runner.os }}-regorus + - name: Fetch dependencies + run: cargo fetch --locked + - name: Run debug CI suite + run: cargo xtask ci-debug --frozen diff --git a/Cargo.lock b/Cargo.lock index 54e9523..ac3d42c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "ahash" version = "0.8.12" @@ -176,6 +182,12 @@ version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "cast" version = "0.3.0" @@ -184,9 +196,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.52" +version = "1.2.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" dependencies = [ "find-msvc-tools", "shlex", @@ -277,8 +289,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", - "proc-macro2 1.0.105", - "quote 1.0.43", + "proc-macro2 1.0.106", + "quote 1.0.44", "syn 2.0.114", ] @@ -300,6 +312,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "criterion" version = "0.8.1" @@ -392,8 +413,8 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ - "proc-macro2 1.0.105", - "quote 1.0.43", + "proc-macro2 1.0.106", + "quote 1.0.44", "syn 2.0.114", ] @@ -439,9 +460,19 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" + +[[package]] +name = "flate2" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +dependencies = [ + "crc32fast", + "miniz_oxide", +] [[package]] name = "fluent-uri" @@ -795,6 +826,16 @@ dependencies = [ "cc", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "msvc_spectre_libs" version = "0.1.3" @@ -1046,9 +1087,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -1064,11 +1105,11 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ - "proc-macro2 1.0.105", + "proc-macro2 1.0.106", ] [[package]] @@ -1150,8 +1191,8 @@ version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ - "proc-macro2 1.0.105", - "quote 1.0.43", + "proc-macro2 1.0.106", + "quote 1.0.44", "syn 2.0.114", ] @@ -1293,8 +1334,8 @@ version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ - "proc-macro2 1.0.105", - "quote 1.0.43", + "proc-macro2 1.0.106", + "quote 1.0.44", "syn 2.0.114", ] @@ -1330,6 +1371,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "siphasher" version = "1.0.1" @@ -1377,8 +1424,8 @@ version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ - "proc-macro2 1.0.105", - "quote 1.0.43", + "proc-macro2 1.0.106", + "quote 1.0.44", "unicode-ident", ] @@ -1388,8 +1435,8 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ - "proc-macro2 1.0.105", - "quote 1.0.43", + "proc-macro2 1.0.106", + "quote 1.0.44", "syn 2.0.114", ] @@ -1407,21 +1454,21 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ - "proc-macro2 1.0.105", - "quote 1.0.43", + "proc-macro2 1.0.106", + "quote 1.0.44", "syn 2.0.114", ] @@ -1564,9 +1611,9 @@ dependencies = [ [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] @@ -1590,7 +1637,7 @@ version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ - "quote 1.0.43", + "quote 1.0.44", "wasm-bindgen-macro-support", ] @@ -1601,8 +1648,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", - "proc-macro2 1.0.105", - "quote 1.0.43", + "proc-macro2 1.0.106", + "quote 1.0.44", "syn 2.0.114", "wasm-bindgen-shared", ] @@ -1676,8 +1723,8 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ - "proc-macro2 1.0.105", - "quote 1.0.43", + "proc-macro2 1.0.106", + "quote 1.0.44", "syn 2.0.114", ] @@ -1687,8 +1734,8 @@ version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ - "proc-macro2 1.0.105", - "quote 1.0.43", + "proc-macro2 1.0.106", + "quote 1.0.44", "syn 2.0.114", ] @@ -1736,9 +1783,9 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" @@ -1755,6 +1802,7 @@ dependencies = [ "regex", "semver", "toml_edit", + "zip", ] [[package]] @@ -1774,8 +1822,8 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ - "proc-macro2 1.0.105", - "quote 1.0.43", + "proc-macro2 1.0.106", + "quote 1.0.44", "syn 2.0.114", "synstructure", ] @@ -1795,8 +1843,8 @@ version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" dependencies = [ - "proc-macro2 1.0.105", - "quote 1.0.43", + "proc-macro2 1.0.106", + "quote 1.0.44", "syn 2.0.114", ] @@ -1815,8 +1863,8 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ - "proc-macro2 1.0.105", - "quote 1.0.43", + "proc-macro2 1.0.106", + "quote 1.0.44", "syn 2.0.114", "synstructure", ] @@ -1849,13 +1897,25 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ - "proc-macro2 1.0.105", - "quote 1.0.43", + "proc-macro2 1.0.106", + "quote 1.0.44", "syn 2.0.114", ] [[package]] -name = "zmij" -version = "1.0.14" +name = "zip" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", + "flate2", +] + +[[package]] +name = "zmij" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" diff --git a/README.md b/README.md index cbad574..ab1326b 100644 --- a/README.md +++ b/README.md @@ -274,6 +274,19 @@ Benchmark 1: opa eval -b tests/aci -d tests/aci/data.json -i tests/aci/input.jso Range (min … max): 43.8 ms … 46.7 ms 62 runs ``` + +## Contributor Workflow + +Regorus uses a small companion CLI under the `xtask` package to keep CI and local development in sync. +The commands mirror our GitHub Actions jobs, making it easy to dry-run CI steps before sending a pull request. + +- Run the full release pipeline with `cargo xtask ci-release` and the debug checks with `cargo xtask ci-debug`. +- Exercise language bindings through focused helpers such as `cargo xtask test-java --release --frozen` or `cargo xtask test-go`. +- Use `cargo xtask test-musl --release --frozen` for the cross-compilation matrix and `cargo xtask test-no-std` for embedded targets. +- Formatting (`cargo xtask fmt`) and linting (`cargo xtask clippy --sarif`) wrap the usual Cargo tooling while matching CI defaults. + +The workflows in `.github/workflows` invoke the same commands, so keeping local runs green is usually enough to satisfy the checks enforced on `main`. + ## OPA Conformance Regorus has been verified to be compliant with [OPA v1.2.0](https://github.com/open-policy-agent/opa/releases/tag/v1.2.0) diff --git a/bindings/csharp/README.md b/bindings/csharp/README.md index 6f22b89..11cb9e8 100644 --- a/bindings/csharp/README.md +++ b/bindings/csharp/README.md @@ -29,6 +29,7 @@ Once the workflow run completes, the generated Nuget can be downloaded by follow ## Local +<<<<<<< HEAD TODO ## Memory Usage Safeguards @@ -60,3 +61,10 @@ Regorus.MemoryLimits.SetThreadFlushThresholdOverride(null); ``` See `bindings/csharp/Regorus.Tests/RegorusTests.cs` for scenario coverage and `bindings/csharp/TargetExampleApp/Program.cs` for end-to-end usage. +======= +The `cargo xtask` runner provides helpers for local builds: + +1. `cargo xtask ffi` builds the `bindings/ffi` crate for the host platform in debug mode. Add `--target ` (repeatable) to cross-compile, or `--release` to produce optimised artefacts. Results land under `bindings/ffi/target//`. +2. `cargo xtask nuget` reuses those artefacts to pack the C# library. It defaults to debug builds for the host but accepts `--target`, `--release`, `--artifacts-dir ` to reuse existing binaries, and `--enforce-artifacts` to require every officially supported platform. +3. `cargo xtask test-csharp` ensures a NuGet is available (rebuilding when required or when `--force-nuget` is passed) and then runs `Regorus.Tests`, `TestApp`, and `TargetExampleApp` against it. The command accepts the same build flags as `cargo xtask nuget`. +>>>>>>> 380a27c (feat(xtask): consolidate CI workflows onto xtask helpers) diff --git a/bindings/csharp/Regorus.Tests/Regorus.Tests.csproj b/bindings/csharp/Regorus.Tests/Regorus.Tests.csproj index a1add44..e28bf07 100644 --- a/bindings/csharp/Regorus.Tests/Regorus.Tests.csproj +++ b/bindings/csharp/Regorus.Tests/Regorus.Tests.csproj @@ -12,11 +12,6 @@ -$(VersionSuffix) - true - - - - $(DefineConstants);REGORUS_FFI_TEST_HOOKS @@ -27,13 +22,7 @@ - - - EnableRegorusTestHooks=true - - - - + \ No newline at end of file diff --git a/bindings/csharp/Regorus/Regorus.csproj b/bindings/csharp/Regorus/Regorus.csproj index 621acdc..4af95ea 100644 --- a/bindings/csharp/Regorus/Regorus.csproj +++ b/bindings/csharp/Regorus/Regorus.csproj @@ -13,6 +13,10 @@ README.md + + release + + @@ -27,7 +31,7 @@ by the publishing pipeline. For each target triple, `Pack` expects the regorus ffi shared library - to be found in $(RegorusFFIArtifactsDir)//release. + to be found in $(RegorusFFIArtifactsDir)//$(RegorusFFIArtifactsProfile). If $(IgnoreMissingArtifacts) is not set, ensure that the binaries for officially supported platforms exists. --> @@ -35,27 +39,27 @@ - - + + - + - - + + - - + + - + - + diff --git a/bindings/ffi/Cargo.lock b/bindings/ffi/Cargo.lock index c367364..a8f1836 100644 --- a/bindings/ffi/Cargo.lock +++ b/bindings/ffi/Cargo.lock @@ -172,9 +172,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.52" +version = "1.2.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" dependencies = [ "find-msvc-tools", "shlex", @@ -329,9 +329,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" [[package]] name = "fluent-uri" @@ -827,18 +827,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -1158,18 +1158,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -1300,9 +1300,9 @@ checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] @@ -1431,9 +1431,9 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" @@ -1540,6 +1540,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" +checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" diff --git a/bindings/java/Cargo.lock b/bindings/java/Cargo.lock index 73bc047..0a5f4db 100644 --- a/bindings/java/Cargo.lock +++ b/bindings/java/Cargo.lock @@ -109,9 +109,9 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cc" -version = "1.2.52" +version = "1.2.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" dependencies = [ "find-msvc-tools", "shlex", @@ -213,9 +213,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" [[package]] name = "fluent-uri" @@ -703,18 +703,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -848,7 +848,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "thiserror 2.0.17", + "thiserror 2.0.18", "url", "uuid", ] @@ -1009,11 +1009,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -1029,9 +1029,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -1125,9 +1125,9 @@ dependencies = [ [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] @@ -1322,9 +1322,9 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" @@ -1431,6 +1431,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" +checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" diff --git a/bindings/java/README.md b/bindings/java/README.md index 6edb6f5..a447990 100644 --- a/bindings/java/README.md +++ b/bindings/java/README.md @@ -32,6 +32,13 @@ $ mvn package And you will have a JAR at `./target/regorus-java-0.1.5.jar`. +### Automation + +The repository exposes helper commands for local workflows: + +- `cargo xtask build-java` runs `mvn package` with quiet output helpers. +- `cargo xtask test-java` rebuilds the native library via the Maven exec plugin and executes the binding tests. + ## Usage You can use Regorus Java bindings as: diff --git a/bindings/python/Cargo.lock b/bindings/python/Cargo.lock index 8dacc4a..8d1b805 100644 --- a/bindings/python/Cargo.lock +++ b/bindings/python/Cargo.lock @@ -103,9 +103,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "cc" -version = "1.2.52" +version = "1.2.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" dependencies = [ "find-msvc-tools", "shlex", @@ -191,9 +191,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" [[package]] name = "fluent-uri" @@ -698,9 +698,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -771,9 +771,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -1057,18 +1057,18 @@ checksum = "b1dd07eb858a2067e2f3c7155d54e929265c264e6f37efe3ee7a8d1b5a1dd0ba" [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -1158,9 +1158,9 @@ checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] @@ -1271,9 +1271,9 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" @@ -1380,6 +1380,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" +checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" diff --git a/bindings/python/README.md b/bindings/python/README.md index 20e31b9..c06cc2c 100644 --- a/bindings/python/README.md +++ b/bindings/python/README.md @@ -10,6 +10,10 @@ Regorus can be used in Python via `regorus` package. (It is not yet available in See [Repository](https://github.com/microsoft/regorus). +## Automation + +Run `cargo xtask build-python` to produce wheels via maturin, or `cargo xtask test-python` to reinstall the package locally and execute the sample script and pytest suite. + To build this binding, see [building](https://github.com/microsoft/regorus/blob/main/bindings/python/building.md) ## Usage diff --git a/bindings/ruby/Cargo.lock b/bindings/ruby/Cargo.lock index bd63a51..c236fca 100644 --- a/bindings/ruby/Cargo.lock +++ b/bindings/ruby/Cargo.lock @@ -111,9 +111,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytecount" @@ -123,9 +123,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "cc" -version = "1.2.47" +version = "1.2.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" +checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" dependencies = [ "find-msvc-tools", "shlex", @@ -148,9 +148,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", "js-sys", @@ -188,9 +188,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "data-encoding" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] name = "displaydoc" @@ -237,9 +237,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" [[package]] name = "fluent-uri" @@ -379,9 +379,9 @@ checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ "icu_collections", "icu_locale_core", @@ -393,9 +393,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" @@ -435,9 +435,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown", @@ -460,15 +460,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -514,9 +514,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libloading" @@ -545,9 +545,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "magnus" @@ -580,7 +580,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "mimalloc" -version = "2.1.2" +version = "2.2.6" dependencies = [ "mimalloc-sys", ] @@ -775,18 +775,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -819,27 +819,27 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom", ] [[package]] name = "rb-sys" -version = "0.9.117" +version = "0.9.124" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f900d1ce4629a2ebffaf5de74bd8f9c1188d4c5ed406df02f97e22f77a006f44" +checksum = "c85c4188462601e2aa1469def389c17228566f82ea72f137ed096f21591bc489" dependencies = [ "rb-sys-build", ] [[package]] name = "rb-sys-build" -version = "0.9.117" +version = "0.9.124" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1e9c857028f631056bcd6d88cec390c751e343ce2223ddb26d23eb4a151d59" +checksum = "568068db4102230882e6d4ae8de6632e224ca75fe5970f6e026a04e91ed635d3" dependencies = [ "bindgen", "lazy_static", @@ -950,7 +950,6 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "smol_str", "thiserror", "url", "uuid", @@ -980,9 +979,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "scopeguard" @@ -1034,15 +1033,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -1071,9 +1070,9 @@ dependencies = [ [[package]] name = "shell-words" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" [[package]] name = "shlex" @@ -1093,15 +1092,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "smol_str" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" -dependencies = [ - "serde", -] - [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -1110,9 +1100,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "syn" -version = "2.0.111" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -1138,18 +1128,18 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -1180,9 +1170,9 @@ checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -1198,9 +1188,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom", "js-sys", @@ -1233,18 +1223,18 @@ checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", @@ -1255,9 +1245,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1265,9 +1255,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", "proc-macro2", @@ -1278,9 +1268,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] @@ -1346,9 +1336,9 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" @@ -1381,18 +1371,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.28" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.28" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" dependencies = [ "proc-macro2", "quote", @@ -1452,3 +1442,9 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" diff --git a/bindings/wasm/Cargo.lock b/bindings/wasm/Cargo.lock index 85b8e10..f636cb3 100644 --- a/bindings/wasm/Cargo.lock +++ b/bindings/wasm/Cargo.lock @@ -120,9 +120,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.52" +version = "1.2.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" dependencies = [ "find-msvc-tools", "shlex", @@ -208,9 +208,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" [[package]] name = "fluent-uri" @@ -746,18 +746,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -1051,18 +1051,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -1162,9 +1162,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] @@ -1356,9 +1356,9 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" @@ -1465,6 +1465,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" +checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" diff --git a/bindings/wasm/README.md b/bindings/wasm/README.md index 902b01f..36aa33c 100644 --- a/bindings/wasm/README.md +++ b/bindings/wasm/README.md @@ -12,6 +12,10 @@ See [Repository](https://github.com/microsoft/regorus). To build this binding, see [building.md](https://github.com/microsoft/regorus/blob/main/bindings/wasm/building.md) +## Automation + +Run `cargo xtask build-wasm` to invoke wasm-pack with sensible defaults, or `cargo xtask test-wasm` to rebuild the package and execute `node test.js`. + ## Usage diff --git a/scripts/pre-commit b/scripts/pre-commit index f497463..27cf387 100755 --- a/scripts/pre-commit +++ b/scripts/pre-commit @@ -4,25 +4,4 @@ set -eo pipefail -if [ -f Cargo.toml ]; then - # Ensure that all targets can be built. - cargo build --all-targets - - #Ensure that code is correctly formatted. - cargo fmt --check || (echo "Run cargo fmt to fix formatting" && exit 1) - - # Ensure that clippy warnings are addressed. - cargo clippy --all-targets --no-deps -- -Dwarnings - - # Ensure binding versions reflect the staged changes. - cargo xtask bindings --check - - # Ensure that all modifications are included. - # TODO refine status checking. - if git status -s | grep -e "MM " -e "??" -e "AM " -e " M " > /dev/null; then - printf "\nUnstaged changes found:\n" - git status -s | grep -e "MM " -e "??" -e "AM " -e " M " - echo "Stage them and try again" - exit 1 - fi -fi +cargo xtask pre-commit diff --git a/scripts/pre-push b/scripts/pre-push index a95a46e..112dc4e 100755 --- a/scripts/pre-push +++ b/scripts/pre-push @@ -4,37 +4,4 @@ set -eo pipefail -if [ -f Cargo.toml ]; then - # Run precommit checks. - dir=$(dirname "${BASH_SOURCE[0]}") - "$dir/pre-commit" - - # Ensure that the public API works. - cargo test --doc - - # Ensure that no_std build succeeds. - # Build for a target that has no std available. - if command -v rustup > /dev/null; then - rustup target add thumbv7m-none-eabi - (cd tests/ensure_no_std; cargo build -r --target thumbv7m-none-eabi) - fi - - # Ensure that we can build with only std. - cargo build --example regorus --no-default-features --features std - - # Ensure that we can build with all features. - cargo build --all-features - - # Ensure that all tests pass. - cargo test - cargo test --test aci - cargo test --test kata - - # Ensure that all tests pass with extensions - cargo test --features rego-extensions - cargo test --test aci --features rego-extensions - cargo test --test kata --features rego-extensions - - # Ensure that OPA conformance tests don't regress. - cargo test --features opa-testutil,serde_json/arbitrary_precision,rego-extensions --test opa -- $(tr '\n' ' ' < tests/opa.passing) -fi +cargo xtask pre-push diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index d0e335b..405c7a2 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -10,3 +10,4 @@ clap = { version = "4.5", features = ["derive"] } regex = "1.11" toml_edit = "0.22" semver = "1.0" +zip = { version = "0.6", default-features = false, features = ["deflate"] } diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 10805b5..671230a 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -6,7 +6,14 @@ mod tasks; use anyhow::Result; use clap::{Parser, Subcommand}; -use tasks::{BindingsCommand, UpdateDepsCommand}; +use tasks::{ + BindingsCommand, BuildAllBindingsCommand, BuildFfiCommand, BuildJavaCommand, BuildNugetCommand, + BuildPythonCommand, BuildWasmCommand, CiDebugCommand, CiReleaseCommand, ClippyCommand, + FmtCommand, PrecommitCommand, PrepushCommand, TestAllBindingsCommand, TestCCommand, + TestCNoStdCommand, TestCppCommand, TestCsharpCommand, TestFfiCommand, TestGoCommand, + TestJavaCommand, TestMuslCommand, TestNoStdCommand, TestPythonCommand, TestRubyCommand, + TestWasmCommand, UpdateDepsCommand, +}; #[derive(Parser)] #[command(author, version, about, propagate_version = true)] @@ -19,6 +26,59 @@ struct Cli { enum Commands { /// Bump binding manifests to match the main regorus crate version Bindings(BindingsCommand), + /// Build the regorus FFI crate for selected targets + #[command(name = "build-ffi", alias = "ffi")] + Ffi(BuildFfiCommand), + /// Run the release-focused CI workflow locally + CiRelease(CiReleaseCommand), + /// Run the debug-focused CI workflow locally + CiDebug(CiDebugCommand), + /// Format the workspace and all binding crates + Fmt(FmtCommand), + /// Run clippy across the workspace and all binding crates + Clippy(ClippyCommand), + /// Run the repository pre-commit validation sequence + PreCommit(PrecommitCommand), + /// Run the repository pre-push validation sequence + PrePush(PrepushCommand), + /// Build every binding artefact with opinionated defaults + BuildAllBindings(BuildAllBindingsCommand), + /// Build the Java bindings via Maven + BuildJava(BuildJavaCommand), + /// Execute the Java binding test suite + TestJava(TestJavaCommand), + /// Build the Python wheels via maturin + BuildPython(BuildPythonCommand), + /// Execute the Python binding tests + TestPython(TestPythonCommand), + /// Build the WASM bindings via wasm-pack + BuildWasm(BuildWasmCommand), + /// Execute the WASM binding smoke tests + TestWasm(TestWasmCommand), + /// Execute the FFI binding test suite + TestFfi(TestFfiCommand), + /// Execute the Go binding smoke tests + TestGo(TestGoCommand), + /// Execute all binding smoke tests + TestAllBindings(TestAllBindingsCommand), + /// Execute the MUSL cross-compilation test matrix + TestMusl(TestMuslCommand), + /// Build the ensure_no_std harness for the embedded target + TestNoStd(TestNoStdCommand), + /// Configure, build, and run the C binding sample + TestC(TestCCommand), + /// Configure, build, and run the C++ binding sample + TestCpp(TestCppCommand), + /// Configure, build, and run the no_std C binding sample + #[command(name = "test-c-no-std", alias = "test-c-nostd")] + TestCNoStd(TestCNoStdCommand), + /// Execute the Ruby binding smoke tests + TestRuby(TestRubyCommand), + /// Build and validate the C# binding via its NuGet package + TestCsharp(TestCsharpCommand), + /// Build the Regorus C# NuGet package from local artefacts + #[command(name = "build-csharp", alias = "build-nuget", alias = "nuget")] + BuildCsharp(BuildNugetCommand), /// Update dependencies across all workspace Cargo.lock files UpdateDeps(UpdateDepsCommand), } @@ -28,6 +88,31 @@ fn main() -> Result<()> { match cli.command { Commands::Bindings(cmd) => cmd.run()?, + Commands::Ffi(cmd) => cmd.run()?, + Commands::CiRelease(cmd) => cmd.run()?, + Commands::CiDebug(cmd) => cmd.run()?, + Commands::Fmt(cmd) => cmd.run()?, + Commands::Clippy(cmd) => cmd.run()?, + Commands::PreCommit(cmd) => cmd.run()?, + Commands::PrePush(cmd) => cmd.run()?, + Commands::BuildAllBindings(cmd) => cmd.run()?, + Commands::BuildJava(cmd) => cmd.run()?, + Commands::TestJava(cmd) => cmd.run()?, + Commands::BuildPython(cmd) => cmd.run()?, + Commands::TestPython(cmd) => cmd.run()?, + Commands::BuildWasm(cmd) => cmd.run()?, + Commands::TestWasm(cmd) => cmd.run()?, + Commands::TestFfi(cmd) => cmd.run()?, + Commands::TestGo(cmd) => cmd.run()?, + Commands::TestAllBindings(cmd) => cmd.run()?, + Commands::TestMusl(cmd) => cmd.run()?, + Commands::TestNoStd(cmd) => cmd.run()?, + Commands::TestC(cmd) => cmd.run()?, + Commands::TestCpp(cmd) => cmd.run()?, + Commands::TestCNoStd(cmd) => cmd.run()?, + Commands::TestRuby(cmd) => cmd.run()?, + Commands::TestCsharp(cmd) => cmd.run()?, + Commands::BuildCsharp(cmd) => cmd.run()?, Commands::UpdateDeps(cmd) => cmd.run()?, } diff --git a/xtask/src/tasks/bindings/all.rs b/xtask/src/tasks/bindings/all.rs new file mode 100644 index 0000000..4cbc344 --- /dev/null +++ b/xtask/src/tasks/bindings/all.rs @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use anyhow::Result; +use clap::Args; + +use super::c::{TestCCommand, TestCNoStdCommand}; +use super::cpp::TestCppCommand; +use super::csharp::{build_nuget_package, BuildNugetConfig, TestCsharpCommand}; +use super::ffi; +use super::java::{BuildJavaCommand, TestJavaCommand}; +use super::python::{BuildPythonCommand, TestPythonCommand}; +use super::wasm::{BuildWasmCommand, TestWasmCommand}; +use crate::tasks::util::workspace_root; + +/// Builds every published binding with opinionated defaults. +#[derive(Args, Default)] +pub struct BuildAllBindingsCommand { + /// Optimise artefacts where supported (defaults to debug builds). + #[arg(long)] + pub release: bool, +} + +impl BuildAllBindingsCommand { + pub fn run(&self) -> Result<()> { + let workspace = workspace_root(); + let release = self.release; + + let targets = ffi::resolve_targets(Vec::new())?; + ffi::build_targets(&workspace, &targets, release)?; + + let ffi_dir = workspace.join("bindings/ffi/target"); + let nuget_result = build_nuget_package(&BuildNugetConfig { + targets: Vec::new(), + release, + clean: false, + artifacts_dir: Some(ffi_dir.clone()), + enforce_artifacts: false, + })?; + + if nuget_result.packages.is_empty() { + println!("NuGet packaging completed but no archives were produced."); + } else { + for package in &nuget_result.packages { + println!("NuGet package ready at {}", package.display()); + } + } + + BuildJavaCommand { skip_tests: true }.run()?; + BuildPythonCommand { + release, + target: None, + target_dir: None, + frozen: false, + } + .run()?; + BuildWasmCommand { + release, + target: "nodejs".to_string(), + out_dir: None, + } + .run()?; + + println!("Completed building all bindings."); + Ok(()) + } +} + +/// Executes the smoke tests for all bindings in sequence. +#[derive(Args, Default)] +pub struct TestAllBindingsCommand { + /// Optimise artefacts where supported (defaults to debug builds). + #[arg(long)] + pub release: bool, + + /// Python executable leveraged by the binding tests. + #[arg(long, value_name = "EXE", default_value = "python3")] + pub python: String, + + /// Node.js executable leveraged by the WASM test. + #[arg(long, value_name = "EXE", default_value = "node")] + pub node: String, +} + +impl TestAllBindingsCommand { + pub fn run(&self) -> Result<()> { + TestCCommand { + release: self.release, + frozen: false, + skip_ffi: false, + } + .run()?; + TestCppCommand { + release: self.release, + frozen: false, + skip_ffi: true, + } + .run()?; + TestCNoStdCommand { + release: self.release, + frozen: false, + skip_ffi: true, + } + .run()?; + TestJavaCommand { + release: self.release, + frozen: false, + } + .run()?; + + TestPythonCommand { + release: self.release, + target: None, + python: self.python.clone(), + } + .run()?; + + TestWasmCommand { + release: self.release, + target: "nodejs".to_string(), + out_dir: None, + node: self.node.clone(), + frozen: false, + skip_build: false, + } + .run()?; + + TestCsharpCommand { + targets: Vec::new(), + release: self.release, + clean: false, + artifacts_dir: None, + enforce_artifacts: false, + force_nuget: false, + nuget_dir: None, + } + .run()?; + + println!("Completed testing all bindings."); + Ok(()) + } +} diff --git a/xtask/src/tasks/bindings/c.rs b/xtask/src/tasks/bindings/c.rs new file mode 100644 index 0000000..21a4569 --- /dev/null +++ b/xtask/src/tasks/bindings/c.rs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use crate::tasks::util::{run_cargo_step, run_command, workspace_root}; +use anyhow::{anyhow, Context, Result}; +use clap::Args; + +#[derive(Args, Default)] +pub struct TestCCommand { + /// Build the FFI crate in release mode before exercising the binding. + #[arg(long)] + pub release: bool, + + /// Pass --frozen to the preparatory cargo invocations. + #[arg(long)] + pub frozen: bool, + + /// Reuse previously built FFI artefacts instead of rebuilding. + #[arg(long)] + pub skip_ffi: bool, +} + +#[derive(Args, Default)] +pub struct TestCNoStdCommand { + /// Build the FFI crate in release mode before exercising the binding. + #[arg(long)] + pub release: bool, + + /// Pass --frozen to the preparatory cargo invocations. + #[arg(long)] + pub frozen: bool, + + /// Reuse previously built FFI artefacts instead of rebuilding. + #[arg(long)] + pub skip_ffi: bool, +} + +impl TestCCommand { + pub fn run(&self) -> Result<()> { + if !self.skip_ffi { + prepare_ffi_artifacts(self.release, self.frozen)?; + } + run_binding("bindings/c", "regorus_test", self.release) + } +} + +impl TestCNoStdCommand { + pub fn run(&self) -> Result<()> { + if !self.skip_ffi { + prepare_ffi_artifacts(self.release, self.frozen)?; + } + run_binding("bindings/c-nostd", "regorus_test", self.release) + } +} + +pub(super) fn run_binding(relative_dir: &str, binary_name: &str, release: bool) -> Result<()> { + let workspace = workspace_root(); + let source_dir = workspace.join(relative_dir); + let build_dir = source_dir.join("build"); + fs::create_dir_all(&build_dir).with_context(|| { + format!( + "failed to create build directory at {}", + build_dir.display() + ) + })?; + + let build_type = if release { "Release" } else { "Debug" }; + let mut configure = Command::new("cmake"); + configure.arg("-S").arg(&source_dir); + configure.arg("-B").arg(&build_dir); + configure.arg(format!("-DCMAKE_BUILD_TYPE={build_type}")); + run_command(configure, &format!("cmake configure ({relative_dir})"))?; + + let mut build = Command::new("cmake"); + build.arg("--build").arg(&build_dir); + build.arg("--config").arg(build_type); + run_command(build, &format!("cmake build ({relative_dir})"))?; + + let executable = locate_executable(&build_dir, binary_name)?; + let mut run = Command::new(&executable); + run.current_dir(&build_dir); + run_command(run, &format!("{binary_name} ({relative_dir})"))?; + + Ok(()) +} + +fn locate_executable(build_dir: &Path, binary_name: &str) -> Result { + let mut candidates = Vec::new(); + if cfg!(windows) { + let exe = format!("{binary_name}.exe"); + candidates.push(build_dir.join(&exe)); + candidates.push(build_dir.join("Release").join(&exe)); + candidates.push(build_dir.join("Debug").join(&exe)); + } else { + candidates.push(build_dir.join(binary_name)); + candidates.push(build_dir.join("Release").join(binary_name)); + candidates.push(build_dir.join("Debug").join(binary_name)); + } + + for candidate in candidates { + if candidate.exists() { + return Ok(candidate); + } + } + + Err(anyhow!( + "failed to locate built executable '{}' under {}", + binary_name, + build_dir.display() + )) +} + +pub(super) fn prepare_ffi_artifacts(release: bool, frozen: bool) -> Result<()> { + let workspace = workspace_root(); + let ffi_dir = workspace.join("bindings/ffi"); + + let mut build_args = vec!["build", "--locked"]; + if release { + build_args.push("--release"); + } + if frozen { + build_args.push("--frozen"); + } + let build_label = if release { + "cargo build --release (bindings/ffi)" + } else { + "cargo build (bindings/ffi)" + }; + run_cargo_step(&ffi_dir, build_label, build_args) +} diff --git a/xtask/src/tasks/bindings/cpp.rs b/xtask/src/tasks/bindings/cpp.rs new file mode 100644 index 0000000..4708ab7 --- /dev/null +++ b/xtask/src/tasks/bindings/cpp.rs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use anyhow::Result; +use clap::Args; + +use super::c::{prepare_ffi_artifacts, run_binding}; + +#[derive(Args, Default)] +pub struct TestCppCommand { + /// Build the FFI crate in release mode before exercising the binding. + #[arg(long)] + pub release: bool, + + /// Pass --frozen to the preparatory cargo invocations. + #[arg(long)] + pub frozen: bool, + + /// Reuse previously built FFI artefacts instead of rebuilding. + #[arg(long)] + pub skip_ffi: bool, +} + +impl TestCppCommand { + pub fn run(&self) -> Result<()> { + if !self.skip_ffi { + prepare_ffi_artifacts(self.release, self.frozen)?; + } + run_binding("bindings/cpp", "regorus_test", self.release) + } +} diff --git a/xtask/src/tasks/bindings/csharp.rs b/xtask/src/tasks/bindings/csharp.rs new file mode 100644 index 0000000..41274c3 --- /dev/null +++ b/xtask/src/tasks/bindings/csharp.rs @@ -0,0 +1,484 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::fs::{self, File}; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use anyhow::{anyhow, Context, Result}; +use clap::Args; + +use super::ffi; +use crate::tasks::util::{dotnet_host_arch, run_command, workspace_root}; +use zip::read::ZipArchive; + +/// Builds the Regorus C# NuGet package from local sources. +#[derive(Args, Clone)] +pub struct BuildNugetCommand { + /// Build the Rust FFI artefacts for the provided target triple (defaults to the host). + #[arg(long = "target", value_name = "TRIPLE")] + pub targets: Vec, + + /// Build the package in release mode (defaults to debug). + #[arg(long)] + pub release: bool, + + /// Remove existing build artefacts before compiling. + #[arg(long)] + pub clean: bool, + + /// Override the directory that contains compiled regorus FFI artefacts. + #[arg(long, value_name = "PATH")] + pub artifacts_dir: Option, + + /// Require all platform artefacts to exist before packing. + #[arg(long)] + pub enforce_artifacts: bool, +} + +/// Parsed build options shared across tasks that need a NuGet package. +#[derive(Clone, Debug)] +pub struct BuildNugetConfig { + pub targets: Vec, + pub release: bool, + pub clean: bool, + pub artifacts_dir: Option, + pub enforce_artifacts: bool, +} + +/// Result of a NuGet build, including generated artefacts. +#[derive(Debug)] +pub struct BuildNugetResult { + pub package_dir: PathBuf, + pub packages: Vec, +} + +/// Builds (or rebuilds) the C# NuGet package with the supplied configuration. +pub fn build_nuget_package(config: &BuildNugetConfig) -> Result { + let workspace_root = workspace_root(); + let configuration = if config.release { "Release" } else { "Debug" }; + let profile = ffi::profile_dir(config.release); + + let base_artifacts_dir = if let Some(dir) = &config.artifacts_dir { + if !config.targets.is_empty() { + println!("Skipping FFI build for specified targets because --artifacts-dir is set."); + } + dir.clone() + } else { + let targets = ffi::resolve_targets(config.targets.clone())?; + ffi::build_targets(&workspace_root, &targets, config.release)?; + + workspace_root.join("bindings/ffi/target") + }; + + let artifacts_dir = base_artifacts_dir.canonicalize().with_context(|| { + format!( + "failed to canonicalize FFI artefacts directory at {}", + base_artifacts_dir.display() + ) + })?; + + let package_dir = invoke_dotnet_pack( + &workspace_root, + &artifacts_dir, + configuration, + &profile, + !config.enforce_artifacts, + config.clean, + )?; + + let packages = find_packages(&package_dir)?; + + Ok(BuildNugetResult { + package_dir, + packages, + }) +} + +/// Returns all NuGet packages currently present in the given directory. +pub fn find_packages(package_dir: &Path) -> Result> { + if !package_dir.exists() { + return Ok(Vec::new()); + } + + let mut packages = Vec::new(); + let entries = fs::read_dir(package_dir).with_context(|| { + format!( + "failed to enumerate NuGet artefacts under {}", + package_dir.display() + ) + })?; + + for entry in entries { + let entry = entry?; + let path = entry.path(); + if path + .extension() + .and_then(|ext| ext.to_str()) + .map_or(false, |ext| ext.eq_ignore_ascii_case("nupkg")) + { + packages.push(path); + } + } + + packages.sort(); + Ok(packages) +} +fn invoke_dotnet_pack( + root: &Path, + artifacts_dir: &Path, + configuration: &str, + profile: &str, + ignore_missing: bool, + clean: bool, +) -> Result { + let project_dir = root.join("bindings/csharp/Regorus"); + let artifacts_dir_str = artifacts_dir + .to_str() + .ok_or_else(|| anyhow!("artefact directory path contains invalid UTF-8"))?; + + let profile_arg = format!("/p:RegorusFFIArtifactsProfile={}", profile); + let dir_arg = format!("/p:RegorusFFIArtifactsDir={}", artifacts_dir_str); + + if clean { + clean_msbuild_project(&project_dir, configuration)?; + let artefact_root = project_dir.join("bin").join(configuration); + if artefact_root.exists() { + fs::remove_dir_all(&artefact_root).with_context(|| { + format!( + "failed to remove existing NuGet artefacts from {}", + artefact_root.display() + ) + })?; + } + } + + let mut restore = Command::new("dotnet"); + restore.current_dir(&project_dir); + restore.arg("restore"); + run_command(restore, "dotnet restore")?; + + let mut build = Command::new("dotnet"); + build.current_dir(&project_dir); + build.arg("build"); + build.arg("--no-restore"); + build.arg("-c"); + build.arg(configuration); + build.arg("--verbosity"); + build.arg("minimal"); + build.arg(&dir_arg); + build.arg(&profile_arg); + if ignore_missing { + build.arg("/p:IgnoreMissingArtifacts=true"); + } + run_command(build, "dotnet build")?; + + let mut pack = Command::new("dotnet"); + pack.current_dir(&project_dir); + pack.arg("pack"); + pack.arg("--no-build"); + pack.arg("-c"); + pack.arg(configuration); + pack.arg(&dir_arg); + pack.arg(&profile_arg); + if ignore_missing { + pack.arg("/p:IgnoreMissingArtifacts=true"); + } + run_command(pack, "dotnet pack")?; + + Ok(project_dir.join("bin").join(configuration)) +} + +impl BuildNugetCommand { + /// Entry point executed by the xtask harness. + pub fn run(&self) -> Result<()> { + let config = self.to_config(); + let result = build_nuget_package(&config)?; + + println!( + "NuGet package(s) available under {}", + result.package_dir.display() + ); + + if result.packages.is_empty() { + println!("No NuGet packages were produced; check earlier log output."); + } else { + print_package_listing(&result.packages)?; + } + + Ok(()) + } + + pub fn to_config(&self) -> BuildNugetConfig { + BuildNugetConfig { + targets: self.targets.clone(), + release: self.release, + clean: self.clean, + artifacts_dir: self.artifacts_dir.clone(), + enforce_artifacts: self.enforce_artifacts, + } + } +} + +fn print_package_listing(packages: &[PathBuf]) -> Result<()> { + for package in packages { + println!("Contents of {}:", package.display()); + let file = + File::open(package).with_context(|| format!("failed to open {}", package.display()))?; + let mut archive = ZipArchive::new(file) + .with_context(|| format!("failed to read zip archive from {}", package.display()))?; + + let mut entries = Vec::new(); + for index in 0..archive.len() { + let file = archive.by_index(index).with_context(|| { + format!("failed to access entry {index} in {}", package.display()) + })?; + entries.push(file.name().to_string()); + } + + entries.sort(); + for entry in entries { + println!(" {}", entry); + } + } + + Ok(()) +} + +/// Builds (if required) and tests the C# bindings against the packaged NuGet. +#[derive(Args, Clone)] +pub struct TestCsharpCommand { + /// Build the Rust FFI artefacts for the provided target triple (defaults to the host). + #[arg(long = "target", value_name = "TRIPLE")] + pub targets: Vec, + + /// Build and pack using the Release configuration (defaults to Debug). + #[arg(long)] + pub release: bool, + + /// Remove existing build artefacts before running the command. + #[arg(long)] + pub clean: bool, + + /// Override the directory that contains compiled regorus FFI artefacts. + #[arg(long, value_name = "PATH")] + pub artifacts_dir: Option, + + /// Require all platform artefacts to exist before packing. + #[arg(long)] + pub enforce_artifacts: bool, + + /// Always rebuild the NuGet package instead of reusing an existing archive. + #[arg(long)] + pub force_nuget: bool, + + /// Restore and test using Regorus NuGet artefacts located at DIR. Defaults to bindings/csharp/Regorus/bin/. + #[arg(long = "nuget-dir", value_name = "DIR")] + pub nuget_dir: Option, +} + +impl TestCsharpCommand { + pub fn run(&self) -> Result<()> { + let workspace = workspace_root(); + let configuration = if self.release { "Release" } else { "Debug" }; + if self.nuget_dir.is_some() && self.force_nuget { + return Err(anyhow!( + "--force-nuget cannot be combined with --nuget-dir; build outputs always land in the default directory" + )); + } + + let mut package_dir = if let Some(dir) = &self.nuget_dir { + let path = PathBuf::from(dir); + if path.is_absolute() { + path + } else { + workspace.join(path) + } + } else { + workspace + .join("bindings/csharp/Regorus/bin") + .join(configuration) + }; + + let build_config = BuildNugetConfig { + targets: self.targets.clone(), + release: self.release, + clean: self.clean, + artifacts_dir: self.artifacts_dir.clone(), + enforce_artifacts: self.enforce_artifacts, + }; + + let mut packages = find_packages(&package_dir)?; + if self.force_nuget || (packages.is_empty() && self.nuget_dir.is_none()) { + println!( + "{} NuGet package(s); invoking build...", + if self.force_nuget { + "Forcing rebuild of" + } else { + "Missing" + } + ); + let build = build_nuget_package(&build_config)?; + package_dir = build.package_dir; + packages = build.packages; + } else { + println!( + "Reusing existing NuGet package(s) under {}.", + package_dir.display() + ); + } + + if packages.is_empty() { + return Err(anyhow!( + "No NuGet packages are available under {}; supply --nuget-dir with a populated directory or omit it to let xtask build the package", + package_dir.display() + )); + } + + println!("Using NuGet package(s):"); + for package in &packages { + println!(" {}", package.display()); + } + + run_regorus_tests(&workspace, configuration, &package_dir, self.clean)?; + + Ok(()) + } +} + +fn run_regorus_tests( + workspace: &Path, + configuration: &str, + package_dir: &Path, + clean: bool, +) -> Result<()> { + let nuget_source = package_dir + .to_str() + .ok_or_else(|| anyhow!("NuGet directory path contains invalid UTF-8"))?; + let properties = vec![format!( + "/p:RestoreAdditionalProjectSources={}", + nuget_source + )]; + let property_args: Vec<&str> = properties.iter().map(|value| value.as_str()).collect(); + + let regorus_tests = workspace.join("bindings/csharp/Regorus.Tests"); + if clean { + clean_msbuild_project(®orus_tests, configuration)?; + } + restore_with_source(®orus_tests, &property_args, "Regorus.Tests")?; + + let mut test = Command::new("dotnet"); + test.current_dir(®orus_tests); + test.arg("test"); + test.arg("--no-restore"); + test.arg("-c"); + test.arg(configuration); + test.arg("--arch"); + test.arg(dotnet_host_arch()); + run_command(test, "dotnet test (Regorus.Tests)")?; + + let test_app = workspace.join("bindings/csharp/TestApp"); + if clean { + clean_msbuild_project(&test_app, configuration)?; + } + restore_with_source(&test_app, &property_args, "TestApp")?; + let mut build = Command::new("dotnet"); + build.current_dir(&test_app); + build.arg("build"); + build.arg("--no-restore"); + build.arg("-c"); + build.arg(configuration); + build.arg("--arch"); + build.arg(dotnet_host_arch()); + run_command(build, "dotnet build (TestApp)")?; + + let mut run = Command::new("dotnet"); + run.current_dir(&test_app); + run.arg("run"); + run.arg("--no-build"); + run.arg("--framework"); + run.arg("net8.0"); + run.arg("-c"); + run.arg(configuration); + run.arg("--arch"); + run.arg(dotnet_host_arch()); + run_command(run, "dotnet run (TestApp)")?; + + let target_example = workspace.join("bindings/csharp/TargetExampleApp"); + if clean { + clean_msbuild_project(&target_example, configuration)?; + } + restore_with_source(&target_example, &property_args, "TargetExampleApp")?; + let mut build_example = Command::new("dotnet"); + build_example.current_dir(&target_example); + build_example.arg("build"); + build_example.arg("--no-restore"); + build_example.arg("-c"); + build_example.arg(configuration); + build_example.arg("--arch"); + build_example.arg(dotnet_host_arch()); + run_command(build_example, "dotnet build (TargetExampleApp)")?; + + let mut run_example = Command::new("dotnet"); + run_example.current_dir(&target_example); + run_example.arg("run"); + run_example.arg("--no-build"); + run_example.arg("--framework"); + run_example.arg("net8.0"); + run_example.arg("-c"); + run_example.arg(configuration); + run_example.arg("--arch"); + run_example.arg(dotnet_host_arch()); + run_command(run_example, "dotnet run (TargetExampleApp)")?; + + Ok(()) +} + +fn restore_with_source(project_dir: &Path, properties: &[&str], label: &str) -> Result<()> { + let mut restore = Command::new("dotnet"); + restore.current_dir(project_dir); + restore.arg("restore"); + restore.arg("--arch"); + restore.arg(dotnet_host_arch()); + for property in properties { + restore.arg(property); + } + run_command(restore, &format!("dotnet restore ({label})")) +} + +fn clean_msbuild_project(project_dir: &Path, configuration: &str) -> Result<()> { + if !project_dir.exists() { + return Ok(()); + } + + let mut clean = Command::new("dotnet"); + clean.current_dir(project_dir); + clean.arg("clean"); + clean.arg("-c"); + clean.arg(configuration); + clean.arg("--verbosity"); + clean.arg("minimal"); + run_command(clean, "dotnet clean")?; + + let bin_dir = project_dir.join("bin"); + if bin_dir.exists() { + fs::remove_dir_all(&bin_dir).with_context(|| { + format!( + "failed to remove bin directory at {} while cleaning", + bin_dir.display() + ) + })?; + } + + let obj_dir = project_dir.join("obj"); + if obj_dir.exists() { + fs::remove_dir_all(&obj_dir).with_context(|| { + format!( + "failed to remove obj directory at {} while cleaning", + obj_dir.display() + ) + })?; + } + + Ok(()) +} diff --git a/xtask/src/tasks/bindings/ffi.rs b/xtask/src/tasks/bindings/ffi.rs new file mode 100644 index 0000000..f09531d --- /dev/null +++ b/xtask/src/tasks/bindings/ffi.rs @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::ffi::OsString; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use anyhow::{anyhow, bail, Context, Result}; +use clap::Args; + +use crate::tasks::util::{dedup, run_cargo_step, workspace_root}; + +/// Builds the regorus FFI crate for the selected targets. +#[derive(Args)] +pub struct BuildFfiCommand { + /// Target triples to compile (defaults to the current host when omitted). + #[arg(long = "target", value_name = "TRIPLE")] + targets: Vec, + + /// Build in release mode instead of debug. + #[arg(long)] + release: bool, +} + +impl BuildFfiCommand { + pub fn run(&self) -> Result<()> { + let workspace_root = workspace_root(); + let targets = resolve_targets(self.targets.clone())?; + let profile = profile_dir(self.release); + + build_targets(&workspace_root, &targets, self.release)?; + + let base = workspace_root.join("bindings/ffi/target"); + if targets.len() == 1 { + println!( + "Built FFI artefacts at {}", + artifact_path(&base, targets[0].as_str(), profile).display() + ); + } else { + println!("Built FFI artefacts:"); + for triple in &targets { + println!( + " {} -> {}", + triple, + artifact_path(&base, triple, profile).display() + ); + } + } + + Ok(()) + } +} + +/// Resolves the list of target triples to compile, defaulting to the host. +pub fn resolve_targets(mut targets: Vec) -> Result> { + if targets.is_empty() { + targets.push(detect_host_triple()?); + } + + dedup(&mut targets); + Ok(targets) +} + +/// Compiles the FFI crate for the supplied target triples. +pub fn build_targets(root: &Path, targets: &[String], release: bool) -> Result<()> { + for target in targets { + cargo_build(root, target, release)?; + } + Ok(()) +} + +/// Returns the cargo profile directory associated with the release flag. +pub fn profile_dir(release: bool) -> &'static str { + if release { + "release" + } else { + "debug" + } +} + +pub fn detect_host_triple() -> Result { + let output = Command::new("rustc") + .arg("-Vv") + .output() + .context("failed to invoke rustc")?; + + if !output.status.success() { + bail!( + "rustc -Vv failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + + let stdout = String::from_utf8(output.stdout).context("rustc output was not valid UTF-8")?; + for line in stdout.lines() { + if let Some(rest) = line.strip_prefix("host: ") { + return Ok(rest.trim().to_string()); + } + } + + Err(anyhow!("failed to detect host target triple")) +} + +fn cargo_build(root: &Path, target: &str, release: bool) -> Result<()> { + let dir = root.join("bindings/ffi"); + let mut args = vec![ + OsString::from("build"), + OsString::from("--locked"), + OsString::from("--target"), + OsString::from(target), + ]; + if release { + args.push(OsString::from("--release")); + } + + let title = format!("cargo build (ffi:{target})"); + run_cargo_step(&dir, &title, args) +} + +fn artifact_path(base: &Path, target: &str, profile: &str) -> PathBuf { + base.join(target).join(profile) +} + +/// Executes the FFI binding test suite. +#[derive(Args, Default)] +pub struct TestFfiCommand { + /// Build the FFI crate in release mode prior to testing. + #[arg(long)] + pub release: bool, + + /// Pass --frozen to all cargo invocations. + #[arg(long)] + pub frozen: bool, +} + +impl TestFfiCommand { + pub fn run(&self) -> Result<()> { + let workspace = workspace_root(); + let ffi_dir = workspace.join("bindings/ffi"); + + let mut build_args = vec![OsString::from("build"), OsString::from("--locked")]; + if self.release { + build_args.push(OsString::from("--release")); + } + if self.frozen { + build_args.push(OsString::from("--frozen")); + } + let build_label = if self.release { + "cargo build --release (bindings/ffi)" + } else { + "cargo build (bindings/ffi)" + }; + run_cargo_step(&ffi_dir, build_label, build_args)?; + + let mut test_args = vec![OsString::from("test"), OsString::from("--locked")]; + if self.release { + test_args.push(OsString::from("--release")); + } + if self.frozen { + test_args.push(OsString::from("--frozen")); + } + test_args.push(OsString::from("--features")); + test_args.push(OsString::from("contention_checks")); + run_cargo_step( + &ffi_dir, + "cargo test --features contention_checks (bindings/ffi)", + test_args, + )?; + + Ok(()) + } +} diff --git a/xtask/src/tasks/bindings/go.rs b/xtask/src/tasks/bindings/go.rs new file mode 100644 index 0000000..9efccac --- /dev/null +++ b/xtask/src/tasks/bindings/go.rs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +use anyhow::{anyhow, Result}; +use clap::Args; + +use super::c::prepare_ffi_artifacts; +use crate::tasks::util::{add_library_search_path, run_command, workspace_root}; + +/// Builds the Go binding sample and runs its smoke test. +#[derive(Args, Default)] +pub struct TestGoCommand { + /// Build the FFI crate in release mode before exercising the Go sample. + #[arg(long)] + pub release: bool, + + /// Skip rebuilding the FFI artefacts and reuse existing outputs. + #[arg(long)] + pub skip_ffi: bool, + + /// Pass --frozen to the preparatory cargo invocations. + #[arg(long)] + pub frozen: bool, +} + +impl TestGoCommand { + pub fn run(&self) -> Result<()> { + let workspace = workspace_root(); + let ffi_dir = workspace.join("bindings/ffi"); + let go_dir = workspace.join("bindings/go"); + let profile = if self.release { "release" } else { "debug" }; + + if !self.skip_ffi { + prepare_ffi_artifacts(self.release, self.frozen)?; + } + + run_go_command(&go_dir, ["mod", "tidy"], "go mod tidy (bindings/go)")?; + run_go_command(&go_dir, ["build"], "go build (bindings/go)")?; + + let binary = go_test_binary(&go_dir); + if !binary.exists() { + return Err(anyhow!( + "expected Go test binary at {} after build", + binary.display() + )); + } + + let lib_dir = ffi_dir.join("target").join(profile); + let mut run = Command::new(&binary); + run.current_dir(&go_dir); + add_library_search_path(&mut run, &lib_dir); + run_command(run, "regorus_test (bindings/go)") + } +} + +fn run_go_command(dir: &Path, args: [&str; N], label: &str) -> Result<()> { + let mut cmd = Command::new("go"); + cmd.current_dir(dir); + for arg in args { + cmd.arg(arg); + } + run_command(cmd, label) +} + +fn go_test_binary(dir: &Path) -> PathBuf { + if cfg!(windows) { + dir.join("regorus_test.exe") + } else { + dir.join("regorus_test") + } +} diff --git a/xtask/src/tasks/bindings/java.rs b/xtask/src/tasks/bindings/java.rs new file mode 100644 index 0000000..ee15fd3 --- /dev/null +++ b/xtask/src/tasks/bindings/java.rs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use anyhow::{anyhow, Context, Result}; +use clap::Args; + +use crate::tasks::util::{path_separator, run_cargo_step, run_command, workspace_root}; + +#[derive(Args, Default)] +pub struct BuildJavaCommand { + /// Skip the Maven test phase while packaging. + #[arg(long)] + pub skip_tests: bool, +} + +impl BuildJavaCommand { + pub fn run(&self) -> Result<()> { + let workspace = workspace_root(); + let java_dir = workspace.join("bindings/java"); + + run_command(self.into_command(&java_dir)?, "mvn package (bindings/java)") + } + + fn into_command(&self, java_dir: &Path) -> Result { + let mut mvn = Command::new("mvn"); + mvn.current_dir(java_dir); + mvn.arg("--batch-mode"); + mvn.arg("--no-transfer-progress"); + mvn.arg("package"); + if self.skip_tests { + mvn.arg("-DskipTests"); + } + Ok(mvn) + } +} + +#[derive(Args, Default)] +pub struct TestJavaCommand { + /// Build the JNI artefacts in release mode before testing. + #[arg(long)] + pub release: bool, + + /// Propagate --frozen to cargo invocations. + #[arg(long)] + pub frozen: bool, +} + +impl TestJavaCommand { + pub fn run(&self) -> Result<()> { + let workspace = workspace_root(); + let java_dir = workspace.join("bindings/java"); + + build_java_crate(&workspace, self.release, self.frozen)?; + run_command( + BuildJavaCommand { skip_tests: false }.into_command(&java_dir)?, + "mvn package (bindings/java)", + )?; + run_java_integration(&java_dir, self.release) + } +} +fn build_java_crate(workspace: &Path, release: bool, frozen: bool) -> Result<()> { + let mut args = vec!["build"]; + if release { + args.push("--release"); + } + if frozen { + args.push("--frozen"); + } + args.push("--manifest-path"); + args.push("bindings/java/Cargo.toml"); + args.push("--locked"); + run_cargo_step(workspace, "cargo build (bindings/java)", args) +} + +fn run_java_integration(java_dir: &Path, release: bool) -> Result<()> { + let jar = locate_java_jar(java_dir)?; + let separator = path_separator(); + let classpath = format!("{}{}.", jar.display(), separator); + + let mut javac = Command::new("javac"); + javac.current_dir(java_dir); + javac.arg("-cp"); + javac.arg(&classpath); + javac.arg("Test.java"); + run_command(javac, "javac Test.java (bindings/java)")?; + + let profile = if release { "release" } else { "debug" }; + let lib_path = java_dir.join("target").join(profile); + let mut java = Command::new("java"); + java.current_dir(java_dir); + java.arg(format!("-Djava.library.path={}", lib_path.display())); + java.arg("-cp"); + java.arg(&classpath); + java.arg("Test"); + run_command(java, "java Test (bindings/java)") +} + +fn locate_java_jar(java_dir: &Path) -> Result { + let target_dir = java_dir.join("target"); + let entries = fs::read_dir(&target_dir).with_context(|| { + format!( + "failed to enumerate built JARs under {}", + target_dir.display() + ) + })?; + + let mut candidates = Vec::new(); + for entry in entries { + let entry = entry?; + let path = entry.path(); + if !path.is_file() { + continue; + } + let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else { + continue; + }; + if !file_name.starts_with("regorus-java-") || !file_name.ends_with(".jar") { + continue; + } + if file_name.contains("-sources") || file_name.contains("-javadoc") { + continue; + } + candidates.push(path); + } + + candidates.sort(); + candidates + .pop() + .ok_or_else(|| anyhow!("no regorus-java jar found under {}", target_dir.display())) +} diff --git a/xtask/src/tasks/bindings/mod.rs b/xtask/src/tasks/bindings/mod.rs new file mode 100644 index 0000000..0782af3 --- /dev/null +++ b/xtask/src/tasks/bindings/mod.rs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +pub mod all; +pub mod c; +pub mod cpp; +pub mod csharp; +pub mod ffi; +pub mod go; +pub mod java; +pub mod python; +pub mod ruby; +pub mod version; +pub mod wasm; + +pub use all::{BuildAllBindingsCommand, TestAllBindingsCommand}; +pub use c::{TestCCommand, TestCNoStdCommand}; +pub use cpp::TestCppCommand; +pub use csharp::{BuildNugetCommand, TestCsharpCommand}; +pub use ffi::{BuildFfiCommand, TestFfiCommand}; +pub use go::TestGoCommand; +pub use java::{BuildJavaCommand, TestJavaCommand}; +pub use python::{BuildPythonCommand, TestPythonCommand}; +pub use ruby::TestRubyCommand; +pub use version::BindingsCommand; +pub use wasm::{BuildWasmCommand, TestWasmCommand}; diff --git a/xtask/src/tasks/bindings/python.rs b/xtask/src/tasks/bindings/python.rs new file mode 100644 index 0000000..59eb243 --- /dev/null +++ b/xtask/src/tasks/bindings/python.rs @@ -0,0 +1,283 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::ffi::OsString; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use anyhow::{anyhow, bail, Context, Result}; +use clap::Args; + +use crate::tasks::util::{path_separator, run_cargo_step, run_command, workspace_root}; + +const MIN_PYTHON_VERSION: (u32, u32) = (3, 10); + +#[derive(Args, Default)] +pub struct BuildPythonCommand { + /// Optimise the wheel artefacts (defaults to debug builds). + #[arg(long)] + pub release: bool, + + /// Cross-compile for the provided Rust target triple. + #[arg(long, value_name = "TRIPLE")] + pub target: Option, + + /// Override the directory that receives built wheel files. + #[arg(long, value_name = "PATH")] + pub target_dir: Option, + + /// Propagate --frozen to cargo invocations prior to packaging. + #[arg(long)] + pub frozen: bool, +} + +impl BuildPythonCommand { + pub fn run(&self) -> Result<()> { + let workspace = workspace_root(); + let python_dir = workspace.join("bindings/python"); + + build_python_crate( + &python_dir, + self.release, + self.target.as_deref(), + self.frozen, + )?; + + let wheel_dir = self + .target_dir + .clone() + .unwrap_or_else(|| python_dir.join("wheels")); + fs::create_dir_all(&wheel_dir).with_context(|| { + format!( + "failed to create Python wheel directory at {}", + wheel_dir.display() + ) + })?; + + let mut maturin = Command::new("maturin"); + maturin.current_dir(&python_dir); + maturin.arg("build"); + if self.release { + maturin.arg("--release"); + } + if let Some(target) = &self.target { + maturin.arg("--target"); + maturin.arg(target); + } + maturin.arg("--target-dir"); + maturin.arg(&wheel_dir); + run_command(maturin, "maturin build (bindings/python)") + } +} + +#[derive(Args, Default)] +pub struct TestPythonCommand { + /// Optimise the extension module used for testing (defaults to debug builds). + #[arg(long)] + pub release: bool, + + /// Cross-compile for the provided Rust target triple. + #[arg(long, value_name = "TRIPLE")] + pub target: Option, + + /// Python interpreter used to run the sample and tests. + #[arg(long, value_name = "EXE", default_value = "python3")] + pub python: String, +} + +impl TestPythonCommand { + pub fn run(&self) -> Result<()> { + let workspace = workspace_root(); + let python_dir = workspace.join("bindings/python"); + + let (venv_python, venv_dir) = ensure_virtual_env(&python_dir, &self.python)?; + + install_testing_dependencies(&venv_python)?; + + install_local_package(&python_dir, self.release, self.target.as_deref(), &venv_dir)?; + + let mut sample = Command::new(&venv_python); + sample.current_dir(&python_dir); + sample.arg("test.py"); + let sample_label = format!("{} test.py (bindings/python)", venv_python.display()); + run_command(sample, &sample_label)?; + + let mut pytest = Command::new(&venv_python); + pytest.current_dir(&python_dir); + pytest.arg("-m"); + pytest.arg("pytest"); + pytest.arg("test_extensions.py"); + let pytest_label = format!( + "{} -m pytest test_extensions.py (bindings/python)", + venv_python.display() + ); + run_command(pytest, &pytest_label) + } +} + +fn install_testing_dependencies(venv_python: &Path) -> Result<()> { + let mut install = Command::new(venv_python); + install.arg("-m"); + install.arg("pip"); + install.arg("install"); + install.arg("pytest"); + let label = format!( + "{} -m pip install pytest (bindings/python)", + venv_python.display() + ); + run_command(install, &label) +} + +fn install_local_package( + python_dir: &Path, + release: bool, + target: Option<&str>, + venv_dir: &Path, +) -> Result<()> { + let mut maturin = Command::new("maturin"); + maturin.current_dir(python_dir); + maturin.arg("develop"); + if release { + maturin.arg("--release"); + } + if let Some(target) = target { + maturin.arg("--target"); + maturin.arg(target); + } + maturin.env("VIRTUAL_ENV", venv_dir); + let bin_dir = venv_bin_dir(venv_dir); + let mut path_value = bin_dir.clone().into_os_string(); + if let Some(existing) = std::env::var_os("PATH") { + if !existing.is_empty() { + path_value.push(path_separator()); + path_value.push(existing); + } + } + maturin.env("PATH", path_value); + run_command(maturin, "maturin develop (bindings/python)") +} + +fn build_python_crate( + python_dir: &Path, + release: bool, + target: Option<&str>, + frozen: bool, +) -> Result<()> { + let mut args = Vec::new(); + args.push(OsString::from("build")); + if release { + args.push(OsString::from("--release")); + } + if let Some(target) = target { + args.push(OsString::from("--target")); + args.push(OsString::from(target)); + } + if frozen { + args.push(OsString::from("--frozen")); + } + args.push(OsString::from("--locked")); + run_cargo_step(python_dir, "cargo build (bindings/python)", args) +} + +fn ensure_virtual_env(python_dir: &Path, python: &str) -> Result<(PathBuf, PathBuf)> { + ensure_python_version(std::ffi::OsStr::new(python), python)?; + + let venv_dir = python_dir.join(".venv"); + if venv_dir.exists() { + let existing_python = venv_bin_dir(&venv_dir).join(venv_python_name()); + if existing_python.exists() + && ensure_python_version( + existing_python.as_os_str(), + &existing_python.display().to_string(), + ) + .is_ok() + { + return Ok((existing_python, venv_dir)); + } + + fs::remove_dir_all(&venv_dir).with_context(|| { + format!( + "failed to remove incompatible virtual environment at {}", + venv_dir.display() + ) + })?; + } + + let mut create = Command::new(python); + create.current_dir(python_dir); + create.arg("-m"); + create.arg("venv"); + create.arg(".venv"); + let label = format!("{} -m venv .venv (bindings/python)", python); + run_command(create, &label)?; + + let python_path = venv_bin_dir(&venv_dir).join(venv_python_name()); + ensure_python_version(python_path.as_os_str(), &python_path.display().to_string())?; + + Ok((python_path, venv_dir)) +} + +fn venv_bin_dir(venv_dir: &Path) -> PathBuf { + if cfg!(windows) { + venv_dir.join("Scripts") + } else { + venv_dir.join("bin") + } +} + +fn venv_python_name() -> &'static str { + if cfg!(windows) { + "python.exe" + } else { + "python" + } +} + +fn ensure_python_version(executable: &std::ffi::OsStr, label: &str) -> Result<()> { + let (major, minor) = query_python_version(executable, label)?; + if (major, minor) < MIN_PYTHON_VERSION { + bail!( + "Python interpreter {} reports version {}.{}; the bindings require Python >= {}.{}. Use --python to provide a compatible interpreter.", + label, + major, + minor, + MIN_PYTHON_VERSION.0, + MIN_PYTHON_VERSION.1 + ); + } + Ok(()) +} + +fn query_python_version(executable: &std::ffi::OsStr, label: &str) -> Result<(u32, u32)> { + let output = Command::new(executable) + .arg("-c") + .arg("import sys; print(f'{sys.version_info[0]}.{sys.version_info[1]}')") + .output() + .with_context(|| format!("failed to query Python version from {}", label))?; + + if !output.status.success() { + bail!( + "{} -c 'import sys; ...' exited with status {}", + label, + output.status + ); + } + + let stdout = String::from_utf8(output.stdout) + .with_context(|| format!("failed to decode Python version output from {}", label))?; + let trimmed = stdout.trim(); + let mut parts = trimmed.split('.'); + let major = parts + .next() + .ok_or_else(|| anyhow!("missing major version in '{}'", trimmed))? + .parse::() + .with_context(|| format!("failed to parse major version from '{}'", trimmed))?; + let minor = parts + .next() + .ok_or_else(|| anyhow!("missing minor version in '{}'", trimmed))? + .parse::() + .with_context(|| format!("failed to parse minor version from '{}'", trimmed))?; + + Ok((major, minor)) +} diff --git a/xtask/src/tasks/bindings/ruby.rs b/xtask/src/tasks/bindings/ruby.rs new file mode 100644 index 0000000..86e7bc1 --- /dev/null +++ b/xtask/src/tasks/bindings/ruby.rs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::ffi::OsString; +use std::process::Command; + +use anyhow::Result; +use clap::Args; + +use crate::tasks::util::{run_cargo_step, run_command, workspace_root}; + +/// Runs the Ruby binding smoke tests. +#[derive(Args, Default)] +pub struct TestRubyCommand { + /// Skip installing the bundler gem before running tests. + #[arg(long)] + pub skip_bundler_install: bool, + + /// Skip the Rust clippy pass prior to executing the Ruby test suite. + #[arg(long)] + pub skip_clippy: bool, + + /// Propagate --frozen to cargo invocations ahead of the Ruby test suite. + #[arg(long)] + pub frozen: bool, +} + +impl TestRubyCommand { + pub fn run(&self) -> Result<()> { + let workspace = workspace_root(); + let ruby_dir = workspace.join("bindings/ruby"); + + if !self.skip_bundler_install { + let mut gem = Command::new("gem"); + gem.current_dir(&ruby_dir); + gem.arg("install"); + gem.arg("bundler"); + gem.arg("--user-install"); + run_command(gem, "gem install bundler (bindings/ruby)")?; + } + + let mut bundle_install = Command::new("bundle"); + bundle_install.current_dir(&ruby_dir); + bundle_install.arg("install"); + run_command(bundle_install, "bundle install (bindings/ruby)")?; + + if !self.skip_clippy { + let mut clippy_args = vec![ + OsString::from("clippy"), + OsString::from("--all-targets"), + OsString::from("--no-deps"), + ]; + if self.frozen { + clippy_args.insert(1, OsString::from("--frozen")); + } + clippy_args.push(OsString::from("--")); + clippy_args.push(OsString::from("-Dwarnings")); + + run_cargo_step( + &ruby_dir, + "cargo clippy --all-targets --no-deps -- -Dwarnings (bindings/ruby)", + clippy_args, + )?; + } + + let mut rake = Command::new("bundle"); + rake.current_dir(&ruby_dir); + rake.arg("exec"); + rake.arg("rake"); + run_command(rake, "bundle exec rake (bindings/ruby)") + } +} diff --git a/xtask/src/tasks/bindings.rs b/xtask/src/tasks/bindings/version.rs similarity index 100% rename from xtask/src/tasks/bindings.rs rename to xtask/src/tasks/bindings/version.rs diff --git a/xtask/src/tasks/bindings/wasm.rs b/xtask/src/tasks/bindings/wasm.rs new file mode 100644 index 0000000..1b282b5 --- /dev/null +++ b/xtask/src/tasks/bindings/wasm.rs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +use anyhow::Result; +use clap::Args; + +use crate::tasks::util::{run_command, workspace_root}; + +#[derive(Args, Default)] +pub struct BuildWasmCommand { + /// Optimise the generated WASM artefacts (defaults to debug builds). + #[arg(long)] + pub release: bool, + + /// Target environment passed to wasm-pack (defaults to nodejs). + #[arg(long, value_name = "TARGET", default_value = "nodejs")] + pub target: String, + + /// Override the directory that receives wasm-pack outputs. + #[arg(long, value_name = "PATH")] + pub out_dir: Option, +} + +impl BuildWasmCommand { + pub fn run(&self) -> Result<()> { + let workspace = workspace_root(); + let wasm_dir = workspace.join("bindings/wasm"); + build_wasm( + &wasm_dir, + self.release, + &self.target, + self.out_dir.as_deref(), + ) + } +} + +#[derive(Args, Default)] +pub struct TestWasmCommand { + /// Optimise the generated WASM artefacts (defaults to debug builds). + #[arg(long)] + pub release: bool, + + /// Target environment passed to wasm-pack (defaults to nodejs). + #[arg(long, value_name = "TARGET", default_value = "nodejs")] + pub target: String, + + /// Override the directory that receives wasm-pack outputs. + #[arg(long, value_name = "PATH")] + pub out_dir: Option, + + /// Node.js executable used for the sample script. + #[arg(long, value_name = "EXE", default_value = "node")] + pub node: String, + + /// Accepted for compatibility with CI; ignored because wasm-pack controls builds. + #[arg(long)] + pub frozen: bool, + + /// Skip rebuilding the wasm artefacts before running the sample. + #[arg(long)] + pub skip_build: bool, +} + +impl TestWasmCommand { + pub fn run(&self) -> Result<()> { + let workspace = workspace_root(); + let wasm_dir = workspace.join("bindings/wasm"); + + if !self.skip_build { + build_wasm( + &wasm_dir, + self.release, + &self.target, + self.out_dir.as_deref(), + )?; + } + + run_wasm_tests(&wasm_dir, self.release)?; + + let mut node = Command::new(&self.node); + node.current_dir(&wasm_dir); + node.arg("test.js"); + let label = format!("{} test.js (bindings/wasm)", self.node); + run_command(node, &label) + } +} + +fn build_wasm(wasm_dir: &Path, release: bool, target: &str, out_dir: Option<&Path>) -> Result<()> { + let mut pack = Command::new("wasm-pack"); + pack.current_dir(wasm_dir); + pack.arg("build"); + pack.arg("--target"); + pack.arg(target); + if release { + pack.arg("--release"); + } + if let Some(out_dir) = out_dir { + pack.arg("--out-dir"); + pack.arg(out_dir); + } + run_command(pack, "wasm-pack build (bindings/wasm)") +} +fn run_wasm_tests(wasm_dir: &Path, release: bool) -> Result<()> { + let mut test = Command::new("wasm-pack"); + test.current_dir(wasm_dir); + test.arg("test"); + test.arg("--node"); + if release { + test.arg("--release"); + } + run_command(test, "wasm-pack test --node (bindings/wasm)") +} diff --git a/xtask/src/tasks/ci/mod.rs b/xtask/src/tasks/ci/mod.rs new file mode 100644 index 0000000..7250a0d --- /dev/null +++ b/xtask/src/tasks/ci/mod.rs @@ -0,0 +1,366 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::ffi::OsString; +use std::path::Path; + +use anyhow::Result; +use clap::Args; + +use crate::tasks::util::{ + opa_passing_arguments, run_cargo_step as util_run_cargo_step, workspace_root, +}; + +pub mod musl; + +pub use musl::TestMuslCommand; + +const DEFAULT_OPA_FEATURES: &str = "opa-testutil,serde_json/arbitrary_precision"; + +/// Mirrors the release-focused GitHub workflow locally. +#[derive(Args, Default)] +pub struct CiReleaseCommand { + /// Propagate --frozen to cargo invocations (match CI by passing --frozen). + #[arg(long)] + pub frozen: bool, + + /// Enable the supplied feature list across build and test steps (comma separated). + #[arg(long, value_delimiter = ',', value_name = "FEATURE")] + pub features: Vec, + + /// Override the feature set passed to the OPA tests. + #[arg(long, value_name = "FEATURES")] + pub opa_features: Option, + + /// Skip the `cargo build --all-features` phase. + #[arg(long)] + pub skip_all_features_build: bool, + + /// Skip the `cargo test --no-default-features` phase. + #[arg(long)] + pub skip_no_default_features_tests: bool, + + /// Skip the Azure Policy feature tests. + #[arg(long)] + pub skip_azure_policy: bool, + + /// Skip the Azure RBAC feature tests. + #[arg(long)] + pub skip_azure_rbac: bool, +} + +/// Mirrors the debug-focused GitHub workflow locally. +#[derive(Args, Default)] +pub struct CiDebugCommand { + /// Propagate --frozen to cargo invocations (match CI by passing --frozen). + #[arg(long)] + pub frozen: bool, +} + +impl CiReleaseCommand { + pub fn run(&self) -> Result<()> { + let opa_features = self + .opa_features + .clone() + .unwrap_or_else(|| DEFAULT_OPA_FEATURES.to_string()); + + run_ci_suite(CiSuiteConfig { + release: true, + frozen: self.frozen, + include_fmt: true, + include_run_example: true, + include_azure_policy: !self.skip_azure_policy, + include_azure_rbac: !self.skip_azure_rbac, + include_all_features_build: !self.skip_all_features_build, + include_no_default_features_tests: !self.skip_no_default_features_tests, + base_features: self.features.clone(), + opa_features, + }) + } +} + +impl CiDebugCommand { + pub fn run(&self) -> Result<()> { + run_ci_suite(CiSuiteConfig { + release: false, + frozen: self.frozen, + include_fmt: false, + include_run_example: false, + include_azure_policy: false, + include_azure_rbac: true, + include_all_features_build: true, + include_no_default_features_tests: true, + base_features: Vec::new(), + opa_features: DEFAULT_OPA_FEATURES.to_string(), + }) + } +} + +struct CiSuiteConfig { + release: bool, + frozen: bool, + include_fmt: bool, + include_run_example: bool, + include_azure_policy: bool, + include_azure_rbac: bool, + include_all_features_build: bool, + include_no_default_features_tests: bool, + base_features: Vec, + opa_features: String, +} + +fn run_ci_suite(config: CiSuiteConfig) -> Result<()> { + let workspace = workspace_root(); + let joined_features = join_features(&config.base_features); + + if config.include_fmt { + run_fmt(&workspace)?; + } + + if config.include_all_features_build { + run_ci_cargo_step( + &workspace, + "build", + config.release, + config.frozen, + None, + &["--all-features"], + "cargo build --all-features (ci)", + )?; + } + + run_ci_cargo_step( + &workspace, + "build", + config.release, + config.frozen, + joined_features.as_deref(), + &[], + "cargo build (ci)", + )?; + + if config.include_no_default_features_tests { + run_ci_cargo_step( + &workspace, + "test", + config.release, + config.frozen, + None, + &["--no-default-features"], + "cargo test --no-default-features (ci)", + )?; + } + + let example_features = example_features(&config.base_features); + let example_label = format!( + "cargo build --example regorus --no-default-features --features {} (ci)", + example_features + ); + run_ci_cargo_step( + &workspace, + "build", + config.release, + config.frozen, + Some(&example_features), + &["--example", "regorus", "--no-default-features"], + &example_label, + )?; + + run_ci_cargo_step( + &workspace, + "test", + config.release, + config.frozen, + joined_features.as_deref(), + &["--doc"], + "cargo test --doc (ci)", + )?; + + run_ci_cargo_step( + &workspace, + "test", + config.release, + config.frozen, + joined_features.as_deref(), + &[], + "cargo test (ci)", + )?; + + if config.include_run_example { + run_example( + &workspace, + config.release, + config.frozen, + joined_features.as_deref(), + )?; + } + + run_named_test( + &workspace, + config.release, + config.frozen, + joined_features.as_deref(), + "aci", + )?; + + run_named_test( + &workspace, + config.release, + config.frozen, + joined_features.as_deref(), + "kata", + )?; + + run_opa_tests( + &workspace, + config.release, + config.frozen, + &config.opa_features, + )?; + + if config.include_azure_policy { + run_ci_cargo_step( + &workspace, + "test", + config.release, + config.frozen, + None, + &["--features", "azure_policy"], + "cargo test --features azure_policy (ci)", + )?; + } + + if config.include_azure_rbac { + run_ci_cargo_step( + &workspace, + "test", + config.release, + config.frozen, + None, + &["--features", "azure-rbac"], + "cargo test --features azure-rbac (ci)", + )?; + } + + Ok(()) +} + +fn join_features(features: &[String]) -> Option { + if features.is_empty() { + None + } else { + Some(features.join(",")) + } +} + +fn example_features(base: &[String]) -> String { + let mut features = Vec::with_capacity(base.len() + 1); + features.push(String::from("std")); + features.extend(base.iter().cloned()); + features.join(",") +} + +fn run_fmt(workspace: &Path) -> Result<()> { + util_run_cargo_step( + workspace, + "cargo xtask fmt --check", + ["xtask", "fmt", "--check"], + ) +} + +fn run_ci_cargo_step( + workspace: &Path, + subcommand: &str, + release: bool, + frozen: bool, + features: Option<&str>, + extra: &[&str], + label: &str, +) -> Result<()> { + let mut args = base_cargo_args(subcommand, release, frozen, features); + for arg in extra { + args.push(OsString::from(*arg)); + } + util_run_cargo_step(workspace, label, args) +} + +fn run_named_test( + workspace: &Path, + release: bool, + frozen: bool, + features: Option<&str>, + name: &str, +) -> Result<()> { + let label = format!("cargo test --test {} (ci)", name); + run_ci_cargo_step( + workspace, + "test", + release, + frozen, + features, + &["--test", name], + &label, + ) +} + +fn run_example( + workspace: &Path, + release: bool, + frozen: bool, + features: Option<&str>, +) -> Result<()> { + let mut args = base_cargo_args("run", release, frozen, features); + args.extend( + [ + "--example", + "regorus", + "--", + "eval", + "-d", + "examples/server/allowed_server.rego", + "-i", + "examples/server/input.json", + "data.example", + ] + .into_iter() + .map(OsString::from), + ); + + util_run_cargo_step(workspace, "cargo run --example regorus (ci)", args) +} + +fn run_opa_tests(workspace: &Path, release: bool, frozen: bool, features: &str) -> Result<()> { + let tests = opa_passing_arguments(workspace)?; + let mut args = base_cargo_args("test", release, frozen, Some(features)); + args.push(OsString::from("--test")); + args.push(OsString::from("opa")); + args.push(OsString::from("--")); + for entry in tests { + args.push(OsString::from(entry)); + } + + let label = format!("cargo test --test opa --features {} (ci)", features); + util_run_cargo_step(workspace, &label, args) +} + +fn base_cargo_args( + subcommand: &str, + release: bool, + frozen: bool, + features: Option<&str>, +) -> Vec { + let mut args = vec![OsString::from(subcommand)]; + if release { + args.push(OsString::from("--release")); + } + if frozen { + args.push(OsString::from("--frozen")); + } + if let Some(features) = features { + if !features.is_empty() { + args.push(OsString::from("--features")); + args.push(OsString::from(features)); + } + } + args +} diff --git a/xtask/src/tasks/ci/musl.rs b/xtask/src/tasks/ci/musl.rs new file mode 100644 index 0000000..24f9aa6 --- /dev/null +++ b/xtask/src/tasks/ci/musl.rs @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::ffi::OsString; +use std::path::Path; +use std::process::{Command, Stdio}; + +use anyhow::{anyhow, Result}; +use clap::Args; + +use crate::tasks::util::{ + opa_passing_arguments, run_cargo_step as util_run_cargo_step, workspace_root, +}; + +/// Exercises the MUSL build and test matrix used in CI. +#[derive(Args, Default)] +pub struct TestMuslCommand { + /// Target triple to compile and test against. + #[arg(long, default_value = "x86_64-unknown-linux-musl")] + pub target: String, + + /// Compile artefacts in release mode. + #[arg(long)] + pub release: bool, + + /// Propagate --frozen to all cargo invocations. + #[arg(long)] + pub frozen: bool, + + /// Feature list passed to the OPA conformance tests. + #[arg(long, default_value = "opa-testutil,serde_json/arbitrary_precision")] + pub opa_features: String, +} + +impl TestMuslCommand { + pub fn run(&self) -> Result<()> { + ensure_musl_gcc()?; + + let workspace = workspace_root(); + + run_build_all_targets(&workspace, &self.target, self.release, self.frozen)?; + run_cargo_test(&workspace, &self.target, self.release, self.frozen, &[])?; + run_named_test(&workspace, &self.target, self.release, self.frozen, "aci")?; + run_named_test(&workspace, &self.target, self.release, self.frozen, "kata")?; + run_opa_tests( + &workspace, + &self.target, + self.release, + self.frozen, + &self.opa_features, + ) + } +} + +fn ensure_musl_gcc() -> Result<()> { + let status = Command::new("musl-gcc") + .arg("--version") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + + match status { + Ok(result) if result.success() => Ok(()), + _ => Err(anyhow!( + "musl-gcc is required but was not found in PATH. Install musl-tools to continue." + )), + } +} +fn run_build_all_targets( + workspace: &Path, + target: &str, + release: bool, + frozen: bool, +) -> Result<()> { + let mut args = cargo_target_args("build", target, release, frozen); + args.push(OsString::from("--all-targets")); + util_run_cargo_step(workspace, "cargo build --all-targets (musl)", args) +} + +fn run_cargo_test( + workspace: &Path, + target: &str, + release: bool, + frozen: bool, + extra: &[&str], +) -> Result<()> { + let mut args = cargo_target_args("test", target, release, frozen); + for arg in extra { + args.push(OsString::from(*arg)); + } + let mut label = String::from("cargo test (musl)"); + if release { + label.push_str(" --release"); + } + if !extra.is_empty() { + label.push(' '); + label.push_str(&extra.join(" ")); + } + util_run_cargo_step(workspace, &label, args) +} + +fn run_named_test( + workspace: &Path, + target: &str, + release: bool, + frozen: bool, + name: &str, +) -> Result<()> { + run_cargo_test(workspace, target, release, frozen, &["--test", name]) +} + +fn run_opa_tests( + workspace: &Path, + target: &str, + release: bool, + frozen: bool, + features: &str, +) -> Result<()> { + let tests = opa_passing_arguments(workspace)?; + let mut args = cargo_target_args("test", target, release, frozen); + args.push(OsString::from("--features")); + args.push(OsString::from(features)); + args.push(OsString::from("--test")); + args.push(OsString::from("opa")); + args.push(OsString::from("--")); + for entry in tests { + args.push(OsString::from(entry)); + } + + let mut label = format!("cargo test --test opa --features {} (musl)", features); + if release { + label.push_str(" --release"); + } + + util_run_cargo_step(workspace, &label, args) +} + +fn cargo_target_args(subcommand: &str, target: &str, release: bool, frozen: bool) -> Vec { + let mut args = vec![OsString::from(subcommand)]; + if release { + args.push(OsString::from("--release")); + } + if frozen { + args.push(OsString::from("--frozen")); + } + args.push(OsString::from("--locked")); + args.push(OsString::from("--target")); + args.push(OsString::from(target)); + args +} diff --git a/xtask/src/tasks/dev/clippy.rs b/xtask/src/tasks/dev/clippy.rs new file mode 100644 index 0000000..a16ac62 --- /dev/null +++ b/xtask/src/tasks/dev/clippy.rs @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::fs::File; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; + +use anyhow::{Context, Result}; +use clap::Args; + +use crate::tasks::util::{run_cargo_step, run_command, workspace_root}; + +const BINDING_MANIFESTS: &[&str] = &[ + "bindings/ffi/Cargo.toml", + "bindings/java/Cargo.toml", + "bindings/python/Cargo.toml", + "bindings/wasm/Cargo.toml", + // TODO: Reenable this once Ruby binding is actively maintained + //"bindings/ruby/Cargo.toml", +]; + +/// Runs Clippy across the workspace and binding crates with CI-equivalent checks. +#[derive(Args, Default)] +pub struct ClippyCommand { + /// Emit SARIF output to the supplied path (relative to the workspace by default). + #[arg(long, value_name = "PATH")] + pub sarif: Option, +} + +impl ClippyCommand { + pub fn run(&self) -> Result<()> { + let workspace = workspace_root(); + + match self.sarif.as_ref() { + Some(sarif) => run_clippy_with_sarif(&workspace, sarif)?, + None => { + run_cargo_step( + &workspace, + "cargo clippy --all-targets --all-features -- -Dwarnings", + [ + "clippy", + "--all-targets", + "--all-features", + "--", + "-Dwarnings", + ], + )?; + } + } + + run_cargo_step( + &workspace, + "cargo clippy --no-default-features -- -Dwarnings", + ["clippy", "--no-default-features", "--", "-Dwarnings"], + )?; + + for manifest in BINDING_MANIFESTS { + let label = + format!("cargo clippy --manifest-path {manifest} --all-targets -- -Dwarnings"); + run_cargo_step( + &workspace, + &label, + [ + "clippy", + "--manifest-path", + manifest, + "--all-targets", + "--", + "-Dwarnings", + ], + )?; + } + + Ok(()) + } +} + +fn run_clippy_with_sarif(workspace: &Path, sarif: &Path) -> Result<()> { + let sarif_path = if sarif.is_absolute() { + sarif.to_path_buf() + } else { + workspace.join(sarif) + }; + + if let Some(parent) = sarif_path.parent() { + std::fs::create_dir_all(parent) + .with_context(|| format!("failed to create SARIF directory at {}", parent.display()))?; + } + + let xtask_target = workspace.join("target").join("xtask"); + std::fs::create_dir_all(&xtask_target).with_context(|| { + format!( + "failed to ensure xtask scratch directory at {}", + xtask_target.display() + ) + })?; + let json_path = xtask_target.join("clippy.json"); + + let json_file = File::create(&json_path).with_context(|| { + format!( + "failed to create intermediate clippy output at {}", + json_path.display() + ) + })?; + + let mut cargo = Command::new("cargo"); + cargo.current_dir(workspace); + cargo.arg("clippy"); + cargo.arg("--all-targets"); + cargo.arg("--all-features"); + cargo.arg("--message-format=json"); + cargo.arg("--"); + cargo.arg("-Dwarnings"); + cargo.stdout(Stdio::from(json_file)); + run_command( + cargo, + "cargo clippy --all-targets --all-features --message-format=json -- -Dwarnings", + )?; + + let json_input = File::open(&json_path).with_context(|| { + format!( + "failed to reopen clippy JSON output at {}", + json_path.display() + ) + })?; + let sarif_file = File::create(&sarif_path) + .with_context(|| format!("failed to create SARIF output at {}", sarif_path.display()))?; + + let mut sarif_cmd = Command::new("clippy-sarif"); + sarif_cmd.stdin(Stdio::from(json_input)); + sarif_cmd.stdout(Stdio::from(sarif_file)); + run_command(sarif_cmd, "clippy-sarif")?; + + let mut fmt = Command::new("sarif-fmt"); + fmt.arg(&sarif_path); + run_command(fmt, "sarif-fmt")?; + + println!("SARIF report written to {}", sarif_path.display()); + + Ok(()) +} diff --git a/xtask/src/tasks/dev/fmt.rs b/xtask/src/tasks/dev/fmt.rs new file mode 100644 index 0000000..7a70f02 --- /dev/null +++ b/xtask/src/tasks/dev/fmt.rs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use anyhow::Result; +use clap::Args; + +use crate::tasks::util::{run_cargo_step, workspace_root}; + +const BINDING_MANIFESTS: &[&str] = &[ + "bindings/ffi/Cargo.toml", + "bindings/java/Cargo.toml", + "bindings/python/Cargo.toml", + "bindings/wasm/Cargo.toml", + "bindings/ruby/Cargo.toml", +]; + +/// Formats every Rust crate in the repository, including binding workspaces. +#[derive(Args, Default)] +pub struct FmtCommand { + /// Fail instead of writing edits when formatting differs + #[arg(long)] + check: bool, +} + +impl FmtCommand { + pub fn run(&self) -> Result<()> { + let workspace = workspace_root(); + + let mut fmt_args = vec!["fmt", "--all"]; + if self.check { + fmt_args.extend(["--", "--check"]); + } + + let fmt_label = if self.check { + "cargo fmt --all -- --check" + } else { + "cargo fmt --all" + }; + + run_cargo_step(&workspace, fmt_label, fmt_args)?; + + for manifest in BINDING_MANIFESTS { + let mut args = vec!["fmt", "--manifest-path", *manifest]; + let label = if self.check { + args.extend(["--", "--check"]); + format!("cargo fmt --manifest-path {manifest} -- --check") + } else { + format!("cargo fmt --manifest-path {manifest}") + }; + + run_cargo_step(&workspace, &label, args)?; + } + + Ok(()) + } +} diff --git a/xtask/src/tasks/dev/mod.rs b/xtask/src/tasks/dev/mod.rs new file mode 100644 index 0000000..85b12ec --- /dev/null +++ b/xtask/src/tasks/dev/mod.rs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +pub mod clippy; +pub mod fmt; +pub mod precommit; +pub mod prepush; + +pub use clippy::ClippyCommand; +pub use fmt::FmtCommand; +pub use precommit::PrecommitCommand; +pub use prepush::PrepushCommand; diff --git a/xtask/src/tasks/dev/precommit.rs b/xtask/src/tasks/dev/precommit.rs new file mode 100644 index 0000000..5171033 --- /dev/null +++ b/xtask/src/tasks/dev/precommit.rs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::collections::HashSet; +use std::io::Write; +use std::process::{Command, Stdio}; + +use anyhow::{anyhow, Context, Result}; +use clap::Args; + +use crate::tasks::util::{log_step, run_cargo_step, workspace_root}; + +/// Runs the repository's pre-commit validation sequence. +#[derive(Args, Default)] +pub struct PrecommitCommand; + +impl PrecommitCommand { + pub fn run(&self) -> Result<()> { + let workspace = workspace_root(); + + log_step("cargo build --all-targets"); + run_cargo_step( + &workspace, + "cargo build --all-targets", + ["build", "--all-targets"], + ) + .with_context(|| "pre-commit: cargo build --all-targets failed".to_string())?; + + run_cargo_step( + &workspace, + "cargo xtask fmt --check", + ["xtask", "fmt", "--check"], + ) + .with_context(|| "pre-commit: cargo xtask fmt --check failed".to_string())?; + + run_cargo_step(&workspace, "cargo xtask clippy", ["xtask", "clippy"]) + .with_context(|| "pre-commit: cargo xtask clippy failed".to_string())?; + + log_step("git status --short"); + verify_clean_status(&workspace)?; + + Ok(()) + } +} + +fn verify_clean_status(root: &std::path::Path) -> Result<()> { + let mut git = Command::new("git"); + git.current_dir(root); + git.arg("status"); + git.arg("-s"); + git.stdout(Stdio::piped()); + + let output = git + .output() + .with_context(|| format!("failed to inspect git status in {}", root.display()))?; + + if !output.status.success() { + return Err(anyhow!( + "git status -s exited with status {}", + output.status + )); + } + + let stdout = String::from_utf8(output.stdout) + .with_context(|| "git status output was not UTF-8".to_string())?; + + let interesting: HashSet<&str> = ["MM", "??", "AM", " M"].into_iter().collect(); + let mut flagged = Vec::new(); + for line in stdout.lines() { + if line.len() >= 2 { + let status = &line[..2]; + if interesting.contains(status) { + flagged.push(line.to_string()); + } + } + } + + if flagged.is_empty() { + return Ok(()); + } + + let mut stderr = std::io::stderr(); + writeln!(stderr, "\nUnstaged changes found:").ok(); + for entry in &flagged { + writeln!(stderr, "{}", entry).ok(); + } + writeln!(stderr, "Stage them and try again").ok(); + + Err(anyhow!("repository contains unstaged changes")) +} diff --git a/xtask/src/tasks/dev/prepush.rs b/xtask/src/tasks/dev/prepush.rs new file mode 100644 index 0000000..669e597 --- /dev/null +++ b/xtask/src/tasks/dev/prepush.rs @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::path::Path; +use std::process::{Command, Stdio}; + +use anyhow::{Context, Result}; +use clap::Args; + +use super::precommit::PrecommitCommand; +use crate::tasks::util::{ + log_step, opa_passing_arguments, run_cargo_step, run_command, workspace_root, +}; + +/// Runs the repository's pre-push validation sequence. +#[derive(Args, Default)] +pub struct PrepushCommand; + +impl PrepushCommand { + pub fn run(&self) -> Result<()> { + let workspace = workspace_root(); + + log_step("pre-commit checks"); + PrecommitCommand::default() + .run() + .with_context(|| "pre-push: pre-commit sequence failed".to_string())?; + + run_cargo_step(&workspace, "cargo test --doc", ["test", "--doc"]) + .with_context(|| "pre-push: cargo test --doc failed".to_string())?; + + if rustup_available() { + log_step("rustup target add thumbv7m-none-eabi"); + let mut rustup = Command::new("rustup"); + rustup.arg("target"); + rustup.arg("add"); + rustup.arg("thumbv7m-none-eabi"); + run_command(rustup, "rustup target add thumbv7m-none-eabi") + .with_context(|| "pre-push: rustup target add failed".to_string())?; + + run_cargo_step( + &workspace, + "cargo xtask test-no-std", + ["xtask", "test-no-std"], + ) + .with_context(|| "pre-push: cargo xtask test-no-std failed".to_string())?; + } + + run_cargo_step( + &workspace, + "cargo build --example regorus --no-default-features --features std", + [ + "build", + "--example", + "regorus", + "--no-default-features", + "--features", + "std", + ], + ) + .with_context(|| { + "pre-push: cargo build --example regorus --no-default-features --features std failed" + .to_string() + })?; + + run_cargo_step( + &workspace, + "cargo build --all-features", + ["build", "--all-features"], + ) + .with_context(|| "pre-push: cargo build --all-features failed".to_string())?; + + run_cargo_step(&workspace, "cargo test", ["test"]) + .with_context(|| "pre-push: cargo test failed".to_string())?; + run_cargo_step( + &workspace, + "cargo test --test aci", + ["test", "--test", "aci"], + ) + .with_context(|| "pre-push: cargo test --test aci failed".to_string())?; + run_cargo_step( + &workspace, + "cargo test --test kata", + ["test", "--test", "kata"], + ) + .with_context(|| "pre-push: cargo test --test kata failed".to_string())?; + + run_cargo_step( + &workspace, + "cargo test --features rego-extensions", + ["test", "--features", "rego-extensions"], + ) + .with_context(|| "pre-push: cargo test --features rego-extensions failed".to_string())?; + run_cargo_step( + &workspace, + "cargo test --test aci --features rego-extensions", + ["test", "--test", "aci", "--features", "rego-extensions"], + ) + .with_context(|| { + "pre-push: cargo test --test aci --features rego-extensions failed".to_string() + })?; + run_cargo_step( + &workspace, + "cargo test --test kata --features rego-extensions", + ["test", "--test", "kata", "--features", "rego-extensions"], + ) + .with_context(|| { + "pre-push: cargo test --test kata --features rego-extensions failed".to_string() + })?; + + log_step("cargo test --features opa-testutil,serde_json/arbitrary_precision,rego-extensions --test opa"); + run_opa_conformance(&workspace).with_context(|| { + "pre-push: cargo test --features opa-testutil,serde_json/arbitrary_precision,rego-extensions --test opa failed" + .to_string() + })?; + + Ok(()) + } +} + +fn run_opa_conformance(root: &Path) -> Result<()> { + let tests = opa_passing_arguments(root)?; + let mut cmd = Command::new("cargo"); + cmd.current_dir(root); + cmd.arg("test"); + cmd.arg("--features"); + cmd.arg("opa-testutil,serde_json/arbitrary_precision,rego-extensions"); + cmd.arg("--test"); + cmd.arg("opa"); + cmd.arg("--"); + for entry in tests { + cmd.arg(entry); + } + + run_command( + cmd, + "cargo test --features opa-testutil,serde_json/arbitrary_precision,rego-extensions --test opa", + ) +} + +fn rustup_available() -> bool { + let mut probe = Command::new("rustup"); + probe.arg("--version"); + probe.stdout(Stdio::null()); + probe.stderr(Stdio::null()); + probe + .status() + .map(|status| status.success()) + .unwrap_or(false) +} diff --git a/xtask/src/tasks/mod.rs b/xtask/src/tasks/mod.rs index 6f8a66e..44fc8a3 100644 --- a/xtask/src/tasks/mod.rs +++ b/xtask/src/tasks/mod.rs @@ -2,7 +2,19 @@ // Licensed under the MIT License. pub mod bindings; +pub mod ci; +pub mod dev; +pub mod no_std; pub mod update_deps; +mod util; -pub use bindings::BindingsCommand; +pub use bindings::{ + BindingsCommand, BuildAllBindingsCommand, BuildFfiCommand, BuildJavaCommand, BuildNugetCommand, + BuildPythonCommand, BuildWasmCommand, TestAllBindingsCommand, TestCCommand, TestCNoStdCommand, + TestCppCommand, TestCsharpCommand, TestFfiCommand, TestGoCommand, TestJavaCommand, + TestPythonCommand, TestRubyCommand, TestWasmCommand, +}; +pub use ci::{CiDebugCommand, CiReleaseCommand, TestMuslCommand}; +pub use dev::{ClippyCommand, FmtCommand, PrecommitCommand, PrepushCommand}; +pub use no_std::TestNoStdCommand; pub use update_deps::UpdateDepsCommand; diff --git a/xtask/src/tasks/no_std.rs b/xtask/src/tasks/no_std.rs new file mode 100644 index 0000000..6e88b5f --- /dev/null +++ b/xtask/src/tasks/no_std.rs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::ffi::OsString; + +use anyhow::Result; +use clap::Args; + +use crate::tasks::util::{run_cargo_step, workspace_root}; + +/// Builds the ensure_no_std harness for an embedded target. +#[derive(Args, Default)] +pub struct TestNoStdCommand { + /// Target triple to compile (defaults to thumbv7m-none-eabi). + #[arg(long, default_value = "thumbv7m-none-eabi")] + pub target: String, + + /// Compile artefacts in release mode. + #[arg(long)] + pub release: bool, + + /// Propagate --frozen to the cargo invocations. + #[arg(long)] + pub frozen: bool, +} + +impl TestNoStdCommand { + pub fn run(&self) -> Result<()> { + let workspace = workspace_root(); + let project_dir = workspace.join("tests/ensure_no_std"); + + let mut args = vec![ + OsString::from("build"), + OsString::from("--target"), + OsString::from(&self.target), + ]; + if self.release { + args.push(OsString::from("--release")); + } + if self.frozen { + args.push(OsString::from("--frozen")); + } + run_cargo_step(&project_dir, "cargo build (tests/ensure_no_std)", args)?; + Ok(()) + } +} diff --git a/xtask/src/tasks/util.rs b/xtask/src/tasks/util.rs new file mode 100644 index 0000000..7488131 --- /dev/null +++ b/xtask/src/tasks/util.rs @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::ffi::{OsStr, OsString}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use anyhow::{bail, Context, Result}; + +/// Returns the root of the workspace that hosts the xtask crate. +pub fn workspace_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("xtask resides in workspace root") + .to_path_buf() +} + +/// Runs a command, surfaces the command line, and maps a failing exit status to an error. +pub fn run_command(mut command: Command, label: &str) -> Result<()> { + let display = format_command(&command); + println!("$ {}", display); + + let status = command + .status() + .with_context(|| format!("failed to spawn {}", label))?; + if !status.success() { + bail!("{} failed with status {}", label, status); + } + + Ok(()) +} + +/// Prints a simple section header to highlight the upcoming action. +pub fn log_step(title: &str) { + println!("\n=== {} ===", title); +} + +/// Logs a section heading and executes a cargo subcommand. +pub fn run_cargo_step(workspace: &Path, title: &str, args: I) -> Result<()> +where + I: IntoIterator, + S: AsRef, +{ + run_cargo_in_step(workspace, title, args) +} + +/// Logs a section heading and executes a cargo subcommand within the provided directory. +pub fn run_cargo_in_step(directory: &Path, title: &str, args: I) -> Result<()> +where + I: IntoIterator, + S: AsRef, +{ + let heading = format!("Running {}", title); + log_step(&heading); + let mut command = Command::new("cargo"); + command.current_dir(directory); + command.args(args); + run_command(command, title) +} + +/// Formats a command with its arguments for diagnostic output. +pub fn format_command(command: &Command) -> String { + let mut parts = Vec::new(); + parts.push(command.get_program().to_string_lossy().into_owned()); + for arg in command.get_args() { + parts.push(arg.to_string_lossy().into_owned()); + } + parts.join(" ") +} + +/// Deduplicates the provided vector while preserving the original order. +pub fn dedup(values: &mut Vec) { + let mut unique = Vec::new(); + for value in values.drain(..) { + if unique.iter().any(|existing| existing == &value) { + continue; + } + unique.push(value); + } + *values = unique; +} + +/// Returns the platform-specific path list separator character. +pub fn path_separator() -> &'static str { + if cfg!(windows) { + ";" + } else { + ":" + } +} + +/// Returns the environment variable used for dynamic library lookup. +pub fn library_search_env_var() -> &'static str { + if cfg!(windows) { + "PATH" + } else if cfg!(target_os = "macos") { + "DYLD_LIBRARY_PATH" + } else { + "LD_LIBRARY_PATH" + } +} + +/// Ensures the provided directory is prepended to the requested environment variable. +pub fn prepend_env_path(command: &mut Command, key: &str, dir: &Path) { + let mut value = OsString::new(); + value.push(dir); + + if let Some(existing) = std::env::var_os(key) { + if !existing.is_empty() { + value.push(path_separator()); + value.push(existing); + } + } + + command.env(key, value); +} + +/// Prepends the supplied directory to the standard dynamic library search path. +pub fn add_library_search_path(command: &mut Command, dir: &Path) { + let key = library_search_env_var(); + prepend_env_path(command, key, dir); +} + +/// Returns the host architecture string expected by dotnet CLI switches. +pub fn dotnet_host_arch() -> &'static str { + match std::env::consts::ARCH { + "aarch64" => "arm64", + "x86_64" => "x64", + "arm" => "arm", + "x86" => "x86", + _ => "x64", + } +} + +/// Loads the list of passing OPA tests from the repository. +pub fn opa_passing_arguments(root: &Path) -> Result> { + let listing = root.join("tests/opa.passing"); + let contents = fs::read_to_string(&listing).with_context(|| { + format!( + "failed to read list of passing OPA tests from {}", + listing.display() + ) + })?; + + Ok(contents + .split_whitespace() + .filter(|entry| !entry.is_empty()) + .map(|entry| entry.to_string()) + .collect()) +}