2388 lines
92 KiB
Python
2388 lines
92 KiB
Python
#!/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 <root>/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 <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#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 <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#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 <cstdlib>
|
|
|
|
#if defined(_WIN32) && !defined(__EMSCRIPTEN__)
|
|
#ifndef WIN32_LEAN_AND_MEAN
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#endif
|
|
#include <windows.h>
|
|
#elif !defined(__EMSCRIPTEN__)
|
|
#include <dlfcn.h>
|
|
#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<LPCSTR>(&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<void *>(&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<indent>[ \t]*)add_library\("
|
|
r"(?P<target>[^\s\)]+)(?P<arguments>[^\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())
|