Enhance public API and add trimming compatibility attributes #30
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Native bindings | |
| on: | |
| push: | |
| branches: [master, develop] | |
| pull_request: | |
| branches: [master, develop] | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: native-bindings-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # Gates the expensive jobs below by which paths actually changed. Filtering happens inside the | |
| # workflow (an `if:` per job), not at the trigger level (`on.pull_request.paths`) - the trigger-level | |
| # form skips the whole workflow run for a non-matching PR, which leaves every required status check | |
| # it owns stuck at "expected" forever and blocks the merge button. A skipped *job* still posts a real | |
| # check run with conclusion=skipped, which GitHub does treat as passing for a required check. | |
| changes: | |
| name: Detect changed paths | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| native: ${{ steps.filter.outputs.native }} | |
| rust: ${{ steps.filter.outputs.rust }} | |
| python: ${{ steps.filter.outputs.python }} | |
| cpp: ${{ steps.filter.outputs.cpp }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| - name: Filter | |
| id: filter | |
| uses: dorny/paths-filter@v4 | |
| with: | |
| filters: | | |
| native: | |
| - 'src/ExcelReader.Native/**' | |
| - 'src/ExcelReader.Core/**' | |
| - 'global.json' | |
| - 'python/scripts/build_native.py' | |
| - '.github/workflows/native-bindings.yml' | |
| rust: | |
| - 'rust/**' | |
| python: | |
| - 'python/**' | |
| - 'tests/ExcelReader.NativeSmoke/**' | |
| cpp: | |
| - 'cpp/**' | |
| # Rust/Python/C++ bindings each need the same NativeAOT-published ExcelReader.Native binary per OS. | |
| # Building it once here and sharing it via artifact (instead of each binding workflow calling | |
| # build_native.py itself) cuts three redundant NativeAOT publishes per OS down to one. Required as | |
| # its own status check: a failure here fails a job with no `needs` gating it on another job's | |
| # result, so it can't be masked by GitHub treating a skipped downstream job as a passed required | |
| # check. | |
| # | |
| # Runs whenever any binding might need the binary it produces - narrower than any single test job, | |
| # since it feeds all three. | |
| build-native: | |
| name: Build native library (${{ matrix.os }}) | |
| needs: changes | |
| if: >- | |
| needs.changes.outputs.native == 'true' || | |
| needs.changes.outputs.rust == 'true' || | |
| needs.changes.outputs.python == 'true' || | |
| needs.changes.outputs.cpp == 'true' | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, windows-latest, macos-latest] | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v6 | |
| with: | |
| global-json-file: global.json | |
| - name: Cache NuGet packages | |
| uses: actions/cache@v6 | |
| with: | |
| path: ~/.nuget/packages | |
| key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/Directory.Build.props') }} | |
| restore-keys: | | |
| ${{ runner.os }}-nuget- | |
| - name: Build the native library | |
| shell: bash | |
| run: python python/scripts/build_native.py | |
| - name: Upload native library | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: native-lib-${{ matrix.os }} | |
| path: python/src/excelreader/_lib/* | |
| if-no-files-found: error | |
| test-rust: | |
| name: Rust crate (${{ matrix.os }}) | |
| needs: [changes, build-native] | |
| if: needs.changes.outputs.native == 'true' || needs.changes.outputs.rust == 'true' | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, windows-latest, macos-latest] | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| # Pinned explicitly rather than relying on the runner image's preinstalled Rust: the clippy | |
| # step below needs the component to be present, and which components an image ships is not | |
| # part of its compatibility promise. | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: clippy | |
| - name: Cache cargo | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: rust | |
| - name: Download native library | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: native-lib-${{ matrix.os }} | |
| path: python/src/excelreader/_lib | |
| # actions/upload-artifact drops the executable bit on the way through. dlopen()'s initial ELF | |
| # header read tolerates that, but the NativeAOT runtime's own PAL init - triggered by the first | |
| # real P/Invoke, not by CDLL() itself - remaps the file with PROT_EXEC and aborts the whole | |
| # process (SIGABRT, not a catchable dlopen error) when that mapping fails. | |
| - name: Restore executable bit (Linux/macOS) | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: chmod +x python/src/excelreader/_lib/* | |
| # rust/excelreader/excelreader.def is a hand-kept copy of the canonical list, needed | |
| # because `cargo package` only ships files under the crate directory (see build.rs). A silent | |
| # divergence would surface as an unresolved symbol at link time in a consumer's build, not here. | |
| - name: Verify the .def copy matches the canonical one | |
| shell: bash | |
| run: diff src/ExcelReader.Native/include/excelreader.def rust/excelreader/excelreader.def | |
| - name: Locate published native library directory | |
| id: lib | |
| shell: bash | |
| run: echo "dir=python/src/excelreader/_lib" >> "$GITHUB_OUTPUT" | |
| - name: Setup MSVC developer environment (Windows) | |
| if: runner.os == 'Windows' | |
| uses: ilammy/msvc-dev-cmd@v1 | |
| # --all-targets covers the integration and trybuild suites, not just the library; --all-features | |
| # covers the optional `chrono` interop, which is otherwise never compiled. | |
| - name: cargo clippy | |
| working-directory: rust | |
| env: | |
| EXCELREADER_NATIVE_LIB_DIR: ${{ github.workspace }}/${{ steps.lib.outputs.dir }} | |
| run: cargo clippy --workspace --all-targets --all-features -- -D warnings | |
| - name: cargo test | |
| working-directory: rust | |
| env: | |
| EXCELREADER_NATIVE_LIB_DIR: ${{ github.workspace }}/${{ steps.lib.outputs.dir }} | |
| run: cargo test --workspace --all-features --verbose | |
| # The default feature set is what crates.io consumers get by default, so it has to build on | |
| # its own - `chrono` being the only feature makes it cheap to prove rather than assume. | |
| - name: cargo build (default features) | |
| working-directory: rust | |
| env: | |
| EXCELREADER_NATIVE_LIB_DIR: ${{ github.workspace }}/${{ steps.lib.outputs.dir }} | |
| run: cargo build --workspace --verbose | |
| test-python: | |
| name: Native + Python + C smoke (${{ matrix.os }}) | |
| needs: [changes, build-native] | |
| if: needs.changes.outputs.native == 'true' || needs.changes.outputs.python == 'true' | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, windows-latest, macos-latest] | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| - name: Setup Python | |
| uses: actions/setup-python@v7 | |
| with: | |
| python-version: '3.12' | |
| cache: pip | |
| cache-dependency-path: python/pyproject.toml | |
| - name: Download native library | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: native-lib-${{ matrix.os }} | |
| path: python/src/excelreader/_lib | |
| # See the matching step in test-rust: upload-artifact strips the executable bit, and the | |
| # NativeAOT runtime aborts the process (not a catchable Python exception) without it. | |
| - name: Restore executable bit (Linux/macOS) | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: chmod +x python/src/excelreader/_lib/* | |
| - name: Install the Python package | |
| run: pip install -e "python[dev]" | |
| - name: Run the Python tests | |
| run: pytest python/tests -v | |
| - name: Build the C smoke test | |
| run: | | |
| cmake -S tests/ExcelReader.NativeSmoke -B build/nativesmoke -DCMAKE_BUILD_TYPE=Release | |
| cmake --build build/nativesmoke --config Release | |
| - name: Run the C smoke test | |
| run: ctest --test-dir build/nativesmoke --output-on-failure -C Release | |
| test-cpp: | |
| name: C++ package + smoke test (${{ matrix.os }}) | |
| needs: [changes, build-native] | |
| if: needs.changes.outputs.native == 'true' || needs.changes.outputs.cpp == 'true' | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, windows-latest, macos-latest] | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Enable symlinks (Windows) | |
| if: runner.os == 'Windows' | |
| run: | | |
| git config core.symlinks true | |
| git checkout -- cpp/include | |
| - name: Download native library | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: native-lib-${{ matrix.os }} | |
| path: python/src/excelreader/_lib | |
| # See the matching step in test-rust: upload-artifact strips the executable bit, and the | |
| # NativeAOT runtime aborts the process (not a catchable error) without it. | |
| - name: Restore executable bit (Linux/macOS) | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: chmod +x python/src/excelreader/_lib/* | |
| - name: Setup MSVC developer environment (Windows) | |
| if: runner.os == 'Windows' | |
| uses: ilammy/msvc-dev-cmd@v1 | |
| - name: Locate published native library | |
| id: lib | |
| shell: bash | |
| run: | | |
| case "${{ runner.os }}" in | |
| Windows) echo "path=${{ github.workspace }}/python/src/excelreader/_lib/ExcelReader.Native.dll" >> "$GITHUB_OUTPUT" ;; | |
| macOS) echo "path=${{ github.workspace }}/python/src/excelreader/_lib/ExcelReader.Native.dylib" >> "$GITHUB_OUTPUT" ;; | |
| *) echo "path=${{ github.workspace }}/python/src/excelreader/_lib/ExcelReader.Native.so" >> "$GITHUB_OUTPUT" ;; | |
| esac | |
| - name: Configure | |
| env: | |
| EXCELREADER_NATIVE_LIB: ${{ steps.lib.outputs.path }} | |
| run: cmake -S cpp -B build/cpp -DEXCELREADER_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Release | |
| - name: Build | |
| run: cmake --build build/cpp --config Release | |
| - name: Test | |
| run: ctest --test-dir build/cpp --output-on-failure -C Release |