Skip to content

Bring in the CMake/PyPI build overhaul - #63

Merged
ryanmrichard merged 54 commits into
masterfrom
build_overhaul
Aug 5, 2026
Merged

Bring in the CMake/PyPI build overhaul#63
ryanmrichard merged 54 commits into
masterfrom
build_overhaul

Conversation

@ryanmrichard

Copy link
Copy Markdown
Member

Summary

  • Imports the CMake dependency-fetching infrastructure (cmake/dependencies/*.cmake, get_dependencies, get_version_from_git, nwx_set_version, nwx_python_module, etc.) and the pip-packaging setup (pyproject.toml, scikit-build-core config) developed on the ryanmrichard fork -- this repo is the shared CMake glue that every other NWChemEx C++ repo's get_nwx_cmake.cmake fetches.
  • Repoints this repo's own CI (merge.yaml, pull_request.yaml) from ryanmrichard/.github to NWChemEx/.github (landed in Land the build/CI overhaul's shared workflows and actions .github#187), and its cmake/dependencies/*.cmake fetches of the ecosystem's own C++ libraries (chemist, pluginplay, parallelzone, tensorwrapper, utilities, simde, chemcache, integrals, nux, nwchemex) from ryanmrichard/<Repo> to NWChemEx/<Repo>.
  • GIT_TAG on those 10 fetches deliberately stays build_overhaul for now -- their NWChemEx master branches don't have this content yet. A follow-up PR flips them to master once the whole ecosystem has migrated.
  • Drops the test.pypi.org repository-url override from the merge workflow's publish step, so merging to master publishes nwchemex-nwxcmake to real PyPI. This is the first package in the migration (nothing else has an NWXCMake floor to publish against yet), and a trusted publisher is being registered for it.

Test plan

ryanmrichard and others added 30 commits April 13, 2026 12:13
…fork testing

Fork-test-only commit: points at the ryanmrichard/.github fork's
reusable workflows, and opts the merge-time PyPI deploy into
Test PyPI rather than the real, production PyPI.
The pybind11 module is a shared object; anything statically linked
into it (including FetchContent'd deps like spdlog) needs -fPIC.
Discovered via fork-testing ParallelZone: linking its Python module
failed on Linux with "relocation R_X86_64_TPOFF32 ... can not be used
when making a shared object" because spdlog was built without PIC.
…or fork testing

PluginPlay's build_overhaul depends on ParallelZone and Utilities'
refactored (get_dependencies-based) CMake structure, which only exists
on their ryanmrichard/* forks' build_overhaul branches; NWChemEx/'s
master still has the old, incompatible build system.
…m_required

libfort v0.4.2's CMakeLists.txt requests a CMake version older than
3.5, which recent CMake releases refuse to configure at all
("Compatibility with CMake < 3.5 has been removed"). Discovered via
fork-testing PluginPlay, which pulls libfort in through get_dependencies.
Lets the macro build test-only pybind11 helper modules (e.g. PluginPlay's
py_test_pluginplay/pluginplay_examples, TensorWrapper's
py_test_tensorwrapper) instead of every repo hand-rolling its own
pybind11_add_module block: NO_INSTALL skips shipping the module in the
wheel, DEPENDS links extra libraries beyond ${PROJECT_NAME}. Existing
2-positional-arg call sites are unaffected.
…ting

Chemist depends on tensorwrapper via get_dependencies(); its fork-testing
build needs the fork's build_overhaul branch, which carries the refactored
CMake build system.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ork testing

SimDE depends on both via get_dependencies(); its fork-testing build needs
both forks' build_overhaul branches.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…st PROJECT_NAME

Only the package's own library (e.g. libparallelzone.so) was ever
co-installed alongside the pybind11 extension; a FetchContent'd shared
dependency built shared (e.g. spdlog) was not, so wheel-repair tools
(auditwheel/delocate) had nothing to bundle it from and failed with
"library not found". install(TARGETS ... RUNTIME_DEPENDENCY_SET ...) walks
the extension's actual dynamic-link dependencies at install time and
stages every non-system shared library it finds, superseding the old
single-target co-install. Verified end-to-end (docker ubuntu:24.04 +
gcc-14, ParallelZone with BUILD_PYBIND11_BINDINGS=ON): both
libparallelzone.so and libspdlog.so.1.16.0 land in the install dir now.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… the

main library to \$ORIGIN + \$ORIGIN/lib, restore explicit co-install

Root cause of the auditwheel/delocate "library not found" failure: it was
never actually a missing-install problem (an install(TARGETS ...
RUNTIME_DEPENDENCY_SET ...) attempt in the prior commit found that CMake
auto-excludes files that already have their own install() rule elsewhere,
e.g. spdlog's bundled install and this library's own install_library()
call -- so nothing new ever got installed by that route). The actual gap
was an rpath mismatch: the pybind11 extension (installed flat at the
platlib root) only searched $ORIGIN, but its shared dependencies land one
directory down at "lib/" via their own pre-existing install rules, and the
main library itself (e.g. libparallelzone.so) had no rpath at all, so even
once co-located with spdlog it still couldn't find it (RUNPATH is
non-transitive). Fixed by giving both the extension and the main library
"$ORIGIN;$ORIGIN/lib" ("@loader_path;@loader_path/lib" on macOS), which
covers both of a library's two install locations (its own "lib/" home, and
a flat co-install next to the extension) symmetrically.

Verified end-to-end (docker ubuntu:24.04 + gcc-14, ParallelZone): ldd
resolves libspdlog.so from both the extension and libparallelzone.so, and
`auditwheel repair` completes successfully.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ded convention

Root cause of the persistent auditwheel "library not found" failure:
spdlog (via its own bundled GNUInstallDirs-respecting install rules)
installs to a platform-specific lib dir -- "lib64" on the manylinux2014
(CentOS7-based) container specifically, since GNUInstallDirs defaults
there for 64-bit systems -- one level away from where install_target.cmake
always puts this project's own libraries ("lib", hardcoded, no
GNUInstallDirs). The rpath widening from the previous commit only ever
covered "lib", so it silently worked on Ubuntu (no lib64 split) but not
on the actual manylinux container CI publishes wheels from. Forcing
CMAKE_INSTALL_LIBDIR=lib before any FetchContent'd dependency configures
keeps everything installed to the one directory every rpath in this
ecosystem actually searches.

Verified against the exact CI container (docker
quay.io/pypa/manylinux2014_x86_64:2024.11.16-1): spdlog now lands in
lib/, not lib64/, and `auditwheel repair` succeeds.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…mode

Config mode (CMP0167 NEW) requires Boost's own exported BoostConfig.cmake,
only shipped since 1.70; every manylinux image's package manager tops out
below that (manylinux2014's boost-devel is 1.53.0, manylinux_2_28's is
1.66.0), so any repo needing Boost failed to configure at all inside a
cibuildwheel wheel build. Nothing in this ecosystem uses more than the
header-only Boost::boost umbrella target, which CMake's own bundled (if
deprecated) FindBoost module provides identically in Module mode, and that
module works fine with much older Boost versions.

Verified end-to-end (docker quay.io/pypa/manylinux2014_x86_64): Boost
1.53.0 resolves via find_package(Boost REQUIRED), producing a working
Boost::boost target; PluginPlay configures and builds successfully.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes a leftover pointing at NWChemEx/SimDE@master that was missed when
the other dependency files were retargeted for fork testing.
ryanmrichard and others added 24 commits July 10, 2026 13:07
libxc's own CMakeLists.txt only defines the plain 'xc' target;
'Libxc::xc' is an imported-target alias created by its install(EXPORT)
rule, which doesn't exist when consumed via FetchContent/add_subdirectory
(same situation as gau2grid's plain 'gg' target). Fixes SCF's
'target was not found' CMake configure error.
…d deps

GauXC transitively FetchContents its own libxc/ExchCXX/IntegratorXX,
at least one of which has a cmake_minimum_required() below CMake's
3.5 floor -- newer CMake (4.x) refuses to configure that at all.
Same fix already applied for libfort.cmake.
…s module

nwx_python_test's ENVIRONMENT test property baked in $ENV{PYTHONPATH}
at CMake *configure* time, so it only ever picked up whatever was on
PYTHONPATH before configure ran -- generally empty on CI, and useless
for a test needing a source-fetched dependency's own pybind11 module
(e.g. SCF/ChemCache's tests importing parallelzone, built as
parallelzone_python under build/_deps/, never installed anywhere).

get_dependencies now records the build-tree location of any fetched
dependency's own "<dep>_python" target (if it built one) in a GLOBAL
property, and nwx_python_test folds those into the test's PYTHONPATH
via target-file generator expressions (evaluated at test-run time, not
configure time) instead of relying solely on the inherited environment.
get_dependencies(nwchemex) is called by SCF's C++ integration tests but
no dependencies/nwchemex.cmake ever existed to satisfy it. Add one that
nests get_dependencies(integrals chemcache nux) and exposes the result
as a single INTERFACE target named nwchemex. Also remove the old
cmaize-era get_nwchemex.cmake, which is unused by the FetchContent-based
get_dependencies() system and points at a NWChemEx CMakeLists.txt that
no longer exists.
get_dependencies(nwchemex)'s nested get_dependencies(integrals chemcache
nux) call needs each of these to have its own dependencies/*.cmake file,
same pattern as simde.cmake/chemist.cmake. These never existed because
none of these repos had previously been consumed as a CMake FetchContent
dependency by another CMake project.
When another dependency in the same build has already declared a
FetchContent dependency named "eigen" (e.g. SCF's own gauxc/eigen
fetch, now reachable via the new nwchemex aggregator), libint2's
internal Eigen detection reuses that already-populated source dir but
wraps it in its own plain INTERFACE target ("libint2_Eigen") that it
unconditionally exports via its own install(EXPORT ...) -- which then
fails CMake's generate-time check because the wrapped include dir is a
build-tree path. We only need libint2's build-tree targets, never its
installed package config, so skip its install rules entirely while
it's being added.
BUILD_TESTING is a global CACHE flag, so without toggling it off around
the nested get_dependencies(integrals chemcache nux) call, each of
those repos also registers (and CTest then runs) its own unit-test
suite as a side effect of being fetched as a library dependency here --
e.g. chemcache's py_utils_test_chemcache, which needs a test-only
Python dependency (requests) this build never installs. We only need
the library targets, same rationale as libint2.cmake's existing
BUILD_TESTING toggle.
CMAKE_SKIP_INSTALL_RULES prevents libint2 from generating its own
cmake_install.cmake, but the parent directory's generated
cmake_install.cmake still contains an unconditional include() of that
path, so `cmake --install` (and thus `pip install`) fails with
"include could not find requested file". Write a no-op stand-in file
so the forwarding include succeeds.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AppleClang ships no OpenMP runtime, so GauXC's find_package(OpenMP
REQUIRED) for C and CXX fails outright on macOS unless something
points FindOpenMP.cmake at libomp. Set the standard Homebrew-recommended
flags/lib hints when Homebrew's libomp is present and nothing has
already hinted OpenMP.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Suppressing libint2's install rules (to dodge its broken install(EXPORT
...) when Eigen is already fetched elsewhere) also threw out the plain
install(TARGETS ...) that puts its .dylib/.so next to consumers.
Without it, anything linking libint2 builds and links fine but fails
at runtime with "Library not loaded: libint2..." once installed. Add
back just the binary install -- no EXPORT set, so it can't hit the
wrapped-Eigen generate-time error this was all working around.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
pluginplay's doc_snippets example library already called nwx_library()
with these keywords (to avoid globbing in Catch2 test files that live
alongside the example implementation), but the macro silently ignored
them, dumping the literal tokens into target_link_libraries() and
falling back to a glob that swept up the Catch2-dependent sources
without ever linking Catch2WithMain against them.
FetchContent_MakeAvailable runs a dependency's CMakeLists.txt as a
subdirectory, sharing this project's BUILD_TESTING/INTEGRATION_TESTING
cache variables verbatim -- so a developer build of e.g. pluginplay was
also configuring and registering utilities' and parallelzone's own unit
and integration tests. Force both off for the duration of the batched
fetch, then restore, mirroring the per-dependency BUILD_TESTING
backup/restore already used by libxc.cmake and friends.
libint2's bundled CMakeLists.txt registers its "libint2/..." tests
(eritest, unit, hf, hf++) unconditionally -- it never checks
BUILD_TESTING, so setting that off around FetchContent_MakeAvailable
has no effect on them. Left alone, a consumer's ctest run tries to
build them on demand via a nested cmake re-configure of the *consumer's*
build tree, which fails outright since libint2's tests assume its own
build layout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
subdirectory's tests from the parent scope

The previous commit's get_property(DIRECTORY ...)/set_tests_properties
approach failed at configure time ("Can not find test to add
properties to") because CMake only lets a directory manage tests it
registered itself. Suppress add_test() for the duration of libint2's
FetchContent_MakeAvailable instead, restoring it immediately after so
integrals' (or any other consumer's) own tests still register normally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The add_test()-override still needs to actually create each test:
libint2's CMakeLists.txt follows several add_test() calls with its own
set_tests_properties() (e.g. FIXTURES_SETUP), which errors out "Can not
find test" if the test was never registered. Call the real _add_test,
then mark the result DISABLED from within the same override -- that
runs in libint2's own directory scope (the caller's scope at the point
add_test() is invoked), so it can actually see the test it just made.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Marking them DISABLED still listed all 13 libint2/* tests in every
ctest run (as "Not Run (Disabled)"), just cluttering the output for a
dependency's internal tests nothing here wants. No-op set_tests_properties()
too (in addition to add_test()) so libint2's own FIXTURES_SETUP/REQUIRED
calls on the never-registered tests don't error out.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The build was rewritten around vanilla CMake modules (get_dependencies plus
a cmake/dependencies/*.cmake file per dependency), but the old CMaize files
were never removed. None of them is reachable from any repo CMakeLists.txt,
any CI workflow, or any other NWXCMake module, and none would even parse
without CMaize defining cmaize_find_or_build_dependency and the
CMaizeTarget/CMaizeProject API.

Removed:
  - 12 get_<dep>.cmake CMaize shims, each superseded by the same-named file
    under cmake/dependencies/ (or, for exachem/tamm, by a dependency that is
    no longer part of the stack)
  - get_cmaize.cmake and nwx_find_package.cmake, the CMaize bootstrap and glue
  - nwx_pybind11.cmake, superseded by nwx_python_module.cmake and
    nwx_python_test.cmake

Keeping them around meant a developer grepping for how a dependency gets
resolved found two competing answers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This repo's CI workflows called back into the shared workflows/actions
in ryanmrichard/.github (the fork they were developed against); repoint
them at NWChemEx/.github now that stage 1 of the migration has landed
there.

Also drops the TestPyPI repository-url override on the merge workflow's
publish step, so a merge to master publishes to real PyPI -- this
repo's package name is free there and a trusted publisher is being
registered for it.

The ecosystem's own C++ libraries fetched via cmake/dependencies/*.cmake
(chemist, pluginplay, parallelzone, ...) get the same owner fix, but
GIT_TAG deliberately stays build_overhaul for now: those repos' NWChemEx
master branches don't have this content yet, so flipping the tag here
too would break every downstream C++ build immediately. A follow-up PR
flips GIT_TAG to master once the last of them has merged.
check_formatting was failing because this repo never had a
.licenserc.yaml (every sibling repo has one). Without it, license-eye
fell back to its own default ASF-style header and prepended that to
every file instead of recognizing the existing NWChemEx-Project one.
Add the same config the other repos use, then apply the actual
pre-commit fixes it and the other hooks caught: a missing header on
skbuild_python.cmake, missing trailing newlines, and trailing
whitespace.

test_pip_build was failing separately because test_nwx_pip_build.yaml
runs pytest by default and this repo has no tests directory or pytest
dev extra at all -- it's CMake modules and a thin Python package with
no test suite of its own. Set run_python_tests: "false" alongside the
existing run_cmake_tests: "false" (this repo has no CMakeLists.txt of
its own either).
@ryanmrichard
ryanmrichard merged commit 99f75a3 into master Aug 5, 2026
8 checks passed
@ryanmrichard
ryanmrichard deleted the build_overhaul branch August 5, 2026 19:27
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

🚀 [bumpr] Bumped!
New version:v0.0.1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant