#!/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, "--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=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["mpi"]: # Keep CMake's compiler identity on the host compiler and let FindMPI # interrogate the wrapper. Treating mpicc itself as CMAKE_C_COMPILER # makes MPI's include path look implicit, so CUDA translation units # compiled by nvcc do not receive mpi.h. options.append(f"-DMPI_C_COMPILER={mpicc}") 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"]: zlib_root = bundle_root(args, "include/zlib.h") options.append(f"-DZLIB_ROOT={zlib_root}") if args.host_system == "emscripten": options.append(f"-DZLIB_LIBRARY={zlib_root / 'lib' / 'libz.a'}") 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())