From 6a54f2625eea19467336184b95ebacaa58c697b7 Mon Sep 17 00:00:00 2001 From: Emily Boudreaux Date: Sat, 5 Sep 2026 07:11:05 -0400 Subject: [PATCH] feat(initial): added portable mfem build system --- .gitignore | 13 + build-check/meson.build | 18 + build-config/meson.build | 2 + build-config/mfem/meson.build | 651 +++++ build-python/meson.build | 41 + cross/wasm32.ini | 21 + examples/meson.build | 35 + examples/parallel_hypre.cpp | 80 + examples/serial_poisson.cpp | 81 + include/meson_mfem_template/config.hpp.in | 261 ++ include/meson_mfem_template/meson.build | 19 + meson.build | 58 + meson_options.txt | 66 + pyproject.toml | 41 + python/meson_mfem_template/__init__.py | 46 + python/meson_mfem_template/_core.pyi | 4 + python/meson_mfem_template/py.typed | 1 + src/python/bindings.cpp | 91 + subprojects/.wraplock | 0 subprojects/algoim.wrap | 10 + subprojects/blitz.wrap | 10 + subprojects/fms.wrap | 10 + subprojects/gslib.wrap | 7 + subprojects/hypre.wrap | 7 + subprojects/libceed.wrap | 10 + subprojects/metis.wrap | 7 + subprojects/mfem.wrap | 7 + subprojects/mpich.wrap | 7 + subprojects/nanobind.wrap | 15 + subprojects/packagefiles/algoim/meson.build | 4 + subprojects/packagefiles/blitz/meson.build | 3 + subprojects/packagefiles/fms/meson.build | 3 + subprojects/packagefiles/gslib/meson.build | 3 + subprojects/packagefiles/hypre/meson.build | 3 + subprojects/packagefiles/libceed/meson.build | 4 + subprojects/packagefiles/metis/meson.build | 2 + subprojects/packagefiles/mfem/meson.build | 3 + subprojects/packagefiles/mpich/meson.build | 3 + subprojects/packagefiles/sundials/meson.build | 3 + subprojects/packagefiles/zlib/meson.build | 3 + subprojects/robin-map.wrap | 15 + subprojects/sundials.wrap | 7 + subprojects/zlib.wrap | 7 + tests/meson.build | 86 + tools/build_mfem_bundle.py | 2387 +++++++++++++++++ tools/ceed_jit_source_root_relocatable.c | 51 + tools/install_bundle.py | 39 + tools/run_mpi_test.py | 33 + tools/validate_matrix.sh | 66 + 49 files changed, 4344 insertions(+) create mode 100644 .gitignore create mode 100644 build-check/meson.build create mode 100644 build-config/meson.build create mode 100644 build-config/mfem/meson.build create mode 100644 build-python/meson.build create mode 100644 cross/wasm32.ini create mode 100644 examples/meson.build create mode 100644 examples/parallel_hypre.cpp create mode 100644 examples/serial_poisson.cpp create mode 100644 include/meson_mfem_template/config.hpp.in create mode 100644 include/meson_mfem_template/meson.build create mode 100644 meson.build create mode 100644 meson_options.txt create mode 100644 pyproject.toml create mode 100644 python/meson_mfem_template/__init__.py create mode 100644 python/meson_mfem_template/_core.pyi create mode 100644 python/meson_mfem_template/py.typed create mode 100644 src/python/bindings.cpp create mode 100644 subprojects/.wraplock create mode 100644 subprojects/algoim.wrap create mode 100644 subprojects/blitz.wrap create mode 100644 subprojects/fms.wrap create mode 100644 subprojects/gslib.wrap create mode 100644 subprojects/hypre.wrap create mode 100644 subprojects/libceed.wrap create mode 100644 subprojects/metis.wrap create mode 100644 subprojects/mfem.wrap create mode 100644 subprojects/mpich.wrap create mode 100644 subprojects/nanobind.wrap create mode 100644 subprojects/packagefiles/algoim/meson.build create mode 100644 subprojects/packagefiles/blitz/meson.build create mode 100644 subprojects/packagefiles/fms/meson.build create mode 100644 subprojects/packagefiles/gslib/meson.build create mode 100644 subprojects/packagefiles/hypre/meson.build create mode 100644 subprojects/packagefiles/libceed/meson.build create mode 100644 subprojects/packagefiles/metis/meson.build create mode 100644 subprojects/packagefiles/mfem/meson.build create mode 100644 subprojects/packagefiles/mpich/meson.build create mode 100644 subprojects/packagefiles/sundials/meson.build create mode 100644 subprojects/packagefiles/zlib/meson.build create mode 100644 subprojects/robin-map.wrap create mode 100644 subprojects/sundials.wrap create mode 100644 subprojects/zlib.wrap create mode 100644 tests/meson.build create mode 100644 tools/build_mfem_bundle.py create mode 100644 tools/ceed_jit_source_root_relocatable.c create mode 100644 tools/install_bundle.py create mode 100644 tools/run_mpi_test.py create mode 100755 tools/validate_matrix.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..06b8974 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +/dist/ +/.mesonpy-*/ +/.pytest_cache/ +/.venv/ +__pycache__/ +*.egg-info/ +*.so +*.dylib +*.dll +subprojects/packagecache/ +subprojects/*/ +!subprojects/packagefiles/ + diff --git a/build-check/meson.build b/build-check/meson.build new file mode 100644 index 0000000..e871baf --- /dev/null +++ b/build-check/meson.build @@ -0,0 +1,18 @@ +message('C compiler: ' + cc.get_id()) +message('C++ compiler: ' + cpp.get_id()) + +if not cpp.has_header('memory') or not cpp.compiles(''' + #include + int main() { static_assert(__cplusplus >= 201703L); return 0; } +''', name: 'C++17 language support') + error('MFEM 4.10 requires a C++17-or-newer compiler.') +endif + +cmake_program = find_program('cmake', required: true) +# With meson-python, the generated native file pins this to the interpreter +# building the wheel. For command-line builds, PATH controls the selection. +python_build = python_mod.find_installation(required: true, pure: false) + +if meson.is_cross_build() + message('Cross build: ' + build_machine.system() + ' -> ' + host_machine.system()) +endif diff --git a/build-config/meson.build b/build-config/meson.build new file mode 100644 index 0000000..965242b --- /dev/null +++ b/build-config/meson.build @@ -0,0 +1,2 @@ +subdir('mfem') + diff --git a/build-config/mfem/meson.build b/build-config/mfem/meson.build new file mode 100644 index 0000000..c27a717 --- /dev/null +++ b/build-config/mfem/meson.build @@ -0,0 +1,651 @@ +profile = get_option('feature_profile') +allow_preinstalled = get_option('allow_preinstalled') +dependency_prefix = get_option('dependency_prefix') +wheel_carries_native_bundle = get_option('build_python') and get_option('python_install_native') + + +effective_allow_preinstalled = allow_preinstalled and not wheel_carries_native_bundle +is_wasm = host_machine.system() == 'emscripten' +is_windows = host_machine.system() == 'windows' +can_build_posix_mpi = not meson.is_cross_build() and not is_windows and not is_wasm + +nvcc_program = find_program('nvcc', required: false) +hipcc_program = find_program('hipcc', required: false) +dependency_has_nvcc = dependency_prefix != '' and fs.exists(dependency_prefix / 'bin' / 'nvcc') +dependency_has_hipcc = dependency_prefix != '' and fs.exists(dependency_prefix / 'bin' / 'hipcc') +cuda_toolkit_available = nvcc_program.found() or dependency_has_nvcc +hip_toolkit_available = hipcc_program.found() or dependency_has_hipcc +openmp_dep = dependency('openmp', required: false) + +bundled_compiler_openmp = cpp.get_id() == 'gcc' and openmp_dep.found() +usable_openmp = openmp_dep.found() and ( + effective_allow_preinstalled or dependency_prefix != '' or bundled_compiler_openmp +) +simd_arch_supported = host_machine.cpu_family() in ['x86', 'x86_64'] +if host_machine.cpu_family() in ['aarch64', 'arm'] + simd_arch_supported = cpp.compiles(''' + #ifndef __ARM_FEATURE_SVE + #error SVE is required by MFEM's ARM SIMD specialization + #endif + int main() { return 0; } + ''', name: 'MFEM ARM SVE SIMD support') +elif host_machine.cpu_family() in ['ppc', 'ppc64'] + simd_arch_supported = cpp.compiles(''' + #ifndef __VSX__ + #error VSX is required by MFEM's Power SIMD specialization + #endif + int main() { return 0; } + ''', name: 'MFEM Power VSX SIMD support') +endif + +mfem_feature_names = [ + 'mpi', 'metis', 'exceptions', 'zlib', 'libunwind', 'lapack', + 'thread_safe', 'openmp', 'legacy_openmp', 'memalloc', 'sundials', + 'suitesparse', 'superlu', 'superlu5', 'mumps', 'strumpack', 'cudss', + 'ginkgo', 'amgx', 'magma', 'gnutls', 'gslib', 'hdf5', 'netcdf', + 'petsc', 'slepc', 'mpfr', 'sidre', 'fms', 'conduit', 'pumi', 'hiop', + 'cuda', 'hip', 'occa', 'raja', 'ceed', 'umpire', 'simd', 'adios2', + 'caliper', 'algoim', 'mkl_cpardiso', 'mkl_pardiso', 'adforward', + 'codipack', 'benchmark', 'parelag', 'tribol', 'enzyme', 'moonolith', + 'simmetrix', +] + +mfem_cmake_names = [ + 'MFEM_USE_MPI', 'MFEM_USE_METIS', 'MFEM_USE_EXCEPTIONS', 'MFEM_USE_ZLIB', + 'MFEM_USE_LIBUNWIND', 'MFEM_USE_LAPACK', 'MFEM_THREAD_SAFE', + 'MFEM_USE_OPENMP', 'MFEM_USE_LEGACY_OPENMP', 'MFEM_USE_MEMALLOC', + 'MFEM_USE_SUNDIALS', 'MFEM_USE_SUITESPARSE', 'MFEM_USE_SUPERLU', + 'MFEM_USE_SUPERLU5', 'MFEM_USE_MUMPS', 'MFEM_USE_STRUMPACK', + 'MFEM_USE_CUDSS', 'MFEM_USE_GINKGO', 'MFEM_USE_AMGX', 'MFEM_USE_MAGMA', + 'MFEM_USE_GNUTLS', 'MFEM_USE_GSLIB', 'MFEM_USE_HDF5', 'MFEM_USE_NETCDF', + 'MFEM_USE_PETSC', 'MFEM_USE_SLEPC', 'MFEM_USE_MPFR', 'MFEM_USE_SIDRE', + 'MFEM_USE_FMS', 'MFEM_USE_CONDUIT', 'MFEM_USE_PUMI', 'MFEM_USE_HIOP', + 'MFEM_USE_CUDA', 'MFEM_USE_HIP', 'MFEM_USE_OCCA', 'MFEM_USE_RAJA', + 'MFEM_USE_CEED', 'MFEM_USE_UMPIRE', 'MFEM_USE_SIMD', 'MFEM_USE_ADIOS2', + 'MFEM_USE_CALIPER', 'MFEM_USE_ALGOIM', 'MFEM_USE_MKL_CPARDISO', + 'MFEM_USE_MKL_PARDISO', 'MFEM_USE_ADFORWARD', 'MFEM_USE_CODIPACK', + 'MFEM_USE_BENCHMARK', 'MFEM_USE_PARELAG', 'MFEM_USE_TRIBOL', + 'MFEM_USE_ENZYME', 'MFEM_USE_MOONOLITH', 'MFEM_USE_SIMMETRIX', +] + +source_capable = { + 'mpi': can_build_posix_mpi, + 'metis': not is_windows and not is_wasm, + 'exceptions': true, + 'zlib': true, + 'thread_safe': true, + 'openmp': bundled_compiler_openmp, + 'legacy_openmp': bundled_compiler_openmp, + 'memalloc': true, + 'sundials': not is_windows and not is_wasm, + 'gslib': not is_windows and not is_wasm, + 'fms': not is_windows, + 'ceed': not is_windows, + 'algoim': not meson.is_cross_build() and not is_windows and not is_wasm, + 'cuda': cuda_toolkit_available and not is_wasm and not host_machine.system() == 'darwin', + 'hip': hip_toolkit_available and not is_wasm and not host_machine.system() == 'darwin', + 'simd': simd_arch_supported and not is_wasm, +} + +single_precision_conflicts = [ + 'sundials', 'suitesparse', 'superlu', 'strumpack', 'ginkgo', 'amgx', + 'slepc', 'pumi', 'simmetrix', 'gslib', 'algoim', 'ceed', 'tribol', 'moonolith', +] + +cuda_option = get_option('mfem_cuda') +hip_option = get_option('mfem_hip') +profile_cuda = false +profile_hip = false +if cuda_option.enabled() and hip_option.enabled() + profile_cuda = true + profile_hip = true +elif cuda_option.enabled() + profile_cuda = true +elif hip_option.enabled() + profile_hip = true +else + profile_cuda = cuda_option.auto() and source_capable.get('cuda') + profile_hip = hip_option.auto() and not profile_cuda and source_capable.get('hip') +endif + +portable_defaults = { + 'mpi': can_build_posix_mpi, + 'metis': can_build_posix_mpi, + 'exceptions': true, + 'zlib': true, + 'thread_safe': true, + 'openmp': usable_openmp, + 'memalloc': true, + 'sundials': not is_windows and not is_wasm, + 'gslib': not is_windows and not is_wasm, + 'fms': not is_windows, + 'ceed': not is_windows, + 'algoim': source_capable.get('algoim'), + 'cuda': profile_cuda, + 'hip': profile_hip, + 'simd': simd_arch_supported and not is_wasm, +} + +full_cuda = false +full_hip = false +if profile == 'full' + full_cuda = profile_cuda + full_hip = profile_hip +endif + +mfem_features = {} +mfem_feature_states = [] +foreach feature_name : mfem_feature_names + feature_opt = get_option('mfem_' + feature_name) + feature_on = feature_opt.enabled() + if feature_opt.auto() + if profile == 'portable' + feature_on = portable_defaults.get(feature_name, false) + elif profile == 'full' + feature_on = true + if feature_name == 'legacy_openmp' or feature_name == 'superlu5' + feature_on = false + elif feature_name == 'openmp' + feature_on = usable_openmp + elif feature_name == 'cuda' + feature_on = full_cuda + elif feature_name == 'hip' + feature_on = full_hip + elif feature_name == 'cudss' or feature_name == 'amgx' + feature_on = full_cuda + elif feature_name == 'magma' + feature_on = full_cuda or full_hip + elif feature_name == 'simd' + feature_on = simd_arch_supported and not is_wasm + elif feature_name == 'enzyme' + feature_on = cpp.get_id() == 'clang' and not full_cuda + elif feature_name in ['benchmark', 'parelag', 'tribol'] + feature_on = false + endif + endif + endif + if ( + feature_opt.auto() and get_option('mfem_precision') == 'single' and + feature_name in single_precision_conflicts + ) + feature_on = false + endif + + if ( + feature_on and + feature_name not in ['cuda', 'hip', 'openmp', 'simd'] and + not effective_allow_preinstalled and dependency_prefix == '' and + not source_capable.get(feature_name, false) + ) + error( + 'Selected feature mfem_' + feature_name + ' has no bundled source provider. ' + + 'Supply -Ddependency_prefix=/path/to/a/hermetic/prefix or enable preinstalled dependencies.' + ) + endif + mfem_features += {feature_name: feature_on} + mfem_feature_states += [feature_on] +endforeach + +foreach build_only_feature : ['benchmark', 'parelag', 'tribol'] + if get_option('mfem_' + build_only_feature).enabled() + error( + 'mfem_' + build_only_feature + ' is an upstream non-installed ' + + 'benchmark/miniapp build switch, not an MFEM library capability. ' + + 'Build that program in a separate consumer project against this SDK.' + ) + endif +endforeach + +mfem_has_mpi = mfem_features.get('mpi') +mfem_has_metis = mfem_features.get('metis') +mfem_has_cuda = mfem_features.get('cuda') +mfem_has_hip = mfem_features.get('hip') +mfem_has_openmp = mfem_features.get('openmp') +mfem_has_gslib = mfem_features.get('gslib') +mfem_has_zlib = mfem_features.get('zlib') +mfem_has_sundials = mfem_features.get('sundials') + +if get_option('mfem_mpi').enabled() and not can_build_posix_mpi and not allow_preinstalled and dependency_prefix == '' + error('The bundled MPI provider needs a native POSIX build. For cross/Windows builds, provide an MPI prefix or use -Dmfem_mpi=disabled.') +endif +if get_option('mfem_cuda').enabled() and not source_capable.get('cuda') + error('CUDA was forced on, but nvcc and a supported non-macOS CUDA host were not found.') +endif +if get_option('mfem_hip').enabled() and not source_capable.get('hip') + error('HIP was forced on, but hipcc was not found.') +endif +if get_option('mfem_openmp').enabled() and not usable_openmp + error('OpenMP was forced on, but no permitted OpenMP runtime was found for the selected compiler.') +endif +if get_option('mfem_simd').enabled() and not simd_arch_supported + error('SIMD was forced on, but MFEM has no intrinsic specialization for this target/compiler.') +endif +if get_option('mfem_enzyme').enabled() and cpp.get_id() != 'clang' + error('Enzyme was forced on, but Enzyme requires a matching Clang/LLVM compiler toolchain.') +endif +if mfem_has_cuda and mfem_has_hip + error('MFEM CUDA and HIP backends are mutually exclusive.') +endif +if mfem_features.get('enzyme') and mfem_has_cuda + error('The bundled CUDA route uses nvcc and cannot be combined with Enzyme; use a dedicated clang-CUDA toolchain or disable one feature.') +endif +if mfem_features.get('cudss') and not mfem_has_cuda + error('cuDSS requires -Dmfem_cuda=enabled (or an auto-detected CUDA toolkit).') +endif +if mfem_features.get('amgx') and not mfem_has_cuda + error('AmgX requires the CUDA backend.') +endif +if mfem_features.get('magma') and not (mfem_has_cuda or mfem_has_hip) + error('MAGMA requires either the CUDA or HIP backend.') +endif +if mfem_features.get('slepc') and not mfem_features.get('petsc') + error('SLEPc requires PETSc.') +endif +if mfem_features.get('adforward') and not mfem_features.get('codipack') + error('Forward-mode automatic differentiation requires CoDiPack.') +endif +if mfem_features.get('superlu5') and not mfem_features.get('superlu') + error('The SuperLU 5 compatibility option requires SuperLU_DIST.') +endif +if mfem_features.get('simmetrix') and not mfem_features.get('pumi') + error('Simmetrix integration requires PUMI.') +endif +foreach mpi_feature : ['superlu', 'mumps', 'strumpack', 'petsc', 'slepc', 'pumi', 'mkl_cpardiso', 'tribol'] + if mfem_features.get(mpi_feature) and not mfem_has_mpi + error('MFEM feature ' + mpi_feature + ' requires MPI/Hypre.') + endif +endforeach +if mfem_features.get('legacy_openmp') and ( + mfem_has_openmp or mfem_has_cuda or mfem_features.get('raja') or mfem_features.get('occa')) + error('Legacy OpenMP cannot be combined with OpenMP, CUDA, RAJA, or OCCA.') +endif +if mfem_features.get('legacy_openmp') and not mfem_features.get('thread_safe') + error('Legacy OpenMP requires MFEM thread-safe mode.') +endif + +if get_option('mfem_precision') == 'single' + foreach single_conflict : single_precision_conflicts + if mfem_features.get(single_conflict) + error('Single precision is incompatible with MFEM feature ' + single_conflict + '.') + endif + endforeach +endif +mfem_consumer_cpp_std = ( + mfem_features.get('raja') or mfem_features.get('umpire') +) ? 'c++20' : 'c++17' +system_mfem = disabler() +mfem_provider = 'source bundle' +if effective_allow_preinstalled + system_candidate = dependency('mfem', version: '>=4.10', required: false, allow_fallback: false) + if system_candidate.found() + system_compatible = true + foreach i : range(mfem_feature_names.length()) + if mfem_feature_states[i] + feature_macro = mfem_cmake_names[i] + macro_ok = cpp.compiles( + '#include \n#ifndef ' + feature_macro + '\n#error missing\n#endif\nint main(){return 0;}', + dependencies: system_candidate, + name: 'system MFEM provides ' + feature_macro, + ) + system_compatible = system_compatible and macro_ok + endif + endforeach + precision_macro = get_option('mfem_precision') == 'single' ? 'MFEM_USE_SINGLE' : 'MFEM_USE_DOUBLE' + system_compatible = system_compatible and cpp.compiles( + '#include \n#ifndef ' + precision_macro + '\n#error precision mismatch\n#endif\nint main(){return 0;}', + dependencies: system_candidate, + name: 'system MFEM uses requested precision', + ) + foreach i : range(mfem_feature_names.length()) + feature_opt = get_option('mfem_' + mfem_feature_names[i]) + feature_macro = mfem_cmake_names[i] + if feature_opt.disabled() + macro_absent = cpp.compiles( + '#include \n#ifdef ' + feature_macro + '\n#error explicitly disabled\n#endif\nint main(){return 0;}', + dependencies: system_candidate, + name: 'system MFEM omits disabled ' + feature_macro, + ) + system_compatible = system_compatible and macro_absent + endif + endforeach + if system_compatible + system_mfem = system_candidate + mfem_provider = 'system' + else + message('The system MFEM does not satisfy the selected feature set; using the pinned source bundle.') + endif + endif +endif + +uses_unmodelled_system_tpl = false +foreach feature_name : mfem_feature_names + if ( + mfem_features.get(feature_name) and feature_name != 'openmp' and + not source_capable.get(feature_name, false) + ) + uses_unmodelled_system_tpl = true + endif +endforeach +if ( + not system_mfem.found() and effective_allow_preinstalled and + dependency_prefix == '' and uses_unmodelled_system_tpl +) + error( + 'The selected features need external TPLs, but no complete compatible system MFEM was found. ' + + 'Provide one coherent -Ddependency_prefix=/path/to/tpls so the source MFEM usage interface remains reproducible.' + ) +endif + +if not system_mfem.found() and is_windows + error( + 'The source fallback currently targets POSIX and Emscripten toolchains. ' + + 'On Windows, provide a complete compatible MFEM installation and use -Dallow_preinstalled=true.' + ) +endif + +mfem_runtime_prefix = '' +mfem_build_target = [] +mpi_launcher_from_dependency = false + +if system_mfem.found() + mfem_dep = system_mfem +else + mfem_source = subproject('mfem').get_variable('mfem_source_dir') + + mpich_source = '' + hypre_source = '' + metis_source = '' + gslib_source = '' + zlib_source = '' + sundials_source = '' + libceed_source = '' + fms_source = '' + algoim_source = '' + blitz_source = '' + + dependency_has_mpi = dependency_prefix != '' and ( + fs.exists(dependency_prefix / 'bin' / 'mpicc') and + fs.exists(dependency_prefix / 'bin' / 'mpicxx') + ) + dependency_has_hypre = dependency_prefix != '' and fs.exists( + dependency_prefix / 'include' / 'HYPRE_config.h' + ) + dependency_has_metis = dependency_prefix != '' and fs.exists( + dependency_prefix / 'include' / 'metis.h' + ) + dependency_has_gslib = dependency_prefix != '' and fs.exists( + dependency_prefix / 'include' / 'gslib' / 'gslib.h' + ) + dependency_has_zlib = dependency_prefix != '' and fs.exists( + dependency_prefix / 'include' / 'zlib.h' + ) + dependency_has_sundials = dependency_prefix != '' and fs.exists( + dependency_prefix / 'include' / 'sundials' / 'sundials_config.h' + ) + dependency_has_ceed = dependency_prefix != '' and fs.exists( + dependency_prefix / 'include' / 'ceed.h' + ) + dependency_has_fms = dependency_prefix != '' and fs.exists( + dependency_prefix / 'include' / 'fms.h' + ) + dependency_has_algoim = dependency_prefix != '' and fs.exists( + dependency_prefix / 'include' / 'algoim_quad.hpp' + ) and fs.exists(dependency_prefix / 'include' / 'blitz' / 'array.h') + + build_mpi_from_source = mfem_has_mpi and can_build_posix_mpi and not dependency_has_mpi + mpi_launcher_from_dependency = mfem_has_mpi and dependency_has_mpi + if build_mpi_from_source + mpich_source = subproject('mpich').get_variable('mpich_source_dir') + endif + if mfem_has_mpi and (build_mpi_from_source or not dependency_has_hypre) + hypre_source = subproject('hypre').get_variable('hypre_source_dir') + endif + if mfem_has_metis and not dependency_has_metis + metis_source = subproject('metis').get_variable('metis_source_dir') + endif + if mfem_has_gslib and (build_mpi_from_source or not dependency_has_gslib) + gslib_source = subproject('gslib').get_variable('gslib_source_dir') + endif + if mfem_has_zlib and not dependency_has_zlib + zlib_source = subproject('zlib').get_variable('zlib_source_dir') + endif + if mfem_has_sundials and (build_mpi_from_source or not dependency_has_sundials) + sundials_source = subproject('sundials').get_variable('sundials_source_dir') + endif + if mfem_features.get('ceed') and not dependency_has_ceed + libceed_source = subproject('libceed').get_variable('libceed_source_dir') + endif + if mfem_features.get('fms') and not dependency_has_fms + fms_source = subproject('fms').get_variable('fms_source_dir') + endif + if mfem_features.get('algoim') and not dependency_has_algoim + algoim_source = subproject('algoim').get_variable('algoim_source_dir') + blitz_source = subproject('blitz').get_variable('blitz_source_dir') + endif + + bundle_is_python_package = get_option('build_python') and get_option('python_install_native') + bundle_output_name = bundle_is_python_package ? 'meson_mfem_template' : 'mfem-prefix' + mfem_runtime_prefix = meson.current_build_dir() / bundle_output_name + bundle_work_dir = meson.current_build_dir() / '_bundle' / 'work' + bundle_builder = files('../../tools/build_mfem_bundle.py') + bundle_auxiliary_files = files('../../tools/ceed_jit_source_root_relocatable.c') + cc_command = cc.cmd_array() + cxx_command = cpp.cmd_array() + known_compiler_launchers = ['ccache', 'sccache', 'distcc'] + cc_executable = '' + cc_launchers = [] + cc_fixed_args = [] + foreach command_part : cc_command + if cc_executable == '' and fs.name(command_part) in known_compiler_launchers + cc_launchers += [command_part] + elif cc_executable == '' + cc_executable = command_part + else + cc_fixed_args += [command_part] + endif + endforeach + cxx_executable = '' + cxx_launchers = [] + cxx_fixed_args = [] + foreach command_part : cxx_command + if cxx_executable == '' and fs.name(command_part) in known_compiler_launchers + cxx_launchers += [command_part] + elif cxx_executable == '' + cxx_executable = command_part + else + cxx_fixed_args += [command_part] + endif + endforeach + if cc_executable == '' or cxx_executable == '' + error('Could not identify the C/C++ compiler executables in Meson compiler commands.') + endif + bundle_command = [ + python_build, + bundle_builder, + '--source', mfem_source, + '--work-dir', bundle_work_dir, + '--prefix', '@OUTPUT@', + '--cmake', cmake_program, + '--cc', cc_executable, + '--cxx', cxx_executable, + '--buildtype', get_option('buildtype'), + '--jobs', get_option('jobs').to_string(), + '--host-system', host_machine.system(), + '--cross-build', meson.is_cross_build() ? 'true' : 'false', + '--library-kind', is_wasm ? 'static' : 'shared', + '--cuda-arch', get_option('mfem_cuda_arch'), + '--hip-arch', get_option('mfem_hip_arch'), + '--precision', get_option('mfem_precision'), + '--dependency-prefix', dependency_prefix, + '--allow-system', effective_allow_preinstalled ? 'true' : 'false', + '--vendor-dependency-prefix', wheel_carries_native_bundle ? 'true' : 'false', + ] + foreach compiler_launcher : cc_launchers + bundle_command += ['--cc-launcher=' + compiler_launcher] + endforeach + foreach compiler_launcher : cxx_launchers + bundle_command += ['--cxx-launcher=' + compiler_launcher] + endforeach + foreach c_arg : cc_fixed_args + bundle_command += ['--c-arg=' + c_arg] + endforeach + foreach c_arg : get_option('c_args') + bundle_command += ['--c-arg=' + c_arg] + endforeach + foreach c_arg : platform_c_args + bundle_command += ['--c-arg=' + c_arg] + endforeach + foreach cpp_arg : get_option('cpp_args') + bundle_command += ['--cxx-arg=' + cpp_arg] + endforeach + foreach cpp_arg : cxx_fixed_args + bundle_command += ['--cxx-arg=' + cpp_arg] + endforeach + foreach cpp_arg : platform_cpp_args + bundle_command += ['--cxx-arg=' + cpp_arg] + endforeach + foreach c_link_arg : get_option('c_link_args') + bundle_command += ['--c-link-arg=' + c_link_arg] + endforeach + foreach cpp_link_arg : get_option('cpp_link_args') + bundle_command += ['--cxx-link-arg=' + cpp_link_arg] + endforeach + foreach platform_link_arg : platform_link_args + bundle_command += ['--c-link-arg=' + platform_link_arg] + bundle_command += ['--cxx-link-arg=' + platform_link_arg] + endforeach + foreach i : range(mfem_feature_names.length()) + bundle_command += [ + '--feature', + mfem_feature_names[i] + '=' + (mfem_feature_states[i] ? 'ON' : 'OFF'), + ] + endforeach + foreach source_pair : [ + ['mpich', mpich_source], + ['hypre', hypre_source], + ['metis', metis_source], + ['gslib', gslib_source], + ['zlib', zlib_source], + ['sundials', sundials_source], + ['libceed', libceed_source], + ['fms', fms_source], + ['algoim', algoim_source], + ['blitz', blitz_source], + ] + if source_pair[1] != '' + bundle_command += ['--' + source_pair[0] + '-source', source_pair[1]] + endif + endforeach + + if bundle_is_python_package and get_option('install_mfem') + mfem_build_target = custom_target( + 'mfem-source-bundle', + output: bundle_output_name, + command: bundle_command, + depend_files: bundle_auxiliary_files, + build_by_default: true, + build_always_stale: dependency_prefix != '', + console: true, + install: true, + install_dir: python_build.get_install_dir(), + install_tag: 'python-native', + ) + else + mfem_build_target = custom_target( + 'mfem-source-bundle', + output: bundle_output_name, + command: bundle_command, + depend_files: bundle_auxiliary_files, + build_by_default: true, + build_always_stale: dependency_prefix != '', + console: true, + ) + endif + + bundle_compile_args = ['-I' + mfem_runtime_prefix / 'include'] + bundle_link_args = [ + '-L' + mfem_runtime_prefix / 'lib', + '-L' + mfem_runtime_prefix / 'lib64', + ] + if dependency_prefix != '' and not wheel_carries_native_bundle + bundle_compile_args += ['-I' + dependency_prefix / 'include'] + bundle_link_args += [ + '-L' + dependency_prefix / 'lib', + '-L' + dependency_prefix / 'lib64', + ] + endif + bundle_link_args += ['-lmfem'] + if mfem_has_mpi + bundle_link_args += ['-lHYPRE', '-lmpi'] + endif + if mfem_has_metis + bundle_link_args += ['-lmetis'] + endif + if mfem_has_gslib + bundle_link_args += ['-lgs'] + endif + if mfem_has_zlib + bundle_link_args += ['-lz'] + endif + if mfem_has_sundials + bundle_link_args += [ + '-lsundials_nvecserial', + '-lsundials_cvodes', + '-lsundials_arkode', + '-lsundials_kinsol', + '-lsundials_core', + ] + if mfem_has_mpi + bundle_link_args += [ + '-lsundials_nvecparallel', + '-lsundials_nvecmpiplusx', + ] + endif + endif + if mfem_features.get('ceed') + bundle_link_args += ['-lceed'] + endif + if mfem_features.get('fms') + bundle_link_args += ['-lfms'] + endif + if mfem_features.get('algoim') + bundle_link_args += ['-lblitz'] + endif + + mfem_dep_parts = [] + if mfem_has_openmp or mfem_features.get('legacy_openmp') + mfem_dep_parts += [openmp_dep] + endif + mfem_dep = declare_dependency( + compile_args: bundle_compile_args, + link_args: bundle_link_args, + dependencies: mfem_dep_parts, + sources: mfem_build_target, + version: '4.10', + variables: { + 'prefix': mfem_runtime_prefix, + 'includedir': mfem_runtime_prefix / 'include', + 'libdir': mfem_runtime_prefix / 'lib', + }, + ) + + if get_option('install_mfem') and not bundle_is_python_package + install_bundle = files('../../tools/install_bundle.py') + meson.add_install_script( + python_build, + install_bundle, + '--source', mfem_runtime_prefix, + '--destination', get_option('prefix'), + install_tag: 'runtime', + ) + endif +endif + +if get_option('build_python') and not get_option('python_install_native') and not system_mfem.found() + error('A source-built Python extension requires -Dpython_install_native=true so its runtime libraries are included in the wheel.') +endif +if get_option('build_python') and get_option('python_install_native') and not get_option('install_mfem') + error('A self-contained Python wheel requires -Dinstall_mfem=true so the native bundle is installed.') +endif + +meson.override_dependency('mfem', mfem_dep) diff --git a/build-python/meson.build b/build-python/meson.build new file mode 100644 index 0000000..5012269 --- /dev/null +++ b/build-python/meson.build @@ -0,0 +1,41 @@ +python_extension = [] +python_extension_dir = '' + +if get_option('build_python') + if is_wasm + error('The native CPython extension cannot be built for Emscripten. Disable build_python for the WASM profile.') + endif + nanobind_dep = dependency('nanobind', version: '>=2.15.0', fallback: 'nanobind') + extension_rpath_args = [] + if host_machine.system() == 'darwin' + extension_rpath_args = [ + '-Wl,-rpath,@loader_path/lib', + '-Wl,-rpath,@loader_path/lib64', + ] + elif not is_windows + extension_rpath_args = [ + '-Wl,-rpath,$ORIGIN/lib', + '-Wl,-rpath,$ORIGIN/lib64', + ] + endif + python_extension = python_build.extension_module( + '_core', + '../src/python/bindings.cpp', + dependencies: [nanobind_dep, mfem_dep, template_include_dep], + override_options: ['cpp_std=' + mfem_consumer_cpp_std], + link_args: extension_rpath_args, + build_rpath: '', + install_rpath: '', + install: true, + install_tag: 'python-runtime', + subdir: 'meson_mfem_template', + ) + python_extension_dir = meson.current_build_dir() + python_build.install_sources( + '../python/meson_mfem_template/__init__.py', + '../python/meson_mfem_template/_core.pyi', + '../python/meson_mfem_template/py.typed', + subdir: 'meson_mfem_template', + install_tag: 'python-runtime', + ) +endif diff --git a/cross/wasm32.ini b/cross/wasm32.ini new file mode 100644 index 0000000..cf9540b --- /dev/null +++ b/cross/wasm32.ini @@ -0,0 +1,21 @@ +[binaries] +c = 'emcc' +cpp = 'em++' +ar = 'emar' +strip = 'emstrip' +exe_wrapper = 'node' + +[built-in options] +c_args = ['-O1'] +cpp_args = ['-O1', '-fwasm-exceptions'] +c_link_args = ['-O1', '-sALLOW_MEMORY_GROWTH=1'] +cpp_link_args = ['-O1', '-fwasm-exceptions', '-sALLOW_MEMORY_GROWTH=1'] + +[host_machine] +system = 'emscripten' +cpu_family = 'wasm32' +cpu = 'wasm32' +endian = 'little' + +[properties] +needs_exe_wrapper = true diff --git a/examples/meson.build b/examples/meson.build new file mode 100644 index 0000000..e30a8a3 --- /dev/null +++ b/examples/meson.build @@ -0,0 +1,35 @@ +example_deps = [mfem_dep, template_include_dep] +example_build_rpath = mfem_runtime_prefix == '' ? '' : mfem_runtime_prefix / 'lib' +example_install_rpath = '' +if mfem_runtime_prefix != '' and not is_windows and not is_wasm + example_install_rpath = host_machine.system() == 'darwin' ? '@loader_path/../lib' : '$ORIGIN/../lib' +endif + +serial_example = [] +parallel_example = [] + +if get_option('build_examples') + serial_example = executable( + 'mfem-serial-poisson', + 'serial_poisson.cpp', + dependencies: example_deps, + override_options: ['cpp_std=' + mfem_consumer_cpp_std], + build_rpath: example_build_rpath, + install_rpath: example_install_rpath, + install: true, + install_tag: 'examples', + ) + + if mfem_has_mpi + parallel_example = executable( + 'mfem-parallel-hypre', + 'parallel_hypre.cpp', + dependencies: example_deps, + override_options: ['cpp_std=' + mfem_consumer_cpp_std], + build_rpath: example_build_rpath, + install_rpath: example_install_rpath, + install: true, + install_tag: 'examples', + ) + endif +endif diff --git a/examples/parallel_hypre.cpp b/examples/parallel_hypre.cpp new file mode 100644 index 0000000..358ede9 --- /dev/null +++ b/examples/parallel_hypre.cpp @@ -0,0 +1,80 @@ +#include +#include + +#include +#include + +#if !MESON_MFEM_HAS_MPI || !MESON_MFEM_HAS_HYPRE +#error "This example requires the MPI/Hypre capability" +#endif + +int main(int argc, char **argv) +{ + mfem::Mpi::Init(argc, argv); + mfem::Hypre::Init(); + const int rank = mfem::Mpi::WorldRank(); + const char *device_name = argc > 1 ? argv[1] : "cpu"; + mfem::Device device(device_name); + + mfem::Mesh serial_mesh = mfem::Mesh::MakeCartesian2D( + 8, 8, mfem::Element::QUADRILATERAL, true, 1.0, 1.0); + mfem::ParMesh mesh(MPI_COMM_WORLD, serial_mesh); + serial_mesh.Clear(); + + mfem::H1_FECollection elements(2, mesh.Dimension()); + mfem::ParFiniteElementSpace space(&mesh, &elements); + mfem::Array essential_boundary(mesh.bdr_attributes.Max()); + essential_boundary = 1; + mfem::Array essential_dofs; + space.GetEssentialTrueDofs(essential_boundary, essential_dofs); + + mfem::ConstantCoefficient one(1.0); + mfem::ParLinearForm rhs(&space); + rhs.AddDomainIntegrator(new mfem::DomainLFIntegrator(one)); + rhs.Assemble(); + + mfem::ParGridFunction solution(&space); + solution = 0.0; + mfem::ParBilinearForm diffusion(&space); + diffusion.AddDomainIntegrator(new mfem::DiffusionIntegrator(one)); + diffusion.Assemble(); + + mfem::OperatorPtr matrix; + mfem::Vector linear_rhs; + mfem::Vector linear_solution; + diffusion.FormLinearSystem( + essential_dofs, solution, rhs, matrix, linear_solution, linear_rhs); + + mfem::HypreBoomerAMG boomer_amg; + boomer_amg.SetPrintLevel(0); + mfem::CGSolver solver(MPI_COMM_WORLD); + solver.SetPreconditioner(boomer_amg); + solver.SetOperator(*matrix); + solver.SetRelTol(1e-10); + solver.SetAbsTol(0.0); + solver.SetMaxIter(300); + solver.SetPrintLevel(0); + solver.Mult(linear_rhs, linear_solution); + diffusion.RecoverFEMSolution(linear_solution, rhs, solution); + +#if MESON_MFEM_HAS_GSLIB + mfem::FindPointsGSLIB find_points(MPI_COMM_WORLD); +#endif +#if MESON_MFEM_HAS_SUNDIALS + mfem::CVODESolver cvode(MPI_COMM_WORLD, CV_BDF); +#endif + + const double local_norm = solution.Norml2(); + double global_norm = 0.0; + MPI_Allreduce(&local_norm, &global_norm, 1, MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD); + if (rank == 0) + { + std::cout << "MFEM parallel Poisson: ranks=" << mfem::Mpi::WorldSize() + << " global_dofs=" << space.GlobalTrueVSize() + << " norm_sum=" << global_norm + << " device=" << device_name << '\n'; + } + return solver.GetConverged() && std::isfinite(global_norm) && global_norm > 0.0 + ? 0 + : 2; +} diff --git a/examples/serial_poisson.cpp b/examples/serial_poisson.cpp new file mode 100644 index 0000000..6f92641 --- /dev/null +++ b/examples/serial_poisson.cpp @@ -0,0 +1,81 @@ +#include +#include + +#include +#include + +int main(int argc, char **argv) +{ + const char *device_name = argc > 1 ? argv[1] : +#if MESON_MFEM_HAS_CEED && !defined(__EMSCRIPTEN__) + "ceed-cpu"; +#else + "cpu"; +#endif + mfem::Device device(device_name); + + mfem::Mesh mesh = mfem::Mesh::MakeCartesian2D( + 6, 6, mfem::Element::QUADRILATERAL, true, 1.0, 1.0); + const int dimension = mesh.Dimension(); + mfem::H1_FECollection elements(2, dimension); + mfem::FiniteElementSpace space(&mesh, &elements); + + mfem::Array essential_boundary(mesh.bdr_attributes.Max()); + essential_boundary = 1; + mfem::Array essential_dofs; + space.GetEssentialTrueDofs(essential_boundary, essential_dofs); + + mfem::ConstantCoefficient one(1.0); + mfem::LinearForm rhs(&space); + rhs.AddDomainIntegrator(new mfem::DomainLFIntegrator(one)); + rhs.Assemble(); + + mfem::GridFunction solution(&space); + solution = 0.0; + + mfem::BilinearForm diffusion(&space); + diffusion.AddDomainIntegrator(new mfem::DiffusionIntegrator(one)); + if (mfem::Device::Allows(mfem::Backend::CEED_MASK)) + { + diffusion.SetAssemblyLevel(mfem::AssemblyLevel::PARTIAL); + } + diffusion.Assemble(); + + mfem::OperatorPtr matrix; + mfem::Vector linear_rhs; + mfem::Vector linear_solution; + diffusion.FormLinearSystem( + essential_dofs, solution, rhs, matrix, linear_solution, linear_rhs); + + mfem::CGSolver solver; + solver.SetOperator(*matrix); + solver.SetRelTol(1e-12); + solver.SetAbsTol(0.0); + solver.SetMaxIter(200); + solver.SetPrintLevel(0); + solver.Mult(linear_rhs, linear_solution); + diffusion.RecoverFEMSolution(linear_solution, rhs, solution); + +#if MESON_MFEM_HAS_FMS + mfem::FMSDataCollection fms_collection("meson-mfem-smoke", &mesh); + fms_collection.SetProtocol("ascii"); +#endif + +#if MESON_MFEM_HAS_ALGOIM + mfem::FunctionCoefficient level_set( + [](const mfem::Vector &point) { return point[0] + point[1] - 0.2; }); + mfem::AlgoimIntegrationRules algoim_rules(2, level_set, 2); + mfem::IntegrationRule cut_rule; + algoim_rules.GetVolumeIntegrationRule( + *mesh.GetElementTransformation(0), cut_rule); + if (cut_rule.GetNPoints() == 0) + { + return 3; + } +#endif + + const double norm = solution.Norml2(); + std::cout << "MFEM serial Poisson: dofs=" << space.GetTrueVSize() + << " l2=" << norm << " device=" << device_name << '\n'; + return solver.GetConverged() && std::isfinite(norm) && norm > 0.0 ? 0 : 2; +} diff --git a/include/meson_mfem_template/config.hpp.in b/include/meson_mfem_template/config.hpp.in new file mode 100644 index 0000000..de1691e --- /dev/null +++ b/include/meson_mfem_template/config.hpp.in @@ -0,0 +1,261 @@ +#pragma once + +#define MESON_MFEM_TEMPLATE_VERSION @MESON_MFEM_TEMPLATE_VERSION@ +#mesondefine01 MESON_MFEM_USING_BUNDLED_MFEM + +#include + +#ifdef MFEM_USE_MPI +#define MESON_MFEM_HAS_MPI 1 +#else +#define MESON_MFEM_HAS_MPI 0 +#endif +#define MESON_MFEM_HAS_HYPRE MESON_MFEM_HAS_MPI + +#ifdef MFEM_USE_METIS +#define MESON_MFEM_HAS_METIS 1 +#else +#define MESON_MFEM_HAS_METIS 0 +#endif +#ifdef MFEM_USE_EXCEPTIONS +#define MESON_MFEM_HAS_EXCEPTIONS 1 +#else +#define MESON_MFEM_HAS_EXCEPTIONS 0 +#endif +#ifdef MFEM_USE_ZLIB +#define MESON_MFEM_HAS_ZLIB 1 +#else +#define MESON_MFEM_HAS_ZLIB 0 +#endif +#ifdef MFEM_USE_LIBUNWIND +#define MESON_MFEM_HAS_LIBUNWIND 1 +#else +#define MESON_MFEM_HAS_LIBUNWIND 0 +#endif +#ifdef MFEM_USE_LAPACK +#define MESON_MFEM_HAS_LAPACK 1 +#else +#define MESON_MFEM_HAS_LAPACK 0 +#endif +#ifdef MFEM_THREAD_SAFE +#define MESON_MFEM_HAS_THREAD_SAFE 1 +#else +#define MESON_MFEM_HAS_THREAD_SAFE 0 +#endif +#ifdef MFEM_USE_OPENMP +#define MESON_MFEM_HAS_OPENMP 1 +#else +#define MESON_MFEM_HAS_OPENMP 0 +#endif +#ifdef MFEM_USE_LEGACY_OPENMP +#define MESON_MFEM_HAS_LEGACY_OPENMP 1 +#else +#define MESON_MFEM_HAS_LEGACY_OPENMP 0 +#endif +#ifdef MFEM_USE_MEMALLOC +#define MESON_MFEM_HAS_MEMALLOC 1 +#else +#define MESON_MFEM_HAS_MEMALLOC 0 +#endif +#ifdef MFEM_USE_SUNDIALS +#define MESON_MFEM_HAS_SUNDIALS 1 +#else +#define MESON_MFEM_HAS_SUNDIALS 0 +#endif +#ifdef MFEM_USE_SUITESPARSE +#define MESON_MFEM_HAS_SUITESPARSE 1 +#else +#define MESON_MFEM_HAS_SUITESPARSE 0 +#endif +#ifdef MFEM_USE_SUPERLU +#define MESON_MFEM_HAS_SUPERLU 1 +#else +#define MESON_MFEM_HAS_SUPERLU 0 +#endif +#ifdef MFEM_USE_SUPERLU5 +#define MESON_MFEM_HAS_SUPERLU5 1 +#else +#define MESON_MFEM_HAS_SUPERLU5 0 +#endif +#ifdef MFEM_USE_MUMPS +#define MESON_MFEM_HAS_MUMPS 1 +#else +#define MESON_MFEM_HAS_MUMPS 0 +#endif +#ifdef MFEM_USE_STRUMPACK +#define MESON_MFEM_HAS_STRUMPACK 1 +#else +#define MESON_MFEM_HAS_STRUMPACK 0 +#endif +#ifdef MFEM_USE_CUDSS +#define MESON_MFEM_HAS_CUDSS 1 +#else +#define MESON_MFEM_HAS_CUDSS 0 +#endif +#ifdef MFEM_USE_GINKGO +#define MESON_MFEM_HAS_GINKGO 1 +#else +#define MESON_MFEM_HAS_GINKGO 0 +#endif +#ifdef MFEM_USE_AMGX +#define MESON_MFEM_HAS_AMGX 1 +#else +#define MESON_MFEM_HAS_AMGX 0 +#endif +#ifdef MFEM_USE_MAGMA +#define MESON_MFEM_HAS_MAGMA 1 +#else +#define MESON_MFEM_HAS_MAGMA 0 +#endif +#ifdef MFEM_USE_GNUTLS +#define MESON_MFEM_HAS_GNUTLS 1 +#else +#define MESON_MFEM_HAS_GNUTLS 0 +#endif +#ifdef MFEM_USE_GSLIB +#define MESON_MFEM_HAS_GSLIB 1 +#else +#define MESON_MFEM_HAS_GSLIB 0 +#endif +#ifdef MFEM_USE_HDF5 +#define MESON_MFEM_HAS_HDF5 1 +#else +#define MESON_MFEM_HAS_HDF5 0 +#endif +#ifdef MFEM_USE_NETCDF +#define MESON_MFEM_HAS_NETCDF 1 +#else +#define MESON_MFEM_HAS_NETCDF 0 +#endif +#ifdef MFEM_USE_PETSC +#define MESON_MFEM_HAS_PETSC 1 +#else +#define MESON_MFEM_HAS_PETSC 0 +#endif +#ifdef MFEM_USE_SLEPC +#define MESON_MFEM_HAS_SLEPC 1 +#else +#define MESON_MFEM_HAS_SLEPC 0 +#endif +#ifdef MFEM_USE_MPFR +#define MESON_MFEM_HAS_MPFR 1 +#else +#define MESON_MFEM_HAS_MPFR 0 +#endif +#ifdef MFEM_USE_SIDRE +#define MESON_MFEM_HAS_SIDRE 1 +#else +#define MESON_MFEM_HAS_SIDRE 0 +#endif +#ifdef MFEM_USE_FMS +#define MESON_MFEM_HAS_FMS 1 +#else +#define MESON_MFEM_HAS_FMS 0 +#endif +#ifdef MFEM_USE_CONDUIT +#define MESON_MFEM_HAS_CONDUIT 1 +#else +#define MESON_MFEM_HAS_CONDUIT 0 +#endif +#ifdef MFEM_USE_PUMI +#define MESON_MFEM_HAS_PUMI 1 +#else +#define MESON_MFEM_HAS_PUMI 0 +#endif +#ifdef MFEM_USE_HIOP +#define MESON_MFEM_HAS_HIOP 1 +#else +#define MESON_MFEM_HAS_HIOP 0 +#endif +#ifdef MFEM_USE_CUDA +#define MESON_MFEM_HAS_CUDA 1 +#else +#define MESON_MFEM_HAS_CUDA 0 +#endif +#ifdef MFEM_USE_HIP +#define MESON_MFEM_HAS_HIP 1 +#else +#define MESON_MFEM_HAS_HIP 0 +#endif +#ifdef MFEM_USE_OCCA +#define MESON_MFEM_HAS_OCCA 1 +#else +#define MESON_MFEM_HAS_OCCA 0 +#endif +#ifdef MFEM_USE_RAJA +#define MESON_MFEM_HAS_RAJA 1 +#else +#define MESON_MFEM_HAS_RAJA 0 +#endif +#ifdef MFEM_USE_CEED +#define MESON_MFEM_HAS_CEED 1 +#else +#define MESON_MFEM_HAS_CEED 0 +#endif +#ifdef MFEM_USE_UMPIRE +#define MESON_MFEM_HAS_UMPIRE 1 +#else +#define MESON_MFEM_HAS_UMPIRE 0 +#endif +#ifdef MFEM_USE_SIMD +#define MESON_MFEM_HAS_SIMD 1 +#else +#define MESON_MFEM_HAS_SIMD 0 +#endif +#ifdef MFEM_USE_ADIOS2 +#define MESON_MFEM_HAS_ADIOS2 1 +#else +#define MESON_MFEM_HAS_ADIOS2 0 +#endif +#ifdef MFEM_USE_CALIPER +#define MESON_MFEM_HAS_CALIPER 1 +#else +#define MESON_MFEM_HAS_CALIPER 0 +#endif +#ifdef MFEM_USE_ALGOIM +#define MESON_MFEM_HAS_ALGOIM 1 +#else +#define MESON_MFEM_HAS_ALGOIM 0 +#endif +#ifdef MFEM_USE_MKL_CPARDISO +#define MESON_MFEM_HAS_MKL_CPARDISO 1 +#else +#define MESON_MFEM_HAS_MKL_CPARDISO 0 +#endif +#ifdef MFEM_USE_MKL_PARDISO +#define MESON_MFEM_HAS_MKL_PARDISO 1 +#else +#define MESON_MFEM_HAS_MKL_PARDISO 0 +#endif +#ifdef MFEM_USE_ADFORWARD +#define MESON_MFEM_HAS_ADFORWARD 1 +#else +#define MESON_MFEM_HAS_ADFORWARD 0 +#endif +#ifdef MFEM_USE_CODIPACK +#define MESON_MFEM_HAS_CODIPACK 1 +#else +#define MESON_MFEM_HAS_CODIPACK 0 +#endif +#ifdef MFEM_USE_BENCHMARK +#define MESON_MFEM_HAS_BENCHMARK 1 +#else +#define MESON_MFEM_HAS_BENCHMARK 0 +#endif +#define MESON_MFEM_HAS_PARELAG 0 +#define MESON_MFEM_HAS_TRIBOL 0 +#ifdef MFEM_USE_ENZYME +#define MESON_MFEM_HAS_ENZYME 1 +#else +#define MESON_MFEM_HAS_ENZYME 0 +#endif +#ifdef MFEM_USE_MOONOLITH +#define MESON_MFEM_HAS_MOONOLITH 1 +#else +#define MESON_MFEM_HAS_MOONOLITH 0 +#endif +#ifdef MFEM_USE_SIMMETRIX +#define MESON_MFEM_HAS_SIMMETRIX 1 +#else +#define MESON_MFEM_HAS_SIMMETRIX 0 +#endif diff --git a/include/meson_mfem_template/meson.build b/include/meson_mfem_template/meson.build new file mode 100644 index 0000000..7a22118 --- /dev/null +++ b/include/meson_mfem_template/meson.build @@ -0,0 +1,19 @@ +mfem_config = configuration_data() +mfem_config.set_quoted('MESON_MFEM_TEMPLATE_VERSION', meson.project_version()) +mfem_config.set10('MESON_MFEM_USING_BUNDLED_MFEM', mfem_provider != 'system') + +mfem_config_header = configure_file( + input: 'config.hpp.in', + output: 'config.hpp', + configuration: mfem_config, + install: true, + install_tag: get_option('build_python') ? 'python-runtime' : 'devel', + install_dir: get_option('build_python') + ? python_build.get_install_dir() / 'meson_mfem_template' / 'include' / 'meson_mfem_template' + : get_option('includedir') / 'meson_mfem_template', +) + +template_include_dep = declare_dependency( + include_directories: include_directories('..'), + sources: mfem_config_header, +) diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..b10f2b5 --- /dev/null +++ b/meson.build @@ -0,0 +1,58 @@ +project( + 'meson-mfem-template', + ['c', 'cpp'], + version: '0.1.0', + meson_version: '>=1.9.1', + default_options: [ + 'c_std=c11', + 'cpp_std=c++23', + 'warning_level=2', + ], +) + +python_mod = import('python') +fs = import('fs') +cc = meson.get_compiler('c') +cpp = meson.get_compiler('cpp') +platform_c_args = [] +platform_cpp_args = [] +platform_link_args = [] +if host_machine.system() == 'darwin' and get_option('macos_deployment_target') != '' + macos_minimum_flag = '-mmacosx-version-min=' + get_option('macos_deployment_target') + platform_c_args += [macos_minimum_flag] + platform_cpp_args += [macos_minimum_flag] + platform_link_args += [macos_minimum_flag] + add_project_arguments(macos_minimum_flag, language: ['c', 'cpp']) + add_project_link_arguments(macos_minimum_flag, language: ['c', 'cpp']) +endif + +subdir('build-check') +subdir('build-config') +subdir('include/meson_mfem_template') +subdir('examples') +subdir('build-python') +subdir('tests') + +summary( + { + 'MFEM provider': mfem_provider, + 'dependency profile': get_option('feature_profile'), + 'allow preinstalled': get_option('allow_preinstalled'), + 'parallel MPI/Hypre': mfem_has_mpi, + 'METIS': mfem_has_metis, + 'CUDA': mfem_has_cuda, + 'HIP': mfem_has_hip, + 'OpenMP': mfem_has_openmp, + 'GSLIB': mfem_has_gslib, + 'zlib': mfem_has_zlib, + 'SUNDIALS': mfem_has_sundials, + 'libCEED': mfem_features.get('ceed'), + 'FMS': mfem_features.get('fms'), + 'Algoim': mfem_features.get('algoim'), + 'Python bindings': get_option('build_python'), + 'examples': get_option('build_examples'), + 'tests': get_option('build_tests'), + }, + section: 'meson-mfem-template ' + meson.project_version(), + bool_yn: true, +) diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..c00fe37 --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,66 @@ +option('allow_preinstalled', type: 'boolean', value: true, description: 'Allow one complete, feature-compatible system MFEM. Set false for the dependency-hermetic source fallback.') +option('feature_profile', type: 'combo', choices: ['minimal', 'portable', 'full'], value: 'portable', description: 'Defaults for auto features. full is strict and requires all selected non-bundled TPLs in dependency_prefix.') +option('build_python', type: 'boolean', value: false, description: 'Build the nanobind Python extension and install native assets inside the Python package.') +option('build_examples', type: 'boolean', value: true, description: 'Build MFEM capability examples.') +option('build_tests', type: 'boolean', value: true, description: 'Register serial, parallel, and Python smoke tests when applicable.') +option('install_mfem', type: 'boolean', value: true, description: 'Install the source-built MFEM bundle and its runtime dependencies.') +option('dependency_prefix', type: 'string', value: '', description: 'Explicit prefix containing optional TPLs. Used even when allow_preinstalled=false.') +option('jobs', type: 'integer', min: 1, max: 1024, value: 4, description: 'Parallel jobs for the CMake/Make source bundle.') +option('mfem_cuda_arch', type: 'string', value: 'native', description: 'CUDA architectures understood by CMake, for example 80;86. native requires a recent CMake/toolkit.') +option('mfem_hip_arch', type: 'string', value: '', description: 'HIP GPU target, for example gfx90a. Empty lets the toolchain choose.') +option('mfem_precision', type: 'combo', choices: ['double', 'single'], value: 'double', description: 'Floating-point precision used consistently by MFEM and Hypre.') +option('python_install_native', type: 'boolean', value: true, description: 'Install MFEM headers, CMake/pkg-config data, and runtime libraries under the Python package.') +option('macos_deployment_target', type: 'string', value: '11.0', description: 'Minimum macOS version for native artifacts; ignored on other systems. Empty uses the toolchain default.') + +option('mfem_mpi', type: 'feature', value: 'auto', description: 'MPI parallel MFEM; implies Hypre.') +option('mfem_metis', type: 'feature', value: 'auto', description: 'METIS graph partitioning.') +option('mfem_exceptions', type: 'feature', value: 'auto', description: 'MFEM C++ exceptions.') +option('mfem_zlib', type: 'feature', value: 'auto', description: 'zlib compressed streams.') +option('mfem_libunwind', type: 'feature', value: 'auto', description: 'libunwind backtraces.') +option('mfem_lapack', type: 'feature', value: 'auto', description: 'BLAS/LAPACK solvers.') +option('mfem_thread_safe', type: 'feature', value: 'auto', description: 'MFEM thread-safe mode.') +option('mfem_openmp', type: 'feature', value: 'auto', description: 'OpenMP backend.') +option('mfem_legacy_openmp', type: 'feature', value: 'disabled', description: 'Legacy OpenMP backend (mutually exclusive with modern device backends).') +option('mfem_memalloc', type: 'feature', value: 'enabled', description: 'MFEM internal memory allocator.') +option('mfem_sundials', type: 'feature', value: 'auto', description: 'SUNDIALS ODE/nonlinear solvers.') +option('mfem_suitesparse', type: 'feature', value: 'auto', description: 'SuiteSparse direct solvers.') +option('mfem_superlu', type: 'feature', value: 'auto', description: 'SuperLU_DIST.') +option('mfem_superlu5', type: 'feature', value: 'disabled', description: 'Compatibility with old SuperLU_DIST 5.1.') +option('mfem_mumps', type: 'feature', value: 'auto', description: 'MUMPS parallel direct solver.') +option('mfem_strumpack', type: 'feature', value: 'auto', description: 'STRUMPACK parallel solver.') +option('mfem_cudss', type: 'feature', value: 'auto', description: 'NVIDIA cuDSS (CUDA only, vendor binary dependency).') +option('mfem_ginkgo', type: 'feature', value: 'auto', description: 'Ginkgo solvers.') +option('mfem_amgx', type: 'feature', value: 'auto', description: 'NVIDIA AmgX.') +option('mfem_magma', type: 'feature', value: 'auto', description: 'MAGMA dense linear algebra.') +option('mfem_gnutls', type: 'feature', value: 'auto', description: 'GnuTLS socket security.') +option('mfem_gslib', type: 'feature', value: 'auto', description: 'GSLIB FindPoints support.') +option('mfem_hdf5', type: 'feature', value: 'auto', description: 'HDF5 data support.') +option('mfem_netcdf', type: 'feature', value: 'auto', description: 'NetCDF data support.') +option('mfem_petsc', type: 'feature', value: 'auto', description: 'PETSc solvers.') +option('mfem_slepc', type: 'feature', value: 'auto', description: 'SLEPc eigensolvers; implies PETSc.') +option('mfem_mpfr', type: 'feature', value: 'auto', description: 'MPFR arbitrary precision support.') +option('mfem_sidre', type: 'feature', value: 'auto', description: 'Axom/Sidre support.') +option('mfem_fms', type: 'feature', value: 'auto', description: 'FMS data model.') +option('mfem_conduit', type: 'feature', value: 'auto', description: 'Conduit data support.') +option('mfem_pumi', type: 'feature', value: 'auto', description: 'PUMI meshes.') +option('mfem_hiop', type: 'feature', value: 'auto', description: 'HiOp optimization.') +option('mfem_cuda', type: 'feature', value: 'auto', description: 'NVIDIA CUDA backend; requires a host CUDA toolkit.') +option('mfem_hip', type: 'feature', value: 'auto', description: 'AMD HIP backend; requires a host ROCm toolkit.') +option('mfem_occa', type: 'feature', value: 'auto', description: 'OCCA device backend.') +option('mfem_raja', type: 'feature', value: 'auto', description: 'RAJA device backend; raises MFEM to C++20.') +option('mfem_ceed', type: 'feature', value: 'auto', description: 'libCEED operator backend.') +option('mfem_umpire', type: 'feature', value: 'auto', description: 'Umpire memory manager; raises MFEM to C++20.') +option('mfem_simd', type: 'feature', value: 'auto', description: 'SIMD intrinsics.') +option('mfem_adios2', type: 'feature', value: 'auto', description: 'ADIOS2 I/O.') +option('mfem_caliper', type: 'feature', value: 'auto', description: 'Caliper profiling.') +option('mfem_algoim', type: 'feature', value: 'auto', description: 'Algoim implicit geometry support.') +option('mfem_mkl_cpardiso', type: 'feature', value: 'auto', description: 'Intel MKL Cluster PARDISO.') +option('mfem_mkl_pardiso', type: 'feature', value: 'auto', description: 'Intel MKL PARDISO.') +option('mfem_adforward', type: 'feature', value: 'auto', description: 'Forward mode for supported automatic differentiation packages.') +option('mfem_codipack', type: 'feature', value: 'auto', description: 'CoDiPack automatic differentiation.') +option('mfem_benchmark', type: 'feature', value: 'disabled', description: 'Upstream benchmark-program build switch; enabling is rejected because it is not an installed MFEM library capability.') +option('mfem_parelag', type: 'feature', value: 'disabled', description: 'Upstream ParELAG miniapp-only switch; enabling is rejected because it is not an installed MFEM library capability.') +option('mfem_tribol', type: 'feature', value: 'disabled', description: 'Upstream Tribol miniapp-only switch; enabling is rejected because it is not an installed MFEM library capability.') +option('mfem_enzyme', type: 'feature', value: 'auto', description: 'Enzyme AD plugin; requires matching Clang/LLVM.') +option('mfem_moonolith', type: 'feature', value: 'auto', description: 'Moonolith transfer miniapp integration.') +option('mfem_simmetrix', type: 'feature', value: 'auto', description: 'Simmetrix commercial mesh integration.') diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0eed551 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,41 @@ +[build-system] +build-backend = "mesonpy" +requires = [ + "meson-python>=0.20", + "meson>=1.9.1", + "ninja>=1.11", + "cmake>=3.24", + "patchelf>=0.17; sys_platform == 'linux'", +] + +[project] +name = "meson-mfem-template" +dynamic = ["version"] +description = "Portable Meson source bundle and nanobind validation module for MFEM" +readme = "README.md" +requires-python = ">=3.10" +license = "MIT" +license-files = ["LICENSE"] + +[tool.meson-python] +allow-windows-internal-shared-libs = true + +[tool.meson-python.args] +setup = [ + "-Dbuildtype=release", + "-Dallow_preinstalled=false", + "-Dfeature_profile=portable", + "-Dbuild_python=true", + "-Dbuild_examples=false", + "-Dbuild_tests=false", + "-Dinstall_mfem=true", + "-Dpython_install_native=true", + "-Dmfem_openmp=disabled", + "-Dmfem_cuda=disabled", + "-Dmfem_hip=disabled", + "--force-fallback-for=nanobind,robin-map", +] +install = [ + "--tags=python-runtime,python-native", + "--skip-subprojects", +] diff --git a/python/meson_mfem_template/__init__.py b/python/meson_mfem_template/__init__.py new file mode 100644 index 0000000..c3b57bd --- /dev/null +++ b/python/meson_mfem_template/__init__.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +import os +from pathlib import Path + + +def _native_search_paths() -> list[Path]: + package = Path(__file__).resolve().parent + return [package / "lib", package / "lib64", package / "bin"] + + +_dll_directory_handles = [] +if os.name == "nt" and hasattr(os, "add_dll_directory"): + for _directory in _native_search_paths(): + if _directory.is_dir(): + _dll_directory_handles.append(os.add_dll_directory(str(_directory))) + +from ._core import capabilities, mfem_version, serial_poisson_dofs + +__all__ = [ + "capabilities", + "get_cmake_prefix", + "get_include", + "get_lib", + "mfem_version", + "serial_poisson_dofs", +] + + +def _package_dir() -> Path: + return Path(__file__).resolve().parent + + +def get_include() -> str: + """Return the wheel-local include directory containing MFEM headers.""" + return str(_package_dir() / "include") + + +def get_lib() -> str: + """Return the wheel-local directory containing MFEM runtime libraries.""" + return str(_package_dir() / "lib") + + +def get_cmake_prefix() -> str: + """Return the prefix that can be appended to CMAKE_PREFIX_PATH.""" + return str(_package_dir()) diff --git a/python/meson_mfem_template/_core.pyi b/python/meson_mfem_template/_core.pyi new file mode 100644 index 0000000..e07d771 --- /dev/null +++ b/python/meson_mfem_template/_core.pyi @@ -0,0 +1,4 @@ +def mfem_version() -> str: ... +def capabilities() -> dict[str, bool]: ... +def serial_poisson_dofs(cells: int = 4, order: int = 2) -> int: ... + diff --git a/python/meson_mfem_template/py.typed b/python/meson_mfem_template/py.typed new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/python/meson_mfem_template/py.typed @@ -0,0 +1 @@ + diff --git a/src/python/bindings.cpp b/src/python/bindings.cpp new file mode 100644 index 0000000..0f58cb4 --- /dev/null +++ b/src/python/bindings.cpp @@ -0,0 +1,91 @@ +#include +#include +#include +#include + +#include +#include +#include + +namespace nb = nanobind; + +namespace +{ +int serial_poisson_dofs(int cells, int order) +{ + if (cells < 2 || order < 1) + { + throw nb::value_error("cells must be >= 2 and order must be >= 1"); + } + mfem::Mesh mesh = mfem::Mesh::MakeCartesian2D( + cells, cells, mfem::Element::QUADRILATERAL, true, 1.0, 1.0); + mfem::H1_FECollection elements(order, mesh.Dimension()); + mfem::FiniteElementSpace space(&mesh, &elements); + mfem::Array essential_boundary(mesh.bdr_attributes.Max()); + essential_boundary = 1; + mfem::Array essential_dofs; + space.GetEssentialTrueDofs(essential_boundary, essential_dofs); + + mfem::ConstantCoefficient one(1.0); + mfem::LinearForm rhs(&space); + rhs.AddDomainIntegrator(new mfem::DomainLFIntegrator(one)); + rhs.Assemble(); + mfem::GridFunction solution(&space); + solution = 0.0; + mfem::BilinearForm diffusion(&space); + diffusion.AddDomainIntegrator(new mfem::DiffusionIntegrator(one)); + diffusion.Assemble(); + + mfem::OperatorPtr matrix; + mfem::Vector linear_rhs; + mfem::Vector linear_solution; + diffusion.FormLinearSystem( + essential_dofs, solution, rhs, matrix, linear_solution, linear_rhs); + mfem::GSSmoother smoother(static_cast(*matrix)); + mfem::CGSolver solver; + solver.SetOperator(*matrix); + solver.SetPreconditioner(smoother); + solver.SetRelTol(1e-10); + solver.SetMaxIter(200); + solver.SetPrintLevel(0); + solver.Mult(linear_rhs, linear_solution); + if (!solver.GetConverged()) + { + throw std::runtime_error("MFEM CG solve did not converge"); + } + return space.GetTrueVSize(); +} + +nb::dict capabilities() +{ + nb::dict result; + result["mpi"] = MESON_MFEM_HAS_MPI != 0; + result["hypre"] = MESON_MFEM_HAS_HYPRE != 0; + result["metis"] = MESON_MFEM_HAS_METIS != 0; + result["cuda"] = MESON_MFEM_HAS_CUDA != 0; + result["hip"] = MESON_MFEM_HAS_HIP != 0; + result["openmp"] = MESON_MFEM_HAS_OPENMP != 0; + result["gslib"] = MESON_MFEM_HAS_GSLIB != 0; + result["zlib"] = MESON_MFEM_HAS_ZLIB != 0; + result["sundials"] = MESON_MFEM_HAS_SUNDIALS != 0; + result["ceed"] = MESON_MFEM_HAS_CEED != 0; + result["fms"] = MESON_MFEM_HAS_FMS != 0; + result["algoim"] = MESON_MFEM_HAS_ALGOIM != 0; + result["simd"] = MESON_MFEM_HAS_SIMD != 0; + result["bundled_mfem"] = MESON_MFEM_USING_BUNDLED_MFEM != 0; + return result; +} +} // namespace + +NB_MODULE(_core, module) +{ + module.doc() = "Small nanobind validation module for the Meson MFEM bundle"; + module.def("mfem_version", []() { return std::string(MFEM_VERSION_STRING); }); + module.def("capabilities", &capabilities); + module.def( + "serial_poisson_dofs", + &serial_poisson_dofs, + nb::arg("cells") = 4, + nb::arg("order") = 2, + "Assemble and solve a small MFEM Poisson problem; return true DOFs."); +} diff --git a/subprojects/.wraplock b/subprojects/.wraplock new file mode 100644 index 0000000..e69de29 diff --git a/subprojects/algoim.wrap b/subprojects/algoim.wrap new file mode 100644 index 0000000..d4f70f4 --- /dev/null +++ b/subprojects/algoim.wrap @@ -0,0 +1,10 @@ +[wrap-file] +directory = algoim-9c9ca0ef094d8ab0390ed36367a1151b459bbe0a +source_url = https://github.com/algoim/algoim/archive/9c9ca0ef094d8ab0390ed36367a1151b459bbe0a.tar.gz +source_filename = algoim-9c9ca0ef094d8ab0390ed36367a1151b459bbe0a.tar.gz +source_hash = 8b0cbeb3821665c57bbd88426cee7ffca21c95cb2e37edf2f670139b1e50c2cf +patch_directory = algoim + +[provide] +algoim = algoim_dep + diff --git a/subprojects/blitz.wrap b/subprojects/blitz.wrap new file mode 100644 index 0000000..13a5ee4 --- /dev/null +++ b/subprojects/blitz.wrap @@ -0,0 +1,10 @@ +[wrap-file] +directory = blitz-1.0.2 +source_url = https://github.com/blitzpp/blitz/archive/refs/tags/1.0.2.tar.gz +source_filename = blitz-1.0.2.tar.gz +source_hash = 500db9c3b2617e1f03d0e548977aec10d36811ba1c43bb5ef250c0e3853ae1c2 +patch_directory = blitz + +[provide] +blitz = blitz_dep + diff --git a/subprojects/fms.wrap b/subprojects/fms.wrap new file mode 100644 index 0000000..91145d1 --- /dev/null +++ b/subprojects/fms.wrap @@ -0,0 +1,10 @@ +[wrap-file] +directory = FMS-0.2 +source_url = https://github.com/CEED/FMS/archive/refs/tags/v0.2.tar.gz +source_filename = fms-0.2.tar.gz +source_hash = 872489a1325b247968dbb7265b8736660af94121a86c93f7938441ce7478183e +patch_directory = fms + +[provide] +fms = fms_dep + diff --git a/subprojects/gslib.wrap b/subprojects/gslib.wrap new file mode 100644 index 0000000..69edd92 --- /dev/null +++ b/subprojects/gslib.wrap @@ -0,0 +1,7 @@ +[wrap-file] +directory = gslib-1.0.9 +source_url = https://github.com/Nek5000/gslib/archive/refs/tags/v1.0.9.tar.gz +source_filename = gslib-1.0.9.tar.gz +source_hash = 572c78b1ca4ddda75ec85c6299ee0597459f4dbf5170b9faa6de4c435dcdc80b +patch_directory = gslib + diff --git a/subprojects/hypre.wrap b/subprojects/hypre.wrap new file mode 100644 index 0000000..74e7508 --- /dev/null +++ b/subprojects/hypre.wrap @@ -0,0 +1,7 @@ +[wrap-file] +directory = hypre-2.33.0 +source_url = https://github.com/hypre-space/hypre/archive/refs/tags/v2.33.0.tar.gz +source_filename = hypre-2.33.0.tar.gz +source_hash = 0f9103c34bce7a5dcbdb79a502720fc8aab4db9fd0146e0791cde7ec878f27da +patch_directory = hypre + diff --git a/subprojects/libceed.wrap b/subprojects/libceed.wrap new file mode 100644 index 0000000..37aed0d --- /dev/null +++ b/subprojects/libceed.wrap @@ -0,0 +1,10 @@ +[wrap-file] +directory = libCEED-0.12.0 +source_url = https://github.com/CEED/libCEED/archive/refs/tags/v0.12.0.tar.gz +source_filename = libceed-0.12.0.tar.gz +source_hash = 2d94c218f6ab1bed072d11b70b0a0e70a5720fdf3d61db924e89909424a2f93e +patch_directory = libceed + +[provide] +libceed = libceed_dep + diff --git a/subprojects/metis.wrap b/subprojects/metis.wrap new file mode 100644 index 0000000..0ba375e --- /dev/null +++ b/subprojects/metis.wrap @@ -0,0 +1,7 @@ +[wrap-file] +directory = tpls-b60352fbe9675d374b00828055e55be4584c7995 +source_url = https://github.com/mfem/tpls/archive/b60352fbe9675d374b00828055e55be4584c7995.tar.gz +source_filename = mfem-tpls-b60352f.tar.gz +source_hash = 026192230134b8e8028ef4969553f180c7f64fca8a156993d2db38868536f71f +patch_directory = metis + diff --git a/subprojects/mfem.wrap b/subprojects/mfem.wrap new file mode 100644 index 0000000..f9682ce --- /dev/null +++ b/subprojects/mfem.wrap @@ -0,0 +1,7 @@ +[wrap-file] +directory = mfem-4.10 +source_url = https://github.com/mfem/mfem/archive/refs/tags/v4.10.tar.gz +source_filename = mfem-4.10.tar.gz +source_hash = d5aabe991b8b5569aa26e2b5d4b59ac617ee10ebac3d45fd6ff9c74b2c1a47dd +patch_directory = mfem + diff --git a/subprojects/mpich.wrap b/subprojects/mpich.wrap new file mode 100644 index 0000000..3431973 --- /dev/null +++ b/subprojects/mpich.wrap @@ -0,0 +1,7 @@ +[wrap-file] +directory = mpich-4.3.2 +source_url = https://www.mpich.org/static/downloads/4.3.2/mpich-4.3.2.tar.gz +source_filename = mpich-4.3.2.tar.gz +source_hash = 47d774587a7156a53752218c811c852e70ac44db9c502dc3f399b4cb817e3818 +patch_directory = mpich + diff --git a/subprojects/nanobind.wrap b/subprojects/nanobind.wrap new file mode 100644 index 0000000..7e250a2 --- /dev/null +++ b/subprojects/nanobind.wrap @@ -0,0 +1,15 @@ +[wrap-file] +directory = nanobind-2.15.0 +source_url = https://github.com/wjakob/nanobind/archive/refs/tags/v2.15.0.tar.gz +source_filename = nanobind-2.15.0.tar.gz +source_hash = 36c8760b3acb25643cd89d549782d8c67bd82ce54c6238608787c22a34fd490f +source_fallback_url = https://wrapdb.mesonbuild.com/v2/nanobind_2.15.0-1/get_source/nanobind-2.15.0.tar.gz +patch_filename = nanobind_2.15.0-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/nanobind_2.15.0-1/get_patch +patch_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/nanobind_2.15.0-1/nanobind_2.15.0-1_patch.zip +patch_hash = 4a3956c67792830e0c85dff3cf88b30a5547bd087c33842fa5f1596ca86ccabf +wrapdb_version = 2.15.0-1 + +[provide] +dependency_names = nanobind + diff --git a/subprojects/packagefiles/algoim/meson.build b/subprojects/packagefiles/algoim/meson.build new file mode 100644 index 0000000..95ab753 --- /dev/null +++ b/subprojects/packagefiles/algoim/meson.build @@ -0,0 +1,4 @@ +project('algoim-source') +algoim_source_dir = meson.current_source_dir() +algoim_dep = declare_dependency(variables: {'source_dir': algoim_source_dir}) + diff --git a/subprojects/packagefiles/blitz/meson.build b/subprojects/packagefiles/blitz/meson.build new file mode 100644 index 0000000..18d910a --- /dev/null +++ b/subprojects/packagefiles/blitz/meson.build @@ -0,0 +1,3 @@ +project('blitz-source', version: '1.0.2') +blitz_source_dir = meson.current_source_dir() +blitz_dep = declare_dependency(variables: {'source_dir': blitz_source_dir}) diff --git a/subprojects/packagefiles/fms/meson.build b/subprojects/packagefiles/fms/meson.build new file mode 100644 index 0000000..fece70b --- /dev/null +++ b/subprojects/packagefiles/fms/meson.build @@ -0,0 +1,3 @@ +project('fms-source', version: '0.2') +fms_source_dir = meson.current_source_dir() +fms_dep = declare_dependency(variables: {'source_dir': fms_source_dir}) diff --git a/subprojects/packagefiles/gslib/meson.build b/subprojects/packagefiles/gslib/meson.build new file mode 100644 index 0000000..4e8c9bc --- /dev/null +++ b/subprojects/packagefiles/gslib/meson.build @@ -0,0 +1,3 @@ +project('gslib-source', 'c', version: '1.0.9', meson_version: '>=1.5.0') +gslib_source_dir = meson.current_source_dir() + diff --git a/subprojects/packagefiles/hypre/meson.build b/subprojects/packagefiles/hypre/meson.build new file mode 100644 index 0000000..d8933fb --- /dev/null +++ b/subprojects/packagefiles/hypre/meson.build @@ -0,0 +1,3 @@ +project('hypre-source', 'c', version: '2.33.0', meson_version: '>=1.5.0') +hypre_source_dir = meson.current_source_dir() + diff --git a/subprojects/packagefiles/libceed/meson.build b/subprojects/packagefiles/libceed/meson.build new file mode 100644 index 0000000..0087a14 --- /dev/null +++ b/subprojects/packagefiles/libceed/meson.build @@ -0,0 +1,4 @@ +project('libceed-source', version: '0.12.0') +libceed_source_dir = meson.current_source_dir() +libceed_dep = declare_dependency(variables: {'source_dir': libceed_source_dir}) + diff --git a/subprojects/packagefiles/metis/meson.build b/subprojects/packagefiles/metis/meson.build new file mode 100644 index 0000000..47c33bd --- /dev/null +++ b/subprojects/packagefiles/metis/meson.build @@ -0,0 +1,2 @@ +project('metis-source', 'c', version: '5.1.0', meson_version: '>=1.5.0') +metis_source_dir = meson.current_source_dir() diff --git a/subprojects/packagefiles/mfem/meson.build b/subprojects/packagefiles/mfem/meson.build new file mode 100644 index 0000000..6075495 --- /dev/null +++ b/subprojects/packagefiles/mfem/meson.build @@ -0,0 +1,3 @@ +project('mfem-source', 'cpp', version: '4.10', meson_version: '>=1.5.0') +mfem_source_dir = meson.current_source_dir() + diff --git a/subprojects/packagefiles/mpich/meson.build b/subprojects/packagefiles/mpich/meson.build new file mode 100644 index 0000000..dfffd08 --- /dev/null +++ b/subprojects/packagefiles/mpich/meson.build @@ -0,0 +1,3 @@ +project('mpich-source', 'c', version: '4.3.2', meson_version: '>=1.5.0') +mpich_source_dir = meson.current_source_dir() + diff --git a/subprojects/packagefiles/sundials/meson.build b/subprojects/packagefiles/sundials/meson.build new file mode 100644 index 0000000..15a7d73 --- /dev/null +++ b/subprojects/packagefiles/sundials/meson.build @@ -0,0 +1,3 @@ +project('sundials-source', ['c', 'cpp'], version: '7.8.0', meson_version: '>=1.5.0') +sundials_source_dir = meson.current_source_dir() + diff --git a/subprojects/packagefiles/zlib/meson.build b/subprojects/packagefiles/zlib/meson.build new file mode 100644 index 0000000..f9da950 --- /dev/null +++ b/subprojects/packagefiles/zlib/meson.build @@ -0,0 +1,3 @@ +project('zlib-source', 'c', version: '1.3.1', meson_version: '>=1.5.0') +zlib_source_dir = meson.current_source_dir() + diff --git a/subprojects/robin-map.wrap b/subprojects/robin-map.wrap new file mode 100644 index 0000000..65deb02 --- /dev/null +++ b/subprojects/robin-map.wrap @@ -0,0 +1,15 @@ +[wrap-file] +directory = robin-map-1.4.1 +source_url = https://github.com/Tessil/robin-map/archive/refs/tags/v1.4.1.tar.gz +source_filename = robin-map-1.4.1.tar.gz +source_hash = 0e3f53a377fdcdc5f9fed7a4c0d4f99e82bbb64175233bd13427fef9a771f4a1 +source_fallback_url = https://wrapdb.mesonbuild.com/v2/robin-map_1.4.1-1/get_source/robin-map-1.4.1.tar.gz +patch_filename = robin-map_1.4.1-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/robin-map_1.4.1-1/get_patch +patch_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/robin-map_1.4.1-1/robin-map_1.4.1-1_patch.zip +patch_hash = 8137d5283004db445d744d603e600119047fed865cbd740f8fec4421dc1a45b6 +wrapdb_version = 1.4.1-1 + +[provide] +dependency_names = tsl-robin-map + diff --git a/subprojects/sundials.wrap b/subprojects/sundials.wrap new file mode 100644 index 0000000..a63afe3 --- /dev/null +++ b/subprojects/sundials.wrap @@ -0,0 +1,7 @@ +[wrap-file] +directory = sundials-7.8.0 +source_url = https://github.com/LLNL/sundials/archive/refs/tags/v7.8.0.tar.gz +source_filename = sundials-7.8.0.tar.gz +source_hash = c2ca15a16d7ae0d79cf1c2c288335f16b0f1c2c6d349db20aff2ce3fd1296d5a +patch_directory = sundials + diff --git a/subprojects/zlib.wrap b/subprojects/zlib.wrap new file mode 100644 index 0000000..759eaa7 --- /dev/null +++ b/subprojects/zlib.wrap @@ -0,0 +1,7 @@ +[wrap-file] +directory = zlib-1.3.1 +source_url = https://github.com/madler/zlib/archive/refs/tags/v1.3.1.tar.gz +source_filename = zlib-1.3.1.tar.gz +source_hash = 17e88863f3600672ab49182f217281b6fc4d3c762bde361935e436a95214d05c +patch_directory = zlib + diff --git a/tests/meson.build b/tests/meson.build new file mode 100644 index 0000000..c1a7e77 --- /dev/null +++ b/tests/meson.build @@ -0,0 +1,86 @@ +if get_option('build_tests') and get_option('build_examples') + runtime_environment = environment() + if ( + mfem_runtime_prefix != '' and dependency_prefix != '' and + not wheel_carries_native_bundle + ) + runtime_environment.prepend('PATH', dependency_prefix / 'bin') + if host_machine.system() == 'darwin' + runtime_environment.prepend('DYLD_LIBRARY_PATH', dependency_prefix / 'lib64') + runtime_environment.prepend('DYLD_LIBRARY_PATH', dependency_prefix / 'lib') + elif host_machine.system() != 'windows' and not is_wasm + runtime_environment.prepend('LD_LIBRARY_PATH', dependency_prefix / 'lib64') + runtime_environment.prepend('LD_LIBRARY_PATH', dependency_prefix / 'lib') + endif + endif + if mfem_runtime_prefix != '' + runtime_environment.prepend('PATH', mfem_runtime_prefix / 'bin') + if host_machine.system() == 'darwin' + runtime_environment.prepend('DYLD_LIBRARY_PATH', mfem_runtime_prefix / 'lib64') + runtime_environment.prepend('DYLD_LIBRARY_PATH', mfem_runtime_prefix / 'lib') + elif host_machine.system() != 'windows' and not is_wasm + runtime_environment.prepend('LD_LIBRARY_PATH', mfem_runtime_prefix / 'lib64') + runtime_environment.prepend('LD_LIBRARY_PATH', mfem_runtime_prefix / 'lib') + endif + endif + + test( + 'serial-poisson', + serial_example, + env: runtime_environment, + timeout: 120, + ) + + if mfem_has_mpi + if mpi_launcher_from_dependency and not wheel_carries_native_bundle + mpi_launcher_program = find_program( + dependency_prefix / 'bin' / 'mpiexec', + dependency_prefix / 'bin' / 'mpirun', + required: true, + ) + mpi_launcher = mpi_launcher_program.full_path() + elif mfem_runtime_prefix != '' + mpi_launcher = mfem_runtime_prefix / 'bin' / 'mpiexec' + else + mpi_launcher_program = find_program('mpiexec', 'mpirun', required: true) + mpi_launcher = mpi_launcher_program.full_path() + endif + test( + 'parallel-hypre-boomeramg', + python_build, + args: [ + files('../tools/run_mpi_test.py'), + '--launcher', mpi_launcher, + '--processes', '2', + parallel_example, + ], + env: runtime_environment, + timeout: 180, + ) + endif +endif + +if get_option('build_tests') and get_option('build_python') + python_test_environment = environment() + python_test_environment.prepend('PYTHONPATH', python_extension_dir) + if mfem_runtime_prefix != '' + if host_machine.system() == 'darwin' + python_test_environment.prepend('DYLD_LIBRARY_PATH', mfem_runtime_prefix / 'lib64') + python_test_environment.prepend('DYLD_LIBRARY_PATH', mfem_runtime_prefix / 'lib') + elif host_machine.system() != 'windows' + python_test_environment.prepend('LD_LIBRARY_PATH', mfem_runtime_prefix / 'lib64') + python_test_environment.prepend('LD_LIBRARY_PATH', mfem_runtime_prefix / 'lib') + endif + endif + test( + 'python-nanobind', + python_build, + args: [ + '-c', + 'import _core; assert _core.serial_poisson_dofs(3, 1) > 0; assert _core.capabilities()["hypre"] == _core.capabilities()["mpi"]', + ], + depends: python_extension, + env: python_test_environment, + timeout: 120, + ) +endif diff --git a/tools/build_mfem_bundle.py b/tools/build_mfem_bundle.py new file mode 100644 index 0000000..e6643f3 --- /dev/null +++ b/tools/build_mfem_bundle.py @@ -0,0 +1,2387 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +from pathlib import Path +import platform +import re +import shlex +import shutil +import subprocess +import sys +import tarfile +from typing import Iterable, Mapping, Sequence + + +CMAKE_FEATURES = { + "mpi": "MFEM_USE_MPI", + "metis": "MFEM_USE_METIS", + "exceptions": "MFEM_USE_EXCEPTIONS", + "zlib": "MFEM_USE_ZLIB", + "libunwind": "MFEM_USE_LIBUNWIND", + "lapack": "MFEM_USE_LAPACK", + "thread_safe": "MFEM_THREAD_SAFE", + "openmp": "MFEM_USE_OPENMP", + "legacy_openmp": "MFEM_USE_LEGACY_OPENMP", + "memalloc": "MFEM_USE_MEMALLOC", + "sundials": "MFEM_USE_SUNDIALS", + "suitesparse": "MFEM_USE_SUITESPARSE", + "superlu": "MFEM_USE_SUPERLU", + "superlu5": "MFEM_USE_SUPERLU5", + "mumps": "MFEM_USE_MUMPS", + "strumpack": "MFEM_USE_STRUMPACK", + "cudss": "MFEM_USE_CUDSS", + "ginkgo": "MFEM_USE_GINKGO", + "amgx": "MFEM_USE_AMGX", + "magma": "MFEM_USE_MAGMA", + "gnutls": "MFEM_USE_GNUTLS", + "gslib": "MFEM_USE_GSLIB", + "hdf5": "MFEM_USE_HDF5", + "netcdf": "MFEM_USE_NETCDF", + "petsc": "MFEM_USE_PETSC", + "slepc": "MFEM_USE_SLEPC", + "mpfr": "MFEM_USE_MPFR", + "sidre": "MFEM_USE_SIDRE", + "fms": "MFEM_USE_FMS", + "conduit": "MFEM_USE_CONDUIT", + "pumi": "MFEM_USE_PUMI", + "hiop": "MFEM_USE_HIOP", + "cuda": "MFEM_USE_CUDA", + "hip": "MFEM_USE_HIP", + "occa": "MFEM_USE_OCCA", + "raja": "MFEM_USE_RAJA", + "ceed": "MFEM_USE_CEED", + "umpire": "MFEM_USE_UMPIRE", + "simd": "MFEM_USE_SIMD", + "adios2": "MFEM_USE_ADIOS2", + "caliper": "MFEM_USE_CALIPER", + "algoim": "MFEM_USE_ALGOIM", + "mkl_cpardiso": "MFEM_USE_MKL_CPARDISO", + "mkl_pardiso": "MFEM_USE_MKL_PARDISO", + "adforward": "MFEM_USE_ADFORWARD", + "codipack": "MFEM_USE_CODIPACK", + "benchmark": "MFEM_USE_BENCHMARK", + "parelag": "MFEM_USE_PARELAG", + "tribol": "MFEM_USE_TRIBOL", + "enzyme": "MFEM_USE_ENZYME", + "moonolith": "MFEM_USE_MOONOLITH", + "simmetrix": "MFEM_USE_SIMMETRIX", +} + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--stamp", type=Path) + parser.add_argument("--source", type=Path, required=True) + parser.add_argument("--work-dir", type=Path, required=True) + parser.add_argument("--prefix", type=Path, required=True) + parser.add_argument("--cmake", required=True) + parser.add_argument("--cc", required=True) + parser.add_argument("--cxx", required=True) + parser.add_argument("--cc-launcher", action="append", default=[]) + parser.add_argument("--cxx-launcher", action="append", default=[]) + parser.add_argument("--buildtype", required=True) + parser.add_argument("--jobs", type=int, required=True) + parser.add_argument("--host-system", required=True) + parser.add_argument("--cross-build", choices=("true", "false"), required=True) + parser.add_argument("--library-kind", choices=("static", "shared"), required=True) + parser.add_argument("--cuda-arch", default="native") + parser.add_argument("--hip-arch", default="") + parser.add_argument("--precision", choices=("single", "double"), default="double") + parser.add_argument("--dependency-prefix", default="") + parser.add_argument("--allow-system", choices=("true", "false"), required=True) + parser.add_argument( + "--vendor-dependency-prefix", choices=("true", "false"), required=True + ) + parser.add_argument("--c-arg", action="append", default=[]) + parser.add_argument("--cxx-arg", action="append", default=[]) + parser.add_argument("--c-link-arg", action="append", default=[]) + parser.add_argument("--cxx-link-arg", action="append", default=[]) + parser.add_argument("--feature", action="append", default=[]) + parser.add_argument("--mpich-source", type=Path) + parser.add_argument("--hypre-source", type=Path) + parser.add_argument("--metis-source", type=Path) + parser.add_argument("--gslib-source", type=Path) + parser.add_argument("--zlib-source", type=Path) + parser.add_argument("--sundials-source", type=Path) + parser.add_argument("--libceed-source", type=Path) + parser.add_argument("--fms-source", type=Path) + parser.add_argument("--algoim-source", type=Path) + parser.add_argument("--blitz-source", type=Path) + return parser.parse_args() + + +def run( + command: Sequence[os.PathLike[str] | str], + *, + cwd: Path | None = None, + env: Mapping[str, str] | None = None, +) -> None: + rendered = [os.fspath(item) for item in command] + print(f"[mfem-bundle] {shlex.join(rendered)}", flush=True) + subprocess.run(rendered, cwd=cwd, env=env, check=True) + + +def output(command: Sequence[str]) -> str: + return subprocess.run( + command, + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ).stdout.strip() + + +def find_program_in_environment(name: str, env: Mapping[str, str]) -> str | None: + """Find a tool using the deliberately constructed bundle PATH.""" + return shutil.which(name, path=env.get("PATH")) + + +def toolchain_root(name: str, env: Mapping[str, str]) -> str: + executable = find_program_in_environment(name, env) + if not executable: + raise RuntimeError(f"{name} is required by the selected GPU backend") + # CUDA and ROCm both install their compiler drivers under /bin. + return os.fspath(Path(executable).resolve().parent.parent) + + +def bundle_has(args: argparse.Namespace, relative_path: str) -> bool: + """Check both the generated prefix and an explicit TPL prefix.""" + roots = [args.prefix] + if args.dependency_prefix: + roots.append(Path(args.dependency_prefix)) + return any((root / relative_path).exists() for root in roots) + + +def bundle_root(args: argparse.Namespace, relative_path: str) -> Path: + """Return the prefix that actually provides a selected artifact.""" + if (args.prefix / relative_path).exists(): + return args.prefix + if args.dependency_prefix: + dependency_root = Path(args.dependency_prefix) + if (dependency_root / relative_path).exists(): + return dependency_root + return args.prefix + + +def bundle_find(args: argparse.Namespace, pattern: str) -> Path | None: + roots = [args.prefix] + if args.dependency_prefix: + roots.append(Path(args.dependency_prefix)) + for root in roots: + matches = sorted(root.glob(pattern)) + if matches: + return matches[0] + return None + + +def remove_tree(path: Path) -> None: + resolved = path.resolve() + if not resolved.name or resolved == resolved.parent: + raise RuntimeError(f"refusing to remove broad path: {resolved}") + shutil.rmtree(resolved, ignore_errors=True) + + +def copy_prefix(source: Path, destination: Path) -> None: + """Seed the private prefix from a user-supplied coherent TPL prefix.""" + if not source.is_dir(): + raise RuntimeError(f"dependency prefix does not exist: {source}") + source = source.resolve() + destination = destination.resolve() + if ( + source == destination + or source.is_relative_to(destination) + or destination.is_relative_to(source) + ): + raise RuntimeError( + "dependency prefix and private build prefix must be disjoint" + ) + broad_prefixes = { + Path("/").resolve(), + Path("/usr").resolve(), + Path("/usr/local").resolve(), + Path("/opt").resolve(), + Path("/opt/homebrew").resolve(), + Path.home().resolve(), + Path(sys.prefix).resolve(), + } + if ( + source in broad_prefixes + or (source / "conda-meta").is_dir() + or (source / "pyvenv.cfg").is_file() + ): + raise RuntimeError( + "refusing to vendor a system, Conda, or virtual-environment prefix; " + "dependency_prefix must be a dedicated redistributable TPL prefix" + ) + for candidate in source.rglob("*"): + if not candidate.is_symlink(): + continue + raw_target = Path(os.readlink(candidate)) + if raw_target.is_absolute(): + raise RuntimeError( + f"dependency_prefix contains an absolute symlink: {candidate}" + ) + resolved_target = (candidate.parent / raw_target).resolve() + if not resolved_target.is_relative_to(source): + raise RuntimeError( + f"dependency_prefix symlink escapes the prefix: {candidate}" + ) + for child in source.iterdir(): + target = destination / child.name + if child.is_dir(): + shutil.copytree(child, target, dirs_exist_ok=True, symlinks=True) + else: + shutil.copy2(child, target, follow_symlinks=False) + + +def write_text_atomic(path: Path, text: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + temporary = path.with_suffix(path.suffix + ".tmp") + temporary.write_text(text, encoding="utf-8") + temporary.replace(path) + + +def prefix_manifest(prefix: Path) -> str: + """Hash the observable shape of an externally managed dependency prefix.""" + digest = hashlib.sha256() + for path in sorted(prefix.rglob("*")): + relative = os.fspath(path.relative_to(prefix)) + digest.update(relative.encode("utf-8", errors="surrogateescape")) + if path.is_symlink(): + digest.update(b"L") + digest.update(os.readlink(path).encode("utf-8", errors="surrogateescape")) + elif path.is_file(): + stat = path.stat() + digest.update(f"F{stat.st_size}:{stat.st_mtime_ns}".encode("ascii")) + if path.suffix.lower() in {".cmake", ".h", ".hpp", ".pc", ".json"}: + digest.update(path.read_bytes()) + elif path.is_dir(): + digest.update(b"D") + return digest.hexdigest() + + +def cmake_build_type(meson_buildtype: str) -> str: + return { + "plain": "Release", + "debug": "Debug", + "debugoptimized": "RelWithDebInfo", + "release": "Release", + "minsize": "MinSizeRel", + "custom": "Release", + }.get(meson_buildtype, "Release") + + +def compiler_flags(build_type: str) -> str: + return { + "Debug": "-O0 -g -fPIC", + "RelWithDebInfo": "-O2 -g -DNDEBUG -fPIC", + "MinSizeRel": "-Os -DNDEBUG -fPIC", + "Release": "-O3 -DNDEBUG -fPIC", + }[build_type] + + +def joined_flags(values: Sequence[str]) -> str: + return " ".join(shlex.quote(value) for value in values) + + +def hypre_version_integer(*prefixes: Path) -> int | None: + """Read HYPRE's release macros without executing target code.""" + for prefix in prefixes: + header = prefix / "include" / "HYPRE_config.h" + if not header.is_file(): + continue + text = header.read_text(encoding="utf-8") + numeric = re.search( + r"^\s*#\s*define\s+HYPRE_RELEASE_NUMBER\s+([0-9]+)", + text, + flags=re.MULTILINE, + ) + if numeric: + return int(numeric.group(1)) + dotted = re.search( + r'^\s*#\s*define\s+HYPRE_RELEASE_VERSION\s+"' + r"([0-9]+)\.([0-9]+)\.([0-9]+)", + text, + flags=re.MULTILINE, + ) + if dotted: + major, minor, patch = (int(value) for value in dotted.groups()) + return major * 10000 + minor * 100 + patch + return None + + +def parse_features(values: Iterable[str]) -> dict[str, bool]: + result: dict[str, bool] = {} + for item in values: + key, separator, value = item.partition("=") + if not separator or value not in {"ON", "OFF"}: + raise ValueError(f"invalid --feature value: {item!r}") + result[key] = value == "ON" + missing = set(CMAKE_FEATURES).difference(result) + if missing: + raise ValueError(f"missing feature states: {', '.join(sorted(missing))}") + return result + + +def runtime_rpath(host_system: str) -> str: + if host_system == "darwin": + return "@loader_path" + if host_system not in {"windows", "emscripten"}: + return "$ORIGIN" + return "" + + +def common_cmake_args( + args: argparse.Namespace, + build_type: str, + *, + c_compiler: str | None = None, + cxx_compiler: str | None = None, +) -> list[str]: + prefix_paths = [os.fspath(args.prefix)] + if args.dependency_prefix: + prefix_paths.append(args.dependency_prefix) + result = [ + f"-DCMAKE_BUILD_TYPE={build_type}", + f"-DCMAKE_INSTALL_PREFIX={args.prefix}", + "-DCMAKE_INSTALL_LIBDIR=lib", + "-DCMAKE_POSITION_INDEPENDENT_CODE=ON", + f"-DCMAKE_PREFIX_PATH={';'.join(prefix_paths)}", + "-DCMAKE_FIND_PACKAGE_PREFER_CONFIG=ON", + "-DCMAKE_FIND_USE_PACKAGE_REGISTRY=OFF", + f"-DBUILD_SHARED_LIBS={'ON' if args.library_kind == 'shared' else 'OFF'}", + ] + if c_compiler: + result.append(f"-DCMAKE_C_COMPILER={c_compiler}") + if c_compiler == args.cc and args.cc_launcher: + result.append(f"-DCMAKE_C_COMPILER_LAUNCHER={';'.join(args.cc_launcher)}") + if cxx_compiler: + result.append(f"-DCMAKE_CXX_COMPILER={cxx_compiler}") + if cxx_compiler == args.cxx and args.cxx_launcher: + result.append( + f"-DCMAKE_CXX_COMPILER_LAUNCHER={';'.join(args.cxx_launcher)}" + ) + c_flags = list(args.c_arg) + cxx_flags = list(args.cxx_arg) + link_flags = list(dict.fromkeys([*args.c_link_arg, *args.cxx_link_arg])) + if args.host_system == "darwin": + link_flags.append("-Wl,-headerpad_max_install_names") + if args.host_system == "emscripten": + if "-fwasm-exceptions" not in cxx_flags: + cxx_flags.append("-fwasm-exceptions") + result.extend( + [ + "-DCMAKE_C_FLAGS_RELEASE=-O1 -DNDEBUG", + "-DCMAKE_CXX_FLAGS_RELEASE=-O1 -DNDEBUG", + "-DCMAKE_C_FLAGS_RELWITHDEBINFO=-O1 -g -DNDEBUG", + "-DCMAKE_CXX_FLAGS_RELWITHDEBINFO=-O1 -g -DNDEBUG", + "-DCMAKE_C_FLAGS_MINSIZEREL=-O1 -DNDEBUG", + "-DCMAKE_CXX_FLAGS_MINSIZEREL=-O1 -DNDEBUG", + "-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=BOTH", + "-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH", + "-DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=BOTH", + ] + ) + for flag in ("-fwasm-exceptions", "-sALLOW_MEMORY_GROWTH=1"): + if flag not in link_flags: + link_flags.append(flag) + if c_flags: + result.append(f"-DCMAKE_C_FLAGS={joined_flags(c_flags)}") + if cxx_flags: + result.append(f"-DCMAKE_CXX_FLAGS={joined_flags(cxx_flags)}") + if link_flags: + rendered_link_flags = joined_flags(link_flags) + result.append(f"-DCMAKE_EXE_LINKER_FLAGS={rendered_link_flags}") + result.append(f"-DCMAKE_SHARED_LINKER_FLAGS={rendered_link_flags}") + rpath = runtime_rpath(args.host_system) + if rpath: + result.extend( + [ + f"-DCMAKE_INSTALL_RPATH={rpath}", + "-DCMAKE_INSTALL_RPATH_USE_LINK_PATH=OFF", + ] + ) + if args.allow_system == "false": + ignored = ["/usr", "/usr/local", "/opt/homebrew", "/opt/local"] + if sys.prefix not in {"/usr", "/usr/local"}: + ignored.append(sys.prefix) + if args.dependency_prefix: + explicit_prefix = Path(args.dependency_prefix).resolve() + ignored = [ + candidate + for candidate in ignored + if not explicit_prefix.is_relative_to(Path(candidate).resolve()) + ] + result.append(f"-DCMAKE_IGNORE_PREFIX_PATH={';'.join(ignored)}") + return result + + +def cmake_configure( + args: argparse.Namespace, + source: Path, + build: Path, + options: Sequence[str], + env: Mapping[str, str], +) -> None: + command: list[str] = [args.cmake, "-S", os.fspath(source), "-B", os.fspath(build)] + if args.host_system == "emscripten": + emcmake = shutil.which("emcmake") + if not emcmake: + raise RuntimeError("Emscripten target requires emcmake in PATH") + command.insert(0, emcmake) + command.extend(options) + run(command, env=env) + + +def cmake_build_install( + args: argparse.Namespace, + source: Path, + build: Path, + options: Sequence[str], + env: Mapping[str, str], +) -> None: + build.mkdir(parents=True, exist_ok=True) + cmake_configure(args, source, build, options, env) + run([args.cmake, "--build", build, "--parallel", str(args.jobs)], env=env) + run([args.cmake, "--install", build], env=env) + + +def prepare_mpich_source(args: argparse.Namespace) -> Path: + """Copy MPICH and disable Hydra's non-relocatable system config default.""" + if not args.mpich_source: + raise RuntimeError("MPI is enabled but the MPICH source wrap was not provided") + source = args.work_dir / "mpich-source" + shutil.copytree(args.mpich_source, source, dirs_exist_ok=True, symlinks=True) + + hydra_config_relative = "@sysconfdir@/mpiexec.hydra.conf" + for relative_path in ( + "src/pm/hydra/mpiexec/Makefile.mk", + "src/pm/hydra/Makefile.in", + ): + makefile = source / relative_path + text = makefile.read_text(encoding="utf-8") + matching_lines = [ + line for line in text.splitlines() if hydra_config_relative in line + ] + if len(matching_lines) != 1: + raise RuntimeError( + f"MPICH's expected Hydra config definition was not found in {relative_path}" + ) + definition_line = matching_lines[0] + retained_flags, separator, definition = definition_line.partition( + " -DHYDRA_CONF_FILE=" + ) + if not separator or definition.count(hydra_config_relative) != 1: + raise RuntimeError( + f"MPICH's Hydra config definition has an unexpected form in {relative_path}" + ) + write_text_atomic(makefile, text.replace(definition_line, retained_flags, 1)) + + parameters_source = source / "src/pm/hydra/mpiexec/get_parameters.c" + text = parameters_source.read_text(encoding="utf-8") + hard_coded_probe = """ /* Check if there's a config file in the hard-coded location */ + conf_file = MPL_strdup(HYDRA_CONF_FILE); + HYDU_ERR_CHKANDJUMP(status, NULL == conf_file, HYD_INTERNAL_ERROR, "strdup failed\\n"); + ret = open(conf_file, O_RDONLY); + if (ret < 0) { + MPL_free(conf_file); + } else { + close(ret); + HYD_ui_mpich_info.config_file = conf_file; + goto config_file_check_exit; + } +""" + if text.count(hard_coded_probe) != 1: + raise RuntimeError("MPICH's expected Hydra hard-coded config probe was not found") + write_text_atomic(parameters_source, text.replace(hard_coded_probe, "", 1)) + return source + + +def build_mpich( + args: argparse.Namespace, + features: Mapping[str, bool], + env: dict[str, str], + build_type: str, +) -> tuple[str, str]: + source = prepare_mpich_source(args) + build = args.work_dir / "mpich" + build.mkdir(parents=True, exist_ok=True) + make = shutil.which("gmake") or shutil.which("make") + if not make: + raise RuntimeError("building MPICH requires make or gmake") + + local_env = dict(env) + mpich_link_flags = [f"-L{args.prefix / 'lib'}"] + mpich_link_flags.extend([*args.c_link_arg, *args.cxx_link_arg]) + if args.host_system == "darwin": + mpich_link_flags.append("-Wl,-headerpad_max_install_names") + local_env.update( + { + "CC": shlex.join([*args.cc_launcher, args.cc]), + "CXX": shlex.join([*args.cxx_launcher, args.cxx]), + "CFLAGS": " ".join( + filter(None, [compiler_flags(build_type), joined_flags(args.c_arg)]) + ), + "CXXFLAGS": " ".join( + filter(None, [compiler_flags(build_type), joined_flags(args.cxx_arg)]) + ), + "CPPFLAGS": f"-I{args.prefix / 'include'}", + "LDFLAGS": " ".join(mpich_link_flags), + } + ) + configure = [ + os.fspath(source / "configure"), + f"--prefix={args.prefix}", + "--enable-shared", + "--disable-static", + "--with-pic", + "--with-hwloc=embedded", + "--with-wrapper-dl-type=none", + ] + if args.host_system == "darwin": + configure.append("--with-device=ch3:sock") + else: + configure.extend( + [ + "--with-device=ch4:ofi:sockets", + "--with-libfabric=embedded", + "--with-ucx=no", + "--with-ze=no", + ] + ) + needs_fortran = features["mumps"] or features["strumpack"] + configure.append("--enable-fortran=all" if needs_fortran else "--disable-fortran") + if features["cuda"]: + nvcc = find_program_in_environment("nvcc", env) + if not nvcc: + raise RuntimeError("CUDA-enabled MPI requested but nvcc is unavailable") + configure.append(f"--with-cuda={Path(nvcc).resolve().parent.parent}") + elif features["hip"]: + hipcc = find_program_in_environment("hipcc", env) + if not hipcc: + raise RuntimeError("HIP-enabled MPI requested but hipcc is unavailable") + configure.append(f"--with-hip={Path(hipcc).resolve().parent.parent}") + run(configure, cwd=build, env=local_env) + run([make, f"-j{args.jobs}"], cwd=build, env=local_env) + run([make, "install"], cwd=build, env=local_env) + mpicc = os.fspath(args.prefix / "bin" / "mpicc") + mpicxx = os.fspath(args.prefix / "bin" / "mpicxx") + return mpicc, mpicxx + + +def build_zlib( + args: argparse.Namespace, + env: Mapping[str, str], + build_type: str, +) -> None: + if not args.zlib_source: + raise RuntimeError("zlib is enabled but its source wrap was not provided") + options = common_cmake_args( + args, build_type, c_compiler=args.cc, cxx_compiler=args.cxx + ) + ["-DZLIB_BUILD_EXAMPLES=OFF"] + if args.host_system == "emscripten": + build = args.work_dir / "zlib" + build.mkdir(parents=True, exist_ok=True) + cmake_configure(args, args.zlib_source, build, options, env) + run( + [ + args.cmake, + "--build", + build, + "--target", + "zlibstatic", + "--parallel", + str(args.jobs), + ], + env=env, + ) + run([args.cmake, "--install", build], env=env) + else: + cmake_build_install( + args, args.zlib_source, args.work_dir / "zlib", options, env + ) + + +def build_metis( + args: argparse.Namespace, + env: Mapping[str, str], + build_type: str, +) -> None: + if not args.metis_source: + raise RuntimeError("METIS is enabled but its source wrap was not provided") + archive_name = "metis-5.1.0.tar.gz" + archive = args.metis_source / archive_name + if not archive.is_file(): + raise RuntimeError(f"missing METIS source archive: {archive}") + source_parent = args.work_dir / "metis-source" + source = source_parent / "metis-5.1.0" + source_parent.mkdir(parents=True, exist_ok=True) + with tarfile.open(archive, "r:gz") as handle: + if sys.version_info >= (3, 12): + handle.extractall(source_parent, filter="data") + else: + handle.extractall(source_parent) + options = common_cmake_args( + args, build_type, c_compiler=args.cc, cxx_compiler=args.cxx + ) + [ + "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", + f"-DGKLIB_PATH={source / 'GKlib'}", + f"-DSHARED={'ON' if args.library_kind == 'shared' else 'OFF'}", + "-DOPENMP=OFF", + ] + cmake_build_install( + args, + source, + args.work_dir / "metis", + options, + env, + ) + legal_dir = args.prefix / "share" / "licenses" / "meson-mfem-template" + legal_dir.mkdir(parents=True, exist_ok=True) + shutil.copy2(source / "LICENSE.txt", legal_dir / "metis-LICENSE.txt") + + +def build_hypre( + args: argparse.Namespace, + features: Mapping[str, bool], + env: Mapping[str, str], + build_type: str, + mpicc: str, +) -> None: + if not args.hypre_source: + raise RuntimeError("MPI is enabled but the Hypre source wrap was not provided") + options = common_cmake_args( + args, + build_type, + c_compiler=mpicc if features["mpi"] else args.cc, + ) + [ + f"-DHYPRE_ENABLE_MPI={'ON' if features['mpi'] else 'OFF'}", + f"-DHYPRE_ENABLE_OPENMP={'ON' if features['openmp'] else 'OFF'}", + f"-DHYPRE_ENABLE_CUDA={'ON' if features['cuda'] else 'OFF'}", + f"-DHYPRE_ENABLE_HIP={'ON' if features['hip'] else 'OFF'}", + f"-DHYPRE_ENABLE_SINGLE={'ON' if args.precision == 'single' else 'OFF'}", + "-DHYPRE_BUILD_EXAMPLES=OFF", + "-DHYPRE_BUILD_TESTS=OFF", + ] + if features["cuda"]: + options.extend( + [ + "-DHYPRE_ENABLE_UNIFIED_MEMORY=ON", + "-DHYPRE_ENABLE_GPU_AWARE_MPI=ON", + f"-DCMAKE_CUDA_ARCHITECTURES={args.cuda_arch}", + ] + ) + if features["hip"] and args.hip_arch: + options.append(f"-DCMAKE_HIP_ARCHITECTURES={args.hip_arch}") + cmake_build_install( + args, + args.hypre_source / "src", + args.work_dir / "hypre", + options, + env, + ) + + +def build_gslib( + args: argparse.Namespace, + features: Mapping[str, bool], + env: Mapping[str, str], + build_type: str, + mpicc: str, +) -> None: + if not args.gslib_source: + raise RuntimeError("GSLIB is enabled but its source wrap was not provided") + source = args.work_dir / "gslib-source" + shutil.copytree(args.gslib_source, source, dirs_exist_ok=True) + make = shutil.which("gmake") or shutil.which("make") + if not make: + raise RuntimeError("building GSLIB requires make or gmake") + gslib_cflags = [compiler_flags(build_type), *args.c_arg] + if args.library_kind == "shared": + gslib_cflags.append("-fPIC") + run( + [ + make, + f"-j{args.jobs}", + f"DESTDIR={args.prefix}", + f"MPI={1 if features['mpi'] else 0}", + f"CC={mpicc if features['mpi'] else shlex.join([*args.cc_launcher, args.cc])}", + f"CFLAGS={' '.join(gslib_cflags)}", + ], + cwd=source, + env=env, + ) + + +def build_sundials( + args: argparse.Namespace, + features: Mapping[str, bool], + env: Mapping[str, str], + build_type: str, + mpicc: str, + mpicxx: str, +) -> None: + if not args.sundials_source: + raise RuntimeError("SUNDIALS is enabled but its source wrap was not provided") + options = common_cmake_args( + args, + build_type, + c_compiler=mpicc if features["mpi"] else args.cc, + cxx_compiler=mpicxx if features["mpi"] else args.cxx, + ) + [ + f"-DSUNDIALS_ENABLE_MPI={'ON' if features['mpi'] else 'OFF'}", + f"-DSUNDIALS_ENABLE_OPENMP={'ON' if features['openmp'] else 'OFF'}", + f"-DSUNDIALS_ENABLE_CUDA={'ON' if features['cuda'] else 'OFF'}", + f"-DSUNDIALS_ENABLE_HIP={'ON' if features['hip'] else 'OFF'}", + "-DSUNDIALS_ENABLE_FORTRAN=OFF", + "-DSUNDIALS_ENABLE_C_EXAMPLES=OFF", + "-DSUNDIALS_ENABLE_CXX_EXAMPLES=OFF", + "-DSUNDIALS_ENABLE_CUDA_EXAMPLES=OFF", + "-DSUNDIALS_ENABLE_EXAMPLES_INSTALL=OFF", + "-DBUILD_STATIC_LIBS=OFF" if args.library_kind == "shared" else "-DBUILD_SHARED_LIBS=OFF", + ] + if features["mpi"]: + options.extend( + [ + f"-DMPI_C_COMPILER={mpicc}", + f"-DMPI_CXX_COMPILER={mpicxx}", + ] + ) + if features["cuda"]: + options.append(f"-DCMAKE_CUDA_ARCHITECTURES={args.cuda_arch}") + if features["hip"] and args.hip_arch: + options.append(f"-DCMAKE_HIP_ARCHITECTURES={args.hip_arch}") + cmake_build_install( + args, + args.sundials_source, + args.work_dir / "sundials", + options, + env, + ) + + +def build_libceed( + args: argparse.Namespace, + features: Mapping[str, bool], + env: Mapping[str, str], + build_type: str, +) -> None: + if not args.libceed_source: + raise RuntimeError("libCEED is enabled but its source wrap was not provided") + source = args.work_dir / "libceed-source" + shutil.copytree(args.libceed_source, source, dirs_exist_ok=True) + relocatable_jit_source = Path(__file__).with_name( + "ceed_jit_source_root_relocatable.c" + ) + if not relocatable_jit_source.is_file(): + raise RuntimeError( + "missing relocatable libCEED JIT source-root implementation" + ) + shutil.copy2( + relocatable_jit_source, + source / "interface" / "ceed-jit-source-root-install.c", + ) + make = shutil.which("gmake") or shutil.which("make") + if not make: + raise RuntimeError("building libCEED requires make or gmake") + command = [make, "-C", os.fspath(source), f"-j{args.jobs}", "install"] + if args.host_system == "emscripten": + emmake = shutil.which("emmake") + if not emmake: + raise RuntimeError("Emscripten libCEED build requires emmake in PATH") + command.insert(0, emmake) + common_language_args = [flag for flag in args.c_arg if flag in args.cxx_arg] + optimization = " ".join( + filter(None, [compiler_flags(build_type), joined_flags(common_language_args)]) + ) + local_env = dict(env) + pic_flag = "" if args.library_kind == "static" else "-fPIC" + local_env["CFLAGS"] = " ".join( + filter( + None, + [ + compiler_flags(build_type), + joined_flags(args.c_arg), + pic_flag, + "-std=c99 -Wall -Wextra -Wno-unused-parameter -MMD -MP", + ], + ) + ) + local_env["CXXFLAGS"] = " ".join( + filter( + None, + [ + compiler_flags(build_type), + joined_flags(args.cxx_arg), + pic_flag, + "-std=c++11 -Wall -Wextra -Wno-unused-parameter -MMD -MP", + ], + ) + ) + link_flags = [*args.c_link_arg, *args.cxx_link_arg] + if args.host_system == "darwin": + link_flags.append("-Wl,-headerpad_max_install_names") + cuda_root = toolchain_root("nvcc", env) if features["cuda"] else "" + rocm_root = toolchain_root("hipcc", env) if features["hip"] else "" + occa_root = args.dependency_prefix or env.get("OCCA_DIR", "") + magma_root = args.dependency_prefix or env.get("MAGMA_DIR", "") + if features["occa"]: + if not occa_root: + raise RuntimeError( + "libCEED's OCCA backend requires dependency_prefix or OCCA_DIR" + ) + root = Path(occa_root) + if not (root / "bin" / "occa").is_file() or not list( + (root / "lib").glob("libocca.*") + ): + raise RuntimeError( + "OCCA was selected, but its executable and lib/libocca were not " + f"found under {root}; libCEED 0.12 requires the lib layout" + ) + if features["magma"]: + if not magma_root: + raise RuntimeError( + "libCEED's MAGMA backend requires dependency_prefix or MAGMA_DIR" + ) + root = Path(magma_root) + if not list((root / "lib").glob("libmagma.*")): + raise RuntimeError( + "MAGMA was selected, but lib/libmagma was not found under " + f"{root}; libCEED 0.12 requires the lib layout" + ) + ceed_cuda_arch = "" + if features["cuda"] and args.cuda_arch not in {"", "native"}: + ceed_cuda_arch = args.cuda_arch.split(";", 1)[0] + ceed_cuda_arch = re.sub(r"-(?:real|virtual)$", "", ceed_cuda_arch) + if re.fullmatch(r"[0-9]+[a-z]?", ceed_cuda_arch, flags=re.IGNORECASE): + ceed_cuda_arch = "sm_" + ceed_cuda_arch + command.extend( + [ + f"prefix={args.prefix}", + f"CC={shlex.join([*args.cc_launcher, args.cc])}", + f"CXX={shlex.join([*args.cxx_launcher, args.cxx])}", + f"OPT={optimization}", + f"LDFLAGS={joined_flags(link_flags)}", + "ARFLAGS=cr", + f"STATIC={'1' if args.library_kind == 'static' else ''}", + "AVX=", + "MARCHFLAG=", + f"OPENMP={'1' if features['openmp'] else ''}", + "MPI=0", + "XSMM_DIR=", + f"OCCA_DIR={occa_root if features['occa'] else ''}", + f"MAGMA_DIR={magma_root if features['magma'] else ''}", + f"CUDA_DIR={cuda_root}", + f"CUDA_ARCH={ceed_cuda_arch}", + f"ROCM_DIR={rocm_root}", + f"HIP_ARCH={args.hip_arch if features['hip'] else ''}", + "SYCL_DIR=", + f"EMSCRIPTEN={'1' if args.host_system == 'emscripten' else ''}", + ] + ) + if args.host_system not in {"darwin", "emscripten"}: + command.append("LDLIBS=-ldl") + run(command, env=local_env) + + +def build_fms( + args: argparse.Namespace, + features: Mapping[str, bool], + env: Mapping[str, str], + build_type: str, +) -> None: + if not args.fms_source: + raise RuntimeError("FMS is enabled but its source wrap was not provided") + conduit_root = "" + if features["conduit"]: + conduit_root = ( + args.dependency_prefix + or env.get("CONDUIT_DIR", "") + or env.get("Conduit_DIR", "") + ) + pkg_config = find_program_in_environment("pkg-config", env) + if not conduit_root and pkg_config and args.allow_system == "true": + probe = subprocess.run( + [pkg_config, "--variable=prefix", "conduit"], + env=env, + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, + ) + if probe.returncode == 0: + conduit_root = probe.stdout.strip() + options = common_cmake_args( + args, build_type, c_compiler=args.cc, cxx_compiler=args.cxx + ) + [ + "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", + "-DFMS_ENABLE_DEMO=OFF", + "-DFMS_ENABLE_TESTS=OFF", + f"-DCONDUIT_DIR={conduit_root}", + ] + build = args.work_dir / "fms" + build.mkdir(parents=True, exist_ok=True) + cmake_configure(args, args.fms_source, build, options, env) + run( + [ + args.cmake, + "--build", + build, + "--target", + "fms", + "--parallel", + str(args.jobs), + ], + env=env, + ) + run([args.cmake, "--install", build], env=env) + + +def build_algoim( + args: argparse.Namespace, + env: Mapping[str, str], + build_type: str, +) -> None: + if not args.algoim_source or not args.blitz_source: + raise RuntimeError( + "Algoim is enabled but its Algoim/Blitz source wraps were not provided" + ) + options = common_cmake_args( + args, build_type, c_compiler=args.cc, cxx_compiler=args.cxx + ) + [ + "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", + "-DBUILD_TESTING=OFF", + "-DBUILD_DOC=OFF", + "-DBUILD_EXAMPLES=OFF", + ] + cmake_build_install( + args, + args.blitz_source, + args.work_dir / "blitz", + options, + env, + ) + cmake_root = args.prefix / "lib" / "cmake" + targets_file = cmake_root / "blitzTargets.cmake" + config_file = cmake_root / "blitzConfig.cmake" + version_file = cmake_root / "blitzConfigVersion.cmake" + if not targets_file.is_file() or not config_file.is_file(): + raise RuntimeError("Blitz install did not provide its expected CMake metadata") + targets_text = targets_file.read_text(encoding="utf-8") + for target_name in ("blitz", "blitz-static"): + declaration = f"add_library({target_name} " + property_line = ( + f'set_property(TARGET {target_name} PROPERTY ' + 'INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include")' + ) + if property_line not in targets_text: + position = targets_text.find(declaration) + if position < 0: + raise RuntimeError(f"Blitz target {target_name} was not exported") + line_end = targets_text.find("\n", position) + targets_text = ( + targets_text[: line_end + 1] + + property_line + + "\n" + + targets_text[line_end + 1 :] + ) + write_text_atomic(targets_file, targets_text) + shim_dir = cmake_root / "blitz" + shim_dir.mkdir(parents=True, exist_ok=True) + write_text_atomic( + shim_dir / "blitzConfig.cmake", + 'include("${CMAKE_CURRENT_LIST_DIR}/../blitzConfig.cmake")\n', + ) + if version_file.is_file(): + write_text_atomic( + shim_dir / "blitzConfigVersion.cmake", + 'include("${CMAKE_CURRENT_LIST_DIR}/../blitzConfigVersion.cmake")\n', + ) + include_dir = args.prefix / "include" + include_dir.mkdir(parents=True, exist_ok=True) + headers = sorted((args.algoim_source / "src").glob("*.hpp")) + if not headers: + raise RuntimeError("Algoim source archive did not contain src/*.hpp") + for header in headers: + shutil.copy2(header, include_dir / header.name) + + +def prepare_mfem_source( + args: argparse.Namespace, features: Mapping[str, bool] +) -> Path: + """Copy MFEM only when a small upstream portability fix is required.""" + if not (features["ceed"] or features["pumi"] or features["sidre"]): + return args.source + source = args.work_dir / "mfem-source" + shutil.copytree(args.source, source, dirs_exist_ok=True, symlinks=True) + cmake_file = source / "CMakeLists.txt" + text = cmake_file.read_text(encoding="utf-8") + if features["ceed"]: + ceed_util = source / "fem" / "ceed" / "interface" / "util.cpp" + ceed_text = ceed_util.read_text(encoding="utf-8") + include_anchor = """#include +#include +#if !defined(_WIN32) || !defined(_MSC_VER) +typedef struct stat struct_stat; +#else +#define stat(dir, buf) _stat(dir, buf) +#define S_ISDIR(mode) (((mode) & _S_IFMT) == _S_IFDIR) +typedef struct _stat struct_stat; +#endif +""" + include_replacement = """#include +#include +#if !defined(_WIN32) || !defined(_MSC_VER) +typedef struct stat struct_stat; +#else +#define stat(dir, buf) _stat(dir, buf) +#define S_ISDIR(mode) (((mode) & _S_IFMT) == _S_IFDIR) +typedef struct _stat struct_stat; +#endif + +#include + +#if defined(_WIN32) && !defined(__EMSCRIPTEN__) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#elif !defined(__EMSCRIPTEN__) +#include +#endif +""" + if include_anchor not in ceed_text: + raise RuntimeError("MFEM's expected CEED platform includes were not found") + ceed_text = ceed_text.replace(include_anchor, include_replacement, 1) + + function_anchor = """std::string ceed_path; + +const std::string &GetCeedPath() +{ + if (ceed_path.empty()) + { + const char *install_dir = MFEM_INSTALL_DIR "/include/mfem/fem/ceed"; + const char *source_dir = MFEM_SOURCE_DIR "/fem/ceed"; + struct_stat m_stat; + if (stat(install_dir, &m_stat) == 0 && S_ISDIR(m_stat.st_mode)) + { + ceed_path = install_dir; + } + else if (stat(source_dir, &m_stat) == 0 && S_ISDIR(m_stat.st_mode)) + { + ceed_path = source_dir; + } + else + { + MFEM_ABORT("Cannot find libCEED kernels in MFEM_INSTALL_DIR or " + "MFEM_SOURCE_DIR"); + } + // Could be useful for debugging: + // out << "Using libCEED dir: " << ceed_path << std::endl; + } + return ceed_path; +} +""" + function_replacement = """std::string ceed_path; + +const std::string &GetCeedPath(); + +static std::string GetLoadedMfemLibraryPath() +{ +#if defined(__EMSCRIPTEN__) + return std::string(); +#elif defined(_WIN32) + HMODULE module = nullptr; + const DWORD flags = GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT; + if (!GetModuleHandleExA(flags, + reinterpret_cast(&GetCeedPath), + &module)) + { + return std::string(); + } + char module_path[MAX_PATH]; + const DWORD length = GetModuleFileNameA(module, module_path, MAX_PATH); + if (length == 0 || length >= MAX_PATH) + { + return std::string(); + } + return std::string(module_path, length); +#else + Dl_info info; + if (dladdr(reinterpret_cast(&GetCeedPath), &info) == 0 || + info.dli_fname == nullptr) + { + return std::string(); + } + return std::string(info.dli_fname); +#endif +} + +const std::string &GetCeedPath() +{ + if (ceed_path.empty()) + { + const char *install_dir = MFEM_INSTALL_DIR "/include/mfem/fem/ceed"; + const char *source_dir = MFEM_SOURCE_DIR "/fem/ceed"; + struct_stat m_stat; + + const char *environment_dir = std::getenv("MFEM_CEED_PATH"); + if (environment_dir != nullptr && + stat(environment_dir, &m_stat) == 0 && S_ISDIR(m_stat.st_mode)) + { + ceed_path = environment_dir; + } + + std::string library_path = GetLoadedMfemLibraryPath(); + const std::string::size_type separator = library_path.find_last_of("/\\\\"); + if (ceed_path.empty() && separator != std::string::npos) + { + const std::string module_dir = library_path.substr(0, separator); + const std::string adjacent_dir = + module_dir + "/include/mfem/fem/ceed"; + const std::string sibling_dir = + module_dir + "/../include/mfem/fem/ceed"; + if (stat(adjacent_dir.c_str(), &m_stat) == 0 && S_ISDIR(m_stat.st_mode)) + { + ceed_path = adjacent_dir; + } + else if (stat(sibling_dir.c_str(), &m_stat) == 0 && + S_ISDIR(m_stat.st_mode)) + { + ceed_path = sibling_dir; + } + } + + if (ceed_path.empty() && + stat(install_dir, &m_stat) == 0 && S_ISDIR(m_stat.st_mode)) + { + ceed_path = install_dir; + } + else if (ceed_path.empty() && + stat(source_dir, &m_stat) == 0 && S_ISDIR(m_stat.st_mode)) + { + ceed_path = source_dir; + } + else if (ceed_path.empty()) + { + MFEM_ABORT("Cannot find libCEED kernels in MFEM_CEED_PATH, next to " + "the loaded MFEM library, in MFEM_INSTALL_DIR, or in " + "MFEM_SOURCE_DIR"); + } + // Could be useful for debugging: + // out << "Using libCEED dir: " << ceed_path << std::endl; + } + return ceed_path; +} +""" + if function_anchor not in ceed_text: + raise RuntimeError("MFEM's expected GetCeedPath implementation was not found") + ceed_text = ceed_text.replace(function_anchor, function_replacement, 1) + write_text_atomic(ceed_util, ceed_text) + + link_anchor = "target_link_libraries(mfem PUBLIC ${TPL_LIBRARIES} ${TPL_TARGETS})\n" + link_replacement = """target_link_libraries(mfem PUBLIC ${TPL_LIBRARIES} ${TPL_TARGETS}) +# dladdr lives in libdl on older glibc systems. It is part of libc/dyld on +# newer Linux and macOS, where CMAKE_DL_LIBS is empty or harmless. +if(MFEM_USE_CEED AND UNIX AND NOT EMSCRIPTEN) + target_link_libraries(mfem PRIVATE ${CMAKE_DL_LIBS}) +endif() +""" + if link_anchor not in text: + raise RuntimeError("MFEM's expected target_link_libraries call was not found") + text = text.replace(link_anchor, link_replacement, 1) + if features["pumi"]: + assignment = " set(MFEM_USE_SIMMETRIX ${SCOREC_gmi_sim_FOUND})" + replacement = """ # Keep the explicit Meson selection authoritative. Upstream normally + # overwrites MFEM_USE_SIMMETRIX from the optional SCOREC component. + if (MFEM_USE_SIMMETRIX AND NOT SCOREC_gmi_sim_FOUND) + message(FATAL_ERROR "MFEM_USE_SIMMETRIX requires SCOREC gmi_sim") + endif()""" + if assignment not in text: + raise RuntimeError("MFEM's expected Simmetrix assignment was not found") + text = text.replace(assignment, replacement, 1) + if features["sidre"]: + axom_request = "find_package(Axom REQUIRED Axom)" + if axom_request not in text: + raise RuntimeError("MFEM's expected Axom component request was not found") + text = text.replace( + axom_request, + "find_package(Axom REQUIRED core sidre)", + 1, + ) + write_text_atomic(cmake_file, text) + return source + + +def build_mfem( + args: argparse.Namespace, + features: Mapping[str, bool], + env: Mapping[str, str], + build_type: str, + mpicc: str, + mpicxx: str, +) -> None: + options = common_cmake_args( + args, build_type, c_compiler=args.cc, cxx_compiler=args.cxx + ) + [ + "-DCMAKE_FIND_PACKAGE_PREFER_CONFIG=OFF", + "-DMFEM_ENABLE_TESTING=OFF", + "-DMFEM_ENABLE_EXAMPLES=OFF", + "-DMFEM_ENABLE_MINIAPPS=OFF", + "-DMFEM_ENABLE_BENCHMARKS=OFF", + "-DMFEM_FETCH_TPLS=OFF", + "-DMFEM_FETCH_HYPRE=OFF", + "-DMFEM_FETCH_METIS=OFF", + "-DMFEM_FETCH_GSLIB=OFF", + "-DMFEM_USE_GNUINSTALLDIRS=ON", + f"-DMFEM_PRECISION={args.precision}", + ] + options.extend( + f"-D{cmake_name}={'ON' if features[name] else 'OFF'}" + for name, cmake_name in CMAKE_FEATURES.items() + ) + if args.dependency_prefix: + external_dir_options = { + "libunwind": "LIBUNWIND_DIR", + "suitesparse": "SuiteSparse_DIR", + "superlu": "SuperLUDist_DIR", + "mumps": "MUMPS_DIR", + "strumpack": "STRUMPACK_DIR", + "cudss": "CUDSS_DIR", + "ginkgo": "Ginkgo_DIR", + "amgx": "AMGX_DIR", + "magma": "MAGMA_DIR", + "gnutls": "GNUTLS_DIR", + "hdf5": "HDF5_DIR", + "netcdf": "NETCDF_DIR", + "petsc": "PETSC_DIR", + "slepc": "SLEPC_DIR", + "mpfr": "MPFR_DIR", + "sidre": "AXOM_DIR", + "conduit": "CONDUIT_DIR", + "pumi": "PUMI_DIR", + "hiop": "HIOP_DIR", + "occa": "OCCA_DIR", + "raja": "RAJA_DIR", + "umpire": "UMPIRE_DIR", + "caliper": "CALIPER_DIR", + "mkl_cpardiso": "MKL_CPARDISO_DIR", + "mkl_pardiso": "MKL_PARDISO_DIR", + "codipack": "CODIPACK_DIR", + "benchmark": "BENCHMARK_DIR", + "tribol": "TRIBOL_DIR", + "enzyme": "ENZYME_DIR", + } + options.extend( + f"-D{directory_name}={args.dependency_prefix}" + for feature_name, directory_name in external_dir_options.items() + if features[feature_name] + ) + if features["petsc"]: + options.append("-DPETSC_ARCH=") + if features["slepc"]: + options.append("-DSLEPC_ARCH=") + if features["parelag"]: + parelag_library = bundle_find(args, "lib*/libParELAG.*") + if parelag_library is None: + raise RuntimeError( + "ParELAG was selected, but libParELAG is absent from dependency_prefix" + ) + options.extend( + [ + f"-DPARELAG_DIR={args.dependency_prefix}", + f"-DPARELAG_INCLUDE_DIRS={Path(args.dependency_prefix) / 'include'}", + f"-DPARELAG_LIBRARIES={parelag_library}", + ] + ) + if features["mpi"]: + mpi_root = args.prefix + if not (mpi_root / "bin" / "mpiexec").is_file() and args.dependency_prefix: + mpi_root = Path(args.dependency_prefix) + mpi_launcher = mpi_root / "bin" / "mpiexec" + if not mpi_launcher.is_file(): + mpi_launcher = mpi_root / "bin" / "mpirun" + hypre_root = args.prefix + if not (hypre_root / "include" / "HYPRE_config.h").is_file() and args.dependency_prefix: + hypre_root = Path(args.dependency_prefix) + options.extend( + [ + f"-DMPI_C_COMPILER={mpicc}", + f"-DMPI_CXX_COMPILER={mpicxx}", + f"-DMFEM_MPIEXEC={mpi_launcher}", + f"-DHYPRE_DIR={hypre_root}", + ] + ) + hypre_prefixes = [args.prefix] + if args.dependency_prefix: + hypre_prefixes.append(Path(args.dependency_prefix)) + hypre_version = hypre_version_integer(*hypre_prefixes) + if hypre_version is not None: + options.append(f"-DHYPRE_VERSION:STRING={hypre_version}") + elif args.cross_build == "true": + raise RuntimeError( + "cross-building parallel MFEM requires HYPRE_config.h in " + "dependency_prefix so HYPRE_VERSION can be determined" + ) + if features["metis"]: + options.append(f"-DMETIS_DIR={bundle_root(args, 'include/metis.h')}") + if features["gslib"]: + options.append( + f"-DGSLIB_DIR={bundle_root(args, 'include/gslib/gslib.h')}" + ) + if features["zlib"]: + options.append(f"-DZLIB_ROOT={bundle_root(args, 'include/zlib.h')}") + if features["sundials"]: + options.append( + f"-DSUNDIALS_DIR={bundle_root(args, 'include/sundials/sundials_config.h')}" + ) + if features["ceed"]: + options.append(f"-DCEED_DIR={bundle_root(args, 'include/ceed.h')}") + if features["fms"]: + options.append(f"-DFMS_DIR={bundle_root(args, 'include/fms.h')}") + if features["algoim"]: + algoim_root = bundle_root(args, "include/algoim_quad.hpp") + blitz_root = bundle_root(args, "include/blitz/array.h") + options.extend( + [ + f"-DALGOIM_DIR={algoim_root}", + f"-DBLITZ_DIR={blitz_root}", + ] + ) + if features["cudss"]: + comm_plugin: Path | None = None + if features["mpi"]: + open_mpi_probe = subprocess.run( + [mpicc, "--showme:version"], + env=env, + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + version_match = re.search(r"Open MPI\s+([0-9]+)\.", open_mpi_probe.stdout) + if ( + open_mpi_probe.returncode != 0 + or "Open MPI" not in open_mpi_probe.stdout + or not version_match + or version_match.group(1) != "4" + ): + raise RuntimeError( + "cuDSS distributed mode requires an Open MPI 4.x prefix; " + "the bundled MPI provider is MPICH" + ) + comm_plugin = bundle_find( + args, "lib*/libcudss_commlayer_openmpi.so*" + ) + if comm_plugin is None: + raise RuntimeError( + "cuDSS distributed mode was selected, but " + "libcudss_commlayer_openmpi.so is absent from dependency_prefix" + ) + if comm_plugin is not None: + options.append(f"-DMFEM_CUDSS_COMM_LIB={comm_plugin.name}") + if features["openmp"]: + threading_plugin = bundle_find( + args, "lib*/libcudss_mtlayer_gomp.so*" + ) + if threading_plugin is None: + raise RuntimeError( + "cuDSS OpenMP mode was selected, but " + "libcudss_mtlayer_gomp.so is absent from dependency_prefix" + ) + options.append(f"-DMFEM_CUDSS_THREADING_LIB={threading_plugin.name}") + if features["cuda"]: + options.extend( + [ + f"-DCMAKE_CUDA_ARCHITECTURES={args.cuda_arch}", + f"-DCUDA_ARCH={args.cuda_arch}", + ] + ) + if features["hip"] and args.hip_arch: + options.extend( + [ + f"-DCMAKE_HIP_ARCHITECTURES={args.hip_arch}", + f"-DHIP_ARCH={args.hip_arch}", + ] + ) + mfem_source = prepare_mfem_source(args, features) + cmake_build_install(args, mfem_source, args.work_dir / "mfem", options, env) + + +def validate_mfem_feature_header( + args: argparse.Namespace, features: Mapping[str, bool] +) -> None: + """Prove that every public, independently selectable MFEM macro agrees.""" + header = args.prefix / "include" / "mfem" / "config" / "_config.hpp" + if not header.is_file(): + raise RuntimeError("MFEM feature validation failed: _config.hpp is missing") + text = header.read_text(encoding="utf-8") + for feature_name, macro in CMAKE_FEATURES.items(): + is_defined = re.search( + rf"^\s*#\s*define\s+{re.escape(macro)}(?:\s|$)", + text, + flags=re.MULTILINE, + ) is not None + if is_defined != features[feature_name]: + expected = "defined" if features[feature_name] else "absent" + raise RuntimeError( + f"MFEM feature validation failed: {macro} should be {expected}" + ) + precision_macro = ( + "MFEM_USE_SINGLE" if args.precision == "single" else "MFEM_USE_DOUBLE" + ) + if re.search( + rf"^\s*#\s*define\s+{precision_macro}(?:\s|$)", + text, + flags=re.MULTILINE, + ) is None: + raise RuntimeError( + f"MFEM feature validation failed: {precision_macro} is absent" + ) + + +def install_licenses(args: argparse.Namespace, source_pairs: Sequence[tuple[str, Path | None]]) -> None: + destination = args.prefix / "share" / "licenses" / "meson-mfem-template" + destination.mkdir(parents=True, exist_ok=True) + for name, source in source_pairs: + if not source: + continue + legal_files: dict[str, Path] = {} + for pattern in ("LICENSE*", "COPYING*", "COPYRIGHT*", "NOTICE*"): + for legal_file in source.glob(pattern): + if legal_file.is_file(): + legal_files[legal_file.name] = legal_file + for candidate, legal_file in sorted(legal_files.items()): + shutil.copy2(legal_file, destination / f"{name}-{candidate}") + + +def _text_file(path: Path) -> str | None: + try: + data = path.read_bytes() + except OSError: + return None + if b"\0" in data[:4096]: + return None + try: + return data.decode("utf-8") + except UnicodeDecodeError: + return None + + +def sanitize_installed_metadata(args: argparse.Namespace) -> None: + """Remove private build-prefix assumptions from installed SDK metadata.""" + prefix = args.prefix + prefix_text = os.fspath(prefix) + private_prefixes = [prefix_text] + if args.dependency_prefix and args.vendor_dependency_prefix == "true": + private_prefixes.append(os.fspath(Path(args.dependency_prefix).resolve())) + + config_header = prefix / "include" / "mfem" / "config" / "_config.hpp" + if config_header.is_file(): + text = config_header.read_text(encoding="utf-8") + text = re.sub( + r'(#define MFEM_SOURCE_DIR) "[^"]*"', r'\1 ""', text + ) + text = re.sub( + r'(#define MFEM_INSTALL_DIR) "[^"]*"', r'\1 ""', text + ) + write_text_atomic(config_header, text) + + sundials_header = prefix / "include" / "sundials" / "sundials_config.h" + if sundials_header.is_file(): + text = sundials_header.read_text(encoding="utf-8") + for language, compiler in ( + ("C", "mpicc"), + ("CXX", "mpicxx"), + ("FORTRAN", "mpifort"), + ): + pattern = rf'^(#define SUN_MPI_{language}_COMPILER) "([^"]*)"$' + match = re.search(pattern, text, flags=re.MULTILINE) + if match and any( + match.group(2).startswith(private_prefix) + for private_prefix in private_prefixes + ): + text = re.sub( + pattern, + rf'\1 "{compiler}"', + text, + count=1, + flags=re.MULTILINE, + ) + write_text_atomic(sundials_header, text) + + config_mk = prefix / "share" / "mfem" / "config.mk" + if config_mk.is_file(): + text = config_mk.read_text(encoding="utf-8") + replacements = { + "MFEM_SOURCE_DIR": "$(MFEM_PREFIX)", + "MFEM_INSTALL_DIR": "$(MFEM_PREFIX)", + "MFEM_CXX": "c++", + "MFEM_HOST_CXX": "c++", + "MFEM_PREFIX": "$(abspath $(dir $(lastword $(MAKEFILE_LIST)))/../..)", + "MFEM_INC_DIR": "$(MFEM_PREFIX)/include", + "MFEM_LIB_DIR": "$(MFEM_PREFIX)/lib", + "MFEM_TEST_MK": "$(MFEM_PREFIX)/share/mfem/test.mk", + } + for variable, value in replacements.items(): + text = re.sub( + rf"^{re.escape(variable)}\s*=.*$", + f"{variable} = {value}", + text, + flags=re.MULTILINE, + ) + for private_prefix in private_prefixes: + text = text.replace(private_prefix, "$(MFEM_PREFIX)") + write_text_atomic(config_mk, text) + + for pc_file in prefix.glob("**/*.pc"): + text = pc_file.read_text(encoding="utf-8") + relative_prefix = Path(os.path.relpath(prefix, pc_file.parent)).as_posix() + text = re.sub( + r"^prefix=.*$", + "prefix=${pcfiledir}/" + relative_prefix, + text, + flags=re.MULTILINE, + ) + for private_prefix in private_prefixes: + text = text.replace(private_prefix, "${prefix}") + if pc_file.name == "mpich.pc": + text = re.sub( + r"^Cflags:.*$", + "Cflags: -I${includedir}", + text, + flags=re.MULTILINE, + ) + for language_flags in ("cxxflags", "fflags", "fcflags"): + text = re.sub( + rf"^{language_flags}=.*$", + f"{language_flags}=-I${{includedir}}", + text, + flags=re.MULTILINE, + ) + if "-Wl,-rpath,${libdir}" not in text: + text = re.sub( + r"^(Libs:.*)$", + r"\1 -Wl,-rpath,${libdir}", + text, + count=1, + flags=re.MULTILINE, + ) + write_text_atomic(pc_file, text) + + local_mpi_wrappers = all( + (prefix / "bin" / wrapper).is_file() for wrapper in ("mpicc", "mpicxx") + ) + for cmake_file in prefix.glob("**/*.cmake"): + text = cmake_file.read_text(encoding="utf-8") + is_targets_file = ( + "targets" in cmake_file.name.lower() or "_IMPORT_PREFIX" in text + ) + if is_targets_file: + for private_prefix in private_prefixes: + text = text.replace(private_prefix, "${_IMPORT_PREFIX}") + soname_prefix = "@rpath/" if args.host_system == "darwin" else "" + text = re.sub( + r'(IMPORTED_SONAME_[A-Z_]+ ")([^"]*/)?([^/"]+)(")', + rf"\1{soname_prefix}\3\4", + text, + ) + else: + relative_prefix = Path( + os.path.relpath(prefix, cmake_file.parent) + ).as_posix() + package_init = ( + 'get_filename_component(PACKAGE_PREFIX_DIR "' + + "$" + + "{CMAKE_CURRENT_LIST_DIR}/" + + relative_prefix + + '" ABSOLUTE)' + ) + if "PACKAGE_PREFIX_DIR" in text: + text = re.sub( + r'^get_filename_component\(PACKAGE_PREFIX_DIR .*? ABSOLUTE\)$', + package_init, + text, + count=1, + flags=re.MULTILINE, + ) + elif any(private_prefix in text for private_prefix in private_prefixes): + text = package_init + "\n" + text + for private_prefix in private_prefixes: + text = text.replace(private_prefix, "${PACKAGE_PREFIX_DIR}") + text = re.sub( + r'^set\(MFEM_CXX_COMPILER ".*"\)$', + 'set(MFEM_CXX_COMPILER "c++")', + text, + flags=re.MULTILINE, + ) + if ( + cmake_file.name == "HYPREConfig.cmake" + and "HYPRE_ENABLE_MPI ON" in text + and (local_mpi_wrappers or args.dependency_prefix) + ): + marker = "# meson-mfem-template: selected MPI_C ownership" + if marker not in text: + if local_mpi_wrappers: + selected_mpi_prefix = "${PACKAGE_PREFIX_DIR}" + selected_mpicc = "${PACKAGE_PREFIX_DIR}/bin/mpicc" + else: + selected_mpi_prefix = args.dependency_prefix.replace("\\", "/") + selected_mpi_prefix = selected_mpi_prefix.replace(";", "\\;") + selected_mpicc = selected_mpi_prefix + "/bin/mpicc" + hypre_mpi_block = """# meson-mfem-template: selected MPI_C ownership +function(_meson_mfem_hypre_mpi_target_is_owned _language _target _expected_wrapper _result) + get_target_property(_meson_mfem_mpi_includes ${_target} INTERFACE_INCLUDE_DIRECTORIES) + get_target_property(_meson_mfem_mpi_libraries ${_target} INTERFACE_LINK_LIBRARIES) + get_target_property(_meson_mfem_mpi_link_dirs ${_target} INTERFACE_LINK_DIRECTORIES) + string(FIND "${_meson_mfem_mpi_includes}" "@MPI_PREFIX@/" _meson_mfem_mpi_include_index) + string(FIND "${_meson_mfem_mpi_libraries};${_meson_mfem_mpi_link_dirs}" "@MPI_PREFIX@/" _meson_mfem_mpi_library_index) + set(_meson_mfem_mpi_owned FALSE) + if(NOT _meson_mfem_mpi_include_index EQUAL -1 AND NOT _meson_mfem_mpi_library_index EQUAL -1) + set(_meson_mfem_mpi_owned TRUE) + else() + # FindMPI deliberately leaves an imported target's interface empty when + # CMake itself is using that language's MPI wrapper as its compiler. That + # state is safe only when both compiler variables resolve to this package's + # selected wrapper and the target contains no contradictory interface data. + get_target_property(_meson_mfem_mpi_compile_options ${_target} INTERFACE_COMPILE_OPTIONS) + get_target_property(_meson_mfem_mpi_link_options ${_target} INTERFACE_LINK_OPTIONS) + get_target_property(_meson_mfem_mpi_compile_definitions ${_target} INTERFACE_COMPILE_DEFINITIONS) + get_target_property(_meson_mfem_mpi_imported ${_target} IMPORTED) + get_target_property(_meson_mfem_mpi_type ${_target} TYPE) + set(_meson_mfem_mpi_interface_empty TRUE) + foreach(_meson_mfem_mpi_value + "${_meson_mfem_mpi_includes}" + "${_meson_mfem_mpi_libraries}" + "${_meson_mfem_mpi_link_dirs}" + "${_meson_mfem_mpi_compile_options}" + "${_meson_mfem_mpi_link_options}" + "${_meson_mfem_mpi_compile_definitions}") + if(NOT "${_meson_mfem_mpi_value}" STREQUAL "" AND NOT "${_meson_mfem_mpi_value}" MATCHES "-NOTFOUND$") + set(_meson_mfem_mpi_interface_empty FALSE) + endif() + endforeach() + set(_meson_mfem_cmake_compiler_variable "CMAKE_${_language}_COMPILER") + set(_meson_mfem_mpi_compiler_variable "MPI_${_language}_COMPILER") + if(_meson_mfem_mpi_interface_empty + AND _meson_mfem_mpi_imported + AND _meson_mfem_mpi_type STREQUAL "INTERFACE_LIBRARY" + AND EXISTS "${_expected_wrapper}" + AND DEFINED ${_meson_mfem_cmake_compiler_variable} + AND DEFINED ${_meson_mfem_mpi_compiler_variable} + AND NOT "${${_meson_mfem_cmake_compiler_variable}}" STREQUAL "" + AND NOT "${${_meson_mfem_mpi_compiler_variable}}" STREQUAL "") + get_filename_component(_meson_mfem_expected_wrapper_real "${_expected_wrapper}" REALPATH) + get_filename_component(_meson_mfem_cmake_compiler_real "${${_meson_mfem_cmake_compiler_variable}}" REALPATH) + get_filename_component(_meson_mfem_mpi_compiler_real "${${_meson_mfem_mpi_compiler_variable}}" REALPATH) + if(_meson_mfem_cmake_compiler_real STREQUAL _meson_mfem_expected_wrapper_real + AND _meson_mfem_mpi_compiler_real STREQUAL _meson_mfem_expected_wrapper_real) + set(_meson_mfem_mpi_owned TRUE) + endif() + endif() + endif() + set(${_result} ${_meson_mfem_mpi_owned} PARENT_SCOPE) +endfunction() + +if(HYPRE_ENABLE_MPI) + if(TARGET MPI::MPI_C) + _meson_mfem_hypre_mpi_target_is_owned(C MPI::MPI_C "@MPICC@" _meson_mfem_mpi_owned) + if(NOT _meson_mfem_mpi_owned) + message(FATAL_ERROR "HYPRE cannot use pre-existing MPI::MPI_C from a different MPI installation") + endif() + endif() + enable_language(C) + set(MPI_C_COMPILER "@MPICC@" CACHE FILEPATH "MPI C compiler selected for HYPRE" FORCE) + if(NOT TARGET MPI::MPI_C) + find_dependency(MPI COMPONENTS C) + endif() + if(NOT TARGET MPI::MPI_C) + message(FATAL_ERROR "HYPRE requires MPI::MPI_C") + endif() + _meson_mfem_hypre_mpi_target_is_owned(C MPI::MPI_C "@MPICC@" _meson_mfem_mpi_owned) + string(FIND "${MPI_C_COMPILER}" "@MPI_PREFIX@/" _meson_mfem_mpi_compiler_index) + if(_meson_mfem_mpi_compiler_index EQUAL -1 OR NOT _meson_mfem_mpi_owned) + message(FATAL_ERROR "HYPRE cannot use MPI::MPI_C from a different MPI installation") + endif() + set_property(TARGET MPI::MPI_C PROPERTY IMPORTED_NO_SYSTEM TRUE) + if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.25") + set_property(TARGET MPI::MPI_C PROPERTY SYSTEM FALSE) + endif() + unset(_meson_mfem_mpi_owned) + unset(_meson_mfem_mpi_compiler_index) +endif() +""" + hypre_mpi_block = ( + hypre_mpi_block.replace("@MPI_PREFIX@", selected_mpi_prefix) + .replace("@MPICC@", selected_mpicc) + ) + mpi_block_pattern = re.compile( + r'^if\(HYPRE_ENABLE_MPI\)\n.*?^endif\(\)\n', + flags=re.MULTILINE | re.DOTALL, + ) + text, replacement_count = mpi_block_pattern.subn( + hypre_mpi_block, + text, + count=1, + ) + if replacement_count != 1: + raise RuntimeError( + "HYPREConfig.cmake MPI dependency block was not found" + ) + if ( + cmake_file.name == "SUNDIALSConfig.cmake" + and (local_mpi_wrappers or args.dependency_prefix) + and 'if("ON" AND NOT TARGET MPI::MPI_C)' in text + ): + marker = "# meson-mfem-template: selected MPI ownership" + if marker not in text: + if local_mpi_wrappers: + selected_mpi_prefix = "${PACKAGE_PREFIX_DIR}" + selected_mpicc = "${PACKAGE_PREFIX_DIR}/bin/mpicc" + selected_mpicxx = "${PACKAGE_PREFIX_DIR}/bin/mpicxx" + else: + selected_mpi_prefix = args.dependency_prefix.replace("\\", "/") + selected_mpi_prefix = selected_mpi_prefix.replace(";", "\\;") + selected_mpicc = selected_mpi_prefix + "/bin/mpicc" + selected_mpicxx = selected_mpi_prefix + "/bin/mpicxx" + sundials_mpi_block = """# meson-mfem-template: selected MPI ownership +function(_meson_mfem_sundials_mpi_target_is_owned _language _target _expected_wrapper _result) + get_target_property(_meson_mfem_mpi_includes ${_target} INTERFACE_INCLUDE_DIRECTORIES) + get_target_property(_meson_mfem_mpi_libraries ${_target} INTERFACE_LINK_LIBRARIES) + get_target_property(_meson_mfem_mpi_link_dirs ${_target} INTERFACE_LINK_DIRECTORIES) + string(FIND "${_meson_mfem_mpi_includes}" "@MPI_PREFIX@/" _meson_mfem_mpi_include_index) + string(FIND "${_meson_mfem_mpi_libraries};${_meson_mfem_mpi_link_dirs}" "@MPI_PREFIX@/" _meson_mfem_mpi_library_index) + if(_language STREQUAL "CXX" AND "${_meson_mfem_mpi_libraries}" MATCHES "MPI::MPI_C") + set(_meson_mfem_mpi_library_index 0) + endif() + set(_meson_mfem_mpi_owned FALSE) + if(NOT _meson_mfem_mpi_include_index EQUAL -1 AND NOT _meson_mfem_mpi_library_index EQUAL -1) + set(_meson_mfem_mpi_owned TRUE) + else() + # FindMPI deliberately leaves an imported target's interface empty when + # CMake itself is using that language's MPI wrapper as its compiler. That + # state is safe only when both compiler variables resolve to this package's + # selected wrapper and the target contains no contradictory interface data. + get_target_property(_meson_mfem_mpi_compile_options ${_target} INTERFACE_COMPILE_OPTIONS) + get_target_property(_meson_mfem_mpi_link_options ${_target} INTERFACE_LINK_OPTIONS) + get_target_property(_meson_mfem_mpi_compile_definitions ${_target} INTERFACE_COMPILE_DEFINITIONS) + get_target_property(_meson_mfem_mpi_imported ${_target} IMPORTED) + get_target_property(_meson_mfem_mpi_type ${_target} TYPE) + set(_meson_mfem_mpi_interface_empty TRUE) + foreach(_meson_mfem_mpi_value + "${_meson_mfem_mpi_includes}" + "${_meson_mfem_mpi_libraries}" + "${_meson_mfem_mpi_link_dirs}" + "${_meson_mfem_mpi_compile_options}" + "${_meson_mfem_mpi_link_options}" + "${_meson_mfem_mpi_compile_definitions}") + if(NOT "${_meson_mfem_mpi_value}" STREQUAL "" AND NOT "${_meson_mfem_mpi_value}" MATCHES "-NOTFOUND$") + set(_meson_mfem_mpi_interface_empty FALSE) + endif() + endforeach() + set(_meson_mfem_cmake_compiler_variable "CMAKE_${_language}_COMPILER") + set(_meson_mfem_mpi_compiler_variable "MPI_${_language}_COMPILER") + if(_meson_mfem_mpi_interface_empty + AND _meson_mfem_mpi_imported + AND _meson_mfem_mpi_type STREQUAL "INTERFACE_LIBRARY" + AND EXISTS "${_expected_wrapper}" + AND DEFINED ${_meson_mfem_cmake_compiler_variable} + AND DEFINED ${_meson_mfem_mpi_compiler_variable} + AND NOT "${${_meson_mfem_cmake_compiler_variable}}" STREQUAL "" + AND NOT "${${_meson_mfem_mpi_compiler_variable}}" STREQUAL "") + get_filename_component(_meson_mfem_expected_wrapper_real "${_expected_wrapper}" REALPATH) + get_filename_component(_meson_mfem_cmake_compiler_real "${${_meson_mfem_cmake_compiler_variable}}" REALPATH) + get_filename_component(_meson_mfem_mpi_compiler_real "${${_meson_mfem_mpi_compiler_variable}}" REALPATH) + if(_meson_mfem_cmake_compiler_real STREQUAL _meson_mfem_expected_wrapper_real + AND _meson_mfem_mpi_compiler_real STREQUAL _meson_mfem_expected_wrapper_real) + set(_meson_mfem_mpi_owned TRUE) + endif() + endif() + endif() + set(${_result} ${_meson_mfem_mpi_owned} PARENT_SCOPE) +endfunction() + +if("ON") + foreach(_meson_mfem_mpi_language C CXX) + set(_meson_mfem_mpi_target "MPI::MPI_${_meson_mfem_mpi_language}") + if(_meson_mfem_mpi_language STREQUAL "C") + set(_meson_mfem_expected_mpi_wrapper "@MPICC@") + else() + set(_meson_mfem_expected_mpi_wrapper "@MPICXX@") + endif() + if(TARGET ${_meson_mfem_mpi_target}) + _meson_mfem_sundials_mpi_target_is_owned( + "${_meson_mfem_mpi_language}" + "${_meson_mfem_mpi_target}" + "${_meson_mfem_expected_mpi_wrapper}" + _meson_mfem_mpi_owned) + if(NOT _meson_mfem_mpi_owned) + message(FATAL_ERROR "SUNDIALS cannot use pre-existing ${_meson_mfem_mpi_target} from a different MPI installation") + endif() + endif() + endforeach() + + get_property(_meson_mfem_enabled_languages GLOBAL PROPERTY ENABLED_LANGUAGES) + if(NOT "C" IN_LIST _meson_mfem_enabled_languages) + enable_language(C) + endif() + if(NOT "CXX" IN_LIST _meson_mfem_enabled_languages) + enable_language(CXX) + endif() + set(MPI_C_COMPILER "@MPICC@" CACHE FILEPATH "MPI C compiler selected for SUNDIALS" FORCE) + set(MPI_CXX_COMPILER "@MPICXX@" CACHE FILEPATH "MPI C++ compiler selected for SUNDIALS" FORCE) + if(NOT TARGET MPI::MPI_C OR NOT TARGET MPI::MPI_CXX) + find_dependency(MPI COMPONENTS C CXX) + endif() + + foreach(_meson_mfem_mpi_language C CXX) + set(_meson_mfem_mpi_target "MPI::MPI_${_meson_mfem_mpi_language}") + set(_meson_mfem_mpi_compiler_variable "MPI_${_meson_mfem_mpi_language}_COMPILER") + if(_meson_mfem_mpi_language STREQUAL "C") + set(_meson_mfem_expected_mpi_wrapper "@MPICC@") + else() + set(_meson_mfem_expected_mpi_wrapper "@MPICXX@") + endif() + if(NOT TARGET ${_meson_mfem_mpi_target}) + message(FATAL_ERROR "SUNDIALS requires ${_meson_mfem_mpi_target}") + endif() + _meson_mfem_sundials_mpi_target_is_owned( + "${_meson_mfem_mpi_language}" + "${_meson_mfem_mpi_target}" + "${_meson_mfem_expected_mpi_wrapper}" + _meson_mfem_mpi_owned) + string(FIND "${${_meson_mfem_mpi_compiler_variable}}" "@MPI_PREFIX@/" _meson_mfem_mpi_compiler_index) + if(_meson_mfem_mpi_compiler_index EQUAL -1 OR NOT _meson_mfem_mpi_owned) + message(FATAL_ERROR "SUNDIALS cannot use ${_meson_mfem_mpi_target} from a different MPI installation") + endif() + set_property(TARGET ${_meson_mfem_mpi_target} PROPERTY IMPORTED_NO_SYSTEM TRUE) + if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.25") + set_property(TARGET ${_meson_mfem_mpi_target} PROPERTY SYSTEM FALSE) + endif() + endforeach() + unset(_meson_mfem_enabled_languages) + unset(_meson_mfem_mpi_language) + unset(_meson_mfem_mpi_target) + unset(_meson_mfem_mpi_compiler_variable) + unset(_meson_mfem_expected_mpi_wrapper) + unset(_meson_mfem_mpi_owned) + unset(_meson_mfem_mpi_compiler_index) +endif() +""" + sundials_mpi_block = ( + sundials_mpi_block.replace( + "@MPI_PREFIX@", selected_mpi_prefix + ) + .replace("@MPICC@", selected_mpicc) + .replace("@MPICXX@", selected_mpicxx) + ) + mpi_block_pattern = re.compile( + r'^if\("ON" AND NOT TARGET MPI::MPI_C\)\n.*?^endif\(\)\n', + flags=re.MULTILINE | re.DOTALL, + ) + text, replacement_count = mpi_block_pattern.subn( + sundials_mpi_block, + text, + count=1, + ) + if replacement_count != 1: + raise RuntimeError( + "SUNDIALSConfig.cmake MPI dependency block was not found" + ) + imported_target = re.compile( + r"^(?P[ \t]*)add_library\(" + r"(?P[^\s\)]+)(?P[^\n\)]*\bIMPORTED\b[^\n\)]*)" + r"\)[ \t]*$", + flags=re.MULTILINE, + ) + + def mark_imported_no_system(match: re.Match[str]) -> str: + declaration = match.group(0) + property_line = ( + f"{match.group('indent')}set_property(TARGET " + f"{match.group('target')} PROPERTY IMPORTED_NO_SYSTEM TRUE)" + ) + return declaration + "\n" + property_line + + if "IMPORTED_NO_SYSTEM TRUE" not in text: + text = imported_target.sub(mark_imported_no_system, text) + write_text_atomic(cmake_file, text) + + for libtool_archive in prefix.glob("**/*.la"): + libtool_archive.unlink() + + mpi_wrappers = {"mpicc", "mpicxx", "mpic++", "mpif77", "mpif90", "mpifort"} + if args.host_system != "windows": + for wrapper in prefix.glob("bin/*"): + text = _text_file(wrapper) + if ( + wrapper.name not in mpi_wrappers + or wrapper.is_symlink() + or text is None + or not text.startswith("#!") + ): + continue + text = re.sub( + r"\A#![^\n]*(?:ba)?sh\s*$", + "#!/usr/bin/env bash", + text, + count=1, + flags=re.MULTILINE, + ) + text = re.sub( + r"^prefix=.*$", + 'prefix="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"', + text, + flags=re.MULTILINE, + ) + compiler_variable = { + "mpicc": ("CC", "cc"), + "mpicxx": ("CXX", "c++"), + "mpic++": ("CXX", "c++"), + "mpif77": ("FC", "gfortran"), + "mpif90": ("FC", "gfortran"), + "mpifort": ("FC", "gfortran"), + }[wrapper.name] + text = re.sub( + rf'^{compiler_variable[0]}=".*"$', + f'{compiler_variable[0]}="{compiler_variable[1]}"', + text, + count=1, + flags=re.MULTILINE, + ) + for language_flags in ( + "final_cflags", + "final_cxxflags", + "final_fflags", + "final_fcflags", + ): + text = re.sub( + rf'^{language_flags}="[^"]*"$', + f'{language_flags}=""', + text, + flags=re.MULTILINE, + ) + for private_prefix in private_prefixes: + text = text.replace(private_prefix, "${prefix}") + wrapper_rpaths = ( + "-Wl,-rpath,${prefix}/lib -Wl,-rpath,${prefix}/lib64" + ) + if wrapper_rpaths not in text: + text = re.sub( + r'^(final_ldflags=")([^"]*)(")$', + rf'\1\2 {wrapper_rpaths}\3', + text, + count=1, + flags=re.MULTILINE, + ) + write_text_atomic(wrapper, text) + wrapper.chmod(wrapper.stat().st_mode | 0o111) + + if args.vendor_dependency_prefix == "true": + for helper in prefix.glob("bin/*"): + if helper.name in mpi_wrappers or helper.is_symlink(): + continue + text = _text_file(helper) + if ( + text is not None + and text.startswith("#!") + and any(candidate in text for candidate in private_prefixes) + ): + helper.unlink() + for private_metadata in ( + prefix / "lib" / "petsc" / "conf", + prefix / "lib64" / "petsc" / "conf", + ): + if private_metadata.is_dir(): + remove_tree(private_metadata) + for hdf5_settings in ( + prefix / "lib" / "libhdf5.settings", + prefix / "lib64" / "libhdf5.settings", + ): + if hdf5_settings.is_file(): + hdf5_settings.unlink() + + metadata_files = [ + config_header, + sundials_header, + config_mk, + *prefix.glob("**/*.pc"), + *prefix.glob("**/*.cmake"), + *prefix.glob("bin/*"), + ] + metadata_files.extend( + path for path in prefix.rglob("*") if path.is_file() and not path.is_symlink() + ) + for metadata_file in metadata_files: + if not metadata_file.is_file(): + continue + text = _text_file(metadata_file) + if text is None: + continue + for private_prefix in private_prefixes: + if private_prefix in text: + raise RuntimeError( + f"non-relocatable prefix remains in metadata: {metadata_file}" + ) + +def _macos_rpaths(binary: Path, otool: str) -> set[str]: + lines = output([otool, "-l", os.fspath(binary)]).splitlines() + paths: set[str] = set() + in_rpath = False + for line in lines: + stripped = line.strip() + if stripped == "cmd LC_RPATH": + in_rpath = True + elif in_rpath and stripped.startswith("path "): + paths.add(stripped.split()[1]) + in_rpath = False + return paths + + +def _is_macho(path: Path) -> bool: + """Return whether *path* starts with a thin or universal Mach-O magic.""" + try: + magic = path.read_bytes()[:4] + except OSError: + return False + return magic in { + b"\xfe\xed\xfa\xce", # MH_MAGIC + b"\xce\xfa\xed\xfe", # MH_CIGAM + b"\xfe\xed\xfa\xcf", # MH_MAGIC_64 + b"\xcf\xfa\xed\xfe", # MH_CIGAM_64 + b"\xca\xfe\xba\xbe", # FAT_MAGIC + b"\xbe\xba\xfe\xca", # FAT_CIGAM + b"\xca\xfe\xba\xbf", # FAT_MAGIC_64 + b"\xbf\xba\xfe\xca", # FAT_CIGAM_64 + } + + +def relocate_macos(prefix: Path, dependency_prefix: str = "") -> None: + install_name_tool = shutil.which("install_name_tool") + otool = shutil.which("otool") + if not install_name_tool or not otool: + raise RuntimeError("macOS relocation requires otool and install_name_tool") + library_roots = [prefix / "lib", prefix / "lib64"] + libraries = sorted( + path + for library_dir in library_roots + for path in library_dir.rglob("*.dylib") + if not path.is_symlink() + ) + library_dirs = sorted({library.parent for library in libraries}) + private_roots = [os.fspath(prefix.resolve())] + if dependency_prefix: + private_roots.append(os.fspath(Path(dependency_prefix).resolve())) + for library in libraries: + run([install_name_tool, "-id", f"@rpath/{library.name}", library]) + for binary in [*libraries, *sorted((prefix / "bin").glob("*"))]: + if not binary.is_file() or binary.is_symlink() or not _is_macho(binary): + continue + try: + dependencies = output([otool, "-L", os.fspath(binary)]).splitlines()[1:] + except subprocess.CalledProcessError: + continue + for dependency_line in dependencies: + old = dependency_line.strip().split(" ", 1)[0] + if any(old.startswith(root) for root in private_roots): + run( + [ + install_name_tool, + "-change", + old, + f"@rpath/{Path(old).name}", + binary, + ] + ) + existing_rpaths = _macos_rpaths(binary, otool) + for existing_rpath in sorted(existing_rpaths): + if any(existing_rpath.startswith(root) for root in private_roots): + run([install_name_tool, "-delete_rpath", existing_rpath, binary]) + existing_rpaths.remove(existing_rpath) + desired_rpaths = {"@loader_path"} + for library_dir in library_dirs: + relative = Path(os.path.relpath(library_dir, binary.parent)).as_posix() + desired_rpaths.add( + "@loader_path" if relative == "." else f"@loader_path/{relative}" + ) + for desired_rpath in sorted(desired_rpaths): + if desired_rpath not in existing_rpaths: + run([install_name_tool, "-add_rpath", desired_rpath, binary]) + remaining_private_rpaths = { + path + for path in _macos_rpaths(binary, otool) + if any(path.startswith(root) for root in private_roots) + } + if remaining_private_rpaths: + raise RuntimeError( + f"non-relocatable LC_RPATH remains in {binary}: " + + ", ".join(sorted(remaining_private_rpaths)) + ) + + +def relocate_elf(prefix: Path, require_tool: bool) -> None: + patchelf = shutil.which("patchelf") + if not patchelf: + if require_tool: + raise RuntimeError( + "a wheel-local Linux bundle requires patchelf; install it in the build environment" + ) + return + libraries = [ + *sorted((prefix / "lib").rglob("*.so*")), + *sorted((prefix / "lib64").rglob("*.so*")), + ] + library_dirs = sorted({library.parent for library in libraries}) + binaries = [*libraries, *sorted((prefix / "bin").glob("*"))] + for binary in binaries: + if not binary.is_file() or binary.is_symlink(): + continue + try: + if binary.read_bytes()[:4] != b"\x7fELF": + continue + except OSError: + continue + desired_rpaths = {"$ORIGIN"} + for library_dir in library_dirs: + relative = Path(os.path.relpath(library_dir, binary.parent)).as_posix() + desired_rpaths.add( + "$ORIGIN" if relative == "." else f"$ORIGIN/{relative}" + ) + desired_rpath = ":".join(sorted(desired_rpaths)) + run([patchelf, "--set-rpath", desired_rpath, binary]) + if binary.is_relative_to(prefix / "lib") or binary.is_relative_to(prefix / "lib64"): + soname = output([patchelf, "--print-soname", os.fspath(binary)]) + if os.path.isabs(soname): + run([patchelf, "--set-soname", Path(soname).name, binary]) + for needed in output([patchelf, "--print-needed", os.fspath(binary)]).splitlines(): + if os.path.isabs(needed): + run([patchelf, "--replace-needed", needed, Path(needed).name, binary]) + + +def main() -> int: + args = parse_args() + args.source = args.source.resolve() + args.work_dir = args.work_dir.resolve() + args.prefix = args.prefix.resolve() + if args.stamp: + args.stamp = args.stamp.resolve() + if args.mpich_source: + args.mpich_source = args.mpich_source.resolve() + if args.hypre_source: + args.hypre_source = args.hypre_source.resolve() + if args.metis_source: + args.metis_source = args.metis_source.resolve() + if args.gslib_source: + args.gslib_source = args.gslib_source.resolve() + if args.zlib_source: + args.zlib_source = args.zlib_source.resolve() + if args.sundials_source: + args.sundials_source = args.sundials_source.resolve() + if args.libceed_source: + args.libceed_source = args.libceed_source.resolve() + if args.fms_source: + args.fms_source = args.fms_source.resolve() + if args.algoim_source: + args.algoim_source = args.algoim_source.resolve() + if args.blitz_source: + args.blitz_source = args.blitz_source.resolve() + if args.dependency_prefix: + args.dependency_prefix = os.fspath(Path(args.dependency_prefix).resolve()) + dependency_root = Path(args.dependency_prefix) + if ( + dependency_root == args.prefix + or dependency_root.is_relative_to(args.prefix) + or args.prefix.is_relative_to(dependency_root) + ): + raise RuntimeError( + "dependency_prefix and the private output prefix must be disjoint" + ) + features = parse_features(args.feature) + build_type = cmake_build_type(args.buildtype) + compiler_identity = { + "cc": output([*args.cc_launcher, args.cc, "--version"]).splitlines()[0], + "cxx": output([*args.cxx_launcher, args.cxx, "--version"]).splitlines()[0], + } + fingerprint_data = { + "argv": sys.argv[1:], + "compiler": compiler_identity, + "platform": platform.platform(), + "builder_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), + "mfem_cmake_sha256": hashlib.sha256( + (args.source / "CMakeLists.txt").read_bytes() + ).hexdigest(), + } + if features["ceed"]: + ceed_jit_source = Path(__file__).with_name( + "ceed_jit_source_root_relocatable.c" + ) + fingerprint_data["ceed_jit_source_sha256"] = hashlib.sha256( + ceed_jit_source.read_bytes() + ).hexdigest() + if args.dependency_prefix: + fingerprint_data["dependency_prefix_manifest"] = prefix_manifest( + Path(args.dependency_prefix) + ) + fingerprint = hashlib.sha256( + json.dumps(fingerprint_data, sort_keys=True).encode("utf-8") + ).hexdigest() + config_file = args.work_dir.parent / "configuration.json" + library_glob = "libmfem.a" if args.library_kind == "static" else "libmfem.*" + existing_libraries = list((args.prefix / "lib").glob(library_glob)) + if config_file.is_file(): + old = json.loads(config_file.read_text(encoding="utf-8")) + if old.get("fingerprint") == fingerprint and existing_libraries: + if args.stamp: + write_text_atomic(args.stamp, fingerprint + "\n") + print("[mfem-bundle] configuration unchanged; reusing private prefix", flush=True) + return 0 + + remove_tree(args.work_dir) + remove_tree(args.prefix) + args.work_dir.mkdir(parents=True, exist_ok=True) + args.prefix.mkdir(parents=True, exist_ok=True) + + env = dict(os.environ) + path_entries = [os.fspath(args.prefix / "bin")] + if args.dependency_prefix: + path_entries.append(os.fspath(Path(args.dependency_prefix) / "bin")) + path_entries.append(env.get("PATH", "")) + env["PATH"] = os.pathsep.join(path_entries) + env["CMAKE_PREFIX_PATH"] = os.pathsep.join( + filter(None, [os.fspath(args.prefix), args.dependency_prefix]) + ) + pkg_config_paths = [ + os.fspath(args.prefix / "lib" / "pkgconfig"), + os.fspath(args.prefix / "lib64" / "pkgconfig"), + os.fspath(args.prefix / "share" / "pkgconfig"), + ] + if args.dependency_prefix: + dependency_root = Path(args.dependency_prefix) + pkg_config_paths.extend( + [ + os.fspath(dependency_root / "lib" / "pkgconfig"), + os.fspath(dependency_root / "lib64" / "pkgconfig"), + os.fspath(dependency_root / "share" / "pkgconfig"), + ] + ) + env["PKG_CONFIG_PATH"] = os.pathsep.join(pkg_config_paths) + if args.allow_system == "false": + env["PKG_CONFIG_LIBDIR"] = env["PKG_CONFIG_PATH"] + for key in ("CPATH", "LIBRARY_PATH", "DYLD_LIBRARY_PATH", "LD_LIBRARY_PATH"): + env.pop(key, None) + + if args.dependency_prefix and args.vendor_dependency_prefix == "true": + copy_prefix(Path(args.dependency_prefix), args.prefix) + + if features["zlib"] and not bundle_has(args, "include/zlib.h"): + build_zlib(args, env, build_type) + mpicc, mpicxx = args.cc, args.cxx + if features["mpi"]: + if args.mpich_source: + mpicc, mpicxx = build_mpich(args, features, env, build_type) + else: + mpi_search_path = [os.fspath(args.prefix / "bin")] + if args.dependency_prefix: + mpi_search_path.append( + os.fspath(Path(args.dependency_prefix) / "bin") + ) + if args.allow_system == "true": + mpi_search_path.append(os.environ.get("PATH", "")) + mpi_path = os.pathsep.join(mpi_search_path) + mpicc_path = shutil.which("mpicc", path=mpi_path) + mpicxx_path = shutil.which("mpicxx", path=mpi_path) + if not mpicc_path or not mpicxx_path: + raise RuntimeError( + "MPI is enabled without the bundled POSIX provider, but " + "mpicc/mpicxx were not found in dependency_prefix/bin or PATH" + ) + mpicc, mpicxx = mpicc_path, mpicxx_path + env["PATH"] = os.pathsep.join( + [os.fspath(args.prefix / "bin"), env.get("PATH", "")] + ) + if features["metis"] and args.metis_source and not bundle_has(args, "include/metis.h"): + build_metis(args, env, build_type) + if features["mpi"] and args.hypre_source: + build_hypre(args, features, env, build_type, mpicc) + if features["gslib"] and args.gslib_source and ( + args.mpich_source or not bundle_has(args, "include/gslib/gslib.h") + ): + build_gslib(args, features, env, build_type, mpicc) + if features["sundials"] and args.sundials_source and ( + args.mpich_source + or not bundle_has(args, "include/sundials/sundials_config.h") + ): + build_sundials(args, features, env, build_type, mpicc, mpicxx) + if features["ceed"] and args.libceed_source and not bundle_has(args, "include/ceed.h"): + build_libceed(args, features, env, build_type) + if features["fms"] and args.fms_source and not bundle_has(args, "include/fms.h"): + build_fms(args, features, env, build_type) + if ( + features["algoim"] + and args.algoim_source + and ( + not bundle_has(args, "include/algoim_quad.hpp") + or not bundle_has(args, "include/blitz/array.h") + ) + ): + build_algoim(args, env, build_type) + build_mfem(args, features, env, build_type, mpicc, mpicxx) + + header = args.prefix / "include" / "mfem.hpp" + libraries = list((args.prefix / "lib").glob(library_glob)) + if not header.is_file() or not libraries: + raise RuntimeError("MFEM install validation failed: missing header or library") + validate_mfem_feature_header(args, features) + + install_licenses( + args, + [ + ("mfem", args.source), + ("mpich", args.mpich_source if features["mpi"] else None), + ("hypre", args.hypre_source if features["mpi"] else None), + ("metis", args.metis_source if features["metis"] else None), + ("gslib", args.gslib_source if features["gslib"] else None), + ("zlib", args.zlib_source if features["zlib"] else None), + ("sundials", args.sundials_source if features["sundials"] else None), + ("libceed", args.libceed_source if features["ceed"] else None), + ("fms", args.fms_source if features["fms"] else None), + ("algoim", args.algoim_source if features["algoim"] else None), + ("blitz", args.blitz_source if features["algoim"] else None), + ], + ) + sanitize_installed_metadata(args) + if args.host_system == "darwin" and args.library_kind == "shared": + relocate_macos( + args.prefix, + args.dependency_prefix + if args.vendor_dependency_prefix == "true" + else "", + ) + elif args.host_system not in {"windows", "emscripten"} and args.library_kind == "shared": + relocate_elf( + args.prefix, + require_tool=args.vendor_dependency_prefix == "true", + ) + + manifest = { + "fingerprint": fingerprint, + "features": features, + "compiler": compiler_identity, + "build_type": build_type, + "prefix": os.fspath(args.prefix), + } + write_text_atomic(config_file, json.dumps(manifest, indent=2, sort_keys=True) + "\n") + if args.stamp: + write_text_atomic(args.stamp, fingerprint + "\n") + print(f"[mfem-bundle] installed MFEM 4.10 in {args.prefix}", flush=True) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/ceed_jit_source_root_relocatable.c b/tools/ceed_jit_source_root_relocatable.c new file mode 100644 index 0000000..5f2d6e5 --- /dev/null +++ b/tools/ceed_jit_source_root_relocatable.c @@ -0,0 +1,51 @@ +// Copyright (c) 2017-2022, Lawrence Livermore National Security, LLC and +// other CEED contributors. All Rights Reserved. +// SPDX-License-Identifier: BSD-2-Clause + +// Relocatable replacement for libCEED's installed JIT source-root object. +// Upstream normally compiles the installation prefix into libceed. Native +// source bundles here are shared libraries, so derive ../include from the +// loaded library instead. Emscripten has no runtime compiler/JIT filesystem. + +#if defined(__EMSCRIPTEN__) + +const char *CeedJitSourceRootDefault = ""; + +#else + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include +#include +#include +#include + +#ifndef PATH_MAX +#define PATH_MAX 4096 +#endif + +static char ceed_jit_source_root[PATH_MAX]; +const char *CeedJitSourceRootDefault = ceed_jit_source_root; + +__attribute__((constructor)) static void CeedSetRelocatableJitSourceRoot(void) { + Dl_info info; + char library_path[PATH_MAX]; + + if (!dladdr((void *)&CeedJitSourceRootDefault, &info) || !info.dli_fname) { + return; + } + if (snprintf(library_path, sizeof library_path, "%s", info.dli_fname) < 0) { + return; + } + char *separator = strrchr(library_path, '/'); + if (!separator) { + return; + } + *separator = '\0'; + (void)snprintf(ceed_jit_source_root, sizeof ceed_jit_source_root, + "%s/../include/", library_path); +} + +#endif diff --git a/tools/install_bundle.py b/tools/install_bundle.py new file mode 100644 index 0000000..fba18e4 --- /dev/null +++ b/tools/install_bundle.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Copy a build-tree MFEM prefix into a Meson/meson-python destination.""" + +from __future__ import annotations + +import argparse +import os +from pathlib import Path +import shutil + + +def staged_destination(destination: Path) -> Path: + destdir = os.environ.get("DESTDIR", "") + if not destdir or not destination.is_absolute(): + return destination + destination_text = os.fspath(destination) + relative = destination_text[len(destination.anchor) :].lstrip("/\\") + return Path(destdir) / relative + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--source", type=Path, required=True) + parser.add_argument("--destination", type=Path, required=True) + args = parser.parse_args() + destination = staged_destination(args.destination) + destination.mkdir(parents=True, exist_ok=True) + for child in args.source.iterdir(): + target = destination / child.name + if child.is_dir(): + shutil.copytree(child, target, dirs_exist_ok=True, symlinks=True) + else: + shutil.copy2(child, target, follow_symlinks=False) + print(f"Installed MFEM native bundle to {destination}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/run_mpi_test.py b/tools/run_mpi_test.py new file mode 100644 index 0000000..ab28156 --- /dev/null +++ b/tools/run_mpi_test.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""Run one MPI test without baking a particular MPI launcher into the meson build tree.""" + +from __future__ import annotations + +import argparse +import os +import sys +import subprocess + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--launcher", required=True) + parser.add_argument("--processes", type=int, default=2) + parser.add_argument("executable") + parser.add_argument("arguments", nargs="*") + args = parser.parse_args() + command = [args.launcher, "-n", str(args.processes), args.executable, *args.arguments] + environment = dict(os.environ) + environment.setdefault("OMPI_ALLOW_RUN_AS_ROOT", "1") + environment.setdefault("OMPI_ALLOW_RUN_AS_ROOT_CONFIRM", "1") + loopback = "lo0" if sys.platform == "darwin" else "lo" + environment.setdefault("FI_PROVIDER", "sockets") + environment.setdefault("FI_SOCKETS_IFACE", loopback) + environment.setdefault("HYDRA_IFACE", loopback) + if sys.platform == "darwin": + environment.setdefault("MPICH_INTERFACE_HOSTNAME", "127.0.0.1") + return subprocess.run(command, env=environment, check=False).returncode + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/validate_matrix.sh b/tools/validate_matrix.sh new file mode 100755 index 0000000..27fdd20 --- /dev/null +++ b/tools/validate_matrix.sh @@ -0,0 +1,66 @@ +#!/bin/sh +set -eu + +project_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +meson_bin=${MESON_BIN:-meson} +meson_dir=$(CDPATH= cd -- "$(dirname -- "$meson_bin")" && pwd) +profile=${MFEM_MATRIX_PROFILE:-portable} +jobs=${MFEM_JOBS:-4} + +compiler_family() { + "$1" --version 2>&1 | sed -n '1p' +} + +require_clang() { + version=$(compiler_family "$1") + case "$version" in + *clang*|*Clang*) ;; + *) echo "error: $1 is not a Clang compiler: $version" >&2; exit 2 ;; + esac +} + +require_gnu() { + version=$(compiler_family "$1") + case "$version" in + *gcc*|*GCC*|*"Free Software Foundation"*) ;; + *) echo "error: $1 is not a GNU compiler: $version" >&2; exit 2 ;; + esac +} + +run_configuration() { + name=$1 + c_compiler=$2 + cxx_compiler=$3 + mode=$4 + build_dir="$project_root/build-$name" + + if test -f "$build_dir/meson-private/coredata.dat"; then + wipe_flag=--wipe + else + wipe_flag= + fi + env PATH="$meson_dir:$PATH" CC="$c_compiler" CXX="$cxx_compiler" \ + "$meson_bin" setup $wipe_flag "$build_dir" "$project_root" \ + --buildtype="$mode" \ + -Dallow_preinstalled=false \ + -Dfeature_profile="$profile" \ + -Dbuild_examples=true \ + -Dbuild_tests=true \ + -Dbuild_python=false \ + -Djobs="$jobs" + env PATH="$meson_dir:$PATH" "$meson_bin" compile -C "$build_dir" + env PATH="$meson_dir:$PATH" "$meson_bin" test -C "$build_dir" --print-errorlogs +} + +clang_cc=${CLANG_CC:-clang} +clang_cxx=${CLANG_CXX:-clang++} +gcc_cc=${GNU_CC:-gcc} +gcc_cxx=${GNU_CXX:-g++} + +require_clang "$clang_cxx" +require_gnu "$gcc_cxx" + +run_configuration clang-debug "$clang_cc" "$clang_cxx" debug +run_configuration clang-release "$clang_cc" "$clang_cxx" release +run_configuration gcc-debug "$gcc_cc" "$gcc_cxx" debug +run_configuration gcc-release "$gcc_cc" "$gcc_cxx" release