diff --git a/.github/actions/build_pypi_dist/action.yml b/.github/actions/build_pypi_dist/action.yml new file mode 100644 index 0000000..5c5af8e --- /dev/null +++ b/.github/actions/build_pypi_dist/action.yml @@ -0,0 +1,361 @@ +# Copyright 2026 NWChemEx-Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Build PyPI Distribution +description: >- + Checkout and build a repository's sdist and/or wheel into dist/, verifying + the metadata and that the wheel installs cleanly. Does NOT publish -- + the calling job must add its own pypa/gh-action-pypi-publish step + afterwards, as a sibling step never nested inside another composite + action: gh-action-pypi-publish's own docker-trampoline image-tag + resolution breaks when called from within a composite action (a known + upstream limitation, actions/runner#2473). Runs on a bare runner + (no `container:`). + +inputs: + cibw_build: + description: >- + A cibuildwheel CIBW_BUILD selector (e.g. cp312-manylinux_x86_64). When + set, the wheel is built with pypa/cibuildwheel instead of `python -m + build`, for a compiled package that needs a manylinux/macOS-portable + wheel. Leave unset for a pure-Python package with no compiled extension + (e.g. NWXCMake itself) -- a package with a CMakeLists.txt should always + set this, since a wheel built directly (not via cibuildwheel) isn't + manylinux-portable and PyPI will reject it. + Intended to be called once per {os, cibw_build} matrix leg from the + calling workflow; the job must NOT run inside a `container:`, since + cibuildwheel manages its own (nested) build containers on Linux. + required: false + default: "" + build_sdist: + description: >- + Whether this invocation ALSO builds the source distribution, in + addition to the wheel. Does not affect the wheel itself -- that is + always built whenever cibw_build is set, regardless of this input. + Only relevant to multi-platform matrices: every leg would build + byte-identical sdist content, and PyPI rejects re-uploading an + existing filename, so set this to "true" on exactly one leg and + "false" on the rest to publish the sdist once instead of once-per-leg. + Ignored (always true) on the plain, non-cibw_build path, since + there's only ever one leg there. + required: false + default: "true" + extra_index_url: + description: >- + Extra package index passed to the post-build `pip install` sanity + check (e.g. https://test.pypi.org/simple/), in addition to the + default index. Needed when this package's runtime dependencies are + other nwx packages that only exist on that extra index (e.g. a + TestPyPI-only publish chain) -- without it, the verification install + can only ever resolve dependencies that are already on the default + index. Leave unset for a package with no such dependencies. + required: false + default: "" + needs_mpi: + description: >- + Whether the cibuildwheel build needs an MPI installation. The + manylinux/macOS cibuildwheel containers are minimal and don't have + one, unlike the ambient dev matrix (see setup_nwx_dev_env). When + "true", installs openmpi3-devel (via EPEL) inside the manylinux + container and puts its bin/ on PATH so CMake's find_package(MPI) + succeeds there, and installs open-mpi via Homebrew on macOS. Ignored + when cibw_build is unset (the plain, non-cibuildwheel path assumes + the ambient runner already has whatever the package needs). + required: false + default: "false" + needs_boost: + description: >- + Whether the cibuildwheel build needs Boost. The manylinux/macOS + cibuildwheel containers are minimal and don't have it. When "true", + installs boost-devel (via EPEL) inside the manylinux container and + boost via Homebrew on macOS. Ignored when cibw_build is unset. + required: false + default: "false" + needs_blas: + description: >- + Whether the cibuildwheel build needs a BLAS/LAPACK installation (e.g. + transitively via GauXC). The manylinux/macOS cibuildwheel containers + are minimal and don't have one. When "true", installs openblas-devel + and lapack-devel (via EPEL) inside the manylinux container, and + openblas via Homebrew on macOS (pointing CMAKE_PREFIX_PATH at it, + since it's keg-only there). Ignored when cibw_build is unset. + required: false + default: "false" + needs_eigen: + description: >- + Whether the cibuildwheel build needs an installed Eigen3 CMake config + (e.g. libint2's own CMakeLists does its own find_package(Eigen3), + independent of and unsatisfied by nwxcmake's own FetchContent'd Eigen + target). The manylinux/macOS cibuildwheel containers don't have one. + When "true", installs eigen3-devel (via EPEL) inside the manylinux + container and eigen via Homebrew on macOS. Ignored when cibw_build is + unset. + required: false + default: "false" + needs_openmp: + description: >- + Whether the cibuildwheel build needs OpenMP (e.g. transitively via + GauXC, which unconditionally find_package(OpenMP)s). Linux's gcc + ships its own OpenMP runtime (libgomp) so no extra provisioning is + needed there; AppleClang has no built-in OpenMP support, so on macOS + this installs libomp via Homebrew and points scikit-build-core's + SKBUILD_CMAKE_ARGS at it (CMake's FindOpenMP can't locate Homebrew's + keg-only libomp on its own). Ignored when cibw_build is unset. + required: false + default: "false" + +runs: + using: composite + steps: + - name: Checkout Source + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Install Packaging Tools + shell: bash + run: pip install --quiet build twine + + # Uses the normal (isolated) build-system.requires resolution, i.e. the + # same git-fetched nwxcmake a real `pip install` from PyPI would use, so + # this publishes exactly what a fresh `pip install` would build. + - name: Build Source Distribution + if: inputs.build_sdist == 'true' + shell: bash + run: python -m build --sdist --outdir dist + + # Building the wheel from the *extracted sdist* (rather than the repo + # checkout) catches packaging bugs where a file the build needs was + # never included in the sdist manifest -- publishing that wheel + # unbuilt-from-sdist could otherwise ship silently broken. Only applies + # to the plain path: cibuildwheel builds its own way (see below) and + # doesn't need this repo's build-system.requires forwarded a second time. + - name: Build Wheel From Source Distribution + if: inputs.cibw_build == '' + shell: bash + run: | + sdist=$(ls dist/*.tar.gz) + rm -rf sdist_extracted + mkdir sdist_extracted + tar -xzf "${sdist}" -C sdist_extracted --strip-components=1 + + python -m build --wheel --outdir dist sdist_extracted + + # Composes the CIBW_BEFORE_ALL_*/CIBW_ENVIRONMENT_* values in a shell + # step (rather than inline ternary expressions in the cibuildwheel env: + # block below) so needs_mpi and needs_boost can each independently + # contribute a clause without one clobbering the other. + - name: Compute cibuildwheel System Dependencies + if: inputs.cibw_build != '' + shell: bash + run: | + before_linux=() + before_macos=() + env_linux=() + env_macos=("MACOSX_DEPLOYMENT_TARGET=11.0") + + if [ "${{ inputs.needs_mpi }}" = "true" ]; then + # Built from source at the *exact* version Ubuntu 24.04's apt + # package provides (see setup_nwx_dev_env), not EPEL's openmpi3 + # (3.1.3) as before -- OpenMPI's ABI is not guaranteed compatible + # across major versions. EPEL7 (manylinux2014's package repo) has + # no openmpi4 package at all, so this can't be a yum install. Still + # needed even though the wheel no longer vendors MPI (see + # CIBW_REPAIR_WHEEL_COMMAND_LINUX below): the extension must be + # *compiled* against a matching OpenMPI to link correctly against + # whatever system OpenMPI the wheel resolves to at runtime. + # + # --disable-dlopen got MPI_Init's MCA shmem component selection + # working (previously failed: "opal_shmem_base_select failed", + # since auditwheel only bundles a wheel's DT_NEEDED dependencies + # found via ldd, not the dlopen()'d MCA plugin .so files normally + # living under $prefix/lib/openmpi/), but a vendored wheel install + # still crashes one step later: OpenMPI's singleton-mode ORTE + # startup (ess_singleton_module.c) needs to locate the `orted` + # daemon executable itself, and auditwheel/delocate only ever + # bundle shared libraries, never the accompanying bin/ executables + # -- "A system-required executable could not be found". Vendoring + # MPI into the wheel is a dead end; see CIBW_REPAIR_WHEEL_COMMAND + # below for the fix (exclude MPI libs, require system MPI instead). + before_linux+=( + "curl -fsSL https://download.open-mpi.org/release/open-mpi/v4.1/openmpi-4.1.6.tar.gz -o /tmp/openmpi.tar.gz" + "mkdir -p /tmp/openmpi-src" + "tar -xzf /tmp/openmpi.tar.gz -C /tmp/openmpi-src --strip-components=1" + "cd /tmp/openmpi-src && ./configure --prefix=/usr/local/openmpi --disable-mpi-fortran --disable-dlopen" + "cd /tmp/openmpi-src && make -j\$(nproc) install" + ) + env_linux+=("PATH=/usr/local/openmpi/bin:\$PATH" "LD_LIBRARY_PATH=/usr/local/openmpi/lib:\$LD_LIBRARY_PATH") + # auditwheel repair normally bundles every DT_NEEDED lib (incl. + # libmpi/libopen-rte/libopen-pal) into the wheel via + # RPATH-relocation. Excluding them here means the wheel links + # against whatever libmpi.so.40 is on the *runtime* system's + # linker path instead -- the one that ships with a real OpenMPI + # install (incl. its bin/orted), which setup_nwx_dev_env already + # provides ambient (libopenmpi-dev/openmpi-bin) and which end users + # need to install themselves (documented as a runtime requirement). + # + # Exact sonames (libmpi.so.40), NOT a glob (libmpi*): cibuildwheel + # v2.22.0's manylinux2014 image (quay.io/pypa/manylinux2014_x86_64: + # 2024.11.16-1) bakes in auditwheel 6.1.0, whose --exclude does a + # plain `soname in exclude` set-membership check (confirmed by + # reading that exact container's lddtree.py) -- fnmatch/glob + # support for --exclude was only added in a later auditwheel + # release. A glob pattern silently matches nothing and bundles the + # library anyway (reproduced locally via Docker: 'libmpi*' bundled + # libmpi/libopen-rte/libopen-pal same as no --exclude at all; + # 'libmpi.so.40' correctly excluded it, and since libopen-rte/ + # libopen-pal are only reachable *through* libmpi in this tree, + # excluding libmpi.so.40 alone is sufficient -- auditwheel doesn't + # recurse into an excluded library's own dependencies). + { + echo "CIBW_REPAIR_WHEEL_COMMAND_LINUX=auditwheel repair --exclude 'libmpi.so.40' --exclude 'libopen-rte.so.40' --exclude 'libopen-pal.so.40' -w {dest_dir} {wheel}" + } >> "$GITHUB_ENV" + before_macos+=("brew install open-mpi") + # Homebrew's open-mpi (and its own deps: libpmix, libhwloc, + # libevent) are built against the actual runner OS, so their + # dylibs report a minimum target matching it (14.0 on macos-14) -- + # higher than cibuildwheel's default arm64 target (11.0). delocate + # then refuses to tag the repaired wheel as 11.0-compatible, since + # that would be false. Raise the declared target to match what's + # actually bundled. + env_macos=("MACOSX_DEPLOYMENT_TARGET=14.0") + fi + + if [ "${{ inputs.needs_boost }}" = "true" ]; then + # manylinux2014's plain boost-devel is 1.53.0, too old for headers + # some repos need (e.g. boost/container_hash/hash.hpp). EPEL's + # boost169-devel (parallel-installable, doesn't replace the base + # boost-devel) is the newest available via yum there, but installs + # under a versioned, non-default include path, so CMake's FindBoost + # needs to be pointed at it explicitly via BOOST_INCLUDEDIR. + before_linux+=("yum install -y boost169-devel") + env_linux+=("BOOST_INCLUDEDIR=/usr/include/boost169") + before_macos+=("brew install boost") + fi + + if [ "${{ inputs.needs_blas }}" = "true" ]; then + before_linux+=("yum install -y epel-release" "yum install -y openblas-devel lapack-devel") + before_macos+=("brew install openblas") + # Quoted: cibuildwheel's CIBW_ENVIRONMENT parses space-separated + # KEY=VALUE tokens, and the $(brew --prefix ...) substitution's + # arguments contain a space, which would otherwise split it into + # two bogus tokens. + env_macos+=('CMAKE_PREFIX_PATH="$(brew --prefix openblas)"') + fi + + if [ "${{ inputs.needs_eigen }}" = "true" ]; then + before_linux+=("yum install -y epel-release" "yum install -y eigen3-devel") + before_macos+=("brew install eigen") + fi + + if [ "${{ inputs.needs_openmp }}" = "true" ]; then + before_macos+=("brew install libomp") + # AppleClang has no built-in OpenMP; CMake's FindOpenMP can't + # locate Homebrew's keg-only libomp without explicit hints. Quoted + # as a single token for the same reason as CMAKE_PREFIX_PATH above. + env_macos+=('SKBUILD_CMAKE_ARGS="-DOpenMP_C_FLAGS=-Xpreprocessor -fopenmp -I$(brew --prefix libomp)/include;-DOpenMP_C_LIB_NAMES=omp;-DOpenMP_CXX_FLAGS=-Xpreprocessor -fopenmp -I$(brew --prefix libomp)/include;-DOpenMP_CXX_LIB_NAMES=omp;-DOpenMP_omp_LIBRARY=$(brew --prefix libomp)/lib/libomp.dylib"') + fi + + join_and() { + local out="" item + for item in "$@"; do + if [ -z "$out" ]; then out="$item"; else out="$out && $item"; fi + done + echo "$out" + } + + { + echo "CIBW_BEFORE_ALL_LINUX=$(join_and "${before_linux[@]}")" + echo "CIBW_BEFORE_ALL_MACOS=$(join_and "${before_macos[@]}")" + echo "CIBW_ENVIRONMENT_LINUX=${env_linux[*]}" + echo "CIBW_ENVIRONMENT_MACOS=${env_macos[*]}" + } >> "$GITHUB_ENV" + + # cibuildwheel manages its own (manylinux/macOS) build images, so it + # builds straight from the repo checkout rather than an extracted sdist. + - name: Build Wheel With cibuildwheel + if: inputs.cibw_build != '' + uses: pypa/cibuildwheel@v2.22.0 + env: + CIBW_BUILD: ${{ inputs.cibw_build }} + CIBW_BUILD_VERBOSITY: "1" + + - name: Collect cibuildwheel Output + if: inputs.cibw_build != '' + shell: bash + run: | + mkdir -p dist + mv wheelhouse/*.whl dist/ + + - name: Check Packaging Metadata + shell: bash + run: twine check dist/* + + # A cibuildwheel wheel is tagged for one specific CPython (e.g. cp312), + # which usually differs from the ambient "Set up Python" 3.x (latest). + # Installing it there would fail with "not a supported wheel on this + # platform", so re-point PATH at a matching interpreter first. + - name: Determine cibuildwheel Python Version + if: inputs.cibw_build != '' + id: cibw_python_version + shell: bash + run: | + # e.g. "cp312-manylinux_x86_64" -> "3.12" + if [[ "${{ inputs.cibw_build }}" =~ ^cp([0-9])([0-9]+) ]]; then + echo "version=${BASH_REMATCH[1]}.${BASH_REMATCH[2]}" >> "$GITHUB_OUTPUT" + else + echo "::error::Could not parse a Python version out of cibw_build='${{ inputs.cibw_build }}'" + exit 1 + fi + + - name: Set up Python Matching cibuildwheel Target + if: inputs.cibw_build != '' + uses: actions/setup-python@v5 + with: + python-version: ${{ steps.cibw_python_version.outputs.version }} + + # The plain (non-cibw) path's wheel itself is version-agnostic + # (py3-none-any), but a pure-Python package's runtime dependencies can + # include other nwx-ecosystem packages that are compiled and only + # published for one CPython version (cp312, per platform_matrix.yaml's + # release_matrix) -- installing under whatever "Set up Python"'s "3.x" + # resolves to today (e.g. 3.14) would find no matching wheel for those + # and fall back to building them from source, which fails on this bare + # verification venv (no MPI/Boost/etc.). Re-point at 3.12 to match. + - name: Set up Python Matching Ecosystem Release Target + if: inputs.cibw_build == '' + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + # Cheap safety net before publishing to a registry where a bad upload + # can only be yanked, never truly deleted: catch an unimportable wheel + # here rather than after it's live on PyPI. + - name: Verify Wheel Installs + shell: bash + run: | + python -m venv /tmp/build_pypi_dist_venv + . /tmp/build_pypi_dist_venv/bin/activate + wheel=$(ls dist/*.whl) + extra_index_args=() + if [ -n "${{ inputs.extra_index_url }}" ]; then + extra_index_args=(--extra-index-url "${{ inputs.extra_index_url }}") + fi + pip install --quiet "${extra_index_args[@]}" "${wheel}" diff --git a/.github/actions/cmake_build/action.yml b/.github/actions/cmake_build/action.yml new file mode 100644 index 0000000..fff89bf --- /dev/null +++ b/.github/actions/cmake_build/action.yml @@ -0,0 +1,139 @@ +# Copyright 2025 NWChemEx-Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: CMake Build +description: >- + Configure, build, and optionally test a repository with CMake using a + native (apt/Homebrew) compiler toolchain on Linux or macOS. The calling + job must provide python/pip on PATH (e.g. via actions/setup-python) for + the Python test step. + +inputs: + compiler: + description: "One of: gcc-14, clang-18. Installed via the native package manager (apt on Linux, Homebrew on macOS)." + required: true + run_cmake_tests: + description: "Whether to build and run the CTest (C++) test suite (sets -DBUILD_TESTING)." + required: false + default: "true" + run_python_tests: + description: "Whether to run the pytest (Python) test suite against the built pybind11 module." + required: false + default: "true" + cmake_opts: + description: "Additional CMake options to pass at configure time." + required: false + default: "" + extra_pip_packages: + description: "Extra packages to pip install into the ambient Python before the C++ test step (e.g. CTest-driven Python tests that import a pip-installed package)." + required: false + default: "" + +runs: + using: composite + steps: + - name: Setup NWX Dev Environment + uses: NWChemEx/.github/.github/actions/setup_nwx_dev_env@master + with: + compiler: ${{ inputs.compiler }} + + # Some repos' CMake build invokes a Python codegen script at configure or + # build time (e.g. GauXC's gau2grid dependency imports numpy to generate + # C source), so this needs to run before Configure/Build below, not only + # before the Python test step later in this same file. + - name: Install Python Requirements + shell: bash + run: | + if [ -f requirements.txt ]; then + pip install --quiet -r requirements.txt + fi + + - name: Cache Dependency Build Tree + uses: actions/cache@v4 + with: + path: build/_deps + key: deps-${{ runner.os }}-${{ inputs.compiler }}-${{ github.run_id }} + restore-keys: | + deps-${{ runner.os }}-${{ inputs.compiler }}- + + - name: Configure + shell: bash + run: | + build_testing=${{ inputs.run_cmake_tests == 'true' && 'ON' || 'OFF' }} + + # Pin CMake's FindPython to the interpreter actions/setup-python put on + # PATH (and that "Install Python Requirements" above just installed + # into). Without this, CMake's own search can prefer a different + # ambient interpreter (e.g. a macOS Homebrew framework Python), + # leaving configure-time-installed packages invisible to build-time + # codegen scripts that embed a Python interpreter. + cmake -Bbuild -H. -GNinja \ + -DCMAKE_INSTALL_PREFIX=./install \ + -DCMAKE_C_COMPILER="${CC}" \ + -DCMAKE_CXX_COMPILER="${CXX}" \ + -DBUILD_TESTING="${build_testing}" \ + -DPython_EXECUTABLE="$(command -v python3)" \ + ${{ inputs.cmake_opts }} + + - name: Build + shell: bash + run: cmake --build build --parallel + + - name: Install Extra Pip Packages + if: inputs.extra_pip_packages != '' + shell: bash + # Runs before "Test C++" because CTest-driven Python tests use the + # ambient Python_EXECUTABLE, whose sys.path always includes + # site-packages regardless of CTest's PYTHONPATH override -- so any + # package such a test imports needs to be pip-installed here. + run: pip install --quiet ${{ inputs.extra_pip_packages }} + + - name: Test C++ + if: inputs.run_cmake_tests == 'true' + shell: bash + working-directory: build + # Some C++ tests embed a Python interpreter that imports this repo's + # own (and its dependencies') built pybind11 modules -- same PYTHONPATH + # need as the pytest step below, just computed relative to build/. + run: | + total_tests=$(ctest -N | grep "Total Tests:" | grep -oE "[0-9]+$") + if [ "${total_tests:-0}" -eq 0 ]; then + echo "::error::run_cmake_tests is true but zero CTest tests were found." + exit 1 + fi + pythonpath=$(find . -name "*.so" -exec dirname {} \; | sort -u | paste -sd: -) + PYTHONPATH="$pythonpath" ctest -VV + + - name: Test Python + if: inputs.run_python_tests == 'true' + shell: bash + run: | + pip install --quiet pytest + + pythonpath=$(find build -name "*.so" -exec dirname {} \; | sort -u | paste -sd: -) + if [ -z "$pythonpath" ]; then + echo "::error::run_python_tests is true but no built Python module (*.so) was found under build/." + exit 1 + fi + + set +e + PYTHONPATH="$pythonpath" pytest -v + pytest_exit=$? + set -e + + if [ "$pytest_exit" -eq 5 ]; then + echo "::error::run_python_tests is true but zero pytest tests were found." + exit 1 + fi + exit "$pytest_exit" diff --git a/.github/actions/pip_build/action.yml b/.github/actions/pip_build/action.yml new file mode 100644 index 0000000..a10f927 --- /dev/null +++ b/.github/actions/pip_build/action.yml @@ -0,0 +1,98 @@ +# Copyright 2025 NWChemEx-Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Pip Build +description: >- + Build and optionally test a repository's Python package with pip, using a + native (apt/Homebrew) compiler toolchain on Linux or macOS. The calling + job must provide python/pip on PATH (e.g. via actions/setup-python). + +inputs: + compiler: + description: "One of: gcc-14, clang-18. Installed via the native package manager (apt on Linux, Homebrew on macOS)." + required: true + run_cmake_tests: + description: "Whether to build and run the CTest (C++) test suite (sets -DBUILD_TESTING)." + required: false + default: "true" + run_python_tests: + description: "Whether to install the package editable with its dev extras and run the pytest (Python) test suite." + required: false + default: "true" + pip_opts: + description: "Additional options to pass to pip install." + required: false + default: "" + +runs: + using: composite + steps: + - name: Setup NWX Dev Environment + uses: NWChemEx/.github/.github/actions/setup_nwx_dev_env@master + with: + compiler: ${{ inputs.compiler }} + + - name: Cache Dependency Build Tree + uses: actions/cache@v4 + with: + path: build/_deps + key: deps-${{ runner.os }}-${{ inputs.compiler }}-${{ github.run_id }} + restore-keys: | + deps-${{ runner.os }}-${{ inputs.compiler }}- + + - name: Install + shell: bash + run: | + build_testing=${{ inputs.run_cmake_tests == 'true' && 'ON' || 'OFF' }} + + if [ "${{ inputs.run_python_tests }}" == "true" ]; then + pip install -e ".[dev]" \ + --config-settings=cmake.define.CMAKE_C_COMPILER="${CC}" \ + --config-settings=cmake.define.CMAKE_CXX_COMPILER="${CXX}" \ + --config-settings=cmake.define.BUILD_TESTING="${build_testing}" \ + ${{ inputs.pip_opts }} + else + pip install . \ + --config-settings=cmake.define.CMAKE_C_COMPILER="${CC}" \ + --config-settings=cmake.define.CMAKE_CXX_COMPILER="${CXX}" \ + --config-settings=cmake.define.BUILD_TESTING="${build_testing}" \ + ${{ inputs.pip_opts }} + fi + + - name: Test Python + if: inputs.run_python_tests == 'true' + shell: bash + run: | + set +e + pytest -v + pytest_exit=$? + set -e + + if [ "$pytest_exit" -eq 5 ]; then + echo "::error::run_python_tests is true but zero pytest tests were found." + exit 1 + fi + exit "$pytest_exit" + + - name: Test C++ + if: inputs.run_cmake_tests == 'true' + shell: bash + working-directory: build + run: | + total_tests=$(ctest -N | grep "Total Tests:" | grep -oE "[0-9]+$") + if [ "${total_tests:-0}" -eq 0 ]; then + echo "::error::run_cmake_tests is true but zero CTest tests were found." + exit 1 + fi + ctest -VV diff --git a/.github/actions/setup_nwx_dev_env/action.yml b/.github/actions/setup_nwx_dev_env/action.yml new file mode 100644 index 0000000..31873f7 --- /dev/null +++ b/.github/actions/setup_nwx_dev_env/action.yml @@ -0,0 +1,120 @@ +# Copyright 2026 NWChemEx-Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Setup NWX Dev Environment +description: >- + Installs a C/C++ compiler plus CMake, Ninja, MPI, Boost, BLAS/LAPACK, and + Eigen3 via the native package manager (apt on Linux, Homebrew on macOS), + and exports CC/CXX to $GITHUB_ENV for later steps in the same job. Single + source of truth for this, shared by cmake_build and pip_build. + +inputs: + compiler: + description: "One of: gcc-14, clang-18." + required: true + needs_nwchem: + description: >- + Whether to install a real NWChem binary, for friend-integration tests + that shell out to the external NWChem quantum chemistry program (e.g. + FriendZone's nwx2molssi/nwx2ase NWChem wrappers, and NWChemEx's + compute_energy tests -- both gated behind + friendzone.friends.is_nwchem_enabled(), which checks whether `nwchem` + is on PATH). No apt or pip package ships the actual NWChem binary, only + Homebrew (macOS) and conda-forge (Linux, via a standalone micromamba + bootstrap -- no full Anaconda needed) do. Off by default: this is a + large, slow install that only a couple of repos' test suites need. + required: false + default: "false" + +runs: + using: composite + steps: + - name: Install Compiler and Dependencies + shell: bash + run: | + case "${{ runner.os }}_${{ inputs.compiler }}" in + Linux_gcc-14) + sudo apt-get update -y + sudo apt-get install -y gcc-14 g++-14 cmake ninja-build libopenmpi-dev openmpi-bin libboost-dev libopenblas-dev liblapack-dev libeigen3-dev + echo "CC=gcc-14" >> "$GITHUB_ENV" + echo "CXX=g++-14" >> "$GITHUB_ENV" + ;; + Linux_clang-18) + sudo apt-get update -y + sudo apt-get install -y clang-18 libomp-18-dev cmake ninja-build libopenmpi-dev openmpi-bin libboost-dev libopenblas-dev liblapack-dev libeigen3-dev + echo "CC=clang-18" >> "$GITHUB_ENV" + echo "CXX=clang++-18" >> "$GITHUB_ENV" + ;; + macOS_gcc-14) + brew install gcc@14 cmake ninja open-mpi boost openblas eigen + echo "CC=gcc-14" >> "$GITHUB_ENV" + echo "CXX=g++-14" >> "$GITHUB_ENV" + echo "CMAKE_PREFIX_PATH=$(brew --prefix openblas)" >> "$GITHUB_ENV" + ;; + macOS_clang-18) + brew install llvm@18 libomp cmake ninja open-mpi boost openblas eigen + prefix=$(brew --prefix llvm@18) + libomp_prefix=$(brew --prefix libomp) + echo "CC=${prefix}/bin/clang" >> "$GITHUB_ENV" + echo "CXX=${prefix}/bin/clang++" >> "$GITHUB_ENV" + echo "CMAKE_PREFIX_PATH=$(brew --prefix openblas);${libomp_prefix}" >> "$GITHUB_ENV" + # llvm@18's own clang has no bundled OpenMP runtime; find_package(OpenMP) + # needs omp.h/libomp.dylib on the compiler's default search paths to pass + # its "-fopenmp compiles" check and locate the library, and CPATH/LIBRARY_PATH + # are how Clang picks those up without needing explicit -I/-L flags threaded + # through every cmake_opts caller. + echo "CPATH=${libomp_prefix}/include" >> "$GITHUB_ENV" + echo "LIBRARY_PATH=${libomp_prefix}/lib" >> "$GITHUB_ENV" + ;; + *) + echo "::error::Unsupported (os, compiler): (${{ runner.os }}, ${{ inputs.compiler }})" + exit 1 + ;; + esac + + - name: Install NWChem + if: inputs.needs_nwchem == 'true' + shell: bash + run: | + case "${{ runner.os }}" in + Linux) + # No apt/pip package ships the real NWChem binary -- conda-forge + # is the only source of a precompiled one. Bootstrap a + # standalone micromamba (not a full Anaconda/conda install), + # install NWChem into its own env, then put just that env's + # bin/ on PATH via GITHUB_PATH rather than `conda activate`, + # so it doesn't shadow the Python venv the rest of the job uses. + "${SHELL}" <(curl -L micro.mamba.pm/install.sh) + "$HOME/.local/bin/micromamba" create -y -n nwchem -c conda-forge nwchem + echo "$HOME/micromamba/envs/nwchem/bin" >> "$GITHUB_PATH" + # conda-forge's nwchem is compiled with a hardcoded default + # NWCHEM_BASIS_LIBRARY pointing at the feedstock's own build + # directory (doesn't exist outside the build container), so + # basis set lookups fail unless overridden. The package's own + # etc/conda/activate.d/nwchem_env.sh sets these two vars + # correctly, but only on `conda activate`, which we don't do + # (just PATH, to avoid shadowing the job's Python venv) -- so + # set them explicitly instead. Confirmed via local repro + # (Docker) that a bare `nwchem` invocation missing these vars + # can hang indefinitely on some tasks (e.g. CCSD(T)) rather + # than fail cleanly, instead of the fast/clean "failed opening + # basis file" abort seen on simpler tasks (e.g. CCSD) -- + # confirmed fixed by setting these. + echo "NWCHEM_BASIS_LIBRARY=$HOME/micromamba/envs/nwchem/share/nwchem/libraries/" >> "$GITHUB_ENV" + echo "NWCHEM_NWPW_LIBRARY=$HOME/micromamba/envs/nwchem/share/nwchem/libraryps/" >> "$GITHUB_ENV" + ;; + macOS) + brew install nwchem + ;; + esac diff --git a/.github/workflows/deploy_nwx_docs.yaml b/.github/workflows/deploy_nwx_docs.yaml index 8131844..e0df086 100644 --- a/.github/workflows/deploy_nwx_docs.yaml +++ b/.github/workflows/deploy_nwx_docs.yaml @@ -41,6 +41,22 @@ jobs: core.setFailed("doc_target must not be blank.") - name: Checkout Source uses: actions/checkout@v4 + # Some repos' CMake build invokes a Python codegen script at configure + # or build time (e.g. GauXC's gau2grid dependency imports numpy to + # generate C source) using whichever python3 is on PATH by default in + # this container -- install into that same interpreter here, before + # CMake ever runs, rather than in the module-docs/Sphinx steps below + # (those activate /pyenv instead, a separate environment). + - name: Install Repo Python Build Dependencies + if: inputs.generate_module_docs == true + run: | + if [ -f requirements.txt ]; then + # This container's system python3 is PEP 668 + # externally-managed; --break-system-packages is safe here + # since the whole container is ephemeral CI state anyway. + python3 -m pip install --quiet --break-system-packages -r requirements.txt + fi + shell: bash # These next two steps will configure CMake if required - name: Configure Only Docs if: | diff --git a/.github/workflows/platform_matrix.yaml b/.github/workflows/platform_matrix.yaml new file mode 100644 index 0000000..65d3147 --- /dev/null +++ b/.github/workflows/platform_matrix.yaml @@ -0,0 +1,56 @@ +# Copyright 2026 NWChemEx-Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +name: Platform Matrix +# Central source of truth for the OS/compiler/cibw_build combinations +# NWChemEx tests and publishes for. Emits two JSON matrix strings as outputs +# for downstream jobs to consume via fromJSON(). + +on: + workflow_call: + outputs: + release_matrix: + description: >- + JSON list of {os, cibw_build} objects for deploy_to_pypi + (compiled-package wheel matrix). + value: ${{ jobs.define_matrix.outputs.release_matrix }} + dev_matrix: + description: >- + JSON list of {os, compiler} objects for cmake_build/pip_build + (dev/test matrix). + value: ${{ jobs.define_matrix.outputs.dev_matrix }} + +jobs: + define_matrix: + runs-on: ubuntu-latest + outputs: + release_matrix: ${{ steps.set.outputs.release_matrix }} + dev_matrix: ${{ steps.set.outputs.dev_matrix }} + steps: + - id: set + shell: bash + run: | + release_matrix='[ + {"os": "ubuntu-latest", "cibw_build": "cp312-manylinux_x86_64"}, + {"os": "macos-14", "cibw_build": "cp312-macosx_arm64"} + ]' + dev_matrix='[ + {"os": "ubuntu-latest", "compiler": "gcc-14"}, + {"os": "ubuntu-latest", "compiler": "clang-18"}, + {"os": "macos-14", "compiler": "gcc-14"}, + {"os": "macos-14", "compiler": "clang-18"} + ]' + echo "release_matrix=$(echo "$release_matrix" | jq -c .)" >> "$GITHUB_OUTPUT" + echo "dev_matrix=$(echo "$dev_matrix" | jq -c .)" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/test_nwx_cmake_build.yaml b/.github/workflows/test_nwx_cmake_build.yaml new file mode 100644 index 0000000..15b84b7 --- /dev/null +++ b/.github/workflows/test_nwx_cmake_build.yaml @@ -0,0 +1,76 @@ +# Copyright 2026 NWChemEx-Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +name: Test NWX CMake Build +# Configure, build, and test a repository with CMake, across the shared +# NWChemEx dev matrix (see platform_matrix.yaml). + +on: + workflow_call: + inputs: + run_cmake_tests: + description: "Whether to build and run the CTest (C++) test suite." + type: string + required: false + default: "true" + run_python_tests: + description: "Whether to run the pytest (Python) test suite against the built pybind11 module." + type: string + required: false + default: "true" + cmake_opts: + description: "Additional CMake options to pass at configure time." + type: string + required: false + default: "" + python_version: + description: "Python version to set up (passed to actions/setup-python)." + type: string + required: false + default: "3.x" + extra_pip_packages: + description: "Extra packages to pip install into the ambient Python before the C++ test step." + type: string + required: false + default: "" + +jobs: + platform_matrix: + uses: NWChemEx/.github/.github/workflows/platform_matrix.yaml@master + + test_cmake_build: + needs: platform_matrix + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON(needs.platform_matrix.outputs.dev_matrix) }} + runs-on: ${{ matrix.os }} + steps: + - name: Checkout Source + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python_version }} + - name: CMake Build + uses: NWChemEx/.github/.github/actions/cmake_build@master + with: + compiler: ${{ matrix.compiler }} + run_cmake_tests: ${{ inputs.run_cmake_tests }} + run_python_tests: ${{ inputs.run_python_tests }} + cmake_opts: ${{ inputs.cmake_opts }} + extra_pip_packages: ${{ inputs.extra_pip_packages }} diff --git a/.github/workflows/test_nwx_pip_build.yaml b/.github/workflows/test_nwx_pip_build.yaml new file mode 100644 index 0000000..0307147 --- /dev/null +++ b/.github/workflows/test_nwx_pip_build.yaml @@ -0,0 +1,65 @@ +# Copyright 2026 NWChemEx-Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +name: Test NWX Pip Build +# Build and test a repository's Python package with pip, across the shared +# NWChemEx dev matrix (see platform_matrix.yaml). + +on: + workflow_call: + inputs: + run_cmake_tests: + description: "Whether to build and run the CTest (C++) test suite (sets -DBUILD_TESTING)." + type: string + required: false + default: "true" + run_python_tests: + description: "Whether to install the package editable with its dev extras and run the pytest (Python) test suite." + type: string + required: false + default: "true" + pip_opts: + description: "Additional options to pass to pip install." + type: string + required: false + default: "" + +jobs: + platform_matrix: + uses: NWChemEx/.github/.github/workflows/platform_matrix.yaml@master + + test_pip_build: + needs: platform_matrix + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON(needs.platform_matrix.outputs.dev_matrix) }} + runs-on: ${{ matrix.os }} + steps: + - name: Checkout Source + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Pip Build + uses: NWChemEx/.github/.github/actions/pip_build@master + with: + compiler: ${{ matrix.compiler }} + run_cmake_tests: ${{ inputs.run_cmake_tests }} + run_python_tests: ${{ inputs.run_python_tests }} + pip_opts: ${{ inputs.pip_opts }}