copp 0.2.2

Convex-objective path parameterization for robotic trajectory planning.
Documentation
name: C ABI

on:
  push:
    branches:
      - "**"
    tags:
      - "v*"
  pull_request:
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: c-abi-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

env:
  CARGO_TERM_COLOR: always
  RUSTFLAGS: "-C target-cpu=generic"

jobs:
  build-package:
    name: ${{ matrix.platform }} / ${{ matrix.link }}
    runs-on: ${{ matrix.os }}

    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-24.04
            platform: linux-x86_64-gnu
            link: dynamic
            static_flag: OFF
          - os: ubuntu-24.04
            platform: linux-x86_64-gnu
            link: static
            static_flag: ON
          - os: windows-2022
            platform: windows-x86_64-msvc
            link: dynamic
            static_flag: OFF
          - os: windows-2022
            platform: windows-x86_64-msvc
            link: static
            static_flag: ON
          - os: macos-15
            platform: macos-aarch64
            link: dynamic
            static_flag: OFF
          - os: macos-15
            platform: macos-aarch64
            link: static
            static_flag: ON
          - os: macos-15-intel
            platform: macos-x86_64
            link: dynamic
            static_flag: OFF
          - os: macos-15-intel
            platform: macos-x86_64
            link: static
            static_flag: ON

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Prepare package metadata
        shell: pwsh
        run: |
          $refName = "${{ github.ref_name }}"
          if ("${{ github.event_name }}" -eq "pull_request") {
            $refName = "pr-${{ github.event.pull_request.number }}"
          }
          $safeRefName = $refName -replace '[^A-Za-z0-9._-]', '-'
          "COPP_PACKAGE_NAME=copp-c-$safeRefName-${{ matrix.platform }}-${{ matrix.link }}" >> $env:GITHUB_ENV

      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable

      - name: Install Linux system dependencies
        if: runner.os == 'Linux'
        run: |
          sudo apt-get update
          sudo apt-get install -y pkg-config libfontconfig1-dev

      - name: Cache Cargo
        uses: Swatinem/rust-cache@v2
        with:
          key: c-abi-${{ matrix.platform }}-${{ matrix.link }}

      - name: Install cbindgen
        run: cargo install cbindgen

      - name: Build Rust native library
        run: cargo build --release --lib --features c

      - name: Install nightly Rust for cbindgen expand
        run: rustup toolchain install nightly --profile minimal

      - name: Generate C headers
        shell: pwsh
        env:
          RUSTUP_TOOLCHAIN: nightly
        run: ./bindings/c/scripts/generate_headers.ps1

      - name: Check generated C headers are committed
        shell: pwsh
        run: |
          $changes = git status --porcelain -- bindings/c/include/copp
          if ($changes) {
            git diff -- bindings/c/include/copp
            $changes
            exit 1
          }

      - name: Configure C tests and examples
        shell: pwsh
        run: |
          cmake -S bindings/c -B bindings/c/build-c-abi `
            -DCMAKE_BUILD_TYPE=Release `
            -DCOPP_LINK_STATIC=${{ matrix.static_flag }} `
            -DCOPP_BUILD_TESTS=ON `
            -DCOPP_BUILD_EXAMPLES=ON `
            -DCOPP_TEST_EXAMPLES=ON `
            -DCOPP_BUILD_INSTALL_TESTS=ON

      - name: Build C tests and examples
        run: cmake --build bindings/c/build-c-abi --config Release --parallel

      - name: Run C tests, examples, and install smoke tests
        run: ctest --test-dir bindings/c/build-c-abi --output-on-failure -C Release

      - name: Install C SDK package
        shell: pwsh
        run: |
          $prefix = Join-Path "dist" $env:COPP_PACKAGE_NAME
          cmake --install bindings/c/build-c-abi --config Release --prefix $prefix

          Copy-Item README.md (Join-Path $prefix "README.md")
          Copy-Item CHANGELOG.md (Join-Path $prefix "CHANGELOG.md")
          Copy-Item bindings/c/README.md (Join-Path $prefix "C-README.md")
          Copy-Item bindings/c/examples (Join-Path $prefix "examples") -Recurse

          $license = Get-ChildItem -Path . -File |
            Where-Object { $_.Name -match '^(LICENSE|COPYING)(\..*)?$' } |
            Select-Object -First 1
          if ($license) {
            Copy-Item $license.FullName (Join-Path $prefix $license.Name)
          }

      - name: Archive C SDK package
        shell: pwsh
        run: |
          New-Item -ItemType Directory -Force release-artifacts | Out-Null
          $archive = Join-Path "release-artifacts" "$env:COPP_PACKAGE_NAME.zip"

          Push-Location dist
          cmake -E tar cfv "../$archive" --format=zip $env:COPP_PACKAGE_NAME
          Pop-Location

          $hash = Get-FileHash $archive -Algorithm SHA256
          "$($hash.Hash.ToLower())  $(Split-Path $archive -Leaf)" |
            Set-Content -Path "$archive.sha256" -NoNewline

      - name: Smoke test archived package
        shell: pwsh
        run: |
          $archive = Join-Path "release-artifacts" "$env:COPP_PACKAGE_NAME.zip"
          $extractRoot = Join-Path "release-smoke" "extract"
          $buildDir = Join-Path "release-smoke" "build"

          New-Item -ItemType Directory -Force $extractRoot | Out-Null
          Push-Location $extractRoot
          cmake -E tar xf "../../$archive"
          Pop-Location

          $prefix = Resolve-Path (Join-Path $extractRoot $env:COPP_PACKAGE_NAME)
          cmake -S bindings/c/tests/package_consumer -B $buildDir `
            -DCMAKE_PREFIX_PATH="$prefix" `
            -DCMAKE_BUILD_TYPE=Release
          cmake --build $buildDir --config Release --parallel
          ctest --test-dir $buildDir --output-on-failure -C Release

      - name: Upload release package artifact
        uses: actions/upload-artifact@v4
        with:
          name: ${{ env.COPP_PACKAGE_NAME }}
          path: |
            release-artifacts/${{ env.COPP_PACKAGE_NAME }}.zip
            release-artifacts/${{ env.COPP_PACKAGE_NAME }}.zip.sha256
          if-no-files-found: error

  publish-draft:
    name: Create Draft GitHub Release
    needs: build-package
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-24.04
    permissions:
      contents: write

    steps:
      - name: Download release package artifacts
        uses: actions/download-artifact@v4
        with:
          path: release-assets

      - name: Create or update draft GitHub Release
        shell: pwsh
        env:
          GH_TOKEN: ${{ github.token }}
          GH_REPO: ${{ github.repository }}
          RELEASE_TAG: ${{ github.ref_name }}
        run: |
          $assetPaths = @(
            Get-ChildItem release-assets -Recurse -File |
              Where-Object { $_.Name -like "*.zip" -or $_.Name -like "*.zip.sha256" } |
              Sort-Object Name |
              ForEach-Object { $_.FullName }
          )
          if ($assetPaths.Count -eq 0) {
            throw "No release assets were downloaded."
          }

          $notesFile = "release-notes.md"
          @'
          Prebuilt C ABI SDK packages are attached below.

          Each package contains the generated C headers, the Release native library, examples, and the CMake package files needed for `find_package(copp CONFIG REQUIRED)`.

          See CHANGELOG.md in this tag for API changes and breaking changes.
          '@ | Set-Content -Path $notesFile -NoNewline

          gh release view $env:RELEASE_TAG *> $null
          if ($LASTEXITCODE -eq 0) {
            & gh release upload $env:RELEASE_TAG @assetPaths --clobber
            & gh release edit $env:RELEASE_TAG `
              --draft `
              --title $env:RELEASE_TAG `
              --notes-file $notesFile
          } else {
            & gh release create $env:RELEASE_TAG @assetPaths `
              --draft `
              --verify-tag `
              --title $env:RELEASE_TAG `
              --notes-file $notesFile
          }