From fd8beb1a44df60c4ce534307d95e598c02d77943 Mon Sep 17 00:00:00 2001 From: "Ryan M. Richard" Date: Thu, 10 Sep 2026 13:38:35 -0500 Subject: [PATCH 1/2] add an import check --- .github/actions/build_pypi_dist/action.yml | 30 +++++++ .../actions/build_pypi_dist/import_check.py | 88 +++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 .github/actions/build_pypi_dist/import_check.py diff --git a/.github/actions/build_pypi_dist/action.yml b/.github/actions/build_pypi_dist/action.yml index 5c5af8e..dbe6573 100644 --- a/.github/actions/build_pypi_dist/action.yml +++ b/.github/actions/build_pypi_dist/action.yml @@ -114,6 +114,27 @@ inputs: keg-only libomp on its own). Ignored when cibw_build is unset. required: false default: "false" + import_check: + description: >- + Space-separated module names to `import` in the post-build + verification venv, after the wheel is installed there. Installing a + wheel only unpacks it -- a compiled extension that crashes the + interpreter the moment it's loaded (e.g. a pybind11 module linked + against a different libpython than the headers it was compiled + with) installs perfectly cleanly, so `pip install` alone can't tell + a working wheel from an unloadable one. Importing it can, and a + segfault there fails the job before the publish step ever runs. + Runs on every leg, macOS included. + + Leave unset (the normal case) to import every top-level module the + installed distribution actually declares in its own RECORD. That is + read off the wheel rather than guessed from the distribution name, + because the two don't reliably match across this ecosystem + (nwchemex-weaklytypedfloat imports as `wtf`, and a package can + install no importable module at all, in which case the check is + correctly a no-op). Set this only to narrow or override that. + required: false + default: "" runs: using: composite @@ -359,3 +380,12 @@ runs: extra_index_args=(--extra-index-url "${{ inputs.extra_index_url }}") fi pip install --quiet "${extra_index_args[@]}" "${wheel}" + + # See the import_check input: pip install proves the wheel unpacks, + # not that its extension modules can actually be loaded. Deliberately + # in this same step rather than a new one, so it inherits the + # interpreter the preceding setup-python step re-pointed PATH at + # (the cibuildwheel target version, not the ambient "3.x"). + dist_name=$(basename "${wheel}" | cut -d- -f1) + IMPORT_CHECK="${{ inputs.import_check }}" DIST_NAME="${dist_name}" \ + python "${GITHUB_ACTION_PATH}/import_check.py" diff --git a/.github/actions/build_pypi_dist/import_check.py b/.github/actions/build_pypi_dist/import_check.py new file mode 100644 index 0000000..120009f --- /dev/null +++ b/.github/actions/build_pypi_dist/import_check.py @@ -0,0 +1,88 @@ +# 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. + +"""Import every top-level module a freshly installed wheel provides. + +Run by build_pypi_dist's "Verify Wheel Installs" step, inside the throwaway +venv the wheel was just installed into. `pip install` only proves the wheel +unpacks; an extension module that crashes the interpreter on load installs +perfectly cleanly, so the only way to catch it before publishing is to +actually import it. + +A separate file rather than a `python -c` one-liner in the action: this +needs enough logic (read the RECORD, filter it down to importable +top-level names) that inlining it would mean quoting a multi-line script +through both YAML and bash. + +Inputs arrive as environment variables so nothing is interpolated into +source text: + IMPORT_CHECK -- optional space-separated override; when set, exactly + these names are imported. + DIST_NAME -- the installed distribution's name, e.g. nwchemex_scf. +""" + +import importlib +import os +import sys +from importlib.metadata import distribution + +# Suffixes a top-level RECORD entry can carry and still name something +# importable. Compiled extensions are the whole point of the check, so the +# platform-tagged forms (scf.cpython-312-darwin.so) matter as much as .py; +# everything else a wheel installs at the root -- libscf.dylib, the +# delocate/auditwheel .dylibs/ and .libs/ directories, lib/, include/, +# share/, the .dist-info -- is data, not a module. +_MODULE_SUFFIXES = (".py", ".so", ".pyd", ".dll") + + +def _top_level_modules(dist_name): + """Top-level importable module names the distribution installs.""" + files = distribution(dist_name).files or [] + modules = set() + for path in files: + parts = path.parts + name = parts[0] + if name.endswith(".dist-info") or name.endswith(".data"): + continue + if len(parts) > 1: + # A directory at the root is a package only if it has an + # __init__; bundled shared-library and data directories don't. + if parts[1] == "__init__.py": + modules.add(name) + continue + if not name.endswith(_MODULE_SUFFIXES): + continue + # scf.cpython-312-darwin.so -> scf; friendzone.py -> friendzone + modules.add(name.split(".")[0]) + return sorted(modules) + + +def main(): + override = os.environ.get("IMPORT_CHECK", "").split() + dist_name = os.environ["DIST_NAME"] + modules = override or _top_level_modules(dist_name) + + if not modules: + print(f"{dist_name} installs no top-level module; nothing to import") + return 0 + + for module in modules: + print(f"Importing {module} ...", flush=True) + importlib.import_module(module) + print(f"Imported {len(modules)} module(s) from {dist_name} cleanly") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 1d611ddee1c7d3c70f9d81875ce3f8be8538b435 Mon Sep 17 00:00:00 2001 From: "Ryan M. Richard" Date: Thu, 10 Sep 2026 14:47:38 -0500 Subject: [PATCH 2/2] system independent installs --- .github/actions/build_pypi_dist/action.yml | 21 +++++++++++++++++++++ .github/actions/cmake_build/action.yml | 11 +++++++++++ .github/actions/pip_build/action.yml | 11 +++++++++++ 3 files changed, 43 insertions(+) diff --git a/.github/actions/build_pypi_dist/action.yml b/.github/actions/build_pypi_dist/action.yml index dbe6573..328776c 100644 --- a/.github/actions/build_pypi_dist/action.yml +++ b/.github/actions/build_pypi_dist/action.yml @@ -294,6 +294,27 @@ runs: 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 + # Appended after every needs_* branch, not seeded with the arrays + # above, because the needs_mpi branch *reassigns* env_macos rather + # than appending to it (to raise MACOSX_DEPLOYMENT_TARGET) and would + # drop anything set earlier. + # + # Unconditional, and deliberately not tied to any needs_* input: a + # published wheel must never be tuned to whichever runner built it. + # gau2grid, libxc and GauXC's vendored gau2grid all default + # ENABLE_XHOST ON, which compiles them "-march=native" -- so a wheel + # built on an AVX-512 runner raises SIGILL on a user's machine + # without it. NWXCMake keeps ON as the default for local builds + # (see set_default_nwx_options) and expects redistributable builds + # to say otherwise, which is what this does. + # + # SKBUILD_CMAKE_DEFINE (scikit-build-core's env spelling of + # cmake.define.*, ";"-separated KEY=VALUE) rather than + # SKBUILD_CMAKE_ARGS, which needs_openmp already claims on macOS -- + # a second assignment to it there would clobber the OpenMP hints. + env_linux+=("SKBUILD_CMAKE_DEFINE=ENABLE_XHOST=OFF") + env_macos+=("SKBUILD_CMAKE_DEFINE=ENABLE_XHOST=OFF") + join_and() { local out="" item for item in "$@"; do diff --git a/.github/actions/cmake_build/action.yml b/.github/actions/cmake_build/action.yml index 84163fd..8b589e3 100644 --- a/.github/actions/cmake_build/action.yml +++ b/.github/actions/cmake_build/action.yml @@ -80,6 +80,16 @@ runs: restore-keys: | ccache-${{ runner.os }}-${{ inputs.compiler }}- + # ENABLE_XHOST=OFF: several dependencies (gau2grid, libxc, GauXC's + # vendored gau2grid) default it ON, which builds them with + # "-march=native". The two caches above are shared across the whole + # runner fleet, and ccache hashes the flag *text* rather than the ISA + # "native" resolves to, so a hit restored from a job that ran on an + # AVX-512 host serves those instructions to a job that ran on one + # without -- which is exactly how NWChemEx/SCF's ubuntu leg started + # failing intermittently with SIGILL inside gau2grid. Passed here + # rather than defaulted OFF in NWXCMake so a local build on the machine + # it will run on still gets the tuning (see set_default_nwx_options). - name: Configure shell: bash run: | @@ -128,6 +138,7 @@ runs: -DCMAKE_C_COMPILER_LAUNCHER=ccache \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ -DBUILD_TESTING="${build_testing}" \ + -DENABLE_XHOST=OFF \ "${python_pin_opts[@]}" \ ${{ inputs.cmake_opts }} diff --git a/.github/actions/pip_build/action.yml b/.github/actions/pip_build/action.yml index a10f927..c13e17a 100644 --- a/.github/actions/pip_build/action.yml +++ b/.github/actions/pip_build/action.yml @@ -51,6 +51,15 @@ runs: restore-keys: | deps-${{ runner.os }}-${{ inputs.compiler }}- + # ENABLE_XHOST=OFF: several dependencies (gau2grid, libxc, GauXC's + # vendored gau2grid) default it ON, which builds them with + # "-march=native". The dependency tree cached above is shared across + # the whole runner fleet, and it holds those dependencies' *built* + # artifacts, so a restore onto a runner with a narrower instruction set + # than the one that filled it hands the build a library it cannot + # execute (SIGILL). Passed here rather than defaulted OFF in NWXCMake + # so a local build on the machine it will run on still gets the tuning + # (see set_default_nwx_options). - name: Install shell: bash run: | @@ -61,12 +70,14 @@ runs: --config-settings=cmake.define.CMAKE_C_COMPILER="${CC}" \ --config-settings=cmake.define.CMAKE_CXX_COMPILER="${CXX}" \ --config-settings=cmake.define.BUILD_TESTING="${build_testing}" \ + --config-settings=cmake.define.ENABLE_XHOST=OFF \ ${{ 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}" \ + --config-settings=cmake.define.ENABLE_XHOST=OFF \ ${{ inputs.pip_opts }} fi