name: Python wheels

on:
  push:
    branches:
      - main
  release:
    types: [published]
  pull_request:
    branches:
      - "**"

concurrency:
  group: ${{ github.event_name == 'pull_request' && format('{0}-{1}', github.workflow_ref, github.event.pull_request.number) || github.run_id }}
  cancel-in-progress: true

permissions:
  id-token: write
  contents: read

jobs:
  build-wheels:
    name: Wheel - ${{ matrix.os.name }} ${{ matrix.python.major-dot-minor }} ${{ matrix.arch.name }}
    runs-on: ${{ matrix.os.runs-on[matrix.arch.matrix] }}
    strategy:
      fail-fast: false
      matrix:
        os:
          - name: macOS
            matrix: macos
            runs-on:
              arm: [macos-14]
              intel: [macos-15-intel]
          - name: Ubuntu
            matrix: ubuntu
            runs-on:
              arm: [Linux, ARM64]
              intel: [ubuntu-latest]
          - name: Windows
            matrix: windows
            runs-on:
              intel: [windows-latest]
        python:
          - major-dot-minor: "3.10"
            cibw-build: "cp310-*"
            by-arch:
              arm:
                manylinux-version: 2_28
                docker-url: ghcr.io/chia-network/build-images/centos-pypa-rust-aarch64
                rustup-target: aarch64-unknown-linux-musl
              intel:
                manylinux-version: 2_28
                docker-url: ghcr.io/chia-network/build-images/centos-pypa-rust-x86_64
                rustup-target: x86_64-unknown-linux-musl
            matrix: "3.10"
          - major-dot-minor: "3.11"
            cibw-build: "cp311-*"
            by-arch:
              arm:
                manylinux-version: 2_28
                docker-url: ghcr.io/chia-network/build-images/centos-pypa-rust-aarch64
                rustup-target: aarch64-unknown-linux-musl
              intel:
                manylinux-version: 2_28
                docker-url: ghcr.io/chia-network/build-images/centos-pypa-rust-x86_64
                rustup-target: x86_64-unknown-linux-musl
            matrix: "3.11"
          - major-dot-minor: "3.12"
            cibw-build: "cp312-*"
            by-arch:
              arm:
                manylinux-version: 2_28
                docker-url: ghcr.io/chia-network/build-images/centos-pypa-rust-aarch64
                rustup-target: aarch64-unknown-linux-musl
              intel:
                manylinux-version: 2_28
                docker-url: ghcr.io/chia-network/build-images/centos-pypa-rust-x86_64
                rustup-target: x86_64-unknown-linux-musl
            matrix: "3.12"
          - major-dot-minor: "3.13"
            cibw-build: "cp313-*"
            by-arch:
              arm:
                manylinux-version: 2_28
                docker-url: ghcr.io/chia-network/build-images/centos-pypa-rust-aarch64
                rustup-target: aarch64-unknown-linux-musl
              intel:
                manylinux-version: 2_28
                docker-url: ghcr.io/chia-network/build-images/centos-pypa-rust-x86_64
                rustup-target: x86_64-unknown-linux-musl
            matrix: "3.13"
          - major-dot-minor: "3.14"
            cibw-build: "cp314-*"
            by-arch:
              arm:
                manylinux-version: 2_28
                docker-url: ghcr.io/chia-network/build-images/centos-pypa-rust-aarch64
                rustup-target: aarch64-unknown-linux-musl
              intel:
                manylinux-version: 2_28
                docker-url: ghcr.io/chia-network/build-images/centos-pypa-rust-x86_64
                rustup-target: x86_64-unknown-linux-musl
            matrix: "3.14"

        arch:
          - name: ARM
            matrix: arm
          - name: Intel
            matrix: intel
        exclude:
          # Only partial entries are required here by GitHub Actions so generally I
          # only specify the `matrix:` entry.  The super linter complains so for now
          # all entries are included to avoid that.  Reported at
          # https://github.com/github/super-linter/issues/3016
          - os:
              name: Windows
              matrix: windows
              runs-on:
                intel: [windows-latest]
            arch:
              name: ARM
              matrix: arm

    steps:
      - name: Clean workspace
        uses: Chia-Network/actions/clean-workspace@main

      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: Chia-Network/actions/setup-python@main
        with:
          python-version: ${{ matrix.python.major-dot-minor }}

      - name: Set up rust
        uses: dtolnay/rust-toolchain@stable

      - uses: chia-network/actions/create-venv@main
        # these conditionals excluding ubuntu are related to the use of `docker run` to handle ubuntu at this point
        if: matrix.os.matrix != 'ubuntu'
        id: create-venv

      - uses: chia-network/actions/activate-venv@main
        if: matrix.os.matrix != 'ubuntu'
        with:
          directories: ${{ steps.create-venv.outputs.activate-venv-directories }}

      - name: Install dependencies
        if: matrix.os.matrix != 'ubuntu'
        run: |
          python -m pip install --upgrade pip
          python -m pip install maturin

      - name: Build MacOs with maturin on Python ${{ matrix.python.major-dot-minor }}
        if: matrix.os.matrix == 'macos'
        env:
          MACOSX_DEPLOYMENT_TARGET: "13.0"
        run: |
          maturin build -i python --release -m wheel/Cargo.toml --features=openssl,pyo3/extension-module

      - name: Build Linux with maturin on Python ${{ matrix.python.major-dot-minor }}
        if: matrix.os.matrix == 'ubuntu'
        run: |
          docker run --rm --pull always \
            -v ${{ github.workspace }}:/ws --workdir=/ws \
            ${{ matrix.python.by-arch[matrix.arch.matrix].docker-url }} \
            bash -exc '\
              yum -y install openssl-devel && \
              yum -y install perl-IPC-Cmd && \
              source $HOME/.cargo/env && \
              rustup target add ${{ matrix.python.by-arch[matrix.arch.matrix].rustup-target }} && \
              python${{ matrix.python.major-dot-minor }} -m venv /venv && \
              . /venv/bin/activate && \
              pip install --upgrade pip && \
              pip install maturin && \
              CC=gcc maturin build --release --manylinux ${{ matrix.python.by-arch[matrix.arch.matrix].manylinux-version }} -m wheel/Cargo.toml --features=openssl,pyo3/extension-module \
            '

      - name: Build Windows with maturin on Python ${{ matrix.python.major-dot-minor }}
        if: matrix.os.matrix == 'windows'
        env:
          CC: "clang"
          CFLAGS: "-D__BLST_PORTABLE__"
        run: |
          maturin build -i python --release -m wheel/Cargo.toml

      - uses: Chia-Network/actions/create-venv@main
        id: create-test-venv

      - uses: Chia-Network/actions/activate-venv@main
        with:
          directories: ${{ steps.create-test-venv.outputs.activate-venv-directories }}

      - name: Install chia_rs wheel
        shell: bash
        run: |
          TMP_DEST=$(mktemp -d)
          pip download --no-index --no-deps --only-binary :all: --find-links target/wheels/ --dest "${TMP_DEST}" chia_rs
          echo ====
          ls -l "${TMP_DEST}"
          echo ====
          pip install "${TMP_DEST}"/*
          rm -rf "${TMP_DEST}"

      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: packages-${{ matrix.os.name }}-${{ matrix.python.major-dot-minor }}-${{ matrix.arch.name }}
          path: ./target/wheels/

  check-typestubs:
    name: Check chia_rs.pyi
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: Chia-Network/actions/setup-python@main

      - name: check generated chia_rs.pyi
        run: |
          python wheel/generate_type_stubs.py
          git diff --exit-code

  build-sdist:
    name: sdist - ${{ matrix.os.name }} ${{ matrix.python.major-dot-minor }} ${{ matrix.arch.name }}
    runs-on: ${{ matrix.os.runs-on[matrix.arch.matrix] }}
    strategy:
      fail-fast: false
      matrix:
        os:
          - name: Ubuntu
            matrix: ubuntu
            runs-on:
              arm: [Linux, ARM64]
              intel: [ubuntu-latest]
        python:
          - major-dot-minor: "3.12"
            matrix: "3.12"
        arch:
          - name: Intel
            matrix: intel

    steps:
      - name: Clean workspace
        uses: Chia-Network/actions/clean-workspace@main

      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: Chia-Network/actions/setup-python@main
        with:
          python-version: ${{ matrix.python.major-dot-minor }}

      - name: Build source distribution
        run: |
          pip install maturin
          maturin sdist -m wheel/Cargo.toml
          cd target/wheels
          dirname=$(basename chia_rs*.tar.gz .tar.gz)
          echo "$dirname"
          mkdir "$dirname"
          cp -r ../../src/ "$dirname/src/"
          gunzip chia_rs*.tar.gz
          tar rvf chia_rs*.tar "$dirname/src"
          gzip chia_rs*.tar
          rm -rf "$dirname"

      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: packages-sdist-${{ matrix.os.name }}-${{ matrix.python.major-dot-minor }}-${{ matrix.arch.name }}
          path: ./target/wheels/

  fmt:
    runs-on: ubuntu-latest
    name: cargo fmt
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 1

      - name: Install rust
        uses: dtolnay/rust-toolchain@1.92.0
        with:
          components: rustfmt, clippy

      - name: fmt
        run: |
          cargo fmt --all -- --files-with-diff --check
          cd wheel
          cargo fmt -- --files-with-diff --check

  clippy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@1.92.0
        with:
          components: clippy

      - name: workspace
        run: |
          cargo clippy --workspace --all-features --all-targets

  actionlint:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v4

      - name: actionlint
        run: |
          bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
          echo ==== shellcheck version
          shellcheck --version
          echo ==== actionlint version
          ./actionlint --version
          echo ==== running actionlint
          ./actionlint -color -shellcheck shellcheck

  unit_tests:
    name: Unit Tests ${{ join(matrix.*.name, ' ') }} ${{ matrix.os.coverage && 'cov' || '' }}
    runs-on: ${{ matrix.os.runs-on }}
    strategy:
      fail-fast: false
      matrix:
        os:
          - name: macOS
            runs-on: macos-latest
            matrix: macos
            coverage: false
            cargo-test-options: --all-features
          - name: Linux
            runs-on: ubuntu-latest
            matrix: linux
            coverage: true
            cargo-test-options: --all-features
          - name: Windows
            runs-on: windows-latest
            matrix: windows
            coverage: false
            cargo-test-options: ""
        mode:
          - name: dev
            matrix: dev
            cargo-test-options: ""
            test-binary-options: ""
          - name: release
            matrix: release
            cargo-test-options: --release
            test-binary-options: --include-ignored
        test-runner:
          - name: Cargo
            matrix: cargo
          - name: pytest
            matrix: pytest
        exclude:
          - os:
              matrix: macos
            test-runner:
              matrix: pytest
          - os:
              matrix: windows
            test-runner:
              matrix: pytest
          - mode:
              matrix: dev
            test-runner:
              matrix: pytest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 1
      - uses: dtolnay/rust-toolchain@stable
      - uses: Chia-Network/actions/setup-python@main
        with:
          python-version: "3.12"
      - name: Prepare for coverage
        if: matrix.os.coverage
        run: |
          cargo install grcov --locked
          {
            echo "RUSTFLAGS=-Cinstrument-coverage"
            echo "LLVM_PROFILE_FILE=$(pwd)/target/chia_rs-%p-%m.profraw"
            echo "CARGO_TARGET_DIR=$(pwd)/target"
          } >> "$GITHUB_ENV"
      - name: cargo test (${{ matrix.mode.name }})
        if: matrix.test-runner.matrix == 'cargo'
        run: cargo test --workspace ${{ matrix.os.cargo-test-options }} ${{ matrix.mode.cargo-test-options }} -- ${{ matrix.mode.test-binary-options }}
      - name: python test with coverage
        if: matrix.os.coverage && matrix.test-runner.matrix == 'pytest'
        run: |
          python -m venv venv
          . venv/bin/activate
          pip install colorama maturin pytest pytest-xdist clvm
          maturin develop --release -m wheel/Cargo.toml --features=clvmr/allocator-debug
          # run pytest in a single process to avoid a race writing the coverage report
          pytest -n 0 tests
      - name: prep coverage
        if: matrix.os.coverage
        run: |
          sudo apt-get update
          sudo apt-get install lcov -y
          rustup component add llvm-tools-preview
          grcov . --binary-path target -s . --branch --ignore-not-existing --ignore='*/.cargo/*' --ignore='tests/*' --ignore='venv/*' -o rust_cov.info
          python -c 'with open("rust_cov.info") as f: lines = [l for l in f if not (l.startswith("DA:") and int(l.split(",")[1].strip()) >= 2**63)]; open("lcov.info", "w").writelines(lines)'
      - name: Upload to Coveralls
        uses: coverallsapp/github-action@v2
        if: matrix.os.coverage
        continue-on-error: true
        env:
          COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
        with:
          flag-name: ${{ join(matrix.*.name, ' ') }}
          parallel: true
          path-to-lcov: "./lcov.info"
          fail-on-error: false

  unit_tests_done:
    name: Unit Tests - Done
    runs-on: ubuntu-latest
    needs: unit_tests
    steps:
      - name: Upload to Coveralls
        uses: coverallsapp/github-action@v2
        continue-on-error: true
        env:
          COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
        with:
          parallel-finished: true
          fail-on-error: false

  upload:
    name: Upload to PyPI - ${{ matrix.os.name }} ${{ matrix.python.major-dot-minor }} ${{ matrix.arch.name }}
    runs-on: ${{ matrix.os.runs-on[matrix.arch.matrix] }}
    needs:
      - build-wheels
      - build-sdist
      - fmt
      - clippy
      - unit_tests
    strategy:
      fail-fast: false
      matrix:
        os:
          - name: Ubuntu
            matrix: ubuntu
            runs-on:
              arm: [Linux, ARM64]
              intel: [ubuntu-latest]
        python:
          - major-dot-minor: "3.12"
            matrix: "3.12"
        arch:
          - name: Intel
            matrix: intel

    steps:
      - name: Clean workspace
        uses: Chia-Network/actions/clean-workspace@main

      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: Chia-Network/actions/setup-python@main
        with:
          python-version: ${{ matrix.python.major-dot-minor }}

      - name: Download artifacts
        uses: actions/download-artifact@v4
        with:
          merge-multiple: true
          pattern: packages-*
          path: ./dist

      - name: Set Env
        uses: Chia-Network/actions/setjobenv@main
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: publish (PyPi)
        if: env.RELEASE == 'true'
        uses: pypa/gh-action-pypi-publish@release/v1
        with:
          packages-dir: dist/
          skip-existing: true

      - name: publish (Test PyPi)
        if: env.PRE_RELEASE == 'true'
        uses: pypa/gh-action-pypi-publish@release/v1
        with:
          repository-url: https://test.pypi.org/legacy/
          packages-dir: dist/
          skip-existing: true
