From e528e911a326c8d22ceb825d74c699254b78ed74 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Tue, 28 Jul 2026 19:42:33 -0700 Subject: [PATCH 01/35] Python: add a scikit-build-core project for a thin pip/uv wheel This binding-only project is targeted at an already-installed Axom and reuses its package tree. --- src/python/CMakeLists.txt | 124 ++++++++++++++++++++++++++++++++++++++ src/python/pyproject.toml | 101 +++++++++++++++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 src/python/CMakeLists.txt create mode 100644 src/python/pyproject.toml diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt new file mode 100644 index 0000000000..2521441e4b --- /dev/null +++ b/src/python/CMakeLists.txt @@ -0,0 +1,124 @@ +# Copyright (c) Lawrence Livermore National Security, LLC and other +# Axom Project Contributors. See top-level LICENSE and COPYRIGHT +# files for dates and other details. +# +# SPDX-License-Identifier: (BSD-3-Clause) +#------------------------------------------------------------------------------ +# Thin, binding-only build of Axom's Python extension module(s). +# +# This project compiles Axom's nanobind translation unit(s) against an already-installed Axom and Conduit. +# It does NOT build Axom or its third-party libraries. +# +# It is driven by the scikit-build-core project in the sibling pyproject.toml (see src/python/README.md), +# but is a valid standalone CMake project as well. +# +# When driven by scikit-build-core (uv build / uv pip install), +# point find_package at the installs with -Daxom_DIR / -DConduit_DIR: +# scikit-build-core force-sets CMAKE_PREFIX_PATH to the isolated build env (to locate its own nanobind), +# so a user-supplied CMAKE_PREFIX_PATH is ignored. +# A standalone cmake invocation has no such layer and uses CMAKE_PREFIX_PATH directly: +# +# cmake -S src/python -B build/py \ +# -DCMAKE_PREFIX_PATH="$AXOM_INSTALL;$CONDUIT_INSTALL;$(python -m nanobind --cmake_dir)" +# cmake --build build/py +# cmake --install build/py --prefix +#------------------------------------------------------------------------------ + +cmake_minimum_required(VERSION 3.21) +project(axom_python LANGUAGES CXX) + +# Development.Module (not Development) so we link libpython-free extensions, +# matching the in-tree build's discovery. +find_package(Python 3.10 REQUIRED COMPONENTS Interpreter Development.Module) +find_package(axom CONFIG REQUIRED) +find_package(nanobind CONFIG REQUIRED) # supplied via build-system.requires + +# The wheel's version metadata is read from this checkout's src/cmake/AxomVersion.cmake +# (see [[tool.dynamic-metadata]] in pyproject.toml), +# but the extension is compiled and linked against the Axom install found above. +# +# Building from a checkout that does not match the install would produce a wheel +# whose axom.__version__ misreports its own binary, so they must agree. +# (AXOM_VERSION_* come from axom-config.cmake; SKBUILD_PROJECT_VERSION is injected by scikit-build-core, +# so this check is active for wheel builds and skipped for a standalone cmake invocation.) +if(DEFINED SKBUILD_PROJECT_VERSION AND DEFINED AXOM_VERSION_MAJOR) + set(_axom_installed_version + "${AXOM_VERSION_MAJOR}.${AXOM_VERSION_MINOR}.${AXOM_VERSION_PATCH}") + if(NOT SKBUILD_PROJECT_VERSION VERSION_EQUAL _axom_installed_version) + message(FATAL_ERROR + "Axom version mismatch: the wheel's metadata version is " + "${SKBUILD_PROJECT_VERSION} (from src/cmake/AxomVersion.cmake in this " + "source tree) but the Axom install it would link against is " + "${_axom_installed_version} (axom_DIR=${axom_DIR}). Build the wheel from " + "the source tree that produced the install, or point axom_DIR at an " + "install built from this source tree.") + endif() +endif() + +#------------------------------------------------------------------------------ +# Binding sources. The translation unit lives with its component, not in this project directory. +#------------------------------------------------------------------------------ +set(_sidre_binding_sources + "${CMAKE_CURRENT_SOURCE_DIR}/../axom/sidre/nanobind_sidre.cpp") + +# Build the extension under the shared 'axom' nanobind domain so that C++ types bound in one Axom module +# (e.g. a sidre::Group*) are recognized by another module from the same build. +# nanobind only shares type bindings across modules that agree on domain *and* nanobind ABI, compiler, and build mode, +# hence the one-build/one-wheel rule. This mirrors src/axom/sidre/CMakeLists.txt. +nanobind_add_module(_sidre NB_DOMAIN axom ${_sidre_binding_sources}) + +# conduit::conduit_python provides conduit_python.hpp and is needed only by the binding TU, not by libsidre. +target_link_libraries(_sidre PRIVATE axom::sidre conduit::conduit_python) + +# Bake the linked libraries' locations into the module's rpath so it resolves libsidre/libconduit/HDF5 +# from the Axom/Conduit install with no LD_LIBRARY_PATH. +# pyproject also sets CMAKE_INSTALL_RPATH_USE_LINK_PATH for the wheel; +# this makes the standalone `cmake --install` path behave the same. +set_target_properties(_sidre PROPERTIES INSTALL_RPATH_USE_LINK_PATH TRUE) + +#------------------------------------------------------------------------------ +# HIP: replicate the in-tree special-case from src/axom/sidre/CMakeLists.txt. +# CMake treats MODULE libraries separately from executables, so HIP executable +# link flags are not applied to the module automatically. +#------------------------------------------------------------------------------ +if(AXOM_ENABLE_HIP) + set_source_files_properties(${_sidre_binding_sources} PROPERTIES LANGUAGE HIP) + string(REPLACE " " ";" _axom_py_module_link_flags "${CMAKE_EXE_LINKER_FLAGS}") + target_link_options(_sidre PRIVATE ${_axom_py_module_link_flags}) + unset(_axom_py_module_link_flags) +endif() + +#------------------------------------------------------------------------------ +# Type stubs (PEP 561). +# +# nanobind_add_stub imports the module ('import _sidre') to introspect it. +# +# Turn this off on hosts where the build-time import is problematic: +# -C cmake.define.AXOM_PYTHON_GENERATE_STUB=OFF +# The wheel still ships the checked-in package stub (axom/sidre/__init__.pyi) +# and py.typed via wheel.packages, so it stays a typed package either way; +# only the detailed _sidre.pyi (which __init__.pyi re-exports) is then absent. +#------------------------------------------------------------------------------ +option(AXOM_PYTHON_GENERATE_STUB + "Generate the _sidre.pyi type stub at build time (imports the module)" ON) + +if(AXOM_PYTHON_GENERATE_STUB) + nanobind_add_stub( + _sidre_stub + MODULE _sidre + OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_sidre.pyi" + PYTHON_PATH $ + DEPENDS _sidre) + + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/_sidre.pyi" + DESTINATION axom/sidre) +endif() + +#------------------------------------------------------------------------------ +# Install only build products here. +# The pure-Python package files +# (axom/__init__.py, axom/py.typed, axom/sidre/__init__.py, axom/sidre/__init__.pyi) +# ship as pure Python via [tool.scikit-build] wheel.packages in pyproject.toml, +# so they are not installed by CMake (avoiding double-packaging). +#------------------------------------------------------------------------------ +install(TARGETS _sidre LIBRARY DESTINATION axom/sidre) diff --git a/src/python/pyproject.toml b/src/python/pyproject.toml new file mode 100644 index 0000000000..311b08e27d --- /dev/null +++ b/src/python/pyproject.toml @@ -0,0 +1,101 @@ +# Copyright (c) Lawrence Livermore National Security, LLC and other +# Axom Project Contributors. See top-level LICENSE and COPYRIGHT +# files for dates and other details. +# +# SPDX-License-Identifier: (BSD-3-Clause) + +# Thin, binding-only wheel for Axom's Python package. +# +# This project compiles Axom's nanobind bindings against an already-installed Axom. +# It does not build Axom or any of its third-party libraries. +# Point it at an install with axom_DIR (NOT CMAKE_PREFIX_PATH, which scikit-build-core +# reserves for its own build environment -- see src/python/README.md): +# +# pip install ./src/python -C cmake.define.axom_DIR=$AXOM_INSTALL/lib/cmake +# +# The pure-Python package tree it wraps lives beside this file under src/ +# and has the same tree layout as the CMake-based installation (see src/python/README.md). + +[build-system] +# nanobind and scikit-build-core are build-time only. +# numpy is listed here too (in addition to [project.dependencies]) because the stub step +# imports the freshly-built module to introspect it, +# and nanobind's ndarray annotations need numpy importable at build time. +# +# conduit is deliberately not required at build time: +# import_conduit() runs lazily inside the Node conversion helpers, +# not at module import, so `import _sidre` does not touch it. +requires = ["scikit-build-core>=1.0", "nanobind>=2.7.0", "numpy>=1.22"] +build-backend = "scikit_build_core.build" + +[project] +name = "axom" +# Version is sourced dynamically from the C++ library's canonical version file (src/cmake/AxomVersion.cmake) +# at build time via [[tool.dynamic-metadata]] below, so the wheel version cannot drift from libaxom. +# axom.__init__ reads it back with importlib.metadata.version("axom") so the installed __version__ resolves. +dynamic = ["version"] +description = "Python bindings for LLNL Axom, a CS infrastructure library for HPC applications" +readme = "README.md" +requires-python = ">=3.10" +license = "BSD-3-Clause" +authors = [{ name = "Axom Project Contributors" }] +keywords = ["axom", "hpc"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "Programming Language :: C++", + "Programming Language :: Python :: 3", + "Topic :: Scientific/Engineering", +] +# conduit is a hard runtime dependency that intentionally does NOT appear here: +# the PyPI name 'conduit' is an unrelated package, and a pip-built Conduit would put a second, +# ABI-incompatible libconduit in the process (see src/python/README.md and the bindings design notes). +# The conduit Python module must come from the same Conduit build Axom links, exposed via a .pth file. +# Revisit if Conduit ever ships a thin, find_package-based binding wheel. +dependencies = ["numpy>=1.22"] + +[project.optional-dependencies] +# Wheel metadata is static, but MPI-ness is a build configuration: +# a wheel built from a +mpi Axom cannot force this extra at install time. +# The README and the import-error guidance tell users to `pip install 'axom[mpi]'` when needed. +mpi = ["mpi4py>=3.1"] +test = ["pytest"] + +[project.urls] +Homepage = "https://github.com/LLNL/axom" +Documentation = "https://axom.readthedocs.io" +Source = "https://github.com/LLNL/axom" + +# Source the wheel version from the C++ library's canonical version file, so it can never drift from libaxom. +# Build from a full repo checkout: the sdist does not carry this out-of-tree file, +# and standalone-sdist/PyPI is out of scope (see the sdist note under [tool.scikit-build]). +[[tool.dynamic-metadata]] +provider = "scikit_build_core.metadata.regex" +field = "version" +input = "../cmake/AxomVersion.cmake" +regex = '''(?sx) +set\( AXOM_VERSION_MAJOR \s+ (?P\d+) \) .*? +set\( AXOM_VERSION_MINOR \s+ (?P\d+) \) .*? +set\( AXOM_VERSION_PATCH \s+ (?P\d+) \) +''' +result = "{major}.{minor}.{patch}" + +[tool.scikit-build] +minimum-version = "build-system.requires" +build-dir = "build/{wheel_tag}" +# The 'axom' regular package (its __init__.py files, py.typed and the checked-in package stub) ships as pure Python from src/axom. +# The compiled extension and its generated _sidre.pyi are installed by CMakeLists.txt into axom/sidre/. +wheel.packages = ["src/axom"] +# NOTE on the sdist: the binding translation unit lives with its component (../axom/sidre/nanobind_sidre.cpp), outside this project directory. +# A `sdist.include = ["../axom/..."]` entry does NOT vendor it -- scikit-build-core restricts sdist contents to the project root +# and silently drops out-of-tree paths (verified with scikit-build-core 1.0.3). +# The resulting sdist is therefore not self-contained: the supported build paths +# compile from a full repo checkout (`pip install ./src/python`, `uv build src/python`), +# where CMakeLists.txt reads the TU from its on-disk relative path. +# Standalone-sdist / PyPI distribution is out of scope. +# If that ever changes, vendor the TU into this tree during a pre-sdist step (or move the project root above the TU). + +[tool.scikit-build.cmake.define] +# Resolve libsidre/libconduit/HDF5 from their install locations at runtime with no LD_LIBRARY_PATH. +CMAKE_INSTALL_RPATH_USE_LINK_PATH = "ON" + From 0492cb84e2973fbe3688c465f7947a0ca285aec8 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Tue, 28 Jul 2026 19:58:39 -0700 Subject: [PATCH 02/35] Python: Allow building the wheel against the stable ABI (abi3) --- src/python/CMakeLists.txt | 39 ++++++++++++++++++++++++++++++++++++--- src/python/pyproject.toml | 10 ++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 2521441e4b..964fe8afbf 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -27,9 +27,35 @@ cmake_minimum_required(VERSION 3.21) project(axom_python LANGUAGES CXX) +# Optional: build against Python's stable ABI (abi3) so one wheel serves every CPython >= 3.12 on the machine. +# Opt-in (default OFF) to provide per-Python wheel tags by default. +# Enable with BOTH build-time flags (they must agree; see pyproject.toml and src/python/README.md): +# -C cmake.define.AXOM_PYTHON_STABLE_ABI=ON -C wheel.py-api=cp312 +# nanobind silently builds a non-stable module below Python 3.12, +# so an abi3 wheel needs a 3.12+ interpreter (which also provides Development.SABIModule). +option(AXOM_PYTHON_STABLE_ABI + "Build the extension against Python's stable ABI (abi3); needs Python >= 3.12" OFF) + # Development.Module (not Development) so we link libpython-free extensions, -# matching the in-tree build's discovery. -find_package(Python 3.10 REQUIRED COMPONENTS Interpreter Development.Module) +# matching the in-tree build's discovery. Development.SABIModule is requested as +# an OPTIONAL component (nanobind's recommended pattern) so a stable-ABI build +# (AXOM_PYTHON_STABLE_ABI=ON, which needs Python >= 3.12) can find it, +# without a conditional component list. +find_package(Python 3.10 REQUIRED + COMPONENTS Interpreter Development.Module + OPTIONAL_COMPONENTS Development.SABIModule) + +# An optional component that is silently absent would turn a requested abi3 build +# into a confusing failure inside nanobind (or a non-stable module), so fail here +# with the actionable message instead. +if(AXOM_PYTHON_STABLE_ABI AND NOT Python_Development.SABIModule_FOUND) + message(FATAL_ERROR + "AXOM_PYTHON_STABLE_ABI=ON requires Python's Development.SABIModule " + "component (CPython >= 3.12), which was not found for " + "${Python_EXECUTABLE} (version ${Python_VERSION}). Build with a 3.12+ " + "interpreter or leave AXOM_PYTHON_STABLE_ABI off.") +endif() + find_package(axom CONFIG REQUIRED) find_package(nanobind CONFIG REQUIRED) # supplied via build-system.requires @@ -65,7 +91,14 @@ set(_sidre_binding_sources # (e.g. a sidre::Group*) are recognized by another module from the same build. # nanobind only shares type bindings across modules that agree on domain *and* nanobind ABI, compiler, and build mode, # hence the one-build/one-wheel rule. This mirrors src/axom/sidre/CMakeLists.txt. -nanobind_add_module(_sidre NB_DOMAIN axom ${_sidre_binding_sources}) +# +# STABLE_ABI is appended only when AXOM_PYTHON_STABLE_ABI is set +# nanobind degrades to a non-stable build below 3.12. +set(_axom_nb_module_args NB_DOMAIN axom) +if(AXOM_PYTHON_STABLE_ABI) + list(APPEND _axom_nb_module_args STABLE_ABI) +endif() +nanobind_add_module(_sidre ${_axom_nb_module_args} ${_sidre_binding_sources}) # conduit::conduit_python provides conduit_python.hpp and is needed only by the binding TU, not by libsidre. target_link_libraries(_sidre PRIVATE axom::sidre conduit::conduit_python) diff --git a/src/python/pyproject.toml b/src/python/pyproject.toml index 311b08e27d..eedf43eaea 100644 --- a/src/python/pyproject.toml +++ b/src/python/pyproject.toml @@ -94,6 +94,16 @@ wheel.packages = ["src/axom"] # where CMakeLists.txt reads the TU from its on-disk relative path. # Standalone-sdist / PyPI distribution is out of scope. # If that ever changes, vendor the TU into this tree during a pre-sdist step (or move the project root above the TU). +# +# STABLE_ABI / abi3 (cp312): opt-in, so the default build produces per-Python-version wheel tags. +# To build one abi3 wheel that serves every CPython >= 3.12 on the machine, pass BOTH of these at build time. +# They must agree, since the first makes nanobind build the limited-API module and the second sets the wheel tag: +# uv build --wheel -C cmake.define.AXOM_PYTHON_STABLE_ABI=ON -C wheel.py-api=cp312 ... +# +# wheel.py-api is intentionally left unset here rather than hard-coded to cp312: +# hard-coding it would tag every wheel cp312 even when built non-stable on < 3.12. +# wheel.py-api = "cp312" # set via -C wheel.py-api=cp312 in the abi3 build only + [tool.scikit-build.cmake.define] # Resolve libsidre/libconduit/HDF5 from their install locations at runtime with no LD_LIBRARY_PATH. From f63ff26aa3b82e6611a6b483ad25df133ab703ba Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Tue, 28 Jul 2026 20:09:23 -0700 Subject: [PATCH 03/35] Python: Adds CI plan to test Python installation with uv --- .github/workflows/ci-tests.yml | 24 ++++ .../github-actions/linux-wheel_and_test.sh | 124 ++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100755 scripts/github-actions/linux-wheel_and_test.sh diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 4879fad67a..1db4e7b4cb 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -118,6 +118,30 @@ jobs: with: name: Test Results ${{ matrix.build_type }} - ${{ matrix.config.job_name }} path: "**/Test.xml" + build_wheel_and_test: + # Build the thin pip/uv wheel (src/python) against a freshly installed Axom + # and run the Sidre Python suite from it without a wrapper or PYTHONPATH updates. + # Uses the nanobind-enabled gcc image + runs-on: ubuntu-24.04 + needs: + - set_image_vars + container: + image: ${{ needs.set_image_vars.outputs.gcc_docker_image }} + volumes: + - /home/axom/axom + # Required - default is set to user "axom" + options: --user root + steps: + - name: Checkout Axom + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + submodules: recursive + - name: Build wheel and test - gcc@13.3.1 + timeout-minutes: 80 + run: | + HOST_CONFIG=gcc@13.3.1.cmake \ + BUILD_TYPE=Debug \ + ./scripts/github-actions/linux-wheel_and_test.sh windows_build_and_test: runs-on: windows-latest strategy: diff --git a/scripts/github-actions/linux-wheel_and_test.sh b/scripts/github-actions/linux-wheel_and_test.sh new file mode 100755 index 0000000000..0fc6fb448e --- /dev/null +++ b/scripts/github-actions/linux-wheel_and_test.sh @@ -0,0 +1,124 @@ +#!/bin/bash +############################################################################## +# Copyright (c) Lawrence Livermore National Security, LLC and other +# Axom Project Contributors. See top-level LICENSE and COPYRIGHT +# files for dates and other details. +# +# SPDX-License-Identifier: (BSD-3-Clause) +############################################################################## + +# Build the thin, pip/uv-installable Axom wheel and exercise it end to end, +# without using the run_python_with_axom.sh wrapper or updating the PYTHONPATH: +# +# 1. configure + build + install Axom (with Python bindings) from a docker t-config; +# 2. build the wheel from src/python against that install (find_package(axom)); +# 3. install the wheel into a fresh uv venv; +# 4. expose the *same-build* Conduit python module via a .pth file +# (a pip-built Conduit would put a second, ABI-incompatible libconduit in the process, +# so we point at the Conduit the install links; see src/python/README.md); +# 5. run the Sidre Python test suite with plain `uv run pytest`. +# +# Intended for the gcc docker image, which is nanobind-enabled. + +set -e +set -o pipefail + +function or_die () { + "$@" + local status=$? + if [[ $status != 0 ]]; then + echo "ERROR $status command: $*" + exit $status + fi +} + +HOST_CONFIG="${HOST_CONFIG:-gcc@13.3.1.cmake}" +BUILD_TYPE="${BUILD_TYPE:-Debug}" +BUILD_DIR="${BUILD_DIR:-builddir_wheel}" + +echo "~~~~ helpful info ~~~~" +echo "USER=$(id -u -n)" +echo "PWD=$(pwd)" +echo "HOST_CONFIG=${HOST_CONFIG}" +echo "BUILD_TYPE=${BUILD_TYPE}" +echo "~~~~~~~~~~~~~~~~~~~~~~" + +NUM_BUILD_PROCS=$(python3 -c 'import os; print(max(2, os.cpu_count() * 8 // 10))') + +echo "~~~~~~ CONFIGURE + BUILD + INSTALL AXOM (+python) ~~~~~~" +or_die python3 ./config-build.py \ + -bp "${BUILD_DIR}" \ + -hc "./host-configs/docker/${HOST_CONFIG}" \ + -bt "${BUILD_TYPE}" +or_die cmake --build "${BUILD_DIR}" -j "${NUM_BUILD_PROCS}" +or_die cmake --install "${BUILD_DIR}" + +# Resolve the Axom install prefix and the Conduit python-modules directory from +# the CMake cache. Prefer CONDUIT_PYTHON_MODULE_DIR (the exact directory the +# in-tree build uses for the same purpose); fall back to CONDUIT_DIR/python-modules. +CACHE="${BUILD_DIR}/CMakeCache.txt" +AXOM_INSTALL=$(awk -F= '/^CMAKE_INSTALL_PREFIX:[A-Z]*=/{print $2}' "${CACHE}") +CONDUIT_PY_DIR=$(awk -F= '/^CONDUIT_PYTHON_MODULE_DIR:[A-Z]*=/{print $2}' "${CACHE}") +if [[ -z "${CONDUIT_PY_DIR}" ]]; then + CONDUIT_DIR=$(awk -F= '/^CONDUIT_DIR:[A-Z]*=/{print $2}' "${CACHE}") + CONDUIT_PY_DIR="${CONDUIT_DIR}/python-modules" +fi +echo "AXOM_INSTALL=${AXOM_INSTALL}" +echo "CONDUIT_PY_DIR=${CONDUIT_PY_DIR}" + +if [[ -z "${AXOM_INSTALL}" || ! -d "${AXOM_INSTALL}" ]]; then + echo "ERROR: Axom install prefix not found (${AXOM_INSTALL})." + exit 1 +fi +if [[ -z "${CONDUIT_PY_DIR}" || ! -d "${CONDUIT_PY_DIR}" ]]; then + echo "ERROR: Conduit python-modules dir not found (${CONDUIT_PY_DIR})." + echo " The wheel needs the same-build Conduit python module (see src/python/README.md)." + exit 1 +fi + +echo "~~~~~~ ENSURE uv IS AVAILABLE ~~~~~~" +if ! command -v uv >/dev/null 2>&1; then + or_die python3 -m pip install --user uv + export PATH="${HOME}/.local/bin:${PATH}" +fi +uv --version + +echo "~~~~~~ BUILD THE THIN WHEEL FROM src/python ~~~~~~" +# Point find_package at the install with axom_DIR. +# Don't use CMAKE_PREFIX_PATH since scikit-build-core force-sets that to its isolated build environment +# (and uses it to locate its own nanobind). +# Conduit resolves transitively from axom's config, which records its Conduit prefix; +# pass -C cmake.define.Conduit_DIR=... as well if that recorded path has moved. See src/python/README.md. +rm -rf dist +or_die uv build --wheel \ + -C cmake.define.axom_DIR="${AXOM_INSTALL}/lib/cmake" \ + --out-dir dist \ + src/python +ls -l dist + +echo "~~~~~~ FRESH VENV + INSTALL THE WHEEL ~~~~~~" +# Pin the interpreter that built the wheel, so the venv cannot pick a different one. +VENV_DIR=/tmp/axom-wheel-venv +rm -rf "${VENV_DIR}" +or_die uv venv --python "$(command -v python3)" "${VENV_DIR}" +VENV_PY="${VENV_DIR}/bin/python" +or_die uv pip install --python "${VENV_PY}" dist/*.whl + +echo "~~~~~~ EXPOSE SAME-BUILD CONDUIT VIA .pth ~~~~~~" +PURELIB=$("${VENV_PY}" -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])') +echo "${CONDUIT_PY_DIR}" > "${PURELIB}/conduit.pth" +echo "wrote ${PURELIB}/conduit.pth -> ${CONDUIT_PY_DIR}" + +echo "~~~~~~ IMPORT SMOKE TEST (no wrapper, no PYTHONPATH) ~~~~~~" +or_die "${VENV_PY}" -c \ + "import axom, axom.sidre, conduit, numpy; print('axom', axom.__version__); print('axom.sidre', axom.sidre.__version__)" + +echo "~~~~~~ RUN THE SIDRE PYTHON SUITE VIA PLAIN pytest ~~~~~~" +# Axom's Python tests are named *_Py.py, which pytest's default python_files patterns +# (test_*.py, *_test.py) do not match -- an unqualified run collects nothing and exits 5. +# Name the pattern explicitly so collection is deterministic. +# The MPI-only spio test skips itself at module level when sidre was built without MPI. +or_die uv pip install --python "${VENV_PY}" pytest +or_die "${VENV_PY}" -m pytest -s -p no:cacheprovider \ + -o python_files='*_Py.py' \ + src/axom/sidre/tests/ From fe4686d3bea14df270cf2585aa209a935f50f040 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Tue, 28 Jul 2026 20:37:11 -0700 Subject: [PATCH 04/35] Python: Updates user docs and README about uv setup --- .../sidre/docs/sphinx/python_interface.rst | 102 ++++++- src/python/README.md | 250 +++++++++++++++++- 2 files changed, 333 insertions(+), 19 deletions(-) diff --git a/src/axom/sidre/docs/sphinx/python_interface.rst b/src/axom/sidre/docs/sphinx/python_interface.rst index f0c91e283a..907dab7423 100644 --- a/src/axom/sidre/docs/sphinx/python_interface.rst +++ b/src/axom/sidre/docs/sphinx/python_interface.rst @@ -89,19 +89,95 @@ After ``spack install``, the environment's interpreter should have a working Axo pip / uv wheel (thin, external Axom) ------------------------------------ -.. note:: - - The pip/uv-installable wheel is planned and not yet available. - This section is a placeholder for the workflow it will enable. - Until it lands, use the build-tree helper for development builds - or a dedicated Spack environment view for installed-package testing. - -The wheel will compile only the binding code against an already-installed Axom -(located via ``CMAKE_PREFIX_PATH``); it will not build Axom or its third-party -libraries. Because a pip-built Conduit would produce a second, ABI-incompatible -``libconduit`` in the same process, the wheel will rely on the Conduit Python -module from the same Axom/Conduit build, exposed via a ``.pth`` file rather -than a PyPI install. +The wheel compiles only the binding code against an already-installed Axom and Conduit; +it does not build Axom or its third-party libraries. +Because a pip-built Conduit would produce a second, ABI-incompatible ``libconduit`` in the same process, +the wheel relies on the Conduit Python module from the same Axom/Conduit build, +exposed via a ``.pth`` file rather than a PyPI install. +A wheel is therefore specific to the toolchain/glibc of the Axom install it was built against. +These wheels are not portable and not intended for PyPI; +they target controlled environments (an LC host-config, a spack view, or a CI image). + +Quick start +^^^^^^^^^^^ + +Point the build at the Axom install with ``axom_DIR``. +Conduit is located transitively through Axom's own CMake config, so it normally needs no flag of its own: + +.. code-block:: bash + + # 1. A venv on the same interpreter Axom and Conduit were built against. + $ uv venv --python $(which python3) + + # 2a. Install a prebuilt wheel from a per-host-config wheelhouse... + $ uv pip install axom --find-links /path/to/wheelhouse/ + + # 2b. ...or build it from a source checkout against your Axom install. + $ uv pip install /path/to/axom/src/python \ + -C cmake.define.axom_DIR="$AXOM_INSTALL/lib/cmake" + + # 3. Expose the *same-build* Conduit Python module (never a PyPI 'conduit'). + $ echo "$CONDUIT_INSTALL/python-modules" > \ + "$(uv run python -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')/conduit.pth" + + # 4. Verify -- no PYTHONPATH and no wrapper script. + $ uv run python -c "import axom.sidre, conduit, numpy; print(axom.__version__)" + +Three details matter when building the wheel yourself: + +* Use ``axom_DIR``, not ``CMAKE_PREFIX_PATH``. scikit-build-core (which backs + ``uv build`` and ``uv pip install``) force-sets ``CMAKE_PREFIX_PATH`` to its own + isolated build environment, so a user-supplied value would be overwritten and + ``find_package(axom)`` would fail. A standalone ``cmake -S src/python`` has no such + layer and can use ``CMAKE_PREFIX_PATH`` directly. +* Add ``-C cmake.define.Conduit_DIR="$CONDUIT_INSTALL/lib/cmake/conduit"`` only if + Conduit has moved since Axom was installed: Axom's config records the Conduit + prefix it was built against, and that recorded path is what the transitive lookup uses. +* Build from the source tree that produced the install. + The wheel takes its version from ``src/cmake/AxomVersion.cmake`` in the checkout, + and the build fails with an explicit message if that disagrees with the installed Axom, + so a wheel can never misreport the version of the binary inside it. + +Using Axom in Jupyter +^^^^^^^^^^^^^^^^^^^^^ + +Because the wheel and the Conduit ``.pth`` live in the venv's ``site-packages``, +a Jupyter kernel running in that venv imports ``axom.sidre`` natively -- there is +nothing extra to configure, and no need to modify ``PYTHONPATH``. +Add Jupyter to the same venv and register it as a kernel: + +.. code-block:: bash + + $ uv pip install jupyterlab ipykernel + $ uv run python -m ipykernel install --user --name axom --display-name "Axom (uv)" + $ uv run jupyter lab + +Select the **Axom (uv)** kernel, then for example: + +.. code-block:: python + + import axom.sidre as sidre + import numpy as np + + ds = sidre.DataStore() + grp = ds.getRoot().createGroup("fields") + view = grp.createViewAndAllocate("velocity", sidre.TypeID.FLOAT64_ID, 4) + np.asarray(view.getDataArray())[:] = [1.0, 2.0, 3.0, 4.0] # zero-copy view + print(np.asarray(grp.getView("velocity").getDataArray())) + +If the kernel cannot import ``axom.sidre``, it is nearly always either the wrong kernel +(one outside the venv) or a missing Conduit ``.pth``. Check both from inside the notebook: + +.. code-block:: python + + import sys; print(sys.executable) # expect /bin/python + import conduit; print(conduit.__file__) # expect $CONDUIT_INSTALL/python-modules/... + +If the underlying Axom is an MPI build and you need to pass a communicator to +``IOManager`` (or to initialize MPI), install the ``mpi`` extra with ``uv pip install 'axom[mpi]'``. + +For the per-host-config wheelhouse convention, the editable developer loop, +and the optional stable-ABI build, see ``src/python/README.md``. ==================================== Working with Conduit and NumPy diff --git a/src/python/README.md b/src/python/README.md index 92125f71c9..c2270731d3 100644 --- a/src/python/README.md +++ b/src/python/README.md @@ -17,9 +17,123 @@ It is consumed by two independent build paths that must produce the same on-disk (so the build tree is import-ready) and installs them under `AXOM_PYTHON_MODULE_INSTALL_PREFIX`. The compiled extension (`_sidre`) and its type stub are emitted into this layout by the build; they are not checked in. -2. **[planned] The pip/uv wheel (out of tree).** A thin, binding-only wheel built with scikit-build-core - will treat this directory as its package root (`wheel.packages = ["src/axom"]` in a sibling `pyproject.toml`), - compiling the binding translation unit against an already-installed Axom. +2. **The pip/uv wheel (out of tree).** A thin, binding-only wheel built with scikit-build-core + (the `pyproject.toml` and `CMakeLists.txt` beside this file). It treats this directory as its + package root (`wheel.packages = ["src/axom"]`) and compiles the binding translation unit against + an already-installed Axom. See "Building and installing the wheel" below. + + +## Two ways to get the Python interface + +The two paths named above differ only in *who* compiles the `_sidre` extension and +*how you import it*. They compile the **same** binding translation unit +(`src/axom/sidre/nanobind_sidre.cpp`) under the **same** nanobind domain +(`NB_DOMAIN axom`) and ship the **same** pure-Python tree from this directory, +so `import axom.sidre` behaves identically either way. Both also stand on top of a +fully built, installed Axom plus a matching Conduit -- neither path builds Axom's +C++ libraries or its third-party libraries (see "What `uv` builds" below). + +### Path A -- in-tree CMake build, imported via `PYTHONPATH` + +Enable the bindings in the same CMake build that compiles Axom +(a Python interpreter must be found; currently only Sidre is bound). +The `_sidre` extension is built next to `libaxom`/`libsidre`, staged into `/python/axom/sidre/`, +and installed under `AXOM_PYTHON_MODULE_INSTALL_PREFIX` (default `lib/python/site-packages`). +To use it, put that directory -- plus Conduit's Python-module dir and numpy -- on `PYTHONPATH`, and `import axom.sidre` works. + +The build configures a convenience wrapper that assembles that environment from the spack prefixes, +so an ad hoc script "just works" without a venv: + +```bash +# runs the build's Python with axom + conduit + numpy (+ mpi4py) already on PYTHONPATH +/bin/run_python_with_axom.sh my_script.py +``` + +This is the natural path during Axom development since it doesn't require a separate packaging step, +and rebuilding Axom rebuilds the bindings in place. +The wrapper is bash-only and does not compose with Jupyter kernels, IDE runners, or debuggers. +For those, use the uv environment in **Quick start** below. + +### Path B -- thin pip/uv wheel, imported into a venv + +Build and install Axom first (the normal CMake/spack path, bindings enabled), +then build a **binding-only** wheel against that install and install it into a virtual environment: + +```bash +# Axom + Conduit already built and installed; compile just the bindings against them. +# Point find_package at the install with axom_DIR +uv build --wheel -C cmake.define.axom_DIR="$AXOM_INSTALL/lib/cmake" src/python +uv pip install dist/axom-*.whl # then expose the same-build Conduit; see below +``` + +The wheel's `CMakeLists.txt` runs `find_package(axom CONFIG REQUIRED)` and +compiles only `nanobind_sidre.cpp` -- it consumes the install, but does not rebuild it. +This is the path for distributing or consuming the bindings in an ordinary Python environment. +The full uv workflows (wheelhouse install, editable rebuild loop, the same-build-Conduit `.pth`, +and the matching-interpreter constraint) are in "Building and installing the wheel" below. + +### What `uv` builds -- and what it does not + +`uv build` (through scikit-build-core) runs **only** the wheel's `CMakeLists.txt`, +which compiles the single binding TU and links it against an already-installed +`axom::sidre` and `conduit::conduit_python`. +It does **not** build: + +- **Axom's C++ libraries** -- supplied by the `find_package(axom)` install. +- **Third-party libraries** (Conduit, HDF5, RAJA, Umpire, MPI, ...) -- provisioned + by spack and not pip-installable. Conduit especially is deliberately *not* a wheel dependency; + its Python module reaches the venv through a `.pth` pointing at the same-build Conduit + (a `pip install conduit` is an unrelated, ABI-incompatible package). + +By design, `uv` does not generate the Axom libraries directly: +Axom and its TPLs come from the CMake/spack world, and `uv` adds only the thin binding layer on top. +Keeping the wheel thin makes it fast and reproducible against a known install. + +## Quick start: an Axom environment with uv (optionally for Jupyter) + +The happy path for *using* the bindings from an ordinary Python environment (Path B). +It assumes Axom was already built and installed with its Python bindings enabled at `$AXOM_INSTALL`, +against a Conduit at `$CONDUIT_INSTALL`. +For the per-host-config wheelhouse, the editable developer loop, and the reasoning behind each step, +see "Building and installing the wheel" below. + +```bash +# 1. A venv on the SAME interpreter your Axom/Conduit were built against. +uv venv --python $(which python3) + +# 2. Install the axom wheel -- either from a prebuilt per-host-config wheelhouse: +uv pip install axom --find-links /path/to/wheelhouse/ +# ...or build it from the source checkout against your install: +uv pip install /path/to/axom/src/python \ + -C cmake.define.axom_DIR="$AXOM_INSTALL/lib/cmake" + +# 3. Expose the *same-build* Conduit Python module (never a PyPI 'conduit') +# with a one-line .pth dropped into the venv's site-packages: +echo "$CONDUIT_INSTALL/python-modules" > \ + "$(uv run python -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')/conduit.pth" + +# 4. Verify -- no PYTHONPATH needed; the installed wheel and the .pth do the work. +uv run python -c "import axom.sidre, conduit, numpy; print(axom.__version__)" +``` + +That environment works anywhere the venv's interpreter runs, +e.g. plain scripts, an IDE, a debugger, and Jupyter. + +### Using it in Jupyter + +Because the wheel and the Conduit `.pth` live in the venv's `site-packages`, +a Jupyter kernel *running in that venv* imports `axom.sidre` natively. +There is nothing extra to wire up. Add Jupyter to the same venv and register it as a kernel: + +```bash +uv pip install jupyterlab ipykernel +uv run python -m ipykernel install --user --name axom --display-name "Axom (uv)" +uv run jupyter lab +``` + +Then select the **Axom (uv)** kernel. See the Sidre user guide's "Python interface page" +for a worked notebook example and the two one-line checks to run when a kernel cannot import `axom.sidre` +(`src/axom/sidre/docs/sphinx/python_interface.rst`). ## Layout @@ -29,6 +143,8 @@ This is a standard "src layout" Python project root: ``` src/python/ README.md <- this file + pyproject.toml <- scikit-build-core project for the pip/uv wheel + CMakeLists.txt <- wheel build: finds an installed Axom, builds the extension src/ axom/ <- the 'axom' regular package __init__.py <- top-level package metadata @@ -55,9 +171,131 @@ A submodule is importable only when its component was enabled in the underlying generated artifacts (the `.so` and `.pyi` are produced by the build), and tests/examples (those live under the component, e.g. `src/axom/sidre/tests/*_Py.py`). +## Building and installing the wheel + +The wheel is thin: it compiles only the binding code against an already-installed Axom and Conduit. +It never builds Axom or its third-party libraries, so a wheel is specific to the +Axom install (host-config / toolchain / glibc) it was built against. + +These wheels are not portable and not intended for PyPI. +They carry absolute rpaths to the install's shared libraries +and skip the `auditwheel` / `delocate` repair a redistributable `manylinux` wheel needs; +they target controlled environments -- an LC host-config, a spack view, a CI image. +Producing portable, many-platform wheels would additionally need a tool such as `cibuildwheel` +plus a bundling/repair step, which is out of scope here. + +**Pointing the build at the install.** Pass one flag, +`-C cmake.define.axom_DIR="$AXOM_INSTALL/lib/cmake"`. Conduit needs no flag of its +own in the common case: `find_package(axom CONFIG)` pulls in Conduit via +`find_dependency`, using the Conduit prefix recorded in `axom-config.cmake` when +Axom was installed. +Pass `-C cmake.define.Conduit_DIR="$CONDUIT_INSTALL/lib/cmake/conduit"` only if +that recorded path no longer resolves (e.g. a relocated install, a different mount, +or a container path). + +Do *not* use `CMAKE_PREFIX_PATH`. Under scikit-build-core (which drives `uv build` and `uv pip install`) +it is force-set to the isolated build environment -- that is how the build locates its own bundled `nanobind` -- +so a user-supplied value would be overwritten and ignored, and `find_package(axom)` +would fail with a "Could not find axom" message. + +**One more precondition.** Build the wheel from the source tree that produced the install: +the wheel's version comes from this checkout's `src/cmake/AxomVersion.cmake` +while the extension links the installed Axom, so the build fails with an explicit message +if the two versions disagree rather than shipping a wheel whose `axom.__version__` misreports its own binary. + +Two constraints apply to every workflow below: + +- **Same-build Conduit.** The bindings exchange `conduit::Node`s with the `conduit` Python module + through Conduit's C capsule API, so that module must wrap the *same* `libconduit` the install links. + A `pip install conduit` is unsafe (unrelated PyPI package; a pip-built Conduit yields a second, + ABI-incompatible `libconduit`). Expose the install's own Conduit instead, + via a one-line `.pth` file (shown below). +- **Matching interpreter.** Build with the interpreter family whose toolchain/glibc matches the host-config. + On LC, pin it explicitly: `uv venv --python $(which python3)`. + +### 1. User install from a per-host-config wheelhouse + +Prebuilt wheels live in a per-host-config wheelhouse, e.g. `/usr/workspace/<...>/wheelhouse//` +(one directory per host-config, since a wheel is not portable across toolchains). +Consume it with `--find-links` (or a `[tool.uv.sources]` entry): + +```bash +uv venv --python $(which python3) +uv pip install axom --find-links /path/to/wheelhouse/dane-gcc13 +``` + +Then add the Conduit `.pth` and verify exactly as in **Quick start** steps 3--4. + +### 2. Build from source against an existing Axom install + +```bash +uv pip install /path/to/axom/src/python \ + -C cmake.define.axom_DIR="$AXOM_INSTALL/lib/cmake" +``` + +`$AXOM_INSTALL` is a spack/uberenv-built Axom prefix (or any `cmake --install` tree) +whose `+python` bindings were enabled. Add the Conduit `.pth` afterward, as in **Quick start** step 3, +and add `Conduit_DIR` only if Conduit has moved since Axom was installed. + +### 3. Developer loop (editable, rebuild-on-import) + +nanobind's recommended editable flow rebuilds the extension automatically when +you re-import it after editing the binding source. Rebuild-on-import is a +scikit-build-core *experimental* feature (`editable.rebuild=true`) and may change; +if it misbehaves, reinstall the editable wheel to force a rebuild: + +```bash +uv pip install nanobind 'scikit-build-core[pyproject]' +uv pip install -e src/python --no-build-isolation \ + -C cmake.define.axom_DIR="$AXOM_INSTALL/lib/cmake" \ + -C build-dir=build/py -C editable.rebuild=true +uv run pytest src/axom/sidre/tests -k _Py +``` + +### Stable ABI (abi3) + +By default the wheel is tagged for the exact CPython that built it. +Opt into a single abi3 wheel that serves every CPython >= 3.12 on the machine +by passing both flags together (the CMake option makes nanobind build the limited-API module; +the scikit-build-core setting sets the wheel tag, and the two must agree): + +```bash +uv build --wheel \ + -C cmake.define.AXOM_PYTHON_STABLE_ABI=ON \ + -C wheel.py-api=cp312 \ + -C cmake.define.axom_DIR="$AXOM_INSTALL/lib/cmake" \ + src/python +``` + +Below Python 3.12 nanobind silently builds a non-stable module, so only enable this on a 3.12+ interpreter; +the build fails with an explicit message if the interpreter does not provide `Development.SABIModule`, +rather than quietly producing a mislabelled wheel. Stable ABI relaxes the Python-version coupling, +not the toolchain coupling: an abi3 wheel is still specific to the host-config it was built against. + +### Free-threaded Python (abi3t) + +Not built today. scikit-build-core 1.0+ can emit free-threaded stable-ABI wheels +(`abi3t`, e.g. a `cp313t` / `cp315t` tag) once the bindings and Conduit run under +a free-threaded interpreter. Revisit if/when Axom targets free-threaded Python; +no action is needed for the GIL-enabled builds above. + +### MPI + +Wheel metadata is static, but whether the underlying Axom is an MPI build is a build-time choice, +so the wheel cannot force the MPI dependency at install time. +When you need mpi4py (to pass a communicator to `IOManager`, or to initialize MPI), install the extra explicitly: + +```bash +uv pip install 'axom[mpi]' +``` + +pytest lives in the `test` extra (`uv pip install 'axom[test]'`), +never in the runtime dependencies. + ## Notes - These files are installed verbatim (no template substitution). They contain no CMake-configured values. -- A `pyproject.toml` for the standalone wheel is not present yet. We will add it in the future when we add the wheel. - Until then this directory is consumed only by the CMake build. -- End-user instructions for installing and importing the bindings currently live in the Sidre user guide's "Python interface" page. +- This directory doubles as the root of the pip/uv wheel project (`pyproject.toml` + `CMakeLists.txt`), + which reuses the very files above, so the two delivery modes cannot drift. +- End-user instructions for installing and importing the bindings also live in the Sidre user guide's + "Python interface" page. From 956663be93f399aeacefe98e6ade87dcecee7668 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Tue, 28 Jul 2026 20:52:01 -0700 Subject: [PATCH 05/35] Sidre/Python bugfix: Do not pin external views onto Sidre-owned storage Otherwise, we could produce a cycle that would retain memory until the end of the execution. --- src/axom/sidre/nanobind_sidre.cpp | 65 +++++++++++++++++++++++ src/axom/sidre/tests/sidre_lifetime_Py.py | 55 +++++++++++++++++++ 2 files changed, 120 insertions(+) diff --git a/src/axom/sidre/nanobind_sidre.cpp b/src/axom/sidre/nanobind_sidre.cpp index 464d30398e..b6813f892c 100644 --- a/src/axom/sidre/nanobind_sidre.cpp +++ b/src/axom/sidre/nanobind_sidre.cpp @@ -11,6 +11,8 @@ #include #include +#include +#include #include #include #include @@ -351,12 +353,67 @@ DataStore* owningDataStore(View* view) //! Erase all pins recorded for \a ds (called when the DataStore is collected). void releaseDataStoreExternalPins(DataStore* ds) { externalDataOwnerRegistry().erase(ds); } +//! Release the pin recorded for \a view, if any (defined below). +void releaseExternalDataOwner(View* view); + +/*! + * \brief True when \a ptr already points into storage owned by a Buffer of \a ds. + * + * Such storage cannot dangle: Sidre owns it, and it outlives any Python proxy. + * Pinning it would cause problems, as described on pinExternalDataOwner() below. + * + * \note The scan is linear in the number of Buffers in \a ds and runs once per external-data pin + * (i.e. per createView/setExternalData call that supplies an ndarray), so creating many external views + * in a DataStore that also holds many Buffers could be expensive. + * + * \note Scoped to Buffers of \a ds only. A pointer into another DataStore's Buffer + * is not tracked by this registry, and would still need a pin. + */ +bool isOwnedByDataStoreBuffer(DataStore* ds, const void* ptr) +{ + if(ds == nullptr || ptr == nullptr) + { + return false; + } + + const auto p = reinterpret_cast(ptr); + for(auto& buffer : ds->buffers()) + { + const void* base_ptr = buffer.getVoidPtr(); + if(base_ptr == nullptr) + { + continue; + } + const auto base = reinterpret_cast(base_ptr); + const auto bytes = static_cast(buffer.getTotalBytes()); + if(p >= base && p < base + bytes) + { + return true; + } + } + return false; +} + /*! * \brief Record \a owner as the pin for \a view, scoped to its DataStore. * * On the first pin into a given DataStore, installs a weak reference on the * DataStore's Python object so the sub-map is cleared when the DataStore is * destroyed. Re-assigning a View*'s pin releases the previous ndarray wrapper. + * + * \note Storage that Sidre already owns is deliberately *not* pinned. + * Pinning it would create a reference cycle that this registry cannot break: + * the pin holds a strong reference to the ndarray, an ndarray produced by Buffer/View.getDataArray() + * transitively holds a strong reference to that Sidre object's Python wrapper, + * and that wrapper keeps the DataStore's Python object alive. But this is the + * object whose collection is supposed to fire the weakref callback that releases the pin. + * The cycle runs through this C++ registry, so Python's cyclic collector cannot see or break it, + * and the DataStore, Group, View and Buffer would be retained for the life of the process + * (nanobind reports them at shutdown as leaked instances). + * The idiom that triggers it is common: `data = view.getBuffer().getDataArray()` + * followed by `group.createView("name", data)`. Skipping the pin is safe because the + * Buffer owns that storage; the dangling-pointer hazard the pin exists to prevent + * only arises for storage owned by a Python object. */ void pinExternalDataOwner(View* view, const nb::ndarray<>& owner) { @@ -370,6 +427,14 @@ void pinExternalDataOwner(View* view, const nb::ndarray<>& owner) return; } + // Sidre-owned storage needs no pin, and pinning it would leak the DataStore + if(isOwnedByDataStoreBuffer(ds, owner.data())) + { + // Drop any pin a previous, non-Sidre-owned array left on this View. + releaseExternalDataOwner(view); + return; + } + DataStoreExternalPins& entry = externalDataOwnerRegistry()[ds]; if(!entry.datastore_weakref.is_valid()) { diff --git a/src/axom/sidre/tests/sidre_lifetime_Py.py b/src/axom/sidre/tests/sidre_lifetime_Py.py index 2196179523..d818ff68d8 100644 --- a/src/axom/sidre/tests/sidre_lifetime_Py.py +++ b/src/axom/sidre/tests/sidre_lifetime_Py.py @@ -891,6 +891,61 @@ def test_concurrent_datastores_registry_isolation(): assert ref2() is None, "DS2 external data not collected after destroyView" +# --------------------------------------------------------------------------- +# External views onto sidre-owned storage must not pin their own DataStore +# --------------------------------------------------------------------------- +# The binding pins the numpy owner of an external view so a dropped temporary +# cannot leave sidre holding a dangling pointer. Storage that sidre already owns +# must be exempt: pinning it forms a cycle the registry cannot break (pin -> array +# -> sidre wrapper -> DataStore python object, whose collection is what releases +# the pin), retaining the DataStore, Group, View and Buffer for the life of the process. +def test_opaque_view_onto_sidre_storage_does_not_retain_datastore(): + ds = sidre.DataStore() + root = ds.getRoot() + field = root.createViewAndAllocate("field", sidre.TypeID.FLOAT64_ID, 20) + data = field.getBuffer().getDataArray() + + ref = weakref.ref(ds) + root.createView("aliased", data) # undescribed/opaque overload + + del data, field, root, ds + _force_gc() + + assert ref() is None, "DataStore retained by a pin onto sidre-owned storage" + + +def test_described_external_view_onto_sidre_storage_does_not_retain_datastore(): + ds = sidre.DataStore() + root = ds.getRoot() + field = root.createViewAndAllocate("field", sidre.TypeID.FLOAT64_ID, 20) + data = field.getBuffer().getDataArray() + + ref = weakref.ref(ds) + root.createView("aliased", sidre.TypeID.FLOAT64_ID, 20, data) + + del data, field, root, ds + _force_gc() + + assert ref() is None, "DataStore retained by a pin onto sidre-owned storage" + + +def test_external_view_onto_sidre_storage_still_reads_correctly(): + # The exemption removes the pin, not the aliasing: + # the view must still read the buffer it points into. + ds = sidre.DataStore() + root = ds.getRoot() + field = root.createViewAndAllocate("field", sidre.TypeID.FLOAT64_ID, 8) + data = field.getBuffer().getDataArray() + data[:] = np.arange(8) + 1.0 + + view = root.createView("aliased", sidre.TypeID.FLOAT64_ID, 8, data) + del data + _force_gc() + + assert view.getDataArray()[0] == 1.0 + assert view.getDataArray()[7] == 8.0 + + if __name__ == "__main__": import sys From eb28a25bc0d3e7518f7ba1188fb08f879103835c Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 09:48:36 -0700 Subject: [PATCH 06/35] sidre: Fixes headers in nanobind_sidre They paths need to be relative to both the build and install root. --- src/axom/sidre/nanobind_sidre.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/axom/sidre/nanobind_sidre.cpp b/src/axom/sidre/nanobind_sidre.cpp index b6813f892c..cc1c2821e1 100644 --- a/src/axom/sidre/nanobind_sidre.cpp +++ b/src/axom/sidre/nanobind_sidre.cpp @@ -21,13 +21,13 @@ #include "axom/core/Types.hpp" #include "axom/slic/interface/slic.hpp" -#include "core/SidreTypes.hpp" -#include "core/Buffer.hpp" -#include "core/View.hpp" -#include "core/DataStore.hpp" -#include "core/Group.hpp" +#include "axom/sidre/core/SidreTypes.hpp" +#include "axom/sidre/core/Buffer.hpp" +#include "axom/sidre/core/View.hpp" +#include "axom/sidre/core/DataStore.hpp" +#include "axom/sidre/core/Group.hpp" #if defined(AXOM_USE_MPI) - #include "spio/IOManager.hpp" + #include "axom/sidre/spio/IOManager.hpp" #endif // Separate Conduit header for python functionality From 68b79af7abb2a3ce3f6868e81bc2876a80b6ae7d Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 10:50:40 -0700 Subject: [PATCH 07/35] Sidre/Python: Improves lifetime tests --- src/axom/sidre/tests/sidre_lifetime_Py.py | 67 +++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/src/axom/sidre/tests/sidre_lifetime_Py.py b/src/axom/sidre/tests/sidre_lifetime_Py.py index d818ff68d8..8ad8e4b76d 100644 --- a/src/axom/sidre/tests/sidre_lifetime_Py.py +++ b/src/axom/sidre/tests/sidre_lifetime_Py.py @@ -946,6 +946,73 @@ def test_external_view_onto_sidre_storage_still_reads_correctly(): assert view.getDataArray()[7] == 8.0 +# The exemption lives in one place (pinExternalDataOwner), so every entry point +# that pins inherits it. Cover the two that createView does not reach, and the +# boundary the exemption must not cross. +def test_set_external_data_onto_sidre_storage_does_not_retain_datastore(): + ds = sidre.DataStore() + root = ds.getRoot() + field = root.createViewAndAllocate("field", sidre.TypeID.FLOAT64_ID, 8) + data = field.getBuffer().getDataArray() + + target = root.createView("aliased") + target.setExternalData(sidre.TypeID.FLOAT64_ID, 8, data) + + ref = weakref.ref(ds) + del target, data, field, root, ds + _force_gc() + + assert ref() is None, "DataStore retained by a setExternalData pin onto its own storage" + + +def test_copied_view_onto_sidre_storage_does_not_retain_datastore(): + ds = sidre.DataStore() + root = ds.getRoot() + field = root.createViewAndAllocate("field", sidre.TypeID.FLOAT64_ID, 8) + data = field.getBuffer().getDataArray() + source = root.createView("aliased", sidre.TypeID.FLOAT64_ID, 8, data) + + # copyView re-pins the destination from the source's pin; an exempt source has + # no pin to copy, so the destination must not acquire one either. + root.createGroup("copy_target").copyView(source) + + ref = weakref.ref(ds) + del source, data, field, root, ds + _force_gc() + + assert ref() is None, "DataStore retained by a copied view's pin onto its own storage" + + +def test_external_view_onto_another_datastores_storage_is_still_pinned(): + # The exemption is per-DataStore: a view in the `consumer` DataStore pointing at storage + # owned by the `donor` DataStore is not exempt and must still be pinned. + # The observable consequence is that the pin keeps the donor alive. + donor = sidre.DataStore() + donor_field = donor.getRoot().createViewAndAllocate("field", sidre.TypeID.FLOAT64_ID, 8) + data = donor_field.getBuffer().getDataArray() + data[:] = np.arange(8) + 1.0 + + consumer = sidre.DataStore() + view = consumer.getRoot().createView("aliased", sidre.TypeID.FLOAT64_ID, 8, data) + + donor_ref = weakref.ref(donor) + del data, donor_field, donor + _force_gc() + + assert donor_ref() is not None, "aliased donor storage was not pinned by the consuming view" + assert view.getDataArray()[0] == 1.0 + assert view.getDataArray()[7] == 8.0 + + # Pinning across DataStores must not make the *consumer* immortal: + # its pin references the donor, not itself, so collecting it releases the donor too. + consumer_ref = weakref.ref(consumer) + del view, consumer + _force_gc() + + assert consumer_ref() is None, "consumer DataStore retained by its own external-data pin" + assert donor_ref() is None, "donor storage still pinned after the consuming view went away" + + if __name__ == "__main__": import sys From a653809b98513665b2de57136f4be76d088f8fd9 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 10:52:20 -0700 Subject: [PATCH 08/35] Adds `build/` to .gitignore This directory is used by the Python installation (e.g. for wheels). --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e918142159..a0b27222da 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ install- .project .settings build-* +build/ install-* _axom_build_and_test_* *.pyc From 9c609421d0b307a232f92e3f1eb46ddc33aefb23 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 11:18:22 -0700 Subject: [PATCH 09/35] Python: Streamlines Python installation docs --- .../github-actions/linux-wheel_and_test.sh | 2 - .../sidre/docs/sphinx/python_interface.rst | 22 ++- src/python/README.md | 177 +++++++----------- src/python/pyproject.toml | 5 +- 4 files changed, 84 insertions(+), 122 deletions(-) diff --git a/scripts/github-actions/linux-wheel_and_test.sh b/scripts/github-actions/linux-wheel_and_test.sh index 0fc6fb448e..2a9d9f6fb0 100755 --- a/scripts/github-actions/linux-wheel_and_test.sh +++ b/scripts/github-actions/linux-wheel_and_test.sh @@ -14,8 +14,6 @@ # 2. build the wheel from src/python against that install (find_package(axom)); # 3. install the wheel into a fresh uv venv; # 4. expose the *same-build* Conduit python module via a .pth file -# (a pip-built Conduit would put a second, ABI-incompatible libconduit in the process, -# so we point at the Conduit the install links; see src/python/README.md); # 5. run the Sidre Python test suite with plain `uv run pytest`. # # Intended for the gcc docker image, which is nanobind-enabled. diff --git a/src/axom/sidre/docs/sphinx/python_interface.rst b/src/axom/sidre/docs/sphinx/python_interface.rst index 907dab7423..fbc5edf382 100644 --- a/src/axom/sidre/docs/sphinx/python_interface.rst +++ b/src/axom/sidre/docs/sphinx/python_interface.rst @@ -90,14 +90,26 @@ pip / uv wheel (thin, external Axom) ------------------------------------ The wheel compiles only the binding code against an already-installed Axom and Conduit; -it does not build Axom or its third-party libraries. -Because a pip-built Conduit would produce a second, ABI-incompatible ``libconduit`` in the same process, -the wheel relies on the Conduit Python module from the same Axom/Conduit build, -exposed via a ``.pth`` file rather than a PyPI install. +it does not build Axom or its third-party libraries. A wheel is therefore specific to the toolchain/glibc of the Axom install it was built against. These wheels are not portable and not intended for PyPI; they target controlled environments (an LC host-config, a spack view, or a CI image). +.. note:: + **Do not install Conduit from PyPI.** Axom's bindings pass ``conduit::Node`` objects across the + C++ boundary to the ``conduit`` Python module, so that module must wrap the *same* ``libconduit`` + that Axom was compiled and linked against. Two separate PyPI packages are easy to reach for by + mistake, and neither works: + + * ``conduit`` is an **unrelated project** (a stream-transformation library for power-engineering + analytics). + * ``llnl-conduit`` **is** LLNL's Conduit, but installing it produces a *separate build* of the library, + compiled by pip with its own compiler, flags and third-party configuration. + It is unlikely to be ABI-compatible with the spack/CMake Conduit inside your Axom install, + and using it would put two ``libconduit`` libraries in one process. + + Instead, expose the Conduit that your Axom was built against, using the one-line ``.pth`` file in step 3 below. + Quick start ^^^^^^^^^^^ @@ -116,7 +128,7 @@ Conduit is located transitively through Axom's own CMake config, so it normally $ uv pip install /path/to/axom/src/python \ -C cmake.define.axom_DIR="$AXOM_INSTALL/lib/cmake" - # 3. Expose the *same-build* Conduit Python module (never a PyPI 'conduit'). + # 3. Expose the *same-build* Conduit Python module (not a PyPI package; see the note above). $ echo "$CONDUIT_INSTALL/python-modules" > \ "$(uv run python -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')/conduit.pth" diff --git a/src/python/README.md b/src/python/README.md index c2270731d3..6157bec0cf 100644 --- a/src/python/README.md +++ b/src/python/README.md @@ -20,18 +20,19 @@ It is consumed by two independent build paths that must produce the same on-disk 2. **The pip/uv wheel (out of tree).** A thin, binding-only wheel built with scikit-build-core (the `pyproject.toml` and `CMakeLists.txt` beside this file). It treats this directory as its package root (`wheel.packages = ["src/axom"]`) and compiles the binding translation unit against - an already-installed Axom. See "Building and installing the wheel" below. + an already-installed Axom. +This file discusses contributor-facing concerns. Installing and using the bindings is documented +in the Sidre user guide's "Python interface" page (`src/axom/sidre/docs/sphinx/python_interface.rst`). ## Two ways to get the Python interface -The two paths named above differ only in *who* compiles the `_sidre` extension and -*how you import it*. They compile the **same** binding translation unit -(`src/axom/sidre/nanobind_sidre.cpp`) under the **same** nanobind domain -(`NB_DOMAIN axom`) and ship the **same** pure-Python tree from this directory, -so `import axom.sidre` behaves identically either way. Both also stand on top of a -fully built, installed Axom plus a matching Conduit -- neither path builds Axom's -C++ libraries or its third-party libraries (see "What `uv` builds" below). +The two paths differ only in *who* compiles the `_sidre` extension and *how you import it*. +They compile the **same** binding translation unit (`src/axom/sidre/nanobind_sidre.cpp`) +under the **same** nanobind domain (`NB_DOMAIN axom`) and ship the **same** pure-Python tree +from this directory, so `import axom.sidre` behaves identically either way. +Both also stand on top of a fully built, installed Axom plus a matching Conduit +(neither path builds Axom's C++ libraries or its third-party libraries). ### Path A -- in-tree CMake build, imported via `PYTHONPATH` @@ -39,7 +40,8 @@ Enable the bindings in the same CMake build that compiles Axom (a Python interpreter must be found; currently only Sidre is bound). The `_sidre` extension is built next to `libaxom`/`libsidre`, staged into `/python/axom/sidre/`, and installed under `AXOM_PYTHON_MODULE_INSTALL_PREFIX` (default `lib/python/site-packages`). -To use it, put that directory -- plus Conduit's Python-module dir and numpy -- on `PYTHONPATH`, and `import axom.sidre` works. +To use it, put that directory, plus Conduit's Python-module dir and numpy, on `PYTHONPATH`, +and you should be able to successfully `import axom.sidre` in a Python script. The build configures a convenience wrapper that assembles that environment from the spack prefixes, so an ad hoc script "just works" without a venv: @@ -51,8 +53,8 @@ so an ad hoc script "just works" without a venv: This is the natural path during Axom development since it doesn't require a separate packaging step, and rebuilding Axom rebuilds the bindings in place. -The wrapper is bash-only and does not compose with Jupyter kernels, IDE runners, or debuggers. -For those, use the uv environment in **Quick start** below. +The wrapper is bash-only and does not compose with Jupyter kernels, IDE runners, or debuggers; +for those, use the venv of Path B. ### Path B -- thin pip/uv wheel, imported into a venv @@ -61,79 +63,45 @@ then build a **binding-only** wheel against that install and install it into a v ```bash # Axom + Conduit already built and installed; compile just the bindings against them. -# Point find_package at the install with axom_DIR uv build --wheel -C cmake.define.axom_DIR="$AXOM_INSTALL/lib/cmake" src/python -uv pip install dist/axom-*.whl # then expose the same-build Conduit; see below +uv pip install dist/axom-*.whl ``` The wheel's `CMakeLists.txt` runs `find_package(axom CONFIG REQUIRED)` and compiles only `nanobind_sidre.cpp` -- it consumes the install, but does not rebuild it. -This is the path for distributing or consuming the bindings in an ordinary Python environment. -The full uv workflows (wheelhouse install, editable rebuild loop, the same-build-Conduit `.pth`, -and the matching-interpreter constraint) are in "Building and installing the wheel" below. + +This is the path for distributing or consuming the bindings in an ordinary Python environment: +it does not require modifying the `PYTHONPATH` or running through a wrapper, so it works with scripts, +IDEs, debuggers and Jupyter kernels. The user guide has the step-by-step instructions +For details on building it, see the "Building the wheel: reference" section below. ### What `uv` builds -- and what it does not -`uv build` (through scikit-build-core) runs **only** the wheel's `CMakeLists.txt`, +`uv build` (through scikit-build-core) runs the wheel's `CMakeLists.txt`, which compiles the single binding TU and links it against an already-installed `axom::sidre` and `conduit::conduit_python`. It does **not** build: - **Axom's C++ libraries** -- supplied by the `find_package(axom)` install. - **Third-party libraries** (Conduit, HDF5, RAJA, Umpire, MPI, ...) -- provisioned - by spack and not pip-installable. Conduit especially is deliberately *not* a wheel dependency; - its Python module reaches the venv through a `.pth` pointing at the same-build Conduit - (a `pip install conduit` is an unrelated, ABI-incompatible package). + by spack and not pip-installable in a way that would match the install. By design, `uv` does not generate the Axom libraries directly: Axom and its TPLs come from the CMake/spack world, and `uv` adds only the thin binding layer on top. Keeping the wheel thin makes it fast and reproducible against a known install. -## Quick start: an Axom environment with uv (optionally for Jupyter) - -The happy path for *using* the bindings from an ordinary Python environment (Path B). -It assumes Axom was already built and installed with its Python bindings enabled at `$AXOM_INSTALL`, -against a Conduit at `$CONDUIT_INSTALL`. -For the per-host-config wheelhouse, the editable developer loop, and the reasoning behind each step, -see "Building and installing the wheel" below. - -```bash -# 1. A venv on the SAME interpreter your Axom/Conduit were built against. -uv venv --python $(which python3) - -# 2. Install the axom wheel -- either from a prebuilt per-host-config wheelhouse: -uv pip install axom --find-links /path/to/wheelhouse/ -# ...or build it from the source checkout against your install: -uv pip install /path/to/axom/src/python \ - -C cmake.define.axom_DIR="$AXOM_INSTALL/lib/cmake" - -# 3. Expose the *same-build* Conduit Python module (never a PyPI 'conduit') -# with a one-line .pth dropped into the venv's site-packages: -echo "$CONDUIT_INSTALL/python-modules" > \ - "$(uv run python -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')/conduit.pth" - -# 4. Verify -- no PYTHONPATH needed; the installed wheel and the .pth do the work. -uv run python -c "import axom.sidre, conduit, numpy; print(axom.__version__)" -``` - -That environment works anywhere the venv's interpreter runs, -e.g. plain scripts, an IDE, a debugger, and Jupyter. - -### Using it in Jupyter - -Because the wheel and the Conduit `.pth` live in the venv's `site-packages`, -a Jupyter kernel *running in that venv* imports `axom.sidre` natively. -There is nothing extra to wire up. Add Jupyter to the same venv and register it as a kernel: - -```bash -uv pip install jupyterlab ipykernel -uv run python -m ipykernel install --user --name axom --display-name "Axom (uv)" -uv run jupyter lab -``` +Conduit is therefore deliberately not a dependency of the wheel; its Python module reaches the +venv through a `.pth` file pointing at the same-build Conduit (see "Same-build Conduit" below). +Two distinct things on PyPI are worth mentioning: -Then select the **Axom (uv)** kernel. See the Sidre user guide's "Python interface page" -for a worked notebook example and the two one-line checks to run when a kernel cannot import `axom.sidre` -(`src/axom/sidre/docs/sphinx/python_interface.rst`). +- **`conduit` on PyPI is an unrelated project** -- it is a a stream-transformation library + for power-engineering analytics, so avoid `pip install conduit` for Axom. +- **`llnl-conduit` on PyPI *is* LLNL's Conduit**, but it is a separate build of the library: + pip compiles or fetches its own `libconduit` with its own compiler, flags and TPL configuration. + Axom's bindings hand `conduit::Node` objects across the C++ boundary to the `conduit` Python module, + so that module must wrap the very same `libconduit` that Axom was compiled and linked against. + A pip-provided Conduit is unlikely to be ABI-compatible with the spack/CMake Conduit in your Axom install, + and mixing the two puts two `libconduit`s in one process. Use the install's own Conduit. ## Layout @@ -171,73 +139,60 @@ A submodule is importable only when its component was enabled in the underlying generated artifacts (the `.so` and `.pyi` are produced by the build), and tests/examples (those live under the component, e.g. `src/axom/sidre/tests/*_Py.py`). -## Building and installing the wheel +## Building the wheel: reference The wheel is thin: it compiles only the binding code against an already-installed Axom and Conduit. It never builds Axom or its third-party libraries, so a wheel is specific to the Axom install (host-config / toolchain / glibc) it was built against. -These wheels are not portable and not intended for PyPI. -They carry absolute rpaths to the install's shared libraries -and skip the `auditwheel` / `delocate` repair a redistributable `manylinux` wheel needs; -they target controlled environments -- an LC host-config, a spack view, a CI image. -Producing portable, many-platform wheels would additionally need a tool such as `cibuildwheel` +These wheels are **not portable and not intended for PyPI**: they carry absolute rpaths to the install's +shared libraries and skip the `auditwheel` / `delocate` repair a redistributable `manylinux` wheel needs. +They target controlled environments -- e.g. an LC host-config, a spack view, a CI image. +Producing portable, many-platform wheels would additionally need a tool such as `cibuildwheel` plus a bundling/repair step, which is out of scope here. **Pointing the build at the install.** Pass one flag, -`-C cmake.define.axom_DIR="$AXOM_INSTALL/lib/cmake"`. Conduit needs no flag of its -own in the common case: `find_package(axom CONFIG)` pulls in Conduit via -`find_dependency`, using the Conduit prefix recorded in `axom-config.cmake` when -Axom was installed. +`-C cmake.define.axom_DIR="$AXOM_INSTALL/lib/cmake"`. +Conduit does not need a flag of its own in the common case since `find_package(axom CONFIG)` +pulls in Conduit via `find_dependency`, using the Conduit prefix recorded in `axom-config.cmake` +when Axom was installed. Pass `-C cmake.define.Conduit_DIR="$CONDUIT_INSTALL/lib/cmake/conduit"` only if -that recorded path no longer resolves (e.g. a relocated install, a different mount, -or a container path). +that recorded path no longer resolves (e.g. a relocated install, a different mount, or a container path). Do *not* use `CMAKE_PREFIX_PATH`. Under scikit-build-core (which drives `uv build` and `uv pip install`) -it is force-set to the isolated build environment -- that is how the build locates its own bundled `nanobind` -- +it is force-set to the isolated build environment (that is how the build locates its own bundled `nanobind`) so a user-supplied value would be overwritten and ignored, and `find_package(axom)` would fail with a "Could not find axom" message. +(A standalone `cmake -S src/python` invocation has no scikit-build-core layer and can use +`CMAKE_PREFIX_PATH` directly, but must then also place Conduit and nanobind on it.) -**One more precondition.** Build the wheel from the source tree that produced the install: -the wheel's version comes from this checkout's `src/cmake/AxomVersion.cmake` -while the extension links the installed Axom, so the build fails with an explicit message -if the two versions disagree rather than shipping a wheel whose `axom.__version__` misreports its own binary. +**Build from the source tree that produced the install.** The wheel's version comes from +this checkout's `src/cmake/AxomVersion.cmake` while the extension links the installed Axom, +so the build fails with an explicit message if the two disagree, rather than shipping a wheel whose +`axom.__version__` misreports its own binary. Note this is a coarse check: Axom's version changes only +at a release, so a `develop` checkout and a same-release install compare equal even though they are +different code. Matching the two is still your responsibility. Two constraints apply to every workflow below: - **Same-build Conduit.** The bindings exchange `conduit::Node`s with the `conduit` Python module - through Conduit's C capsule API, so that module must wrap the *same* `libconduit` the install links. - A `pip install conduit` is unsafe (unrelated PyPI package; a pip-built Conduit yields a second, - ABI-incompatible `libconduit`). Expose the install's own Conduit instead, - via a one-line `.pth` file (shown below). + through Conduit's C capsule API, so that module must wrap the *same* `libconduit` the install links. + Expose the install's own Conduit with a one-line `.pth` in the venv's `site-packages` + (the user guide shows the command); do not install either PyPI package (see "What `uv` builds" above). - **Matching interpreter.** Build with the interpreter family whose toolchain/glibc matches the host-config. - On LC, pin it explicitly: `uv venv --python $(which python3)`. + On LC, pin it explicitly: `uv venv --python $(which python3)`. -### 1. User install from a per-host-config wheelhouse +### Per-host-config wheelhouse Prebuilt wheels live in a per-host-config wheelhouse, e.g. `/usr/workspace/<...>/wheelhouse//` (one directory per host-config, since a wheel is not portable across toolchains). Consume it with `--find-links` (or a `[tool.uv.sources]` entry): ```bash -uv venv --python $(which python3) uv pip install axom --find-links /path/to/wheelhouse/dane-gcc13 ``` -Then add the Conduit `.pth` and verify exactly as in **Quick start** steps 3--4. - -### 2. Build from source against an existing Axom install - -```bash -uv pip install /path/to/axom/src/python \ - -C cmake.define.axom_DIR="$AXOM_INSTALL/lib/cmake" -``` - -`$AXOM_INSTALL` is a spack/uberenv-built Axom prefix (or any `cmake --install` tree) -whose `+python` bindings were enabled. Add the Conduit `.pth` afterward, as in **Quick start** step 3, -and add `Conduit_DIR` only if Conduit has moved since Axom was installed. - -### 3. Developer loop (editable, rebuild-on-import) +### Developer loop (editable, rebuild-on-import) nanobind's recommended editable flow rebuilds the extension automatically when you re-import it after editing the binding source. Rebuild-on-import is a @@ -249,9 +204,12 @@ uv pip install nanobind 'scikit-build-core[pyproject]' uv pip install -e src/python --no-build-isolation \ -C cmake.define.axom_DIR="$AXOM_INSTALL/lib/cmake" \ -C build-dir=build/py -C editable.rebuild=true -uv run pytest src/axom/sidre/tests -k _Py +uv run pytest -o python_files='*_Py.py' src/axom/sidre/tests/ ``` +Axom's Python tests are named `*_Py.py`, which pytest's default `python_files` patterns do not match, +so name the pattern explicitly (as above) or nothing is collected. + ### Stable ABI (abi3) By default the wheel is tagged for the exact CPython that built it. @@ -271,15 +229,10 @@ Below Python 3.12 nanobind silently builds a non-stable module, so only enable t the build fails with an explicit message if the interpreter does not provide `Development.SABIModule`, rather than quietly producing a mislabelled wheel. Stable ABI relaxes the Python-version coupling, not the toolchain coupling: an abi3 wheel is still specific to the host-config it was built against. +Free-threaded (`abi3t`) wheels are not built today; scikit-build-core 1.0+ can emit those tags once the +bindings and Conduit run under a free-threaded interpreter. -### Free-threaded Python (abi3t) - -Not built today. scikit-build-core 1.0+ can emit free-threaded stable-ABI wheels -(`abi3t`, e.g. a `cp313t` / `cp315t` tag) once the bindings and Conduit run under -a free-threaded interpreter. Revisit if/when Axom targets free-threaded Python; -no action is needed for the GIL-enabled builds above. - -### MPI +### MPI and test extras Wheel metadata is static, but whether the underlying Axom is an MPI build is a build-time choice, so the wheel cannot force the MPI dependency at install time. @@ -297,5 +250,3 @@ never in the runtime dependencies. - These files are installed verbatim (no template substitution). They contain no CMake-configured values. - This directory doubles as the root of the pip/uv wheel project (`pyproject.toml` + `CMakeLists.txt`), which reuses the very files above, so the two delivery modes cannot drift. -- End-user instructions for installing and importing the bindings also live in the Sidre user guide's - "Python interface" page. diff --git a/src/python/pyproject.toml b/src/python/pyproject.toml index eedf43eaea..19e586e3a4 100644 --- a/src/python/pyproject.toml +++ b/src/python/pyproject.toml @@ -48,8 +48,9 @@ classifiers = [ "Topic :: Scientific/Engineering", ] # conduit is a hard runtime dependency that intentionally does NOT appear here: -# the PyPI name 'conduit' is an unrelated package, and a pip-built Conduit would put a second, -# ABI-incompatible libconduit in the process (see src/python/README.md and the bindings design notes). +# the PyPI name 'conduit' is an unrelated project, and 'llnl-conduit' is a separate build of +# Conduit that is unlikely to be ABI-compatible with the one Axom links -- either way pip would put a +# second libconduit in the process (see src/python/README.md). # The conduit Python module must come from the same Conduit build Axom links, exposed via a .pth file. # Revisit if Conduit ever ships a thin, find_package-based binding wheel. dependencies = ["numpy>=1.22"] From 6993439737dfe47c92296faa77b03846bc9b2c95 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 11:22:52 -0700 Subject: [PATCH 10/35] Python: Tests generate files, so run run from a scratch directory --- scripts/github-actions/linux-wheel_and_test.sh | 10 +++++++++- src/python/README.md | 7 ++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/scripts/github-actions/linux-wheel_and_test.sh b/scripts/github-actions/linux-wheel_and_test.sh index 2a9d9f6fb0..4c28063a6d 100755 --- a/scripts/github-actions/linux-wheel_and_test.sh +++ b/scripts/github-actions/linux-wheel_and_test.sh @@ -116,7 +116,15 @@ echo "~~~~~~ RUN THE SIDRE PYTHON SUITE VIA PLAIN pytest ~~~~~~" # (test_*.py, *_test.py) do not match -- an unqualified run collects nothing and exits 5. # Name the pattern explicitly so collection is deterministic. # The MPI-only spio test skips itself at module level when sidre was built without MPI. +# Several tests write output tmp files into the current directory, +# so run from a scratch directory or_die uv pip install --python "${VENV_PY}" pytest +TEST_DIR="$(pwd)/src/axom/sidre/tests" +SCRATCH="$(mktemp -d)" +# Note: not a ( subshell ) -- or_die exits on failure, and from a subshell that would +# only exit the subshell and let the lane report success. +cd "${SCRATCH}" or_die "${VENV_PY}" -m pytest -s -p no:cacheprovider \ -o python_files='*_Py.py' \ - src/axom/sidre/tests/ + "${TEST_DIR}" +cd - > /dev/null diff --git a/src/python/README.md b/src/python/README.md index 6157bec0cf..498a6c7ab8 100644 --- a/src/python/README.md +++ b/src/python/README.md @@ -204,11 +204,12 @@ uv pip install nanobind 'scikit-build-core[pyproject]' uv pip install -e src/python --no-build-isolation \ -C cmake.define.axom_DIR="$AXOM_INSTALL/lib/cmake" \ -C build-dir=build/py -C editable.rebuild=true -uv run pytest -o python_files='*_Py.py' src/axom/sidre/tests/ +(cd "$(mktemp -d)" && uv run --project "$OLDPWD" \ + pytest -o python_files='*_Py.py' "$OLDPWD/src/axom/sidre/tests/") ``` -Axom's Python tests are named `*_Py.py`, which pytest's default `python_files` patterns do not match, -so name the pattern explicitly (as above) or nothing is collected. +Note that Axom's Python tests are named `*_Py.py`, which pytest's default `python_files` patterns do not match +and several tests write output files into the current directory, so we run them from a scratch directory. ### Stable ABI (abi3) From d5519d4d203d0af9e5ddc780343c5875f0fa93bf Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 11:54:55 -0700 Subject: [PATCH 11/35] Minor touchups to docs --- .../sidre/docs/sphinx/python_interface.rst | 4 +-- src/python/CMakeLists.txt | 9 +++---- src/python/README.md | 25 +++++++++++-------- src/python/pyproject.toml | 7 +++--- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/axom/sidre/docs/sphinx/python_interface.rst b/src/axom/sidre/docs/sphinx/python_interface.rst index fbc5edf382..4780adadb0 100644 --- a/src/axom/sidre/docs/sphinx/python_interface.rst +++ b/src/axom/sidre/docs/sphinx/python_interface.rst @@ -98,8 +98,8 @@ they target controlled environments (an LC host-config, a spack view, or a CI im .. note:: **Do not install Conduit from PyPI.** Axom's bindings pass ``conduit::Node`` objects across the C++ boundary to the ``conduit`` Python module, so that module must wrap the *same* ``libconduit`` - that Axom was compiled and linked against. Two separate PyPI packages are easy to reach for by - mistake, and neither works: + that Axom was compiled and linked against. + Two separate PyPI packages are easy to reach for by mistake, and neither works: * ``conduit`` is an **unrelated project** (a stream-transformation library for power-engineering analytics). diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 964fe8afbf..dbd7a6ffe6 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -18,8 +18,7 @@ # so a user-supplied CMAKE_PREFIX_PATH is ignored. # A standalone cmake invocation has no such layer and uses CMAKE_PREFIX_PATH directly: # -# cmake -S src/python -B build/py \ -# -DCMAKE_PREFIX_PATH="$AXOM_INSTALL;$CONDUIT_INSTALL;$(python -m nanobind --cmake_dir)" +# cmake -S src/python -B build/py -DCMAKE_PREFIX_PATH="$AXOM_INSTALL;$CONDUIT_INSTALL;$(python -m nanobind --cmake_dir)" # cmake --build build/py # cmake --install build/py --prefix #------------------------------------------------------------------------------ @@ -37,9 +36,9 @@ option(AXOM_PYTHON_STABLE_ABI "Build the extension against Python's stable ABI (abi3); needs Python >= 3.12" OFF) # Development.Module (not Development) so we link libpython-free extensions, -# matching the in-tree build's discovery. Development.SABIModule is requested as -# an OPTIONAL component (nanobind's recommended pattern) so a stable-ABI build -# (AXOM_PYTHON_STABLE_ABI=ON, which needs Python >= 3.12) can find it, +# matching the in-tree build's discovery. +# Development.SABIModule is requested as an OPTIONAL component (nanobind's recommended pattern) +# so a stable-ABI build (AXOM_PYTHON_STABLE_ABI=ON, which needs Python >= 3.12) can find it, # without a conditional component list. find_package(Python 3.10 REQUIRED COMPONENTS Interpreter Development.Module diff --git a/src/python/README.md b/src/python/README.md index 498a6c7ab8..f713c42d18 100644 --- a/src/python/README.md +++ b/src/python/README.md @@ -28,11 +28,14 @@ in the Sidre user guide's "Python interface" page (`src/axom/sidre/docs/sphinx/p ## Two ways to get the Python interface The two paths differ only in *who* compiles the `_sidre` extension and *how you import it*. -They compile the **same** binding translation unit (`src/axom/sidre/nanobind_sidre.cpp`) -under the **same** nanobind domain (`NB_DOMAIN axom`) and ship the **same** pure-Python tree -from this directory, so `import axom.sidre` behaves identically either way. -Both also stand on top of a fully built, installed Axom plus a matching Conduit -(neither path builds Axom's C++ libraries or its third-party libraries). +They compile: + +- the **same** binding translation unit (`src/axom/sidre/nanobind_sidre.cpp`) +- under the **same** nanobind domain (`NB_DOMAIN axom`) +- and ship the **same** pure-Python tree from this directory, so `import axom.sidre` behaves identically either way. + +Both also stand on top of a fully built, installed Axom plus a matching Conduit -- +neither path builds Axom's C++ libraries or its third-party libraries. ### Path A -- in-tree CMake build, imported via `PYTHONPATH` @@ -51,7 +54,7 @@ so an ad hoc script "just works" without a venv: /bin/run_python_with_axom.sh my_script.py ``` -This is the natural path during Axom development since it doesn't require a separate packaging step, +This is a natural path during Axom development since it doesn't require a separate packaging step, and rebuilding Axom rebuilds the bindings in place. The wrapper is bash-only and does not compose with Jupyter kernels, IDE runners, or debuggers; for those, use the venv of Path B. @@ -70,9 +73,9 @@ uv pip install dist/axom-*.whl The wheel's `CMakeLists.txt` runs `find_package(axom CONFIG REQUIRED)` and compiles only `nanobind_sidre.cpp` -- it consumes the install, but does not rebuild it. -This is the path for distributing or consuming the bindings in an ordinary Python environment: -it does not require modifying the `PYTHONPATH` or running through a wrapper, so it works with scripts, -IDEs, debuggers and Jupyter kernels. The user guide has the step-by-step instructions +Use this path for distributing or consuming the bindings in an ordinary Python environment. +It does not require modifying the `PYTHONPATH` or running through a wrapper, so it works with scripts, +IDEs, debuggers and Jupyter kernels. The user guide has the step-by-step instructions. For details on building it, see the "Building the wheel: reference" section below. ### What `uv` builds -- and what it does not @@ -80,6 +83,7 @@ For details on building it, see the "Building the wheel: reference" section belo `uv build` (through scikit-build-core) runs the wheel's `CMakeLists.txt`, which compiles the single binding TU and links it against an already-installed `axom::sidre` and `conduit::conduit_python`. + It does **not** build: - **Axom's C++ libraries** -- supplied by the `find_package(axom)` install. @@ -250,4 +254,5 @@ never in the runtime dependencies. - These files are installed verbatim (no template substitution). They contain no CMake-configured values. - This directory doubles as the root of the pip/uv wheel project (`pyproject.toml` + `CMakeLists.txt`), - which reuses the very files above, so the two delivery modes cannot drift. + which reuses these files. + diff --git a/src/python/pyproject.toml b/src/python/pyproject.toml index 19e586e3a4..44dd133a14 100644 --- a/src/python/pyproject.toml +++ b/src/python/pyproject.toml @@ -47,10 +47,10 @@ classifiers = [ "Programming Language :: Python :: 3", "Topic :: Scientific/Engineering", ] -# conduit is a hard runtime dependency that intentionally does NOT appear here: +# conduit is a hard runtime dependency for sidre that intentionally does NOT appear here: # the PyPI name 'conduit' is an unrelated project, and 'llnl-conduit' is a separate build of -# Conduit that is unlikely to be ABI-compatible with the one Axom links -- either way pip would put a -# second libconduit in the process (see src/python/README.md). +# Conduit that is unlikely to be ABI-compatible with the one Axom links +# -- either way pip would put a second libconduit in the process (see src/python/README.md). # The conduit Python module must come from the same Conduit build Axom links, exposed via a .pth file. # Revisit if Conduit ever ships a thin, find_package-based binding wheel. dependencies = ["numpy>=1.22"] @@ -87,6 +87,7 @@ build-dir = "build/{wheel_tag}" # The 'axom' regular package (its __init__.py files, py.typed and the checked-in package stub) ships as pure Python from src/axom. # The compiled extension and its generated _sidre.pyi are installed by CMakeLists.txt into axom/sidre/. wheel.packages = ["src/axom"] + # NOTE on the sdist: the binding translation unit lives with its component (../axom/sidre/nanobind_sidre.cpp), outside this project directory. # A `sdist.include = ["../axom/..."]` entry does NOT vendor it -- scikit-build-core restricts sdist contents to the project root # and silently drops out-of-tree paths (verified with scikit-build-core 1.0.3). From ac66160f078dffcf70c744b5300e4a9fc7364084 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 16:22:25 -0700 Subject: [PATCH 12/35] Python: Fixes and streamlines setup for uv/pip --- .../sidre/docs/sphinx/python_interface.rst | 36 +++++++-- src/python/CMakeLists.txt | 73 ++++++++++++++++++- src/python/README.md | 67 +++++++++++++++-- src/python/cmake/axom-python-env.sh.in | 42 +++++++++++ .../cmake/axom-python-host-config.cmake.in | 59 +++++++++++++++ src/python/cmake/conduit.pth.in | 1 + src/python/pyproject.toml | 4 +- src/python/src/axom/config.py | 53 ++++++++++++++ 8 files changed, 322 insertions(+), 13 deletions(-) create mode 100644 src/python/cmake/axom-python-env.sh.in create mode 100644 src/python/cmake/axom-python-host-config.cmake.in create mode 100644 src/python/cmake/conduit.pth.in create mode 100644 src/python/src/axom/config.py diff --git a/src/axom/sidre/docs/sphinx/python_interface.rst b/src/axom/sidre/docs/sphinx/python_interface.rst index 4780adadb0..520d3ce3fc 100644 --- a/src/axom/sidre/docs/sphinx/python_interface.rst +++ b/src/axom/sidre/docs/sphinx/python_interface.rst @@ -108,7 +108,8 @@ they target controlled environments (an LC host-config, a spack view, or a CI im It is unlikely to be ABI-compatible with the spack/CMake Conduit inside your Axom install, and using it would put two ``libconduit`` libraries in one process. - Instead, expose the Conduit that your Axom was built against, using the one-line ``.pth`` file in step 3 below. + Instead, use the Conduit that your Axom was built against. The Axom wheel + records that same-build Conduit Python path in the venv with a ``conduit.pth`` file. Quick start ^^^^^^^^^^^ @@ -128,12 +129,29 @@ Conduit is located transitively through Axom's own CMake config, so it normally $ uv pip install /path/to/axom/src/python \ -C cmake.define.axom_DIR="$AXOM_INSTALL/lib/cmake" - # 3. Expose the *same-build* Conduit Python module (not a PyPI package; see the note above). - $ echo "$CONDUIT_INSTALL/python-modules" > \ + # 3. Verify -- no PYTHONPATH and no wrapper script. + $ uv run python -c "import axom.sidre, conduit, numpy; print(axom.__version__)" + +If ``axom.sidre`` is already installed in a venv but ``import conduit`` fails, +add the same-build Conduit Python package with one ``.pth`` file: + +.. code-block:: bash + + $ CONDUIT_PYTHON_MODULE_DIR=/path/to/conduit/install/lib/pythonX.Y/site-packages + $ printf '%s\n' "$CONDUIT_PYTHON_MODULE_DIR" > \ "$(uv run python -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')/conduit.pth" + $ uv run python -c "import axom.sidre, conduit; print(conduit.__file__)" - # 4. Verify -- no PYTHONPATH and no wrapper script. - $ uv run python -c "import axom.sidre, conduit, numpy; print(axom.__version__)" +Use the Conduit install that Axom was built against. The correct path is +``CONDUIT_PYTHON_MODULE_DIR`` in Conduit's CMake config; on current LC installs +it is usually ``$CONDUIT_INSTALL/lib/pythonX.Y/site-packages``, not +``$CONDUIT_INSTALL/python-modules``. + +Use the wheel's generated host-config when configuring downstream CMake projects: + +.. code-block:: bash + + $ cmake -C "$(uv run axom-python-config --host-config)" -S /path/to/project -B build Three details matter when building the wheel yourself: @@ -145,6 +163,12 @@ Three details matter when building the wheel yourself: * Add ``-C cmake.define.Conduit_DIR="$CONDUIT_INSTALL/lib/cmake/conduit"`` only if Conduit has moved since Axom was installed: Axom's config records the Conduit prefix it was built against, and that recorded path is what the transitive lookup uses. + If Conduit's Python module lives in a nonstandard location that is not recorded + by Conduit's CMake config, also pass + ``-C cmake.define.CONDUIT_PYTHON_MODULE_DIR=/path/to/site-packages``. +* For MPI-enabled Axom installs, use the same C/C++ compilers and MPI compiler + wrappers that built Axom. Copy these from the Axom build's ``CMakeCache.txt`` + or host-config. * Build from the source tree that produced the install. The wheel takes its version from ``src/cmake/AxomVersion.cmake`` in the checkout, and the build fails with an explicit message if that disagrees with the installed Axom, @@ -183,7 +207,7 @@ If the kernel cannot import ``axom.sidre``, it is nearly always either the wrong .. code-block:: python import sys; print(sys.executable) # expect /bin/python - import conduit; print(conduit.__file__) # expect $CONDUIT_INSTALL/python-modules/... + import conduit; print(conduit.__file__) # expect $CONDUIT_INSTALL/lib/pythonX.Y/site-packages/... If the underlying Axom is an MPI build and you need to pass a communicator to ``IOManager`` (or to initialize MPI), install the ``mpi`` extra with ``uv pip install 'axom[mpi]'``. diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index dbd7a6ffe6..a04b1b62a5 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -24,7 +24,7 @@ #------------------------------------------------------------------------------ cmake_minimum_required(VERSION 3.21) -project(axom_python LANGUAGES CXX) +project(axom_python LANGUAGES C CXX) # Optional: build against Python's stable ABI (abi3) so one wheel serves every CPython >= 3.12 on the machine. # Opt-in (default OFF) to provide per-Python wheel tags by default. @@ -58,6 +58,52 @@ endif() find_package(axom CONFIG REQUIRED) find_package(nanobind CONFIG REQUIRED) # supplied via build-system.requires +set(_axom_py_conduit_cmake_dir "") +if(DEFINED Conduit_DIR) + set(_axom_py_conduit_cmake_dir "${Conduit_DIR}") +elseif(DEFINED CONDUIT_INSTALL_PREFIX) + set(_axom_py_conduit_cmake_dir "${CONDUIT_INSTALL_PREFIX}/lib/cmake/conduit") +elseif(DEFINED AXOM_CONDUIT_DIR) + set(_axom_py_conduit_cmake_dir "${AXOM_CONDUIT_DIR}/lib/cmake/conduit") +endif() + +set(_axom_py_conduit_prefix "") +if(DEFINED CONDUIT_INSTALL_PREFIX) + set(_axom_py_conduit_prefix "${CONDUIT_INSTALL_PREFIX}") +elseif(DEFINED AXOM_CONDUIT_DIR) + set(_axom_py_conduit_prefix "${AXOM_CONDUIT_DIR}") +elseif(DEFINED CONDUIT_DIR) + set(_axom_py_conduit_prefix "${CONDUIT_DIR}") +endif() + +set(_axom_py_conduit_python_module_dir "") +if(DEFINED CONDUIT_PYTHON_MODULE_DIR) + if(IS_ABSOLUTE "${CONDUIT_PYTHON_MODULE_DIR}") + set(_axom_py_conduit_python_module_dir "${CONDUIT_PYTHON_MODULE_DIR}") + elseif(_axom_py_conduit_prefix) + set(_axom_py_conduit_python_module_dir + "${_axom_py_conduit_prefix}/${CONDUIT_PYTHON_MODULE_DIR}") + endif() +elseif(_axom_py_conduit_prefix) + foreach(_axom_py_conduit_python_module_dir_candidate + "${_axom_py_conduit_prefix}/lib/python${Python_VERSION_MAJOR}.${Python_VERSION_MINOR}/site-packages" + "${_axom_py_conduit_prefix}/python-modules") + if(EXISTS "${_axom_py_conduit_python_module_dir_candidate}/conduit") + set(_axom_py_conduit_python_module_dir + "${_axom_py_conduit_python_module_dir_candidate}") + break() + endif() + endforeach() +endif() + +if(NOT _axom_py_conduit_python_module_dir + OR NOT EXISTS "${_axom_py_conduit_python_module_dir}/conduit") + message(FATAL_ERROR + "Could not determine the Conduit Python module directory for this " + "Axom install. Pass -C cmake.define.CONDUIT_PYTHON_MODULE_DIR= " + "where contains the same-build conduit Python package.") +endif() + # The wheel's version metadata is read from this checkout's src/cmake/AxomVersion.cmake # (see [[tool.dynamic-metadata]] in pyproject.toml), # but the extension is compiled and linked against the Axom install found above. @@ -108,6 +154,31 @@ target_link_libraries(_sidre PRIVATE axom::sidre conduit::conduit_python) # this makes the standalone `cmake --install` path behave the same. set_target_properties(_sidre PROPERTIES INSTALL_RPATH_USE_LINK_PATH TRUE) +#------------------------------------------------------------------------------ +# Generated runtime/development helpers. +# +# A thin wheel is tied to the Axom/Conduit install it was built against. Ship the +# corresponding Conduit Python path and CMake configuration hints with the wheel +# so users do not have to rediscover them by hand. +#------------------------------------------------------------------------------ +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/axom-python-host-config.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/axom-python-host-config.cmake" + @ONLY) +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/axom-python-env.sh.in" + "${CMAKE_CURRENT_BINARY_DIR}/axom-python-env.sh" + @ONLY) +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/axom-python-host-config.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/axom-python-env.sh" + DESTINATION axom/share) + +if(_axom_py_conduit_python_module_dir) + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/conduit.pth.in" + "${CMAKE_CURRENT_BINARY_DIR}/conduit.pth" + @ONLY) + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/conduit.pth" + DESTINATION ".") +endif() + #------------------------------------------------------------------------------ # HIP: replicate the in-tree special-case from src/axom/sidre/CMakeLists.txt. # CMake treats MODULE libraries separately from executables, so HIP executable diff --git a/src/python/README.md b/src/python/README.md index f713c42d18..8e65424244 100644 --- a/src/python/README.md +++ b/src/python/README.md @@ -95,7 +95,8 @@ Axom and its TPLs come from the CMake/spack world, and `uv` adds only the thin b Keeping the wheel thin makes it fast and reproducible against a known install. Conduit is therefore deliberately not a dependency of the wheel; its Python module reaches the -venv through a `.pth` file pointing at the same-build Conduit (see "Same-build Conduit" below). +venv through a generated `.pth` file pointing at the same-build Conduit +(see "Same-build Conduit" below). Two distinct things on PyPI are worth mentioning: - **`conduit` on PyPI is an unrelated project** -- it is a a stream-transformation library @@ -163,6 +164,24 @@ when Axom was installed. Pass `-C cmake.define.Conduit_DIR="$CONDUIT_INSTALL/lib/cmake/conduit"` only if that recorded path no longer resolves (e.g. a relocated install, a different mount, or a container path). +If the Axom install is MPI-enabled, make the wheel build use the same compiler +and MPI wrapper family that built Axom. The values can be copied from the Axom +build's `CMakeCache.txt` or host-config: + +```bash +uv build --wheel \ + -C cmake.define.axom_DIR="$AXOM_INSTALL/lib/cmake" \ + -C cmake.define.CMAKE_C_COMPILER="$AXOM_C_COMPILER" \ + -C cmake.define.CMAKE_CXX_COMPILER="$AXOM_CXX_COMPILER" \ + -C cmake.define.MPI_C_COMPILER="$AXOM_MPI_C_COMPILER" \ + -C cmake.define.MPI_CXX_COMPILER="$AXOM_MPI_CXX_COMPILER" \ + src/python +``` + +This matters because `axom-config.cmake` re-runs CMake's MPI discovery while +loading Axom's MPI-enabled dependencies, and a plain environment may discover a +different system MPI than the one recorded in the Axom install. + Do *not* use `CMAKE_PREFIX_PATH`. Under scikit-build-core (which drives `uv build` and `uv pip install`) it is force-set to the isolated build environment (that is how the build locates its own bundled `nanobind`) so a user-supplied value would be overwritten and ignored, and `find_package(axom)` @@ -181,10 +200,50 @@ Two constraints apply to every workflow below: - **Same-build Conduit.** The bindings exchange `conduit::Node`s with the `conduit` Python module through Conduit's C capsule API, so that module must wrap the *same* `libconduit` the install links. - Expose the install's own Conduit with a one-line `.pth` in the venv's `site-packages` - (the user guide shows the command); do not install either PyPI package (see "What `uv` builds" above). + The wheel writes a `conduit.pth` file into the venv's `site-packages` using + `CONDUIT_PYTHON_MODULE_DIR` from Conduit's CMake config. Do not install either + PyPI package (see "What `uv` builds" above). - **Matching interpreter.** Build with the interpreter family whose toolchain/glibc matches the host-config. On LC, pin it explicitly: `uv venv --python $(which python3)`. +- **Matching compiler/MPI wrappers.** For MPI Axom installs, pass the same + C/C++ compilers and MPI wrappers used by the Axom build, as shown above. + +If Axom is already installed in a venv but `import conduit` fails, this is the +only manual step usually needed: + +```bash +CONDUIT_PYTHON_MODULE_DIR=/path/to/conduit/install/lib/pythonX.Y/site-packages +printf '%s\n' "$CONDUIT_PYTHON_MODULE_DIR" > \ + "$(uv run python -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')/conduit.pth" +uv run python -c "import axom.sidre, conduit; print(conduit.__file__)" +``` + +Use `CONDUIT_PYTHON_MODULE_DIR` from Conduit's CMake config. On current LC +installs it is usually `lib/pythonX.Y/site-packages`, not `python-modules`. + +If Conduit's Python module is not recorded by Conduit's CMake config, pass it +explicitly: + +```bash +uv build --wheel \ + -C cmake.define.axom_DIR="$AXOM_INSTALL/lib/cmake" \ + -C cmake.define.CONDUIT_PYTHON_MODULE_DIR="$CONDUIT_INSTALL/lib/pythonX.Y/site-packages" \ + src/python +``` + +The wheel also installs development helpers: + +```bash +axom-python-config --host-config # path to axom/share/axom-python-host-config.cmake +axom-python-config --env-script # path to axom/share/axom-python-env.sh +``` + +Use the host-config to seed downstream CMake projects with the same Axom, +Conduit, compiler, MPI and Python settings used by the wheel: + +```bash +cmake -C "$(axom-python-config --host-config)" -S -B +``` ### Per-host-config wheelhouse @@ -252,7 +311,5 @@ never in the runtime dependencies. ## Notes -- These files are installed verbatim (no template substitution). They contain no CMake-configured values. - This directory doubles as the root of the pip/uv wheel project (`pyproject.toml` + `CMakeLists.txt`), which reuses these files. - diff --git a/src/python/cmake/axom-python-env.sh.in b/src/python/cmake/axom-python-env.sh.in new file mode 100644 index 0000000000..bc854f059a --- /dev/null +++ b/src/python/cmake/axom-python-env.sh.in @@ -0,0 +1,42 @@ +# Copyright (c) Lawrence Livermore National Security, LLC and other +# Axom Project Contributors. See top-level LICENSE and COPYRIGHT +# files for dates and other details. +# +# SPDX-License-Identifier: (BSD-3-Clause) + +# Generated by Axom's Python wheel build. +# Source this bash file to expose paths useful for downstream CMake configuration: +# . /axom/share/axom-python-env.sh + +_axom_python_env_script="${BASH_SOURCE[0]}" +_axom_python_share_dir=$(CDPATH= cd -- "$(dirname -- "${_axom_python_env_script}")" && pwd) +_axom_python_package_dir=$(CDPATH= cd -- "${_axom_python_share_dir}/.." && pwd) +_axom_python_site_packages_dir=$(CDPATH= cd -- "${_axom_python_package_dir}/.." && pwd) +_axom_python_version_dir=$(CDPATH= cd -- "${_axom_python_site_packages_dir}/.." && pwd) +_axom_python_lib_dir=$(CDPATH= cd -- "${_axom_python_version_dir}/.." && pwd) +_axom_python_prefix_dir=$(CDPATH= cd -- "${_axom_python_lib_dir}/.." && pwd) + +export AXOM_PYTHON_HOST_CONFIG="${_axom_python_share_dir}/axom-python-host-config.cmake" +export axom_DIR="@axom_DIR@" +export AXOM_DIR="@axom_DIR@" +export AXOM_INSTALL_PREFIX="@AXOM_INSTALL_PREFIX@" +export Conduit_DIR="@_axom_py_conduit_cmake_dir@" +export CONDUIT_DIR="@AXOM_CONDUIT_DIR@" +export CONDUIT_PYTHON_MODULE_DIR="@_axom_py_conduit_python_module_dir@" +export CMAKE_C_COMPILER="@CMAKE_C_COMPILER@" +export CMAKE_CXX_COMPILER="@CMAKE_CXX_COMPILER@" +export MPI_C_COMPILER="@MPI_C_COMPILER@" +export MPI_CXX_COMPILER="@MPI_CXX_COMPILER@" +if [ -x "${_axom_python_prefix_dir}/bin/python" ]; then + export Python_EXECUTABLE="${_axom_python_prefix_dir}/bin/python" +else + export Python_EXECUTABLE="@Python_EXECUTABLE@" +fi + +unset _axom_python_share_dir +unset _axom_python_env_script +unset _axom_python_package_dir +unset _axom_python_site_packages_dir +unset _axom_python_version_dir +unset _axom_python_lib_dir +unset _axom_python_prefix_dir diff --git a/src/python/cmake/axom-python-host-config.cmake.in b/src/python/cmake/axom-python-host-config.cmake.in new file mode 100644 index 0000000000..f5a2845beb --- /dev/null +++ b/src/python/cmake/axom-python-host-config.cmake.in @@ -0,0 +1,59 @@ +# Copyright (c) Lawrence Livermore National Security, LLC and other +# Axom Project Contributors. See top-level LICENSE and COPYRIGHT +# files for dates and other details. +# +# SPDX-License-Identifier: (BSD-3-Clause) + +# Generated by Axom's Python wheel build. +# Use with: +# cmake -C -S -B + +get_filename_component(_AXOM_PYTHON_SHARE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) +get_filename_component(_AXOM_PYTHON_PACKAGE_DIR "${_AXOM_PYTHON_SHARE_DIR}" DIRECTORY) +get_filename_component(_AXOM_PYTHON_SITE_PACKAGES_DIR "${_AXOM_PYTHON_PACKAGE_DIR}" DIRECTORY) +get_filename_component(_AXOM_PYTHON_VERSION_DIR "${_AXOM_PYTHON_SITE_PACKAGES_DIR}" DIRECTORY) +get_filename_component(_AXOM_PYTHON_LIB_DIR "${_AXOM_PYTHON_VERSION_DIR}" DIRECTORY) +get_filename_component(_AXOM_PYTHON_PREFIX_DIR "${_AXOM_PYTHON_LIB_DIR}" DIRECTORY) + +set(_AXOM_PYTHON_INSTALLED_EXECUTABLE "") +if(EXISTS "${_AXOM_PYTHON_PREFIX_DIR}/bin/python") + set(_AXOM_PYTHON_INSTALLED_EXECUTABLE "${_AXOM_PYTHON_PREFIX_DIR}/bin/python") +endif() + +set(axom_DIR "@axom_DIR@" CACHE PATH "Axom CMake package directory") +set(AXOM_DIR "@axom_DIR@" CACHE PATH "Axom CMake package directory") +set(AXOM_INSTALL_PREFIX "@AXOM_INSTALL_PREFIX@" CACHE PATH "Axom install prefix") + +set(Conduit_DIR "@_axom_py_conduit_cmake_dir@" CACHE PATH "Conduit CMake package directory") +set(CONDUIT_DIR "@AXOM_CONDUIT_DIR@" CACHE PATH "Conduit install prefix") +set(CONDUIT_PYTHON_MODULE_DIR "@_axom_py_conduit_python_module_dir@" CACHE PATH "Conduit Python module directory") + +if(EXISTS "@CMAKE_C_COMPILER@") + set(CMAKE_C_COMPILER "@CMAKE_C_COMPILER@" CACHE FILEPATH "C compiler used for the Axom Python wheel") +endif() + +if(EXISTS "@CMAKE_CXX_COMPILER@") + set(CMAKE_CXX_COMPILER "@CMAKE_CXX_COMPILER@" CACHE FILEPATH "CXX compiler used for the Axom Python wheel") +endif() + +if(EXISTS "@MPI_C_COMPILER@") + set(MPI_C_COMPILER "@MPI_C_COMPILER@" CACHE FILEPATH "MPI C compiler wrapper used for the Axom Python wheel") +endif() + +if(EXISTS "@MPI_CXX_COMPILER@") + set(MPI_CXX_COMPILER "@MPI_CXX_COMPILER@" CACHE FILEPATH "MPI CXX compiler wrapper used for the Axom Python wheel") +endif() + +if(_AXOM_PYTHON_INSTALLED_EXECUTABLE) + set(Python_EXECUTABLE "${_AXOM_PYTHON_INSTALLED_EXECUTABLE}" CACHE FILEPATH "Python interpreter for this Axom Python installation") +elseif(EXISTS "@Python_EXECUTABLE@") + set(Python_EXECUTABLE "@Python_EXECUTABLE@" CACHE FILEPATH "Python interpreter used for the Axom Python wheel") +endif() + +unset(_AXOM_PYTHON_SHARE_DIR) +unset(_AXOM_PYTHON_PACKAGE_DIR) +unset(_AXOM_PYTHON_SITE_PACKAGES_DIR) +unset(_AXOM_PYTHON_VERSION_DIR) +unset(_AXOM_PYTHON_LIB_DIR) +unset(_AXOM_PYTHON_PREFIX_DIR) +unset(_AXOM_PYTHON_INSTALLED_EXECUTABLE) diff --git a/src/python/cmake/conduit.pth.in b/src/python/cmake/conduit.pth.in new file mode 100644 index 0000000000..735ea6f459 --- /dev/null +++ b/src/python/cmake/conduit.pth.in @@ -0,0 +1 @@ +@_axom_py_conduit_python_module_dir@ diff --git a/src/python/pyproject.toml b/src/python/pyproject.toml index 44dd133a14..f3ea490d44 100644 --- a/src/python/pyproject.toml +++ b/src/python/pyproject.toml @@ -67,6 +67,9 @@ Homepage = "https://github.com/LLNL/axom" Documentation = "https://axom.readthedocs.io" Source = "https://github.com/LLNL/axom" +[project.scripts] +axom-python-config = "axom.config:main" + # Source the wheel version from the C++ library's canonical version file, so it can never drift from libaxom. # Build from a full repo checkout: the sdist does not carry this out-of-tree file, # and standalone-sdist/PyPI is out of scope (see the sdist note under [tool.scikit-build]). @@ -110,4 +113,3 @@ wheel.packages = ["src/axom"] [tool.scikit-build.cmake.define] # Resolve libsidre/libconduit/HDF5 from their install locations at runtime with no LD_LIBRARY_PATH. CMAKE_INSTALL_RPATH_USE_LINK_PATH = "ON" - diff --git a/src/python/src/axom/config.py b/src/python/src/axom/config.py new file mode 100644 index 0000000000..4741cb176b --- /dev/null +++ b/src/python/src/axom/config.py @@ -0,0 +1,53 @@ +# Copyright (c) Lawrence Livermore National Security, LLC and other +# Axom Project Contributors. See top-level LICENSE and COPYRIGHT +# files for dates and other details. +# +# SPDX-License-Identifier: (BSD-3-Clause) + +"""Helpers for locating Axom Python wheel configuration files.""" + +from __future__ import annotations + +import argparse +from importlib import resources +from pathlib import Path + + +def share_dir() -> Path: + """Return the installed Axom Python package share directory.""" + return Path(resources.files("axom").joinpath("share")) + + +def host_config_path() -> Path: + """Return the CMake host-config generated for this Axom Python wheel.""" + return share_dir() / "axom-python-host-config.cmake" + + +def env_script_path() -> Path: + """Return the shell environment helper generated for this Axom Python wheel.""" + return share_dir() / "axom-python-env.sh" + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Print Axom Python wheel configuration paths.") + group = parser.add_mutually_exclusive_group() + group.add_argument("--host-config", action="store_true", help="print the CMake host-config path") + group.add_argument("--env-script", action="store_true", help="print the shell environment script path") + group.add_argument("--share-dir", action="store_true", help="print the Axom Python share directory") + group.add_argument("--cmake-args", action="store_true", help="print CMake arguments using the host-config") + args = parser.parse_args(argv) + + if args.env_script: + print(env_script_path()) + elif args.share_dir: + print(share_dir()) + elif args.cmake_args: + print(f"-C {host_config_path()}") + else: + print(host_config_path()) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From bf1dad5f5a3b31be599d3b7c28d78517d68ad08a Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 16:47:09 -0700 Subject: [PATCH 13/35] Python: Use AXOM_DIR instead of axom_DIR Also deemphasizes the discussion of platform-dependent wheelhouses. --- .../sidre/docs/sphinx/python_interface.rst | 47 ++++++++++--------- src/python/CMakeLists.txt | 10 ++-- src/python/README.md | 29 +++++++----- 3 files changed, 49 insertions(+), 37 deletions(-) diff --git a/src/axom/sidre/docs/sphinx/python_interface.rst b/src/axom/sidre/docs/sphinx/python_interface.rst index 520d3ce3fc..b578e4b4b1 100644 --- a/src/axom/sidre/docs/sphinx/python_interface.rst +++ b/src/axom/sidre/docs/sphinx/python_interface.rst @@ -114,22 +114,18 @@ they target controlled environments (an LC host-config, a spack view, or a CI im Quick start ^^^^^^^^^^^ -Point the build at the Axom install with ``axom_DIR``. -Conduit is located transitively through Axom's own CMake config, so it normally needs no flag of its own: +Point the wheel build at the Axom install with ``AXOM_DIR``. This should be the +directory containing ``axom-config.cmake``, usually ``$AXOM_INSTALL/lib/cmake``. +Use an absolute path; relative paths can be interpreted from the temporary build +directory that ``uv`` creates. .. code-block:: bash - # 1. A venv on the same interpreter Axom and Conduit were built against. $ uv venv --python $(which python3) - # 2a. Install a prebuilt wheel from a per-host-config wheelhouse... - $ uv pip install axom --find-links /path/to/wheelhouse/ - - # 2b. ...or build it from a source checkout against your Axom install. $ uv pip install /path/to/axom/src/python \ - -C cmake.define.axom_DIR="$AXOM_INSTALL/lib/cmake" + -C cmake.define.AXOM_DIR="$AXOM_INSTALL/lib/cmake" - # 3. Verify -- no PYTHONPATH and no wrapper script. $ uv run python -c "import axom.sidre, conduit, numpy; print(axom.__version__)" If ``axom.sidre`` is already installed in a venv but ``import conduit`` fails, @@ -142,10 +138,16 @@ add the same-build Conduit Python package with one ``.pth`` file: "$(uv run python -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')/conduit.pth" $ uv run python -c "import axom.sidre, conduit; print(conduit.__file__)" -Use the Conduit install that Axom was built against. The correct path is -``CONDUIT_PYTHON_MODULE_DIR`` in Conduit's CMake config; on current LC installs -it is usually ``$CONDUIT_INSTALL/lib/pythonX.Y/site-packages``, not -``$CONDUIT_INSTALL/python-modules``. +Use the Conduit install that Axom was built against. +The correct path is ``CONDUIT_PYTHON_MODULE_DIR`` in Conduit's CMake config. +On current LC installs it is usually ``$CONDUIT_INSTALL/lib/pythonX.Y/site-packages``. + +.. note:: + + Prebuilt wheelhouses are site-specific. + If your platform team publishes one for your host-config, install from the path they provide with + ``uv pip install axom --find-links ``. + This page does not assume a central Axom wheelhouse exists. Use the wheel's generated host-config when configuring downstream CMake projects: @@ -155,17 +157,18 @@ Use the wheel's generated host-config when configuring downstream CMake projects Three details matter when building the wheel yourself: -* Use ``axom_DIR``, not ``CMAKE_PREFIX_PATH``. scikit-build-core (which backs - ``uv build`` and ``uv pip install``) force-sets ``CMAKE_PREFIX_PATH`` to its own - isolated build environment, so a user-supplied value would be overwritten and - ``find_package(axom)`` would fail. A standalone ``cmake -S src/python`` has no such - layer and can use ``CMAKE_PREFIX_PATH`` directly. +* Use ``AXOM_DIR`` or ``axom_DIR``, not ``CMAKE_PREFIX_PATH``. + ``axom_DIR`` is CMake's package variable for ``find_package(axom)`` + ``AXOM_DIR`` is accepted by Axom's wheel build as a conventional uppercase alias. + scikit-build-core (which backs ``uv build`` and ``uv pip install``) + force-sets ``CMAKE_PREFIX_PATH`` to its own isolated build environment, + so a user-supplied value would be overwritten and ``find_package(axom)`` would fail. + A standalone ``cmake -S src/python`` has no such layer and can use ``CMAKE_PREFIX_PATH`` directly. * Add ``-C cmake.define.Conduit_DIR="$CONDUIT_INSTALL/lib/cmake/conduit"`` only if Conduit has moved since Axom was installed: Axom's config records the Conduit prefix it was built against, and that recorded path is what the transitive lookup uses. If Conduit's Python module lives in a nonstandard location that is not recorded - by Conduit's CMake config, also pass - ``-C cmake.define.CONDUIT_PYTHON_MODULE_DIR=/path/to/site-packages``. + by Conduit's CMake config, also pass ``-C cmake.define.CONDUIT_PYTHON_MODULE_DIR=/path/to/site-packages``. * For MPI-enabled Axom installs, use the same C/C++ compilers and MPI compiler wrappers that built Axom. Copy these from the Axom build's ``CMakeCache.txt`` or host-config. @@ -212,8 +215,8 @@ If the kernel cannot import ``axom.sidre``, it is nearly always either the wrong If the underlying Axom is an MPI build and you need to pass a communicator to ``IOManager`` (or to initialize MPI), install the ``mpi`` extra with ``uv pip install 'axom[mpi]'``. -For the per-host-config wheelhouse convention, the editable developer loop, -and the optional stable-ABI build, see ``src/python/README.md``. +For site-specific wheelhouses, the editable developer loop, and the optional +stable-ABI build, see ``src/python/README.md``. ==================================== Working with Conduit and NumPy diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index a04b1b62a5..c65af4a3a7 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -13,7 +13,7 @@ # but is a valid standalone CMake project as well. # # When driven by scikit-build-core (uv build / uv pip install), -# point find_package at the installs with -Daxom_DIR / -DConduit_DIR: +# point find_package at the installs with -DAXOM_DIR / -DConduit_DIR: # scikit-build-core force-sets CMAKE_PREFIX_PATH to the isolated build env (to locate its own nanobind), # so a user-supplied CMAKE_PREFIX_PATH is ignored. # A standalone cmake invocation has no such layer and uses CMAKE_PREFIX_PATH directly: @@ -55,6 +55,10 @@ if(AXOM_PYTHON_STABLE_ABI AND NOT Python_Development.SABIModule_FOUND) "interpreter or leave AXOM_PYTHON_STABLE_ABI off.") endif() +if(DEFINED AXOM_DIR AND NOT DEFINED axom_DIR) + set(axom_DIR "${AXOM_DIR}" CACHE PATH "Axom CMake package directory") +endif() + find_package(axom CONFIG REQUIRED) find_package(nanobind CONFIG REQUIRED) # supplied via build-system.requires @@ -120,8 +124,8 @@ if(DEFINED SKBUILD_PROJECT_VERSION AND DEFINED AXOM_VERSION_MAJOR) "Axom version mismatch: the wheel's metadata version is " "${SKBUILD_PROJECT_VERSION} (from src/cmake/AxomVersion.cmake in this " "source tree) but the Axom install it would link against is " - "${_axom_installed_version} (axom_DIR=${axom_DIR}). Build the wheel from " - "the source tree that produced the install, or point axom_DIR at an " + "${_axom_installed_version} (AXOM_DIR=${axom_DIR}). Build the wheel from " + "the source tree that produced the install, or point AXOM_DIR at an " "install built from this source tree.") endif() endif() diff --git a/src/python/README.md b/src/python/README.md index 8e65424244..a8eaf354da 100644 --- a/src/python/README.md +++ b/src/python/README.md @@ -66,7 +66,7 @@ then build a **binding-only** wheel against that install and install it into a v ```bash # Axom + Conduit already built and installed; compile just the bindings against them. -uv build --wheel -C cmake.define.axom_DIR="$AXOM_INSTALL/lib/cmake" src/python +uv build --wheel -C cmake.define.AXOM_DIR="$AXOM_INSTALL/lib/cmake" src/python uv pip install dist/axom-*.whl ``` @@ -157,11 +157,15 @@ Producing portable, many-platform wheels would additionally need a tool such as plus a bundling/repair step, which is out of scope here. **Pointing the build at the install.** Pass one flag, -`-C cmake.define.axom_DIR="$AXOM_INSTALL/lib/cmake"`. +`-C cmake.define.AXOM_DIR="$AXOM_INSTALL/lib/cmake"`. +Use an absolute path to the directory containing `axom-config.cmake`. +Relative paths may be interpreted from scikit-build-core's temporary build directory. +The underlying CMake package variable is `axom_DIR` because the project calls `find_package(axom)`, +and that spelling still works. `AXOM_DIR` is accepted as an Axom-conventional alias by this wheel build. + Conduit does not need a flag of its own in the common case since `find_package(axom CONFIG)` pulls in Conduit via `find_dependency`, using the Conduit prefix recorded in `axom-config.cmake` -when Axom was installed. -Pass `-C cmake.define.Conduit_DIR="$CONDUIT_INSTALL/lib/cmake/conduit"` only if +when Axom was installed. Pass `-C cmake.define.Conduit_DIR="$CONDUIT_INSTALL/lib/cmake/conduit"` only if that recorded path no longer resolves (e.g. a relocated install, a different mount, or a container path). If the Axom install is MPI-enabled, make the wheel build use the same compiler @@ -170,7 +174,7 @@ build's `CMakeCache.txt` or host-config: ```bash uv build --wheel \ - -C cmake.define.axom_DIR="$AXOM_INSTALL/lib/cmake" \ + -C cmake.define.AXOM_DIR="$AXOM_INSTALL/lib/cmake" \ -C cmake.define.CMAKE_C_COMPILER="$AXOM_C_COMPILER" \ -C cmake.define.CMAKE_CXX_COMPILER="$AXOM_CXX_COMPILER" \ -C cmake.define.MPI_C_COMPILER="$AXOM_MPI_C_COMPILER" \ @@ -226,7 +230,7 @@ explicitly: ```bash uv build --wheel \ - -C cmake.define.axom_DIR="$AXOM_INSTALL/lib/cmake" \ + -C cmake.define.AXOM_DIR="$AXOM_INSTALL/lib/cmake" \ -C cmake.define.CONDUIT_PYTHON_MODULE_DIR="$CONDUIT_INSTALL/lib/pythonX.Y/site-packages" \ src/python ``` @@ -247,12 +251,13 @@ cmake -C "$(axom-python-config --host-config)" -S -B ### Per-host-config wheelhouse -Prebuilt wheels live in a per-host-config wheelhouse, e.g. `/usr/workspace/<...>/wheelhouse//` -(one directory per host-config, since a wheel is not portable across toolchains). -Consume it with `--find-links` (or a `[tool.uv.sources]` entry): +Axom does not assume a central public wheelhouse. If a site, CI job, or team +publishes prebuilt Axom wheels, keep them separated by host-config because these +wheels are not portable across toolchains. Consume that site-provided directory +with `--find-links` (or a `[tool.uv.sources]` entry): ```bash -uv pip install axom --find-links /path/to/wheelhouse/dane-gcc13 +uv pip install axom --find-links /path/to/site/wheelhouse/ ``` ### Developer loop (editable, rebuild-on-import) @@ -265,7 +270,7 @@ if it misbehaves, reinstall the editable wheel to force a rebuild: ```bash uv pip install nanobind 'scikit-build-core[pyproject]' uv pip install -e src/python --no-build-isolation \ - -C cmake.define.axom_DIR="$AXOM_INSTALL/lib/cmake" \ + -C cmake.define.AXOM_DIR="$AXOM_INSTALL/lib/cmake" \ -C build-dir=build/py -C editable.rebuild=true (cd "$(mktemp -d)" && uv run --project "$OLDPWD" \ pytest -o python_files='*_Py.py' "$OLDPWD/src/axom/sidre/tests/") @@ -285,7 +290,7 @@ the scikit-build-core setting sets the wheel tag, and the two must agree): uv build --wheel \ -C cmake.define.AXOM_PYTHON_STABLE_ABI=ON \ -C wheel.py-api=cp312 \ - -C cmake.define.axom_DIR="$AXOM_INSTALL/lib/cmake" \ + -C cmake.define.AXOM_DIR="$AXOM_INSTALL/lib/cmake" \ src/python ``` From b90dec128c0463e7ff5c41aa62e5bc8af129d2c7 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 17:18:17 -0700 Subject: [PATCH 14/35] Python: More streamlining of docs --- .../sidre/docs/sphinx/python_interface.rst | 77 +++--------- src/python/CMakeLists.txt | 84 ++----------- src/python/README.md | 113 +++++++----------- 3 files changed, 72 insertions(+), 202 deletions(-) diff --git a/src/axom/sidre/docs/sphinx/python_interface.rst b/src/axom/sidre/docs/sphinx/python_interface.rst index b578e4b4b1..fbd6c30caf 100644 --- a/src/axom/sidre/docs/sphinx/python_interface.rst +++ b/src/axom/sidre/docs/sphinx/python_interface.rst @@ -89,35 +89,23 @@ After ``spack install``, the environment's interpreter should have a working Axo pip / uv wheel (thin, external Axom) ------------------------------------ -The wheel compiles only the binding code against an already-installed Axom and Conduit; -it does not build Axom or its third-party libraries. -A wheel is therefore specific to the toolchain/glibc of the Axom install it was built against. -These wheels are not portable and not intended for PyPI; -they target controlled environments (an LC host-config, a spack view, or a CI image). +The wheel compiles only the Sidre binding against an already-installed Axom. +It is tied to that Axom install, its Conduit install, and its host-config; +it is not a portable PyPI-style wheel. .. note:: - **Do not install Conduit from PyPI.** Axom's bindings pass ``conduit::Node`` objects across the - C++ boundary to the ``conduit`` Python module, so that module must wrap the *same* ``libconduit`` - that Axom was compiled and linked against. - Two separate PyPI packages are easy to reach for by mistake, and neither works: + **Do not install Conduit from PyPI.** ``axom.sidre`` must use the same + ``libconduit`` that Axom was built against. The PyPI packages named + ``conduit`` and ``llnl-conduit`` do not provide that same build. - * ``conduit`` is an **unrelated project** (a stream-transformation library for power-engineering - analytics). - * ``llnl-conduit`` **is** LLNL's Conduit, but installing it produces a *separate build* of the library, - compiled by pip with its own compiler, flags and third-party configuration. - It is unlikely to be ABI-compatible with the spack/CMake Conduit inside your Axom install, - and using it would put two ``libconduit`` libraries in one process. - - Instead, use the Conduit that your Axom was built against. The Axom wheel - records that same-build Conduit Python path in the venv with a ``conduit.pth`` file. + Use the Conduit Python package from the Conduit install recorded by Axom. + Wheels built by Axom's Python project record that path in ``conduit.pth``. Quick start ^^^^^^^^^^^ -Point the wheel build at the Axom install with ``AXOM_DIR``. This should be the -directory containing ``axom-config.cmake``, usually ``$AXOM_INSTALL/lib/cmake``. -Use an absolute path; relative paths can be interpreted from the temporary build -directory that ``uv`` creates. +Use an absolute ``AXOM_DIR`` pointing at the directory containing +``axom-config.cmake``, usually ``$AXOM_INSTALL/lib/cmake``. .. code-block:: bash @@ -129,7 +117,9 @@ directory that ``uv`` creates. $ uv run python -c "import axom.sidre, conduit, numpy; print(axom.__version__)" If ``axom.sidre`` is already installed in a venv but ``import conduit`` fails, -add the same-build Conduit Python package with one ``.pth`` file: +add the same-build Conduit Python package with one ``.pth`` file. On current LC +installs this path is usually ``$CONDUIT_INSTALL/lib/pythonX.Y/site-packages``; +the authoritative value is ``CONDUIT_PYTHON_MODULE_DIR`` in Conduit's CMake config. .. code-block:: bash @@ -138,44 +128,18 @@ add the same-build Conduit Python package with one ``.pth`` file: "$(uv run python -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')/conduit.pth" $ uv run python -c "import axom.sidre, conduit; print(conduit.__file__)" -Use the Conduit install that Axom was built against. -The correct path is ``CONDUIT_PYTHON_MODULE_DIR`` in Conduit's CMake config. -On current LC installs it is usually ``$CONDUIT_INSTALL/lib/pythonX.Y/site-packages``. - -.. note:: +If your site publishes a host-config-specific wheelhouse, install from the path +they provide with ``uv pip install axom --find-links ``. +Axom does not assume a central wheelhouse. - Prebuilt wheelhouses are site-specific. - If your platform team publishes one for your host-config, install from the path they provide with - ``uv pip install axom --find-links ``. - This page does not assume a central Axom wheelhouse exists. - -Use the wheel's generated host-config when configuring downstream CMake projects: +The installed wheel also carries a CMake host-config for downstream projects: .. code-block:: bash $ cmake -C "$(uv run axom-python-config --host-config)" -S /path/to/project -B build -Three details matter when building the wheel yourself: - -* Use ``AXOM_DIR`` or ``axom_DIR``, not ``CMAKE_PREFIX_PATH``. - ``axom_DIR`` is CMake's package variable for ``find_package(axom)`` - ``AXOM_DIR`` is accepted by Axom's wheel build as a conventional uppercase alias. - scikit-build-core (which backs ``uv build`` and ``uv pip install``) - force-sets ``CMAKE_PREFIX_PATH`` to its own isolated build environment, - so a user-supplied value would be overwritten and ``find_package(axom)`` would fail. - A standalone ``cmake -S src/python`` has no such layer and can use ``CMAKE_PREFIX_PATH`` directly. -* Add ``-C cmake.define.Conduit_DIR="$CONDUIT_INSTALL/lib/cmake/conduit"`` only if - Conduit has moved since Axom was installed: Axom's config records the Conduit - prefix it was built against, and that recorded path is what the transitive lookup uses. - If Conduit's Python module lives in a nonstandard location that is not recorded - by Conduit's CMake config, also pass ``-C cmake.define.CONDUIT_PYTHON_MODULE_DIR=/path/to/site-packages``. -* For MPI-enabled Axom installs, use the same C/C++ compilers and MPI compiler - wrappers that built Axom. Copy these from the Axom build's ``CMakeCache.txt`` - or host-config. -* Build from the source tree that produced the install. - The wheel takes its version from ``src/cmake/AxomVersion.cmake`` in the checkout, - and the build fails with an explicit message if that disagrees with the installed Axom, - so a wheel can never misreport the version of the binary inside it. +For build details, including MPI compiler wrappers, editable installs, stable +ABI wheels, and site-specific wheelhouses, see ``src/python/README.md``. Using Axom in Jupyter ^^^^^^^^^^^^^^^^^^^^^ @@ -215,9 +179,6 @@ If the kernel cannot import ``axom.sidre``, it is nearly always either the wrong If the underlying Axom is an MPI build and you need to pass a communicator to ``IOManager`` (or to initialize MPI), install the ``mpi`` extra with ``uv pip install 'axom[mpi]'``. -For site-specific wheelhouses, the editable developer loop, and the optional -stable-ABI build, see ``src/python/README.md``. - ==================================== Working with Conduit and NumPy ==================================== diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index c65af4a3a7..1c530224b2 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -6,47 +6,22 @@ #------------------------------------------------------------------------------ # Thin, binding-only build of Axom's Python extension module(s). # -# This project compiles Axom's nanobind translation unit(s) against an already-installed Axom and Conduit. -# It does NOT build Axom or its third-party libraries. -# -# It is driven by the scikit-build-core project in the sibling pyproject.toml (see src/python/README.md), -# but is a valid standalone CMake project as well. -# -# When driven by scikit-build-core (uv build / uv pip install), -# point find_package at the installs with -DAXOM_DIR / -DConduit_DIR: -# scikit-build-core force-sets CMAKE_PREFIX_PATH to the isolated build env (to locate its own nanobind), -# so a user-supplied CMAKE_PREFIX_PATH is ignored. -# A standalone cmake invocation has no such layer and uses CMAKE_PREFIX_PATH directly: -# -# cmake -S src/python -B build/py -DCMAKE_PREFIX_PATH="$AXOM_INSTALL;$CONDUIT_INSTALL;$(python -m nanobind --cmake_dir)" -# cmake --build build/py -# cmake --install build/py --prefix +# This project compiles Axom's Python bindings against an installed Axom. +# It does not build Axom or its third-party libraries. #------------------------------------------------------------------------------ cmake_minimum_required(VERSION 3.21) project(axom_python LANGUAGES C CXX) -# Optional: build against Python's stable ABI (abi3) so one wheel serves every CPython >= 3.12 on the machine. -# Opt-in (default OFF) to provide per-Python wheel tags by default. -# Enable with BOTH build-time flags (they must agree; see pyproject.toml and src/python/README.md): +# Enable with both build-time flags; see src/python/README.md: # -C cmake.define.AXOM_PYTHON_STABLE_ABI=ON -C wheel.py-api=cp312 -# nanobind silently builds a non-stable module below Python 3.12, -# so an abi3 wheel needs a 3.12+ interpreter (which also provides Development.SABIModule). option(AXOM_PYTHON_STABLE_ABI "Build the extension against Python's stable ABI (abi3); needs Python >= 3.12" OFF) -# Development.Module (not Development) so we link libpython-free extensions, -# matching the in-tree build's discovery. -# Development.SABIModule is requested as an OPTIONAL component (nanobind's recommended pattern) -# so a stable-ABI build (AXOM_PYTHON_STABLE_ABI=ON, which needs Python >= 3.12) can find it, -# without a conditional component list. find_package(Python 3.10 REQUIRED COMPONENTS Interpreter Development.Module OPTIONAL_COMPONENTS Development.SABIModule) -# An optional component that is silently absent would turn a requested abi3 build -# into a confusing failure inside nanobind (or a non-stable module), so fail here -# with the actionable message instead. if(AXOM_PYTHON_STABLE_ABI AND NOT Python_Development.SABIModule_FOUND) message(FATAL_ERROR "AXOM_PYTHON_STABLE_ABI=ON requires Python's Development.SABIModule " @@ -108,14 +83,7 @@ if(NOT _axom_py_conduit_python_module_dir "where contains the same-build conduit Python package.") endif() -# The wheel's version metadata is read from this checkout's src/cmake/AxomVersion.cmake -# (see [[tool.dynamic-metadata]] in pyproject.toml), -# but the extension is compiled and linked against the Axom install found above. -# -# Building from a checkout that does not match the install would produce a wheel -# whose axom.__version__ misreports its own binary, so they must agree. -# (AXOM_VERSION_* come from axom-config.cmake; SKBUILD_PROJECT_VERSION is injected by scikit-build-core, -# so this check is active for wheel builds and skipped for a standalone cmake invocation.) +# Keep the wheel metadata version aligned with the Axom install it links. if(DEFINED SKBUILD_PROJECT_VERSION AND DEFINED AXOM_VERSION_MAJOR) set(_axom_installed_version "${AXOM_VERSION_MAJOR}.${AXOM_VERSION_MINOR}.${AXOM_VERSION_PATCH}") @@ -136,34 +104,20 @@ endif() set(_sidre_binding_sources "${CMAKE_CURRENT_SOURCE_DIR}/../axom/sidre/nanobind_sidre.cpp") -# Build the extension under the shared 'axom' nanobind domain so that C++ types bound in one Axom module -# (e.g. a sidre::Group*) are recognized by another module from the same build. -# nanobind only shares type bindings across modules that agree on domain *and* nanobind ABI, compiler, and build mode, -# hence the one-build/one-wheel rule. This mirrors src/axom/sidre/CMakeLists.txt. -# -# STABLE_ABI is appended only when AXOM_PYTHON_STABLE_ABI is set -# nanobind degrades to a non-stable build below 3.12. +# Match the in-tree Sidre binding domain so future Axom modules can share bound +# C++ types when built together. set(_axom_nb_module_args NB_DOMAIN axom) if(AXOM_PYTHON_STABLE_ABI) list(APPEND _axom_nb_module_args STABLE_ABI) endif() nanobind_add_module(_sidre ${_axom_nb_module_args} ${_sidre_binding_sources}) -# conduit::conduit_python provides conduit_python.hpp and is needed only by the binding TU, not by libsidre. target_link_libraries(_sidre PRIVATE axom::sidre conduit::conduit_python) -# Bake the linked libraries' locations into the module's rpath so it resolves libsidre/libconduit/HDF5 -# from the Axom/Conduit install with no LD_LIBRARY_PATH. -# pyproject also sets CMAKE_INSTALL_RPATH_USE_LINK_PATH for the wheel; -# this makes the standalone `cmake --install` path behave the same. set_target_properties(_sidre PROPERTIES INSTALL_RPATH_USE_LINK_PATH TRUE) #------------------------------------------------------------------------------ -# Generated runtime/development helpers. -# -# A thin wheel is tied to the Axom/Conduit install it was built against. Ship the -# corresponding Conduit Python path and CMake configuration hints with the wheel -# so users do not have to rediscover them by hand. +# Generated runtime/development helpers tied to the Axom/Conduit install. #------------------------------------------------------------------------------ configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/axom-python-host-config.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/axom-python-host-config.cmake" @@ -175,13 +129,11 @@ install(FILES "${CMAKE_CURRENT_BINARY_DIR}/axom-python-host-config.cmake" "${CMAKE_CURRENT_BINARY_DIR}/axom-python-env.sh" DESTINATION axom/share) -if(_axom_py_conduit_python_module_dir) - configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/conduit.pth.in" - "${CMAKE_CURRENT_BINARY_DIR}/conduit.pth" - @ONLY) - install(FILES "${CMAKE_CURRENT_BINARY_DIR}/conduit.pth" - DESTINATION ".") -endif() +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/conduit.pth.in" + "${CMAKE_CURRENT_BINARY_DIR}/conduit.pth" + @ONLY) +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/conduit.pth" + DESTINATION ".") #------------------------------------------------------------------------------ # HIP: replicate the in-tree special-case from src/axom/sidre/CMakeLists.txt. @@ -196,15 +148,9 @@ if(AXOM_ENABLE_HIP) endif() #------------------------------------------------------------------------------ -# Type stubs (PEP 561). -# # nanobind_add_stub imports the module ('import _sidre') to introspect it. -# # Turn this off on hosts where the build-time import is problematic: # -C cmake.define.AXOM_PYTHON_GENERATE_STUB=OFF -# The wheel still ships the checked-in package stub (axom/sidre/__init__.pyi) -# and py.typed via wheel.packages, so it stays a typed package either way; -# only the detailed _sidre.pyi (which __init__.pyi re-exports) is then absent. #------------------------------------------------------------------------------ option(AXOM_PYTHON_GENERATE_STUB "Generate the _sidre.pyi type stub at build time (imports the module)" ON) @@ -222,10 +168,6 @@ if(AXOM_PYTHON_GENERATE_STUB) endif() #------------------------------------------------------------------------------ -# Install only build products here. -# The pure-Python package files -# (axom/__init__.py, axom/py.typed, axom/sidre/__init__.py, axom/sidre/__init__.pyi) -# ship as pure Python via [tool.scikit-build] wheel.packages in pyproject.toml, -# so they are not installed by CMake (avoiding double-packaging). +# Pure-Python package files ship via [tool.scikit-build] wheel.packages. #------------------------------------------------------------------------------ install(TARGETS _sidre LIBRARY DESTINATION axom/sidre) diff --git a/src/python/README.md b/src/python/README.md index a8eaf354da..fe0743a05f 100644 --- a/src/python/README.md +++ b/src/python/README.md @@ -94,19 +94,9 @@ By design, `uv` does not generate the Axom libraries directly: Axom and its TPLs come from the CMake/spack world, and `uv` adds only the thin binding layer on top. Keeping the wheel thin makes it fast and reproducible against a known install. -Conduit is therefore deliberately not a dependency of the wheel; its Python module reaches the -venv through a generated `.pth` file pointing at the same-build Conduit -(see "Same-build Conduit" below). -Two distinct things on PyPI are worth mentioning: - -- **`conduit` on PyPI is an unrelated project** -- it is a a stream-transformation library - for power-engineering analytics, so avoid `pip install conduit` for Axom. -- **`llnl-conduit` on PyPI *is* LLNL's Conduit**, but it is a separate build of the library: - pip compiles or fetches its own `libconduit` with its own compiler, flags and TPL configuration. - Axom's bindings hand `conduit::Node` objects across the C++ boundary to the `conduit` Python module, - so that module must wrap the very same `libconduit` that Axom was compiled and linked against. - A pip-provided Conduit is unlikely to be ABI-compatible with the spack/CMake Conduit in your Axom install, - and mixing the two puts two `libconduit`s in one process. Use the install's own Conduit. +Conduit is deliberately not a Python dependency of the wheel. +The bindings must use the Conduit Python package from the same Conduit install Axom links, +not a separately built PyPI package. ## Layout @@ -146,31 +136,45 @@ A submodule is importable only when its component was enabled in the underlying ## Building the wheel: reference -The wheel is thin: it compiles only the binding code against an already-installed Axom and Conduit. -It never builds Axom or its third-party libraries, so a wheel is specific to the -Axom install (host-config / toolchain / glibc) it was built against. +The wheel compiles Axom's Python binding against an existing Axom install. +It is specific to that install and host-config; it is not repaired with `auditwheel` +and is not intended for PyPI. -These wheels are **not portable and not intended for PyPI**: they carry absolute rpaths to the install's -shared libraries and skip the `auditwheel` / `delocate` repair a redistributable `manylinux` wheel needs. -They target controlled environments -- e.g. an LC host-config, a spack view, a CI image. -Producing portable, many-platform wheels would additionally need a tool such as `cibuildwheel` -plus a bundling/repair step, which is out of scope here. +Use an absolute `AXOM_DIR` pointing at the directory containing `axom-config.cmake`, +normally `$AXOM_INSTALL/lib/cmake`: -**Pointing the build at the install.** Pass one flag, -`-C cmake.define.AXOM_DIR="$AXOM_INSTALL/lib/cmake"`. -Use an absolute path to the directory containing `axom-config.cmake`. -Relative paths may be interpreted from scikit-build-core's temporary build directory. -The underlying CMake package variable is `axom_DIR` because the project calls `find_package(axom)`, -and that spelling still works. `AXOM_DIR` is accepted as an Axom-conventional alias by this wheel build. +```bash +uv build --wheel -C cmake.define.AXOM_DIR="$AXOM_INSTALL/lib/cmake" src/python +``` -Conduit does not need a flag of its own in the common case since `find_package(axom CONFIG)` -pulls in Conduit via `find_dependency`, using the Conduit prefix recorded in `axom-config.cmake` -when Axom was installed. Pass `-C cmake.define.Conduit_DIR="$CONDUIT_INSTALL/lib/cmake/conduit"` only if -that recorded path no longer resolves (e.g. a relocated install, a different mount, or a container path). +The underlying CMake package variable is `axom_DIR`. +`AXOM_DIR` is accepted as an Axom-conventional alias. +Do not use `CMAKE_PREFIX_PATH` for `uv build` or `uv pip install` since scikit-build-core +uses it internally for the isolated build environment. + +Conduit is found through `axom-config.cmake` in the normal case. +Add `Conduit_DIR` only if Axom's recorded Conduit package path no longer resolves: + +```bash +uv build --wheel \ + -C cmake.define.AXOM_DIR="$AXOM_INSTALL/lib/cmake" \ + -C cmake.define.Conduit_DIR="$CONDUIT_INSTALL/lib/cmake/conduit" \ + src/python +``` -If the Axom install is MPI-enabled, make the wheel build use the same compiler -and MPI wrapper family that built Axom. The values can be copied from the Axom -build's `CMakeCache.txt` or host-config: +Add `CONDUIT_PYTHON_MODULE_DIR` only if Conduit's Python package path is not +recorded by Conduit's CMake config: + +```bash +uv build --wheel \ + -C cmake.define.AXOM_DIR="$AXOM_INSTALL/lib/cmake" \ + -C cmake.define.CONDUIT_PYTHON_MODULE_DIR="$CONDUIT_INSTALL/lib/pythonX.Y/site-packages" \ + src/python +``` + +For MPI-enabled Axom installs, pass the same compiler and MPI wrapper family +used by the Axom build. Copy these from the Axom build's `CMakeCache.txt` or +host-config: ```bash uv build --wheel \ @@ -182,35 +186,8 @@ uv build --wheel \ src/python ``` -This matters because `axom-config.cmake` re-runs CMake's MPI discovery while -loading Axom's MPI-enabled dependencies, and a plain environment may discover a -different system MPI than the one recorded in the Axom install. - -Do *not* use `CMAKE_PREFIX_PATH`. Under scikit-build-core (which drives `uv build` and `uv pip install`) -it is force-set to the isolated build environment (that is how the build locates its own bundled `nanobind`) -so a user-supplied value would be overwritten and ignored, and `find_package(axom)` -would fail with a "Could not find axom" message. -(A standalone `cmake -S src/python` invocation has no scikit-build-core layer and can use -`CMAKE_PREFIX_PATH` directly, but must then also place Conduit and nanobind on it.) - -**Build from the source tree that produced the install.** The wheel's version comes from -this checkout's `src/cmake/AxomVersion.cmake` while the extension links the installed Axom, -so the build fails with an explicit message if the two disagree, rather than shipping a wheel whose -`axom.__version__` misreports its own binary. Note this is a coarse check: Axom's version changes only -at a release, so a `develop` checkout and a same-release install compare equal even though they are -different code. Matching the two is still your responsibility. - -Two constraints apply to every workflow below: - -- **Same-build Conduit.** The bindings exchange `conduit::Node`s with the `conduit` Python module - through Conduit's C capsule API, so that module must wrap the *same* `libconduit` the install links. - The wheel writes a `conduit.pth` file into the venv's `site-packages` using - `CONDUIT_PYTHON_MODULE_DIR` from Conduit's CMake config. Do not install either - PyPI package (see "What `uv` builds" above). -- **Matching interpreter.** Build with the interpreter family whose toolchain/glibc matches the host-config. - On LC, pin it explicitly: `uv venv --python $(which python3)`. -- **Matching compiler/MPI wrappers.** For MPI Axom installs, pass the same - C/C++ compilers and MPI wrappers used by the Axom build, as shown above. +Build from the source tree that produced the install. The build compares the +wheel metadata version with the installed Axom version and fails if they differ. If Axom is already installed in a venv but `import conduit` fails, this is the only manual step usually needed: @@ -225,16 +202,6 @@ uv run python -c "import axom.sidre, conduit; print(conduit.__file__)" Use `CONDUIT_PYTHON_MODULE_DIR` from Conduit's CMake config. On current LC installs it is usually `lib/pythonX.Y/site-packages`, not `python-modules`. -If Conduit's Python module is not recorded by Conduit's CMake config, pass it -explicitly: - -```bash -uv build --wheel \ - -C cmake.define.AXOM_DIR="$AXOM_INSTALL/lib/cmake" \ - -C cmake.define.CONDUIT_PYTHON_MODULE_DIR="$CONDUIT_INSTALL/lib/pythonX.Y/site-packages" \ - src/python -``` - The wheel also installs development helpers: ```bash From b11a5699ad2eeb99019af659f366f9395f1a8620 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 21:13:55 -0700 Subject: [PATCH 15/35] Sidre/Python: Annotate and name sidre binding function arguments This allows for better IDE integration. --- src/axom/sidre/nanobind_sidre.cpp | 324 +++++++++++++++++++++--------- 1 file changed, 232 insertions(+), 92 deletions(-) diff --git a/src/axom/sidre/nanobind_sidre.cpp b/src/axom/sidre/nanobind_sidre.cpp index cc1c2821e1..0e8024097e 100644 --- a/src/axom/sidre/nanobind_sidre.cpp +++ b/src/axom/sidre/nanobind_sidre.cpp @@ -724,8 +724,14 @@ NB_MODULE(_sidre, m_sidre) m_sidre.attr("InvalidIndex") = axom::InvalidIndex; m_sidre.attr("InvalidName") = axom::utilities::string::InvalidName; - m_sidre.def("indexIsValid", &indexIsValid, "Returns true if idx is valid, else false."); - m_sidre.def("nameIsValid", &nameIsValid, "Returns true if name is valid, else false."); + m_sidre.def("indexIsValid", + &indexIsValid, + "Returns true if idx is valid, else false.", + nb::arg("idx")); + m_sidre.def("nameIsValid", + &nameIsValid, + "Returns true if name is valid, else false.", + nb::arg("name")); #if defined(AXOM_USE_HDF5) m_sidre.attr("AXOM_USE_HDF5") = true; @@ -792,11 +798,13 @@ NB_MODULE(_sidre, m_sidre) .def("getNumBuffers", &DataStore::getNumBuffers, "Return number of Buffers in the DataStore") .def("hasBuffer", &DataStore::hasBuffer, - "Return true if DataStore owns a Buffer with given index; else false") + "Return true if DataStore owns a Buffer with given index; else false", + nb::arg("idx")) .def("getBuffer", &DataStore::getBuffer, nb::rv_policy::reference_internal, - "Return pointer to Buffer object with the given index") + "Return pointer to Buffer object with the given index", + nb::arg("idx")) .def("createBuffer", nb::overload_cast<>(&DataStore::createBuffer), @@ -805,13 +813,17 @@ NB_MODULE(_sidre, m_sidre) .def("createBuffer", nb::overload_cast(&DataStore::createBuffer), nb::rv_policy::reference_internal, - "Create a Buffer object with specified type and number of elements") + "Create a Buffer object with specified type and number of elements", + nb::arg("type"), + nb::arg("num_elems")) .def("destroyBuffer", nb::overload_cast(&DataStore::destroyBuffer), - "Remove Buffer from the DataStore and destroy it and its data") + "Remove Buffer from the DataStore and destroy it and its data", + nb::arg("buffer")) .def("destroyBuffer", nb::overload_cast(&DataStore::destroyBuffer), - "Remove Buffer with given index from the DataStore and destroy it and its data.") + "Remove Buffer with given index from the DataStore and destroy it and its data.", + nb::arg("idx")) .def("destroyAllBuffers", &DataStore::destroyAllBuffers, "Remove all Buffers from the DataStore and destroy them and their data") @@ -820,12 +832,17 @@ NB_MODULE(_sidre, m_sidre) "Return first valid Buffer index") .def("getNextValidBufferIndex", &DataStore::getNextValidBufferIndex, - "Return next valid Buffer index after given index") + "Return next valid Buffer index after given index", + nb::arg("idx")) .def("generateBlueprintIndex", nb::overload_cast( &DataStore::generateBlueprintIndex), - "Generate a Conduit Blueprint index based on a mesh in stored in this DataStore.") + "Generate a Conduit Blueprint index based on a mesh in stored in this DataStore.", + nb::arg("domain_path"), + nb::arg("mesh_name"), + nb::arg("index_path"), + nb::arg("num_domains")) .def("buffers", nb::overload_cast<>(&DataStore::buffers), nb::keep_alive<0, 1>(), @@ -849,33 +866,42 @@ NB_MODULE(_sidre, m_sidre) .def("createAttributeString", &DataStore::createAttributeString, nb::rv_policy::reference_internal, - "Create an Attribute object with a default string value") + "Create an Attribute object with a default string value", + nb::arg("name"), + nb::arg("default_value").noconvert()) .def("hasAttribute", nb::overload_cast(&DataStore::hasAttribute, nb::const_), - "Return true if DataStore has created attribute name, else false") + "Return true if DataStore has created attribute name, else false", + nb::arg("name")) .def("hasAttribute", nb::overload_cast(&DataStore::hasAttribute, nb::const_), - "Return true if DataStore has created attribute with index, else false") + "Return true if DataStore has created attribute with index, else false", + nb::arg("idx")) .def("destroyAttribute", nb::overload_cast(&DataStore::destroyAttribute), - "Remove Attribute from the DataStore and destroy it and its data") + "Remove Attribute from the DataStore and destroy it and its data", + nb::arg("name")) .def("destroyAttribute", nb::overload_cast(&DataStore::destroyAttribute), - "Remove Attribute with given index from the DataStore and destroy it and its data") + "Remove Attribute with given index from the DataStore and destroy it and its data", + nb::arg("idx")) .def("destroyAttribute", nb::overload_cast(&DataStore::destroyAttribute), - "Remove Attribute from the DataStore and destroy it and its data") + "Remove Attribute from the DataStore and destroy it and its data", + nb::arg("attr")) .def("destroyAllAttributes", &DataStore::destroyAllAttributes, "Remove all Attributes from the DataStore and destroy them and their data") .def("getAttribute", nb::overload_cast(&DataStore::getAttribute), nb::rv_policy::reference_internal, - "Return pointer to non-const Attribute with given index") + "Return pointer to non-const Attribute with given index", + nb::arg("idx")) .def("getAttribute", nb::overload_cast(&DataStore::getAttribute), nb::rv_policy::reference_internal, - "Return pointer to non-const Attribute with given name") + "Return pointer to non-const Attribute with given name", + nb::arg("name")) // Requires conduit::Node information // .def("saveAttributeLayout", @@ -892,7 +918,8 @@ NB_MODULE(_sidre, m_sidre) .def("getNextValidAttributeIndex", &DataStore::getNextValidAttributeIndex, "Return next valid Attribute index in DataStore object after given index" - "(i.e., smallest index over all Attribute indices larger than given one)") + "(i.e., smallest index over all Attribute indices larger than given one)", + nb::arg("idx")) .def("attributes", nb::overload_cast<>(&DataStore::attributes), nb::keep_alive<0, 1>(), @@ -1034,7 +1061,9 @@ NB_MODULE(_sidre, m_sidre) "Return number of dimensions in data view and shape information" " of this data view object." " ndims - maximum number of dimensions to return." - " shape - user supplied numpy 1D array assumed to be ndims long.") + " shape - user supplied numpy 1D array assumed to be ndims long.", + nb::arg("ndims"), + nb::arg("shape")) .def("allocate", nb::overload_cast(&View::allocate), @@ -1051,7 +1080,8 @@ NB_MODULE(_sidre, m_sidre) .def("reallocate", nb::overload_cast(&View::reallocate), nb::rv_policy::reference, - "Reallocate data for the View.") + "Reallocate data for the View.", + nb::arg("num_elems")) .def("attachBuffer", nb::overload_cast(&View::attachBuffer), nb::rv_policy::reference, @@ -1102,7 +1132,10 @@ NB_MODULE(_sidre, m_sidre) return self.apply(type, ndims, shape.data()); }, nb::rv_policy::reference, - "Apply data description with type and numpy shape.") + "Apply data description with type and numpy shape.", + nb::arg("type"), + nb::arg("ndims"), + nb::arg("shape")) .def("setScalar", &View::setScalar, nb::rv_policy::reference, @@ -1145,7 +1178,10 @@ NB_MODULE(_sidre, m_sidre) return setExternalDataAndPinOwner(self, type, num_elems, external_ptr); }, nb::rv_policy::reference, - "Set the View to hold described external data (numpy array).") + "Set the View to hold described external data (numpy array).", + nb::arg("type"), + nb::arg("num_elems"), + nb::arg("external_ptr")) .def( "setExternalData", [](View& self, @@ -1156,7 +1192,11 @@ NB_MODULE(_sidre, m_sidre) return setExternalDataAndPinOwner(self, type, ndims, shape, external_ptr); }, nb::rv_policy::reference, - "Set the View to hold described external data (numpy array).") + "Set the View to hold described external data (numpy array).", + nb::arg("type"), + nb::arg("ndims"), + nb::arg("shape"), + nb::arg("external_ptr")) .def("getString", &View::getString, @@ -1184,24 +1224,28 @@ NB_MODULE(_sidre, m_sidre) .def("print", nb::overload_cast<>(&View::print, nb::const_), "Print JSON description of the View.") - .def("rename", &View::rename, "Change the name of the View.") + .def("rename", &View::rename, "Change the name of the View.", nb::arg("new_name")) // Attribute accessors .def("getAttribute", nb::overload_cast(&View::getAttribute), nb::rv_policy::reference_internal, - "Get Attribute by index") + "Get Attribute by index", + nb::arg("idx")) .def("getAttribute", nb::overload_cast(&View::getAttribute), nb::rv_policy::reference_internal, - "Get Attribute by name") + "Get Attribute by name", + nb::arg("name")) .def("hasAttributeValue", nb::overload_cast(&View::hasAttributeValue, nb::const_), - "Return true if the attribute (by index) has been explicitly set; else false.") + "Return true if the attribute (by index) has been explicitly set; else false.", + nb::arg("idx")) .def("hasAttributeValue", nb::overload_cast(&View::hasAttributeValue, nb::const_), - "Return true if the attribute (by name) has been explicitly set; else false.") + "Return true if the attribute (by name) has been explicitly set; else false.", + nb::arg("name")) .def("hasAttributeValue", nb::overload_cast(&View::hasAttributeValue, nb::const_), nb::arg("attr").none(), @@ -1209,10 +1253,12 @@ NB_MODULE(_sidre, m_sidre) .def("setAttributeToDefault", nb::overload_cast(&View::setAttributeToDefault), - "Set Attribute (by index) to its default value") + "Set Attribute (by index) to its default value", + nb::arg("idx")) .def("setAttributeToDefault", nb::overload_cast(&View::setAttributeToDefault), - "Set Attribute (by name) to its default value") + "Set Attribute (by name) to its default value", + nb::arg("name")) .def("setAttributeToDefault", nb::overload_cast(&View::setAttributeToDefault), nb::arg("attr").none(), @@ -1222,46 +1268,64 @@ NB_MODULE(_sidre, m_sidre) .def( "setAttributeScalar", [](View& self, IndexType idx, int value) { return self.setAttributeScalar(idx, value); }, - "Set Attribute (by index) to int value") + "Set Attribute (by index) to int value", + nb::arg("idx"), + nb::arg("value").noconvert()) .def( "setAttributeScalar", [](View& self, IndexType idx, double value) { return self.setAttributeScalar(idx, value); }, - "Set Attribute (by index) to float (C++ double) value") + "Set Attribute (by index) to float (C++ double) value", + nb::arg("idx"), + nb::arg("value").noconvert()) .def( "setAttributeScalar", [](View& self, const std::string& name, int value) { return self.setAttributeScalar(name, value); }, - "Set Attribute (by name) to int value") + "Set Attribute (by name) to int value", + nb::arg("name"), + nb::arg("value").noconvert()) .def( "setAttributeScalar", [](View& self, const std::string& name, double value) { return self.setAttributeScalar(name, value); }, - "Set Attribute (by name) to float (C++ double) value") + "Set Attribute (by name) to float (C++ double) value", + nb::arg("name"), + nb::arg("value").noconvert()) .def( "setAttributeScalar", [](View& self, const Attribute* attr, int value) { return self.setAttributeScalar(attr, value); }, - "Set Attribute (by pointer) to int value") + "Set Attribute (by pointer) to int value", + nb::arg("attr").none(), + nb::arg("value").noconvert()) .def( "setAttributeScalar", [](View& self, const Attribute* attr, double value) { return self.setAttributeScalar(attr, value); }, - "Set Attribute (by pointer) to float (C++ double) value") + "Set Attribute (by pointer) to float (C++ double) value", + nb::arg("attr").none(), + nb::arg("value").noconvert()) // String setters .def("setAttributeString", nb::overload_cast(&View::setAttributeString), - "Set Attribute (by index) to string value") + "Set Attribute (by index) to string value", + nb::arg("idx"), + nb::arg("value").noconvert()) .def("setAttributeString", nb::overload_cast(&View::setAttributeString), - "Set Attribute (by name) to string value") + "Set Attribute (by name) to string value", + nb::arg("name"), + nb::arg("value").noconvert()) .def("setAttributeString", nb::overload_cast(&View::setAttributeString), - "Set Attribute (by pointer) to string value") + "Set Attribute (by pointer) to string value", + nb::arg("attr").none(), + nb::arg("value").noconvert()) // Requires conduit::Node information // Scalar getters (Node::ConstValue version) @@ -1279,19 +1343,23 @@ NB_MODULE(_sidre, m_sidre) .def( "getAttributeScalarInt", [](View& self, IndexType idx) { return self.getAttributeScalar(idx); }, - "Return scalar Attribute value (by index) as int") + "Return scalar Attribute value (by index) as int", + nb::arg("idx")) .def( "getAttributeScalarFloat", [](View& self, IndexType idx) { return self.getAttributeScalar(idx); }, - "Return scalar Attribute value (by index) as float (C++ double)") + "Return scalar Attribute value (by index) as float (C++ double)", + nb::arg("idx")) .def( "getAttributeScalarInt", [](View& self, const std::string& name) { return self.getAttributeScalar(name); }, - "Return scalar Attribute value (by name) as int") + "Return scalar Attribute value (by name) as int", + nb::arg("name")) .def( "getAttributeScalarFloat", [](View& self, const std::string& name) { return self.getAttributeScalar(name); }, - "Return scalar Attribute value (by name) as float (C++ double)") + "Return scalar Attribute value (by name) as float (C++ double)", + nb::arg("name")) .def( "getAttributeScalarInt", [](View& self, const Attribute* attr) { return self.getAttributeScalar(attr); }, @@ -1306,13 +1374,16 @@ NB_MODULE(_sidre, m_sidre) // String getters .def("getAttributeString", nb::overload_cast(&View::getAttributeString, nb::const_), - "Return string Attribute value (by index)") + "Return string Attribute value (by index)", + nb::arg("idx")) .def("getAttributeString", nb::overload_cast(&View::getAttributeString, nb::const_), - "Return string Attribute value (by name)") + "Return string Attribute value (by name)", + nb::arg("name")) .def("getAttributeString", nb::overload_cast(&View::getAttributeString, nb::const_), - "Return string Attribute value (by pointer)") + "Return string Attribute value (by pointer)", + nb::arg("attr").none()) // Requires conduit::Node information // Node reference getters @@ -1323,7 +1394,8 @@ NB_MODULE(_sidre, m_sidre) return nodeToNbObject(node); }, nb::rv_policy::reference, - "Return reference to Attribute Node (by index)") + "Return reference to Attribute Node (by index)", + nb::arg("idx")) .def( "getAttributeNodeRef", [](View& self, const std::string& name) { @@ -1331,7 +1403,8 @@ NB_MODULE(_sidre, m_sidre) return nodeToNbObject(node); }, nb::rv_policy::reference, - "Return reference to Attribute Node (by name)") + "Return reference to Attribute Node (by name)", + nb::arg("name")) .def( "getAttributeNodeRef", [](View& self, const Attribute* attr) { @@ -1339,7 +1412,8 @@ NB_MODULE(_sidre, m_sidre) return nodeToNbObject(node); }, nb::rv_policy::reference, - "Return reference to Attribute Node (by pointer)") + "Return reference to Attribute Node (by pointer)", + nb::arg("attr").none()) // Attribute index iteration .def("getFirstValidAttrValueIndex", @@ -1349,7 +1423,8 @@ NB_MODULE(_sidre, m_sidre) .def("getNextValidAttrValueIndex", &View::getNextValidAttrValueIndex, "Return next valid Attribute index for a set Attribute in View object after given index" - "(i.e., smallest index over all Attribute indices larger than given one)"); + "(i.e., smallest index over all Attribute indices larger than given one)", + nb::arg("idx")); // Bindings for the Group class nb::class_(m_sidre, "Group") @@ -1383,44 +1458,56 @@ NB_MODULE(_sidre, m_sidre) .def("hasView", nb::overload_cast(&Group::hasView, nb::const_), - "Return true if Group includes a descendant View with given name or path; else false.") + "Return true if Group includes a descendant View with given name or path; else false.", + nb::arg("path")) .def("hasView", nb::overload_cast(&Group::hasView, nb::const_), - "Return true if this Group owns a View with given index; else false") + "Return true if this Group owns a View with given index; else false", + nb::arg("idx")) .def("hasChildView", &Group::hasChildView, - "Return true if this Group owns a View with given name (not path); else false.") + "Return true if this Group owns a View with given name (not path); else false.", + nb::arg("name")) .def("getViewIndex", &Group::getViewIndex, - "Return index of View with given name owned by this Group object.") + "Return index of View with given name owned by this Group object.", + nb::arg("name")) .def("getViewName", &Group::getViewName, - "Return name of View with given index owned by Group object.") + "Return name of View with given index owned by Group object.", + nb::arg("idx")) .def("getView", nb::overload_cast(&Group::getView, nb::const_), nb::rv_policy::reference_internal, - "Return pointer to const View with given name or path.") + "Return pointer to const View with given name or path.", + nb::arg("path")) .def("getView", nb::overload_cast(&Group::getView, nb::const_), nb::rv_policy::reference_internal, - "Return pointer to non-const View with given index.") + "Return pointer to non-const View with given index.", + nb::arg("idx")) .def("getFirstValidViewIndex", &Group::getFirstValidViewIndex, "Return first valid View index in Group object.") .def("getNextValidViewIndex", &Group::getNextValidViewIndex, - "Return next valid View index in Group object after given index.") + "Return next valid View index in Group object after given index.", + nb::arg("idx")) .def("createView", nb::overload_cast(&Group::createView), nb::rv_policy::reference_internal, - "Create an undescribed (i.e., empty) View object with given name or path in this Group.") + "Create an undescribed (i.e., empty) View object with given name or path in this Group.", + nb::arg("path")) .def("createView", nb::overload_cast(&Group::createView), nb::rv_policy::reference_internal, "Create View object with given name or path in this Group that has a data description " - "with data type and number of elements.") + "with data type and number of elements.", + nb::arg("path"), + nb::arg("type"), + nb::arg("num_elems")) .def( "createViewWithShape", [](Group& self, const std::string& path, TypeID type, int ndims, const nb::ndarray& shape) { @@ -1428,17 +1515,27 @@ NB_MODULE(_sidre, m_sidre) }, nb::rv_policy::reference_internal, "Create View object with given name or path in this Group that has a data description " - "with data type and shape.") + "with data type and shape.", + nb::arg("path"), + nb::arg("type"), + nb::arg("ndims"), + nb::arg("shape")) .def("createView", nb::overload_cast(&Group::createView), nb::rv_policy::reference_internal, "Create an undescribed View object with given name or path in this Group and attach given " - "Buffer to it.") + "Buffer to it.", + nb::arg("path"), + nb::arg("buffer").none()) .def("createView", nb::overload_cast(&Group::createView), nb::rv_policy::reference_internal, "Create View object with given name or path in this Group that has a data description " - "with data type and number of elements and attach given Buffer to it.") + "with data type and number of elements and attach given Buffer to it.", + nb::arg("path"), + nb::arg("type"), + nb::arg("num_elems"), + nb::arg("buffer").none()) .def( "createViewWithShape", [](Group& self, @@ -1451,7 +1548,12 @@ NB_MODULE(_sidre, m_sidre) }, nb::rv_policy::reference_internal, "Create View object with given name or path in this Group that has a data description " - "with data type and shape and attach given Buffer to it.") + "with data type and shape and attach given Buffer to it.", + nb::arg("path"), + nb::arg("type"), + nb::arg("ndims"), + nb::arg("shape"), + nb::arg("buffer").none()) .def( "createView", @@ -1460,7 +1562,9 @@ NB_MODULE(_sidre, m_sidre) pinExternalDataOwner(view, a); return view; }, - nb::rv_policy::reference_internal) + nb::rv_policy::reference_internal, + nb::arg("path"), + nb::arg("external_ptr")) .def( "createView", @@ -1471,7 +1575,11 @@ NB_MODULE(_sidre, m_sidre) }, nb::rv_policy::reference_internal, "Create View object with given name or path in this Group that has a data description " - "with data type and number of elements and attach externally-owned data to it.") + "with data type and number of elements and attach externally-owned data to it.", + nb::arg("path"), + nb::arg("type"), + nb::arg("num_elems"), + nb::arg("external_ptr")) .def( "createViewWithShape", @@ -1487,7 +1595,12 @@ NB_MODULE(_sidre, m_sidre) }, nb::rv_policy::reference_internal, "Create View object with given name or path in this Group that has a data description " - "with data type and shape and attach externally-owned data (numpy array) to it.") + "with data type and shape and attach externally-owned data (numpy array) to it.", + nb::arg("path"), + nb::arg("type"), + nb::arg("ndims"), + nb::arg("shape"), + nb::arg("external_ptr")) .def("createViewAndAllocate", nb::overload_cast(&Group::createViewAndAllocate), nb::rv_policy::reference_internal, @@ -1504,7 +1617,11 @@ NB_MODULE(_sidre, m_sidre) }, nb::rv_policy::reference_internal, "Create View object with given name or path in this Group that has a data description " - "with data type and shape and allocate data for it.") + "with data type and shape and allocate data for it.", + nb::arg("path"), + nb::arg("type"), + nb::arg("ndims"), + nb::arg("shape")) .def("createViewScalar", &Group::createViewScalar, @@ -1537,7 +1654,8 @@ NB_MODULE(_sidre, m_sidre) releaseExternalDataOwner(self.getView(path)); self.destroyView(path); }, - "Destroy View with given name or path owned by this Group, but leave its data intact.") + "Destroy View with given name or path owned by this Group, but leave its data intact.", + nb::arg("path")) .def( "destroyView", [](Group& self, IndexType idx) { @@ -1545,7 +1663,8 @@ NB_MODULE(_sidre, m_sidre) releaseExternalDataOwner(self.getView(idx)); self.destroyView(idx); }, - "Destroy View with given index owned by this Group, but leave its data intact.") + "Destroy View with given index owned by this Group, but leave its data intact.", + nb::arg("idx")) .def( "destroyViewAndData", [](Group& self, const std::string& path) { @@ -1553,7 +1672,8 @@ NB_MODULE(_sidre, m_sidre) releaseExternalDataOwner(self.getView(path)); self.destroyViewAndData(path); }, - "Destroy View with given name or path owned by this Group and deallocate") + "Destroy View with given name or path owned by this Group and deallocate", + nb::arg("path")) .def( "destroyViewAndData", [](Group& self, IndexType idx) { @@ -1561,7 +1681,8 @@ NB_MODULE(_sidre, m_sidre) self.destroyViewAndData(idx); }, "Destroy View with given index owned by this Group and deallocate its data if it's the " - "only View associated with that data.") + "only View associated with that data.", + nb::arg("idx")) .def( "destroyViewsAndData", [](Group& self) { @@ -1574,7 +1695,8 @@ NB_MODULE(_sidre, m_sidre) .def("moveView", &Group::moveView, nb::rv_policy::reference_internal, - "Remove given View object from its owning Group and move it to this Group.") + "Remove given View object from its owning Group and move it to this Group.", + nb::arg("view")) .def( "copyView", [](Group& self, View* view) { @@ -1588,31 +1710,39 @@ NB_MODULE(_sidre, m_sidre) return copy; }, nb::rv_policy::reference_internal, - "Create a (shallow) copy of given View object and add it to this Group.") + "Create a (shallow) copy of given View object and add it to this Group.", + nb::arg("view")) .def("hasGroup", nb::overload_cast(&Group::hasGroup, nb::const_), - "Return true if this Group has a descendant Group with given name or path; else false.") + "Return true if this Group has a descendant Group with given name or path; else false.", + nb::arg("path")) .def("hasGroup", nb::overload_cast(&Group::hasGroup, nb::const_), - "Return true if Group has an immediate child Group with given index; else false.") + "Return true if Group has an immediate child Group with given index; else false.", + nb::arg("idx")) .def("hasChildGroup", &Group::hasChildGroup, - "Return true if this Group has a child Group with given name; else false.") + "Return true if this Group has a child Group with given name; else false.", + nb::arg("name")) .def("getGroupIndex", &Group::getGroupIndex, - "Return the index of immediate child Group with given name.") + "Return the index of immediate child Group with given name.", + nb::arg("name")) .def("getGroupName", &Group::getGroupName, - "Return the name of immediate child Group with given index.") + "Return the name of immediate child Group with given index.", + nb::arg("idx")) .def("getGroup", nb::overload_cast(&Group::getGroup), nb::rv_policy::reference_internal, - "Return pointer to non-const child Group with given name or path.") + "Return pointer to non-const child Group with given name or path.", + nb::arg("path")) .def("getGroup", nb::overload_cast(&Group::getGroup), nb::rv_policy::reference_internal, - "Return pointer to non-const immediate child Group with given index.") + "Return pointer to non-const immediate child Group with given index.", + nb::arg("idx")) .def("views", nb::overload_cast<>(&Group::views), nb::keep_alive<0, 1>(), @@ -1626,7 +1756,8 @@ NB_MODULE(_sidre, m_sidre) "Return first valid child Group index (i.e., smallest index over all child Groups).") .def("getNextValidGroupIndex", &Group::getNextValidGroupIndex, - "Return next valid child Group index after given index.") + "Return next valid child Group index after given index.", + nb::arg("idx")) .def("createGroup", &Group::createGroup, nb::rv_policy::reference_internal, @@ -1646,7 +1777,8 @@ NB_MODULE(_sidre, m_sidre) releaseExternalDataOwners(self.getGroup(path)); self.destroyGroup(path); }, - "Destroy child Group in this Group with given name or path.") + "Destroy child Group in this Group with given name or path.", + nb::arg("path")) .def( "destroyGroup", [](Group& self, IndexType idx) { @@ -1654,7 +1786,8 @@ NB_MODULE(_sidre, m_sidre) releaseExternalDataOwners(self.getGroup(idx)); self.destroyGroup(idx); }, - "Destroy child Group within this Group with given index.") + "Destroy child Group within this Group with given index.", + nb::arg("idx")) .def( "destroyGroupAndData", [](Group& self, const std::string& path) { @@ -1663,7 +1796,8 @@ NB_MODULE(_sidre, m_sidre) self.destroyGroupAndData(path); }, "Destroy child Group at the given path, and destroy data that is " - "not shared elsewhere.") + "not shared elsewhere.", + nb::arg("path")) .def( "destroyGroupAndData", [](Group& self, IndexType idx) { @@ -1671,7 +1805,8 @@ NB_MODULE(_sidre, m_sidre) self.destroyGroupAndData(idx); }, "Destroy child Group with the given index, and destroy data that " - "is not shared elsewhere.") + "is not shared elsewhere.", + nb::arg("idx")) .def( "destroyGroupsAndData", [](Group& self) { @@ -1704,7 +1839,8 @@ NB_MODULE(_sidre, m_sidre) .def("moveGroup", &Group::moveGroup, nb::rv_policy::reference_internal, - "Remove given Group object from its parent Group and make it a child of this Group.") + "Remove given Group object from its parent Group and make it a child of this Group.", + nb::arg("group")) .def( "copyGroup", [](Group& self, Group* group) { @@ -1718,7 +1854,8 @@ NB_MODULE(_sidre, m_sidre) }, nb::rv_policy::reference_internal, "Create a (shallow) copy of Group hierarchy rooted at given " - "Group and make the copy a child of this Group.") + "Group and make the copy a child of this Group.", + nb::arg("group")) .def("deepCopyGroup", &Group::deepCopyGroup, nb::rv_policy::reference_internal, @@ -1765,11 +1902,12 @@ NB_MODULE(_sidre, m_sidre) .def("loadExternalData", nb::overload_cast(&Group::loadExternalData), - "Load data into the Group's external views from a file.") + "Load data into the Group's external views from a file.", + nb::arg("path")) .def_static("getDefaultIOProtocol", &Group::getDefaultIOProtocol, "Return the default I/O protocol for this Axom build.") - .def("rename", &Group::rename, "Change the name of this Group."); + .def("rename", &Group::rename, "Change the name of this Group.", nb::arg("new_name")); // Bindings for the Attribute class nb::class_(m_sidre, "Attribute") @@ -1786,7 +1924,8 @@ NB_MODULE(_sidre, m_sidre) nb::arg("value").noconvert()) .def("setDefaultString", &Attribute::setDefaultString, - "Set default value of Attribute as string. Return true if successfully changed.") + "Set default value of Attribute as string. Return true if successfully changed.", + nb::arg("value").noconvert()) .def( "getDefaultNodeRef", @@ -1892,7 +2031,8 @@ NB_MODULE(_sidre, m_sidre) nb::arg("root_file")) .def_static("correspondingRelayProtocol", &IOManager::correspondingRelayProtocol, - "Finds conduit relay protocol corresponding to a sidre protocol."); + "Finds conduit relay protocol corresponding to a sidre protocol.", + nb::arg("sidre_protocol")); #endif } From f4cb4b4b21e27687ec3eef2c16cd54b00d59609e Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 21:28:01 -0700 Subject: [PATCH 16/35] Python: Improves docs about installing optional dependencies And about better IDE integration with Jupyter. --- .../sidre/docs/sphinx/python_interface.rst | 43 ++++++++++++++++++- src/python/README.md | 26 +++++++++-- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/src/axom/sidre/docs/sphinx/python_interface.rst b/src/axom/sidre/docs/sphinx/python_interface.rst index fbd6c30caf..f8157c79d7 100644 --- a/src/axom/sidre/docs/sphinx/python_interface.rst +++ b/src/axom/sidre/docs/sphinx/python_interface.rst @@ -116,6 +116,22 @@ Use an absolute ``AXOM_DIR`` pointing at the directory containing $ uv run python -c "import axom.sidre, conduit, numpy; print(axom.__version__)" +Optional dependencies use the normal Python extras syntax on the local source +path. Keep the same CMake ``-C`` options used for the Axom install: + +.. code-block:: bash + + $ uv pip install '/path/to/axom/src/python[mpi]' \ + -C cmake.define.AXOM_DIR="$AXOM_INSTALL/lib/cmake" + + $ uv pip install '/path/to/axom/src/python[test]' \ + -C cmake.define.AXOM_DIR="$AXOM_INSTALL/lib/cmake" + +Use ``[mpi]`` for ``mpi4py`` support, ``[test]`` for ``pytest``, +or combine extras as ``'/path/to/axom/src/python[mpi,test]'``. +If the Axom wheel is already installed and you only need the optional dependency package, +installing ``mpi4py`` or ``pytest`` directly is also fine. + If ``axom.sidre`` is already installed in a venv but ``import conduit`` fails, add the same-build Conduit Python package with one ``.pth`` file. On current LC installs this path is usually ``$CONDUIT_INSTALL/lib/pythonX.Y/site-packages``; @@ -146,7 +162,7 @@ Using Axom in Jupyter Because the wheel and the Conduit ``.pth`` live in the venv's ``site-packages``, a Jupyter kernel running in that venv imports ``axom.sidre`` natively -- there is -nothing extra to configure, and no need to modify ``PYTHONPATH``. +nothing extra to configure, and no need to modify ``PYTHONPATH``. Add Jupyter to the same venv and register it as a kernel: .. code-block:: bash @@ -155,6 +171,17 @@ Add Jupyter to the same venv and register it as a kernel: $ uv run python -m ipykernel install --user --name axom --display-name "Axom (uv)" $ uv run jupyter lab +For more IDE-like completions, signature help, and hover documentation in JupyterLab, +install the language-server packages in the same venv: + +.. code-block:: bash + + $ uv pip install jupyterlab-lsp 'python-lsp-server[all]' + +The Axom wheel installs PEP 561 type information and generated ``.pyi`` stubs for ``axom.sidre``. +JupyterLab's LSP extension can use those stubs for richer completion and overload help +than the classic notebook frontend usually shows. + Select the **Axom (uv)** kernel, then for example: .. code-block:: python @@ -168,6 +195,15 @@ Select the **Axom (uv)** kernel, then for example: np.asarray(view.getDataArray())[:] = [1.0, 2.0, 3.0, 4.0] # zero-copy view print(np.asarray(grp.getView("velocity").getDataArray())) +.. warning:: + + Sidre currently preserves the C++ API's no-op semantics for some invalid operations. + For example, ``grp.createGroup("foo")`` followed by another ``grp.createGroup("foo")`` + returns ``None`` for the second call unless ``accept_existing=True`` is passed. + The related SLIC diagnostic may be written to the process stderr/log stream + instead of appearing as a notebook cell error, so notebook code should either check for ``None`` + or use the explicit ``accept_existing`` option when reusing a group is intended. + If the kernel cannot import ``axom.sidre``, it is nearly always either the wrong kernel (one outside the venv) or a missing Conduit ``.pth``. Check both from inside the notebook: @@ -177,7 +213,10 @@ If the kernel cannot import ``axom.sidre``, it is nearly always either the wrong import conduit; print(conduit.__file__) # expect $CONDUIT_INSTALL/lib/pythonX.Y/site-packages/... If the underlying Axom is an MPI build and you need to pass a communicator to -``IOManager`` (or to initialize MPI), install the ``mpi`` extra with ``uv pip install 'axom[mpi]'``. +``IOManager`` (or to initialize MPI), install the ``mpi`` extra. +For a local source install, use ``uv pip install '/path/to/axom/src/python[mpi]' -C ...`` +as shown above; for a prebuilt wheel from a wheelhouse, +use ``uv pip install 'axom[mpi]' --find-links ``. ==================================== Working with Conduit and NumPy diff --git a/src/python/README.md b/src/python/README.md index fe0743a05f..5a6f7b8c14 100644 --- a/src/python/README.md +++ b/src/python/README.md @@ -272,14 +272,32 @@ bindings and Conduit run under a free-threaded interpreter. Wheel metadata is static, but whether the underlying Axom is an MPI build is a build-time choice, so the wheel cannot force the MPI dependency at install time. -When you need mpi4py (to pass a communicator to `IOManager`, or to initialize MPI), install the extra explicitly: +When installing from this source tree, put extras on the local project path and +keep the same CMake `-C` options used to build against the Axom install: ```bash -uv pip install 'axom[mpi]' +uv pip install 'src/python[mpi]' \ + -C cmake.define.AXOM_DIR="$AXOM_INSTALL/lib/cmake" + +uv pip install 'src/python[test]' \ + -C cmake.define.AXOM_DIR="$AXOM_INSTALL/lib/cmake" + +uv pip install 'src/python[mpi,test]' \ + -C cmake.define.AXOM_DIR="$AXOM_INSTALL/lib/cmake" +``` + +Use `mpi` for `mpi4py` support (passing a communicator to `IOManager`, or initializing MPI) +and `test` for pytest. If Axom is already installed and you only need the optional dependency package, +installing `mpi4py` or `pytest` directly is also fine. + +For a prebuilt wheel from a site wheelhouse, put extras on the package name: + +```bash +uv pip install 'axom[mpi]' --find-links /path/to/site/wheelhouse/ +uv pip install 'axom[test]' --find-links /path/to/site/wheelhouse/ ``` -pytest lives in the `test` extra (`uv pip install 'axom[test]'`), -never in the runtime dependencies. +pytest lives in the `test` extra, never in the runtime dependencies. ## Notes From aaef4d114b8463fc583d0184a4d3c5d73eaf7d41 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 22:20:05 -0700 Subject: [PATCH 17/35] Python: Updates Python docker CI script to match docs --- .../github-actions/linux-wheel_and_test.sh | 67 ++++++++----------- 1 file changed, 29 insertions(+), 38 deletions(-) diff --git a/scripts/github-actions/linux-wheel_and_test.sh b/scripts/github-actions/linux-wheel_and_test.sh index 4c28063a6d..99f7a5c7f8 100755 --- a/scripts/github-actions/linux-wheel_and_test.sh +++ b/scripts/github-actions/linux-wheel_and_test.sh @@ -13,8 +13,8 @@ # 1. configure + build + install Axom (with Python bindings) from a docker t-config; # 2. build the wheel from src/python against that install (find_package(axom)); # 3. install the wheel into a fresh uv venv; -# 4. expose the *same-build* Conduit python module via a .pth file -# 5. run the Sidre Python test suite with plain `uv run pytest`. +# 4. verify the wheel installed conduit.pth for the same-build Conduit python module; +# 5. run the Sidre Python test suite with plain pytest. # # Intended for the gcc docker image, which is nanobind-enabled. @@ -51,28 +51,15 @@ or_die python3 ./config-build.py \ or_die cmake --build "${BUILD_DIR}" -j "${NUM_BUILD_PROCS}" or_die cmake --install "${BUILD_DIR}" -# Resolve the Axom install prefix and the Conduit python-modules directory from -# the CMake cache. Prefer CONDUIT_PYTHON_MODULE_DIR (the exact directory the -# in-tree build uses for the same purpose); fall back to CONDUIT_DIR/python-modules. +# Resolve the Axom install prefix from the CMake cache CACHE="${BUILD_DIR}/CMakeCache.txt" AXOM_INSTALL=$(awk -F= '/^CMAKE_INSTALL_PREFIX:[A-Z]*=/{print $2}' "${CACHE}") -CONDUIT_PY_DIR=$(awk -F= '/^CONDUIT_PYTHON_MODULE_DIR:[A-Z]*=/{print $2}' "${CACHE}") -if [[ -z "${CONDUIT_PY_DIR}" ]]; then - CONDUIT_DIR=$(awk -F= '/^CONDUIT_DIR:[A-Z]*=/{print $2}' "${CACHE}") - CONDUIT_PY_DIR="${CONDUIT_DIR}/python-modules" -fi echo "AXOM_INSTALL=${AXOM_INSTALL}" -echo "CONDUIT_PY_DIR=${CONDUIT_PY_DIR}" if [[ -z "${AXOM_INSTALL}" || ! -d "${AXOM_INSTALL}" ]]; then echo "ERROR: Axom install prefix not found (${AXOM_INSTALL})." exit 1 fi -if [[ -z "${CONDUIT_PY_DIR}" || ! -d "${CONDUIT_PY_DIR}" ]]; then - echo "ERROR: Conduit python-modules dir not found (${CONDUIT_PY_DIR})." - echo " The wheel needs the same-build Conduit python module (see src/python/README.md)." - exit 1 -fi echo "~~~~~~ ENSURE uv IS AVAILABLE ~~~~~~" if ! command -v uv >/dev/null 2>&1; then @@ -82,17 +69,19 @@ fi uv --version echo "~~~~~~ BUILD THE THIN WHEEL FROM src/python ~~~~~~" -# Point find_package at the install with axom_DIR. -# Don't use CMAKE_PREFIX_PATH since scikit-build-core force-sets that to its isolated build environment -# (and uses it to locate its own nanobind). -# Conduit resolves transitively from axom's config, which records its Conduit prefix; -# pass -C cmake.define.Conduit_DIR=... as well if that recorded path has moved. See src/python/README.md. +# Point find_package at the install with AXOM_DIR +# Conduit resolves transitively from axom's config, which records its Conduit prefix rm -rf dist or_die uv build --wheel \ - -C cmake.define.axom_DIR="${AXOM_INSTALL}/lib/cmake" \ + -C cmake.define.AXOM_DIR="${AXOM_INSTALL}/lib/cmake" \ --out-dir dist \ src/python ls -l dist +AXOM_WHEEL=$(find dist -maxdepth 1 -name 'axom-*.whl' -print -quit) +if [[ -z "${AXOM_WHEEL}" ]]; then + echo "ERROR: Axom wheel not found in dist/." + exit 1 +fi echo "~~~~~~ FRESH VENV + INSTALL THE WHEEL ~~~~~~" # Pin the interpreter that built the wheel, so the venv cannot pick a different one. @@ -100,31 +89,33 @@ VENV_DIR=/tmp/axom-wheel-venv rm -rf "${VENV_DIR}" or_die uv venv --python "$(command -v python3)" "${VENV_DIR}" VENV_PY="${VENV_DIR}/bin/python" -or_die uv pip install --python "${VENV_PY}" dist/*.whl +or_die uv pip install --python "${VENV_PY}" "${AXOM_WHEEL}[test]" -echo "~~~~~~ EXPOSE SAME-BUILD CONDUIT VIA .pth ~~~~~~" +echo "~~~~~~ VERIFY WHEEL-INSTALLED CONDUIT .pth ~~~~~~" PURELIB=$("${VENV_PY}" -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])') -echo "${CONDUIT_PY_DIR}" > "${PURELIB}/conduit.pth" -echo "wrote ${PURELIB}/conduit.pth -> ${CONDUIT_PY_DIR}" +CONDUIT_PTH="${PURELIB}/conduit.pth" +if [[ ! -f "${CONDUIT_PTH}" ]]; then + echo "ERROR: Expected wheel to install ${CONDUIT_PTH}." + echo " The wheel should expose the same-build Conduit python module without a manual PYTHONPATH update." + exit 1 +fi +CONDUIT_PY_DIR=$(sed -n '1p' "${CONDUIT_PTH}") +if [[ -z "${CONDUIT_PY_DIR}" || ! -d "${CONDUIT_PY_DIR}" ]]; then + echo "ERROR: ${CONDUIT_PTH} points to missing Conduit python module directory '${CONDUIT_PY_DIR}'." + exit 1 +fi +echo "verified ${CONDUIT_PTH} -> ${CONDUIT_PY_DIR}" -echo "~~~~~~ IMPORT SMOKE TEST (no wrapper, no PYTHONPATH) ~~~~~~" +echo "~~~~~~ IMPORT SMOKE TEST ~~~~~~" or_die "${VENV_PY}" -c \ "import axom, axom.sidre, conduit, numpy; print('axom', axom.__version__); print('axom.sidre', axom.sidre.__version__)" echo "~~~~~~ RUN THE SIDRE PYTHON SUITE VIA PLAIN pytest ~~~~~~" -# Axom's Python tests are named *_Py.py, which pytest's default python_files patterns -# (test_*.py, *_test.py) do not match -- an unqualified run collects nothing and exits 5. -# Name the pattern explicitly so collection is deterministic. -# The MPI-only spio test skips itself at module level when sidre was built without MPI. -# Several tests write output tmp files into the current directory, -# so run from a scratch directory -or_die uv pip install --python "${VENV_PY}" pytest +# Axom's Python tests are named *_Py.py, which pytest's default python_files patterns do not match TEST_DIR="$(pwd)/src/axom/sidre/tests" SCRATCH="$(mktemp -d)" -# Note: not a ( subshell ) -- or_die exits on failure, and from a subshell that would -# only exit the subshell and let the lane report success. -cd "${SCRATCH}" +pushd "${SCRATCH}" > /dev/null or_die "${VENV_PY}" -m pytest -s -p no:cacheprovider \ -o python_files='*_Py.py' \ "${TEST_DIR}" -cd - > /dev/null +popd > /dev/null From d1201a128802989d49e121b723d661dda6bc4ab5 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 22:49:45 -0700 Subject: [PATCH 18/35] Python: More consolidation of Python setup/installation docs --- .../sidre/docs/sphinx/python_interface.rst | 6 +- src/python/README.md | 140 +++--------------- 2 files changed, 23 insertions(+), 123 deletions(-) diff --git a/src/axom/sidre/docs/sphinx/python_interface.rst b/src/axom/sidre/docs/sphinx/python_interface.rst index f8157c79d7..225e4c5427 100644 --- a/src/axom/sidre/docs/sphinx/python_interface.rst +++ b/src/axom/sidre/docs/sphinx/python_interface.rst @@ -90,7 +90,7 @@ pip / uv wheel (thin, external Axom) ------------------------------------ The wheel compiles only the Sidre binding against an already-installed Axom. -It is tied to that Axom install, its Conduit install, and its host-config; +It is tied to that Axom install, its Conduit install, and its host-config; it is not a portable PyPI-style wheel. .. note:: @@ -154,8 +154,8 @@ The installed wheel also carries a CMake host-config for downstream projects: $ cmake -C "$(uv run axom-python-config --host-config)" -S /path/to/project -B build -For build details, including MPI compiler wrappers, editable installs, stable -ABI wheels, and site-specific wheelhouses, see ``src/python/README.md``. +For build details, including MPI compiler wrappers, editable installs, +and stable ABI wheels, see ``src/python/README.md``. Using Axom in Jupyter ^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/python/README.md b/src/python/README.md index 5a6f7b8c14..e3813dec15 100644 --- a/src/python/README.md +++ b/src/python/README.md @@ -13,7 +13,7 @@ It is consumed by two independent build paths that must produce the same on-disk 1. **The CMake build (in tree).** When Axom is configured with a component's Python bindings enabled (currently Sidre), the build stages this tree into the build directory and installs it into a `site-packages`-shaped prefix. - See `src/axom/sidre/CMakeLists.txt`: it copies the files below into `${PROJECT_BINARY_DIR}/python/` + See `src/axom/sidre/CMakeLists.txt`: it copies the files below into `${PROJECT_BINARY_DIR}/python/` (so the build tree is import-ready) and installs them under `AXOM_PYTHON_MODULE_INSTALL_PREFIX`. The compiled extension (`_sidre`) and its type stub are emitted into this layout by the build; they are not checked in. @@ -25,79 +25,26 @@ It is consumed by two independent build paths that must produce the same on-disk This file discusses contributor-facing concerns. Installing and using the bindings is documented in the Sidre user guide's "Python interface" page (`src/axom/sidre/docs/sphinx/python_interface.rst`). -## Two ways to get the Python interface +## Build paths at a glance -The two paths differ only in *who* compiles the `_sidre` extension and *how you import it*. -They compile: +Both build paths install the same package layout and should expose the same Python API: - the **same** binding translation unit (`src/axom/sidre/nanobind_sidre.cpp`) - under the **same** nanobind domain (`NB_DOMAIN axom`) -- and ship the **same** pure-Python tree from this directory, so `import axom.sidre` behaves identically either way. +- with the **same** pure-Python tree from this directory. -Both also stand on top of a fully built, installed Axom plus a matching Conduit -- -neither path builds Axom's C++ libraries or its third-party libraries. +They differ in where the Axom C++ libraries come from: -### Path A -- in-tree CMake build, imported via `PYTHONPATH` - -Enable the bindings in the same CMake build that compiles Axom -(a Python interpreter must be found; currently only Sidre is bound). -The `_sidre` extension is built next to `libaxom`/`libsidre`, staged into `/python/axom/sidre/`, -and installed under `AXOM_PYTHON_MODULE_INSTALL_PREFIX` (default `lib/python/site-packages`). -To use it, put that directory, plus Conduit's Python-module dir and numpy, on `PYTHONPATH`, -and you should be able to successfully `import axom.sidre` in a Python script. - -The build configures a convenience wrapper that assembles that environment from the spack prefixes, -so an ad hoc script "just works" without a venv: - -```bash -# runs the build's Python with axom + conduit + numpy (+ mpi4py) already on PYTHONPATH -/bin/run_python_with_axom.sh my_script.py -``` - -This is a natural path during Axom development since it doesn't require a separate packaging step, -and rebuilding Axom rebuilds the bindings in place. -The wrapper is bash-only and does not compose with Jupyter kernels, IDE runners, or debuggers; -for those, use the venv of Path B. - -### Path B -- thin pip/uv wheel, imported into a venv - -Build and install Axom first (the normal CMake/spack path, bindings enabled), -then build a **binding-only** wheel against that install and install it into a virtual environment: - -```bash -# Axom + Conduit already built and installed; compile just the bindings against them. -uv build --wheel -C cmake.define.AXOM_DIR="$AXOM_INSTALL/lib/cmake" src/python -uv pip install dist/axom-*.whl -``` - -The wheel's `CMakeLists.txt` runs `find_package(axom CONFIG REQUIRED)` and -compiles only `nanobind_sidre.cpp` -- it consumes the install, but does not rebuild it. - -Use this path for distributing or consuming the bindings in an ordinary Python environment. -It does not require modifying the `PYTHONPATH` or running through a wrapper, so it works with scripts, -IDEs, debuggers and Jupyter kernels. The user guide has the step-by-step instructions. -For details on building it, see the "Building the wheel: reference" section below. - -### What `uv` builds -- and what it does not - -`uv build` (through scikit-build-core) runs the wheel's `CMakeLists.txt`, -which compiles the single binding TU and links it against an already-installed -`axom::sidre` and `conduit::conduit_python`. - -It does **not** build: - -- **Axom's C++ libraries** -- supplied by the `find_package(axom)` install. -- **Third-party libraries** (Conduit, HDF5, RAJA, Umpire, MPI, ...) -- provisioned - by spack and not pip-installable in a way that would match the install. - -By design, `uv` does not generate the Axom libraries directly: -Axom and its TPLs come from the CMake/spack world, and `uv` adds only the thin binding layer on top. -Keeping the wheel thin makes it fast and reproducible against a known install. - -Conduit is deliberately not a Python dependency of the wheel. -The bindings must use the Conduit Python package from the same Conduit install Axom links, -not a separately built PyPI package. +- **In-tree CMake build.** Axom's normal CMake build compiles the C++ libraries, + builds `_sidre` in the same build tree, stages the package under `/python/`, + and installs it under `AXOM_PYTHON_MODULE_INSTALL_PREFIX`. +- **Thin pip/uv wheel.** The scikit-build-core project in this directory consumes + an already-installed Axom via `find_package(axom CONFIG REQUIRED)` and compiles + only the Python binding module against that install. +The wheel deliberately does not build Axom, Conduit, HDF5, RAJA, Umpire, MPI, or other TPLs. +Those come from the CMake/spack side. Conduit is also not listed as a Python dependency +because `axom.sidre` must import the Python module from the same Conduit build that Axom links. ## Layout @@ -134,7 +81,7 @@ A submodule is importable only when its component was enabled in the underlying generated artifacts (the `.so` and `.pyi` are produced by the build), and tests/examples (those live under the component, e.g. `src/axom/sidre/tests/*_Py.py`). -## Building the wheel: reference +## Wheel build reference The wheel compiles Axom's Python binding against an existing Axom install. It is specific to that install and host-config; it is not repaired with `auditwheel` @@ -189,19 +136,6 @@ uv build --wheel \ Build from the source tree that produced the install. The build compares the wheel metadata version with the installed Axom version and fails if they differ. -If Axom is already installed in a venv but `import conduit` fails, this is the -only manual step usually needed: - -```bash -CONDUIT_PYTHON_MODULE_DIR=/path/to/conduit/install/lib/pythonX.Y/site-packages -printf '%s\n' "$CONDUIT_PYTHON_MODULE_DIR" > \ - "$(uv run python -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')/conduit.pth" -uv run python -c "import axom.sidre, conduit; print(conduit.__file__)" -``` - -Use `CONDUIT_PYTHON_MODULE_DIR` from Conduit's CMake config. On current LC -installs it is usually `lib/pythonX.Y/site-packages`, not `python-modules`. - The wheel also installs development helpers: ```bash @@ -216,17 +150,6 @@ Conduit, compiler, MPI and Python settings used by the wheel: cmake -C "$(axom-python-config --host-config)" -S -B ``` -### Per-host-config wheelhouse - -Axom does not assume a central public wheelhouse. If a site, CI job, or team -publishes prebuilt Axom wheels, keep them separated by host-config because these -wheels are not portable across toolchains. Consume that site-provided directory -with `--find-links` (or a `[tool.uv.sources]` entry): - -```bash -uv pip install axom --find-links /path/to/site/wheelhouse/ -``` - ### Developer loop (editable, rebuild-on-import) nanobind's recommended editable flow rebuilds the extension automatically when @@ -268,36 +191,13 @@ not the toolchain coupling: an abi3 wheel is still specific to the host-config i Free-threaded (`abi3t`) wheels are not built today; scikit-build-core 1.0+ can emit those tags once the bindings and Conduit run under a free-threaded interpreter. -### MPI and test extras +### Package metadata and extras Wheel metadata is static, but whether the underlying Axom is an MPI build is a build-time choice, -so the wheel cannot force the MPI dependency at install time. -When installing from this source tree, put extras on the local project path and -keep the same CMake `-C` options used to build against the Axom install: - -```bash -uv pip install 'src/python[mpi]' \ - -C cmake.define.AXOM_DIR="$AXOM_INSTALL/lib/cmake" - -uv pip install 'src/python[test]' \ - -C cmake.define.AXOM_DIR="$AXOM_INSTALL/lib/cmake" - -uv pip install 'src/python[mpi,test]' \ - -C cmake.define.AXOM_DIR="$AXOM_INSTALL/lib/cmake" -``` - -Use `mpi` for `mpi4py` support (passing a communicator to `IOManager`, or initializing MPI) -and `test` for pytest. If Axom is already installed and you only need the optional dependency package, -installing `mpi4py` or `pytest` directly is also fine. - -For a prebuilt wheel from a site wheelhouse, put extras on the package name: - -```bash -uv pip install 'axom[mpi]' --find-links /path/to/site/wheelhouse/ -uv pip install 'axom[test]' --find-links /path/to/site/wheelhouse/ -``` - -pytest lives in the `test` extra, never in the runtime dependencies. +so the wheel cannot force MPI dependencies at install time. +The `mpi` extra declares `mpi4py`, and the `test` extra declares `pytest`. +Runtime dependencies intentionally stay minimal: `numpy` is required, +while Conduit's Python module is exposed by the generated `conduit.pth` file. ## Notes From 3cad60b48d02a8f932360cd9bc96218e5a39004b Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 23:11:08 -0700 Subject: [PATCH 19/35] Python: Clarifies error message and docs associate with stable API --- src/python/CMakeLists.txt | 29 ++++++++++++++++++++++------- src/python/README.md | 20 ++++++++++++-------- src/python/pyproject.toml | 5 +++-- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 1c530224b2..86a735a153 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -18,16 +18,31 @@ project(axom_python LANGUAGES C CXX) option(AXOM_PYTHON_STABLE_ABI "Build the extension against Python's stable ABI (abi3); needs Python >= 3.12" OFF) -find_package(Python 3.10 REQUIRED - COMPONENTS Interpreter Development.Module - OPTIONAL_COMPONENTS Development.SABIModule) +if(AXOM_PYTHON_STABLE_ABI AND CMAKE_VERSION VERSION_LESS 3.26) + message(FATAL_ERROR + "AXOM_PYTHON_STABLE_ABI=ON requires CMake >= 3.26 because " + "FindPython's Development.SABIModule component is needed " + "to locate Python stable ABI development artifacts. " + "Current CMake is ${CMAKE_VERSION}. " + "Build with CMake >= 3.26 or leave AXOM_PYTHON_STABLE_ABI off.") +endif() + +if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.26) + find_package(Python 3.10 REQUIRED + COMPONENTS Interpreter Development.Module + OPTIONAL_COMPONENTS Development.SABIModule) +else() + find_package(Python 3.10 REQUIRED + COMPONENTS Interpreter Development.Module) +endif() if(AXOM_PYTHON_STABLE_ABI AND NOT Python_Development.SABIModule_FOUND) message(FATAL_ERROR - "AXOM_PYTHON_STABLE_ABI=ON requires Python's Development.SABIModule " - "component (CPython >= 3.12), which was not found for " - "${Python_EXECUTABLE} (version ${Python_VERSION}). Build with a 3.12+ " - "interpreter or leave AXOM_PYTHON_STABLE_ABI off.") + "AXOM_PYTHON_STABLE_ABI=ON requires CMake >= 3.26 and Python's " + "Development.SABIModule component (CPython >= 3.12), " + "which was not found for ${Python_EXECUTABLE} (version ${Python_VERSION}). " + "Build with a 3.12+ interpreter that provides stable ABI development artifacts, " + "or leave AXOM_PYTHON_STABLE_ABI off.") endif() if(DEFINED AXOM_DIR AND NOT DEFINED axom_DIR) diff --git a/src/python/README.md b/src/python/README.md index e3813dec15..4f3e114205 100644 --- a/src/python/README.md +++ b/src/python/README.md @@ -172,8 +172,9 @@ and several tests write output files into the current directory, so we run them ### Stable ABI (abi3) By default the wheel is tagged for the exact CPython that built it. -Opt into a single abi3 wheel that serves every CPython >= 3.12 on the machine -by passing both flags together (the CMake option makes nanobind build the limited-API module; +With CMake >= 3.26 and Python >= 3.12, opt into a single abi3 wheel that serves +every CPython >= 3.12 on the machine by passing both flags together +(the CMake option makes nanobind build the limited-API module; the scikit-build-core setting sets the wheel tag, and the two must agree): ```bash @@ -184,12 +185,15 @@ uv build --wheel \ src/python ``` -Below Python 3.12 nanobind silently builds a non-stable module, so only enable this on a 3.12+ interpreter; -the build fails with an explicit message if the interpreter does not provide `Development.SABIModule`, -rather than quietly producing a mislabelled wheel. Stable ABI relaxes the Python-version coupling, -not the toolchain coupling: an abi3 wheel is still specific to the host-config it was built against. -Free-threaded (`abi3t`) wheels are not built today; scikit-build-core 1.0+ can emit those tags once the -bindings and Conduit run under a free-threaded interpreter. +Below Python 3.12 nanobind silently builds a non-stable module, +so only enable this on a 3.12+ interpreter. CMake's `FindPython` needs its +`Development.SABIModule` component for this path, which is available starting in CMake 3.26. +The build fails with an explicit message if either prerequisite is missing, +rather than quietly producing a mislabelled wheel. +Stable ABI relaxes the Python-version coupling, not the toolchain coupling: +an abi3 wheel is still specific to the host-config it was built against. +Free-threaded (`abi3t`) wheels are not built today; scikit-build-core 1.0+ +can emit those tags once the bindings and Conduit run under a free-threaded interpreter. ### Package metadata and extras diff --git a/src/python/pyproject.toml b/src/python/pyproject.toml index f3ea490d44..fd393d83f1 100644 --- a/src/python/pyproject.toml +++ b/src/python/pyproject.toml @@ -101,8 +101,9 @@ wheel.packages = ["src/axom"] # If that ever changes, vendor the TU into this tree during a pre-sdist step (or move the project root above the TU). # # STABLE_ABI / abi3 (cp312): opt-in, so the default build produces per-Python-version wheel tags. -# To build one abi3 wheel that serves every CPython >= 3.12 on the machine, pass BOTH of these at build time. -# They must agree, since the first makes nanobind build the limited-API module and the second sets the wheel tag: +# To build one abi3 wheel that serves every CPython >= 3.12 on the machine, use CMake >= 3.26 +# and Python >= 3.12, and pass BOTH of these at build time. They must agree, since the first +# makes nanobind build the limited-API module and the second sets the wheel tag: # uv build --wheel -C cmake.define.AXOM_PYTHON_STABLE_ABI=ON -C wheel.py-api=cp312 ... # # wheel.py-api is intentionally left unset here rather than hard-coded to cp312: From 8e46b465442a47ed462d3315eb9730c44629a4a4 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 23:39:35 -0700 Subject: [PATCH 20/35] Python: Improves logic for finding the conduit python module --- .../sidre/docs/sphinx/python_interface.rst | 2 +- src/cmake/axom-config.cmake.in | 4 ++ src/python/CMakeLists.txt | 46 ++++--------------- src/python/README.md | 5 +- 4 files changed, 17 insertions(+), 40 deletions(-) diff --git a/src/axom/sidre/docs/sphinx/python_interface.rst b/src/axom/sidre/docs/sphinx/python_interface.rst index 225e4c5427..8ba9352669 100644 --- a/src/axom/sidre/docs/sphinx/python_interface.rst +++ b/src/axom/sidre/docs/sphinx/python_interface.rst @@ -135,7 +135,7 @@ installing ``mpi4py`` or ``pytest`` directly is also fine. If ``axom.sidre`` is already installed in a venv but ``import conduit`` fails, add the same-build Conduit Python package with one ``.pth`` file. On current LC installs this path is usually ``$CONDUIT_INSTALL/lib/pythonX.Y/site-packages``; -the authoritative value is ``CONDUIT_PYTHON_MODULE_DIR`` in Conduit's CMake config. +the value recorded by an Axom install is ``AXOM_CONDUIT_PYTHON_MODULE_DIR`` in ``axom-config.cmake``. .. code-block:: bash diff --git a/src/cmake/axom-config.cmake.in b/src/cmake/axom-config.cmake.in index 66987f6603..64d547d8bb 100644 --- a/src/cmake/axom-config.cmake.in +++ b/src/cmake/axom-config.cmake.in @@ -151,9 +151,13 @@ if(NOT AXOM_FOUND) # conduit if(AXOM_USE_CONDUIT) set(AXOM_CONDUIT_DIR "@CONDUIT_DIR@") + set(AXOM_CONDUIT_PYTHON_MODULE_DIR "@CONDUIT_PYTHON_MODULE_DIR@") if(NOT CONDUIT_DIR) set(CONDUIT_DIR ${AXOM_CONDUIT_DIR}) endif() + if(NOT CONDUIT_PYTHON_MODULE_DIR AND AXOM_CONDUIT_PYTHON_MODULE_DIR) + set(CONDUIT_PYTHON_MODULE_DIR "${AXOM_CONDUIT_PYTHON_MODULE_DIR}") + endif() # Load mpi targets because we require the optional Conduit mpi targets if(AXOM_USE_MPI) diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 86a735a153..5f081afda0 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -52,50 +52,24 @@ endif() find_package(axom CONFIG REQUIRED) find_package(nanobind CONFIG REQUIRED) # supplied via build-system.requires -set(_axom_py_conduit_cmake_dir "") -if(DEFINED Conduit_DIR) - set(_axom_py_conduit_cmake_dir "${Conduit_DIR}") -elseif(DEFINED CONDUIT_INSTALL_PREFIX) - set(_axom_py_conduit_cmake_dir "${CONDUIT_INSTALL_PREFIX}/lib/cmake/conduit") -elseif(DEFINED AXOM_CONDUIT_DIR) +set(_axom_py_conduit_cmake_dir "${Conduit_DIR}") +if(NOT _axom_py_conduit_cmake_dir AND AXOM_CONDUIT_DIR) set(_axom_py_conduit_cmake_dir "${AXOM_CONDUIT_DIR}/lib/cmake/conduit") endif() -set(_axom_py_conduit_prefix "") -if(DEFINED CONDUIT_INSTALL_PREFIX) - set(_axom_py_conduit_prefix "${CONDUIT_INSTALL_PREFIX}") -elseif(DEFINED AXOM_CONDUIT_DIR) - set(_axom_py_conduit_prefix "${AXOM_CONDUIT_DIR}") -elseif(DEFINED CONDUIT_DIR) - set(_axom_py_conduit_prefix "${CONDUIT_DIR}") -endif() - -set(_axom_py_conduit_python_module_dir "") -if(DEFINED CONDUIT_PYTHON_MODULE_DIR) - if(IS_ABSOLUTE "${CONDUIT_PYTHON_MODULE_DIR}") - set(_axom_py_conduit_python_module_dir "${CONDUIT_PYTHON_MODULE_DIR}") - elseif(_axom_py_conduit_prefix) - set(_axom_py_conduit_python_module_dir - "${_axom_py_conduit_prefix}/${CONDUIT_PYTHON_MODULE_DIR}") - endif() -elseif(_axom_py_conduit_prefix) - foreach(_axom_py_conduit_python_module_dir_candidate - "${_axom_py_conduit_prefix}/lib/python${Python_VERSION_MAJOR}.${Python_VERSION_MINOR}/site-packages" - "${_axom_py_conduit_prefix}/python-modules") - if(EXISTS "${_axom_py_conduit_python_module_dir_candidate}/conduit") - set(_axom_py_conduit_python_module_dir - "${_axom_py_conduit_python_module_dir_candidate}") - break() - endif() - endforeach() +set(_axom_py_conduit_python_module_dir "${CONDUIT_PYTHON_MODULE_DIR}") +if(NOT _axom_py_conduit_python_module_dir AND AXOM_CONDUIT_PYTHON_MODULE_DIR) + set(_axom_py_conduit_python_module_dir "${AXOM_CONDUIT_PYTHON_MODULE_DIR}") endif() if(NOT _axom_py_conduit_python_module_dir OR NOT EXISTS "${_axom_py_conduit_python_module_dir}/conduit") message(FATAL_ERROR - "Could not determine the Conduit Python module directory for this " - "Axom install. Pass -C cmake.define.CONDUIT_PYTHON_MODULE_DIR= " - "where contains the same-build conduit Python package.") + "Could not find Conduit's Python package for this Axom install. " + "Expected AXOM_CONDUIT_PYTHON_MODULE_DIR or CONDUIT_PYTHON_MODULE_DIR " + "to name a directory containing the same-build conduit Python package. " + "Reconfigure Axom with CONDUIT_PYTHON_MODULE_DIR=, or pass " + "-C cmake.define.CONDUIT_PYTHON_MODULE_DIR= to this wheel build.") endif() # Keep the wheel metadata version aligned with the Axom install it links. diff --git a/src/python/README.md b/src/python/README.md index 4f3e114205..c96d8efc9e 100644 --- a/src/python/README.md +++ b/src/python/README.md @@ -99,7 +99,7 @@ The underlying CMake package variable is `axom_DIR`. Do not use `CMAKE_PREFIX_PATH` for `uv build` or `uv pip install` since scikit-build-core uses it internally for the isolated build environment. -Conduit is found through `axom-config.cmake` in the normal case. +Conduit and its Python package path are found through `axom-config.cmake` in the normal case. Add `Conduit_DIR` only if Axom's recorded Conduit package path no longer resolves: ```bash @@ -109,8 +109,7 @@ uv build --wheel \ src/python ``` -Add `CONDUIT_PYTHON_MODULE_DIR` only if Conduit's Python package path is not -recorded by Conduit's CMake config: +Add `CONDUIT_PYTHON_MODULE_DIR` only if Axom's recorded Conduit Python package path is missing or stale: ```bash uv build --wheel \ From fcaa2761360ecb1f508bf161b15f75d9876442a9 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Thu, 30 Jul 2026 12:20:41 -0700 Subject: [PATCH 21/35] Python: Resolve conduit python location to an absolute path --- src/cmake/thirdparty/SetupAxomThirdParty.cmake | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/cmake/thirdparty/SetupAxomThirdParty.cmake b/src/cmake/thirdparty/SetupAxomThirdParty.cmake index 6bc358c31f..9c6a4e304a 100644 --- a/src/cmake/thirdparty/SetupAxomThirdParty.cmake +++ b/src/cmake/thirdparty/SetupAxomThirdParty.cmake @@ -135,6 +135,20 @@ if (CONDUIT_DIR) set(CONDUIT_FOUND TRUE) blt_convert_to_system_includes(TARGET conduit::conduit) + + # Resolve CONDUIT_PYTHON_MODULE_DIR to an absolute path + # Preserve user-supplied cache values if present over the one from conduit's install + get_property(_axom_conduit_py_dir_cache + CACHE CONDUIT_PYTHON_MODULE_DIR PROPERTY VALUE) + if(_axom_conduit_py_dir_cache) + set(CONDUIT_PYTHON_MODULE_DIR "${_axom_conduit_py_dir_cache}") + endif() + unset(_axom_conduit_py_dir_cache) + + if(CONDUIT_PYTHON_MODULE_DIR AND NOT IS_ABSOLUTE "${CONDUIT_PYTHON_MODULE_DIR}") + get_filename_component(CONDUIT_PYTHON_MODULE_DIR + "${CONDUIT_DIR}/${CONDUIT_PYTHON_MODULE_DIR}" ABSOLUTE) + endif() else() message(STATUS "Conduit support is OFF") endif() From e78c00714b0bec52644fb2062b53f48c7a97f923 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Thu, 30 Jul 2026 12:35:24 -0700 Subject: [PATCH 22/35] Python: Fixes wheel lookup of Conduit Python path --- .../sidre/docs/sphinx/python_interface.rst | 11 ++++--- src/python/CMakeLists.txt | 30 ++++++++++++++----- src/python/README.md | 9 ++++-- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/axom/sidre/docs/sphinx/python_interface.rst b/src/axom/sidre/docs/sphinx/python_interface.rst index 8ba9352669..08d4a6b23f 100644 --- a/src/axom/sidre/docs/sphinx/python_interface.rst +++ b/src/axom/sidre/docs/sphinx/python_interface.rst @@ -133,15 +133,14 @@ If the Axom wheel is already installed and you only need the optional dependency installing ``mpi4py`` or ``pytest`` directly is also fine. If ``axom.sidre`` is already installed in a venv but ``import conduit`` fails, -add the same-build Conduit Python package with one ``.pth`` file. On current LC -installs this path is usually ``$CONDUIT_INSTALL/lib/pythonX.Y/site-packages``; -the value recorded by an Axom install is ``AXOM_CONDUIT_PYTHON_MODULE_DIR`` in ``axom-config.cmake``. +add the same-build Conduit Python package with one ``.pth`` file. +An Axom install records this path as ``AXOM_CONDUIT_PYTHON_MODULE_DIR`` in ``axom-config.cmake``: .. code-block:: bash - $ CONDUIT_PYTHON_MODULE_DIR=/path/to/conduit/install/lib/pythonX.Y/site-packages - $ printf '%s\n' "$CONDUIT_PYTHON_MODULE_DIR" > \ - "$(uv run python -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')/conduit.pth" + $ CONDUIT_PY_DIR=/path/to/conduit/install/python-modules + $ printf '%s\n' "$CONDUIT_PY_DIR" > \ + "$(uv run python -c 'import sysconfig; print(sysconfig.get_paths()["platlib"])')/axom-conduit.pth" $ uv run python -c "import axom.sidre, conduit; print(conduit.__file__)" If your site publishes a host-config-specific wheelhouse, install from the path diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 5f081afda0..14ecd1caa2 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -57,19 +57,35 @@ if(NOT _axom_py_conduit_cmake_dir AND AXOM_CONDUIT_DIR) set(_axom_py_conduit_cmake_dir "${AXOM_CONDUIT_DIR}/lib/cmake/conduit") endif() -set(_axom_py_conduit_python_module_dir "${CONDUIT_PYTHON_MODULE_DIR}") -if(NOT _axom_py_conduit_python_module_dir AND AXOM_CONDUIT_PYTHON_MODULE_DIR) +# Conduit's Python package directory -- needs to be absolute +set(AXOM_PYTHON_CONDUIT_MODULE_DIR "" CACHE PATH + "Directory holding the same-build conduit Python package; overrides the value recorded by the Axom install") + +if(AXOM_PYTHON_CONDUIT_MODULE_DIR) # use explicit override, if provided + set(_axom_py_conduit_python_module_dir "${AXOM_PYTHON_CONDUIT_MODULE_DIR}") +elseif(AXOM_CONDUIT_PYTHON_MODULE_DIR) # else, use value from Axom's export set(_axom_py_conduit_python_module_dir "${AXOM_CONDUIT_PYTHON_MODULE_DIR}") +else() # and fallback to conduit's path + set(_axom_py_conduit_python_module_dir "${CONDUIT_PYTHON_MODULE_DIR}") +endif() + +if(_axom_py_conduit_python_module_dir + AND NOT IS_ABSOLUTE "${_axom_py_conduit_python_module_dir}") + get_filename_component(_axom_py_conduit_python_module_dir + "${AXOM_CONDUIT_DIR}/${_axom_py_conduit_python_module_dir}" ABSOLUTE) endif() if(NOT _axom_py_conduit_python_module_dir OR NOT EXISTS "${_axom_py_conduit_python_module_dir}/conduit") message(FATAL_ERROR - "Could not find Conduit's Python package for this Axom install. " - "Expected AXOM_CONDUIT_PYTHON_MODULE_DIR or CONDUIT_PYTHON_MODULE_DIR " - "to name a directory containing the same-build conduit Python package. " - "Reconfigure Axom with CONDUIT_PYTHON_MODULE_DIR=, or pass " - "-C cmake.define.CONDUIT_PYTHON_MODULE_DIR= to this wheel build.") + "Could not find Conduit's Python package for this Axom install.\n" + " Looked for a directory containing a 'conduit' package at: " + "'${_axom_py_conduit_python_module_dir}'\n" + " AXOM_CONDUIT_DIR = ${AXOM_CONDUIT_DIR}\n" + " AXOM_CONDUIT_PYTHON_MODULE_DIR = ${AXOM_CONDUIT_PYTHON_MODULE_DIR}\n" + "Either rebuild Axom against a Conduit configured with Python support, or " + "pass the path explicitly:\n" + " -C cmake.define.AXOM_PYTHON_CONDUIT_MODULE_DIR=") endif() # Keep the wheel metadata version aligned with the Axom install it links. diff --git a/src/python/README.md b/src/python/README.md index c96d8efc9e..50cd1a1681 100644 --- a/src/python/README.md +++ b/src/python/README.md @@ -109,15 +109,20 @@ uv build --wheel \ src/python ``` -Add `CONDUIT_PYTHON_MODULE_DIR` only if Axom's recorded Conduit Python package path is missing or stale: +Add `AXOM_PYTHON_CONDUIT_MODULE_DIR` only if Axom's recorded Conduit Python package path is +missing or stale: ```bash uv build --wheel \ -C cmake.define.AXOM_DIR="$AXOM_INSTALL/lib/cmake" \ - -C cmake.define.CONDUIT_PYTHON_MODULE_DIR="$CONDUIT_INSTALL/lib/pythonX.Y/site-packages" \ + -C cmake.define.AXOM_PYTHON_CONDUIT_MODULE_DIR="$CONDUIT_INSTALL/python-modules" \ src/python ``` +Note the deliberately distinct name: `CONDUIT_PYTHON_MODULE_DIR` cannot be used here. +`find_package(axom)` pulls in `ConduitConfig.cmake`, which sets that variable with a plain +`set()` and so overwrites whatever the caller passed. + For MPI-enabled Axom installs, pass the same compiler and MPI wrapper family used by the Axom build. Copy these from the Axom build's `CMakeCache.txt` or host-config: From 2f3854749f244b1a4c1e83c0707261cc9baff801 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Thu, 30 Jul 2026 12:38:07 -0700 Subject: [PATCH 23/35] Python: Remove untested HIP workaround for uv bindings --- src/python/CMakeLists.txt | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 14ecd1caa2..ac681f04e9 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -141,16 +141,9 @@ install(FILES "${CMAKE_CURRENT_BINARY_DIR}/conduit.pth" DESTINATION ".") #------------------------------------------------------------------------------ -# HIP: replicate the in-tree special-case from src/axom/sidre/CMakeLists.txt. -# CMake treats MODULE libraries separately from executables, so HIP executable -# link flags are not applied to the module automatically. +# TODO: +# - Check that we can generate local bindings for HIP/CUDA #------------------------------------------------------------------------------ -if(AXOM_ENABLE_HIP) - set_source_files_properties(${_sidre_binding_sources} PROPERTIES LANGUAGE HIP) - string(REPLACE " " ";" _axom_py_module_link_flags "${CMAKE_EXE_LINKER_FLAGS}") - target_link_options(_sidre PRIVATE ${_axom_py_module_link_flags}) - unset(_axom_py_module_link_flags) -endif() #------------------------------------------------------------------------------ # nanobind_add_stub imports the module ('import _sidre') to introspect it. From 28fd116a69d854bd4c6b59c534cfe22f481ddf23 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Thu, 30 Jul 2026 12:54:38 -0700 Subject: [PATCH 24/35] Python: Clarify and fix env shell script Only export variables with content, and clarify the intent of the two variable groups. --- src/python/cmake/axom-python-env.sh.in | 55 +++++++++++++++++++------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/src/python/cmake/axom-python-env.sh.in b/src/python/cmake/axom-python-env.sh.in index bc854f059a..a8e9616b39 100644 --- a/src/python/cmake/axom-python-env.sh.in +++ b/src/python/cmake/axom-python-env.sh.in @@ -5,8 +5,22 @@ # SPDX-License-Identifier: (BSD-3-Clause) # Generated by Axom's Python wheel build. -# Source this bash file to expose paths useful for downstream CMake configuration: +# Source this bash file to expose the Axom install this wheel was built against: # . /axom/share/axom-python-env.sh +# +# Two groups of variables are set: +# +# * Variables honored by a subsequent `cmake` invocation. CMake's find_package() reads +# _DIR from the environment, and its compiler detection reads CC and CXX. +# +# * Informational variables that capture the build environment +# +# To reproduce Axom's full toolchain: +# +# cmake -C "$AXOM_PYTHON_HOST_CONFIG" -S -B +# +# Empty values are skipped rather than exported, so a setting this wheel has +# nothing to say about (e.g. the MPI wrappers of a non-MPI build) is left alone. _axom_python_env_script="${BASH_SOURCE[0]}" _axom_python_share_dir=$(CDPATH= cd -- "$(dirname -- "${_axom_python_env_script}")" && pwd) @@ -16,23 +30,36 @@ _axom_python_version_dir=$(CDPATH= cd -- "${_axom_python_site_packages_dir}/.." _axom_python_lib_dir=$(CDPATH= cd -- "${_axom_python_version_dir}/.." && pwd) _axom_python_prefix_dir=$(CDPATH= cd -- "${_axom_python_lib_dir}/.." && pwd) -export AXOM_PYTHON_HOST_CONFIG="${_axom_python_share_dir}/axom-python-host-config.cmake" -export axom_DIR="@axom_DIR@" -export AXOM_DIR="@axom_DIR@" -export AXOM_INSTALL_PREFIX="@AXOM_INSTALL_PREFIX@" -export Conduit_DIR="@_axom_py_conduit_cmake_dir@" -export CONDUIT_DIR="@AXOM_CONDUIT_DIR@" -export CONDUIT_PYTHON_MODULE_DIR="@_axom_py_conduit_python_module_dir@" -export CMAKE_C_COMPILER="@CMAKE_C_COMPILER@" -export CMAKE_CXX_COMPILER="@CMAKE_CXX_COMPILER@" -export MPI_C_COMPILER="@MPI_C_COMPILER@" -export MPI_CXX_COMPILER="@MPI_CXX_COMPILER@" +# export NAME=VALUE, skipping empty values +_axom_python_export() { + if [ -n "$2" ]; then + export "$1=$2" + fi +} + +# --- honored by cmake ------------------------------------------------------- +_axom_python_export AXOM_PYTHON_HOST_CONFIG "${_axom_python_share_dir}/axom-python-host-config.cmake" +_axom_python_export axom_DIR "@axom_DIR@" +_axom_python_export AXOM_DIR "@axom_DIR@" +_axom_python_export Conduit_DIR "@_axom_py_conduit_cmake_dir@" +_axom_python_export CC "@CMAKE_C_COMPILER@" +_axom_python_export CXX "@CMAKE_CXX_COMPILER@" + +# --- informational ---------------------------------------------------------- +_axom_python_export AXOM_INSTALL_PREFIX "@AXOM_INSTALL_PREFIX@" +_axom_python_export CONDUIT_DIR "@AXOM_CONDUIT_DIR@" +_axom_python_export CONDUIT_PYTHON_MODULE_DIR "@_axom_py_conduit_python_module_dir@" +_axom_python_export CMAKE_C_COMPILER "@CMAKE_C_COMPILER@" +_axom_python_export CMAKE_CXX_COMPILER "@CMAKE_CXX_COMPILER@" +_axom_python_export MPI_C_COMPILER "@MPI_C_COMPILER@" +_axom_python_export MPI_CXX_COMPILER "@MPI_CXX_COMPILER@" if [ -x "${_axom_python_prefix_dir}/bin/python" ]; then - export Python_EXECUTABLE="${_axom_python_prefix_dir}/bin/python" + _axom_python_export Python_EXECUTABLE "${_axom_python_prefix_dir}/bin/python" else - export Python_EXECUTABLE="@Python_EXECUTABLE@" + _axom_python_export Python_EXECUTABLE "@Python_EXECUTABLE@" fi +unset -f _axom_python_export unset _axom_python_share_dir unset _axom_python_env_script unset _axom_python_package_dir From f2f5efbd4ab102c1fe85a8d012b5ea38c28a0407 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Thu, 30 Jul 2026 13:34:58 -0700 Subject: [PATCH 25/35] Sidre/Python: Improves pinning logic for external buffers Do not pin pointers within blocks that are already pinned. --- src/axom/sidre/nanobind_sidre.cpp | 66 ++++++++++++++++- src/axom/sidre/tests/sidre_lifetime_Py.py | 86 +++++++++++++++++++++++ 2 files changed, 151 insertions(+), 1 deletion(-) diff --git a/src/axom/sidre/nanobind_sidre.cpp b/src/axom/sidre/nanobind_sidre.cpp index 0e8024097e..ad42722164 100644 --- a/src/axom/sidre/nanobind_sidre.cpp +++ b/src/axom/sidre/nanobind_sidre.cpp @@ -394,6 +394,49 @@ bool isOwnedByDataStoreBuffer(DataStore* ds, const void* ptr) return false; } +/*! + * \brief Return the pin already recorded in \a ds whose storage contains \a ptr, else nullptr. + * + * Used to redirect a pin away from an array that is merely a window onto storage + * this DataStore already pins. See the discussion on pinExternalDataOwner(). + * + * \note The scan is linear in the number of pins recorded for \a ds, alongside the + * Buffer scan in isOwnedByDataStoreBuffer(), and runs once per external-data pin. + * + * \note The returned pointer is into the registry's map and is invalidated by the + * next insertion, so callers must copy the ndarray before modifying the map. + */ +const nb::ndarray<>* findExistingPinOwning(DataStore* ds, const void* ptr) +{ + if(ds == nullptr || ptr == nullptr) + { + return nullptr; + } + + auto entry = externalDataOwnerRegistry().find(ds); + if(entry == externalDataOwnerRegistry().end()) + { + return nullptr; + } + + const auto p = reinterpret_cast(ptr); + for(const auto& pin : entry->second.pins) + { + const void* base_ptr = pin.second.data(); + if(base_ptr == nullptr) + { + continue; + } + const auto base = reinterpret_cast(base_ptr); + const auto bytes = static_cast(pin.second.nbytes()); + if(p >= base && p < base + bytes) + { + return &pin.second; + } + } + return nullptr; +} + /*! * \brief Record \a owner as the pin for \a view, scoped to its DataStore. * @@ -414,6 +457,15 @@ bool isOwnedByDataStoreBuffer(DataStore* ds, const void* ptr) * followed by `group.createView("name", data)`. Skipping the pin is safe because the * Buffer owns that storage; the dangling-pointer hazard the pin exists to prevent * only arises for storage owned by a Python object. + * + * \note The same cycle also arises one step removed, when the array is a window onto + * storage this DataStore already pins -- `arr = external_view.getDataArray()` followed + * by `group.createView("name", arr)`. Here the storage is *not* Sidre-owned, so a pin is + * genuinely needed, but `arr` is owned by the source View's Python wrapper and pinning it + * would retain the DataStore just as above. The pin is therefore redirected to the + * original owner recorded for that storage (see findExistingPinOwning()), which owns the + * memory and holds no Sidre reference. The redirect is per-DataStore, so aliasing another + * DataStore's external storage still pins the array as given. */ void pinExternalDataOwner(View* view, const nb::ndarray<>& owner) { @@ -435,6 +487,18 @@ void pinExternalDataOwner(View* view, const nb::ndarray<>& owner) return; } + // The array may be a window onto storage this DataStore already pins, e.g. + // `arr = external_view.getDataArray()` followed by `group.createView(name, arr)`. + // Such an array is owned by the source View's Python wrapper, so pinning it + // recreates the cycle described above. Pin the original owner instead: it is + // the object that actually owns the memory and it holds no Sidre reference. + // Copy it out before touching the map, which may rehash. + nb::ndarray<> pinned(owner); + if(const nb::ndarray<>* existing = findExistingPinOwning(ds, owner.data())) + { + pinned = nb::ndarray<>(*existing); + } + DataStoreExternalPins& entry = externalDataOwnerRegistry()[ds]; if(!entry.datastore_weakref.is_valid()) { @@ -452,7 +516,7 @@ void pinExternalDataOwner(View* view, const nb::ndarray<>& owner) } // Map assignment releases the previous ndarray wrapper if one was present. - entry.pins[view] = nb::ndarray<>(owner); + entry.pins[view] = pinned; } void releaseExternalDataOwner(View* view) diff --git a/src/axom/sidre/tests/sidre_lifetime_Py.py b/src/axom/sidre/tests/sidre_lifetime_Py.py index 8ad8e4b76d..03ff20320a 100644 --- a/src/axom/sidre/tests/sidre_lifetime_Py.py +++ b/src/axom/sidre/tests/sidre_lifetime_Py.py @@ -1013,6 +1013,92 @@ def test_external_view_onto_another_datastores_storage_is_still_pinned(): assert donor_ref() is None, "donor storage still pinned after the consuming view went away" +# --------------------------------------------------------------------------- +# Aliasing an already-pinned external view must not pin a sidre wrapper +# --------------------------------------------------------------------------- +# Storage behind an external view is owned by Python, not sidre, so a view onto +# it does need a pin -- but the array handed in may be +# `external_view.getDataArray()`, which is owned by that View's python wrapper. +# Pinning that array recreates the cycle the buffer exemption avoids +# (pin -> array -> View wrapper -> DataStore python object, whose collection is +# what releases the pin). The pin must be redirected to the original numpy owner. +def test_view_aliasing_a_pinned_external_view_does_not_retain_datastore(): + ds = sidre.DataStore() + root = ds.getRoot() + source_data = np.arange(8, dtype=np.float64) + 1.0 + external = root.createView("external", sidre.TypeID.FLOAT64_ID, 8, source_data) + + # Owned by `external`'s python wrapper, not by source_data. + aliased_data = external.getDataArray() + root.createView("aliased", sidre.TypeID.FLOAT64_ID, 8, aliased_data) + + ref = weakref.ref(ds) + del aliased_data, external, root, ds + _force_gc() + + assert ref() is None, "DataStore retained by a pin onto its own external view's storage" + + +def test_view_aliasing_a_pinned_external_view_still_pins_the_numpy_owner(): + # The redirect must not drop the pin. Destroy the source view so *its* pin is + # gone, leaving the aliasing view's redirected pin as the only thing keeping + # the numpy storage alive. A fix that simply skipped the pin (the way the + # sidre-owned case does) would let the array be collected here and leave the + # aliasing view pointing at freed memory. + ds = sidre.DataStore() + root = ds.getRoot() + source_data = np.arange(8, dtype=np.float64) + 1.0 + external = root.createView("external", sidre.TypeID.FLOAT64_ID, 8, source_data) + + aliased = root.createView("aliased", sidre.TypeID.FLOAT64_ID, 8, external.getDataArray()) + + array_ref = weakref.ref(source_data) + del external + root.destroyView("external") # releases the source view's own pin + del source_data + _force_gc() + + assert array_ref() is not None, "numpy owner was not pinned by the aliasing view" + assert aliased.getDataArray()[0] == 1.0 + assert aliased.getDataArray()[7] == 8.0 + + # Drop everything before returning: leaving a live DataStore (and its pin) + # in this frame perturbs later tests in this module, which assert on + # collection of their own DataStores. + del aliased, root, ds + _force_gc() + + +def test_setExternalData_aliasing_a_pinned_external_view_does_not_retain_datastore(): + # Same redirect, reached through setExternalData rather than createView. + # Structured like the createView case above so it discriminates the redirect + # from a fix that merely skips the pin: the source view is destroyed, so the + # redirected pin is the only remaining reference to the numpy storage. + ds = sidre.DataStore() + root = ds.getRoot() + source_data = np.arange(8, dtype=np.float64) + 1.0 + external = root.createView("external", sidre.TypeID.FLOAT64_ID, 8, source_data) + + target = root.createView("target") + target.setExternalData(sidre.TypeID.FLOAT64_ID, 8, external.getDataArray()) + + array_ref = weakref.ref(source_data) + del external + root.destroyView("external") # releases the source view's own pin + del source_data + _force_gc() + + assert array_ref() is not None, "numpy owner was not pinned by the aliasing view" + assert target.getDataArray()[0] == 1.0 + assert target.getDataArray()[7] == 8.0 + + ref = weakref.ref(ds) + del target, root, ds + _force_gc() + + assert ref() is None, "DataStore retained by a setExternalData pin onto its own external storage" + + if __name__ == "__main__": import sys From 5379dfabbc32f97907e92bbb777cb57672a94cd1 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Thu, 30 Jul 2026 13:46:51 -0700 Subject: [PATCH 26/35] Sidre/Python: Adds tests and clarifying comment about repurcussions of noconvert() --- src/axom/sidre/nanobind_sidre.cpp | 9 +++- src/axom/sidre/tests/sidre_attribute_Py.py | 60 ++++++++++++++++++++++ src/axom/sidre/tests/sidre_lifetime_Py.py | 3 +- 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/src/axom/sidre/nanobind_sidre.cpp b/src/axom/sidre/nanobind_sidre.cpp index ad42722164..ee30f8ec0a 100644 --- a/src/axom/sidre/nanobind_sidre.cpp +++ b/src/axom/sidre/nanobind_sidre.cpp @@ -1328,7 +1328,14 @@ NB_MODULE(_sidre, m_sidre) nb::arg("attr").none(), "Set Attribute (by pointer) to its default value") - // Scalar setters for int and python float (C++ double) + // Scalar setters for int and python float (C++ double). + // + // NOTE: the value argument is bound with .noconvert(), so nanobind skips its + // converting overload pass and only an exact python int or float is accepted. + // This is deliberate. With conversion enabled, nanobind tries the overloads in declaration order, + // so a numpy float binds to the int overload and the value is silently truncated + // (e.g. np.float32(3.5) stored as 3). Rejecting the call is better than storing the wrong number. + // Callers holding a numpy scalar convert at the call site, e.g. int(x), float(x) or x.item(). .def( "setAttributeScalar", [](View& self, IndexType idx, int value) { return self.setAttributeScalar(idx, value); }, diff --git a/src/axom/sidre/tests/sidre_attribute_Py.py b/src/axom/sidre/tests/sidre_attribute_Py.py index ac92fc0fd5..083d0af1bd 100644 --- a/src/axom/sidre/tests/sidre_attribute_Py.py +++ b/src/axom/sidre/tests/sidre_attribute_Py.py @@ -6,6 +6,7 @@ import axom.sidre as sidre import numpy as np +import pytest import conduit # Global attribute values, used by multiple tests @@ -945,3 +946,62 @@ def test_save_load_group_with_attributes_same_ds(): assert gr.getView("scalar2").getAttributeString(g_name_color) == g_color_red assert gr.getView("scalar3").hasAttributeValue(g_name_color) assert gr.getView("scalar3").getAttributeString(g_name_color) == g_color_blue + + +# --------------------------------------------------------------------------- +# Scalar setters require an exact python int or float +# --------------------------------------------------------------------------- +# The int and float overloads of the scalar setters are bound with nb::arg("value").noconvert(), +# so nanobind skips its converting overload pass, so numpy floats don't silently get bound +# to the int and truncated. As a consequence, numpy scalars must be converted by the caller, +# e.g. float(x) or x.item(). +def test_setAttributeScalar_requires_exact_python_scalar_types(): + ds = sidre.DataStore() + ds.createAttributeScalar(g_name_dump, g_dump_no) + view = ds.getRoot().createViewScalar("scalar", 0) + + # Exact python types are accepted. + assert view.setAttributeScalar(g_name_dump, 1) + assert view.getAttributeScalarInt(g_name_dump) == 1 + + # numpy scalars, 0-d arrays, bool and str are rejected rather than converted. + for rejected in (np.int32(1), np.int64(1), np.float32(1.0), np.float64(1.0), np.array(1), True, + "1"): + with pytest.raises(TypeError): + view.setAttributeScalar(g_name_dump, rejected) + + # The value is unchanged by the rejected calls. + assert view.getAttributeScalarInt(g_name_dump) == 1 + + # The documented conversion at the call site works. + assert view.setAttributeScalar(g_name_dump, int(np.int64(7))) + assert view.getAttributeScalarInt(g_name_dump) == 7 + + +def test_noconvert_prevents_silent_float_to_int_truncation(): + # This is what the noconvert annotations buy. With conversion enabled, + # np.float32(3.5) binds to the int overload and stores 3. + ds = sidre.DataStore() + ds.createAttributeScalar(g_name_size, g_size_small) + view = ds.getRoot().createViewScalar("scalar", 0) + + with pytest.raises(TypeError): + view.setAttributeScalar(g_name_size, np.float32(3.5)) + + # Converting explicitly keeps the fractional part. + assert view.setAttributeScalar(g_name_size, float(np.float32(3.5))) + assert view.getAttributeScalarFloat(g_name_size) == 3.5 + + +def test_setScalar_requires_exact_python_scalar_types(): + # The same contract on View.setScalar, which has carried noconvert since + # before the attribute setters did. + ds = sidre.DataStore() + view = ds.getRoot().createViewScalar("scalar", 0) + + assert view.setScalar(5) is not None + assert view.getDataInt() == 5 + + for rejected in (np.int64(5), np.float64(5.0), np.array(5), True): + with pytest.raises(TypeError): + view.setScalar(rejected) diff --git a/src/axom/sidre/tests/sidre_lifetime_Py.py b/src/axom/sidre/tests/sidre_lifetime_Py.py index 03ff20320a..6b7417a602 100644 --- a/src/axom/sidre/tests/sidre_lifetime_Py.py +++ b/src/axom/sidre/tests/sidre_lifetime_Py.py @@ -1096,7 +1096,8 @@ def test_setExternalData_aliasing_a_pinned_external_view_does_not_retain_datasto del target, root, ds _force_gc() - assert ref() is None, "DataStore retained by a setExternalData pin onto its own external storage" + assert ref() is None, ( + "DataStore retained by a setExternalData pin onto its own external storage") if __name__ == "__main__": From a6c69a8bbebfe757344eb447a4c973534f148fc8 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Thu, 30 Jul 2026 13:53:00 -0700 Subject: [PATCH 27/35] Python: Handle differences between two python installation paths Allow for the possibility of some generated config files not being present and return a proper error message. --- src/axom/sidre/CMakeLists.txt | 3 ++ src/python/README.md | 6 ++++ src/python/src/axom/config.py | 67 +++++++++++++++++++++++++++-------- 3 files changed, 62 insertions(+), 14 deletions(-) diff --git a/src/axom/sidre/CMakeLists.txt b/src/axom/sidre/CMakeLists.txt index 2091cb2fdd..360d78ddb0 100644 --- a/src/axom/sidre/CMakeLists.txt +++ b/src/axom/sidre/CMakeLists.txt @@ -208,6 +208,8 @@ if(NANOBIND_FOUND) "${_axom_py_build_root}/axom/__init__.py" COPYONLY) axom_configure_file("${_axom_py_pkg_src}/axom/py.typed" "${_axom_py_build_root}/axom/py.typed" COPYONLY) + axom_configure_file("${_axom_py_pkg_src}/axom/config.py" + "${_axom_py_build_root}/axom/config.py" COPYONLY) axom_configure_file("${_axom_py_pkg_src}/axom/sidre/__init__.py" "${_axom_py_build_root}/axom/sidre/__init__.py" COPYONLY) # Hand-written package stub: re-exports the generated _sidre.pyi statically @@ -249,6 +251,7 @@ if(NANOBIND_FOUND) # Namespace-root package files install once (not per component). install(FILES "${_axom_py_pkg_src}/axom/__init__.py" "${_axom_py_pkg_src}/axom/py.typed" + "${_axom_py_pkg_src}/axom/config.py" DESTINATION "${AXOM_PYTHON_MODULE_INSTALL_PREFIX}/axom") endif() diff --git a/src/python/README.md b/src/python/README.md index 50cd1a1681..3a2530943f 100644 --- a/src/python/README.md +++ b/src/python/README.md @@ -58,6 +58,7 @@ src/python/ src/ axom/ <- the 'axom' regular package __init__.py <- top-level package metadata + config.py <- locates the wheel-generated helpers (axom-python-config) py.typed <- PEP 561 marker (typed package) sidre/ __init__.py <- re-exports the compiled 'axom.sidre._sidre' @@ -68,6 +69,11 @@ src/python/ Parenthesized entries are build products and are intentionally not in the repository. +Both build paths install this tree. Only the wheel build additionally generates +`axom/share/axom-python-host-config.cmake` and `axom/share/axom-python-env.sh`; +on a CMake installation `axom.config.has_wheel_config()` returns `False` and the +path accessors raise `FileNotFoundError` with that explanation. + Each bound Axom component installs as a submodule of the `axom` package (`axom.sidre`, and later `axom.quest`, `axom.primal`, ...). A submodule is importable only when its component was enabled in the underlying Axom build. diff --git a/src/python/src/axom/config.py b/src/python/src/axom/config.py index 4741cb176b..588e58a047 100644 --- a/src/python/src/axom/config.py +++ b/src/python/src/axom/config.py @@ -4,28 +4,63 @@ # # SPDX-License-Identifier: (BSD-3-Clause) -"""Helpers for locating Axom Python wheel configuration files.""" +"""Helpers for locating Axom Python wheel configuration files. + +The files these helpers point at (``axom-python-host-config.cmake`` and ``axom-python-env.sh``) +are generated by the pip/uv wheel build in ``src/python``. +Axom's in-tree CMake build installs the same ``axom`` package but does not generate them, +so on such an installation the accessors below raise :class:`FileNotFoundError` +rather than returning a path that does not exist. +Use :func:`has_wheel_config` to test without raising. +""" from __future__ import annotations import argparse +import sys from importlib import resources from pathlib import Path +_MISSING_HINT = ( + "This is expected for an Axom installed by its CMake build: the host-config " + "and environment script are generated only by the pip/uv wheel build in " + "src/python. Reinstall with `pip install /src/python` to get them." +) + def share_dir() -> Path: - """Return the installed Axom Python package share directory.""" + """Return the installed Axom Python package share directory. + + The directory is not required to exist; see :func:`has_wheel_config`. + """ return Path(resources.files("axom").joinpath("share")) +def has_wheel_config() -> bool: + """Return True when the wheel-generated configuration files are present.""" + return (share_dir() / "axom-python-host-config.cmake").is_file() + + +def _require(path: Path, what: str) -> Path: + if not path.is_file(): + raise FileNotFoundError(f"{what} not found at {path}. {_MISSING_HINT}") + return path + + def host_config_path() -> Path: - """Return the CMake host-config generated for this Axom Python wheel.""" - return share_dir() / "axom-python-host-config.cmake" + """Return the CMake host-config generated for this Axom Python wheel. + + :raises FileNotFoundError: if this installation carries no generated host-config. + """ + return _require(share_dir() / "axom-python-host-config.cmake", "Axom Python host-config") def env_script_path() -> Path: - """Return the shell environment helper generated for this Axom Python wheel.""" - return share_dir() / "axom-python-env.sh" + """Return the shell environment helper generated for this Axom Python wheel. + + :raises FileNotFoundError: if this installation carries no generated env script. + """ + return _require(share_dir() / "axom-python-env.sh", "Axom Python environment script") def main(argv: list[str] | None = None) -> int: @@ -37,14 +72,18 @@ def main(argv: list[str] | None = None) -> int: group.add_argument("--cmake-args", action="store_true", help="print CMake arguments using the host-config") args = parser.parse_args(argv) - if args.env_script: - print(env_script_path()) - elif args.share_dir: - print(share_dir()) - elif args.cmake_args: - print(f"-C {host_config_path()}") - else: - print(host_config_path()) + try: + if args.env_script: + print(env_script_path()) + elif args.share_dir: + print(share_dir()) + elif args.cmake_args: + print(f"-C {host_config_path()}") + else: + print(host_config_path()) + except FileNotFoundError as err: + print(err, file=sys.stderr) + return 1 return 0 From 4516535c57b5fb2bc384843419e55cbbf18d37c3 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Thu, 30 Jul 2026 14:25:12 -0700 Subject: [PATCH 28/35] Python: Use `set -e` and `set -x` instead of or_die in wheels CI script Misc: Slight fixes to docs. --- .../github-actions/linux-wheel_and_test.sh | 34 ++++++++----------- src/python/README.md | 18 ++++------ .../cmake/axom-python-host-config.cmake.in | 4 +++ 3 files changed, 24 insertions(+), 32 deletions(-) diff --git a/scripts/github-actions/linux-wheel_and_test.sh b/scripts/github-actions/linux-wheel_and_test.sh index 99f7a5c7f8..0e6e6df3be 100755 --- a/scripts/github-actions/linux-wheel_and_test.sh +++ b/scripts/github-actions/linux-wheel_and_test.sh @@ -18,17 +18,11 @@ # # Intended for the gcc docker image, which is nanobind-enabled. +# Fail on the first error, including inside pipelines, and trace every command so +# a CI failure is readable from the log alone. set -e set -o pipefail - -function or_die () { - "$@" - local status=$? - if [[ $status != 0 ]]; then - echo "ERROR $status command: $*" - exit $status - fi -} +set -x HOST_CONFIG="${HOST_CONFIG:-gcc@13.3.1.cmake}" BUILD_TYPE="${BUILD_TYPE:-Debug}" @@ -44,12 +38,12 @@ echo "~~~~~~~~~~~~~~~~~~~~~~" NUM_BUILD_PROCS=$(python3 -c 'import os; print(max(2, os.cpu_count() * 8 // 10))') echo "~~~~~~ CONFIGURE + BUILD + INSTALL AXOM (+python) ~~~~~~" -or_die python3 ./config-build.py \ +python3 ./config-build.py \ -bp "${BUILD_DIR}" \ -hc "./host-configs/docker/${HOST_CONFIG}" \ -bt "${BUILD_TYPE}" -or_die cmake --build "${BUILD_DIR}" -j "${NUM_BUILD_PROCS}" -or_die cmake --install "${BUILD_DIR}" +cmake --build "${BUILD_DIR}" -j "${NUM_BUILD_PROCS}" +cmake --install "${BUILD_DIR}" # Resolve the Axom install prefix from the CMake cache CACHE="${BUILD_DIR}/CMakeCache.txt" @@ -63,7 +57,7 @@ fi echo "~~~~~~ ENSURE uv IS AVAILABLE ~~~~~~" if ! command -v uv >/dev/null 2>&1; then - or_die python3 -m pip install --user uv + python3 -m pip install --user uv export PATH="${HOME}/.local/bin:${PATH}" fi uv --version @@ -72,7 +66,7 @@ echo "~~~~~~ BUILD THE THIN WHEEL FROM src/python ~~~~~~" # Point find_package at the install with AXOM_DIR # Conduit resolves transitively from axom's config, which records its Conduit prefix rm -rf dist -or_die uv build --wheel \ +uv build --wheel \ -C cmake.define.AXOM_DIR="${AXOM_INSTALL}/lib/cmake" \ --out-dir dist \ src/python @@ -87,13 +81,13 @@ echo "~~~~~~ FRESH VENV + INSTALL THE WHEEL ~~~~~~" # Pin the interpreter that built the wheel, so the venv cannot pick a different one. VENV_DIR=/tmp/axom-wheel-venv rm -rf "${VENV_DIR}" -or_die uv venv --python "$(command -v python3)" "${VENV_DIR}" +uv venv --python "$(command -v python3)" "${VENV_DIR}" VENV_PY="${VENV_DIR}/bin/python" -or_die uv pip install --python "${VENV_PY}" "${AXOM_WHEEL}[test]" +uv pip install --python "${VENV_PY}" "${AXOM_WHEEL}[test]" echo "~~~~~~ VERIFY WHEEL-INSTALLED CONDUIT .pth ~~~~~~" -PURELIB=$("${VENV_PY}" -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])') -CONDUIT_PTH="${PURELIB}/conduit.pth" +PLATLIB=$("${VENV_PY}" -c 'import sysconfig; print(sysconfig.get_paths()["platlib"])') +CONDUIT_PTH="${PLATLIB}/conduit.pth" if [[ ! -f "${CONDUIT_PTH}" ]]; then echo "ERROR: Expected wheel to install ${CONDUIT_PTH}." echo " The wheel should expose the same-build Conduit python module without a manual PYTHONPATH update." @@ -107,7 +101,7 @@ fi echo "verified ${CONDUIT_PTH} -> ${CONDUIT_PY_DIR}" echo "~~~~~~ IMPORT SMOKE TEST ~~~~~~" -or_die "${VENV_PY}" -c \ +"${VENV_PY}" -c \ "import axom, axom.sidre, conduit, numpy; print('axom', axom.__version__); print('axom.sidre', axom.sidre.__version__)" echo "~~~~~~ RUN THE SIDRE PYTHON SUITE VIA PLAIN pytest ~~~~~~" @@ -115,7 +109,7 @@ echo "~~~~~~ RUN THE SIDRE PYTHON SUITE VIA PLAIN pytest ~~~~~~" TEST_DIR="$(pwd)/src/axom/sidre/tests" SCRATCH="$(mktemp -d)" pushd "${SCRATCH}" > /dev/null -or_die "${VENV_PY}" -m pytest -s -p no:cacheprovider \ +"${VENV_PY}" -m pytest -s -p no:cacheprovider \ -o python_files='*_Py.py' \ "${TEST_DIR}" popd > /dev/null diff --git a/src/python/README.md b/src/python/README.md index 3a2530943f..73d147bd03 100644 --- a/src/python/README.md +++ b/src/python/README.md @@ -146,19 +146,18 @@ uv build --wheel \ Build from the source tree that produced the install. The build compares the wheel metadata version with the installed Axom version and fails if they differ. -The wheel also installs development helpers: +The wheel also installs development helpers, which report their own paths: ```bash axom-python-config --host-config # path to axom/share/axom-python-host-config.cmake axom-python-config --env-script # path to axom/share/axom-python-env.sh ``` -Use the host-config to seed downstream CMake projects with the same Axom, -Conduit, compiler, MPI and Python settings used by the wheel: - -```bash -cmake -C "$(axom-python-config --host-config)" -S -B -``` +The host-config seeds a downstream CMake project with the same Axom, Conduit, +compiler, MPI and Python settings the wheel used; the env script exports the +subset of those that CMake reads from the environment. Both are generated only +by this wheel build, not by the in-tree CMake install. See the "pip / uv wheel" +section of the Sidre user guide for the usage examples. ### Developer loop (editable, rebuild-on-import) @@ -212,8 +211,3 @@ so the wheel cannot force MPI dependencies at install time. The `mpi` extra declares `mpi4py`, and the `test` extra declares `pytest`. Runtime dependencies intentionally stay minimal: `numpy` is required, while Conduit's Python module is exposed by the generated `conduit.pth` file. - -## Notes - -- This directory doubles as the root of the pip/uv wheel project (`pyproject.toml` + `CMakeLists.txt`), - which reuses these files. diff --git a/src/python/cmake/axom-python-host-config.cmake.in b/src/python/cmake/axom-python-host-config.cmake.in index f5a2845beb..8b71c89b1a 100644 --- a/src/python/cmake/axom-python-host-config.cmake.in +++ b/src/python/cmake/axom-python-host-config.cmake.in @@ -26,6 +26,10 @@ set(AXOM_INSTALL_PREFIX "@AXOM_INSTALL_PREFIX@" CACHE PATH "Axom install prefix" set(Conduit_DIR "@_axom_py_conduit_cmake_dir@" CACHE PATH "Conduit CMake package directory") set(CONDUIT_DIR "@AXOM_CONDUIT_DIR@" CACHE PATH "Conduit install prefix") +# NOTE: after find_package(axom), ConduitConfig.cmake may set the normal +# CONDUIT_PYTHON_MODULE_DIR variable to a path relative to Conduit's install root, +# shadowing this cache entry. Use AXOM_CONDUIT_PYTHON_MODULE_DIR instead +# since axom-config.cmake records it as an absolute path. set(CONDUIT_PYTHON_MODULE_DIR "@_axom_py_conduit_python_module_dir@" CACHE PATH "Conduit Python module directory") if(EXISTS "@CMAKE_C_COMPILER@") From 8ad35f049c3ef1ccebda56b5c7b6d29cf7ac5bfb Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Thu, 30 Jul 2026 14:47:42 -0700 Subject: [PATCH 29/35] Python: Ensure our Python gets MPI when Axom is configured with MPI --- .../github-actions/linux-wheel_and_test.sh | 59 +++++++++++++++++-- src/python/README.md | 5 +- src/python/cmake/axom-python-env.sh.in | 8 ++- .../cmake/axom-python-host-config.cmake.in | 15 +++-- 4 files changed, 73 insertions(+), 14 deletions(-) diff --git a/scripts/github-actions/linux-wheel_and_test.sh b/scripts/github-actions/linux-wheel_and_test.sh index 0e6e6df3be..2226d7263d 100755 --- a/scripts/github-actions/linux-wheel_and_test.sh +++ b/scripts/github-actions/linux-wheel_and_test.sh @@ -55,6 +55,40 @@ if [[ -z "${AXOM_INSTALL}" || ! -d "${AXOM_INSTALL}" ]]; then exit 1 fi +cache_value() { + awk -F= -v key="$1" '$1 ~ "^" key ":[^=]*$" {print $2; exit}' "${CACHE}" +} + +cache_bool_is_on() { + local value + value=$(cache_value "$1") + case "${value^^}" in + ON|TRUE|YES|1) + return 0 + ;; + *) + return 1 + ;; + esac +} + +require_cmake_define_from_cache() { + local name="$1" + local value + value=$(cache_value "${name}") + if [[ -z "${value}" ]]; then + echo "ERROR: Required CMake cache entry ${name} was not found in ${CACHE}." + exit 1 + fi + WHEEL_BUILD_ARGS+=("-C" "cmake.define.${name}=${value}") +} + +AXOM_WHEEL_ENABLE_MPI=OFF +if cache_bool_is_on ENABLE_MPI || cache_bool_is_on AXOM_ENABLE_MPI; then + AXOM_WHEEL_ENABLE_MPI=ON +fi +echo "AXOM_WHEEL_ENABLE_MPI=${AXOM_WHEEL_ENABLE_MPI}" + echo "~~~~~~ ENSURE uv IS AVAILABLE ~~~~~~" if ! command -v uv >/dev/null 2>&1; then python3 -m pip install --user uv @@ -66,10 +100,16 @@ echo "~~~~~~ BUILD THE THIN WHEEL FROM src/python ~~~~~~" # Point find_package at the install with AXOM_DIR # Conduit resolves transitively from axom's config, which records its Conduit prefix rm -rf dist -uv build --wheel \ - -C cmake.define.AXOM_DIR="${AXOM_INSTALL}/lib/cmake" \ - --out-dir dist \ - src/python +WHEEL_BUILD_ARGS=( + "-C" "cmake.define.AXOM_DIR=${AXOM_INSTALL}/lib/cmake" +) +require_cmake_define_from_cache CMAKE_C_COMPILER +require_cmake_define_from_cache CMAKE_CXX_COMPILER +if [[ "${AXOM_WHEEL_ENABLE_MPI}" == "ON" ]]; then + require_cmake_define_from_cache MPI_C_COMPILER + require_cmake_define_from_cache MPI_CXX_COMPILER +fi +uv build --wheel "${WHEEL_BUILD_ARGS[@]}" --out-dir dist src/python ls -l dist AXOM_WHEEL=$(find dist -maxdepth 1 -name 'axom-*.whl' -print -quit) if [[ -z "${AXOM_WHEEL}" ]]; then @@ -83,7 +123,11 @@ VENV_DIR=/tmp/axom-wheel-venv rm -rf "${VENV_DIR}" uv venv --python "$(command -v python3)" "${VENV_DIR}" VENV_PY="${VENV_DIR}/bin/python" -uv pip install --python "${VENV_PY}" "${AXOM_WHEEL}[test]" +AXOM_WHEEL_EXTRAS="test" +if [[ "${AXOM_WHEEL_ENABLE_MPI}" == "ON" ]]; then + AXOM_WHEEL_EXTRAS="test,mpi" +fi +uv pip install --python "${VENV_PY}" "${AXOM_WHEEL}[${AXOM_WHEEL_EXTRAS}]" echo "~~~~~~ VERIFY WHEEL-INSTALLED CONDUIT .pth ~~~~~~" PLATLIB=$("${VENV_PY}" -c 'import sysconfig; print(sysconfig.get_paths()["platlib"])') @@ -103,6 +147,11 @@ echo "verified ${CONDUIT_PTH} -> ${CONDUIT_PY_DIR}" echo "~~~~~~ IMPORT SMOKE TEST ~~~~~~" "${VENV_PY}" -c \ "import axom, axom.sidre, conduit, numpy; print('axom', axom.__version__); print('axom.sidre', axom.sidre.__version__)" +if [[ "${AXOM_WHEEL_ENABLE_MPI}" == "ON" ]]; then + "${VENV_PY}" -c "import mpi4py, axom.sidre as sidre; assert sidre.AXOM_ENABLE_MPI" +else + "${VENV_PY}" -c "import axom.sidre as sidre; assert not sidre.AXOM_ENABLE_MPI" +fi echo "~~~~~~ RUN THE SIDRE PYTHON SUITE VIA PLAIN pytest ~~~~~~" # Axom's Python tests are named *_Py.py, which pytest's default python_files patterns do not match diff --git a/src/python/README.md b/src/python/README.md index 73d147bd03..41da13ab7d 100644 --- a/src/python/README.md +++ b/src/python/README.md @@ -154,8 +154,8 @@ axom-python-config --env-script # path to axom/share/axom-python-env.sh ``` The host-config seeds a downstream CMake project with the same Axom, Conduit, -compiler, MPI and Python settings the wheel used; the env script exports the -subset of those that CMake reads from the environment. Both are generated only +compiler, `ENABLE_MPI`, MPI wrapper and Python settings the wheel used; the env script +exports the subset of those that CMake reads from the environment. Both are generated only by this wheel build, not by the in-tree CMake install. See the "pip / uv wheel" section of the Sidre user guide for the usage examples. @@ -211,3 +211,4 @@ so the wheel cannot force MPI dependencies at install time. The `mpi` extra declares `mpi4py`, and the `test` extra declares `pytest`. Runtime dependencies intentionally stay minimal: `numpy` is required, while Conduit's Python module is exposed by the generated `conduit.pth` file. + diff --git a/src/python/cmake/axom-python-env.sh.in b/src/python/cmake/axom-python-env.sh.in index a8e9616b39..6b350782e8 100644 --- a/src/python/cmake/axom-python-env.sh.in +++ b/src/python/cmake/axom-python-env.sh.in @@ -49,10 +49,14 @@ _axom_python_export CXX "@CMAKE_CXX_COMPILER@" _axom_python_export AXOM_INSTALL_PREFIX "@AXOM_INSTALL_PREFIX@" _axom_python_export CONDUIT_DIR "@AXOM_CONDUIT_DIR@" _axom_python_export CONDUIT_PYTHON_MODULE_DIR "@_axom_py_conduit_python_module_dir@" +_axom_python_export ENABLE_MPI "@AXOM_USE_MPI@" +_axom_python_export AXOM_ENABLE_MPI "@AXOM_USE_MPI@" _axom_python_export CMAKE_C_COMPILER "@CMAKE_C_COMPILER@" _axom_python_export CMAKE_CXX_COMPILER "@CMAKE_CXX_COMPILER@" -_axom_python_export MPI_C_COMPILER "@MPI_C_COMPILER@" -_axom_python_export MPI_CXX_COMPILER "@MPI_CXX_COMPILER@" +if [ "@AXOM_USE_MPI@" = "ON" ] || [ "@AXOM_USE_MPI@" = "TRUE" ] || [ "@AXOM_USE_MPI@" = "1" ]; then + _axom_python_export MPI_C_COMPILER "@MPI_C_COMPILER@" + _axom_python_export MPI_CXX_COMPILER "@MPI_CXX_COMPILER@" +fi if [ -x "${_axom_python_prefix_dir}/bin/python" ]; then _axom_python_export Python_EXECUTABLE "${_axom_python_prefix_dir}/bin/python" else diff --git a/src/python/cmake/axom-python-host-config.cmake.in b/src/python/cmake/axom-python-host-config.cmake.in index 8b71c89b1a..e095ba05d2 100644 --- a/src/python/cmake/axom-python-host-config.cmake.in +++ b/src/python/cmake/axom-python-host-config.cmake.in @@ -32,6 +32,9 @@ set(CONDUIT_DIR "@AXOM_CONDUIT_DIR@" CACHE PATH "Conduit install prefix") # since axom-config.cmake records it as an absolute path. set(CONDUIT_PYTHON_MODULE_DIR "@_axom_py_conduit_python_module_dir@" CACHE PATH "Conduit Python module directory") +set(ENABLE_MPI "@AXOM_USE_MPI@" CACHE BOOL "Enable MPI to match the Axom Python wheel") +set(AXOM_ENABLE_MPI "@AXOM_USE_MPI@" CACHE BOOL "Whether this Axom Python wheel was built against MPI-enabled Axom") + if(EXISTS "@CMAKE_C_COMPILER@") set(CMAKE_C_COMPILER "@CMAKE_C_COMPILER@" CACHE FILEPATH "C compiler used for the Axom Python wheel") endif() @@ -40,12 +43,14 @@ if(EXISTS "@CMAKE_CXX_COMPILER@") set(CMAKE_CXX_COMPILER "@CMAKE_CXX_COMPILER@" CACHE FILEPATH "CXX compiler used for the Axom Python wheel") endif() -if(EXISTS "@MPI_C_COMPILER@") - set(MPI_C_COMPILER "@MPI_C_COMPILER@" CACHE FILEPATH "MPI C compiler wrapper used for the Axom Python wheel") -endif() +if(ENABLE_MPI) + if(EXISTS "@MPI_C_COMPILER@") + set(MPI_C_COMPILER "@MPI_C_COMPILER@" CACHE FILEPATH "MPI C compiler wrapper used for the Axom Python wheel") + endif() -if(EXISTS "@MPI_CXX_COMPILER@") - set(MPI_CXX_COMPILER "@MPI_CXX_COMPILER@" CACHE FILEPATH "MPI CXX compiler wrapper used for the Axom Python wheel") + if(EXISTS "@MPI_CXX_COMPILER@") + set(MPI_CXX_COMPILER "@MPI_CXX_COMPILER@" CACHE FILEPATH "MPI CXX compiler wrapper used for the Axom Python wheel") + endif() endif() if(_AXOM_PYTHON_INSTALLED_EXECUTABLE) From 8082cfb49a4ca1d4872e4a63bd7d4b7ff15ea6c9 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Thu, 30 Jul 2026 18:24:33 -0700 Subject: [PATCH 30/35] Python: Fixes CI to work locally --- .github/workflows/ci-tests.yml | 15 ++- .../github-actions/linux-wheel_and_test.sh | 112 ++++++++---------- src/python/README.md | 16 +-- 3 files changed, 71 insertions(+), 72 deletions(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 1db4e7b4cb..de3d2ebb0e 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -119,8 +119,8 @@ jobs: name: Test Results ${{ matrix.build_type }} - ${{ matrix.config.job_name }} path: "**/Test.xml" build_wheel_and_test: - # Build the thin pip/uv wheel (src/python) against a freshly installed Axom - # and run the Sidre Python suite from it without a wrapper or PYTHONPATH updates. + # Build the thin pip/uv wheel (src/python) against the prebuilt Axom install + # in the docker image's host-config and run the Sidre Python suite from it # Uses the nanobind-enabled gcc image runs-on: ubuntu-24.04 needs: @@ -139,8 +139,15 @@ jobs: - name: Build wheel and test - gcc@13.3.1 timeout-minutes: 80 run: | - HOST_CONFIG=gcc@13.3.1.cmake \ - BUILD_TYPE=Debug \ + shopt -s nullglob + axom_cmake_dirs=(/home/axom/axom_tpls/gcc-13.3.1/axom-develop-*/lib/cmake) + if [[ ${#axom_cmake_dirs[@]} -ne 1 ]]; then + echo "Expected exactly one prebuilt Axom CMake package, found ${#axom_cmake_dirs[@]}" + printf '%s\n' "${axom_cmake_dirs[@]}" + exit 1 + fi + HOST_CONFIG=host-configs/docker/gcc@13.3.1.cmake \ + AXOM_DIR="${axom_cmake_dirs[0]}" \ ./scripts/github-actions/linux-wheel_and_test.sh windows_build_and_test: runs-on: windows-latest diff --git a/scripts/github-actions/linux-wheel_and_test.sh b/scripts/github-actions/linux-wheel_and_test.sh index 2226d7263d..3c4d9a19a8 100755 --- a/scripts/github-actions/linux-wheel_and_test.sh +++ b/scripts/github-actions/linux-wheel_and_test.sh @@ -10,11 +10,11 @@ # Build the thin, pip/uv-installable Axom wheel and exercise it end to end, # without using the run_python_with_axom.sh wrapper or updating the PYTHONPATH: # -# 1. configure + build + install Axom (with Python bindings) from a docker t-config; -# 2. build the wheel from src/python against that install (find_package(axom)); -# 3. install the wheel into a fresh uv venv; -# 4. verify the wheel installed conduit.pth for the same-build Conduit python module; -# 5. run the Sidre Python test suite with plain pytest. +# 1. build the wheel from src/python against the prebuilt Axom install +# specified by AXOM_DIR or AXOM_INSTALL (find_package(axom)); +# 2. install the wheel into a fresh uv venv; +# 3. verify the wheel installed conduit.pth for the same-build Conduit python module; +# 4. run the Sidre Python test suite with plain pytest. # # Intended for the gcc docker image, which is nanobind-enabled. @@ -24,67 +24,63 @@ set -e set -o pipefail set -x -HOST_CONFIG="${HOST_CONFIG:-gcc@13.3.1.cmake}" -BUILD_TYPE="${BUILD_TYPE:-Debug}" -BUILD_DIR="${BUILD_DIR:-builddir_wheel}" +HOST_CONFIG="${HOST_CONFIG:-host-configs/docker/gcc@13.3.1.cmake}" echo "~~~~ helpful info ~~~~" echo "USER=$(id -u -n)" echo "PWD=$(pwd)" echo "HOST_CONFIG=${HOST_CONFIG}" -echo "BUILD_TYPE=${BUILD_TYPE}" echo "~~~~~~~~~~~~~~~~~~~~~~" -NUM_BUILD_PROCS=$(python3 -c 'import os; print(max(2, os.cpu_count() * 8 // 10))') - -echo "~~~~~~ CONFIGURE + BUILD + INSTALL AXOM (+python) ~~~~~~" -python3 ./config-build.py \ - -bp "${BUILD_DIR}" \ - -hc "./host-configs/docker/${HOST_CONFIG}" \ - -bt "${BUILD_TYPE}" -cmake --build "${BUILD_DIR}" -j "${NUM_BUILD_PROCS}" -cmake --install "${BUILD_DIR}" - -# Resolve the Axom install prefix from the CMake cache -CACHE="${BUILD_DIR}/CMakeCache.txt" -AXOM_INSTALL=$(awk -F= '/^CMAKE_INSTALL_PREFIX:[A-Z]*=/{print $2}' "${CACHE}") -echo "AXOM_INSTALL=${AXOM_INSTALL}" +absolute_path() { + local path="$1" + local dir + local base + dir=$(dirname "${path}") + base=$(basename "${path}") + printf "%s/%s" "$(cd "${dir}" && pwd -P)" "${base}" +} -if [[ -z "${AXOM_INSTALL}" || ! -d "${AXOM_INSTALL}" ]]; then - echo "ERROR: Axom install prefix not found (${AXOM_INSTALL})." +if [[ ! -f "${HOST_CONFIG}" ]]; then + echo "ERROR: Host-config not found: ${HOST_CONFIG}" >&2 exit 1 fi +HOST_CONFIG_PATH=$(absolute_path "${HOST_CONFIG}") +echo "HOST_CONFIG_PATH=${HOST_CONFIG_PATH}" -cache_value() { - awk -F= -v key="$1" '$1 ~ "^" key ":[^=]*$" {print $2; exit}' "${CACHE}" -} - -cache_bool_is_on() { - local value - value=$(cache_value "$1") - case "${value^^}" in - ON|TRUE|YES|1) - return 0 - ;; - *) - return 1 - ;; - esac -} - -require_cmake_define_from_cache() { +# extract value from host-config line of the form `set(${name} ON CACHE BOOL "")` +# then capitalizes it and looks for true-like patterns +cmake_bool_from_file_is_on() { local name="$1" local value - value=$(cache_value "${name}") - if [[ -z "${value}" ]]; then - echo "ERROR: Required CMake cache entry ${name} was not found in ${CACHE}." - exit 1 - fi - WHEEL_BUILD_ARGS+=("-C" "cmake.define.${name}=${value}") + value=$(awk -v name="${name}" ' + $0 ~ "set\\(" name "[ \t\"]+" { + line = $0 + sub("^[ \t]*set\\(" name "[ \t\"]+", "", line) + sub("[ \t\"\\)].*$", "", line) + print line + exit + } + ' "${HOST_CONFIG_PATH}") + value="${value^^}" + [[ "${value}" == "ON" || "${value}" == "TRUE" || "${value}" == "YES" || "${value}" == "1" ]] } +if [[ -n "${AXOM_INSTALL:-}" && -z "${AXOM_DIR:-}" ]]; then + AXOM_DIR="${AXOM_INSTALL%/}/lib/cmake" +fi + +if [[ -z "${AXOM_DIR:-}" || ! -f "${AXOM_DIR}/axom-config.cmake" ]]; then + echo "ERROR: Axom CMake package not found." >&2 + echo " Set AXOM_DIR to the directory containing axom-config.cmake," >&2 + echo " or set AXOM_INSTALL to an Axom install prefix." >&2 + exit 1 +fi +AXOM_DIR=$(absolute_path "${AXOM_DIR}") +echo "AXOM_DIR=${AXOM_DIR}" + AXOM_WHEEL_ENABLE_MPI=OFF -if cache_bool_is_on ENABLE_MPI || cache_bool_is_on AXOM_ENABLE_MPI; then +if cmake_bool_from_file_is_on ENABLE_MPI; then AXOM_WHEEL_ENABLE_MPI=ON fi echo "AXOM_WHEEL_ENABLE_MPI=${AXOM_WHEEL_ENABLE_MPI}" @@ -100,16 +96,12 @@ echo "~~~~~~ BUILD THE THIN WHEEL FROM src/python ~~~~~~" # Point find_package at the install with AXOM_DIR # Conduit resolves transitively from axom's config, which records its Conduit prefix rm -rf dist -WHEEL_BUILD_ARGS=( - "-C" "cmake.define.AXOM_DIR=${AXOM_INSTALL}/lib/cmake" -) -require_cmake_define_from_cache CMAKE_C_COMPILER -require_cmake_define_from_cache CMAKE_CXX_COMPILER -if [[ "${AXOM_WHEEL_ENABLE_MPI}" == "ON" ]]; then - require_cmake_define_from_cache MPI_C_COMPILER - require_cmake_define_from_cache MPI_CXX_COMPILER -fi -uv build --wheel "${WHEEL_BUILD_ARGS[@]}" --out-dir dist src/python +uv build --wheel \ + -C cmake.args=-C \ + -C "cmake.args=${HOST_CONFIG_PATH}" \ + -C "cmake.define.AXOM_DIR=${AXOM_DIR}" \ + --out-dir dist \ + src/python ls -l dist AXOM_WHEEL=$(find dist -maxdepth 1 -name 'axom-*.whl' -print -quit) if [[ -z "${AXOM_WHEEL}" ]]; then diff --git a/src/python/README.md b/src/python/README.md index 41da13ab7d..c92890cff7 100644 --- a/src/python/README.md +++ b/src/python/README.md @@ -129,17 +129,15 @@ Note the deliberately distinct name: `CONDUIT_PYTHON_MODULE_DIR` cannot be used `find_package(axom)` pulls in `ConduitConfig.cmake`, which sets that variable with a plain `set()` and so overwrites whatever the caller passed. -For MPI-enabled Axom installs, pass the same compiler and MPI wrapper family -used by the Axom build. Copy these from the Axom build's `CMakeCache.txt` or -host-config: +When building against a host-config, pass the same cache script used for the +Axom install instead of duplicating compiler and MPI settings one variable at a +time: ```bash uv build --wheel \ + -C cmake.args=-C \ + -C cmake.args=/absolute/path/to/host-config.cmake \ -C cmake.define.AXOM_DIR="$AXOM_INSTALL/lib/cmake" \ - -C cmake.define.CMAKE_C_COMPILER="$AXOM_C_COMPILER" \ - -C cmake.define.CMAKE_CXX_COMPILER="$AXOM_CXX_COMPILER" \ - -C cmake.define.MPI_C_COMPILER="$AXOM_MPI_C_COMPILER" \ - -C cmake.define.MPI_CXX_COMPILER="$AXOM_MPI_CXX_COMPILER" \ src/python ``` @@ -211,4 +209,6 @@ so the wheel cannot force MPI dependencies at install time. The `mpi` extra declares `mpi4py`, and the `test` extra declares `pytest`. Runtime dependencies intentionally stay minimal: `numpy` is required, while Conduit's Python module is exposed by the generated `conduit.pth` file. - +The GitHub wheel test lane builds against an explicitly passed prebuilt Axom +install, passes the matching host-config through `cmake.args=-C`, and selects +the `mpi` extra automatically when that host-config reports `ENABLE_MPI=ON`. From 93e4c31ea22b12ab7649b5ae19e476ce54f06851 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Thu, 30 Jul 2026 23:13:44 -0700 Subject: [PATCH 31/35] Python: Some minor fixups and clarifications --- scripts/github-actions/linux-wheel_and_test.sh | 4 ++++ src/python/CMakeLists.txt | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/scripts/github-actions/linux-wheel_and_test.sh b/scripts/github-actions/linux-wheel_and_test.sh index 3c4d9a19a8..ee447f2308 100755 --- a/scripts/github-actions/linux-wheel_and_test.sh +++ b/scripts/github-actions/linux-wheel_and_test.sh @@ -34,6 +34,10 @@ echo "~~~~~~~~~~~~~~~~~~~~~~" absolute_path() { local path="$1" + if [[ ! -e "${path}" ]]; then + echo "ERROR: Path does not exist: ${path}" >&2 + return 1 + fi local dir local base dir=$(dirname "${path}") diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index ac681f04e9..7a225e164f 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -58,6 +58,12 @@ if(NOT _axom_py_conduit_cmake_dir AND AXOM_CONDUIT_DIR) endif() # Conduit's Python package directory -- needs to be absolute +# +# NOTE: Renamed from CONDUIT_PYTHON_MODULE_DIR to avoid collision: +# find_package(axom) pulls in ConduitConfig.cmake, which sets +# CONDUIT_PYTHON_MODULE_DIR with plain set() and would overwrite +# any value the user passed. AXOM_PYTHON_CONDUIT_MODULE_DIR is +# distinct and not shadowed by Conduit's config. set(AXOM_PYTHON_CONDUIT_MODULE_DIR "" CACHE PATH "Directory holding the same-build conduit Python package; overrides the value recorded by the Axom install") From 747ee1e41f850ce6442c53ad812b6bc846414ba3 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Sun, 2 Aug 2026 12:43:23 -0700 Subject: [PATCH 32/35] CI bugfix -- need to run the wheels test through a job that has an Axom install --- .github/workflows/ci-tests.yml | 50 +++++++++++++--------------------- 1 file changed, 19 insertions(+), 31 deletions(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index de3d2ebb0e..c47c43b06c 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -40,18 +40,21 @@ jobs: cmake_opts: '-DBUILD_SHARED_LIBS=ON -DENABLE_BENCHMARKS:BOOL=ON' do_build: 'yes' do_benchmarks: 'yes' + do_wheel: 'yes' - job_name: gcc@13.3.1, shared, 32bit host_config: gcc@13.3.1.cmake compiler_image: ${{ needs.set_image_vars.outputs.gcc_docker_image }} cmake_opts: '-DBUILD_SHARED_LIBS=ON -DAXOM_USE_64BIT_INDEXTYPE:BOOL=OFF -DAXOM_NO_INT64_T=1' do_build: 'yes' do_benchmarks: 'no' + do_wheel: 'no' - job_name: llvm@19.0.0, shared, benchmarks, quest regression host_config: llvm@19.0.0.cmake compiler_image: ${{ needs.set_image_vars.outputs.clang_docker_image }} cmake_opts: '-DBUILD_SHARED_LIBS=ON -DENABLE_BENCHMARKS:BOOL=ON' do_build: 'yes' do_benchmarks: 'yes' + do_wheel: 'no' include: - build_type: Debug config: @@ -61,6 +64,7 @@ jobs: cmake_opts: '-DBUILD_SHARED_LIBS=ON -DAXOM_ENABLE_MIR:BOOL=OFF -DAXOM_ENABLE_BUMP:BOOL=OFF -U RAJA_DIR' do_build: 'yes' do_benchmarks: 'no' + do_wheel: 'no' - build_type: Debug config: job_name: llvm@19.0.0, shared, no umpire @@ -69,6 +73,7 @@ jobs: cmake_opts: '-DBUILD_SHARED_LIBS=ON -U UMPIRE_DIR' do_build: 'yes' do_benchmarks: 'no' + do_wheel: 'no' - build_type: Debug config: job_name: llvm@19.0.0, shared, no raja and umpire @@ -77,6 +82,7 @@ jobs: cmake_opts: '-DBUILD_SHARED_LIBS=ON -DAXOM_ENABLE_MIR:BOOL=OFF -DAXOM_ENABLE_BUMP:BOOL=OFF -U RAJA_DIR -U UMPIRE_DIR' do_build: 'yes' do_benchmarks: 'no' + do_wheel: 'no' - build_type: Debug config: job_name: llvm@19.0.0, shared, no profiling @@ -85,6 +91,7 @@ jobs: cmake_opts: '-DBUILD_SHARED_LIBS=ON -U CALIPER_DIR -U ADIAK_DIR' do_build: 'yes' do_benchmarks: 'no' + do_wheel: 'no' name: ${{ matrix.build_type }} - ${{ matrix.config.job_name }} container: image: ${{ matrix.config.compiler_image }} @@ -113,42 +120,23 @@ jobs: CMAKE_EXTRA_FLAGS=" ${{ matrix.config.cmake_opts }} -DAXOM_QUEST_ENABLE_EXTRA_REGRESSION_TESTS:BOOL=${{ contains(matrix.config.job_name, 'quest regression') && 'ON' || 'OFF' }} " \ BUILD_TYPE=${{ matrix.build_type }} \ ./scripts/github-actions/linux-build_and_test.sh + - name: Build and test the Python wheel - ${{ matrix.build_type }} - ${{ matrix.config.job_name }} + # Builds Python wheel (src/python) against the Axom that the step above just installed; one build type is enough. + if: matrix.config.do_wheel == 'yes' && matrix.build_type == 'Release' + shell: bash + timeout-minutes: 40 + run: | + # Find the installation path + AXOM_INSTALL=$(awk -F= '/^CMAKE_INSTALL_PREFIX:PATH=/{print $2}' builddir/CMakeCache.txt) + # Run the wheels test + HOST_CONFIG=host-configs/docker/${{ matrix.config.host_config }} \ + AXOM_INSTALL="${AXOM_INSTALL}" \ + ./scripts/github-actions/linux-wheel_and_test.sh - name: Upload Test Results uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 with: name: Test Results ${{ matrix.build_type }} - ${{ matrix.config.job_name }} path: "**/Test.xml" - build_wheel_and_test: - # Build the thin pip/uv wheel (src/python) against the prebuilt Axom install - # in the docker image's host-config and run the Sidre Python suite from it - # Uses the nanobind-enabled gcc image - runs-on: ubuntu-24.04 - needs: - - set_image_vars - container: - image: ${{ needs.set_image_vars.outputs.gcc_docker_image }} - volumes: - - /home/axom/axom - # Required - default is set to user "axom" - options: --user root - steps: - - name: Checkout Axom - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - with: - submodules: recursive - - name: Build wheel and test - gcc@13.3.1 - timeout-minutes: 80 - run: | - shopt -s nullglob - axom_cmake_dirs=(/home/axom/axom_tpls/gcc-13.3.1/axom-develop-*/lib/cmake) - if [[ ${#axom_cmake_dirs[@]} -ne 1 ]]; then - echo "Expected exactly one prebuilt Axom CMake package, found ${#axom_cmake_dirs[@]}" - printf '%s\n' "${axom_cmake_dirs[@]}" - exit 1 - fi - HOST_CONFIG=host-configs/docker/gcc@13.3.1.cmake \ - AXOM_DIR="${axom_cmake_dirs[0]}" \ - ./scripts/github-actions/linux-wheel_and_test.sh windows_build_and_test: runs-on: windows-latest strategy: From 573061b366a464ce406c6728a8e264f03b62e5eb Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Sun, 2 Aug 2026 12:48:56 -0700 Subject: [PATCH 33/35] CI bugfix -- ensure we can install uv in our CI images --- scripts/github-actions/linux-wheel_and_test.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/github-actions/linux-wheel_and_test.sh b/scripts/github-actions/linux-wheel_and_test.sh index ee447f2308..6bc8aa1e7a 100755 --- a/scripts/github-actions/linux-wheel_and_test.sh +++ b/scripts/github-actions/linux-wheel_and_test.sh @@ -91,7 +91,13 @@ echo "AXOM_WHEEL_ENABLE_MPI=${AXOM_WHEEL_ENABLE_MPI}" echo "~~~~~~ ENSURE uv IS AVAILABLE ~~~~~~" if ! command -v uv >/dev/null 2>&1; then - python3 -m pip install --user uv + # Distro Pythons (e.g. Ubuntu 24.04) ship a PEP 668 EXTERNALLY-MANAGED marker, which makes `pip install --user` fail. + # uv only lands in the user site directory so opting out is safe here. + pip_args="--user" + if python3 -c 'import os, sysconfig, sys; sys.exit(0 if os.path.exists(os.path.join(sysconfig.get_path("stdlib"), "EXTERNALLY-MANAGED")) else 1)'; then + pip_args="${pip_args} --break-system-packages" + fi + python3 -m pip install ${pip_args} uv export PATH="${HOME}/.local/bin:${PATH}" fi uv --version From 427fcadbd532f0a8fc0a6e9bf7c304f07a242e66 Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Tue, 11 Aug 2026 14:20:13 -0700 Subject: [PATCH 34/35] Python: Ensures the python bindings project is using the same CXX_STANDARD as Axom --- src/cmake/axom-config.cmake.in | 1 + src/python/CMakeLists.txt | 13 +++++++++++++ src/python/cmake/axom-python-host-config.cmake.in | 5 +++++ 3 files changed, 19 insertions(+) diff --git a/src/cmake/axom-config.cmake.in b/src/cmake/axom-config.cmake.in index 64d547d8bb..37fdaaad98 100644 --- a/src/cmake/axom-config.cmake.in +++ b/src/cmake/axom-config.cmake.in @@ -30,6 +30,7 @@ if(NOT AXOM_FOUND) #---------------------------------------------------------------------------- # Language features + set(AXOM_CXX_STANDARD "@CMAKE_CXX_STANDARD@") set(AXOM_ENABLE_FORTRAN "@ENABLE_FORTRAN@") set(AXOM_USE_CUDA "@AXOM_USE_CUDA@") set(AXOM_USE_HIP "@AXOM_USE_HIP@") diff --git a/src/python/CMakeLists.txt b/src/python/CMakeLists.txt index 7a225e164f..51045c141f 100644 --- a/src/python/CMakeLists.txt +++ b/src/python/CMakeLists.txt @@ -52,6 +52,19 @@ endif() find_package(axom CONFIG REQUIRED) find_package(nanobind CONFIG REQUIRED) # supplied via build-system.requires +# Compile the bindings with the C++ standard of the Axom install. +if(NOT AXOM_CXX_STANDARD) + set(AXOM_CXX_STANDARD 20) +endif() + +if(NOT DEFINED CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD ${AXOM_CXX_STANDARD}) +elseif(CMAKE_CXX_STANDARD LESS AXOM_CXX_STANDARD) + message(FATAL_ERROR + "CMAKE_CXX_STANDARD is C++${CMAKE_CXX_STANDARD}, " + "but the Axom install at '${axom_DIR}' was built with C++${AXOM_CXX_STANDARD}.") +endif() + set(_axom_py_conduit_cmake_dir "${Conduit_DIR}") if(NOT _axom_py_conduit_cmake_dir AND AXOM_CONDUIT_DIR) set(_axom_py_conduit_cmake_dir "${AXOM_CONDUIT_DIR}/lib/cmake/conduit") diff --git a/src/python/cmake/axom-python-host-config.cmake.in b/src/python/cmake/axom-python-host-config.cmake.in index e095ba05d2..eace669d3a 100644 --- a/src/python/cmake/axom-python-host-config.cmake.in +++ b/src/python/cmake/axom-python-host-config.cmake.in @@ -32,6 +32,11 @@ set(CONDUIT_DIR "@AXOM_CONDUIT_DIR@" CACHE PATH "Conduit install prefix") # since axom-config.cmake records it as an absolute path. set(CONDUIT_PYTHON_MODULE_DIR "@_axom_py_conduit_python_module_dir@" CACHE PATH "Conduit Python module directory") +# Axom's headers, and the third-party headers they include, require this standard. +# BLT_CXX_STD is what BLT-based projects read; CMAKE_CXX_STANDARD covers the rest. +set(BLT_CXX_STD "c++@CMAKE_CXX_STANDARD@" CACHE STRING "C++ standard used for the Axom Python wheel") +set(CMAKE_CXX_STANDARD "@CMAKE_CXX_STANDARD@" CACHE STRING "C++ standard used for the Axom Python wheel") + set(ENABLE_MPI "@AXOM_USE_MPI@" CACHE BOOL "Enable MPI to match the Axom Python wheel") set(AXOM_ENABLE_MPI "@AXOM_USE_MPI@" CACHE BOOL "Whether this Axom Python wheel was built against MPI-enabled Axom") From 8f67e363da864f27b34efc1b783540db2523865c Mon Sep 17 00:00:00 2001 From: Kenneth Weiss Date: Wed, 29 Jul 2026 11:25:58 -0700 Subject: [PATCH 35/35] Updates RELEASE-NOTES --- RELEASE-NOTES.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index e961902bf2..af782d85ad 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -61,6 +61,8 @@ The Axom project release numbers follow [Semantic Versioning](http://semver.org/ - Klee: Adds support for lua-based input decks for shaping - Slam: Adds convenience aliases in `axom/slam/Aliases.hpp` for the most common set and relation configurations, including `ArraySet`, `ArrayViewSet`, `VariableRelation`, `ConstantRelation` and their `View` forms. +- Python: Adds a scikit-build-core project under `src/python/` for building a thin, pip/uv-installable `axom` wheel. + The wheel compiles Axom's bindings against an already-installed Axom. ### Removed - Bump: Removed `axom::bump::views::MultiBufferMaterialView`, which was a view type for an obsolete flavor of Blueprint matset. @@ -104,6 +106,8 @@ The Axom project release numbers follow [Semantic Versioning](http://semver.org/ - Quest: Status-returning reader/writer operations in `C2CReader`, `MFEMReader`, `ProEReader`, `STEPReader`, `STLReader`, `STLWriter`, and their parallel variants are now marked `[[nodiscard]]`. Callers that previously ignored returned status values must check them to avoid compiler diagnostics. +- Python: Sidre's bindings all name their arguments, so they can be passed by keyword and show up in IDE + completion and signature help. ### Fixed - MIR/Bump: `MergeCoordsetPoints` now only emits its node-merge `SLIC_INFO` when MIR `verbose` is enabled on the Conduit options passed through ELVIRA.