name: CI

on:
  push:
    branches:
      - main
  pull_request:

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

env:
  CARGO_TERM_COLOR: always

jobs:
  fmt:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-python@v2
      - run: pip install nox
      - uses: actions-rs/toolchain@v1
        with:
          toolchain: stable
          profile: minimal
          components: rustfmt
      - name: Check python formatting (black)
        run: nox -s fmt-py
      - name: Check rust formatting (rustfmt)
        run: nox -s fmt-rust

  clippy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-python@v2
      - run: pip install nox
      - uses: actions-rs/toolchain@v1
        with:
          toolchain: stable
          profile: minimal
          components: clippy
      - run: nox -s clippy

  check-target:
    needs: [fmt]
    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:
        target: [powerpc64le-unknown-linux-gnu, s390x-unknown-linux-gnu, wasm32-wasi]
    name: check-${{ matrix.target }}
    steps:
      - uses: actions/checkout@v2
      - name: Install Rust toolchain
        uses: actions-rs/toolchain@v1
        with:
          toolchain: stable
          target: ${{ matrix.target }}
          profile: minimal
          default: true

      - name: Run cargo checks
        run: |
          set -x
          VERSIONS=("3.7" "3.8" "3.9" "3.10")
          for VERSION in ${VERSIONS[@]}; do
            echo "version=$VERSION" > config.txt
            echo "suppress_build_script_link_lines=true" >> config.txt
            PYO3_BUILD_CONFIG=$(pwd)/config.txt cargo check --all-targets --no-default-features
            PYO3_BUILD_CONFIG=$(pwd)/config.txt cargo check --all-targets --no-default-features --features "abi3"
            PYO3_BUILD_CONFIG=$(pwd)/config.txt cargo check --all-targets --features full
            PYO3_BUILD_CONFIG=$(pwd)/config.txt cargo check --all-targets --features "abi3 full"
          done

  build:
    needs: [fmt] # don't wait for clippy as fails rarely and takes longer
    name: python${{ matrix.python-version }}-${{ matrix.platform.python-architecture }} ${{ matrix.platform.os }} rust-${{ matrix.rust }}
    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:
        rust: [stable]
        python-version: [
          "3.7",
          "3.8",
          "3.9",
          "3.10",
          "3.11-dev",
          "pypy-3.7",
          "pypy-3.8",
          "pypy-3.9"
        ]
        platform:
          [
            {
              os: "macos-latest",
              python-architecture: "x64",
              rust-target: "x86_64-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:
          # Test minimal supported Rust version
          - rust: 1.48.0
            python-version: "3.10"
            platform:
              {
                os: "ubuntu-latest",
                python-architecture: "x64",
                rust-target: "x86_64-unknown-linux-gnu",
              }
            msrv: "MSRV"

          # Test the `nightly` feature
          - rust: nightly
            python-version: "3.10"
            platform:
              {
                os: "ubuntu-latest",
                python-architecture: "x64",
                rust-target: "x86_64-unknown-linux-gnu",
              }
            extra_features: "nightly"

          # Test 32-bit Windows only with the latest Python version
          - rust: stable
            python-version: "3.10"
            platform:
              {
                os: "windows-latest",
                python-architecture: "x86",
                rust-target: "i686-pc-windows-msvc",
              }
    steps:
      - uses: actions/checkout@v2

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

      - name: Install Rust toolchain
        uses: actions-rs/toolchain@v1
        with:
          toolchain: ${{ matrix.rust }}
          target: ${{ matrix.platform.rust-target }}
          profile: minimal
          default: true
          # needed to correctly format errors, see #1865
          components: rust-src

      - uses: Swatinem/rust-cache@v1
        with:
          key: cargo-${{ matrix.platform.python-architecture }}-${{ matrix.platform.os }}-${{ matrix.msrv }}
        continue-on-error: true

      - if: matrix.platform.os == 'ubuntu-latest'
        name: Prepare LD_LIBRARY_PATH (Ubuntu only)
        run: echo LD_LIBRARY_PATH=${pythonLocation}/lib >> $GITHUB_ENV

      - if: matrix.msrv == 'MSRV'
        name: Prepare minimal package versions (MSRV only)
        run: |
          set -x
          cargo update -p indexmap --precise 1.6.2
          cargo update -p hashbrown:0.12.0 --precise 0.9.1
          PROJECTS=("." "examples/decorator" "examples/maturin-starter" "examples/setuptools-rust-starter" "examples/word-count")
          for PROJ in ${PROJECTS[@]}; do
            cargo update --manifest-path "$PROJ/Cargo.toml" -p parking_lot --precise 0.11.0
          done

      - name: Build docs
        run: cargo doc --no-deps --no-default-features --features "full ${{ matrix.extra_features }}"

      - name: Build (no features)
        run: cargo build --lib --tests --no-default-features

      # --no-default-features when used with `cargo build/test -p` doesn't seem to work!
      - name: Build pyo3-build-config (no features)
        run: |
          cd pyo3-build-config
          cargo build --no-default-features

      # Run tests (except on PyPy, because no embedding API).
      - if: ${{ !startsWith(matrix.python-version, 'pypy') }}
        name: Test (no features)
        run: cargo test --no-default-features --lib --tests

      # --no-default-features when used with `cargo build/test -p` doesn't seem to work!
      - name: Test pyo3-build-config (no features)
        run: |
          cd pyo3-build-config
          cargo test --no-default-features

      - name: Build (all additive features)
        run: cargo build --lib --tests --no-default-features --features "full ${{ matrix.extra_features }}"

      - if: ${{ startsWith(matrix.python-version, 'pypy') }}
        name: Build PyPy (abi3-py37)
        run: cargo build --lib --tests --no-default-features --features "abi3-py37 full ${{ matrix.extra_features }}"

      # Run tests (except on PyPy, because no embedding API).
      - if: ${{ !startsWith(matrix.python-version, 'pypy') }}
        name: Test
        run: cargo test --no-default-features --features "full ${{ matrix.extra_features }}"

      # Run tests again, but in abi3 mode
      - if: ${{ !startsWith(matrix.python-version, 'pypy') }}
        name: Test (abi3)
        run: cargo test --no-default-features --features "abi3 full ${{ matrix.extra_features }}"

      # Run tests again, for abi3-py37 (the minimal Python version)
      - if: ${{ (!startsWith(matrix.python-version, 'pypy')) && (matrix.python-version != '3.7') }}
        name: Test (abi3-py37)
        run: cargo test --no-default-features --features "abi3-py37 full ${{ matrix.extra_features }}"

      - name: Test proc-macro code
        run: cargo test --manifest-path=pyo3-macros-backend/Cargo.toml

      - name: Test build config
        run: cargo test --manifest-path=pyo3-build-config/Cargo.toml

      - name: Test python examples and tests
        shell: bash
        run: |
          python -m pip install -U pip nox
          cargo xtask test-py
        env:
          CARGO_TARGET_DIR: ${{ github.workspace }}/target

      - name: Test cross compilation
        if: ${{ matrix.platform.os == 'ubuntu-latest' && matrix.python-version == '3.9' }}
        uses: messense/maturin-action@v1
        env:
          PYO3_CROSS_LIB_DIR: /opt/python/cp39-cp39/lib
        with:
          target: aarch64-unknown-linux-gnu
          manylinux: auto
          args: --release -i python3.9 --no-sdist -m examples/maturin-starter/Cargo.toml

      - run: sudo rm -rf examples/maturin-starter/target
        if: ${{ matrix.platform.os == 'ubuntu-latest' && matrix.python-version == '3.9' }}
      - name: Test cross compile to same architecture
        if: ${{ matrix.platform.os == 'ubuntu-latest' && matrix.python-version == '3.9' }}
        uses: messense/maturin-action@v1
        env:
          PYO3_CROSS_LIB_DIR: /opt/python/cp39-cp39/lib
        with:
          target: x86_64-unknown-linux-gnu
          manylinux: auto
          args: --release -i python3.9 --no-sdist -m examples/maturin-starter/Cargo.toml

      - name: Test cross compilation
        if: ${{ matrix.platform.os == 'macos-latest' && matrix.python-version == '3.9' }}
        uses: messense/maturin-action@v1
        with:
          target: aarch64-apple-darwin
          args: --release -i python3.9 --no-sdist -m examples/maturin-starter/Cargo.toml

    env:
      CARGO_TERM_VERBOSE: true
      CARGO_BUILD_TARGET: ${{ matrix.platform.rust-target }}
      RUST_BACKTRACE: 1
      RUSTFLAGS: "-D warnings"
      RUSTDOCFLAGS: "-D warnings"
      # TODO: this is a hack to workaround compile_error! warnings about auto-initialize on PyPy
      # Once cargo's `resolver = "2"` is stable (~ MSRV Rust 1.52), remove this.
      PYO3_CI: 1
      # This is a hack to make CARGO_PRIMARY_PACKAGE always set even for the
      # msrv job. MSRV is currently 1.48, but CARGO_PRIMARY_PACKAGE only came in
      # 1.49.
      CARGO_PRIMARY_PACKAGE: 1

  coverage:
    needs: [fmt]
    name: coverage-${{ matrix.os }}
    strategy:
      matrix:
        os: ["windows", "macos", "ubuntu"]
    runs-on: ${{ matrix.os }}-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-python@v2
        with:
          python-version: "3.10"
      - uses: Swatinem/rust-cache@v1
        with:
          key: coverage-cargo-${{ matrix.os }}
        continue-on-error: true
      - uses: actions-rs/toolchain@v1
        with:
          toolchain: nightly
          override: true
          profile: minimal
          components: llvm-tools-preview
      - name: Install cargo-llvm-cov
        uses: taiki-e/install-action@cargo-llvm-cov
      - run: python -m pip install -U pip nox
      - run: cargo xtask coverage --output-lcov coverage.lcov
      - uses: codecov/codecov-action@v2
        with:
          file: coverage.lcov
          name: ${{ matrix.os }}
