name: Build

on:
  workflow_dispatch:
  push:
    branches: [master]
    tags:
      - v*
  pull_request:
    branches: [master]

env:
  CARGO_TERM_COLOR: always

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: 3.11
      - uses: pre-commit/action@v3.0.1

  build:
    runs-on: ${{ matrix.os }}
    needs: [lint]
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
    steps:
      - uses: actions/checkout@v4
      - uses: Swatinem/rust-cache@v2
      - name: Install Dependencies
        run: sudo apt install libunwind-dev
        if: runner.os == 'Linux'
      - uses: actions/setup-python@v5
        with:
          python-version: 3.9
      - name: Build
        run: cargo build --release --verbose --examples
      - uses: actions/setup-python@v5
        with:
          python-version: 3.9
      - name: Install optional numpy dependency
        run: pip install numpy>=2
      - name: Test
        id: test
        continue-on-error: true
        run: cargo test --release
      - name: Test (retry#1)
        id: test1
        run: cargo test --release
        if: steps.test.outcome=='failure'
        continue-on-error: true
      - name: Test (retry#2)
        run: cargo test --release
        if: steps.test1.outcome=='failure'
      - name: Build Wheel
        run: |
          pip install --upgrade maturin
          maturin build --release -o dist --all-features
        if: runner.os == 'Windows'
      - name: Build Wheel - universal2
        env:
          DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
          SDKROOT: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk
          MACOSX_DEPLOYMENT_TARGET: 10.9
        run: |
          rustup target add aarch64-apple-darwin
          rustup target add x86_64-apple-darwin
          pip install --upgrade maturin
          maturin build --release -o dist
          maturin build --release -o dist --target universal2-apple-darwin
        if: matrix.os == 'macos-latest'
      - name: Rename Wheels
        run: |
          python3 -c "import shutil; import glob; wheels = glob.glob('dist/*.whl'); [shutil.move(wheel, wheel.replace('py3', 'py2.py3')) for wheel in wheels if 'py2' not in wheel]"
        if: runner.os != 'Linux'
      - name: Upload Windows Wheels
        uses: actions/upload-artifact@v4
        with:
          name: wheels-windows
          path: dist
        if: runner.os == 'Windows'
      - name: Upload Macos Wheels
        uses: actions/upload-artifact@v4
        with:
          name: wheels-macos
          path: dist
        if: runner.os == 'macOS'

  build-linux-cross:
    runs-on: ubuntu-latest
    needs: [lint]
    strategy:
      fail-fast: false
      matrix:
        target:
          [
            i686-unknown-linux-musl,
            armv7-unknown-linux-musleabihf,
            aarch64-unknown-linux-musl,
            x86_64-unknown-linux-musl,
          ]
    container:
      image: ghcr.io/benfred/rust-musl-cross:${{ matrix.target }}
      env:
        RUSTUP_HOME: /root/.rustup
        CARGO_HOME: /root/.cargo
    steps:
      - uses: actions/checkout@v4
      - uses: Swatinem/rust-cache@v2
      - name: Build
        run: |
          python3 -m pip install --upgrade maturin
          maturin build --release -o dist --target ${{ matrix.target }} --features unwind
          maturin sdist -o dist
        if: ${{ matrix.target == 'armv7-unknown-linux-musleabihf' || matrix.target == 'x86_64-unknown-linux-musl'}}
      - name: Build
        run: |
          python3 -m pip install --upgrade maturin
          maturin build --release -o dist --target ${{ matrix.target }}
          maturin sdist -o dist
        if: ${{ matrix.target == 'i686-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
      - name: Rename Wheels
        run: |
          python3 -c "import shutil; import glob; wheels = glob.glob('dist/*.whl'); [shutil.move(wheel, wheel.replace('py3', 'py2.py3')) for wheel in wheels if 'py2' not in wheel]"
      - name: Upload wheels
        uses: actions/upload-artifact@v4
        with:
          name: wheels-${{ matrix.target }}
          path: dist

  build-freebsd:
    runs-on: ubuntu-22.04
    needs: [lint]
    timeout-minutes: 30
    strategy:
      matrix:
        box:
          - freebsd-14
    steps:
      - uses: actions/checkout@v4
      - name: Display CPU info
        run: lscpu
      - name: Install VM tools
        run: |
          sudo apt-get update -qq
          sudo apt-get install -qq -o=Dpkg::Use-Pty=0 moreutils
          sudo chronic apt-get install -qq -o=Dpkg::Use-Pty=0 vagrant virtualbox qemu libvirt-daemon-system
      - name: Set up VM
        shell: sudo bash {0}
        run: |
          vagrant plugin install vagrant-libvirt
          vagrant plugin install vagrant-scp
          ln -sf ci/Vagrantfile Vagrantfile
          vagrant status
          vagrant up --no-tty --provider libvirt ${{ matrix.box }}
      - name: Build and test
        shell: sudo bash {0}
        run: vagrant ssh ${{ matrix.box }} -- bash /vagrant/ci/test_freebsd.sh
      - name: Retrieve build artifacts for caching purposes
        shell: sudo bash {0}
        run: |
          vagrant scp ${{ matrix.box }}:/vagrant/build-artifacts.tar build-artifacts.tar
          ls -ahl build-artifacts.tar
      - name: Prepare binary for upload
        run: |
          tar xf build-artifacts.tar target/release/py-spy
          mv target/release/py-spy py-spy-x86_64-unknown-freebsd
      - name: Upload Binaries
        uses: actions/upload-artifact@v4
        with:
          name: py-spy-x86_64-unknown-freebsd
          path: py-spy-x86_64-unknown-freebsd

  test-wheels:
    name: Test Wheels
    needs: [build, build-linux-cross]
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      # automatically generated by ci/update_python_test_versions.py
      matrix:
        python-version:
          [
            3.6.7,
            3.6.15,
            3.7.1,
            3.7.17,
            3.8.0,
            3.8.18,
            3.9.0,
            3.9.23,
            3.10.0,
            3.10.18,
            3.11.0,
            3.11.13,
            3.12.0,
            3.12.1,
            3.12.2,
            3.12.3,
            3.12.4,
            3.12.5,
            3.12.6,
            3.12.7,
            3.12.8,
            3.12.9,
            3.12.10,
            3.12.11,
            3.13.0,
            3.13.1,
            3.13.2,
            3.13.3,
            3.13.4,
            3.13.5,
          ]
        os: [ubuntu-22.04, macos-13, windows-latest, ubuntu-22.04-arm]
        # some versions of python can't be tested on GHA with osx because of SIP:
        exclude:
          - os: ubuntu-22.04
            python-version: 3.6.7
          - os: ubuntu-22.04-arm
            python-version: 3.6.7
          - os: windows-latest
            python-version: 3.6.15
          - os: ubuntu-22.04
            python-version: 3.6.15
          - os: ubuntu-22.04-arm
            python-version: 3.6.15
          - os: ubuntu-22.04
            python-version: 3.7.1
          - os: ubuntu-22.04-arm
            python-version: 3.7.1
          - os: windows-latest
            python-version: 3.7.17
          - os: ubuntu-22.04-arm
            python-version: 3.7.17
          - os: ubuntu-22.04
            python-version: 3.8.0
          - os: ubuntu-22.04-arm
            python-version: 3.8.0
          - os: windows-latest
            python-version: 3.8.18
          - os: ubuntu-22.04
            python-version: 3.9.0
          - os: ubuntu-22.04-arm
            python-version: 3.9.0
          - os: windows-latest
            python-version: 3.9.23
          - os: ubuntu-22.04
            python-version: 3.10.0
          - os: ubuntu-22.04-arm
            python-version: 3.10.0
          - os: windows-latest
            python-version: 3.10.18
          - os: macos-13
            python-version: 3.11.13
          - os: windows-latest
            python-version: 3.11.13
          - os: macos-13
            python-version: 3.12.0
          - os: macos-13
            python-version: 3.12.1
          - os: macos-13
            python-version: 3.12.2
          - os: macos-13
            python-version: 3.12.3
          - os: macos-13
            python-version: 3.12.4
          - os: macos-13
            python-version: 3.12.5
          - os: macos-13
            python-version: 3.12.6
          - os: macos-13
            python-version: 3.12.7
          - os: macos-13
            python-version: 3.12.8
          - os: macos-13
            python-version: 3.12.9
          - os: macos-13
            python-version: 3.12.10
          - os: macos-13
            python-version: 3.12.11
          - os: windows-latest
            python-version: 3.12.11

    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with:
          pattern: wheels-*
          merge-multiple: true
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - name: Install wheel
        run: |
          pip install --force-reinstall --no-index --find-links . py-spy
      - name: Test Wheel
        id: test
        run: python tests/integration_test.py
        if: runner.os != 'macOS'
        continue-on-error: true
      - name: Test Wheel (Retry#1)
        id: test1
        run: python tests/integration_test.py
        if: steps.test.outcome=='failure'
        continue-on-error: true
      - name: Test Wheel (Retry#2)
        id: test2
        run: python tests/integration_test.py
        if: steps.test1.outcome=='failure'
      - name: Test macOS Wheel
        id: osx_test
        run: sudo "PATH=$PATH" python tests/integration_test.py
        if: runner.os == 'macOS'
        continue-on-error: true
      - name: Test macOS Wheel (Retry#1)
        id: osx_test1
        run: sudo "PATH=$PATH" python tests/integration_test.py
        if: steps.osx_test.outcome=='failure'
        continue-on-error: true
      - name: Test macOS Wheel (Retry#2)
        id: osx_test2
        run: sudo "PATH=$PATH" python tests/integration_test.py
        if: steps.osx_test1.outcome=='failure'

  release:
    name: Release
    runs-on: ubuntu-latest
    if: "startsWith(github.ref, 'refs/tags/')"
    needs: [test-wheels]
    steps:
      - uses: actions/download-artifact@v4
        with:
          pattern: wheels-*
          merge-multiple: true
      - name: Create GitHub Release
        uses: fnkr/github-action-ghr@v1.3
        env:
          GHR_PATH: .
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      - name: Install Dependencies
        run: sudo apt install libunwind-dev
        if: runner.os == 'Linux'
      - uses: actions/setup-python@v5
        with:
          python-version: 3.9
      - name: Push to PyPi
        env:
          TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
          TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
        run: |
          pip install --upgrade wheel pip setuptools twine
          twine upload *
          rm *
      - uses: actions/checkout@v4
      - name: Push to crates.io
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: cargo publish
