Skip to content

Enhance public API and add trimming compatibility attributes #28

Enhance public API and add trimming compatibility attributes

Enhance public API and add trimming compatibility attributes #28

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