From a652c839c3881e8c8a9ac48ef5a3b090c88024f2 Mon Sep 17 00:00:00 2001 From: Simon Pinches Date: Thu, 28 May 2026 11:13:09 +0200 Subject: [PATCH 1/3] Support a two-stage build for separate libimas-core / imas-core packaging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an opt-in AL_USE_INSTALLED_CORE option that lets the Python wrapper be built against a pre-installed al-core via find_package, instead of compiling and bundling libal alongside the Cython extensions. This enables splitting the project into two downstream packages (e.g. the libimas-core and imas-core conda packages) without forking the build system, while keeping the existing single-shot wheel build as the default. - New option AL_USE_INSTALLED_CORE (default OFF). When ON, the al library is located with find_package(al-core CONFIG REQUIRED) and aliased as `al` so the rest of the tree links transparently. - The C/C++ library block is gated with `AND NOT AL_USE_INSTALLED_CORE` so it is skipped in stage-2 builds. - Install an al-coreTargets export plus a generated al-coreConfig.cmake / al-coreConfigVersion.cmake (additive — also benefits any other downstream C++ consumer). - python/CMakeLists.txt early-returns when AL_USE_INSTALLED_CORE is ON, installing only the Cython extensions and skipping the libal / runtime-deps bundling that is not valid for an alias of an imported target. - Expose AL_USE_INSTALLED_CORE through pyproject.toml so the option flows through scikit-build-core via the matching env var. Default behaviour is unchanged: with AL_USE_INSTALLED_CORE=OFF the build still compiles libal, builds the Python bindings, and produces the same self-contained wheel as before. Inspired by conda-forge/staged-recipes#31554, but keeping CMake as the single source of truth rather than introducing a parallel Meson build. --- CMakeLists.txt | 58 ++++++++++++++++++++++++++++++++---- cmake/al-coreConfig.cmake.in | 8 +++++ pyproject.toml | 1 + python/CMakeLists.txt | 8 +++++ 4 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 cmake/al-coreConfig.cmake.in diff --git a/CMakeLists.txt b/CMakeLists.txt index 9f14738a..081ca2ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,13 @@ endif() # ############################################################################## option(AL_PYTHON_BINDINGS "Build Python bindings" OFF) +# When ON, the al C++ library is not built here; instead it is located with +# find_package(al-core CONFIG). Enables a two-stage build where libimas-core +# (the C/C++ library) and imas-core (the Python wrapper) are packaged +# separately, e.g. as two conda packages. +option(AL_USE_INSTALLED_CORE + "Link Python bindings against a pre-installed al-core (find_package)" OFF) + # Configuration options for shared libraries # ############################################################################## @@ -127,8 +134,19 @@ if(WIN32) find_package(dlfcn-win32 CONFIG REQUIRED) endif() -# build AL_CORE library only if backend is enabled -if(AL_BACKEND_HDF5 OR AL_BACKEND_MDSPLUS OR AL_BACKEND_UDA OR AL_BACKEND_UDAFAT OR AL_PYTHON_BINDINGS) +# Two-stage build entry: locate a pre-installed al-core instead of building it. +# Exposes target `al-core::al` and an unqualified alias `al` so the python/ +# subdirectory and any consumers can link to `al` transparently. +if(AL_USE_INSTALLED_CORE) + find_package(al-core CONFIG REQUIRED) + if(NOT TARGET al) + add_library(al ALIAS al-core::al) + endif() +endif() + +# build AL_CORE library only if backend is enabled and we are not reusing an +# already-installed al-core +if((AL_BACKEND_HDF5 OR AL_BACKEND_MDSPLUS OR AL_BACKEND_UDA OR AL_BACKEND_UDAFAT OR AL_PYTHON_BINDINGS) AND NOT AL_USE_INSTALLED_CORE) # Core dependencies set(Boost_USE_MULTITHREADED FALSE) @@ -199,14 +217,43 @@ add_dependencies( imas_print_version al ) # ############################################################################## # Install al library +include(GNUInstallDirs) install( TARGETS al + EXPORT al-coreTargets RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT core ) +# Export CMake package config so downstream projects (notably the Python +# wrapper built with AL_USE_INSTALLED_CORE=ON) can `find_package(al-core CONFIG)`. +include(CMakePackageConfigHelpers) +set(_al_core_cmake_dir ${CMAKE_INSTALL_LIBDIR}/cmake/al-core) +install( + EXPORT al-coreTargets + FILE al-coreTargets.cmake + NAMESPACE al-core:: + DESTINATION ${_al_core_cmake_dir} +) +configure_package_config_file( + ${CMAKE_CURRENT_SOURCE_DIR}/cmake/al-coreConfig.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/al-coreConfig.cmake + INSTALL_DESTINATION ${_al_core_cmake_dir} +) +write_basic_package_version_file( + ${CMAKE_CURRENT_BINARY_DIR}/al-coreConfigVersion.cmake + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion +) +install( + FILES + ${CMAKE_CURRENT_BINARY_DIR}/al-coreConfig.cmake + ${CMAKE_CURRENT_BINARY_DIR}/al-coreConfigVersion.cmake + DESTINATION ${_al_core_cmake_dir} +) + # TODO: put public heades in a separate directory? install( FILES ${PUBLIC_HEADER_FILES} @@ -240,14 +287,15 @@ install(DIRECTORY common TYPE DATA) # Install Dummy install(TARGETS imas_print_version DESTINATION bin) -# Scikit-build-core entry point for python bindings +endif() # AL core library: (AL_BACKEND_* OR AL_PYTHON_BINDINGS) AND NOT AL_USE_INSTALLED_CORE + +# Scikit-build-core entry point for python bindings — works whether `al` was +# just built here or imported via find_package(al-core). # ############################################################################## if(AL_PYTHON_BINDINGS) include(skbuild.cmake) endif() -endif() # AL core library (AL_BACKEND_HDF5 OR AL_BACKEND_MDSPLUS OR AL_BACKEND_UDA OR AL_BACKEND_UDAFAT OR AL_PYTHON_BINDINGS) - # MDSplus models # ############################################################################## diff --git a/cmake/al-coreConfig.cmake.in b/cmake/al-coreConfig.cmake.in new file mode 100644 index 00000000..4d8b30ed --- /dev/null +++ b/cmake/al-coreConfig.cmake.in @@ -0,0 +1,8 @@ +@PACKAGE_INIT@ + +include(CMakeFindDependencyMacro) +find_dependency(Boost COMPONENTS filesystem) + +include("${CMAKE_CURRENT_LIST_DIR}/al-coreTargets.cmake") + +check_required_components(al-core) diff --git a/pyproject.toml b/pyproject.toml index e0ce230c..68165362 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,6 +83,7 @@ AL_BACKEND_UDA = { env = "AL_BACKEND_UDA", default = "ON" } AL_BACKEND_MDSPLUS = { env = "AL_BACKEND_MDSPLUS", default = "OFF" } AL_BUILD_MDSPLUS_MODELS = { env = "AL_BUILD_MDSPLUS_MODELS", default = "OFF" } AL_PYTHON_BINDINGS = "ON" +AL_USE_INSTALLED_CORE = { env = "AL_USE_INSTALLED_CORE", default = "OFF" } DOWNLOAD_DEPENDENCIES = { env = "DOWNLOAD_DEPENDENCIES", default = "ON" } DD_GIT_REPOSITORY = { env = "DD_GIT_REPOSITORY", default = "EMPTY" } DD_VERSION = { env = "DD_VERSION", default = "EMPTY" } diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 528e33f6..42341ece 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -8,6 +8,14 @@ python_add_library(al_defs MODULE ${al_defs_source} WITH_SOABI) target_link_libraries(_al_lowlevel PRIVATE Python::NumPy al) target_link_libraries(al_defs PRIVATE Python::NumPy al) +if(AL_USE_INSTALLED_CORE) + # libal is provided externally (e.g. the libimas-core conda package). + # Install only the Python extensions — the dynamic linker resolves libal + # at load time via the host's normal library search path / rpath. + install(TARGETS _al_lowlevel al_defs DESTINATION imas_core) + return() +endif() + # Handling RPATH in macOS: if(APPLE) set_target_properties(al PROPERTIES INSTALL_RPATH "@loader_path") From 0723109070327ab435f0c038a97cd278802969b3 Mon Sep 17 00:00:00 2001 From: Simon Pinches Date: Mon, 1 Jun 2026 11:58:16 +0200 Subject: [PATCH 2/3] Give AL_USE_INSTALLED_CORE a tailored error when al-core is missing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit find_package(al-core CONFIG REQUIRED) bails with CMake's stock "could not find a package configuration file" message, which doesn't tell the user that AL_USE_INSTALLED_CORE specifically requires a stage-1 install from this branch — released modules (e.g. IMAS-Core/5.6.0) only ship al-core.pc, not al-coreConfig.cmake, so simply `module load`ing an older IMAS-Core does not satisfy the option. Replace REQUIRED with an explicit FATAL_ERROR that names the missing file, points at CMAKE_PREFIX_PATH / al-core_DIR, and explains why the previously-released modules will not work. --- CMakeLists.txt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 081ca2ab..76281850 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -138,7 +138,19 @@ endif() # Exposes target `al-core::al` and an unqualified alias `al` so the python/ # subdirectory and any consumers can link to `al` transparently. if(AL_USE_INSTALLED_CORE) - find_package(al-core CONFIG REQUIRED) + find_package(al-core CONFIG) + if(NOT al-core_FOUND) + message(FATAL_ERROR + "AL_USE_INSTALLED_CORE=ON requires an installed al-core that ships " + "al-coreConfig.cmake (introduced in IMAS-Core 5.7.1 or later). " + "find_package(al-core CONFIG) could not locate it.\n" + "Point CMAKE_PREFIX_PATH (or al-core_DIR) at an install prefix that " + "contains lib/cmake/al-core/al-coreConfig.cmake — typically the " + "stage-1 install tree of this branch. Releases built before this " + "change (e.g. IMAS-Core/5.6.0) only ship al-core.pc and will NOT " + "satisfy AL_USE_INSTALLED_CORE; either install stage 1 from source " + "or wait for a release that includes the CMake package config.") + endif() if(NOT TARGET al) add_library(al ALIAS al-core::al) endif() From 991026ceb72a704a7bd90c37beb7cc1b21d53295 Mon Sep 17 00:00:00 2001 From: Simon Pinches Date: Mon, 1 Jun 2026 15:35:09 +0200 Subject: [PATCH 3/3] Fix direct cmake stage 2 build with AL_USE_INSTALLED_CORE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When AL_USE_INSTALLED_CORE=ON and CMake is invoked directly (not via scikit-build-core / pip wheel), skbuild.cmake's non-SKBUILD path tried to attach `add_custom_command(TARGET al POST_BUILD ...)` to the `al` target. With AL_USE_INSTALLED_CORE that target is an ALIAS of an imported target, so CMake refused with: TARGET 'al' is IMPORTED and does not build here. The pip-wheel build does not need to be a POST_BUILD hook on `al` — it is fundamentally a wheel build that consumes `al`, not a transform of it. Split the path: when AL_USE_INSTALLED_CORE is ON, drive `pip wheel` from al-python-bindings's own COMMAND (with ALL so default `cmake --build` triggers it); otherwise keep the existing POST_BUILD behaviour that scikit-build-core relies on in the monolithic flow. Verified by reproducing the failing direct-cmake stage 2 reported in PR #62 — configure, build, and install now all succeed and the produced wheel imports cleanly against a stage-1 libimas-core install. --- skbuild.cmake | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/skbuild.cmake b/skbuild.cmake index facd0058..b19e0964 100644 --- a/skbuild.cmake +++ b/skbuild.cmake @@ -34,19 +34,36 @@ elseif(NOT ${AL_PYTHON_BINDINGS} MATCHES "[Oo][Nn]") message(FATAL_ERROR "AL_PYTHON_BINDINGS=${AL_PYTHON_BINDINGS} not e|editable|no-build-isolation|[Oo][Nn]") endif() -add_custom_command( - TARGET al POST_BUILD - COMMAND - ${CMAKE_COMMAND} -E env +if(AL_USE_INSTALLED_CORE) + # `al` is an ALIAS to an imported target — it is not built in this tree, + # so it cannot carry a POST_BUILD hook. Drive pip wheel from the custom + # target itself, and put it in ALL so `cmake --build` triggers it. + add_custom_target(al-python-bindings ALL + COMMAND + ${CMAKE_COMMAND} -E env CMAKE_ARGS=${CMAKE_ARGS} - SKBUILD_BUILD_DIR=${CMAKE_CURRENT_BINARY_DIR}/{wheel_tag} + SKBUILD_BUILD_DIR=${CMAKE_CURRENT_BINARY_DIR}/{wheel_tag} ${Python_EXECUTABLE} - -m pip wheel + -m pip wheel ${CMAKE_CURRENT_SOURCE_DIR} - ${PIP_OPTIONS} - --wheel-dir ${CMAKE_CURRENT_BINARY_DIR}/dist/ -) -add_custom_target(al-python-bindings DEPENDS al) + ${PIP_OPTIONS} + --wheel-dir ${CMAKE_CURRENT_BINARY_DIR}/dist/ + ) +else() + add_custom_command( + TARGET al POST_BUILD + COMMAND + ${CMAKE_COMMAND} -E env + CMAKE_ARGS=${CMAKE_ARGS} + SKBUILD_BUILD_DIR=${CMAKE_CURRENT_BINARY_DIR}/{wheel_tag} + ${Python_EXECUTABLE} + -m pip wheel + ${CMAKE_CURRENT_SOURCE_DIR} + ${PIP_OPTIONS} + --wheel-dir ${CMAKE_CURRENT_BINARY_DIR}/dist/ + ) + add_custom_target(al-python-bindings DEPENDS al) +endif() set_target_properties( al-python-bindings PROPERTIES DIST_FOLDER ${CMAKE_CURRENT_BINARY_DIR}/dist/) install(CODE "execute_process(COMMAND ${Python_EXECUTABLE}