unparser-shared 0.12.0

Shared plumbing for the un* document extraction family — C-ABI conventions today; markdown refine and VLM/AI capabilities land as opt-in features
Documentation
name: Build and Test Bindings (reusable)

# Builds the native library for every shipped platform, packs the Python wheel/sdist and the
# NuGet package around it, and tests both packages. It publishes nothing: the release pipeline
# (release-reusable.yml) calls this as its binding-tests gate and publishes these artifacts
# itself, after the GitHub Release exists -- so the bytes that passed here are the bytes that
# ship. The artifact names (python-wheel, python-sdist, nuget-package, native-<rid>) are that
# hand-off's contract.

on:
  workflow_call:
    inputs:
      package-name:
        description: 'Rust crate name (also the Python import name), e.g. unhwp'
        required: true
        type: string
      csharp-project-name:
        description: 'PascalCase C# project name, e.g. Unhwp'
        required: true
        type: string
      nuget-staging-mode:
        description: 'How the native library is staged for C# tests: "flat" or "runtime-rid"'
        required: true
        type: string
      nuget-test-matrix:
        description: 'JSON array of {os, artifact, runtime} entries for the NuGet test matrix'
        required: true
        type: string
      validate-packed-nuget:
        description: 'Swap ProjectReference for the packed PackageReference before testing'
        required: false
        type: boolean
        default: false

env:
  CARGO_TERM_COLOR: always

jobs:
  build-native:
    name: Build Native Libraries
    runs-on: ${{ matrix.os }}
    timeout-minutes: 60
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
            artifact: lib${{ inputs.package-name }}.so
            runtime: linux-x64
          - os: ubuntu-latest
            target: x86_64-unknown-linux-musl
            artifact: lib${{ inputs.package-name }}.so
            runtime: linux-musl-x64
          - os: windows-latest
            target: x86_64-pc-windows-msvc
            artifact: ${{ inputs.package-name }}.dll
            runtime: win-x64
          - os: macos-latest
            target: x86_64-apple-darwin
            artifact: lib${{ inputs.package-name }}.dylib
            runtime: osx-x64
          - os: macos-latest
            target: aarch64-apple-darwin
            artifact: lib${{ inputs.package-name }}.dylib
            runtime: osx-arm64

    steps:
      - uses: actions/checkout@v4

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      - name: Install musl-tools
        if: contains(matrix.target, 'musl')
        run: sudo apt-get update && sudo apt-get install -y musl-tools

      - name: Build native library
        run: cargo build --release --features ffi --target ${{ matrix.target }}
        env:
          RUSTFLAGS: ${{ contains(matrix.target, 'musl') && '-C target-feature=-crt-static' || '' }}

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: native-${{ matrix.runtime }}
          path: target/${{ matrix.target }}/release/${{ matrix.artifact }}
          retention-days: 7

  build-python:
    name: Build Python Wheel
    needs: build-native
    runs-on: ubuntu-latest
    timeout-minutes: 20

    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Download all native libraries
        uses: actions/download-artifact@v4
        with:
          pattern: native-*
          path: ./native-libs/

      - name: Copy native libraries to package
        run: |
          mkdir -p bindings/python/src/${{ inputs.package-name }}/lib/linux-x64
          mkdir -p bindings/python/src/${{ inputs.package-name }}/lib/linux-musl-x64
          mkdir -p bindings/python/src/${{ inputs.package-name }}/lib/win-x64
          mkdir -p bindings/python/src/${{ inputs.package-name }}/lib/osx-x64
          mkdir -p bindings/python/src/${{ inputs.package-name }}/lib/osx-arm64
          cp ./native-libs/native-linux-x64/* bindings/python/src/${{ inputs.package-name }}/lib/linux-x64/
          cp ./native-libs/native-linux-musl-x64/* bindings/python/src/${{ inputs.package-name }}/lib/linux-musl-x64/
          cp ./native-libs/native-win-x64/* bindings/python/src/${{ inputs.package-name }}/lib/win-x64/
          cp ./native-libs/native-osx-x64/* bindings/python/src/${{ inputs.package-name }}/lib/osx-x64/
          cp ./native-libs/native-osx-arm64/* bindings/python/src/${{ inputs.package-name }}/lib/osx-arm64/
          echo "Native libraries:"
          find bindings/python/src/${{ inputs.package-name }}/lib -type f

      - name: Install build dependencies
        run: |
          python -m pip install --upgrade pip
          pip install build wheel

      - name: Build wheel
        working-directory: bindings/python
        run: python -m build

      - name: Verify wheel
        run: |
          pip install bindings/python/dist/*.whl
          python -c "import ${{ inputs.package-name }}; print('Import OK')"
          unzip -l bindings/python/dist/*.whl | head -50

      - name: Upload wheel
        uses: actions/upload-artifact@v4
        with:
          name: python-wheel
          path: bindings/python/dist/*.whl
          retention-days: 7

      - name: Upload sdist
        uses: actions/upload-artifact@v4
        with:
          name: python-sdist
          path: bindings/python/dist/*.tar.gz
          retention-days: 7

  build-nuget:
    name: Build NuGet Package
    needs: build-native
    runs-on: ubuntu-latest
    timeout-minutes: 20

    steps:
      - uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '10.0.x'

      - name: Create runtime directories
        run: |
          mkdir -p bindings/csharp/${{ inputs.csharp-project-name }}/runtimes/win-x64/native
          mkdir -p bindings/csharp/${{ inputs.csharp-project-name }}/runtimes/linux-x64/native
          mkdir -p bindings/csharp/${{ inputs.csharp-project-name }}/runtimes/linux-musl-x64/native
          mkdir -p bindings/csharp/${{ inputs.csharp-project-name }}/runtimes/osx-x64/native
          mkdir -p bindings/csharp/${{ inputs.csharp-project-name }}/runtimes/osx-arm64/native

      - name: Download Windows native library
        uses: actions/download-artifact@v4
        with:
          name: native-win-x64
          path: bindings/csharp/${{ inputs.csharp-project-name }}/runtimes/win-x64/native/

      - name: Download Linux native library
        uses: actions/download-artifact@v4
        with:
          name: native-linux-x64
          path: bindings/csharp/${{ inputs.csharp-project-name }}/runtimes/linux-x64/native/

      - name: Download Linux musl native library
        uses: actions/download-artifact@v4
        with:
          name: native-linux-musl-x64
          path: bindings/csharp/${{ inputs.csharp-project-name }}/runtimes/linux-musl-x64/native/

      - name: Download macOS x64 native library
        uses: actions/download-artifact@v4
        with:
          name: native-osx-x64
          path: bindings/csharp/${{ inputs.csharp-project-name }}/runtimes/osx-x64/native/

      - name: Download macOS ARM64 native library
        uses: actions/download-artifact@v4
        with:
          name: native-osx-arm64
          path: bindings/csharp/${{ inputs.csharp-project-name }}/runtimes/osx-arm64/native/

      - name: Build NuGet package
        working-directory: bindings/csharp
        run: dotnet pack ${{ inputs.csharp-project-name }}/${{ inputs.csharp-project-name }}.csproj -c Release -o ./artifacts

      - name: Upload NuGet package
        uses: actions/upload-artifact@v4
        with:
          name: nuget-package
          path: bindings/csharp/artifacts/*.nupkg
          retention-days: 7

  test-python:
    name: Test Python Bindings
    needs: build-python
    runs-on: ${{ matrix.os }}
    timeout-minutes: 30
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        python-version: ['3.11']

    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}

      - name: Download wheel
        uses: actions/download-artifact@v4
        with:
          name: python-wheel
          path: ./wheels/

      - name: Install package
        shell: bash
        run: |
          pip install pytest
          pip install ./wheels/*.whl

      - name: Run tests
        working-directory: bindings/python
        shell: bash
        run: pytest tests/ -v

  test-nuget:
    name: Test NuGet Package
    needs: build-nuget
    runs-on: ${{ matrix.os }}
    timeout-minutes: 30
    strategy:
      matrix:
        include: ${{ fromJSON(inputs.nuget-test-matrix) }}

    steps:
      - uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '10.0.x'

      - name: Download NuGet package
        uses: actions/download-artifact@v4
        with:
          name: nuget-package
          path: ./packages/

      - name: Download native library
        uses: actions/download-artifact@v4
        with:
          name: ${{ matrix.artifact }}
          path: ./native-lib/

      - name: Create local NuGet source
        if: ${{ inputs.validate-packed-nuget }}
        run: dotnet nuget add source "${{ github.workspace }}/packages" -n local

      - name: Switch test project to the packed NuGet package
        if: ${{ inputs.validate-packed-nuget }}
        shell: bash
        run: |
          NUPKG=$(ls packages/*.nupkg | head -1)
          VERSION=$(basename "$NUPKG" | sed 's/${{ inputs.csharp-project-name }}\.\(.*\)\.nupkg/\1/')
          echo "Using ${{ inputs.csharp-project-name }} version: $VERSION"
          # Replace ProjectReference with PackageReference in test project
          # Use wildcard pattern to match any path separator (/ or \)
          sed -i.bak 's|<ProjectReference Include="[^"]*${{ inputs.csharp-project-name }}\.csproj"[^/]*/\?>|<PackageReference Include="${{ inputs.csharp-project-name }}" Version="'"$VERSION"'" />|g' bindings/csharp/${{ inputs.csharp-project-name }}.Tests/${{ inputs.csharp-project-name }}.Tests.csproj
          echo "Updated test project:"
          cat bindings/csharp/${{ inputs.csharp-project-name }}.Tests/${{ inputs.csharp-project-name }}.Tests.csproj

      - name: Build test project
        working-directory: bindings/csharp
        run: dotnet build ${{ inputs.csharp-project-name }}.Tests/${{ inputs.csharp-project-name }}.Tests.csproj -c Release

      - name: Stage native library (flat)
        if: ${{ inputs.nuget-staging-mode == 'flat' }}
        shell: bash
        run: |
          # Find all test output directories and copy native library
          find bindings/csharp/${{ inputs.csharp-project-name }}.Tests/bin/Release -type d -name "net*" | while read dir; do
            cp ./native-lib/* "$dir/"
            echo "Copied native library to $dir"
          done

      - name: Stage native library at shipping runtime path (Unix)
        if: ${{ inputs.nuget-staging-mode == 'runtime-rid' && matrix.os != 'windows-latest' }}
        shell: bash
        run: |
          # NativeLibraryTests asserts the library is loaded from the
          # runtimes/<rid>/native/ shipping layout (not a flat copy).
          # Staging to that location in CI matches the local-dev behavior
          # of NativeTestSupport.EnsureNativeLibraryPrepared().
          find bindings/csharp/${{ inputs.csharp-project-name }}.Tests/bin/Release -type d -name "net*" | while read dir; do
            runtime_dir="$dir/runtimes/${{ matrix.runtime }}/native"
            mkdir -p "$runtime_dir"
            cp ./native-lib/* "$runtime_dir/"
            echo "Staged native library at $runtime_dir"
            ls -la "$runtime_dir"
          done

      - name: Stage native library at shipping runtime path (Windows)
        if: ${{ inputs.nuget-staging-mode == 'runtime-rid' && matrix.os == 'windows-latest' }}
        shell: pwsh
        run: |
          # Stage the native library under runtimes/<rid>/native/ so it does
          # not collide with the managed assembly at the flat bin root
          # (case-insensitive Windows filesystem).
          Get-ChildItem -Path "bindings/csharp/${{ inputs.csharp-project-name }}.Tests/bin/Release" -Directory -Recurse | Where-Object { $_.Name -like "net*" } | ForEach-Object {
            $runtimeDir = Join-Path $_.FullName "runtimes/${{ matrix.runtime }}/native"
            New-Item -ItemType Directory -Path $runtimeDir -Force | Out-Null
            $srcFile = Get-ChildItem "./native-lib/*" | Select-Object -First 1
            $destFile = Join-Path $runtimeDir "${{ inputs.package-name }}.dll"
            Copy-Item $srcFile.FullName -Destination $destFile -Force
            Write-Host "Staged native library at $destFile"
          }

      - name: Run tests
        working-directory: bindings/csharp
        run: dotnet test ${{ inputs.csharp-project-name }}.Tests/${{ inputs.csharp-project-name }}.Tests.csproj -c Release --no-build