name: NEURON CI

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

on:
  push:
    branches:
      - master
      - release/**
  pull_request:
    branches:
      - master
      - release/**
# TODO : https://github.com/neuronsimulator/nrn/issues/1063
#    paths-ignore:
#      - '**.md'
#      - '**.rst'
#      - 'docs/**'

env:
  BUILD_TYPE: Release
  DESIRED_CMAKE_VERSION: 3.15.0
  PY_MIN_VERSION: '3.7'
  PY_MAX_VERSION: '3.11'

jobs:
  ci:
    runs-on: ${{ matrix.os }}

    name: ${{ matrix.os }} - ${{ matrix.config.build_mode }} (${{ matrix.config.cmake_option }}${{ matrix.config.config_options }}${{ matrix.config.matrix_eval }})

    timeout-minutes: 45

    env:
      INSTALL_DIR: install
      SDK_ROOT: $(xcrun --sdk macosx --show-sdk-path)
      SKIP_WHEELHOUSE_REPAIR: true
      BUILD_TYPE: Release
      DESIRED_CMAKE_VERSION: 3.15.0
      PY_MIN_VERSION: ${{ matrix.config.python_min_version || '3.8' }}
      PY_MAX_VERSION: ${{ matrix.config.python_max_version || '3.11' }}
      MUSIC_INSTALL_DIR: /opt/MUSIC

    strategy:
      matrix:
        os: [ macOS-12, ubuntu-20.04]
        config:
          - { matrix_eval : "CC=gcc-9 CXX=g++-9",   build_mode: "setuptools"}
          - { matrix_eval : "CC=gcc-8 CXX=g++-8",   build_mode: "cmake", music: ON}
          - { matrix_eval : "CC=gcc-9 CXX=g++-9",   build_mode: "cmake", python_dynamic: ON}
          - { matrix_eval : "CC=gcc-7 CXX=g++-7" ,  build_mode: "cmake", cmake_option: "-DNRN_ENABLE_CORENEURON=ON"}
          - { matrix_eval : "CC=gcc-7 CXX=g++-7",   build_mode: "cmake", cmake_option: "-DNRN_ENABLE_MPI=OFF -DNRN_ENABLE_INTERVIEWS=OFF -DNRN_ENABLE_CORENEURON=ON"}
          - { matrix_eval : "CC=gcc-10 CXX=g++-10", build_mode: "cmake", cmake_option: "-DNRN_ENABLE_PYTHON=OFF -DNRN_ENABLE_RX3D=OFF -DNRN_ENABLE_CORENEURON=ON"}
        include:
          - os: ubuntu-22.04
            config:
              build_mode: cmake
              # TODO: -DCORENRN_ENABLE_NMODL=ON -DNMODL_SANITIZERS=undefined
              cmake_option: -DNRN_ENABLE_CORENEURON=ON
                -DNRN_ENABLE_INTERVIEWS=OFF -DCORENRN_SANITIZERS=undefined
              flag_warnings: ON
              sanitizer: undefined
          - os: ubuntu-22.04
            config:
              build_mode: cmake
              # TODO: -DCORENRN_ENABLE_NMODL=ON -DNMODL_SANITIZERS=address,leak
              # TODO: CoreNEURON is only LeakSanitizer-clean if we disable MPI
              cmake_option: -DNRN_ENABLE_CORENEURON=ON
                -DNRN_ENABLE_INTERVIEWS=OFF -DCORENRN_SANITIZERS=address
              # TODO: address-leak is the dream, but there are many problems,
              # including external ones from the MPI implementations
              sanitizer: address
      fail-fast: false

    steps:
      - name: Setup cmake
        uses: jwlawson/actions-setup-cmake@v2
        with:
          cmake-version: ${{ env.DESIRED_CMAKE_VERSION }}

      - name: Fix kernel mmap rnd bits
        if: ${{matrix.os == 'ubuntu-22.04'}}
        # Asan in llvm 14 provided in ubuntu 22.04 is incompatible with
        # high-entropy ASLR in much newer kernels that GitHub runners are
        # using leading to random crashes: https://reviews.llvm.org/D148280
        run: sudo sysctl vm.mmap_rnd_bits=28

      - name: Install homebrew packages
        if: startsWith(matrix.os, 'macOS')
        run: |
          brew install ccache coreutils doxygen flex mpich ninja xz autoconf autoconf automake libtool
          brew unlink mpich
          brew install openmpi
          brew install --cask xquartz
          echo /usr/local/opt/flex/bin >> $GITHUB_PATH
          # Core https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources
          echo CMAKE_BUILD_PARALLEL_LEVEL=3 >> $GITHUB_ENV
          echo CTEST_PARALLEL_LEVEL=3 >> $GITHUB_ENV
          echo CI_OS_NAME=osx >> $GITHUB_ENV
        shell: bash

      - name: Install apt packages
        if: startsWith(matrix.os, 'ubuntu')
        run: |
          sudo apt-get install build-essential ccache libopenmpi-dev \
            libmpich-dev libx11-dev libxcomposite-dev mpich ninja-build \
            openmpi-bin patchelf
          # The sanitizer builds use ubuntu 22.04
          if [[ "${{matrix.os}}" == "ubuntu-20.04" ]]; then
            sudo apt-get install g++-7 g++-8
          fi
          # Core https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources
          echo CMAKE_BUILD_PARALLEL_LEVEL=2 >> $GITHUB_ENV
          echo CTEST_PARALLEL_LEVEL=2 >> $GITHUB_ENV
          echo CI_OS_NAME=linux >> $GITHUB_ENV
        shell: bash

      - uses: actions/checkout@v4
        with:
          fetch-depth: 2

      - name: Set up Python@${{ env.PY_MIN_VERSION }}
        if: ${{matrix.config.python_dynamic == 'ON'}}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PY_MIN_VERSION }}

      - name: Install Python@${{ env.PY_MIN_VERSION }} dependencies
        if: ${{ matrix.config.python_dynamic == 'ON' }}
        working-directory: ${{runner.workspace}}/nrn
        run: |
          python -m pip install --upgrade pip -r nrn_requirements.txt

      - name: Set up Python@${{ env.PY_MAX_VERSION }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PY_MAX_VERSION }}

      - name: Install Python@${{ env.PY_MAX_VERSION }} dependencies
        working-directory: ${{runner.workspace}}/nrn
        run: |
          python -m pip install --upgrade pip -r nrn_requirements.txt

      - name: Setup MUSIC
        if: matrix.config.music == 'ON'
        run: |
          python3 -m venv music-venv
          source music-venv/bin/activate
          python3 -m pip install mpi4py "cython<3" "numpy<2"
          sudo mkdir -p $MUSIC_INSTALL_DIR
          sudo chown -R $USER $MUSIC_INSTALL_DIR
          # Stable build: https://github.com/INCF/MUSIC/archive/refs/heads/switch-to-MPI-C-interface.zip  @ f33b66ea9348888eed1761738ab48c23ffc8a0d0
          curl -L -o MUSIC.zip https://codeload.github.com/INCF/MUSIC/zip/f33b66ea9348888eed1761738ab48c23ffc8a0d0
          unzip MUSIC.zip && mv MUSIC-* MUSIC && cd MUSIC
          ./autogen.sh
          # workaround for MUSIC on MacOS 12
          if [[ "${{matrix.os}}" == "macOS-12" ]]; then
            MPI_CXXFLAGS="-g" MPI_CFLAGS="-g" MPI_LDFLAGS="-g" CC=mpicc CXX=mpicxx ./configure --with-python-sys-prefix --prefix=$MUSIC_INSTALL_DIR --disable-anysource
          else
            ./configure --with-python-sys-prefix --prefix=$MUSIC_INSTALL_DIR --disable-anysource
          fi
          make -j install
          deactivate
        working-directory: ${{runner.temp}}

      - name: Register gcc problem matcher
        if: ${{matrix.config.flag_warnings == 'ON'}}
        run: echo "::add-matcher::.github/problem-matchers/gcc.json"

      - name: Register sanitizer problem matcher
        if: ${{matrix.config.sanitizer}}
        run: echo "::add-matcher::.github/problem-matchers/${{matrix.config.sanitizer}}.json"

      - name: Hash config dictionary
        run: |
          cat << EOF > matrix.json
          ${{toJSON(matrix.config)}}
          EOF
          echo matrix.config JSON:
          cat matrix.json
          echo -----

      - name: Restore compiler cache
        uses: actions/cache@v4
        with:
          path: ${{runner.workspace}}/ccache
          save-always: true
          key: ${{matrix.os}}-${{hashfiles('matrix.json')}}-${{github.ref}}-${{github.sha}}
          restore-keys: |
            ${{matrix.os}}-${{hashfiles('matrix.json')}}-${{github.ref}}-
            ${{matrix.os}}-${{hashfiles('matrix.json')}}-

      - name: Build and Test
        id: build-test
        shell: bash
        working-directory: ${{runner.workspace}}/nrn
        run:  |
          # OS related
          if [ "$RUNNER_OS" == "Linux" ]; then
            export ${MATRIX_EVAL};
            export SHELL="/bin/bash"
          else
            export CXX=${CXX:-g++};
            export CC=${CC:-gcc};
          fi
          if [ "$RUNNER_OS" == "macOS" ]; then
            # TODO - this is a workaround that was implemented for Azure being reported as getting stuck.
            # However it does not get stuck: neuron module not found and script goes to interpreter, seeming stuck.
            # This needs to be addressed and SKIP_EMBEDED_PYTHON_TEST logic removed everywhere.
            export SKIP_EMBEDED_PYTHON_TEST="true"
          fi

          # Python setup
          export PYTHONPATH=$PYTHONPATH:$INSTALL_DIR/lib/python/
          # Python setup
          export PYTHON_MIN=$(command -v $PYTHON_MIN_NAME);
          export PYTHON_MAX=$(command -v $PYTHON_MAX_NAME);
          export PYTHON=$PYTHON_MAX
          if [ "$RUNNER_OS" == "macOS" ]; then
            # Python is not installed as a framework, so we need to writ 'backend: TkAgg' to `matplotlibrc`.
            # Since we are in a virtual environment, we cannot use `$HOME/matplotlibrc`
            # The following solution is generic and relies on `matplotlib.__file__` to know where to append backend setup.
            $PYTHON -c "import os,matplotlib; f =open(os.path.join(os.path.dirname(matplotlib.__file__), 'mpl-data/matplotlibrc'),'a'); f.write('backend: TkAgg');f.close();"
          fi;

          # Some logging
          echo $LANG
          echo $LC_ALL
          python3 -c 'import os,sys; os.set_blocking(sys.stdout.fileno(), True)'
          cmake --version

          # different builds with CMake
          if [[ "$BUILD_MODE" == "cmake" ]]; then
              cmake_args=(-G Ninja)
              # Sanitizer-specific setup
              if [[ -n "${{matrix.config.sanitizer}}" ]]; then
                CC=$(command -v clang-14)
                CXX=$(command -v clang++-14)
                symbolizer_path=$(realpath $(command -v llvm-symbolizer-14))
                echo symbolizer_path=${symbolizer_path}
                cmake_args+=(-DCMAKE_BUILD_TYPE=Custom \
                             -DCMAKE_C_FLAGS="-O1 -g -Wno-writable-strings" \
                             -DCMAKE_CXX_FLAGS="-O1 -g -Wno-writable-strings" \
                             -DLLVM_SYMBOLIZER_PATH="${symbolizer_path}" \
                             -DNRN_SANITIZERS=$(echo ${{matrix.config.sanitizer}} | sed -e 's/-/,/g'))
              fi
              cmake_args+=(-DCMAKE_C_COMPILER="${CC}" \
                           -DCMAKE_C_COMPILER_LAUNCHER=ccache \
                           -DCMAKE_CXX_COMPILER="${CXX}" \
                           -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
                           -DCMAKE_INSTALL_PREFIX="${INSTALL_DIR}" \
                           -DNRN_ENABLE_TESTS=ON \
                           ${{matrix.config.cmake_option}})
              if [[ "$NRN_ENABLE_PYTHON_DYNAMIC" == "ON" ]]; then
                cmake_args+=(-DNRN_ENABLE_PYTHON=ON \
                             -DNRN_ENABLE_PYTHON_DYNAMIC=ON \
                             -DNRN_PYTHON_DYNAMIC="${PYTHON_MIN};${PYTHON_MAX}" \
                             -DNRN_ENABLE_CORENEURON=ON)
              else
                cmake_args+=(-DPYTHON_EXECUTABLE="${PYTHON}")
              fi
              if [[ "$NRN_ENABLE_MUSIC" == "ON" ]]; then
                cmake_args+=(-DNRN_ENABLE_MUSIC=ON \
                             -DCMAKE_PREFIX_PATH=${MUSIC_INSTALL_DIR} \
                             -DMUSIC_ROOT=${MUSIC_INSTALL_DIR})
              fi
              # Enable more warnings in the builds whose compiler warnings we
              # highlight in the GitHub UI
              if [[ "${{matrix.config.flag_warnings}}" == "ON" ]]; then
                cmake_args+=(-DNRN_EXTRA_CXX_FLAGS="-Wall \
                                -Wno-char-subscripts \
                                -Wno-unknown-pragmas \
                                -Wno-unused-variable \
                                -Wno-unused-function \
                                -Wno-unused-but-set-variable \
                                -Wno-reorder \
                                -Wno-sign-compare \
                                -Wno-strict-aliasing \
                                -Wno-missing-braces \
                                -Wno-mismatched-tags" \
                             -DNRN_EXTRA_MECH_CXX_FLAGS="")
              fi
              mkdir build && cd build
              echo "Building with: ${cmake_args[@]}"
              cmake .. "${cmake_args[@]}"
              if ccache --version | grep -E '^ccache version 4\.(4|4\.1)$'
              then
                echo "------- Disable ccache direct mode -------"
                # https://github.com/ccache/ccache/issues/935
                export CCACHE_NODIRECT=1
              fi
              ccache -z
              # Older versions don't support -v (verbose)
              ccache -vs 2>/dev/null || ccache -s
              cmake --build . --parallel
              ccache -vs 2>/dev/null || ccache -s
              if [ "$RUNNER_OS" == "macOS" ]
              then
                echo $'[install]\nprefix='>src/nrnpython/setup.cfg;
              fi
              if [[ "$NRN_ENABLE_PYTHON_DYNAMIC" == "ON" ]]; then
                echo "--RUNNING BASIC TESTS FROM BUILD DIR--"
                for python in "${PYTHON_MIN}" "${PYTHON_MAX}"
                do
                  echo "Using ${python}"
                  NEURONHOME="${PWD}/share/nrn" \
                    PYTHONPATH="${PWD}/lib/python" \
                    PATH="${PWD}/bin" \
                    LD_LIBRARY_PATH="${PWD}/lib:${LD_LIBRARY_PATH}" \
                    DYLD_LIBRARY_PATH="${PWD}/lib:${DYLD_LIBRARY_PATH}" \
                    "${python}" -c "from neuron import h; import neuron; neuron.test()"
                done
              fi
              ctest --output-on-failure
              cmake --build . --target install
              export PATH="${INSTALL_DIR}/bin:${PATH}"
              if [[ -f "${INSTALL_DIR}/bin/nrn-enable-sanitizer" ]]; then
                echo --- bin/nrn-enable-sanitizer ---
                cat bin/nrn-enable-sanitizer
                echo ---
                nrn_enable_sanitizer=${INSTALL_DIR}/bin/nrn-enable-sanitizer
                nrn_enable_sanitizer_preload="${nrn_enable_sanitizer} --preload"
              else
                echo nrn-enable-sanitizer not found, not using it
              fi
          elif [[ "$BUILD_MODE" == "setuptools" ]]; then
            ./packaging/python/build_wheels.bash CI;
          fi;

          # basic test for cmake when python is not disabled
          if [[ "$BUILD_MODE" == "cmake" && ! "${cmake_args[*]}" =~ "NRN_ENABLE_PYTHON=OFF" ]]; then
            $PYTHON --version && ${nrn_enable_sanitizer_preload} $PYTHON -c 'import neuron; neuron.test()'
          fi;

          # test neurondemo with cmake
          if [[ "$BUILD_MODE" != "setuptools" ]]; then
            ${nrn_enable_sanitizer} neurondemo -nogui -c 'demo(4)' -c 'run()' -c 'quit()'
          fi;

          # with cmake dynamic check python_min and python_max together
          if [[ "$BUILD_MODE" == "cmake" && "$NRN_ENABLE_PYTHON_DYNAMIC" == "ON" ]]; then
            ${nrn_enable_sanitizer_preload} $PYTHON_MAX -c 'import neuron; neuron.test()'
            ${nrn_enable_sanitizer_preload} $PYTHON_MIN -c 'import neuron; neuron.test()'
          fi;

          # run rxd tests manually if rxd is enabled *and CoreNEURON is
          # disabled -- otherwise hh-related tests fail
          if [[ "$BUILD_MODE" == "cmake" \
                && ! "${cmake_args[*]}" =~ "NRN_ENABLE_RX3D=OFF" \
                && ! "${cmake_args[*]}" =~ "NRN_ENABLE_CORENEURON=ON" ]]; then
            ${nrn_enable_sanitizer_preload} $PYTHON ../share/lib/python/neuron/rxdtests/run_all.py
          fi;

          if [ "$BUILD_MODE" == "setuptools" ]; then
            neuron_wheel=wheelhouse/NEURON*.whl;
            # test with virtual environment
            ${nrn_enable_sanitizer_preload} ./packaging/python/test_wheels.sh $PYTHON $neuron_wheel
            # test with global installation
            ${nrn_enable_sanitizer_preload} ./packaging/python/test_wheels.sh $PYTHON $neuron_wheel false
          fi;
        env:
          BUILD_MODE: ${{ matrix.config.build_mode }}
          CCACHE_BASEDIR: ${{runner.workspace}}/nrn
          CCACHE_DIR: ${{runner.workspace}}/ccache
          NRN_ENABLE_PYTHON_DYNAMIC : ${{ matrix.config.python_dynamic }}
          NRN_ENABLE_MUSIC: ${{ matrix.config.music }}
          PYTHON_MIN_NAME: "python${{ env.PY_MIN_VERSION }}"
          PYTHON_MAX_NAME: "python${{ env.PY_MAX_VERSION }}"
          INSTALL_DIR : ${{ runner.workspace }}/install
          MATRIX_EVAL: ${{ matrix.config.matrix_eval }}

      # This step will set up an SSH connection on tmate.io for live debugging.
      # To enable it, you have to:
      #   * add 'live-debug-ci' to your PR title
      #   * push something to your PR branch (note that just re-running the pipeline disregards the title update)
      - name: live debug session on failure (manual steps required, check `.github/neuron-ci.yml`)
        if: failure() && contains(github.event.pull_request.title, 'live-debug-ci')
        uses: mxschmitt/action-tmate@v3

  # see https://github.com/orgs/community/discussions/26822
  final:
    name: Final CI
    needs: [ci]
    if: ${{ always() }}
    runs-on: ubuntu-latest
    steps:
      - name: Check ci matrix all done
        if: >-
          ${{
               contains(needs.*.result, 'failure')
            || contains(needs.*.result, 'cancelled')
            || contains(needs.*.result, 'skipped')
          }}
        run: exit 1
