name: CI
on:
  push:
    branches:
      - main
  pull_request:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}
  cancel-in-progress: true

# Setting a fixed target dir ensures that all builds of examples re-use the same common dependencies
env:
  CARGO_TARGET_DIR: ${{ github.workspace }}/target

jobs:
  ruff:
    runs-on: "ubuntu-latest"
    steps:
      - uses: actions/checkout@v6

      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: "3.x"

      - run: pip install nox

      - run: nox -s ruff

  mypy:
    runs-on: "ubuntu-latest"
    steps:
      - uses: actions/checkout@v6

      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: "3.x"

      - run: pip install nox

      - run: nox -s mypy

  pytest:
    runs-on: "ubuntu-latest"
    steps:
      - uses: actions/checkout@v6

      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: "3.x"

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

      - run: pip install nox

      - run: nox -s test

  build:
    name: ${{ matrix.python-version }} ${{ matrix.platform.os }}-${{ matrix.platform.python-architecture }}
    runs-on: ${{ matrix.platform.os }}
    strategy:
      # If one platform fails, allow the rest to keep testing if `CI-no-fail-fast` label is present
      fail-fast: ${{ !contains(github.event.pull_request.labels.*.name, 'CI-no-fail-fast') }}
      matrix:
        python-version:
          [
            "3.9",
            "3.10",
            "3.11",
            "3.14",
            "3.13",
            "3.13t",
            "3.14",
            "3.14t",
            "pypy-3.11",
            "graalpy25.0",
          ]
        platform:
          [
            {
              os: "macos-latest",
              python-architecture: "arm64",
              rust-target: "aarch64-apple-darwin",
            },
            {
              os: "ubuntu-latest",
              python-architecture: "x64",
              rust-target: "x86_64-unknown-linux-gnu",
            },
            {
              os: "windows-latest",
              python-architecture: "x64",
              rust-target: "x86_64-pc-windows-msvc",
            },
          ]
        include:
          # Just test one x86 Windows Python for simplicity
          - python-version: "3.x"
            platform:
              {
                os: "windows-latest",
                python-architecture: "x86",
                rust-target: "i686-pc-windows-msvc",
              }
          # Just test one arm64 Windows Python for simplicity
          - python-version: "3.x"
            platform:
              {
                os: "windows-11-arm",
                python-architecture: "arm64",
                rust-target: "aarch64-pc-windows-msvc",
              }
          # Just test one x64 macOS Python for simplicity
          - python-version: "3.x"
            platform:
              {
                os: "macos-15-intel",
                python-architecture: "x64",
                rust-target: "x86_64-apple-darwin",
              }
          # Just test one arm64 Ubuntu Python for simplicity
          - python-version: "3.x"
            platform:
              {
                os: "ubuntu-24.04-arm",
                python-architecture: "arm64",
                rust-target: "aarch64-unknown-linux-gnu",
              }
        exclude:
          # macOS arm doesn't have Python builds before 3.10
          - python-version: 3.9
            platform:
              {
                os: "macos-latest",
                python-architecture: "arm64",
                rust-target: "aarch64-apple-darwin",
              }
          # no graalpy available on Windows
          - python-version: graalpy25.0
            platform:
              {
                os: "windows-latest",
                python-architecture: "x64",
                rust-target: "x86_64-pc-windows-msvc",
              }

    env:
      SETUPTOOLS_RUST_CARGO_PROFILE: dev
    steps:
      - uses: actions/checkout@v6

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v6
        with:
          python-version: ${{ matrix.python-version }}
          architecture: ${{ matrix.platform.python-architecture }}
          allow-prereleases: true

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

      - name: Install test dependencies
        run: pip install --upgrade nox

      - name: Build package
        run: pip install -e .

      - name: Test examples
        shell: bash
        env:
          PYTHON: ${{ matrix.python-version }}
        run: nox -s test-examples

      - name: Test macOS universal2
        if: ${{ startsWith(matrix.platform.os, 'macos') && !startsWith(matrix.python-version, 'pypy') && !startsWith(matrix.python-version, 'graalpy') }}
        shell: bash
        env:
          ARCHFLAGS: -arch x86_64 -arch arm64
        run: |
          rustup target add aarch64-apple-darwin
          rustup target add x86_64-apple-darwin
          cd examples/namespace_package
          pip install build 'setuptools>=70.1'
          python -m build --no-isolation
          ls -l dist/
          pip install --force-reinstall dist/namespace_package*_universal2.whl
          cd -
          python -c "from namespace_package import rust; assert rust.rust_func() == 14"
          python -c "from namespace_package import python; assert python.python_func() == 15"

      - name: Test sdist vendor Rust crates
        shell: bash
        run: nox -s test-sdist-vendor
        env:
          CARGO_BUILD_TARGET: ${{ matrix.platform.rust-target }}

  test-abi3:
    runs-on: ${{ matrix.os }}
    strategy:
      # If one platform fails, allow the rest to keep testing if `CI-no-fail-fast` label is present
      fail-fast: ${{ !contains(github.event.pull_request.labels.*.name, 'CI-no-fail-fast') }}
      matrix:
        os:
          [
            ubuntu-latest,
            macos-latest,
            windows-latest,
            windows-11-arm,
            macos-15-intel,
            ubuntu-24.04-arm,
          ]
    steps:
      - uses: actions/checkout@v6
      - name: Setup python
        uses: actions/setup-python@v6
        with:
          python-version: "3.x"

      - uses: dtolnay/rust-toolchain@stable

      - name: Build package
        run: pip install -e .

      - name: Build an abi3 wheel
        shell: bash
        env:
          DIST_EXTRA_CONFIG: /tmp/build-opts.cfg
        run: |
          set -e
          cd examples/rust_with_cffi/
          python --version
          pip install -U build cffi 'setuptools>=70.1'
          echo -e "[bdist_wheel]\npy_limited_api=cp39" > $DIST_EXTRA_CONFIG
          python -m build --no-isolation
          ls -la dist/

      # Now we switch to a different Python version and ensure we can install
      # the wheel we just built.
      - name: Setup python
        uses: actions/setup-python@v6
        with:
          python-version: "3.11"

      - name: Install abi3 wheel and run tests
        shell: bash
        run: |
          set -e
          cd examples/
          python --version
          pip install rust_with_cffi/dist/rust_with_cffi*.whl
          python -c "from rust_with_cffi import rust; assert rust.rust_func() == 14"
          python -c "from rust_with_cffi.cffi import lib; assert lib.cffi_func() == 15"

      - name: Run abi3audit
        shell: bash
        run: |
          set -e
          pip install abi3audit
          cd examples/
          abi3audit rust_with_cffi/dist/rust_with_cffi*.whl

  test-crossenv:
    runs-on: ubuntu-latest
    strategy:
      # If one platform fails, allow the rest to keep testing if `CI-no-fail-fast` label is present
      fail-fast: ${{ !contains(github.event.pull_request.labels.*.name, 'CI-no-fail-fast') }}
      matrix:
        platform: [{ arch: "aarch64" }, { arch: "armv7" }]
    steps:
      - uses: actions/checkout@v6
      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: "3.x"
      - uses: docker/setup-qemu-action@v4
      - run: pip install nox
      - run: nox -s test-crossenv -- ${{ matrix.platform.arch }}

  test-cross:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - name: Setup python
        uses: actions/setup-python@v6
        with:
          python-version: "3.x"
      - uses: dtolnay/rust-toolchain@stable
      - name: Install cross
        # need cross HEAD for https://github.com/cross-rs/cross/issues/1645
        # will probably be ok to use released cross from 0.3
        run: cargo install cross --git https://github.com/cross-rs/cross
      - uses: docker/setup-qemu-action@v4
      - run: pip install nox
      - run: nox -s test-cross

  test-zigbuild:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - name: Setup python
        uses: actions/setup-python@v6
        with:
          python-version: "3.10" # must match the ubuntu version in the install, and also the version copied out of the docker image below
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: aarch64-unknown-linux-gnu
      - name: Install cargo-zigbuild
        run: |
          python3 -m pip install cargo-zigbuild
      - name: Build package
        run: pip install -e .
      - name: Build wheel using cargo-zigbuild
        shell: bash
        env:
          CARGO: cargo-zigbuild
          CARGO_BUILD_TARGET: aarch64-unknown-linux-gnu
          PYO3_CROSS_LIB_DIR: /opt/python/cp310-cp310/lib
          DIST_EXTRA_CONFIG: /tmp/build-opts.cfg
        run: |
          mkdir -p $PYO3_CROSS_LIB_DIR
          docker cp -L $(docker create --rm quay.io/pypa/manylinux2014_aarch64:latest):/opt/python/cp310-cp310 /opt/python
          cd examples/namespace_package
          python -m pip install build wheel
          echo -e "[bdist_wheel]\nplat_name=manylinux2014_aarch64" > $DIST_EXTRA_CONFIG
          python -m build --no-isolation
          ls -la dist/
          unzip -l dist/*.whl  # debug all files inside wheel file
      - uses: uraimo/run-on-arch-action@v3.1.0
        name: Install built wheel
        with:
          arch: aarch64
          distro: ubuntu22.04
          dockerRunArgs: |
            --volume "${PWD}/examples/namespace_package:/io"
          install: |
            apt-get update
            apt-get install -y --no-install-recommends python3 python3-venv python3-pip
          run: |
            python3 -m venv .venv
            source .venv/bin/activate
            pip install namespace_package --no-index --find-links /io/dist/ --force-reinstall
            python -c "from namespace_package import rust; assert rust.rust_func() == 14"
            python -c "from namespace_package import python; assert python.python_func() == 15"

  test-cibuildwheel:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: aarch64-apple-darwin,x86_64-apple-darwin
      - uses: pypa/cibuildwheel@v3.4.1
        env:
          CIBW_BUILD: cp39-*
          CIBW_BEFORE_BUILD: >
            pip install -U 'pip>=23.2.1' 'setuptools>=70.1.0' 'auditwheel>=5.4.0'
            && pip install -e .
            && pip list
          CIBW_ARCHS_MACOS: "x86_64 universal2 arm64"
          CIBW_BUILD_VERBOSITY: 3
          CIBW_BUILD_FRONTEND: "build; args: --no-isolation"
          MACOSX_DEPLOYMENT_TARGET: 10.12
        with:
          package-dir: examples/namespace_package

  test-mingw:
    runs-on: windows-latest
    name: ${{ matrix.python-version }} mingw-${{ matrix.arch }}
    strategy:
      fail-fast: false
      matrix:
        include:
          [
            {
              msystem: MINGW64,
              arch: x86_64,
              path: mingw64,
              rust_target: x86_64-pc-windows-gnu,
            },
            {
              msystem: MINGW32,
              arch: i686,
              path: mingw32,
              rust_target: i686-pc-windows-gnu,
            },
          ]
    steps:
      - uses: actions/checkout@v6
      - name: Install MSys2 and dependencies
        uses: msys2/setup-msys2@v2
        with:
          update: true
          msystem: ${{ matrix.msystem }}
          install: >-
            git
            mingw-w64-${{ matrix.arch }}-python
            mingw-w64-${{ matrix.arch }}-python-pip
            mingw-w64-${{ matrix.arch }}-openssl
            mingw-w64-${{ matrix.arch }}-toolchain

      - uses: dtolnay/rust-toolchain@master
        with:
          toolchain: stable-${{ matrix.rust_target }}

      - name: Install test dependencies
        shell: msys2 {0}
        run: python -m pip install --upgrade pip nox

      - name: Create libpython symlink
        shell: msys2 {0}
        run: |
          PYTHON_MINOR_VER=$(python -c "import sys; print(sys.version_info.minor)")
          ln -s /${{ matrix.path }}/lib/libpython3.${PYTHON_MINOR_VER}.dll.a /${{ matrix.path }}/lib/libpython3${PYTHON_MINOR_VER}.dll.a

      - name: Test examples
        shell: msys2 {0}
        run: PATH="$PATH:/c/Users/runneradmin/.cargo/bin" nox -s test-examples

  test-emscripten:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: astral-sh/setup-uv@v7
      - uses: actions/setup-node@v6
        with:
          node-version: 22
      - run: |
          uvx nox -s install-pyodide-emscripten
          echo "ORIG_PATH=$PATH" >> $GITHUB_ENV
      - uses: dtolnay/rust-toolchain@nightly
        with:
          components: rust-src
          targets: wasm32-unknown-emscripten
      - uses: mymindstorm/setup-emsdk@v16
        with:
          version: ${{env.EMSCRIPTEN_VERSION}}
          actions-cache-folder: emsdk-cache
      - uses: actions/cache@v5
        with:
          path: |
            tests/pyodide
          key: ${{ hashFiles('tests/*.js') }} - ${{ hashFiles('noxfile.py') }} - ${{ steps.setup-python.outputs.python-path }}
      - uses: Swatinem/rust-cache@v2
      - name: Test
        run: |
          export PATH=$ORIG_PATH:$PATH
          uvx nox -s test-examples-emscripten
        env:
          UV_PYTHON: ${{ env.PYTHON_VERSION }}
