Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/actions/build_pypi_dist/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -273,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
Expand Down Expand Up @@ -359,3 +401,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"
88 changes: 88 additions & 0 deletions .github/actions/build_pypi_dist/import_check.py
Original file line number Diff line number Diff line change
@@ -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())
11 changes: 11 additions & 0 deletions .github/actions/cmake_build/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down Expand Up @@ -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 }}

Expand Down
11 changes: 11 additions & 0 deletions .github/actions/pip_build/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand All @@ -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

Expand Down
Loading