diff --git a/.github/actions/build-mpas/action.yml b/.github/actions/build-mpas/action.yml new file mode 100644 index 0000000000..4b58e1abff --- /dev/null +++ b/.github/actions/build-mpas/action.yml @@ -0,0 +1,213 @@ +name: 'Build MPAS' +description: 'Build MPAS-Atmosphere with specified compiler configuration' + +inputs: + compiler: + description: 'Compiler family (gcc, nvhpc, oneapi, etc.)' + required: true + core: + description: 'MPAS core to build (atmosphere, init_atmosphere)' + required: false + default: 'atmosphere' + use-pio: + description: 'Use PIO library (true/false)' + required: false + default: 'false' + openacc: + description: 'Enable OpenACC (true/false)' + required: false + default: 'false' + precision: + description: 'Floating-point precision (single or double)' + required: false + default: 'single' + build-timeout: + description: 'Build timeout in minutes' + required: false + default: '20' + make-target: + description: 'Make target to build. Empty = look up MAKE_TARGET_ in ci-config.env' + required: false + default: '' + make-flags: + description: 'Extra make arguments for this build (e.g. "OPENMP=true"), added after the compiler defaults from ci-config.env' + required: false + default: '' + use-target-arch: + description: 'NVHPC only: add NVHPC_TARGET_ARCH from ci-config.env to the build flags (true/false). Use false for performance runs.' + required: false + default: 'true' + clean-core: + description: 'Run "make clean" for this core before building (use when a different core was built earlier in the same source tree; empty = no clean)' + required: false + default: '' + log-file: + description: 'Write the build output to this file as well as the job log (empty = no file)' + required: false + default: '' + +outputs: + executable: + description: 'Path to built executable (atmosphere_model or init_atmosphere_model)' + value: ${{ steps.build.outputs.executable }} + +runs: + using: 'composite' + steps: + - name: Build MPAS-A + id: build + shell: bash + env: + INPUT_MAKE_TARGET: ${{ inputs.make-target }} + INPUT_MAKE_FLAGS: ${{ inputs.make-flags }} + run: | + # Source container environment if available + # This sets NETCDF, PNETCDF, PIO, and other library paths + if [ -f /container/config_env.sh ]; then + echo "Sourcing container environment from /container/config_env.sh" + source /container/config_env.sh + fi + + # MPAS Makefile uses `cpp` for Registry preprocessing. + # Some containers (e.g. openSUSE Leap) don't ship it separately. + if ! command -v cpp &>/dev/null; then + echo "cpp not found, installing..." + if command -v zypper &>/dev/null; then + zypper install -y --no-recommends cpp + elif command -v dnf &>/dev/null; then + dnf install -y cpp + elif command -v apt-get &>/dev/null; then + apt-get update && apt-get install -y cpp + fi + fi + + # Map compiler input to COMPILER_FAMILY if not already set + # The container should set this, but we provide a fallback + if [ -z "${COMPILER_FAMILY}" ]; then + case "${{ inputs.compiler }}" in + gcc|gfortran|gnu) export COMPILER_FAMILY="gcc" ;; + nvhpc|nvfortran) export COMPILER_FAMILY="nvhpc" ;; + oneapi|intel|ifx) export COMPILER_FAMILY="oneapi" ;; + llvm|flang|clang) export COMPILER_FAMILY="clang" ;; + *) export COMPILER_FAMILY="${{ inputs.compiler }}" ;; + esac + fi + + # Set up I/O configuration + if [ "${{ inputs.use-pio }}" = "true" ]; then + export PIO_ROOT=${PIO_ROOT:-/container/pio} + export USE_PIO2=true + else + unset PIO + export USE_PIO2=false + fi + + # Set up accelerator configuration + if [ "${{ inputs.openacc }}" = "true" ]; then + export OPENACC=true + fi + + echo "Build configuration:" + echo " Core: ${{ inputs.core }}" + echo " Compiler input: ${{ inputs.compiler }}" + echo " COMPILER_FAMILY: ${COMPILER_FAMILY}" + echo " NETCDF: ${NETCDF:-not set}" + echo " PNETCDF: ${PNETCDF:-not set}" + echo " USE_PIO2: ${USE_PIO2}" + echo " OPENACC: ${OPENACC:-false}" + echo " PRECISION: ${{ inputs.precision }}" + + # Settings from .github/ci-config.env — edit that file to change targets/workarounds + CI_CONFIG="${GITHUB_WORKSPACE}/.github/ci-config.env" + if [ -f "${CI_CONFIG}" ]; then + source "${CI_CONFIG}" + fi + + # Make target: the make-target input, else MAKE_TARGET_ from ci-config.env + MAKE_TARGET="${INPUT_MAKE_TARGET}" + if [ -z "${MAKE_TARGET}" ]; then + VARNAME="MAKE_TARGET_${COMPILER_FAMILY}" + MAKE_TARGET="${!VARNAME}" + fi + if [ -z "${MAKE_TARGET}" ]; then + echo "::error::No make target for COMPILER_FAMILY=${COMPILER_FAMILY}. Add MAKE_TARGET_${COMPILER_FAMILY}= to .github/ci-config.env" + exit 1 + fi + + case "${{ inputs.core }}" in + atmosphere) EXE="atmosphere_model" ;; + init_atmosphere) EXE="init_atmosphere_model" ;; + *) echo "::error::Unsupported core '${{ inputs.core }}' (atmosphere, init_atmosphere)"; exit 1 ;; + esac + + if [ "${COMPILER_FAMILY}" = "nvhpc" ]; then + ARCH="" + if [ "${{ inputs.use-target-arch }}" = "true" ]; then + ARCH="${NVHPC_TARGET_ARCH}" + fi + if [ -n "${ARCH}" ]; then + for FLAG in FFLAGS_OPT CFLAGS_OPT CXXFLAGS_OPT LDFLAGS_OPT; do + if ! sed -n "/^${MAKE_TARGET}:/,/^[a-z]/p" Makefile | grep -qF "\"${FLAG} = "; then + echo "::error::Could not locate ${FLAG} in the '${MAKE_TARGET}' Makefile target" + exit 1 + fi + # Skip if already applied (the action can run twice in one source tree) + if sed -n "/^${MAKE_TARGET}:/,/^[a-z]/p" Makefile | grep -qF "\"${FLAG} = ${ARCH} "; then + continue + fi + sed -i "/^${MAKE_TARGET}:/,/^[a-z]/ s/\"${FLAG} = /\"${FLAG} = ${ARCH} /" Makefile + done + fi + fi + + # Compiler defaults from ci-config.env (_EXTRA_MAKE_FLAGS, family in + # upper case), then the make-flags input + FLAGS_VAR="${COMPILER_FAMILY^^}_EXTRA_MAKE_FLAGS" + EXTRA_MAKE_FLAGS="${!FLAGS_VAR}" + if [ -n "${INPUT_MAKE_FLAGS}" ]; then + EXTRA_MAKE_FLAGS="${EXTRA_MAKE_FLAGS:+${EXTRA_MAKE_FLAGS} }${INPUT_MAKE_FLAGS}" + fi + + echo " Make target: ${MAKE_TARGET}" + echo " Extra make flags: ${EXTRA_MAKE_FLAGS:-}" + echo " Parallel jobs: ${MAKE_J_PROCS:-$(nproc)}" + + PRECISION_FLAG="" + if [ "${{ inputs.precision }}" = "double" ]; then + PRECISION_FLAG="PRECISION=double" + fi + + CLEAN_CMD="" + if [ -n "${{ inputs.clean-core }}" ]; then + CLEAN_CMD="make clean CORE=${{ inputs.clean-core }}" + fi + MAKE_CMD="timeout ${{ inputs.build-timeout }}m make ${MAKE_TARGET} CORE=${{ inputs.core }} ${EXTRA_MAKE_FLAGS} ${PRECISION_FLAG} --jobs ${MAKE_J_PROCS:-$(nproc)}" + + LOG_FILE="${{ inputs.log-file }}" + set -o pipefail + # Tool versions and the exact commands (job log + log file) + PRINT_ENV="${GITHUB_WORKSPACE}/.github/actions/build-mpas/print-build-env.sh" + if [ ! -f "${PRINT_ENV}" ]; then + echo "::error::${PRINT_ENV} not found" + exit 1 + fi + export COMPILER_FAMILY MAKE_TARGET ARCH CLEAN_CMD MAKE_CMD + CORE="${{ inputs.core }}" USE_PIO="${{ inputs.use-pio }}" PRECISION="${{ inputs.precision }}" \ + bash "${PRINT_ENV}" | tee "${LOG_FILE:-/dev/null}" + if [ -n "${CLEAN_CMD}" ]; then + ${CLEAN_CMD} 2>&1 | tee -a "${LOG_FILE:-/dev/null}" + fi + ${MAKE_CMD} 2>&1 | tee -a "${LOG_FILE:-/dev/null}" + + echo "executable=$(pwd)/${EXE}" >> "$GITHUB_OUTPUT" + + - name: Verify executable + shell: bash + run: | + EXE="${{ steps.build.outputs.executable }}" + if [ ! -f "${EXE}" ]; then + echo "::error::${EXE} not found" + exit 1 + fi + ls -la "${EXE}" + file "${EXE}" diff --git a/.github/actions/build-mpas/print-build-env.sh b/.github/actions/build-mpas/print-build-env.sh new file mode 100644 index 0000000000..da81c4d56b --- /dev/null +++ b/.github/actions/build-mpas/print-build-env.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Print the tool versions and exact commands for an MPAS build. +# Called by action.yml; the output goes to the job log and the build log. +# +# Environment variables set by the action: +# COMPILER_FAMILY gcc, oneapi, nvhpc, ... +# MAKE_TARGET make target +# CORE atmosphere or init_atmosphere +# USE_PIO true or false +# PRECISION single or double +# ARCH NVHPC target arch flag that was applied (empty = none) +# CLEAN_CMD clean command run before the build (empty = none) +# MAKE_CMD the build command +# NETCDF, PNETCDF and PIO_ROOT come from the container environment. + +# First non-empty line of a command's output, or "not found" +v() { command -v "$1" >/dev/null 2>&1 || { echo "not found"; return 0; }; { "$@" 2>&1 || true; } | grep -m1 . || true; } +case "${COMPILER_FAMILY}" in + gcc) FC_SER=gfortran; CC_SER=gcc; FC_MPI=mpif90 ;; + oneapi) FC_SER=ifx; CC_SER=icx; FC_MPI=mpifort ;; + nvhpc) FC_SER=nvfortran; CC_SER=nvc; FC_MPI=mpifort ;; + *) FC_SER=mpifort; CC_SER=mpicc; FC_MPI=mpifort ;; +esac +PATH="${NETCDF:+${NETCDF}/bin:}${PNETCDF:+${PNETCDF}/bin:}${PATH}" +# MPI implementation: MPICH or Open MPI +if command -v mpichversion >/dev/null 2>&1; then + MPI_VERSION="$(v mpichversion)"; MPI_SHOW="-show" +elif command -v ompi_info >/dev/null 2>&1; then + MPI_VERSION="$(v ompi_info --version)"; MPI_SHOW="--showme" +else + MPI_VERSION="not found"; MPI_SHOW="-show" +fi +echo "==== Build environment ====" +echo "Core: ${CORE} (${COMPILER_FAMILY}, make target ${MAKE_TARGET})" +echo "Fortran: $(v ${FC_SER} --version)" +echo "C: $(v ${CC_SER} --version)" +echo "MPI: ${MPI_VERSION}" +echo "MPI wrapper: $(v ${FC_MPI} ${MPI_SHOW} | cut -c1-160)" +echo "NetCDF-C: $(v nc-config --version)" +echo "NetCDF-Fortran: $(v nf-config --version)" +echo "PnetCDF: $(v pnetcdf-config --version)" +if [ "${USE_PIO}" = "true" ]; then + echo "I/O layer: PIO (${PIO_ROOT})" +else + echo "I/O layer: SMIOL (in-tree)" +fi +echo "Precision: ${PRECISION}" +echo "Container OS: $( (. /etc/os-release && echo "${PRETTY_NAME}") 2>/dev/null || echo unknown)" +if [ "${COMPILER_FAMILY}" = "nvhpc" ]; then + if [ -n "${ARCH}" ]; then + echo "Makefile edit: added ${ARCH} to FFLAGS_OPT, CFLAGS_OPT, CXXFLAGS_OPT, LDFLAGS_OPT" + else + echo "Makefile edit: none (use-target-arch is false)" + fi +fi +if [ -n "${CLEAN_CMD}" ]; then + echo "Clean command: ${CLEAN_CMD}" +fi +echo "Build command: ${MAKE_CMD}" +echo "==== End build environment ====" diff --git a/.github/actions/resolve-container/action.yml b/.github/actions/resolve-container/action.yml new file mode 100644 index 0000000000..fccafffd76 --- /dev/null +++ b/.github/actions/resolve-container/action.yml @@ -0,0 +1,65 @@ +name: 'Resolve Container Image' +description: 'Assemble a container image name from ci-config.env templates' + +inputs: + compiler: + description: 'Compiler family (gcc, nvhpc, oneapi)' + required: true + mpi: + description: 'MPI implementation (openmpi, mpich)' + required: true + gpu: + description: 'GPU variant (empty string, or "cuda")' + required: false + default: '' + +outputs: + image: + description: 'Fully qualified container image name' + value: ${{ steps.resolve.outputs.image }} + +runs: + using: 'composite' + steps: + - name: Resolve container image + id: resolve + shell: bash + run: | + CI_CONFIG="${GITHUB_WORKSPACE}/.github/ci-config.env" + if [ ! -f "${CI_CONFIG}" ]; then + echo "::error::ci-config.env not found at ${CI_CONFIG}" + exit 1 + fi + source "${CI_CONFIG}" + + COMPILER="${{ inputs.compiler }}" + MPI="${{ inputs.mpi }}" + GPU="${{ inputs.gpu }}" + + # Resolve compiler name (fallback to raw value if no mapping) + comp_var="CONTAINER_COMPILER_${COMPILER}" + COMPILER_NAME="${!comp_var:-$COMPILER}" + + # Resolve MPI name (fallback to raw value if no mapping) + mpi_var="CONTAINER_MPI_${MPI}" + MPI_NAME="${!mpi_var:-$MPI}" + + # Pick template: GPU vs CPU, then check for per-compiler override + if [ -n "${GPU}" ]; then + gpu_override="CONTAINER_IMAGE_GPU_${COMPILER}" + TEMPLATE="${!gpu_override:-$CONTAINER_IMAGE_GPU}" + else + cpu_override="CONTAINER_IMAGE_${COMPILER}" + TEMPLATE="${!cpu_override:-$CONTAINER_IMAGE}" + fi + + # Substitute placeholders + IMAGE="${TEMPLATE//\{compiler\}/$COMPILER_NAME}" + IMAGE="${IMAGE//\{mpi\}/$MPI_NAME}" + + echo "Resolved container image: ${IMAGE}" + echo " Compiler: ${COMPILER} → ${COMPILER_NAME}" + echo " MPI: ${MPI} → ${MPI_NAME}" + echo " GPU: ${GPU:-none}" + + echo "image=${IMAGE}" >> $GITHUB_OUTPUT diff --git a/.github/ci-config.env b/.github/ci-config.env new file mode 100644 index 0000000000..12f971b1c4 --- /dev/null +++ b/.github/ci-config.env @@ -0,0 +1,47 @@ +# .github/ci-config.env — Central CI configuration +# +# Sourced by workflows and composite actions. Edit here to change +# containers, compilers, or build workarounds. + + +# ── Container images ────────────────────────────── +# {compiler} and {mpi} are replaced by resolve-container. +# Tags: https://hub.docker.com/r/ncarcisl/hpcdev-x86_64/tags + +CONTAINER_IMAGE="docker.io/ncarcisl/hpcdev-x86_64:almalinux9-{compiler}-{mpi}-26.02" + +# Per-compiler override: CONTAINER_IMAGE_{compiler}="template" + + +# ── Container name mappings ─────────────────────── +# Map CI compiler/MPI names to Docker Hub tag strings when they differ. + +CONTAINER_COMPILER_gcc=gcc14 +# CONTAINER_MPI_mpich=mpich3 + + +# ── Makefile target mapping ─────────────────────── +# MAKE_TARGET_{compiler} → MPAS Makefile target name. + +MAKE_TARGET_gcc=gfortran +MAKE_TARGET_nvhpc=nvhpc +MAKE_TARGET_oneapi=intel +MAKE_TARGET_clang=llvm + + +# ── Compiler workarounds ───────────────────────── + +# Target arch flag added to the NVHPC build flags. -tp=px is portable +# (CI workflows can sometimes build and run on different hosts). A build +# turns it off with use-target-arch: 'false' on the build-mpas action. +NVHPC_TARGET_ARCH=-tp=px + +# Extra make arguments for every build with a compiler: +# _EXTRA_MAKE_FLAGS, with the family in upper case +# (GCC, NVHPC, ONEAPI, CLANG). A single build can add more with +# the make-flags input on the build-mpas action. +# +# Disable MPI Fortran 2008 bindings (not supported by MPICH +# builds in current containers). +NVHPC_EXTRA_MAKE_FLAGS="MPAS_MPI_F08=0" +ONEAPI_EXTRA_MAKE_FLAGS="MPAS_MPI_F08=0" diff --git a/.github/workflows/_build-compiler.yml b/.github/workflows/_build-compiler.yml new file mode 100644 index 0000000000..821f3e708a --- /dev/null +++ b/.github/workflows/_build-compiler.yml @@ -0,0 +1,143 @@ +# Reusable workflow: build the MPAS-A cores for a single compiler + MPI. +# The I/O layer (smiol or pio) and precision (single or double) are inputs; +# the defaults are smiol and double. Both cores are built in one source +# tree, in sequence: atmosphere first, then init_atmosphere. +# Called by per-compiler workflows (e.g. build-gcc-mpich.yml). +# Build only: no model runs and no validation. + +name: _build-compiler + +permissions: + contents: read + +on: + workflow_call: + inputs: + compiler: + description: 'Compiler family (gcc, nvhpc, oneapi)' + required: true + type: string + mpi: + description: 'MPI implementation (mpich, openmpi)' + required: true + type: string + io: + description: 'I/O layer (smiol, pio)' + required: false + type: string + default: 'smiol' + precision: + description: 'Floating-point precision (single, double)' + required: false + type: string + default: 'double' + +jobs: + config: + name: Resolve Config + runs-on: ubuntu-26.04 + timeout-minutes: 10 + outputs: + image: ${{ steps.container.outputs.image }} + sha: ${{ steps.sha.outputs.short }} + steps: + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + with: + sparse-checkout: .github + persist-credentials: false + - uses: ./.github/actions/resolve-container + id: container + with: + compiler: ${{ inputs.compiler }} + mpi: ${{ inputs.mpi }} + - name: Check inputs + shell: bash + env: + IO: ${{ inputs.io }} + PRECISION: ${{ inputs.precision }} + run: | + case "${IO}" in smiol|pio) ;; *) echo "::error::io must be smiol or pio (got '${IO}')"; exit 1 ;; esac + case "${PRECISION}" in single|double) ;; *) echo "::error::precision must be single or double (got '${PRECISION}')"; exit 1 ;; esac + - name: Short commit SHA (PR head for pull requests) + id: sha + shell: bash + env: + SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: echo "short=${SHA::7}" >> "$GITHUB_OUTPUT" + + build: + needs: config + name: Build atmosphere + init_atmosphere (${{ inputs.compiler }}, ${{ inputs.mpi }}, ${{ inputs.io }}, ${{ inputs.precision }}) + runs-on: ubuntu-26.04 + timeout-minutes: 60 + container: + image: ${{ needs.config.outputs.image }} + + steps: + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + with: + submodules: 'true' + persist-credentials: false + + # atmosphere first: it is the more likely to fail + - name: Build atmosphere (${{ inputs.precision }} precision, ${{ inputs.io }}) + id: atmosphere + uses: ./.github/actions/build-mpas + with: + compiler: ${{ inputs.compiler }} + core: atmosphere + use-pio: ${{ inputs.io == 'pio' && 'true' || 'false' }} + precision: ${{ inputs.precision }} + log-file: build-atmosphere.log + + # Same source tree: clean atmosphere, then build init_atmosphere. + # Skipped (not failed) if the atmosphere build failed. + - name: Build init_atmosphere (${{ inputs.precision }} precision, ${{ inputs.io }}) + id: init_atmosphere + uses: ./.github/actions/build-mpas + with: + compiler: ${{ inputs.compiler }} + core: init_atmosphere + clean-core: atmosphere + use-pio: ${{ inputs.io == 'pio' && 'true' || 'false' }} + precision: ${{ inputs.precision }} + log-file: build-init_atmosphere.log + + - name: Summary + if: always() + shell: bash + run: | + summarize() { # core, step outcome, executable path + local core="$1" outcome="$2" exe="$3" log="build-$1.log" + if [ "${outcome}" = "success" ]; then + echo "✅ **${core}**: built $(basename "${exe}") (${{ inputs.compiler }}, ${{ inputs.mpi }}, ${{ inputs.io }}, ${{ inputs.precision }})" + elif [ "${outcome}" = "skipped" ]; then + echo "⏭️ **${core}**: skipped" + else + echo "❌ **${core}**: build failed (${{ inputs.compiler }}, ${{ inputs.mpi }}, ${{ inputs.io }}, ${{ inputs.precision }})" + fi + if [ -f "${log}" ]; then + echo "
${core}: build environment and command" + echo + echo '```' + sed -n '/^==== Build environment/,/^==== End build environment/p' "${log}" + echo '```' + echo "
" + echo + fi + } + { + summarize atmosphere "${{ steps.atmosphere.outcome }}" "${{ steps.atmosphere.outputs.executable }}" + summarize init_atmosphere "${{ steps.init_atmosphere.outcome }}" "${{ steps.init_atmosphere.outputs.executable }}" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload build logs + if: always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6 + with: + name: build-logs-${{ needs.config.outputs.sha }}-${{ inputs.compiler }}-${{ inputs.mpi }}-${{ inputs.io }}-${{ inputs.precision }} + path: | + build-atmosphere.log + build-init_atmosphere.log + if-no-files-found: ignore + retention-days: 7 diff --git a/.github/workflows/build-gcc-mpich.yml b/.github/workflows/build-gcc-mpich.yml new file mode 100644 index 0000000000..993159a31f --- /dev/null +++ b/.github/workflows/build-gcc-mpich.yml @@ -0,0 +1,18 @@ +# BUILD: GNU + MPICH (CPU) +# Builds the atmosphere and init_atmosphere cores with SMIOL. + +name: "BUILD: GNU + MPICH (CPU)" + +on: + workflow_dispatch: + push: + branches: [master] + pull_request: + branches: [develop] + +jobs: + build: + uses: ./.github/workflows/_build-compiler.yml + with: + compiler: gcc + mpi: mpich diff --git a/.github/workflows/build-intel-mpich.yml b/.github/workflows/build-intel-mpich.yml new file mode 100644 index 0000000000..fbc026d1e1 --- /dev/null +++ b/.github/workflows/build-intel-mpich.yml @@ -0,0 +1,18 @@ +# BUILD: Intel OneAPI + MPICH (CPU) +# Builds the atmosphere and init_atmosphere cores with SMIOL. + +name: "BUILD: Intel OneAPI + MPICH (CPU)" + +on: + workflow_dispatch: + push: + branches: [master] + pull_request: + branches: [develop] + +jobs: + build: + uses: ./.github/workflows/_build-compiler.yml + with: + compiler: oneapi + mpi: mpich diff --git a/.github/workflows/build-nvhpc-mpich.yml b/.github/workflows/build-nvhpc-mpich.yml new file mode 100644 index 0000000000..f4c0df9599 --- /dev/null +++ b/.github/workflows/build-nvhpc-mpich.yml @@ -0,0 +1,18 @@ +# BUILD: NVHPC + MPICH (CPU) +# Builds the atmosphere and init_atmosphere cores with SMIOL. + +name: "BUILD: NVHPC + MPICH (CPU)" + +on: + workflow_dispatch: + push: + branches: [master] + pull_request: + branches: [develop] + +jobs: + build: + uses: ./.github/workflows/_build-compiler.yml + with: + compiler: nvhpc + mpi: mpich