name: CI

on:
  push:
    # Build on tags that look like releases
    tags:
      - v*
    # Build when main, testing, or the maintenance branch is pushed to
    branches:
      - main
      - testing
      # This workflow does not publish artifacts, so we can ignore the cache
      # poisoning finding.
      - "3.1" # zizmor: ignore[cache-poisoning]
  pull_request:
    # Build when a pull request targets main or the maintenance branch
    branches:
      - main
      - "3.1"

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  matlab-test-ubuntu:
    name: MATLAB on ubuntu-latest with Python 3.10
    runs-on: ubuntu-latest
    env:
      CANTERA_ROOT: /home/runner/work/cantera/cantera
      CANTERA_DATA: /home/runner/work/cantera/cantera/data
    steps:
      - uses: actions/checkout@v4
        name: Checkout the repository
        with:
          submodules: recursive
          persist-credentials: false
      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.10'
          architecture: x64
      - name: Install Apt dependencies
        run: |
          sudo apt update
          sudo apt install libboost-dev gfortran libopenmpi-dev libopenblas-openmp-dev \
            libhdf5-dev
          gcc --version
      - name: Upgrade pip
        run: python3 -m pip install -U pip setuptools wheel
      - name: Install Python dependencies
        run: |
          python3 -m pip install ruamel.yaml scons==4.0.1 packaging
      - name: Build Cantera with legacy CLib
        run: |
          python3 `which scons` build env_vars=all -j4 debug=n --debug=time \
          cc_flags=-D_GLIBCXX_ASSERTIONS clib_legacy=y
      - name: Set LD_PRELOAD environment variable for MATLAB
        run: |
          LIB_STDCXX=$(ldconfig -p | grep libstdc++.so.6 | awk '{print $4}' | head -n 1)
          LIB_OPENBLAS=$(ldconfig -p | grep libopenblas.so.0 | awk '{print $4}' | head -n 1)
          LIB_LAPACK=$(ldconfig -p | grep liblapack.so.3 | awk '{print $4}' | head -n 1)
          echo "LD_PRELOAD=$LIB_STDCXX:$LIB_OPENBLAS:$LIB_LAPACK" >> $GITHUB_ENV
      - name: Set up MATLAB
        uses: matlab-actions/setup-matlab@718d4320188c73c86eb94ce76b553cbf89778487 # v2.5.0
      - name: Run tests
        uses: matlab-actions/run-tests@f9fc3d8ca29fadef6227fa52884b144b9011fa2f # v2.1.1
        with:
          select-by-folder: /home/runner/work/cantera/cantera/test/matlab_experimental

  ubuntu-multiple-pythons:
    name: ${{ matrix.os }} with Python ${{ matrix.python-version }}, Numpy ${{ matrix.numpy || 'latest' }}, Cython ${{ matrix.cython || 'latest' }}
    runs-on: ${{ matrix.os }}
    timeout-minutes: 60
    strategy:
      matrix:
        python-version: ['3.10', '3.11', '3.12', '3.13']
        os: ['ubuntu-24.04']
        numpy: ['']
        cython: ['!=3.1.2'] # Specifier can be dropped after 3.1.3 or 3.2.0 is released
        include:
          # Keep some test cases with NumPy 1.x until we drop support
        - python-version: '3.12'
          os: 'ubuntu-22.04'
          numpy: "'<2.0'"
          cython: '!=3.1.2' # Specifier can be dropped after 3.1.3 or 3.2.0 is released
          # Keep some test cases with older Cython versions
        - python-version: '3.10'
          os: 'ubuntu-22.04'
          cython: "==0.29.31" # minimum supported version
        - python-version: '3.11'
          os: 'ubuntu-22.04'
          cython: "==3.0.8" # System version for Ubuntu 24.04
        - python-version: '3.13'
          os: 'ubuntu-22.04'
          numpy: "'<2.2'"
          cython: '!=3.1.2' # Specifier can be dropped after 3.1.3 or 3.2.0 is released
      fail-fast: false
    steps:
    - uses: actions/checkout@v4
      name: Checkout the repository
      with:
        submodules: recursive
        persist-credentials: false
    - name: Setup Python
      uses: actions/setup-python@v5
      with:
        python-version: ${{ matrix.python-version }}
        architecture: x64
    - name: Install Apt dependencies
      run: |
        sudo apt update
        sudo apt install libboost-dev gfortran libopenmpi-dev libopenblas-openmp-dev \
          libhdf5-dev libfmt-dev doxygen
        gcc --version
    - name: Upgrade pip
      run: python3 -m pip install -U pip setuptools wheel
    - name: Install Python dependencies
      run: |
        python3 -m pip install ruamel.yaml scons==4.0.1 numpy${{ matrix.numpy }} \
          cython${{ matrix.cython }} pandas pytest \
          pytest-github-actions-annotate-failures pytest-xdist pint graphviz Jinja2
    - name: Build Cantera with legacy CLib
      run: |
        python3 `which scons` build env_vars=all -j4 debug=n --debug=time \
        system_fmt=y cc_flags=-D_GLIBCXX_ASSERTIONS clib_legacy=y
    - name: Build Tests
      run:
        python3 `which scons` -j4 build-tests --debug=time
    - name: Run compiled tests
      run: python3 `which scons` test-gtest test-legacy --debug=time
    - name: Run Python tests
      run: |
        LD_LIBRARY_PATH=build/lib
        python3 -m pytest -raP -v -n auto --durations=50 test/python
    - name: Rebuild Cantera with generated CLib
      run: |
        python3 `which scons` build  clib_legacy=n -j4
      if: matrix.python-version == '3.11'
    - name: Run googletests for clib_generated
      run: scons test-clib --debug=time
      if: matrix.python-version == '3.11'
    - name: Upload shared library
      # Pin to 4.3.4 to resolve errors around only uploading symlinks.
      # See https://github.com/actions/upload-artifact/issues/589
      uses: actions/upload-artifact@v4.3.4
      if: matrix.python-version == '3.11'
      with:
        path: build/lib/libcantera_shared.so
        name: libcantera_shared-${{ matrix.os }}.so
        retention-days: 2
    - name: Create archive for Doxygen output
      run: tar -czf doxygen.tar.gz Cantera.tag  doxygen/xml
      working-directory: build/doc
      if: matrix.python-version == '3.11'
    - name: Upload Doxygen output
      # Only need one copy from CI run
      uses: actions/upload-artifact@v4
      with:
        path: build/doc/doxygen.tar.gz
        name: doxygen-tree
        overwrite: true
        retention-days: 2
      if: matrix.python-version == '3.11'
    - name: Save the wheel file to install Cantera
      uses: actions/upload-artifact@v4
      with:
        path: build/python/dist/cantera*.whl
        retention-days: 2
        name: cantera-wheel-${{ matrix.python-version }}-${{ matrix.os }}
        if-no-files-found: error

  clang-compiler:
    name: LLVM/Clang with Python 3.13
    runs-on: ubuntu-22.04
    timeout-minutes: 60
    steps:
    - uses: actions/checkout@v4
      name: Checkout the repository
      with:
        submodules: recursive
        persist-credentials: false
    - name: Setup Python
      uses: actions/setup-python@v5
      with:
        python-version: '3.13'
        architecture: x64
    - name: Install Apt dependencies
      run: |
        sudo apt update
        sudo apt install libboost-dev gfortran libomp-dev libomp5 \
          libopenblas-openmp-dev libhdf5-dev doxygen graphviz
    - name: Upgrade pip
      run: python3 -m pip install -U pip setuptools wheel
    - name: Install Python dependencies
      run: python3 -m pip install ruamel.yaml scons numpy cython!=3.1.2 pandas pytest Jinja2
        pytest-xdist pytest-github-actions-annotate-failures pint graphviz
    - name: Build Cantera
      run: python3 `which scons` build env_vars=all
        CXX=clang++-14 CC=clang-14 f90_interface=n extra_lib_dirs=/usr/lib/llvm/lib
        -j4 debug=n --debug=time logging=debug
        warning_flags='-Wall -Werror -Wsuggest-override'
    - name: Build Tests
      run:
        python3 `which scons` -j4 build-tests --debug=time
    - name: Run compiled tests
      run: python3 `which scons` test-gtest test-legacy --debug=time
    - name: Run Python tests
      run: |
        LD_LIBRARY_PATH=build/lib
        python3 -m pytest -raP -v -n auto --durations=50 test/python
    - name: Run googletests
      run: python3 `which scons` test-clib --debug=time
    - name: Run sample demo
      run: python3 `which scons` test-clib-demo --debug=time

  macos-multiple-pythons:
    name: ${{ matrix.macos-version }} with Python ${{ matrix.python-version }}
    runs-on: ${{ matrix.macos-version }}
    timeout-minutes: 90
    strategy:
      matrix:
        macos-version: ['macos-13', 'macos-14', 'macos-15']
        python-version: ['3.12', '3.13']
        include:
          - macos-version: 'macos-13'
            python-version: '3.11'
          - macos-version: 'macos-14'
            python-version: '3.10'
            extra-build-args: cxx_flags='-std=c++20'
          - macos-version: 'macos-15'
            python-version: '3.11'
      fail-fast: false
    steps:
    - uses: actions/checkout@v4
      name: Checkout the repository
      with:
        submodules: recursive
        persist-credentials: false
    - name: Write dependencies to a file for caching
      run: |
        echo "scons ruamel.yaml numpy cython!=3.1.2 pandas pytest pytest-xdist pytest-github-actions-annotate-failures pint graphviz Jinja2" | tr " " "\n" > requirements.txt
    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: ${{ matrix.python-version }}
        cache: pip
        cache-dependency-path: requirements.txt
    - name: Install Brew dependencies
      run: brew install --display-times boost libomp hdf5 doxygen
    - name: Set Include folder
      run:
        echo "BOOST_INC_DIR=$(brew --prefix)/include" >> $GITHUB_ENV
    - name: Upgrade pip
      run: python -m pip install -U pip setuptools wheel
    - name: Install Python dependencies
      run: python -m pip install -r requirements.txt
    - name: Build Cantera with legacy CLib
      run: scons build env_vars=all -j3 debug=n --debug=time
        boost_inc_dir=${BOOST_INC_DIR} ${{ matrix.extra-build-args }} clib_legacy=y
    - name: Build Tests
      run: scons -j3 build-tests --debug=time
    - name: Run compiled tests
      run: scons test-gtest test-legacy --debug=time
    - name: Run Python tests
      run: |
        export DYLD_LIBRARY_PATH=build/lib
        python3 -m pytest -raP -v -n auto --durations=50 test/python
    - name: Rebuild Cantera with generated CLib
      run: |
        scons build clib_legacy=n -j3
      if: matrix.python-version == '3.12' && matrix.macos-version == 'macos-13'
    - name: Run googletests for generated CLib
      run: scons test-clib --debug=time
      if: matrix.python-version == '3.12' && matrix.macos-version == 'macos-13'
    - name: Upload shared library
      uses: actions/upload-artifact@v4
      if: matrix.python-version == '3.12' && matrix.macos-version == 'macos-13'
      with:
        path: build/lib/libcantera_shared.dylib
        name: libcantera_shared.dylib
        retention-days: 2

  # Coverage is its own job because macOS builds of the samples
  # use Homebrew gfortran which is not compatible for coverage
  # with XCode clang. Also, turning off optimization really
  # slows down the tests
  coverage:
    name: Coverage
    runs-on: ubuntu-latest
    timeout-minutes: 90
    steps:
    - uses: actions/checkout@v4
      name: Checkout the repository
      with:
        submodules: recursive
        persist-credentials: false
    - name: Setup python
      uses: actions/setup-python@v5
      with:
        python-version: '3.11'
        architecture: x64
    - name: Install Apt dependencies
      run: |
        sudo apt update
        sudo apt install libboost-dev gfortran libopenblas-openmp-dev libsundials-dev \
          libhdf5-dev doxygen
        gcc --version
    - name: Upgrade pip
      run: python3 -m pip install -U pip setuptools wheel
    - name: Install Python dependencies
      run: |
        python3 -m pip install ruamel.yaml scons numpy cython!=3.1.2 pandas scipy pytest \
        pytest-github-actions-annotate-failures pytest-cov gcovr!=7.0.0 pint graphviz \
        Jinja2
    - name: Setup .NET Core SDK
      uses: actions/setup-dotnet@v4
      with:
        dotnet-version: '8.x'
    - name: Build Cantera with legacy CLib
      run: |
        python3 `which scons` build blas_lapack_libs=lapack,blas coverage=y \
        optimize=n skip_slow_tests=y no_optimize_flags='-DNDEBUG -O0' \
        FORTRANFLAGS='-O0' env_vars=all clib_legacy=y -j4 --debug=time
    - name: Build Tests
      run: python3 `which scons` -j4 build-tests --debug=time
    - name: Run compiled tests
      run: python3 `which scons` test-gtest test-legacy --debug=time
    - name: Run Python tests
      run: python3 `which scons` test-python show_long_tests=yes verbose_tests=yes
    - name: Set environment variables for MATLAB
      # MATLAB requires the legacy CLib
      run: |
        LIB_STDCXX=$(ldconfig -p | grep libstdc++.so.6 | awk '{print $4}' | head -n 1)
        LIB_OPENBLAS=$(ldconfig -p | grep libopenblas.so.0 | awk '{print $4}' | head -n 1)
        LIB_LAPACK=$(ldconfig -p | grep liblapack.so.3 | awk '{print $4}' | head -n 1)
        echo "LD_PRELOAD=$LIB_STDCXX:$LIB_OPENBLAS:$LIB_LAPACK" >> $GITHUB_ENV
    - name: Set up MATLAB
      uses: matlab-actions/setup-matlab@718d4320188c73c86eb94ce76b553cbf89778487 # v2.5.0
    - name: Test the MATLAB interface and generate coverage report
      uses: matlab-actions/run-command@67f012f1ee4bc1627a7a95a0ccdeec745c9f6f36 # v2.2.1
      env:
        CANTERA_ROOT: /home/runner/work/cantera/cantera
        CANTERA_DATA: /home/runner/work/cantera/cantera/data
      with:
        command: |
          addpath('/home/runner/work/cantera/cantera/test/matlab_experimental');
          matlabCoverage;
    - name: Rebuild Cantera with generated CLib
      run: scons build clib_legacy=n -j4
    - name: Run googletests for generated CLib
      run: scons test-clib --debug=time
    - name: Build the .NET interface
      run: dotnet build
      working-directory: interfaces/dotnet
    - name: Test the .NET interface
      # Collect coverage info using Coverlet (identified by magic string below)
      run: |
        dotnet test --collect:"XPlat Code Coverage"
        mv Cantera.Tests/TestResults/*/coverage.cobertura.xml .
        dotnet new tool-manifest
        dotnet tool install --local dotnet-reportgenerator-globaltool
        dotnet reportgenerator -reports:"coverage.cobertura.xml" -targetdir:"coveragereport" -reporttypes:Html
      working-directory: interfaces/dotnet
    - name: Process coverage files
      run: |
        gcovr --root . --exclude-unreachable-branches --exclude-throw-branches \
        --exclude-directories '\.sconf_temp' --exclude-directories 'build/ext$' \
        --exclude '.*ext.*' --exclude '(.+/)?_cantera\.cpp$' --exclude '^test.*' \
        --xml coverage.xml --html-details htmlcoverage.html --txt
    - name: Archive C++ coverage results
      uses: actions/upload-artifact@v4
      with:
        name: cxx-coverage-report
        path: htmlcoverage*
        retention-days: 5
    - name: Archive Python coverage results
      uses: actions/upload-artifact@v4
      with:
        name: python-coverage-report
        path: build/python-coverage*
        retention-days: 5
    - name: Archive .NET coverage results
      uses: actions/upload-artifact@v4
      with:
        name: dotnet-coverage-report
        path: interfaces/dotnet/coveragereport*
        retention-days: 5
    - name: Archive MATLAB coverage results
      uses: actions/upload-artifact@v4
      with:
        name: matlab-coverage-report
        path: test/matlab_experimental/matlabCoverage*
        retention-days: 5
    - name: Upload Coverage to Codecov
      uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
      with:
        verbose: true
        files: ./coverage.xml,./build/pycov.xml,./interfaces/dotnet/coverage.cobertura.xml,./test/matlab_experimental/matlabCoverage.xml
        fail_ci_if_error: true

  docs:
    name: Build docs
    runs-on: ubuntu-latest
    timeout-minutes: 60
    defaults:
      run:
        shell: bash -l {0}
    steps:
      - uses: actions/checkout@v4
        name: Checkout the repository
        with:
          submodules: recursive
          persist-credentials: false
      - name: Set up micromamba
        uses: mamba-org/setup-micromamba@0dea6379afdaffa5d528b3d1dabc45da37f443fc # v2.0.4
        with:
          environment-name: test-env
          create-args: >-
            python=3.11 doxygen=1.9.7 scons pip scikits.odes
          post-cleanup: none
      - name: Install Apt dependencies
        run: |
          sudo apt update
          sudo apt install libboost-dev graphviz texlive-bibtex-extra \
            libopenblas-openmp-dev libhdf5-dev libfmt-dev libsundials-dev
      - name: Upgrade pip
        run: pip install -U pip setuptools wheel
      - name: Install Python dependencies
        # Pinned sphinx and pydata-sphinx-theme versions are for the Cantera 3.1.x
        # series. These versions can be relaxed on main branch afterwards.
        run: |
          pip install ruamel.yaml scons numpy cython!=3.1.2 'sphinx>=7.3.7,<7.4' sphinx-gallery \
            sphinxcontrib-matlabdomain sphinxcontrib-doxylink sphinxcontrib-bibtex \
            'pydata-sphinx-theme>=0.15.3,<0.16' sphinx-argparse sphinx_design myst-nb \
            sphinx-copybutton matplotlib pandas scipy pint coolprop graphviz
          pip install "git+https://github.com/Cantera/sphinx-tags.git@main"
      - name: Build Cantera
        run: scons build -j4 debug=n optimize=y use_pch=n
      - name: Build documentation
        run: |
          scons sphinx doxygen logging=debug \
            sphinx_options="-W --keep-going --warning-file=sphinx-warnings.txt"
      - name: Show Sphinx warnings
        run: |
          cat sphinx-warnings.txt
        if: failure()
      - name: Ensure 'scons help' options work
        run: |
          scons help --options
          scons help --list-options
          scons help --option=prefix
      - name: Create archive for docs output
        run: |
          cd build/doc
          tar --exclude="*.map" --exclude="*.md5" -czf docs.tar.gz html
        if: failure() || success()
      - name: Store a copy of docs output
        uses: actions/upload-artifact@v4
        with:
          path: build/doc/docs.tar.gz
          name: docs
          retention-days: 14
        if: failure() || success()
      - name: Determine whether to deploy
        id: deploy-conf
        if: github.event_name == 'push' && github.repository_owner == 'Cantera'
        run: |
          if [[ ${GITHUB_REF} =~ ^refs\/heads\/([0-9]+\.[0-9]+)$ ]]; then
              echo "match=true" >> $GITHUB_OUTPUT
              echo "rsync_dest=cantera/${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT
              echo "commit=true" >> $GITHUB_OUTPUT
              echo "git_dest=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT
              echo "git_branch=staging" >> $GITHUB_OUTPUT
          elif [[ ${GITHUB_REF} == "refs/heads/main" ]]; then
              echo "match=true" >> $GITHUB_OUTPUT
              echo "rsync_dest=cantera/dev" >> $GITHUB_OUTPUT
          elif [[ ${GITHUB_REF} == "refs/heads/testing" ]]; then
              echo "match=true" >> $GITHUB_OUTPUT
              echo "rsync_dest=testing.cantera.org/dev" >> $GITHUB_OUTPUT
              echo "commit=true" >> $GITHUB_OUTPUT
              echo "git_dest=testing" >> $GITHUB_OUTPUT
              echo "git_branch=testing" >> $GITHUB_OUTPUT
          fi
        env:
          GITHUB_REF: ${{ github.ref }}
      # The known_hosts key is generated with `ssh-keygen -F cantera.org` from a
      # machine that has previously logged in to cantera.org and trusts
      # that it logged in to the right machine
      - name: Set up SSH key and host for deploy
        if: steps.deploy-conf.outputs.match == 'true'
        uses: shimataro/ssh-key-action@d4fffb50872869abe2d9a9098a6d9c5aa7d16be4 # v2.7.0
        with:
          key: ${{ secrets.CTDEPLOY_KEY }}
          known_hosts: ${{ secrets.CTDEPLOY_HOST }}
      - name: Commit docs to api-docs repo
        if: steps.deploy-conf.outputs.commit == 'true'
        env:
          CANTERABOT_DOCS_TOKEN: ${{ secrets.CANTERABOT_DOCS_TOKEN }}
          # Directory in api-docs repo: either the version (X.Y) or "testing"
          GIT_DEST: ${{ steps.deploy-conf.outputs.git_dest }}
          # Branch in api-docs to commit to: either 'staging' or 'testing'
          BRANCH: ${{ steps.deploy-conf.outputs.git_branch }}
        run: |
          set -ex
          REPO_SHA=`git rev-parse --short=8 HEAD`
          git clone https://canterabot:${CANTERABOT_DOCS_TOKEN}@github.com/Cantera/api-docs

          cd api-docs
          git config user.name "CanteraBot"
          git config user.email "96191898+CanteraBot@users.noreply.github.com"

          git checkout --track origin/${BRANCH}
          mkdir -p ${GIT_DEST}
          tar -zxf ../build/doc/docs.tar.gz --strip-components=1 -C ${GIT_DEST}
          git add .
          git commit -m "Update for Cantera ${GIT_DEST} - Cantera/cantera@${REPO_SHA}"
          git show --stat HEAD
          git push origin
      - name: Upload the docs to cantera.org
        if: steps.deploy-conf.outputs.match == 'true'
        env:
          RSYNC_USER: "ctdeploy"
          RSYNC_SERVER: "cantera.org"
          RSYNC_DEST: ${{ steps.deploy-conf.outputs.rsync_dest }}
          DOCS_OUTPUT_DIR: "./build/doc/html/"
        run: |
          rsync -avzP --checksum --exclude='*.map' --exclude='*.md5' \
            --delete --delete-excluded --filter='P .htaccess' \
            "${DOCS_OUTPUT_DIR}" ${RSYNC_USER}@${RSYNC_SERVER}:${RSYNC_DEST}

  run-examples:
    name: Run Python ${{ matrix.python-version }} examples on ${{ matrix.os }}, NumPy ${{ matrix.numpy || 'latest' }}
    runs-on: ${{ matrix.os }}
    timeout-minutes: 60
    needs: ["ubuntu-multiple-pythons"]
    strategy:
      matrix:
        # Keep some test cases with NumPy 1.x until we explicitly drop support
        include:
        - os: "ubuntu-22.04"
          python-version: "3.10"
          numpy: ""
          libhdf5: "libhdf5-103"
        - os: "ubuntu-24.04"
          python-version: "3.10"
          numpy: "==1.21.6"
          libhdf5: "libhdf5-103-1t64"
        - os: "ubuntu-22.04"
          python-version: "3.11"
          numpy: "==1.23.5"
          libhdf5: "libhdf5-103"
        - os: "ubuntu-24.04"
          python-version: "3.11"
          numpy: ""
          libhdf5: "libhdf5-103-1t64"
        - os: "ubuntu-22.04"
          python-version: "3.12"
          numpy: "==1.26.4"
          libhdf5: "libhdf5-103"
        - os: "ubuntu-24.04"
          python-version: "3.12"
          numpy: ""
          libhdf5: "libhdf5-103-1t64"
        - os: "ubuntu-22.04"
          python-version: "3.13"
          numpy: ""
          libhdf5: "libhdf5-103"
      fail-fast: false
    steps:
      # We're not building Cantera here, we only need the checkout for the samples
      # folder, so no need to do a recursive checkout
      - uses: actions/checkout@v4
        name: Checkout the repository
        with:
          persist-credentials: false
      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          architecture: x64
      - name: Install Apt dependencies
        run: |
          sudo apt update
          sudo apt install graphviz ${{ matrix.libhdf5 }} libfmt-dev libopenblas0-openmp
      - name: Download the wheel artifact
        uses: actions/download-artifact@v4
        with:
          name: cantera-wheel-${{ matrix.python-version }}-${{ matrix.os }}
          path: dist
      - name: Download the Cantera shared library (.so)
        uses: actions/download-artifact@v4
        with:
          name: libcantera_shared-${{ matrix.os }}.so
          path: build/lib
      - name: Upgrade pip
        run: python3 -m pip install -U pip setuptools wheel
      - name: Install Python dependencies
        run: |
            python3 -m pip install numpy${{ matrix.numpy }} ruamel.yaml pandas pyarrow \
              matplotlib scipy pint graphviz CoolProp
            python3 -m pip install --pre --no-index --find-links dist cantera
      - name: Run the examples
        # See https://unix.stackexchange.com/a/392973 for an explanation of the -exec part.
        # Skip 1D_packed_bed.py due to difficulty installing scikits.odes.
        # Increase figure limit to handle flame_speed_convergence_analysis.py.
        # Skip equations_of_state.py in cases where CoolProp is broken (temporarily, for Python 3.13)
        run: |
          ln -s libcantera_shared.so build/lib/libcantera_shared.so.3
          rm samples/python/reactors/1D_packed_bed.py
          python3 -m CoolProp || rm samples/python/thermo/equations_of_state.py
          echo "figure.max_open_warning: 100" > matplotlibrc
          export LD_LIBRARY_PATH=build/lib
          find samples/python -type f -iname "*.py" \
          -exec sh -c 'for n; do echo "$n" | tee -a results.txt && python3 "$n" >> results.txt || exit 1; done' sh {} +
        env:
          # The pyparsing ignore setting is due to a new warning introduced in Matplotlib==3.6.0
          # @todo: Remove the trapz-related ignore when dropping support for NumPy 1.x
          #     and replacing np.trapz with np.trapezoid
          # Ignore NasaPoly2 warnings from n-hexane-NUIG-2015.yaml
          PYTHONWARNINGS: "error,ignore:warn_name_set_on_empty_Forward::pyparsing,ignore:datetime.datetime.utcfromtimestamp:DeprecationWarning:,ignore:`trapz`:DeprecationWarning,ignore:NasaPoly2:UserWarning:"
          MPLBACKEND: Agg
      - name: Save the results file for inspection
        uses: actions/upload-artifact@v4
        with:
          path: results.txt
          retention-days: 2
          name: example-results-${{ matrix.python-version }}-${{ matrix.os }}-np${{ matrix.numpy || 'latest' }}
        # Run this step if the job was successful or failed, but not if it was cancelled
        # Using always() would run this step if the job was cancelled as well.
        if: failure() || success()

  multiple-sundials:
    name: Sundials ${{ matrix.sundials-ver }} / fmt ${{ matrix.fmt-ver }}
    runs-on: ubuntu-latest
    timeout-minutes: 60
    env:
      PYTHON_VERSION: '3.13'
    defaults:
      run:
        shell: bash -l {0}
    strategy:
      matrix:
        include:
        - sundials-ver: 5.8
          fmt-ver: 9.1
        - sundials-ver: 6.4.1
          fmt-ver: 10
        - sundials-ver: 6.6
          fmt-ver: 10
        - sundials-ver: 7.2
          fmt-ver: 11
          extra-build-args: cxx_flags='-std=c++20'
      fail-fast: false
    steps:
      - uses: actions/checkout@v4
        name: Checkout the repository
        with:
          submodules: recursive
          persist-credentials: false
      - name: Set up micromamba
        uses: mamba-org/setup-micromamba@0dea6379afdaffa5d528b3d1dabc45da37f443fc # v2.0.4
        with:
          environment-name: test-env
          # See https://github.com/conda-forge/boost-cpp-feedstock/issues/41 for why we
          # use boost-cpp rather than boost from conda-forge
          create-args: >-
            python=${{ env.PYTHON_VERSION }} sundials=${{ matrix.sundials-ver }} scons
            numpy ruamel.yaml cython!=3.1.2 boost-cpp fmt=${{ matrix.fmt-ver }} eigen yaml-cpp
            pandas libgomp openblas pytest pytest-xdist highfive python-graphviz
            setuptools Jinja2 doxygen
          post-cleanup: none
      - name: Build Cantera
        run: |
          scons build system_fmt=y system_eigen=y system_yamlcpp=y system_sundials=y \
          system_highfive=y blas_lapack_libs='lapack,blas' -j4 logging=debug debug=n \
          optimize_flags='-O3 -ffast-math -fno-finite-math-only' \
          ${{ matrix.extra-build-args}}
      - name: Test Cantera
        run: scons test-zeroD test-python-reactor show_long_tests=yes verbose_tests=yes
      - name: Test Install
        # spot-check installation locations
        run: |
          scons install
          test -f ${CONDA_PREFIX}/lib/libcantera_shared.so
          test -f ${CONDA_PREFIX}/include/cantera/base/Solution.h
          test -f ${CONDA_PREFIX}/bin/ck2yaml
          test -f ${CONDA_PREFIX}/share/cantera/data/gri30.yaml
          test -d ${CONDA_PREFIX}/share/cantera/samples
          test -d ${CONDA_PREFIX}/share/cantera/samples/python
          test -d ${CONDA_PREFIX}/lib/python${{ env.PYTHON_VERSION }}/site-packages/cantera
      - name: Test Essentials
        # ensure that Python package loads and converter scripts work
        run: |
          python -c 'import cantera as ct; import sys; sys.exit(0) if ct.__version__.startswith("3.1.0") else sys.exit(1)'
          ck2yaml --input=test/data/h2o2.inp --output=h2o2-test.yaml
          test -f h2o2-test.yaml
          cti2yaml test/data/ch4_ion.cti ch4_ion-test.yaml
          test -f ch4_ion-test.yaml
          yaml2ck data/h2o2.yaml --mechanism=h2o2-test.ck
          test -f h2o2-test.ck

  windows-2022:
    name: Windows 2022, Python ${{ matrix.python-version }}, fmt ${{ matrix.fmt-ver }}
    runs-on: windows-2022
    timeout-minutes: 60
    strategy:
      matrix:
        include:
        - python-version: '3.13'
          fmt-ver: '10'
        - python-version: '3.10'
          fmt-ver: '8.1'
        - python-version: '3.11'
          fmt-ver: '9.1'
        - python-version: '3.12'
          fmt-ver: '10.0'
        - python-version: '3.13'
          fmt-ver: '11.0'
          extra-build-args: cxx_flags='/EHsc /std:c++20 /utf-8'
      fail-fast: false
    steps:
    - uses: actions/checkout@v4
      name: Checkout the repository
      with:
        submodules: recursive
        persist-credentials: false
    - name: Set up micromamba
      uses: mamba-org/setup-micromamba@0dea6379afdaffa5d528b3d1dabc45da37f443fc # v2.0.4
      with:
        environment-name: test-env
        # See https://github.com/conda-forge/boost-cpp-feedstock/issues/41 for why we
        # use boost-cpp rather than boost from conda-forge
        # Install SCons >=4.4.0 to make sure that MSVC_TOOLSET_VERSION variable is present
        create-args: >-
          python=${{ matrix.python-version }} scons numpy cython!=3.1.2 ruamel.yaml boost-cpp
          eigen yaml-cpp pandas pytest pytest-xdist highfive pint python-graphviz
          fmt=${{ matrix.fmt-ver }} setuptools Jinja2 doxygen openblas
        post-cleanup: none
        init-shell: powershell
    - name: Build Cantera with legacy CLib
      run: scons build system_eigen=y system_yamlcpp=y system_highfive=y logging=debug
        toolchain=msvc f90_interface=n debug=n ${{ matrix.extra-build-args }}
        --debug=time clib_legacy=y -j4 blas_lapack_libs=openblas optimize=n
    - name: Build Tests
      run: scons -j4 build-tests --debug=time
    - name: Run compiled tests
      run: scons test-gtest test-legacy --debug=time
    - name: Run Python tests
      run: |
        pytest -raP -v -n auto --durations=50 test/python
    - name: Test Install
      # spot-check installation locations
      run: |
        scons install
        $paths = @('Library/bin/cantera_shared.dll',
            'Library/include/cantera/base/Solution.h', 'Scripts/ck2yaml.exe',
            'share/cantera/data/gri30.yaml', 'share/cantera/samples',
            'share/cantera/samples/python', 'Lib/site-packages/cantera')
        Foreach ($path in $paths) {
          if (-not (Test-Path $Env:CONDA_PREFIX/$path)) {
            echo "$path not found in install directory"
            exit 1
          }
        }
    - name: Test Essentials
      # ensure that Python package loads and converter scripts work
      run: |
        python -c 'import cantera as ct; import sys; sys.exit(0) if ct.__version__.startswith("3.1.0") else sys.exit(1)'
        ck2yaml --input=test/data/h2o2.inp --output=h2o2-test.yaml
        if (-not (Test-Path h2o2-test.yaml)) { exit 1 }
        cti2yaml test/data/ch4_ion.cti ch4_ion-test.yaml
        if (-not (Test-Path ch4_ion-test.yaml)) { exit 1 }
        yaml2ck data/h2o2.yaml --mechanism=h2o2-test.ck
        if (-not (Test-Path h2o2-test.ck)) { exit 1 }
    - name: Rebuild Cantera with generated CLib
      run: |
        scons build clib_legacy=n -j4
      if: matrix.python-version == '3.11'
    - name: Run googletests for generated CLib
      run: scons test-clib --debug=time
      if: matrix.python-version == '3.11'
    - name: Upload shared library
      # Pin to 4.3.4 to resolve errors around only uploading symlinks.
      # See https://github.com/actions/upload-artifact/issues/589
      uses: actions/upload-artifact@v4.3.4
      if: matrix.python-version == '3.11'
      with:
        path: build/lib/cantera_shared.dll
        name: cantera_shared.dll
        retention-days: 2

  windows:
    name: "Windows 2025, MSVC ${{ matrix.vs-toolset }}, Python ${{ matrix.python-version }}"
    runs-on: windows-2025
    timeout-minutes: 60
    env:
      BOOST_ROOT: ${{github.workspace}}/3rdparty/boost
      BOOST_URL: https://github.com/boostorg/boost/releases/download/boost-1.87.0/boost-1.87.0-b2-nodocs.7z
    strategy:
      matrix:
        python-version: ["3.10", "3.11", "3.12", "3.13"]
      fail-fast: false
    steps:
      - uses: actions/checkout@v4
        name: Checkout the repository
        with:
          submodules: recursive
          persist-credentials: false
      - name: Set Up Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          architecture: x64
          check-latest: true
      - name: Install Python dependencies
        run: |
          python -m pip install -U pip setuptools wheel
          python -m pip install 'scons<4.4.0' pypiwin32 numpy ruamel.yaml cython!=3.1.2 pandas graphviz pytest pytest-xdist pytest-github-actions-annotate-failures
      - name: Restore Boost cache
        uses: actions/cache@v4
        id: cache-boost
        with:
          path: ${{env.BOOST_ROOT}}
          key: boost-187-win
      - name: Install Boost Headers
        if: steps.cache-boost.outputs.cache-hit != 'true'
        run: |
          BOOST_ROOT=$(echo $BOOST_ROOT | sed 's/\\/\//g')
          mkdir -p $BOOST_ROOT
          curl --progress-bar --location --output $BOOST_ROOT/download.7z -L $BOOST_URL
          7z -o$BOOST_ROOT x $BOOST_ROOT/download.7z -y -bd boost-1.87.0/boost
          mv $BOOST_ROOT/boost-1.87.0/boost $BOOST_ROOT/boost
          rm $BOOST_ROOT/download.7z
        shell: bash
      - name: Build Cantera
        run: scons build -j4 boost_inc_dir=$Env:BOOST_ROOT debug=n logging=debug
          python_package=y env_vars=USERPROFILE,GITHUB_ACTIONS clib_legacy=y
          f90_interface=n toolchain=msvc --debug=time optimize=n
      - name: Build Tests
        run: scons -j4 build-tests --debug=time
      - name: Run compiled tests
        run: scons test-gtest test-legacy --debug=time
      - name: Run Python tests
        run: python -m pytest -raP -v -n auto --durations=50 test/python

  # Adapted from https://www.scivision.dev/intel-oneapi-github-actions/
  linux-intel-oneapi:
    name: intel-oneAPI on Ubuntu, Python 3.12
    runs-on: ubuntu-latest
    timeout-minutes: 60
    env:
      INTEL_REPO: https://apt.repos.intel.com
      INTEL_KEY: GPG-PUB-KEY-INTEL-SW-PRODUCTS-2023.PUB
    steps:
    - name: Intel Apt repository
      timeout-minutes: 1
      run: |
        wget ${INTEL_REPO}/intel-gpg-keys/${INTEL_KEY}
        sudo apt-key add ${INTEL_KEY}
        rm ${INTEL_KEY}
        echo "deb ${INTEL_REPO}/oneapi all main" | sudo tee /etc/apt/sources.list.d/oneAPI.list
        sudo apt-get update
    - name: Install Intel oneAPI
      timeout-minutes: 15
      run: |
        sudo apt-get install intel-oneapi-compiler-fortran intel-oneapi-compiler-dpcpp-cpp-and-cpp-classic \
        intel-oneapi-mpi intel-oneapi-mpi-devel intel-oneapi-mkl ninja-build libboost-dev libhdf5-dev \
        doxygen
    - uses: actions/checkout@v4
      name: Checkout the repository
      with:
        submodules: recursive
        persist-credentials: false
    - name: Setup Python
      uses: actions/setup-python@v5
      with:
        python-version: '3.12'
        architecture: x64
    - name: Upgrade pip
      run: python3 -m pip install -U pip setuptools wheel
    - name: Install Python dependencies
      run: |
        python3 -m pip install ruamel.yaml scons numpy cython!=3.1.2 pandas pytest \
        pytest-xdist pytest-github-actions-annotate-failures pint graphviz Jinja2
    - name: Setup Intel oneAPI environment
      run: |
        source /opt/intel/oneapi/setvars.sh
        printenv >> $GITHUB_ENV
    - name: Build Cantera
      run: python3 `which scons` build env_vars=all CC=icx CXX=icpx -j4 debug=n
        --debug=time f90_interface=n # FORTRAN=ifx
    - name: Build Tests
      run: python3 `which scons` -j4 build-tests --debug=time
    - name: Run compiled tests
      run: python3 `which scons` test-gtest test-legacy --debug=time
    - name: Run Python tests
      run: |
        LD_LIBRARY_PATH=$LD_LIBRARY_PATH:build/lib
        python3 -m pytest -raP -v -n auto --durations=50 test/python

  windows-mingw:
    name: mingw on Windows, Python 3.10
    runs-on: windows-2022
    timeout-minutes: 120 # MinGW is slooooow
    env:
      BOOST_ROOT: ${{github.workspace}}/3rdparty/boost
      BOOST_URL: https://github.com/boostorg/boost/releases/download/boost-1.87.0/boost-1.87.0-b2-nodocs.7z
    steps:
    - uses: actions/checkout@v4
      name: Checkout the repository
      with:
        submodules: recursive
        persist-credentials: false
    - name: Set Up Python
      uses: actions/setup-python@v5
      with:
        python-version: "3.10"
        architecture: x64
    - name: Install Python dependencies
      run: |
        python -m pip install -U pip setuptools wheel
        python -m pip install scons pypiwin32 numpy ruamel.yaml cython!=3.1.2 h5py pandas pytest pytest-xdist pytest-github-actions-annotate-failures pint graphviz
    - name: Restore Boost cache
      uses: actions/cache@v4
      id: cache-boost
      with:
        path: ${{env.BOOST_ROOT}}
        key: boost-187-win
    - name: Set up MinGW
      uses: egor-tensin/setup-mingw@84c781b557efd538dec66bde06988d81cd3138cf # v2.2.0
      with:
        platform: x64
        static: false
        version: 12.2
    - name: Install Boost Headers
      if: steps.cache-boost.outputs.cache-hit != 'true'
      run: |
        BOOST_ROOT=$(echo $BOOST_ROOT | sed 's/\\/\//g')
        mkdir -p $BOOST_ROOT
        curl --progress-bar --location --output $BOOST_ROOT/download.7z -L $BOOST_URL
        7z -o$BOOST_ROOT x $BOOST_ROOT/download.7z -y -bd boost-1.87.0/boost
        mv $BOOST_ROOT/boost-1.87.0/boost $BOOST_ROOT/boost
        rm $BOOST_ROOT/download.7z
      shell: bash
    - name: Build Cantera
      run: scons build -j4 boost_inc_dir=$Env:BOOST_ROOT debug=n logging=debug
        python_package=y env_vars=USERPROFILE,PYTHONPATH,GITHUB_ACTIONS
        toolchain=mingw f90_interface=n --debug=time clib_legacy=y
    - name: Upload Wheel
      uses: actions/upload-artifact@v4
      with:
        path: build\python\dist\cantera*.whl
        name: cantera-win_amd64.whl
        retention-days: 2
    - name: Build Tests
      run: scons -j4 build-tests --debug=time
    - name: Run compiled tests
      run: scons test-gtest test-legacy --debug=time
    - name: Run Python tests
      run: python -m pytest -raP -v -n auto --durations=50 test/python
    - name: Upload Test binaries
      if: always()
      uses: actions/upload-artifact@v4
      with:
        path: |
          build/test/**/*.exe
          build/lib/*.dll
        name: mingw-gtest-binaries
        retention-days: 2

  dotnet:
    name: .NET on ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-22.04, windows-2022, macos-13]
      fail-fast: false
    runs-on: ${{ matrix.os }}
    needs: [ubuntu-multiple-pythons, macos-multiple-pythons, windows-2022]
    timeout-minutes: 60
    steps:
    - uses: actions/checkout@v4
      name: Checkout the repository
      with:
        persist-credentials: false
    - name: Override default Python (Windows)
      # Other operating systems use default system Python 3
      uses: actions/setup-python@v5
      with:
        python-version: "3.10"
        architecture: x64
      if: matrix.os == 'windows-2022'
    - name: Install Python dependencies
      # Install Python dependencies, which are used by 'dotnet build'
      run: |
        python3 -m pip install --upgrade pip setuptools wheel build
        python3 -m pip install ruamel.yaml Jinja2 typing-extensions
    - name: Install library dependencies with micromamba (Windows)
      uses: mamba-org/setup-micromamba@0dea6379afdaffa5d528b3d1dabc45da37f443fc # v2.0.4
      with:
        environment-name: test-env
        # fmt needs to match the version of the windows-2022 runner selected to upload
        # the cantera_shared.dll artifact
        create-args: >-
          yaml-cpp openblas highfive fmt=9.1
        init-shell: bash powershell
        post-cleanup: none
      if: matrix.os == 'windows-2022'
    - name: Install Brew dependencies (macOS)
      run: brew install --display-times hdf5
      if: matrix.os == 'macos-13'
    - name: Install Apt dependencies (Ubuntu)
      run: |
        sudo apt update
        sudo apt install libhdf5-dev libfmt-dev libopenblas0-openmp
      if: matrix.os == 'ubuntu-22.04'
    - name: Setup .NET Core SDK
      uses: actions/setup-dotnet@v4
      with:
        dotnet-version: '8.x'
    - name: Download the Cantera shared library (.so)
      uses: actions/download-artifact@v4
      with:
        name: libcantera_shared-ubuntu-22.04.so
        path: build/lib
    - name: Download the Cantera shared library (.dylib)
      uses: actions/download-artifact@v4
      with:
        name: libcantera_shared.dylib
        path: build/lib
    - name: Download the Cantera shared library (.dll)
      uses: actions/download-artifact@v4
      with:
        name: cantera_shared.dll
        path: build/lib
    - name: Download the Doxygen artifacts
      uses: actions/download-artifact@v4
      with:
        name: doxygen-tree
        path: build/doc
    - name: Extract Doxygen artifacts
      run: tar -xzf doxygen.tar.gz
      working-directory: build/doc
    - name: Build the .NET interface
      run: dotnet build
      working-directory: interfaces/dotnet
    - name: Test the .NET interface
      run: dotnet test
      working-directory: interfaces/dotnet
    - name: Run the .NET samples
      run: |
        dotnet run --project examples/Application
        dotnet run --project examples/SoundSpeed
      working-directory: interfaces/dotnet
    - name: Clean .NET
      run: |
        dotnet clean
        if [ -e Cantera/obj/sourcegen/* ]; then
          echo 'Cantera/obj/sourcegen not cleaned as expected!' >&2
          exit 1
        fi
      shell: bash
      working-directory: interfaces/dotnet
    - name: Install sourcegen
      run: python -m pip install -e interfaces/sourcegen
    - name: Test YAML output generation
      run: sourcegen --api=yaml --output=build/yaml -v
