From bda181b891f5c75e98949d6ddecb9f5d5790e98e Mon Sep 17 00:00:00 2001 From: Wei Lin Date: Sat, 5 Sep 2026 19:03:26 +0800 Subject: [PATCH 1/6] feat: add multi-platform MiniExcelRust NuGet Add the Rust C ABI, managed streaming facade, isolated package consumer test, eight-RID CI matrix, and protected preview release workflow. --- .github/workflows/ci.yml | 102 ++ .github/workflows/release.yml | 217 ++++ Cargo.lock | 982 ++++++++++++++++++ Cargo.toml | 11 + build/Build-Native.ps1 | 58 ++ build/Test-Package.ps1 | 55 + build/Verify-Package.ps1 | 40 + native/miniexcel-ffi/Cargo.toml | 16 + native/miniexcel-ffi/src/lib.rs | 282 +++++ rust-toolchain.toml | 4 + src/MiniExcelRust/MiniExcelRust.cs | 254 +++++ src/MiniExcelRust/MiniExcelRust.csproj | 56 + .../MiniExcelRust.PackageTests.csproj | 15 + tests/MiniExcelRust.PackageTests/Program.cs | 83 ++ 14 files changed, 2175 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 build/Build-Native.ps1 create mode 100644 build/Test-Package.ps1 create mode 100644 build/Verify-Package.ps1 create mode 100644 native/miniexcel-ffi/Cargo.toml create mode 100644 native/miniexcel-ffi/src/lib.rs create mode 100644 rust-toolchain.toml create mode 100644 src/MiniExcelRust/MiniExcelRust.cs create mode 100644 src/MiniExcelRust/MiniExcelRust.csproj create mode 100644 tests/MiniExcelRust.PackageTests/MiniExcelRust.PackageTests.csproj create mode 100644 tests/MiniExcelRust.PackageTests/Program.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ab6d57e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,102 @@ +name: CI + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + validate: + name: Validate source + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@1.85.0 + with: + components: clippy, rustfmt + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + - uses: Swatinem/rust-cache@v2 + - name: Check Rust formatting + run: cargo fmt --all -- --check + - name: Run Clippy + run: cargo clippy --workspace --all-targets --locked -- -D warnings + - name: Run Rust tests + run: cargo test --workspace --all-targets --locked + - name: Build managed package + run: dotnet build src/MiniExcelRust/MiniExcelRust.csproj -c Release + + native-package-test: + name: Test ${{ matrix.rid }} package + strategy: + fail-fast: false + matrix: + include: + - runner: windows-latest + rid: win-x64 + - runner: windows-11-arm + rid: win-arm64 + - runner: ubuntu-latest + rid: linux-x64 + - runner: ubuntu-24.04-arm + rid: linux-arm64 + - runner: macos-15-intel + rid: osx-x64 + - runner: macos-latest + rid: osx-arm64 + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@1.85.0 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + - uses: Swatinem/rust-cache@v2 + with: + key: ${{ matrix.rid }} + - name: Build, pack, and test + shell: pwsh + run: ./build/Test-Package.ps1 -Rid '${{ matrix.rid }}' + + musl-package-test: + name: Test ${{ matrix.rid }} package + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-latest + rid: linux-musl-x64 + - runner: ubuntu-24.04-arm + rid: linux-musl-arm64 + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@1.85.0 + with: + targets: ${{ matrix.rid == 'linux-musl-x64' && 'x86_64-unknown-linux-musl' || 'aarch64-unknown-linux-musl' }} + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + - uses: mlugg/setup-zig@v2 + - uses: taiki-e/install-action@v2 + with: + tool: cargo-zigbuild + - uses: Swatinem/rust-cache@v2 + with: + key: ${{ matrix.rid }} + - name: Build native asset + shell: pwsh + run: ./build/Build-Native.ps1 -Rid '${{ matrix.rid }}' -UseZig + - name: Pack + run: dotnet pack src/MiniExcelRust/MiniExcelRust.csproj -c Release -o artifacts/packages -p:MiniExcelRustRequireAllNativeAssets=false + - name: Test package in Alpine + shell: bash + run: | + docker run --rm \ + -v "$GITHUB_WORKSPACE:/work" \ + -w /work \ + mcr.microsoft.com/dotnet/sdk:8.0-alpine \ + sh -lc 'dotnet restore tests/MiniExcelRust.PackageTests/MiniExcelRust.PackageTests.csproj --source artifacts/packages && dotnet run --project tests/MiniExcelRust.PackageTests/MiniExcelRust.PackageTests.csproj -c Release --no-restore' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..065a513 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,217 @@ +name: Release + +on: + push: + tags: + - 'v*.*.*-preview.*' + workflow_dispatch: + inputs: + version: + description: NuGet version, for example 0.1.0-preview.1 + required: true + default: 0.1.0-preview.1 + type: string + publish: + description: Publish to NuGet.org after all tests pass + required: true + default: false + type: boolean + +permissions: + contents: read + +jobs: + prepare: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.value }} + steps: + - id: version + name: Validate version + shell: bash + env: + REQUESTED_VERSION: ${{ github.event_name == 'push' && github.ref_name || inputs.version }} + run: | + version="${REQUESTED_VERSION#v}" + if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+-preview\.[0-9]+$ ]]; then + echo "::error::Version '$version' must match N.N.N-preview.N." + exit 1 + fi + echo "value=$version" >> "$GITHUB_OUTPUT" + + build-native: + needs: prepare + name: Build ${{ matrix.rid }} + strategy: + fail-fast: false + matrix: + include: + - runner: windows-latest + rid: win-x64 + target: x86_64-pc-windows-msvc + zig: false + - runner: windows-11-arm + rid: win-arm64 + target: aarch64-pc-windows-msvc + zig: false + - runner: ubuntu-latest + rid: linux-x64 + target: x86_64-unknown-linux-gnu + zig: false + - runner: ubuntu-24.04-arm + rid: linux-arm64 + target: aarch64-unknown-linux-gnu + zig: false + - runner: ubuntu-latest + rid: linux-musl-x64 + target: x86_64-unknown-linux-musl + zig: true + - runner: ubuntu-24.04-arm + rid: linux-musl-arm64 + target: aarch64-unknown-linux-musl + zig: true + - runner: macos-15-intel + rid: osx-x64 + target: x86_64-apple-darwin + zig: false + - runner: macos-latest + rid: osx-arm64 + target: aarch64-apple-darwin + zig: false + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@1.85.0 + with: + targets: ${{ matrix.target }} + - uses: mlugg/setup-zig@v2 + if: matrix.zig + - uses: taiki-e/install-action@v2 + if: matrix.zig + with: + tool: cargo-zigbuild + - uses: Swatinem/rust-cache@v2 + with: + key: release-${{ matrix.rid }} + - name: Build native asset + shell: pwsh + run: | + $arguments = @{ Rid = '${{ matrix.rid }}' } + if ('${{ matrix.zig }}' -eq 'true') { + $arguments.UseZig = $true + } + ./build/Build-Native.ps1 @arguments + - uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.rid }} + path: artifacts/native/${{ matrix.rid }}/* + if-no-files-found: error + + pack: + needs: [prepare, build-native] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + - uses: actions/download-artifact@v4 + with: + pattern: '*' + path: artifacts/native + - name: Pack complete NuGet + run: dotnet pack src/MiniExcelRust/MiniExcelRust.csproj -c Release -o artifacts/packages -p:PackageVersion=${{ needs.prepare.outputs.version }} + - name: Verify package contents + shell: pwsh + run: ./build/Verify-Package.ps1 -PackagePath 'artifacts/packages/MiniExcelRust.${{ needs.prepare.outputs.version }}.nupkg' + - uses: actions/upload-artifact@v4 + with: + name: nuget-package + path: artifacts/packages/MiniExcelRust.${{ needs.prepare.outputs.version }}.*nupkg + if-no-files-found: error + + test-package: + needs: [prepare, pack] + name: Consume ${{ matrix.rid }} package + strategy: + fail-fast: false + matrix: + include: + - runner: windows-latest + rid: win-x64 + musl: false + - runner: windows-11-arm + rid: win-arm64 + musl: false + - runner: ubuntu-latest + rid: linux-x64 + musl: false + - runner: ubuntu-24.04-arm + rid: linux-arm64 + musl: false + - runner: ubuntu-latest + rid: linux-musl-x64 + musl: true + - runner: ubuntu-24.04-arm + rid: linux-musl-arm64 + musl: true + - runner: macos-15-intel + rid: osx-x64 + musl: false + - runner: macos-latest + rid: osx-arm64 + musl: false + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + - uses: actions/download-artifact@v4 + with: + name: nuget-package + path: artifacts/packages + - name: Consume package + if: ${{ !matrix.musl }} + shell: pwsh + run: | + dotnet restore tests/MiniExcelRust.PackageTests/MiniExcelRust.PackageTests.csproj --source artifacts/packages -p:MiniExcelRustPackageVersion=${{ needs.prepare.outputs.version }} + dotnet run --project tests/MiniExcelRust.PackageTests/MiniExcelRust.PackageTests.csproj -c Release --no-restore -p:MiniExcelRustPackageVersion=${{ needs.prepare.outputs.version }} + - name: Consume package in Alpine + if: matrix.musl + shell: bash + run: | + docker run --rm \ + -v "$GITHUB_WORKSPACE:/work" \ + -w /work \ + mcr.microsoft.com/dotnet/sdk:8.0-alpine \ + sh -lc 'dotnet restore tests/MiniExcelRust.PackageTests/MiniExcelRust.PackageTests.csproj --source artifacts/packages -p:MiniExcelRustPackageVersion=${{ needs.prepare.outputs.version }} && dotnet run --project tests/MiniExcelRust.PackageTests/MiniExcelRust.PackageTests.csproj -c Release --no-restore -p:MiniExcelRustPackageVersion=${{ needs.prepare.outputs.version }}' + + publish: + needs: [prepare, test-package] + if: github.event_name == 'push' || inputs.publish == true + runs-on: ubuntu-latest + environment: release + permissions: + contents: write + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + steps: + - uses: actions/download-artifact@v4 + with: + name: nuget-package + path: artifacts/packages + - name: Require NuGet API key + shell: bash + run: | + if [ -z "$NUGET_API_KEY" ]; then + echo "::error::Add NUGET_API_KEY to the protected release environment." + exit 1 + fi + - name: Publish NuGet package + run: dotnet nuget push artifacts/packages/MiniExcelRust.${{ needs.prepare.outputs.version }}.nupkg --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json --skip-duplicate + - name: Create GitHub release + if: github.event_name == 'push' + env: + GH_TOKEN: ${{ github.token }} + run: gh release create '${{ github.ref_name }}' artifacts/packages/* --verify-tag --generate-notes --title 'MiniExcelRust ${{ needs.prepare.outputs.version }}' diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..c25a165 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,982 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "atoi_simd" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ad17c7c205c2c28b527b9845eeb91cf1b4d008b438f98ce0e628227a822758e" +dependencies = [ + "debug_unsafe", +] + +[[package]] +name = "atomicwrites" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef1bb8d1b645fe38d51dfc331d720fb5fc2c94b440c76cc79c80ff265ca33e3" +dependencies = [ + "rustix 0.38.44", + "tempfile", + "windows-sys 0.52.0", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "calamine" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8822fe6253ca47aa5ad9a3be09f6fe7cd20c6a74e41b0aa42e8f4e3d523508df" +dependencies = [ + "atoi_simd", + "byteorder", + "chrono", + "codepage", + "encoding_rs", + "fast-float2", + "log", + "quick-xml", + "serde", + "zip", +] + +[[package]] +name = "cc" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "codepage" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f68d061bc2828ae826206326e61251aca94c1e4a5305cf52d9138639c918b4" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + +[[package]] +name = "debug_unsafe" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eed2c4702fa172d1ce21078faa7c5203e69f5394d48cc436d25928394a867a2" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "encoding_rs_io" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba3fe847045ecff794b9c138293a80db914678c453ad63fbf0c6a9eb6e00b22" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fast-float2" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e8948ce679d00a02a94739ea185595dca7118ed04feb991127e443bd3d761f" + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "zlib-rs", +] + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "miniexcel" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3afc87a40a375f5a85fdc749be750efebb789c4929a271fdea985cea22528fcd" +dependencies = [ + "atomicwrites", + "calamine", + "chrono", + "csv", + "encoding_rs", + "encoding_rs_io", + "fs2", + "indexmap", + "quick-xml", + "rust_xlsxwriter", + "same-file", + "serde", + "serde_json", + "sha2", + "tempfile", + "thiserror", + "uuid", + "zip", +] + +[[package]] +name = "miniexcel-ffi" +version = "0.1.0" +dependencies = [ + "miniexcel", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "encoding_rs", + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rust_xlsxwriter" +version = "0.96.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd1746025420e17b5d62528b930e550e016e857038794d74e169018126ef3d14" +dependencies = [ + "chrono", + "rust_xlsxwriter_derive", + "serde", + "tempfile", + "zip", +] + +[[package]] +name = "rust_xlsxwriter_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "400cc5bbf116143f0ed897d7908b49f604feea981414ef644731e62449d1cc44" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "uuid" +version = "1.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.5", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zip" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42e33efc22a0650c311c2ef19115ce232583abbe80850bc8b66509ebef02de0" +dependencies = [ + "crc32fast", + "flate2", + "indexmap", + "memchr", + "typed-path", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..858ba33 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[workspace] +members = ["native/miniexcel-ffi"] +resolver = "3" + +[workspace.package] +version = "0.1.0" +edition = "2024" +rust-version = "1.85" +license = "Apache-2.0" +repository = "https://github.com/mini-software/MiniExcelRust" +homepage = "https://github.com/mini-software/MiniExcelRust" diff --git a/build/Build-Native.ps1 b/build/Build-Native.ps1 new file mode 100644 index 0000000..c0060f0 --- /dev/null +++ b/build/Build-Native.ps1 @@ -0,0 +1,58 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [ValidateSet( + 'win-x64', + 'win-arm64', + 'linux-x64', + 'linux-arm64', + 'linux-musl-x64', + 'linux-musl-arm64', + 'osx-x64', + 'osx-arm64' + )] + [string] $Rid, + + [switch] $UseZig +) + +$ErrorActionPreference = 'Stop' +$repositoryRoot = Split-Path $PSScriptRoot -Parent + +$targets = @{ + 'win-x64' = @{ Triple = 'x86_64-pc-windows-msvc'; File = 'miniexcel_ffi.dll' } + 'win-arm64' = @{ Triple = 'aarch64-pc-windows-msvc'; File = 'miniexcel_ffi.dll' } + 'linux-x64' = @{ Triple = 'x86_64-unknown-linux-gnu'; File = 'libminiexcel_ffi.so' } + 'linux-arm64' = @{ Triple = 'aarch64-unknown-linux-gnu'; File = 'libminiexcel_ffi.so' } + 'linux-musl-x64' = @{ Triple = 'x86_64-unknown-linux-musl'; File = 'libminiexcel_ffi.so' } + 'linux-musl-arm64' = @{ Triple = 'aarch64-unknown-linux-musl'; File = 'libminiexcel_ffi.so' } + 'osx-x64' = @{ Triple = 'x86_64-apple-darwin'; File = 'libminiexcel_ffi.dylib' } + 'osx-arm64' = @{ Triple = 'aarch64-apple-darwin'; File = 'libminiexcel_ffi.dylib' } +} + +$target = $targets[$Rid] +Push-Location $repositoryRoot +try { + & rustup target add $target.Triple + if ($LASTEXITCODE -ne 0) { + throw "Failed to install Rust target $($target.Triple)." + } + + if ($UseZig) { + & cargo zigbuild --release --locked -p miniexcel-ffi --target $target.Triple + } + else { + & cargo build --release --locked -p miniexcel-ffi --target $target.Triple + } + if ($LASTEXITCODE -ne 0) { + throw "Failed to build native library for $Rid." + } + + $source = Join-Path $repositoryRoot "target/$($target.Triple)/release/$($target.File)" + $destinationDirectory = Join-Path $repositoryRoot "artifacts/native/$Rid" + New-Item -ItemType Directory -Path $destinationDirectory -Force | Out-Null + Copy-Item $source (Join-Path $destinationDirectory $target.File) -Force +} +finally { + Pop-Location +} diff --git a/build/Test-Package.ps1 b/build/Test-Package.ps1 new file mode 100644 index 0000000..70ee724 --- /dev/null +++ b/build/Test-Package.ps1 @@ -0,0 +1,55 @@ +[CmdletBinding()] +param( + [ValidateSet('win-x64', 'win-arm64', 'linux-x64', 'linux-arm64', 'linux-musl-x64', 'linux-musl-arm64', 'osx-x64', 'osx-arm64')] + [string] $Rid = 'win-x64', + + [string] $Version = '0.1.0-preview.1', + + [switch] $SkipNativeBuild +) + +$ErrorActionPreference = 'Stop' +$repositoryRoot = Split-Path $PSScriptRoot -Parent +$packageDirectory = Join-Path $repositoryRoot 'artifacts/packages' +$consumerProject = Join-Path $repositoryRoot 'tests/MiniExcelRust.PackageTests/MiniExcelRust.PackageTests.csproj' + +if (-not $SkipNativeBuild) { + & (Join-Path $PSScriptRoot 'Build-Native.ps1') -Rid $Rid +} + +New-Item -ItemType Directory -Path $packageDirectory -Force | Out-Null +& dotnet pack (Join-Path $repositoryRoot 'src/MiniExcelRust/MiniExcelRust.csproj') ` + -c Release ` + -o $packageDirectory ` + -p:PackageVersion=$Version ` + -p:MiniExcelRustRequireAllNativeAssets=false +if ($LASTEXITCODE -ne 0) { + throw 'NuGet pack failed.' +} + +$package = Join-Path $packageDirectory "MiniExcelRust.$Version.nupkg" +Add-Type -AssemblyName System.IO.Compression.FileSystem +$archive = [System.IO.Compression.ZipFile]::OpenRead($package) +try { + $nativeEntry = $archive.Entries | Where-Object { $_.FullName -like "runtimes/$Rid/native/*" } + if ($null -eq $nativeEntry) { + throw "The package does not contain a native asset for $Rid." + } +} +finally { + $archive.Dispose() +} + +& dotnet restore $consumerProject ` + --force ` + --source $packageDirectory ` + -p:MiniExcelRustPackageVersion=$Version +if ($LASTEXITCODE -ne 0) { + throw 'Package consumer restore failed.' +} + +& dotnet run --project $consumerProject -c Release --no-restore ` + -p:MiniExcelRustPackageVersion=$Version +if ($LASTEXITCODE -ne 0) { + throw 'Package consumer smoke test failed.' +} diff --git a/build/Verify-Package.ps1 b/build/Verify-Package.ps1 new file mode 100644 index 0000000..f941001 --- /dev/null +++ b/build/Verify-Package.ps1 @@ -0,0 +1,40 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [string] $PackagePath +) + +$ErrorActionPreference = 'Stop' +$expectedEntries = @( + 'runtimes/win-x64/native/miniexcel_ffi.dll', + 'runtimes/win-arm64/native/miniexcel_ffi.dll', + 'runtimes/linux-x64/native/libminiexcel_ffi.so', + 'runtimes/linux-arm64/native/libminiexcel_ffi.so', + 'runtimes/linux-musl-x64/native/libminiexcel_ffi.so', + 'runtimes/linux-musl-arm64/native/libminiexcel_ffi.so', + 'runtimes/osx-x64/native/libminiexcel_ffi.dylib', + 'runtimes/osx-arm64/native/libminiexcel_ffi.dylib' +) + +Add-Type -AssemblyName System.IO.Compression.FileSystem +$archive = [System.IO.Compression.ZipFile]::OpenRead((Resolve-Path $PackagePath)) +try { + $actualEntries = @($archive.Entries | ForEach-Object FullName) + foreach ($expectedEntry in $expectedEntries) { + if ($expectedEntry -notin $actualEntries) { + throw "The package is missing $expectedEntry." + } + } + + $unexpectedNativeEntries = @($actualEntries | Where-Object { + $_ -like 'runtimes/*/native/*' -and $_ -notin $expectedEntries + }) + if ($unexpectedNativeEntries.Count -ne 0) { + throw "The package contains unexpected native assets: $($unexpectedNativeEntries -join ', ')." + } +} +finally { + $archive.Dispose() +} + +Write-Host "Verified all $($expectedEntries.Count) native package assets." diff --git a/native/miniexcel-ffi/Cargo.toml b/native/miniexcel-ffi/Cargo.toml new file mode 100644 index 0000000..ae5a13e --- /dev/null +++ b/native/miniexcel-ffi/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "miniexcel-ffi" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +description = "Native C ABI for MiniExcel .NET interop" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +miniexcel = "=0.4.0" diff --git a/native/miniexcel-ffi/src/lib.rs b/native/miniexcel-ffi/src/lib.rs new file mode 100644 index 0000000..93413d4 --- /dev/null +++ b/native/miniexcel-ffi/src/lib.rs @@ -0,0 +1,282 @@ +use std::cell::RefCell; +use std::ffi::{CStr, c_char}; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::ptr; +use std::str::FromStr; + +use miniexcel::{CellReference, CellValue, DynamicRow, HeaderMode, MiniExcel, ReadOptions}; + +const ABI_VERSION: u32 = 1; +const RESULT_END: i32 = 0; +const RESULT_BATCH: i32 = 1; +const ERROR_INVALID_ARGUMENT: i32 = -1; +const ERROR_QUERY: i32 = -2; +const ERROR_PANIC: i32 = -3; + +thread_local! { + static LAST_ERROR: RefCell> = const { RefCell::new(Vec::new()) }; +} + +pub struct QueryHandle { + rows: Box> + Send>, + frame: Vec, +} + +#[unsafe(no_mangle)] +pub extern "C" fn miniexcel_abi_version() -> u32 { + ABI_VERSION +} + +/// Opens a path-based XLSX query and returns an opaque native handle. +/// +/// # Safety +/// +/// String pointers must be null-terminated UTF-8. `path`, `start_cell`, and `out_handle` must be +/// non-null and valid for the duration of the call. `sheet_name` may be null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn miniexcel_query_open( + path: *const c_char, + use_header_row: u8, + sheet_name: *const c_char, + start_cell: *const c_char, + out_handle: *mut *mut QueryHandle, +) -> i32 { + ffi_result(|| { + if path.is_null() || start_cell.is_null() || out_handle.is_null() { + set_last_error("path, start_cell, and out_handle are required"); + return Err(ERROR_INVALID_ARGUMENT); + } + + let path = unsafe { read_utf8(path) }?; + let start_cell = unsafe { read_utf8(start_cell) }?; + let start_cell = CellReference::from_str(start_cell).map_err(|error| { + set_last_error(error.to_string()); + ERROR_INVALID_ARGUMENT + })?; + + let mut options = ReadOptions::new() + .with_header_mode(if use_header_row == 0 { + HeaderMode::None + } else { + HeaderMode::FirstRow + }) + .with_start_cell(start_cell); + + if !sheet_name.is_null() { + let sheet_name = unsafe { read_utf8(sheet_name) }?; + if !sheet_name.is_empty() { + options = options.with_sheet_name(sheet_name); + } + } + + let rows = MiniExcel::query_with_options(path, &options).map_err(|error| { + set_last_error(error.to_string()); + ERROR_QUERY + })?; + let handle = Box::new(QueryHandle { + rows, + frame: Vec::new(), + }); + unsafe { ptr::write(out_handle, Box::into_raw(handle)) }; + Ok(RESULT_BATCH) + }) +} + +/// Writes the next bounded batch into memory owned by the query handle. +/// +/// # Safety +/// +/// `handle` must have been returned by `miniexcel_query_open` and not yet closed. Output pointers +/// must be non-null and writable. Returned data is valid until the next call using the handle. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn miniexcel_query_next_batch( + handle: *mut QueryHandle, + max_rows: u32, + out_data: *mut *const u8, + out_length: *mut usize, +) -> i32 { + ffi_result(|| { + if handle.is_null() || max_rows == 0 || out_data.is_null() || out_length.is_null() { + set_last_error("handle, max_rows, out_data, and out_length are required"); + return Err(ERROR_INVALID_ARGUMENT); + } + + let handle = unsafe { &mut *handle }; + handle.frame.clear(); + write_u32(&mut handle.frame, 0); + + let mut row_count = 0_u32; + while row_count < max_rows { + let Some(row) = handle.rows.next() else { + break; + }; + let row = row.map_err(|error| { + set_last_error(error.to_string()); + ERROR_QUERY + })?; + write_row(&mut handle.frame, &row)?; + row_count += 1; + } + + if row_count == 0 { + unsafe { + ptr::write(out_data, ptr::null()); + ptr::write(out_length, 0); + } + return Ok(RESULT_END); + } + + handle.frame[0..4].copy_from_slice(&row_count.to_le_bytes()); + unsafe { + ptr::write(out_data, handle.frame.as_ptr()); + ptr::write(out_length, handle.frame.len()); + } + Ok(RESULT_BATCH) + }) +} + +/// Closes a query handle and releases its worker and temporary resources. +/// +/// # Safety +/// +/// `handle` must be null or a handle returned by `miniexcel_query_open` that has not been closed. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn miniexcel_query_close(handle: *mut QueryHandle) { + if !handle.is_null() { + let _ = catch_unwind(AssertUnwindSafe(|| drop(unsafe { Box::from_raw(handle) }))); + } +} + +/// Returns the last error recorded on the current native thread. +/// +/// # Safety +/// +/// `out_length` may be null; otherwise it must be writable. The returned data remains valid until +/// the next MiniExcel FFI error on this thread. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn miniexcel_last_error(out_length: *mut usize) -> *const u8 { + LAST_ERROR.with(|error| { + let error = error.borrow(); + if !out_length.is_null() { + unsafe { ptr::write(out_length, error.len()) }; + } + error.as_ptr() + }) +} + +fn ffi_result(operation: impl FnOnce() -> Result) -> i32 { + match catch_unwind(AssertUnwindSafe(operation)) { + Ok(Ok(result)) => result, + Ok(Err(code)) => code, + Err(_) => { + set_last_error("Rust panic crossed the MiniExcel FFI boundary"); + ERROR_PANIC + } + } +} + +unsafe fn read_utf8<'a>(value: *const c_char) -> Result<&'a str, i32> { + unsafe { CStr::from_ptr(value) }.to_str().map_err(|error| { + set_last_error(error.to_string()); + ERROR_INVALID_ARGUMENT + }) +} + +fn set_last_error(message: impl AsRef) { + LAST_ERROR.with(|error| { + let mut error = error.borrow_mut(); + error.clear(); + error.extend_from_slice(message.as_ref().as_bytes()); + }); +} + +fn write_row(frame: &mut Vec, row: &DynamicRow) -> Result<(), i32> { + write_length(frame, row.len())?; + for (name, value) in row { + write_string(frame, name)?; + match value { + CellValue::Empty => frame.push(0), + CellValue::Bool(value) => { + frame.push(1); + frame.push(u8::from(*value)); + } + CellValue::Int(value) => { + frame.push(2); + frame.extend_from_slice(&value.to_le_bytes()); + } + CellValue::Float(value) => { + frame.push(3); + frame.extend_from_slice(&value.to_le_bytes()); + } + CellValue::String(value) => { + frame.push(4); + write_string(frame, value)?; + } + CellValue::Date(value) => { + frame.push(5); + write_string(frame, value.format("%Y-%m-%d").to_string())?; + } + CellValue::Time(value) => { + frame.push(6); + write_string(frame, value.format("%H:%M:%S%.f").to_string())?; + } + CellValue::DateTime(value) => { + frame.push(7); + write_string(frame, value.format("%Y-%m-%dT%H:%M:%S%.f").to_string())?; + } + CellValue::Duration(value) => { + frame.push(8); + frame.extend_from_slice(&value.num_milliseconds().to_le_bytes()); + } + CellValue::Error(value) => { + frame.push(9); + write_string(frame, value)?; + } + } + } + Ok(()) +} + +fn write_string(frame: &mut Vec, value: impl AsRef) -> Result<(), i32> { + let bytes = value.as_ref().as_bytes(); + write_length(frame, bytes.len())?; + frame.extend_from_slice(bytes); + Ok(()) +} + +fn write_length(frame: &mut Vec, length: usize) -> Result<(), i32> { + let length = u32::try_from(length).map_err(|_| { + set_last_error("FFI frame value exceeds the 4 GiB format limit"); + ERROR_QUERY + })?; + write_u32(frame, length); + Ok(()) +} + +fn write_u32(frame: &mut Vec, value: u32) { + frame.extend_from_slice(&value.to_le_bytes()); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn reports_the_supported_abi_version() { + assert_eq!(miniexcel_abi_version(), 1); + } + + #[test] + fn rejects_missing_required_query_arguments() { + let result = unsafe { + miniexcel_query_open(ptr::null(), 0, ptr::null(), ptr::null(), ptr::null_mut()) + }; + + assert_eq!(result, ERROR_INVALID_ARGUMENT); + + let mut length = 0; + let error = unsafe { miniexcel_last_error(&mut length) }; + let message = unsafe { std::slice::from_raw_parts(error, length) }; + assert_eq!(message, b"path, start_cell, and out_handle are required"); + } +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..67c827c --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.85.0" +profile = "minimal" +components = ["clippy", "rustfmt"] diff --git a/src/MiniExcelRust/MiniExcelRust.cs b/src/MiniExcelRust/MiniExcelRust.cs new file mode 100644 index 0000000..01d0660 --- /dev/null +++ b/src/MiniExcelRust/MiniExcelRust.cs @@ -0,0 +1,254 @@ +using System.Collections; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.Win32.SafeHandles; + +namespace MiniExcelLibs; + +/// +/// Provides XLSX queries backed by the native MiniExcel Rust library. +/// +public static class MiniExcelRust +{ + private const int BatchSize = 64; + + /// + /// Streams rows from an XLSX file through the native Rust query engine. + /// + public static IEnumerable> Query( + string path, + bool useHeaderRow = false, + string? sheetName = null, + string startCell = "A1") + { + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException("The path is required.", nameof(path)); + if (string.IsNullOrWhiteSpace(startCell)) + throw new ArgumentException("The start cell is required.", nameof(startCell)); + + return QueryIterator(Path.GetFullPath(path), useHeaderRow, sheetName, startCell); + } + + private static IEnumerable> QueryIterator( + string path, + bool useHeaderRow, + string? sheetName, + string startCell) + { + EnsureAbiVersion(); + + using var nativePath = new Utf8String(path); + using var nativeSheetName = new Utf8String(sheetName); + using var nativeStartCell = new Utf8String(startCell); + var result = NativeMethods.QueryOpen( + nativePath.Pointer, + useHeaderRow ? (byte)1 : (byte)0, + nativeSheetName.Pointer, + nativeStartCell.Pointer, + out var rawHandle); + if (result < 0) + throw CreateNativeException(result); + + using var handle = new NativeQueryHandle(rawHandle); + while (true) + { + result = NativeMethods.QueryNextBatch(handle, BatchSize, out var data, out var length); + if (result == 0) + yield break; + if (result < 0) + throw CreateNativeException(result); + + var byteLength = checked((int)length.ToUInt64()); + var frame = new byte[byteLength]; + Marshal.Copy(data, frame, 0, byteLength); + foreach (var row in DecodeBatch(frame)) + yield return row; + } + } + + private static IEnumerable> DecodeBatch(byte[] frame) + { + var reader = new FrameReader(frame); + var rowCount = reader.ReadLength(); + for (var rowIndex = 0; rowIndex < rowCount; rowIndex++) + { + var cellCount = reader.ReadLength(); + IDictionary row = new Dictionary(cellCount, StringComparer.Ordinal); + for (var cellIndex = 0; cellIndex < cellCount; cellIndex++) + row.Add(reader.ReadString(), reader.ReadValue()); + yield return row; + } + + reader.EnsureComplete(); + } + + private static void EnsureAbiVersion() + { + var version = NativeMethods.GetAbiVersion(); + if (version != 1) + throw new NotSupportedException($"MiniExcel Rust ABI version {version} is not supported."); + } + + private static Exception CreateNativeException(int result) + { + var data = NativeMethods.GetLastError(out var length); + var byteLength = checked((int)length.ToUInt64()); + if (data == IntPtr.Zero || byteLength == 0) + return new InvalidOperationException($"MiniExcel Rust query failed with native error {result}."); + + var bytes = new byte[byteLength]; + Marshal.Copy(data, bytes, 0, byteLength); + return new InvalidOperationException(Encoding.UTF8.GetString(bytes)); + } + + private sealed class FrameReader(byte[] frame) + { + private int _offset; + + public int ReadLength() + { + var value = ReadUInt32(); + if (value > int.MaxValue) + throw new InvalidDataException("The native MiniExcel frame contains an unsupported length."); + return (int)value; + } + + public string ReadString() + { + var length = ReadLength(); + EnsureAvailable(length); + var value = Encoding.UTF8.GetString(frame, _offset, length); + _offset += length; + return value; + } + + public object? ReadValue() + { + EnsureAvailable(1); + return frame[_offset++] switch + { + 0 => null, + 1 => ReadBoolean(), + 2 => Convert.ToDouble(ReadInt64(), CultureInfo.InvariantCulture), + 3 => BitConverter.Int64BitsToDouble(ReadInt64()), + 4 => ReadString(), + 5 => DateTime.ParseExact(ReadString(), "yyyy-MM-dd", CultureInfo.InvariantCulture), + 6 => TimeSpan.Parse(ReadString(), CultureInfo.InvariantCulture), + 7 => DateTime.Parse(ReadString(), CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind), + 8 => TimeSpan.FromMilliseconds(ReadInt64()), + 9 => ReadString(), + var tag => throw new InvalidDataException($"The native MiniExcel frame contains unknown value tag {tag}.") + }; + } + + public void EnsureComplete() + { + if (_offset != frame.Length) + throw new InvalidDataException("The native MiniExcel frame contains trailing data."); + } + + private bool ReadBoolean() + { + EnsureAvailable(1); + return frame[_offset++] != 0; + } + + private uint ReadUInt32() + { + EnsureAvailable(sizeof(uint)); + var value = (uint)(frame[_offset] + | frame[_offset + 1] << 8 + | frame[_offset + 2] << 16 + | frame[_offset + 3] << 24); + _offset += sizeof(uint); + return value; + } + + private long ReadInt64() + { + EnsureAvailable(sizeof(long)); + ulong value = 0; + for (var index = 0; index < sizeof(long); index++) + value |= (ulong)frame[_offset + index] << (index * 8); + _offset += sizeof(long); + return unchecked((long)value); + } + + private void EnsureAvailable(int length) + { + if (length < 0 || _offset > frame.Length - length) + throw new InvalidDataException("The native MiniExcel frame is truncated."); + } + } + + private sealed class Utf8String : IDisposable + { + public Utf8String(string? value) + { + if (value is null) + return; + + var bytes = Encoding.UTF8.GetBytes(value); + Pointer = Marshal.AllocHGlobal(bytes.Length + 1); + Marshal.Copy(bytes, 0, Pointer, bytes.Length); + Marshal.WriteByte(Pointer, bytes.Length, 0); + } + + public IntPtr Pointer { get; private set; } + + public void Dispose() + { + if (Pointer == IntPtr.Zero) + return; + + Marshal.FreeHGlobal(Pointer); + Pointer = IntPtr.Zero; + } + } + + private sealed class NativeQueryHandle : SafeHandleZeroOrMinusOneIsInvalid + { + public NativeQueryHandle() : base(true) { } + + public NativeQueryHandle(IntPtr value) : this() + { + SetHandle(value); + } + + protected override bool ReleaseHandle() + { + NativeMethods.QueryClose(handle); + return true; + } + } + + private static class NativeMethods + { + private const string LibraryName = "miniexcel_ffi"; + + [DllImport(LibraryName, EntryPoint = "miniexcel_abi_version", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern uint GetAbiVersion(); + + [DllImport(LibraryName, EntryPoint = "miniexcel_query_open", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern int QueryOpen( + IntPtr path, + byte useHeaderRow, + IntPtr sheetName, + IntPtr startCell, + out IntPtr handle); + + [DllImport(LibraryName, EntryPoint = "miniexcel_query_next_batch", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern int QueryNextBatch( + NativeQueryHandle handle, + uint maxRows, + out IntPtr data, + out UIntPtr length); + + [DllImport(LibraryName, EntryPoint = "miniexcel_query_close", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern void QueryClose(IntPtr handle); + + [DllImport(LibraryName, EntryPoint = "miniexcel_last_error", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern IntPtr GetLastError(out UIntPtr length); + } +} \ No newline at end of file diff --git a/src/MiniExcelRust/MiniExcelRust.csproj b/src/MiniExcelRust/MiniExcelRust.csproj new file mode 100644 index 0000000..23b6f7f --- /dev/null +++ b/src/MiniExcelRust/MiniExcelRust.csproj @@ -0,0 +1,56 @@ + + + + netstandard2.0;net8.0 + latest + enable + enable + MiniExcelLibs + MiniExcelRust + MiniExcelRust + 0.1.0-preview.1 + MiniExcelRust + Low-memory, high-performance XLSX queries for .NET powered by MiniExcel for Rust. + Mini-Software + Mini-Software + excel;xlsx;rust;native;streaming + Apache-2.0 + https://github.com/mini-software/MiniExcelRust + https://github.com/mini-software/MiniExcelRust + git + README.md + false + true + true + snupkg + $(MSBuildThisFileDirectory)..\..\artifacts\native + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/MiniExcelRust.PackageTests/MiniExcelRust.PackageTests.csproj b/tests/MiniExcelRust.PackageTests/MiniExcelRust.PackageTests.csproj new file mode 100644 index 0000000..244880d --- /dev/null +++ b/tests/MiniExcelRust.PackageTests/MiniExcelRust.PackageTests.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + enable + 0.1.0-preview.1 + + + + + + + diff --git a/tests/MiniExcelRust.PackageTests/Program.cs b/tests/MiniExcelRust.PackageTests/Program.cs new file mode 100644 index 0000000..4c760c7 --- /dev/null +++ b/tests/MiniExcelRust.PackageTests/Program.cs @@ -0,0 +1,83 @@ +using System.IO.Compression; +using System.Text; +using MiniExcelLibs; + +var workbookPath = Path.Combine(Path.GetTempPath(), $"miniexcel-rust-{Guid.NewGuid():N}.xlsx"); + +try +{ + CreateWorkbook(workbookPath); + + var rows = MiniExcelRust.Query(workbookPath, useHeaderRow: true).ToList(); + Require(rows.Count == 2, $"Expected 2 rows, received {rows.Count}."); + Require(Equals(rows[0]["Name"], "alpha"), "The first string value did not match."); + Require(Equals(rows[0]["Value"], 42d), "The first numeric value did not match."); + Require(Equals(rows[1]["Name"], "beta"), "The second string value did not match."); + Require(Equals(rows[1]["Value"], true), "The boolean value did not match."); + + using (var enumerator = MiniExcelRust.Query(workbookPath).GetEnumerator()) + Require(enumerator.MoveNext(), "The early-disposal query returned no rows."); + + File.Delete(workbookPath); + Console.WriteLine("MiniExcelRust package smoke test passed."); +} +finally +{ + if (File.Exists(workbookPath)) + File.Delete(workbookPath); +} + +static void Require(bool condition, string message) +{ + if (!condition) + throw new InvalidOperationException(message); +} + +static void CreateWorkbook(string path) +{ + using var archive = ZipFile.Open(path, ZipArchiveMode.Create); + AddEntry(archive, "[Content_Types].xml", """ + + + + + + + + """); + AddEntry(archive, "_rels/.rels", """ + + + + + """); + AddEntry(archive, "xl/workbook.xml", """ + + + + + """); + AddEntry(archive, "xl/_rels/workbook.xml.rels", """ + + + + + """); + AddEntry(archive, "xl/worksheets/sheet1.xml", """ + + + + NameValue + alpha42 + beta1 + + + """); +} + +static void AddEntry(ZipArchive archive, string name, string contents) +{ + var entry = archive.CreateEntry(name, CompressionLevel.Fastest); + using var writer = new StreamWriter(entry.Open(), new UTF8Encoding(false)); + writer.Write(contents); +} \ No newline at end of file From 6b9e54ffb1382e61f531cc76d3a7e5f31047ae33 Mon Sep 17 00:00:00 2001 From: Wei Lin Date: Sat, 5 Sep 2026 19:33:21 +0800 Subject: [PATCH 2/6] fix: use cdylib-capable Rust for musl builds Keep Rust 1.85 as the source MSRV, but pin Rust 1.97.1 for the two musl release artifacts because the 1.85 musl targets cannot emit cdylibs. --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 11 ++++++++++- README.md | 4 ++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab6d57e..32a6907 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,7 +74,7 @@ jobs: runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.85.0 + - uses: dtolnay/rust-toolchain@1.97.1 with: targets: ${{ matrix.rid == 'linux-musl-x64' && 'x86_64-unknown-linux-musl' || 'aarch64-unknown-linux-musl' }} - uses: actions/setup-dotnet@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 065a513..4526205 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,40 +49,49 @@ jobs: - runner: windows-latest rid: win-x64 target: x86_64-pc-windows-msvc + rust: 1.85.0 zig: false - runner: windows-11-arm rid: win-arm64 target: aarch64-pc-windows-msvc + rust: 1.85.0 zig: false - runner: ubuntu-latest rid: linux-x64 target: x86_64-unknown-linux-gnu + rust: 1.85.0 zig: false - runner: ubuntu-24.04-arm rid: linux-arm64 target: aarch64-unknown-linux-gnu + rust: 1.85.0 zig: false - runner: ubuntu-latest rid: linux-musl-x64 target: x86_64-unknown-linux-musl + rust: 1.97.1 zig: true - runner: ubuntu-24.04-arm rid: linux-musl-arm64 target: aarch64-unknown-linux-musl + rust: 1.97.1 zig: true - runner: macos-15-intel rid: osx-x64 target: x86_64-apple-darwin + rust: 1.85.0 zig: false - runner: macos-latest rid: osx-arm64 target: aarch64-apple-darwin + rust: 1.85.0 zig: false runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.85.0 + - uses: dtolnay/rust-toolchain@master with: + toolchain: ${{ matrix.rust }} targets: ${{ matrix.target }} - uses: mlugg/setup-zig@v2 if: matrix.zig diff --git a/README.md b/README.md index fb07539..9fe247b 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,10 @@ library under `runtimes/{rid}/native/`. .NET selects the matching asset at publi Rust 1.85 and .NET SDK 8 or later are required. +Rust 1.85 is the source MSRV and builds the Windows, GNU Linux, and macOS libraries. The +official Rust 1.85 musl targets cannot emit `cdylib` artifacts, so release automation pins +Rust 1.97.1 for the two musl libraries. + ```powershell cargo test --workspace --all-targets --locked dotnet build ./src/MiniExcelRust/MiniExcelRust.csproj -c Release From 36b1149d34186f5321b08ecb81abe22377597327 Mon Sep 17 00:00:00 2001 From: Wei Lin Date: Sat, 5 Sep 2026 19:42:09 +0800 Subject: [PATCH 3/6] fix: select the musl Rust toolchain explicitly --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 5 ++++- build/Build-Native.ps1 | 27 ++++++++++++++++++++++----- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32a6907..c6ee6dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,7 +89,7 @@ jobs: key: ${{ matrix.rid }} - name: Build native asset shell: pwsh - run: ./build/Build-Native.ps1 -Rid '${{ matrix.rid }}' -UseZig + run: ./build/Build-Native.ps1 -Rid '${{ matrix.rid }}' -UseZig -Toolchain 1.97.1 - name: Pack run: dotnet pack src/MiniExcelRust/MiniExcelRust.csproj -c Release -o artifacts/packages -p:MiniExcelRustRequireAllNativeAssets=false - name: Test package in Alpine diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4526205..a587c6c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -105,7 +105,10 @@ jobs: - name: Build native asset shell: pwsh run: | - $arguments = @{ Rid = '${{ matrix.rid }}' } + $arguments = @{ + Rid = '${{ matrix.rid }}' + Toolchain = '${{ matrix.rust }}' + } if ('${{ matrix.zig }}' -eq 'true') { $arguments.UseZig = $true } diff --git a/build/Build-Native.ps1 b/build/Build-Native.ps1 index c0060f0..87b5215 100644 --- a/build/Build-Native.ps1 +++ b/build/Build-Native.ps1 @@ -13,7 +13,9 @@ param( )] [string] $Rid, - [switch] $UseZig + [switch] $UseZig, + + [string] $Toolchain ) $ErrorActionPreference = 'Stop' @@ -33,16 +35,31 @@ $targets = @{ $target = $targets[$Rid] Push-Location $repositoryRoot try { - & rustup target add $target.Triple + if (-not [string]::IsNullOrWhiteSpace($Toolchain)) { + & rustup target add --toolchain $Toolchain $target.Triple + } + else { + & rustup target add $target.Triple + } if ($LASTEXITCODE -ne 0) { throw "Failed to install Rust target $($target.Triple)." } - if ($UseZig) { - & cargo zigbuild --release --locked -p miniexcel-ffi --target $target.Triple + $buildArguments = @( + $(if ($UseZig) { 'zigbuild' } else { 'build' }), + '--release', + '--locked', + '-p', + 'miniexcel-ffi', + '--target', + $target.Triple + ) + + if (-not [string]::IsNullOrWhiteSpace($Toolchain)) { + & rustup run $Toolchain cargo @buildArguments } else { - & cargo build --release --locked -p miniexcel-ffi --target $target.Triple + & cargo @buildArguments } if ($LASTEXITCODE -ne 0) { throw "Failed to build native library for $Rid." From 2a60423dfa9337932c44bff7983aceed80fec2b5 Mon Sep 17 00:00:00 2001 From: Wei Lin Date: Sat, 5 Sep 2026 19:58:01 +0800 Subject: [PATCH 4/6] fix: build musl cdylibs with dynamic CRT --- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- README.md | 6 +++--- build/Build-Native.ps1 | 6 ++++++ 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6ee6dc..3342e85 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,7 +74,7 @@ jobs: runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.97.1 + - uses: dtolnay/rust-toolchain@1.85.0 with: targets: ${{ matrix.rid == 'linux-musl-x64' && 'x86_64-unknown-linux-musl' || 'aarch64-unknown-linux-musl' }} - uses: actions/setup-dotnet@v4 @@ -89,7 +89,7 @@ jobs: key: ${{ matrix.rid }} - name: Build native asset shell: pwsh - run: ./build/Build-Native.ps1 -Rid '${{ matrix.rid }}' -UseZig -Toolchain 1.97.1 + run: ./build/Build-Native.ps1 -Rid '${{ matrix.rid }}' -UseZig -Toolchain 1.85.0 - name: Pack run: dotnet pack src/MiniExcelRust/MiniExcelRust.csproj -c Release -o artifacts/packages -p:MiniExcelRustRequireAllNativeAssets=false - name: Test package in Alpine diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a587c6c..4d459fb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,12 +69,12 @@ jobs: - runner: ubuntu-latest rid: linux-musl-x64 target: x86_64-unknown-linux-musl - rust: 1.97.1 + rust: 1.85.0 zig: true - runner: ubuntu-24.04-arm rid: linux-musl-arm64 target: aarch64-unknown-linux-musl - rust: 1.97.1 + rust: 1.85.0 zig: true - runner: macos-15-intel rid: osx-x64 diff --git a/README.md b/README.md index 9fe247b..00cb2ac 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,9 @@ library under `runtimes/{rid}/native/`. .NET selects the matching asset at publi Rust 1.85 and .NET SDK 8 or later are required. -Rust 1.85 is the source MSRV and builds the Windows, GNU Linux, and macOS libraries. The -official Rust 1.85 musl targets cannot emit `cdylib` artifacts, so release automation pins -Rust 1.97.1 for the two musl libraries. +Rust 1.85 is the source and release MSRV. The musl targets default to a static C runtime, +which cannot produce a `cdylib`, so their build disables `crt-static` and links dynamically +against musl before testing the package in Alpine. ```powershell cargo test --workspace --all-targets --locked diff --git a/build/Build-Native.ps1 b/build/Build-Native.ps1 index 87b5215..493f220 100644 --- a/build/Build-Native.ps1 +++ b/build/Build-Native.ps1 @@ -33,6 +33,7 @@ $targets = @{ } $target = $targets[$Rid] +$originalRustFlags = $env:RUSTFLAGS Push-Location $repositoryRoot try { if (-not [string]::IsNullOrWhiteSpace($Toolchain)) { @@ -55,6 +56,10 @@ try { $target.Triple ) + if ($UseZig) { + $env:RUSTFLAGS = "$originalRustFlags -C target-feature=-crt-static".Trim() + } + if (-not [string]::IsNullOrWhiteSpace($Toolchain)) { & rustup run $Toolchain cargo @buildArguments } @@ -71,5 +76,6 @@ try { Copy-Item $source (Join-Path $destinationDirectory $target.File) -Force } finally { + $env:RUSTFLAGS = $originalRustFlags Pop-Location } From 65703dae5f871035d94d5c79a59a52794527b375 Mon Sep 17 00:00:00 2001 From: Wei Lin Date: Sat, 5 Sep 2026 20:01:04 +0800 Subject: [PATCH 5/6] ci: assemble the complete preview package --- .github/workflows/ci.yml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3342e85..996b9fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,8 @@ name: CI on: push: + branches: + - main pull_request: permissions: @@ -60,6 +62,11 @@ jobs: - name: Build, pack, and test shell: pwsh run: ./build/Test-Package.ps1 -Rid '${{ matrix.rid }}' + - uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.rid }} + path: artifacts/native/${{ matrix.rid }}/* + if-no-files-found: error musl-package-test: name: Test ${{ matrix.rid }} package @@ -100,3 +107,32 @@ jobs: -w /work \ mcr.microsoft.com/dotnet/sdk:8.0-alpine \ sh -lc 'dotnet restore tests/MiniExcelRust.PackageTests/MiniExcelRust.PackageTests.csproj --source artifacts/packages && dotnet run --project tests/MiniExcelRust.PackageTests/MiniExcelRust.PackageTests.csproj -c Release --no-restore' + - uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.rid }} + path: artifacts/native/${{ matrix.rid }}/* + if-no-files-found: error + + assemble-package: + name: Assemble eight-RID NuGet + needs: [validate, native-package-test, musl-package-test] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + - uses: actions/download-artifact@v4 + with: + pattern: '*' + path: artifacts/native + - name: Pack complete NuGet + run: dotnet pack src/MiniExcelRust/MiniExcelRust.csproj -c Release -o artifacts/packages + - name: Verify package contents + shell: pwsh + run: ./build/Verify-Package.ps1 -PackagePath artifacts/packages/MiniExcelRust.0.1.0-preview.1.nupkg + - uses: actions/upload-artifact@v4 + with: + name: nuget-preview + path: artifacts/packages/MiniExcelRust.0.1.0-preview.1.*nupkg + if-no-files-found: error From cc1c2c73ce66d2a31ffd424f612dd7145f6e7fa5 Mon Sep 17 00:00:00 2001 From: Wei Lin Date: Sat, 5 Sep 2026 20:32:12 +0800 Subject: [PATCH 6/6] ci: use NuGet trusted publishing --- .github/workflows/release.yml | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4d459fb..a98063d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -206,22 +206,19 @@ jobs: environment: release permissions: contents: write - env: - NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + id-token: write steps: - uses: actions/download-artifact@v4 with: name: nuget-package path: artifacts/packages - - name: Require NuGet API key - shell: bash - run: | - if [ -z "$NUGET_API_KEY" ]; then - echo "::error::Add NUGET_API_KEY to the protected release environment." - exit 1 - fi + - name: NuGet login + id: nuget-login + uses: NuGet/login@v1 + with: + user: minisoftware - name: Publish NuGet package - run: dotnet nuget push artifacts/packages/MiniExcelRust.${{ needs.prepare.outputs.version }}.nupkg --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json --skip-duplicate + run: dotnet nuget push artifacts/packages/MiniExcelRust.${{ needs.prepare.outputs.version }}.nupkg --api-key "${{ steps.nuget-login.outputs.NUGET_API_KEY }}" --source https://api.nuget.org/v3/index.json --skip-duplicate - name: Create GitHub release if: github.event_name == 'push' env: