# Workflow to build wheels for upload to PyPI.
# Inspired by numpy's cibuildwheel config https://github.com/numpy/numpy/blob/main/.github/workflows/wheels.yml
#
# In an attempt to save CI resources, wheel builds do
# not run on each push but only weekly and for releases.
# Wheel builds can be triggered from the Actions page
# (if you have the permissions) on a commit to main.
#
# Alternatively, you can add labels to the pull request in order to trigger wheel
# builds.
# The label(s) that trigger builds are:
# - Build
name: Wheel builder

on:
  release:
    types: [published]
  schedule:
  # 3:27 UTC every day
  - cron: "27 3 * * *"
  push:
  pull_request:
    types: [labeled, opened, synchronize, reopened]
    paths-ignore:
      - "doc/**"
      - "web/**"
  workflow_dispatch:

concurrency:
  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
  cancel-in-progress: true

permissions: {}

jobs:
  build_sdist:
    name: Build sdist
    permissions:
      contents: read
    if: >-
      (github.event_name == 'schedule') ||
      github.event_name == 'workflow_dispatch' ||
      github.event_name == 'release' ||
      (github.event_name == 'pull_request' &&
      contains(github.event.pull_request.labels.*.name, 'Build')) ||
      (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && ( ! endsWith(github.ref, 'dev0')))
    runs-on: ubuntu-24.04
    outputs:
      sdist_file: ${{ steps.save-path.outputs.sdist_name }}
    steps:
      - name: Checkout pandas
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
        with:
          fetch-depth: 0

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

      - name: Build sdist
        run: |
          python -m pip install build
          python -m build --sdist

      - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
        with:
          name: sdist
          path: ./dist/*

      - name: Sanity check sdist files
        run: |
          ls ./dist

      - name: Output sdist name
        id: save-path
        shell: bash -el {0}
        run: echo "sdist_name=$(ls ./dist)" >> "$GITHUB_OUTPUT"

  build_wheels:
    needs: build_sdist
    permissions:
      contents: read
    name: Build wheel for ${{ matrix.python[0] }}-${{ matrix.buildplat[1] }}
    if: >-
      (github.event_name == 'schedule') ||
      github.event_name == 'workflow_dispatch' ||
      github.event_name == 'release' ||
      (github.event_name == 'pull_request' &&
      contains(github.event.pull_request.labels.*.name, 'Build')) ||
      (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && ( ! endsWith(github.ref, 'dev0')))
    runs-on: ${{ matrix.buildplat[0] }}
    strategy:
      fail-fast: false
      matrix:
        # GitHub Actions doesn't support pairing matrix values together, let's improvise
        # https://github.com/github/feedback/discussions/7835#discussioncomment-1769026
        buildplat:
        - [ubuntu-24.04, manylinux_x86_64]
        - [ubuntu-24.04, musllinux_x86_64]
        - [ubuntu-24.04-arm, manylinux_aarch64]
        - [ubuntu-24.04-arm, musllinux_aarch64]
        - [macos-15-intel, macosx_x86_64]
        - [macos-15, macosx_arm64]
        - [windows-2025, win_amd64]
        - [windows-11-arm, win_arm64]
        python: [["cp311", "3.11"], ["cp312", "3.12"], ["cp313", "3.13"], ["cp314", "3.14"], ["cp314t", "3.14"]]
        include:
        # Build Pyodide wheels and upload them to Anaconda.org
        # NOTE: this job is similar to the one in unit-tests.yml except for the fact
        # that it uses cibuildwheel instead of a standard Pyodide xbuildenv setup.
        - buildplat: [ubuntu-24.04, pyodide_wasm32]
          python: ["cp312", "3.12"]
          cibw_build_frontend: 'build'

    concurrency:
      # https://github.community/t/concurrecy-not-work-for-push/183068/7
      group: ${{ github.event_name == 'push' && github.run_number || github.ref }}-${{ matrix.buildplat[0] }}-${{ matrix.buildplat[1] }}-${{ matrix.python[0]}}-build-wheels
      cancel-in-progress: true

    steps:
      - name: Checkout pandas
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
        with:
          fetch-depth: 0

      - name: Set up MSVC environment for ARM64
        if: matrix.buildplat[1] == 'win_arm64'
        uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1
        with:
          arch: arm64

      # TODO: Build wheels from sdist again
      # There's some sort of weird race condition?
      # within Github that makes the sdist be missing files

      # We need to build wheels from the sdist since the sdist
      # removes unnecessary files from the release
      - name: Download sdist (not macOS)
        #if: ${{ matrix.buildplat[1] != 'macosx_*' }}
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
        with:
          name: sdist
          path: ./dist

      - name: Output sdist name (macOS)
        id: save-path
        shell: bash -el {0}
        run: echo "sdist_name=$(ls ./dist)" >> "$GITHUB_ENV"

      # Python version used to build sdist doesn't matter
      # wheel will be built from sdist with the correct version
      - name: Unzip sdist (macOS)
        if: ${{ startsWith(matrix.buildplat[1], 'macosx') }}
        run: |
          tar -xzf ./dist/${{ env.sdist_name }} -C ./dist

      - name: Output sdist name (macOS)
        id: save-path2
        shell: bash -el {0}
        run: echo "sdist_name=$(cd ./dist && ls -d */)" >> "$GITHUB_ENV"

      - name: Build wheels
        uses: pypa/cibuildwheel@294735312765b09d24a2fbec22660ce817587d55 # v4.1.0
        with:
         package-dir: ./dist/${{ startsWith(matrix.buildplat[1], 'macosx') && env.sdist_name || needs.build_sdist.outputs.sdist_file }}
        env:
          CIBW_BUILD: ${{ matrix.python[0] }}-${{ matrix.buildplat[1] }}
          CIBW_BUILD_FRONTEND: ${{ matrix.cibw_build_frontend || 'pip' }}
          CIBW_PLATFORM: ${{ (matrix.buildplat[1] == 'pyodide_wasm32' && 'pyodide') || (matrix.buildplat[1] == 'win_arm64' && 'windows') || 'auto' }}
          CIBW_ARCHS: ${{ matrix.buildplat[1] == 'win_arm64' && 'ARM64' || 'auto' }}
          CIBW_BEFORE_BUILD_WINDOWS: 'python -m pip install delvewheel'

      - name: Set up Python for validation/upload (non-ARM64 Windows & other OS)
        # micromamba is not available for ARM64 Windows
        if: matrix.buildplat[1] != 'win_arm64'
        uses: mamba-org/setup-micromamba@add3a49764cedee8ee24e82dfde87f5bc2914462 # v2
        with:
          environment-name: wheel-env
          # Use a fixed Python, since we might have an unreleased Python not
          # yet present on conda-forge
          create-args: >-
            python=3.11
            wheel
          cache-downloads: true
          cache-environment: true

      - name: Install wheel for win_arm64
        # installing wheel here because micromamba step was skipped
        if: matrix.buildplat[1] == 'win_arm64'
        shell: bash -el {0}
        # cryptography pin due to https://github.com/pyca/cryptography/pull/14216
        run: python -m pip install wheel cryptography==46.0.3

      - name: Validate wheel RECORD
        shell: bash -el {0}
        run: for whl in $(ls wheelhouse); do wheel unpack wheelhouse/$whl -d /tmp; done

      - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
        with:
          name: ${{ matrix.python[0] }}-${{ matrix.buildplat[1] }}
          path: ./wheelhouse/*.whl

  upload_nightly:
    # trigger an upload to https://anaconda.org/scientific-python-nightly-wheels/pandas
    # for cron jobs or "Run workflow" (restricted to main branch).
    name: Upload nightly packages to Anaconda
    permissions:
      contents: read
    if: >
      github.repository == 'pandas-dev/pandas' &&
      (github.event_name == 'schedule' ||
      (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main'))
    needs:
      - build_sdist
      - build_wheels
    runs-on: ubuntu-latest

    steps:
      - name: Download all artefacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
        with:
          path: dist
          merge-multiple: true

      - name: Upload wheels & sdist
        uses: scientific-python/upload-nightly-action@5748273c71e2d8d3a61f3a11a16421c8954f9ecf # 0.6.3
        with:
          artifacts_path: dist
          anaconda_nightly_upload_token: ${{secrets.PANDAS_NIGHTLY_UPLOAD_TOKEN}}

  publish:
    if: >
      github.repository == 'pandas-dev/pandas' &&
      github.event_name == 'release' &&
      startsWith(github.ref, 'refs/tags/v')

    needs:
      - build_sdist
      - build_wheels

    runs-on: ubuntu-24.04

    environment:
      name: pypi
    permissions:
      id-token: write         # OIDC for Trusted Publishing
      contents: read

    steps:
      - name: Download all artefacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
        with:
          path: dist          # everything lands in ./dist/**

      # TODO: This step can be probably be achieved by actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
      # by specifying merge-multiple: true, and a glob pattern
      - name: Collect files
        run: |
          mkdir -p upload
          # skip any wheel that contains 'pyodide'
          find dist -name '*pyodide*.whl' -prune -o \
                    -name '*.whl'   -exec mv {} upload/ \;
          find dist -name '*.tar.gz' -exec mv {} upload/ \;

      - name: Publish to **PyPI** (Trusted Publishing)
        uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
        with:
          packages-dir: upload
          skip-existing: true
