diff --git a/build-check/CPPC/meson.build b/build-check/CPPC/meson.build index 77bda1df..12ab9e03 100644 --- a/build-check/CPPC/meson.build +++ b/build-check/CPPC/meson.build @@ -61,7 +61,6 @@ version_sufficient = required_min == '' ? true : compiler_version.version_compar # --- failure analysis and reporting ---------------------------------------- if toolchain_functional and not version_sufficient - # Works in practice; don't break a functioning setup over a number. warning(toolchain_desc + ' is below the minimum GridFire tests against (' + required_min + '), but all C++23 capability probes passed. ' + 'Proceeding; if you hit compiler errors deep in the build, ' @@ -69,7 +68,6 @@ if toolchain_functional and not version_sufficient endif if not toolchain_functional - # 1) Name the failure precisely. failure_detail = '' if not have_print_hdr failure_detail += '\n * C++ standard library header not found.' @@ -83,14 +81,11 @@ if not toolchain_functional if not version_sufficient failure_detail += '\n * ' + toolchain_desc + ' is below the required minimum (' + required_min + ').' elif compiler_id == 'clang' and not is_apple_clang - # New-enough clang but probes failed: almost always the C++ stdlib - # underneath it, not clang itself. failure_detail += ('\n * clang itself is new enough; on Linux clang uses the system ' + 'libstdc++, so the GNU C++ runtime is likely too old. Install GCC >= ' + gridfire_gcc_min + ' (clang will pick up its libstdc++), or use -Dcpp_args=-stdlib=libc++ with libc++ >= 17 installed.') endif - # 2) Search for a suitable already-installed alternate. candidate_names = [] if compiler_id == 'gcc' candidate_names += ['g++-16', 'g++-15', 'g++-14', 'clang++-21', 'clang++-20', 'clang++-19', 'clang++-18', 'clang++-17'] @@ -110,7 +105,6 @@ if not toolchain_functional p = find_program(cand, required: false) if p.found() cand_ver = p.version() - # Decide the applicable minimum from the candidate's family. cand_min = cand.contains('clang') ? gridfire_clang_min : gridfire_gcc_min if cand_ver != 'unknown' and cand_ver.version_compare('>=' + cand_min) candidates_report += '\n [OK] ' + p.full_path() + ' (version ' + cand_ver + ')' @@ -123,7 +117,6 @@ if not toolchain_functional endif endforeach - # 3) OS-specific install guidance. os_help = '' if host_machine.system() == 'darwin' os_help = ''' @@ -160,7 +153,6 @@ How to get a suitable compiler on Linux:''' + distro_hint + ''' os_help = '\nInstall GCC >= ' + gridfire_gcc_min + ' or LLVM clang >= ' + gridfire_clang_min + ' for your platform.' endif - # 4) Assemble the verdict. if suitable_cxx != '' action = ('\nA suitable compiler IS already installed. Meson cannot switch compilers ' + 'after configuration starts, so re-run setup pointing at it:\n\n' @@ -181,18 +173,9 @@ How to get a suitable compiler on Linux:''' + distro_hint + ''' + action) endif -# --- everything below unchanged from the original check --------------------- -# For Eigen add_project_arguments('-Wno-deprecated-declarations', language: 'cpp') - -if get_option('build_python') - message('enabling hidden visibility for C++ symbols when building Python extension. This reduces the size of the resulting shared library.') - add_project_arguments('-fvisibility=hidden', language: 'cpp') -else - message('enabling default visibility for C++ symbols') - add_project_arguments('-fvisibility=default', language: 'cpp') -endif +add_project_arguments('-fvisibility=default', language: 'cpp') if get_option('openmp_support') gridfire_args += ['-DGF_USE_OPENMP'] diff --git a/build-config/fourdst/meson.build b/build-config/fourdst/meson.build index 093aa317..b7d3f3ae 100644 --- a/build-config/fourdst/meson.build +++ b/build-config/fourdst/meson.build @@ -1,82 +1,74 @@ -fourdst_build_lib_all = true -if not get_option('plugin_support') - fourdst_build_lib_all=false - message('Disabling fourdst plugin support as per user request.') -endif - -fourdst_default_options = [ - 'build_tests=' + get_option('build_tests').to_string(), - 'build_python=' + get_option('build_python').to_string(), - 'build_lib_all=' + fourdst_build_lib_all.to_string(), - 'build_lib_comp=true', - 'build_lib_config=true', - 'build_lib_log=true', - 'build_lib_const=true', - 'pkg_config=' + get_option('pkg_config').to_string(), -] - if get_option('build_python') - fourdst_default_options += ['default_library=static'] -endif + fourdst_inc_probe = run_command(py_installation, '-c', + 'import fourdst; print("\\n".join(fourdst.get_include_dirs()))', + check: false) + if fourdst_inc_probe.returncode() != 0 + error('Could not interrogate the fourdst wheel:\n' + fourdst_inc_probe.stderr() + + '\nIs fourdst installed in the build environment?') + endif + fourdst_inc_dirs = fourdst_inc_probe.stdout().strip().split('\n') -fourdst_sp = subproject('fourdst', default_options: fourdst_default_options) + fourdst_lib_probe = run_command(py_installation, '-c', + 'import fourdst; print("\\n".join(fourdst.get_lib_dirs()))', + check: false) + if fourdst_lib_probe.returncode() != 0 + error('Could not interrogate the fourdst wheel:\n' + fourdst_lib_probe.stderr()) + endif + fourdst_lib_dirs = fourdst_lib_probe.stdout().strip().split('\n') + fourdst_inc_args = [] + foreach d : fourdst_inc_dirs + fourdst_inc_args += ['-I' + d] + endforeach -composition_dep = fourdst_sp.get_variable('composition_dep') -log_dep = fourdst_sp.get_variable('log_dep') -const_dep = fourdst_sp.get_variable('const_dep') -config_dep = fourdst_sp.get_variable('config_dep') -if get_option('plugin_support') - warning('Including plugin library from fourdst. Note this will bring in minizip-ng and openssl, which can cause build issues with cross compilation due to their complexity.') - plugin_dep = fourdst_sp.get_variable('plugin_dep') -endif + cpp = meson.get_compiler('cpp') + comp_lib = cpp.find_library('composition', dirs: fourdst_lib_dirs) + log_lib = cpp.find_library('logging', dirs: fourdst_lib_dirs) + const_lib = cpp.find_library('const', dirs: fourdst_lib_dirs) + refl_lib = cpp.find_library('reflect_cpp', dirs: fourdst_lib_dirs) -libcomposition = fourdst_sp.get_variable('libcomposition') -libconst = fourdst_sp.get_variable('libconst') -liblogging = fourdst_sp.get_variable('liblogging') + composition_dep = declare_dependency(compile_args: fourdst_inc_args, dependencies: [comp_lib, refl_lib]) + log_dep = declare_dependency(compile_args: fourdst_inc_args, dependencies: log_lib) + const_dep = declare_dependency(compile_args: fourdst_inc_args, dependencies: const_lib) + config_dep = declare_dependency(compile_args: fourdst_inc_args) # header-only libconfig +else + fourdst_build_lib_all = true + if not get_option('plugin_support') + fourdst_build_lib_all=false + message('Disabling fourdst plugin support as per user request.') + endif -if get_option('plugin_support') - warning('Including plugin library from fourdst. Note this will bring in minizip-ng and openssl, which can cause build issues with cross compilation due to their complexity.') - libplugin = fourdst_sp.get_variable('libplugin') -endif - -if get_option('build_python') - sp_root = meson.project_source_root() / 'subprojects' - - fourdst_header_trees = [ - - ['config', - sp_root / 'libconfig' / 'src' / 'config' / 'include' / 'fourdst' / 'config', - gridfire_includedir / 'fourdst'], - ['composition', - sp_root / 'libcomposition' / 'src' / 'composition' / 'include' / 'fourdst' / 'composition', - gridfire_includedir / 'fourdst'], - ['atomic', - sp_root / 'libcomposition' / 'src' / 'composition' / 'include' / 'fourdst' / 'atomic', - gridfire_includedir / 'fourdst'], - ['constants', - sp_root / 'libconstants' / 'src' / 'constants' / 'include' / 'fourdst' / 'constants', - gridfire_includedir / 'fourdst'], - ['logging', - sp_root / 'liblogging' / 'src' / 'logging' / 'include' / 'fourdst' / 'logging', - gridfire_includedir / 'fourdst'], - ['toml++', - sp_root / 'libconfig' / 'build-config' / 'tomlpp' / 'vendor' / 'include' / 'toml++', - gridfire_fourdst_vendor_includedir], - ['quill', - sp_root / 'quill' / 'include' / 'quill', - gridfire_fourdst_vendor_includedir], - ['CLI', - sp_root / 'CLI11-2.6.1' / 'include' / 'CLI', - gridfire_fourdst_vendor_includedir], + fourdst_default_options = [ + 'build_tests=' + get_option('build_tests').to_string(), + 'build_python=' + get_option('build_python').to_string(), + 'build_lib_all=' + fourdst_build_lib_all.to_string(), + 'build_lib_comp=true', + 'build_lib_config=true', + 'build_lib_log=true', + 'build_lib_const=true', + 'pkg_config=' + get_option('pkg_config').to_string(), ] - foreach t : fourdst_header_trees - custom_target( - 'wheel_headers_' + t[0].underscorify(), - command: copytree_cmd + [t[1], '@OUTPUT@'], - output: t[0], - install: true, - install_dir: t[2], - ) - endforeach + if get_option('build_python') + fourdst_default_options += ['default_library=static'] + endif + + fourdst_sp = subproject('fourdst', default_options: fourdst_default_options) + + composition_dep = fourdst_sp.get_variable('composition_dep') + log_dep = fourdst_sp.get_variable('log_dep') + const_dep = fourdst_sp.get_variable('const_dep') + config_dep = fourdst_sp.get_variable('config_dep') + if get_option('plugin_support') + warning('Including plugin library from fourdst. Note this will bring in minizip-ng and openssl, which can cause build issues with cross compilation due to their complexity.') + plugin_dep = fourdst_sp.get_variable('plugin_dep') + endif + + libcomposition = fourdst_sp.get_variable('libcomposition') + libconst = fourdst_sp.get_variable('libconst') + liblogging = fourdst_sp.get_variable('liblogging') + + if get_option('plugin_support') + warning('Including plugin library from fourdst. Note this will bring in minizip-ng and openssl, which can cause build issues with cross compilation due to their complexity.') + libplugin = fourdst_sp.get_variable('libplugin') + endif endif diff --git a/build-python/meson.build b/build-python/meson.build index ef857fee..84fbe5c0 100644 --- a/build-python/meson.build +++ b/build-python/meson.build @@ -36,28 +36,16 @@ if get_option('build_python') ] - if meson.is_cross_build() and host_machine.system() == 'darwin' - py_mod = shared_module( - '_gridfire', - sources: py_sources, - dependencies: gridfire_py_deps, - name_prefix: '', - name_suffix: 'so', - install: true, - install_rpath: gridfire_ext_rpath, - install_dir: py_installation.get_install_dir() + '/gridfire' - ) - else - py_mod = py_installation.extension_module( - '_gridfire', - sources: py_sources, - dependencies : gridfire_py_deps, - install : true, - install_rpath: gridfire_ext_rpath, - subdir: 'gridfire', - ) - endif - + py_mod = py_installation.extension_module( + '_gridfire', + sources: py_sources, + dependencies: gridfire_py_deps, + install: true, + link_args: gridfire_ext_rpath_args, + build_rpath: gridfire_ext_rpath, + install_rpath: gridfire_ext_rpath, + subdir: 'gridfire', + ) py_installation.install_sources( files( diff --git a/meson.build b/meson.build index cced545e..16e3cd49 100644 --- a/meson.build +++ b/meson.build @@ -18,7 +18,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # *********************************************************************** # -project('GridFire', ['c', 'cpp'], version: 'v0.7.6rc4.2', default_options: ['cpp_std=c++23'], meson_version: '>=1.5.0') +project('GridFire', ['c', 'cpp'], version: 'v0.7.6rc4.dev2', default_options: ['cpp_std=c++23'], meson_version: '>=1.5.0') gridfire_args = [] diff --git a/pip_install_mac_patch.sh b/pip_install_mac_patch.sh deleted file mode 100755 index 151f4a2f..00000000 --- a/pip_install_mac_patch.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/bin/bash -# pip_install_mac_patch.sh - Workaround for meson-python duplicate RPATH bug on macOS - -set -e - -# Color codes for output -RED='\033[0;31m' -YELLOW='\033[1;33m' -GREEN='\033[0;32m' -NC='\033[0m' # No Color - -# Returns 0 if the Mach-O binary at $1 has duplicate LC_RPATH entries. -has_duplicate_rpaths() { - local binary="$1" - local rpaths dup - rpaths=$(otool -l "$binary" | awk '/cmd LC_RPATH/{getline; getline; print $2}') - dup=$(printf '%s\n' "$rpaths" | sort | uniq -d) - [ -n "$dup" ] -} - -echo -e "${YELLOW}" -echo "=========================================================================" -echo " INSTALLATION + DUPLICATE-RPATH SAFETY NET (macOS)" -echo "=========================================================================" -echo -e "${NC}" -echo "" -echo "This script installs gridfire with pip and then checks the installed" -echo "extension modules for duplicate LC_RPATH entries (a meson-python bug" -echo "exposed by macOS 26.1, see:" -echo " https://github.com/mesonbuild/meson-python/issues/813 )." -echo "" -echo "With the current self-contained wheel layout the bug should not" -echo "trigger; binaries are only patched if duplicates are actually found." -echo "" -echo -e "${YELLOW}Continue? [y/N]${NC} " -read -r response - -if [[ ! "$response" =~ ^[Yy]$ ]]; then - echo -e "${RED}Installation cancelled.${NC}" - exit 1 -fi - -echo "" -echo -e "${GREEN}Step 1: Finding current Python environment...${NC}" - -PYTHON_BIN=$(which python3) -if [ -z "$PYTHON_BIN" ]; then - echo -e "${RED}Error: python3 not found in PATH${NC}" - exit 1 -fi - -echo "Using Python: $PYTHON_BIN" -PYTHON_VERSION=$($PYTHON_BIN --version) -echo "Python version: $PYTHON_VERSION" - -SITE_PACKAGES=$($PYTHON_BIN -c "import site; print(site.getsitepackages()[0])") -echo "Site packages: $SITE_PACKAGES" -echo "" - -echo -e "${GREEN}Step 2: Installing gridfire with pip...${NC}" -$PYTHON_BIN -m pip install . -v --no-build-isolation - -if [ $? -ne 0 ]; then - echo -e "${RED}Error: pip install failed${NC}" - exit 1 -fi -echo "" - -FIX_SCRIPT="build-python/fix_rpaths.py" - -check_and_fix() { - local label="$1" so_file="$2" - - if [ -z "$so_file" ]; then - echo -e "${YELLOW}Skipping ${label}: extension module not found (package may not be installed).${NC}" - return 0 - fi - - echo "Found ${label} extension module: $so_file" - if has_duplicate_rpaths "$so_file"; then - echo -e "${YELLOW}Duplicate LC_RPATH entries detected in ${label}; applying fix...${NC}" - if [ ! -f "$FIX_SCRIPT" ]; then - echo -e "${RED}Error: $FIX_SCRIPT not found${NC}" - echo "Please run this script from the project root directory." - exit 1 - fi - $PYTHON_BIN "$FIX_SCRIPT" "$so_file" - else - echo -e "${GREEN}No duplicate LC_RPATH entries in ${label}; no patch needed.${NC}" - fi - echo "" -} - -echo -e "${GREEN}Step 3: Checking installed extension modules...${NC}" - -GRIDFIRE_SO=$(find "$SITE_PACKAGES/gridfire" -name "_gridfire.cpython-*-darwin.so" 2>/dev/null | head -n 1) -check_and_fix "gridfire" "$GRIDFIRE_SO" - -FOURDST_SO=$(find "$SITE_PACKAGES/fourdst" -name "_phys.cpython-*-darwin.so" 2>/dev/null | head -n 1) -check_and_fix "fourdst" "$FOURDST_SO" - -echo -e "${GREEN}=========================================================================${NC}" -echo -e "${GREEN} Installation Complete!${NC}" -echo -e "${GREEN}=========================================================================${NC}" -echo "" -echo "Test the installation with:" -echo " $PYTHON_BIN -c 'import gridfire; print(gridfire.__version__)'" -echo "" diff --git a/pyproject.toml b/pyproject.toml index fc2e428f..88607da9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,10 @@ [build-system] -requires = [ - "meson-python>=0.19.0", - "meson>=1.9.1", - "pybind11>=2.10" -] +requires = ["meson-python>=0.19.0", "meson>=1.9.1", "pybind11==3.0.0", "fourdst==0.10.5"] build-backend = "mesonpy" [project] name = "gridfire" -version = "v0.7.6rc4.2" +dynamic = ["version"] description = "Python interface to the GridFire nuclear network code" readme = "README.md" license = { file = "LICENSE.txt" } @@ -21,6 +17,8 @@ maintainers = [ {name = "Emily M. Boudreaux", email = "emily@boudreauxmail.com"} ] +dependencies = ["fourdst==0.10.5"] + [tool.meson-python.args] setup = [ '-Ddefault_library=static', diff --git a/src/include/gridfire/engine/scratchpads/engine_multiscale_scratchpad.h b/src/include/gridfire/engine/scratchpads/engine_multiscale_scratchpad.h index 4ebf5956..f1058908 100644 --- a/src/include/gridfire/engine/scratchpads/engine_multiscale_scratchpad.h +++ b/src/include/gridfire/engine/scratchpads/engine_multiscale_scratchpad.h @@ -130,7 +130,7 @@ struct MultiscalePartitioningEngineViewScratchPad final : AbstractScratchPad { * @brief Check whether the scratchpad has been initialized. * @return true if initialized with a valid SUNContext, false otherwise. */ - [[nodiscard]] bool is_initialized() const override { return has_initialized; } + [[nodiscard]] bool is_initialized() const override; /** * @brief Initialize the scratchpad by creating a SUNDIALS context. @@ -150,16 +150,7 @@ struct MultiscalePartitioningEngineViewScratchPad final : AbstractScratchPad { * SUNContext ctx = scratch.sun_ctx; * @endcode */ - void initialize() { - if (has_initialized) return; - - const int flag = SUNContext_Create(SUN_COMM_NULL, &sun_ctx); - if (flag != 0) { - throw std::runtime_error("Failed to create SUNContext in MultiscalePartitioningEngineViewScratchPad."); - } - - has_initialized = true; - } + void initialize(); /** * @brief Destructor that properly releases SUNDIALS resources. @@ -167,13 +158,7 @@ struct MultiscalePartitioningEngineViewScratchPad final : AbstractScratchPad { * Clears all QSE solvers before freeing the SUNContext to ensure * proper cleanup order and avoid dangling references. */ - ~MultiscalePartitioningEngineViewScratchPad() override { - qse_solvers.clear(); - if (sun_ctx != nullptr) { - SUNContext_Free(&sun_ctx); - sun_ctx = nullptr; - } - } + ~MultiscalePartitioningEngineViewScratchPad() override; /** * @brief Create a partial copy of this scratchpad. @@ -196,20 +181,7 @@ struct MultiscalePartitioningEngineViewScratchPad final : AbstractScratchPad { * * @endcode */ - [[nodiscard]] std::unique_ptr clone() const override { - auto clone_pad = std::make_unique(); - clone_pad->qse_groups = this->qse_groups; - clone_pad->dynamic_species = this->dynamic_species; - clone_pad->algebraic_species = this->algebraic_species; - clone_pad->composition_cache = this->composition_cache; - - clone_pad->initialize(); - clone_pad->qse_solvers.reserve(this->qse_solvers.size()); - for (const auto& solver : qse_solvers) { - clone_pad->qse_solvers.push_back(solver->clone(clone_pad->sun_ctx)); // Must rebind context to new SUNContext - } - return clone_pad; - } + [[nodiscard]] std::unique_ptr clone() const override; }; } // namespace gridfire::engine::scratch diff --git a/src/lib/engine/scratchpads/engine_multiscale_scratchpad.cpp b/src/lib/engine/scratchpads/engine_multiscale_scratchpad.cpp new file mode 100644 index 00000000..a20b50b9 --- /dev/null +++ b/src/lib/engine/scratchpads/engine_multiscale_scratchpad.cpp @@ -0,0 +1,52 @@ +#include "gridfire/engine/views/engine_multiscale.h" +#include "gridfire/engine/scratchpads/scratchpad_abstract.h" +#include "gridfire/engine/scratchpads/types.h" +#include "gridfire/engine/scratchpads/engine_multiscale_scratchpad.h" + +#include "fourdst/atomic/atomicSpecies.h" + +#include +#include +#include + +#include "sundials/sundials_context.h" + + +namespace gridfire::engine::scratch { + bool MultiscalePartitioningEngineViewScratchPad::is_initialized() const{ return has_initialized; } + + void MultiscalePartitioningEngineViewScratchPad::initialize() { + if (has_initialized) return; + + const int flag = SUNContext_Create(SUN_COMM_NULL, &sun_ctx); + if (flag != 0) { + throw std::runtime_error("Failed to create SUNContext in MultiscalePartitioningEngineViewScratchPad."); + } + + has_initialized = true; + } + + MultiscalePartitioningEngineViewScratchPad::~MultiscalePartitioningEngineViewScratchPad() { + qse_solvers.clear(); + if (sun_ctx != nullptr) { + SUNContext_Free(&sun_ctx); + sun_ctx = nullptr; + } + } + + std::unique_ptr MultiscalePartitioningEngineViewScratchPad::clone() const { + auto clone_pad = std::make_unique(); + clone_pad->qse_groups = this->qse_groups; + clone_pad->dynamic_species = this->dynamic_species; + clone_pad->algebraic_species = this->algebraic_species; + clone_pad->composition_cache = this->composition_cache; + + clone_pad->initialize(); + clone_pad->qse_solvers.reserve(this->qse_solvers.size()); + for (const auto& solver : qse_solvers) { + clone_pad->qse_solvers.push_back(solver->clone(clone_pad->sun_ctx)); // Must rebind context to new SUNContext + } + return clone_pad; + } + +} \ No newline at end of file diff --git a/src/meson.build b/src/meson.build index e9b89811..fbf0005d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -12,6 +12,7 @@ gridfire_sources = files( 'lib/engine/procedures/construction.cpp', 'lib/engine/diagnostics/dynamic_engine_diagnostics.cpp', 'lib/engine/types/jacobian.cpp', + 'lib/engine/scratchpads/engine_multiscale_scratchpad.cpp', 'lib/reaction/reaction.cpp', 'lib/reaction/reaclib.cpp', 'lib/reaction/weak/weak.cpp', @@ -67,39 +68,97 @@ gridfire_link_args = cpp.get_supported_link_arguments( ) if get_option('build_python') - gridfire_link_whole += [libcomposition, libconst, liblogging] if get_option('plugin_support') - gridfire_link_whole += [libplugin] + error('plugin_support is not available when build_python=true: ' + + 'the fourdst wheel does not ship libplugin, and bundling it ' + + 'would break cross-package type compatibility.') endif + + # ---- rpaths --------------------------------------------------------- + # Wheel layout (both packages share one site-packages): + # /gridfire/_gridfire..so + # /gridfire/lib/libgridfire.{so,dylib} + # /fourdst/lib/lib{composition,logging,const}.* + # /fourdst/lib/vendor/libreflect_cpp.* + # + # libgridfire (in gridfire/lib/) needs: itself-dir, ../../fourdst/lib{,/vendor} + # _gridfire (in gridfire/) needs: ./lib, ../fourdst/lib{,/vendor} + # + # Platform split: + # * macOS / Mach-O: LC_RPATH cannot hold colon-separated lists, + # meson carries a user-supplied build_rpath as a single string, and + # install_rpath is only applied by `meson install` (which + # meson-python never runs). So on darwin we emit one raw + # `-Wl,-rpath,` link arg per path, and deliberately do NOT + # set build_rpath/install_rpath (a real `meson install` would then + # add install_rpath on top of the link args, producing duplicate + # LC_RPATH entries, which macOS 26+ dyld treats as a hard load + # error). + # * Linux / ELF: colon-joined DT_RUNPATH is the native format, and + # install_rpath is baked into the binary at link time, so the + # kwargs work for both meson-python wheels and `meson install`. + # + # NOTE: compose paths with '+', never the '/' join operator — + # 'x' / '/y' silently discards 'x' in Meson. + # + # gridfire_ext_rpath_args / gridfire_ext_rpath are consumed later by + # build-python/meson.build for the _gridfire extension module (this + # subdir is processed before build-python in the root meson.build). + if host_machine.system() == 'darwin' + gridfire_lib_rpath_args = [ + '-Wl,-rpath,@loader_path', + '-Wl,-rpath,@loader_path/../../fourdst/lib', + '-Wl,-rpath,@loader_path/../../fourdst/lib/vendor', + ] + gridfire_lib_rpath = '' + + gridfire_ext_rpath_args = [ + '-Wl,-rpath,@loader_path/lib', + '-Wl,-rpath,@loader_path/../fourdst/lib', + '-Wl,-rpath,@loader_path/../fourdst/lib/vendor', + ] + gridfire_ext_rpath = '' + else + gridfire_lib_rpath_args = [] + gridfire_lib_rpath = '$ORIGIN:' + '$ORIGIN/../../fourdst/lib:' + '$ORIGIN/../../fourdst/lib/vendor' + + gridfire_ext_rpath_args = [] + gridfire_ext_rpath = '$ORIGIN/lib:' + '$ORIGIN/../fourdst/lib:' + '$ORIGIN/../fourdst/lib/vendor' + endif + libgridfire = shared_library('gridfire', - gridfire_sources, - include_directories: include_directories('include'), - dependencies: gridfire_build_dependencies, - cpp_args: gridfire_args, - link_whole: gridfire_link_whole, - link_args: gridfire_link_args, - install: true, - install_dir: gridfire_libdir) + gridfire_sources, + include_directories: include_directories('include'), + dependencies: gridfire_build_dependencies, + cpp_args: gridfire_args, + link_whole: gridfire_link_whole, + link_args: gridfire_link_args + gridfire_lib_rpath_args, + install: true, + install_dir: gridfire_libdir, + build_rpath: gridfire_lib_rpath, + install_rpath: gridfire_lib_rpath, + ) else libgridfire = library('gridfire', - gridfire_sources, - include_directories: include_directories('include'), - dependencies: gridfire_build_dependencies, - link_whole: gridfire_link_whole, - link_args: gridfire_link_args, - cpp_args: gridfire_args, - install : true) + gridfire_sources, + include_directories: include_directories('include'), + dependencies: gridfire_build_dependencies, + link_whole: gridfire_link_whole, + link_args: gridfire_link_args, + cpp_args: gridfire_args, + install : true + ) endif if get_option('build_python') - gridfire_iface_deps = [] - foreach d : gridfire_build_dependencies - gridfire_iface_deps += d.partial_dependency(compile_args: true, includes: true) - endforeach + gridfire_iface_dep = declare_dependency( + dependencies: gridfire_build_dependencies, + ).partial_dependency(compile_args: true, includes: true) + gridfire_dep = declare_dependency( include_directories: include_directories('include'), link_with: libgridfire, - dependencies: gridfire_iface_deps, + dependencies: [gridfire_iface_dep], compile_args: gridfire_args, ) else @@ -114,8 +173,8 @@ endif meson.override_dependency('gridfire', gridfire_dep) install_subdir('include/gridfire', - install_dir: gridfire_includedir, - exclude_files: ['utils/config.h.in'], + install_dir: gridfire_includedir, + exclude_files: ['utils/config.h.in'], ) @@ -126,4 +185,3 @@ if get_option('build_c_api') message('Configuring C API...') subdir('extern') endif - diff --git a/src/python/gridfire/__init__.py b/src/python/gridfire/__init__.py index b0d38784..ac9bfefe 100644 --- a/src/python/gridfire/__init__.py +++ b/src/python/gridfire/__init__.py @@ -1,3 +1,13 @@ +try: + import fourdst as fst +except ImportError as e: + raise ImportError( + "gridfire requires the fourdst package (its C++ types and shared " + "libraries come from there). pip install fourdst." + ) from e + +from ._gridfire import * + from ._gridfire import * import sys @@ -70,4 +80,49 @@ def gf_credits(): "Emily M. Boudreaux - Lead Developer", "Aaron Dotter - Co-Developer", "4D-STAR Collaboration - Contributors" - ] \ No newline at end of file + ] + +import os +from pathlib import Path +from typing import List + +_PACKAGE_DIR = Path(__file__).resolve().parent +def gf_get_include_dirs(): + return [ + os.fspath(_PACKAGE_DIR / "include"), + os.fspath(_PACKAGE_DIR / "include" / "gridfire" / "vendor"), + ] + +def gf_get_lib_dirs(): + return [ + os.fspath(_PACKAGE_DIR / "lib"), + ] + +def gf_get_rpath_flags() -> List[str]: + return ["-Wl,-rpath," + os.fspath(_PACKAGE_DIR / "lib")] + + +def gf_get_lib_flags() -> List[str]: + flags = ["-L" + d for d in gf_get_lib_dirs()] + flags += ["-lgridfire"] + return flags + +def gf_get_include_flags() -> List[str]: + return ["-I" + d for d in gf_get_include_dirs()] + +def gf_get_extra_flags() -> List[str]: + return ['--std=c++23', '-fPIC'] +def gf_compiler_flags(just_gridfire=False): + flags = [] + if not just_gridfire: + flags.extend(fourdst_flags = fst.get_compiler_flags()) + flags.extend(gf_get_rpath_flags()) + flags.extend(gf_get_lib_flags()) + flags.extend(gf_get_include_flags()) + flags.extend(gf_get_extra_flags()) + return flags + +def gf_get_compiler_flags_formatted(just_gridfire=False) -> int: + flags = gf_compiler_flags(just_gridfire) + print(" ".join(flags)) + return 0 diff --git a/src/python/solver/bindings.cpp b/src/python/solver/bindings.cpp index 0f4539c9..9dac18d0 100644 --- a/src/python/solver/bindings.cpp +++ b/src/python/solver/bindings.cpp @@ -17,14 +17,6 @@ namespace py = pybind11; void register_solver_bindings(const py::module &m) { auto py_cvode_timestep_context = py::class_(m, "PointSolverTimestepContext"); py_cvode_timestep_context.def_readonly("t", &gridfire::solver::PointSolverTimestepContext::t); - py_cvode_timestep_context.def_property_readonly( - "state", - [](const gridfire::solver::PointSolverTimestepContext& self) -> std::vector { - const sunrealtype* nvec_data = N_VGetArrayPointer(self.state); - const sunindextype length = N_VGetLength(self.state); - return {nvec_data, nvec_data + length}; - } - ); py_cvode_timestep_context.def_readonly("dt", &gridfire::solver::PointSolverTimestepContext::dt); py_cvode_timestep_context.def_readonly("last_step_time", &gridfire::solver::PointSolverTimestepContext::last_step_time); py_cvode_timestep_context.def_readonly("T9", &gridfire::solver::PointSolverTimestepContext::T9); @@ -57,6 +49,20 @@ void register_solver_bindings(const py::module &m) { return self.getPhysicalComposition(); } ); + py_cvode_timestep_context.def_property_readonly( + "rawState", + [](const gridfire::solver::PointSolverTimestepContext& self) -> std::vector { + const std::span s = self.rawState(); + return std::vector(s.begin(), s.end()); + } + ); + py_cvode_timestep_context.def("abundance", + py::overload_cast(&gridfire::solver::PointSolverTimestepContext::abundance, py::const_), + py::arg("species_index")); + py_cvode_timestep_context.def("abundance", + py::overload_cast(&gridfire::solver::PointSolverTimestepContext::abundance, py::const_), + py::arg("species")); + py_cvode_timestep_context.def_property_readonly("accumulatedSpecificEnergy", &gridfire::solver::PointSolverTimestepContext::accumulatedSpecificEnergy); diff --git a/subprojects/fourdst.wrap b/subprojects/fourdst.wrap index cdfbbf1d..926e2766 100644 --- a/subprojects/fourdst.wrap +++ b/subprojects/fourdst.wrap @@ -1,4 +1,4 @@ [wrap-git] url = https://github.com/4D-STAR/fourdst -revision = v0.10.2 +revision = v0.10.6 depth = 1 diff --git a/tests/meson.build b/tests/meson.build index 5f17b7ab..4b0e9cf9 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -4,5 +4,9 @@ #gtest_nomain_dep = dependency('gtest', main: false, required : true) # Subdirectories for unit and integration tests -subdir('graphnet_sandbox') -subdir('extern') +if get_option('build_tests') and not get_option('build_python') + subdir('graphnet_sandbox') + subdir('extern') +else + message('Tests disabled by build options! To enable them build with build_tests = true and build_python=false') +endif diff --git a/utils/wheels/build-wheels-linux_aarch64.sh b/utils/wheels/build-wheels-linux_aarch64.sh index 74235635..b2194b08 100755 --- a/utils/wheels/build-wheels-linux_aarch64.sh +++ b/utils/wheels/build-wheels-linux_aarch64.sh @@ -1,12 +1,15 @@ #!/usr/bin/env bash set -euo pipefail -if [[ $# -ne 1 ]]; then - echo "Usage: $0 " +if [[ $# -lt 1 ]]; then + echo "Usage: $0 [fourdst-wheels-dir]" + echo " fourdst-wheels-dir: optional local directory of fourdst wheels to" + echo " install from instead of PyPI (for bootstrapping a new fourdst+gridfire pair)" exit 1 fi REPO_URL="$1" +LOCAL_FOURDST_WHEELS="${2:-}" WORK_DIR="$(pwd)" WHEEL_DIR="${WORK_DIR}/wheels_linux_aarch64" @@ -17,21 +20,70 @@ TMPDIR="$(mktemp -d)" echo "➤ Cloning ${REPO_URL} → ${TMPDIR}/project" git clone "${REPO_URL}" "${TMPDIR}/project" +DOCKER_MOUNTS=(-v "${WHEEL_DIR}":/io/wheels -v "${TMPDIR}/project":/io/project) +if [[ -n "${LOCAL_FOURDST_WHEELS}" ]]; then + DOCKER_MOUNTS+=(-v "${LOCAL_FOURDST_WHEELS}":/io/fourdst-wheels) +fi + for IMAGE in \ tboudreaux/manylinux_2_28_aarch64_boost_1_88_0:latest do docker run --rm \ - -v "${WHEEL_DIR}":/io/wheels \ - -v "${TMPDIR}/project":/io/project \ + "${DOCKER_MOUNTS[@]}" \ "${IMAGE}" \ /bin/bash -eux -c ' cd /io/project + + # Does this project link against the fourdst wheel? Single source of + # truth: the pin in pyproject.toml. + FOURDST_PIN="$(grep -oE "fourdst==[0-9][0-9a-zA-Z.]*" pyproject.toml | head -n1 || true)" + + # If a local fourdst wheel dir was mounted, let pip (including the + # isolated build env) resolve fourdst from it. + if [ -d /io/fourdst-wheels ]; then + export PIP_FIND_LINKS=/io/fourdst-wheels + fi + for PY in /opt/python/*/bin/python; do "$PY" -m pip install --upgrade pip setuptools wheel meson meson-python - CC=clang CXX=clang++ "$PY" -m pip wheel . --config-settings=setup-args=-Dunity=on -w /io/wheels -vv - auditwheel repair /io/wheels/*.whl -w /io/wheels + + # Build into a per-iteration temp dir so we repair exactly the + # wheel we just built (the old glob re-repaired every accumulated + # wheel on every loop iteration). + BUILD_WHEEL_DIR="$(mktemp -d)" + CC=clang CXX=clang++ "$PY" -m pip wheel . \ + --config-settings=setup-args=-Dunity=on \ + -w "$BUILD_WHEEL_DIR" -vv + + CURRENT_WHEEL="$(find "$BUILD_WHEEL_DIR" -name "*.whl" | head -n1)" + + if [ -n "$FOURDST_PIN" ]; then + # Install fourdst for THIS interpreter so auditwheel can resolve + # the libraries it must NOT graft. Excluding them keeps fourdst a + # runtime dependency: grafting copies would break cross-package + # pybind11 type compatibility. + "$PY" -m pip install --force-reinstall "$FOURDST_PIN" + FOURDST_LIB_PATH="$("$PY" -c "import fourdst, os; print(os.pathsep.join(fourdst.get_lib_dirs()))")" + LD_LIBRARY_PATH="$FOURDST_LIB_PATH" auditwheel repair \ + --exclude "libcomposition.so*" \ + --exclude "liblogging.so*" \ + --exclude "libconst.so*" \ + --exclude "libreflect_cpp.so*" \ + -w /io/wheels "$CURRENT_WHEEL" + + # Post-repair sanity check: no vendored fourdst libs. + REPAIRED="$(ls -t /io/wheels/*.whl | head -n1)" + if unzip -l "$REPAIRED" | grep -E "libcomposition|liblogging|libconst[^a-z]|libreflect_cpp"; then + echo "ERROR: repaired wheel contains vendored fourdst libraries" + exit 1 + fi + else + auditwheel repair -w /io/wheels "$CURRENT_WHEEL" + fi + + rm -rf "$BUILD_WHEEL_DIR" done echo "✅ Linux wheels ready in /io/wheels" ' -done \ No newline at end of file +done diff --git a/utils/wheels/build-wheels-linux_x86_64.sh b/utils/wheels/build-wheels-linux_x86_64.sh index 6dc3d8b0..1f1dfd13 100755 --- a/utils/wheels/build-wheels-linux_x86_64.sh +++ b/utils/wheels/build-wheels-linux_x86_64.sh @@ -1,12 +1,15 @@ #!/usr/bin/env bash set -euo pipefail -if [[ $# -ne 1 ]]; then - echo "Usage: $0 " +if [[ $# -lt 1 ]]; then + echo "Usage: $0 [fourdst-wheels-dir]" + echo " fourdst-wheels-dir: optional local directory of fourdst wheels to" + echo " install from instead of PyPI (for bootstrapping a new fourdst+gridfire pair)" exit 1 fi REPO_URL="$1" +LOCAL_FOURDST_WHEELS="${2:-}" WORK_DIR="$(pwd)" WHEEL_DIR="${WORK_DIR}/wheels_linux_x86_64" @@ -17,19 +20,68 @@ TMPDIR="$(mktemp -d)" echo "➤ Cloning ${REPO_URL} → ${TMPDIR}/project" git clone "${REPO_URL}" "${TMPDIR}/project" +DOCKER_MOUNTS=(-v "${WHEEL_DIR}":/io/wheels -v "${TMPDIR}/project":/io/project) +if [[ -n "${LOCAL_FOURDST_WHEELS}" ]]; then + DOCKER_MOUNTS+=(-v "${LOCAL_FOURDST_WHEELS}":/io/fourdst-wheels) +fi + for IMAGE in \ tboudreaux/manylinux_2_28_x86_64_boost_1_88_0:latest do docker run --rm \ - -v "${WHEEL_DIR}":/io/wheels \ - -v "${TMPDIR}/project":/io/project \ + "${DOCKER_MOUNTS[@]}" \ "${IMAGE}" \ /bin/bash -eux -c ' cd /io/project + + # Does this project link against the fourdst wheel? Single source of + # truth: the pin in pyproject.toml. + FOURDST_PIN="$(grep -oE "fourdst==[0-9][0-9a-zA-Z.]*" pyproject.toml | head -n1 || true)" + + # If a local fourdst wheel dir was mounted, let pip (including the + # isolated build env) resolve fourdst from it. + if [ -d /io/fourdst-wheels ]; then + export PIP_FIND_LINKS=/io/fourdst-wheels + fi + for PY in /opt/python/*/bin/python; do "$PY" -m pip install --upgrade pip setuptools wheel meson meson-python - CC=clang CXX=clang++ "$PY" -m pip wheel . --config-settings=setup-args=-Dunity=on -w /io/wheels -vv - auditwheel repair /io/wheels/*.whl -w /io/wheels + + # Build into a per-iteration temp dir so we repair exactly the + # wheel we just built (the old glob re-repaired every accumulated + # wheel on every loop iteration). + BUILD_WHEEL_DIR="$(mktemp -d)" + CC=clang CXX=clang++ "$PY" -m pip wheel . \ + --config-settings=setup-args=-Dunity=on \ + -w "$BUILD_WHEEL_DIR" -vv + + CURRENT_WHEEL="$(find "$BUILD_WHEEL_DIR" -name "*.whl" | head -n1)" + + if [ -n "$FOURDST_PIN" ]; then + # Install fourdst for THIS interpreter so auditwheel can resolve + # the libraries it must NOT graft. Excluding them keeps fourdst a + # runtime dependency: grafting copies would break cross-package + # pybind11 type compatibility. + "$PY" -m pip install --force-reinstall "$FOURDST_PIN" + FOURDST_LIB_PATH="$("$PY" -c "import fourdst, os; print(os.pathsep.join(fourdst.get_lib_dirs()))")" + LD_LIBRARY_PATH="$FOURDST_LIB_PATH" auditwheel repair \ + --exclude "libcomposition.so*" \ + --exclude "liblogging.so*" \ + --exclude "libconst.so*" \ + --exclude "libreflect_cpp.so*" \ + -w /io/wheels "$CURRENT_WHEEL" + + # Post-repair sanity check: no vendored fourdst libs. + REPAIRED="$(ls -t /io/wheels/*.whl | head -n1)" + if unzip -l "$REPAIRED" | grep -E "libcomposition|liblogging|libconst[^a-z]|libreflect_cpp"; then + echo "ERROR: repaired wheel contains vendored fourdst libraries" + exit 1 + fi + else + auditwheel repair -w /io/wheels "$CURRENT_WHEEL" + fi + + rm -rf "$BUILD_WHEEL_DIR" done echo "✅ Linux wheels ready in /io/wheels" diff --git a/utils/wheels/build-wheels-macos_aarch64.sh b/utils/wheels/build-wheels-macos_aarch64.sh index 5c92ead6..5306f1cf 100755 --- a/utils/wheels/build-wheels-macos_aarch64.sh +++ b/utils/wheels/build-wheels-macos_aarch64.sh @@ -7,17 +7,19 @@ if [[ $(uname -m) != "arm64" ]]; then exit 1 fi -if [[ $# -ne 1 ]]; then - echo "Usage: $0 " +if [[ $# -lt 1 ]]; then + echo "Usage: $0 [fourdst-wheels-dir]" + echo " fourdst-wheels-dir: optional local directory of fourdst wheels to" + echo " install from instead of PyPI (for bootstrapping a new fourdst+gridfire pair)" exit 1 fi # 2. Setup Directories REPO_URL="$1" +LOCAL_FOURDST_WHEELS="${2:-}" WORK_DIR="$(pwd)" WHEEL_DIR="${WORK_DIR}/wheels_macos_aarch64_tmp" FINAL_WHEEL_DIR="${WORK_DIR}/wheels_macos_aarch64" -RPATH_SCRIPT="${WORK_DIR}/../../build-python/fix_rpaths.py" # Assumes script is in this location relative to execution echo "➤ Creating wheel output directories" mkdir -p "${WHEEL_DIR}" @@ -29,8 +31,17 @@ git clone --depth 1 "${REPO_URL}" "${TMPDIR}/project" cd "${TMPDIR}/project" # 3. Build Configuration +# NOTE: must match the value used to build the fourdst wheels — the two +# packages are one ABI unit. export MACOSX_DEPLOYMENT_TARGET=15.0 +# Does this project link against the fourdst wheel? Derive the pin from +# pyproject.toml so there is a single source of truth. +FOURDST_PIN="$(grep -oE 'fourdst==[0-9][0-9a-zA-Z.]*' pyproject.toml | head -n1 || true)" +if [[ -n "${FOURDST_PIN}" ]]; then + echo "➤ Project depends on ${FOURDST_PIN}; wheel repair will exclude fourdst libraries" +fi + PYTHON_VERSIONS=("3.9.23" "3.10.18" "3.11.13" "3.12.11" "3.13.5" "3.13.5t" "3.14.0rc1" "3.14.0rc1t" 'pypy3.10-7.3.19' "pypy3.11-7.3.20") if ! command -v pyenv &> /dev/null; then @@ -46,27 +57,60 @@ for PY_VERSION in "${PYTHON_VERSIONS[@]}"; do pyenv shell "${PY_VERSION}" PY="$(pyenv which python)" - + echo "----------------------------------------------------------------" echo "➤ Building for $($PY --version) on macOS arm64" echo "----------------------------------------------------------------" - # Install build deps explicitly so we can skip build isolation + # Install build deps explicitly so we can skip build isolation. + # IMPORTANT: with --no-build-isolation, EVERYTHING in + # build-system.requires must be installed here by hand — including + # fourdst, otherwise the meson probe (`import fourdst`) fails. "$PY" -m pip install --upgrade pip setuptools wheel meson-python delocate "$PY" -m pip install meson==1.9.1 + if [[ -n "${FOURDST_PIN}" ]]; then + if [[ -n "${LOCAL_FOURDST_WHEELS}" ]]; then + "$PY" -m pip install --force-reinstall \ + --find-links "${LOCAL_FOURDST_WHEELS}" "${FOURDST_PIN}" + else + "$PY" -m pip install --force-reinstall "${FOURDST_PIN}" + fi + fi + echo "➤ Building wheel with ccache enabled" echo "➤ Found meson version $(meson --version)" - # for every single build, saving significant I/O and network time. - CC="ccache clang" CXX="ccache clang++" "$PY" -m pip wheel . --no-build-isolation -w "${WHEEL_DIR}" -v + CC="ccache clang" CXX="ccache clang++" \ + "$PY" -m pip wheel . --no-build-isolation -w "${WHEEL_DIR}" -v # We expect exactly one new wheel in the tmp dir per iteration CURRENT_WHEEL=$(find "${WHEEL_DIR}" -name "*.whl" | head -n 1) echo "➤ Repairing wheel with delocate" - # Delocate moves the repaired wheel to FINAL_WHEEL_DIR - delocate-wheel -w "${FINAL_WHEEL_DIR}" "$CURRENT_WHEEL" + if [[ -n "${FOURDST_PIN}" ]]; then + # Resolve @rpath references against the installed fourdst wheel, + # but EXCLUDE its libraries from being grafted into this wheel: + # they must stay a runtime dependency, or cross-package pybind11 + # type compatibility breaks. + FOURDST_LIB_PATH="$("$PY" -c 'import fourdst, os; print(os.pathsep.join(fourdst.get_lib_dirs()))')" + DYLD_LIBRARY_PATH="${FOURDST_LIB_PATH}" \ + delocate-wheel --require-archs arm64 \ + -e composition -e logging -e const -e reflect_cpp \ + -w "${FINAL_WHEEL_DIR}" -v "$CURRENT_WHEEL" + else + delocate-wheel --require-archs arm64 -w "${FINAL_WHEEL_DIR}" -v "$CURRENT_WHEEL" + fi + + # Post-repair sanity check: import the wheel in a throwaway env and + # make sure no fourdst library snuck back in. + if [[ -n "${FOURDST_PIN}" ]]; then + REPAIRED_WHEEL=$(find "${FINAL_WHEEL_DIR}" -name "*.whl" -newer "$CURRENT_WHEEL" | head -n 1) + if [[ -n "${REPAIRED_WHEEL}" ]] && unzip -l "${REPAIRED_WHEEL}" | grep -E 'libcomposition|liblogging|libconst|libreflect_cpp' ; then + echo "ERROR: repaired wheel contains vendored fourdst libraries" + exit 1 + fi + fi # Clean up the intermediate wheel from this iteration so it doesn't confuse the next rm "$CURRENT_WHEEL" @@ -77,4 +121,4 @@ done rm -rf "${TMPDIR}" rm -rf "${WHEEL_DIR}" -echo "✅ All builds complete. Artifacts in ${FINAL_WHEEL_DIR}" +echo "✅ All builds complete. Artifacts in ${FINAL_WHEEL_DIR}" \ No newline at end of file