|
from __future__ import annotations |
|
|
|
import os |
|
import errno |
|
import shutil |
|
import subprocess |
|
import sys |
|
import re |
|
from pathlib import Path |
|
|
|
from ._backend import Backend |
|
from string import Template |
|
from itertools import chain |
|
|
|
import warnings |
|
|
|
|
|
class MesonTemplate: |
|
"""Template meson build file generation class.""" |
|
|
|
def __init__( |
|
self, |
|
modulename: str, |
|
sources: list[Path], |
|
deps: list[str], |
|
libraries: list[str], |
|
library_dirs: list[Path], |
|
include_dirs: list[Path], |
|
object_files: list[Path], |
|
linker_args: list[str], |
|
fortran_args: list[str], |
|
build_type: str, |
|
python_exe: str, |
|
): |
|
self.modulename = modulename |
|
self.build_template_path = ( |
|
Path(__file__).parent.absolute() / "meson.build.template" |
|
) |
|
self.sources = sources |
|
self.deps = deps |
|
self.libraries = libraries |
|
self.library_dirs = library_dirs |
|
if include_dirs is not None: |
|
self.include_dirs = include_dirs |
|
else: |
|
self.include_dirs = [] |
|
self.substitutions = {} |
|
self.objects = object_files |
|
|
|
self.fortran_args = [ |
|
f"'{x}'" if not (x.startswith("'") and x.endswith("'")) else x |
|
for x in fortran_args |
|
] |
|
self.pipeline = [ |
|
self.initialize_template, |
|
self.sources_substitution, |
|
self.deps_substitution, |
|
self.include_substitution, |
|
self.libraries_substitution, |
|
self.fortran_args_substitution, |
|
] |
|
self.build_type = build_type |
|
self.python_exe = python_exe |
|
self.indent = " " * 21 |
|
|
|
def meson_build_template(self) -> str: |
|
if not self.build_template_path.is_file(): |
|
raise FileNotFoundError( |
|
errno.ENOENT, |
|
"Meson build template" |
|
f" {self.build_template_path.absolute()}" |
|
" does not exist.", |
|
) |
|
return self.build_template_path.read_text() |
|
|
|
def initialize_template(self) -> None: |
|
self.substitutions["modulename"] = self.modulename |
|
self.substitutions["buildtype"] = self.build_type |
|
self.substitutions["python"] = self.python_exe |
|
|
|
def sources_substitution(self) -> None: |
|
self.substitutions["source_list"] = ",\n".join( |
|
[f"{self.indent}'''{source}'''," for source in self.sources] |
|
) |
|
|
|
def deps_substitution(self) -> None: |
|
self.substitutions["dep_list"] = f",\n{self.indent}".join( |
|
[f"{self.indent}dependency('{dep}')," for dep in self.deps] |
|
) |
|
|
|
def libraries_substitution(self) -> None: |
|
self.substitutions["lib_dir_declarations"] = "\n".join( |
|
[ |
|
f"lib_dir_{i} = declare_dependency(link_args : ['''-L{lib_dir}'''])" |
|
for i, lib_dir in enumerate(self.library_dirs) |
|
] |
|
) |
|
|
|
self.substitutions["lib_declarations"] = "\n".join( |
|
[ |
|
f"{lib.replace('.','_')} = declare_dependency(link_args : ['-l{lib}'])" |
|
for lib in self.libraries |
|
] |
|
) |
|
|
|
self.substitutions["lib_list"] = f"\n{self.indent}".join( |
|
[f"{self.indent}{lib.replace('.','_')}," for lib in self.libraries] |
|
) |
|
self.substitutions["lib_dir_list"] = f"\n{self.indent}".join( |
|
[f"{self.indent}lib_dir_{i}," for i in range(len(self.library_dirs))] |
|
) |
|
|
|
def include_substitution(self) -> None: |
|
self.substitutions["inc_list"] = f",\n{self.indent}".join( |
|
[f"{self.indent}'''{inc}'''," for inc in self.include_dirs] |
|
) |
|
|
|
def fortran_args_substitution(self) -> None: |
|
if self.fortran_args: |
|
self.substitutions["fortran_args"] = ( |
|
f"{self.indent}fortran_args: [{', '.join([arg for arg in self.fortran_args])}]," |
|
) |
|
else: |
|
self.substitutions["fortran_args"] = "" |
|
|
|
def generate_meson_build(self): |
|
for node in self.pipeline: |
|
node() |
|
template = Template(self.meson_build_template()) |
|
meson_build = template.substitute(self.substitutions) |
|
meson_build = re.sub(r",,", ",", meson_build) |
|
return meson_build |
|
|
|
|
|
class MesonBackend(Backend): |
|
def __init__(self, *args, **kwargs): |
|
super().__init__(*args, **kwargs) |
|
self.dependencies = self.extra_dat.get("dependencies", []) |
|
self.meson_build_dir = "bbdir" |
|
self.build_type = ( |
|
"debug" if any("debug" in flag for flag in self.fc_flags) else "release" |
|
) |
|
self.fc_flags = _get_flags(self.fc_flags) |
|
|
|
def _move_exec_to_root(self, build_dir: Path): |
|
walk_dir = Path(build_dir) / self.meson_build_dir |
|
path_objects = chain( |
|
walk_dir.glob(f"{self.modulename}*.so"), |
|
walk_dir.glob(f"{self.modulename}*.pyd"), |
|
) |
|
|
|
|
|
for path_object in path_objects: |
|
dest_path = Path.cwd() / path_object.name |
|
if dest_path.exists(): |
|
dest_path.unlink() |
|
shutil.copy2(path_object, dest_path) |
|
os.remove(path_object) |
|
|
|
def write_meson_build(self, build_dir: Path) -> None: |
|
"""Writes the meson build file at specified location""" |
|
meson_template = MesonTemplate( |
|
self.modulename, |
|
self.sources, |
|
self.dependencies, |
|
self.libraries, |
|
self.library_dirs, |
|
self.include_dirs, |
|
self.extra_objects, |
|
self.flib_flags, |
|
self.fc_flags, |
|
self.build_type, |
|
sys.executable, |
|
) |
|
src = meson_template.generate_meson_build() |
|
Path(build_dir).mkdir(parents=True, exist_ok=True) |
|
meson_build_file = Path(build_dir) / "meson.build" |
|
meson_build_file.write_text(src) |
|
return meson_build_file |
|
|
|
def _run_subprocess_command(self, command, cwd): |
|
subprocess.run(command, cwd=cwd, check=True) |
|
|
|
def run_meson(self, build_dir: Path): |
|
setup_command = ["meson", "setup", self.meson_build_dir] |
|
self._run_subprocess_command(setup_command, build_dir) |
|
compile_command = ["meson", "compile", "-C", self.meson_build_dir] |
|
self._run_subprocess_command(compile_command, build_dir) |
|
|
|
def compile(self) -> None: |
|
self.sources = _prepare_sources(self.modulename, self.sources, self.build_dir) |
|
self.write_meson_build(self.build_dir) |
|
self.run_meson(self.build_dir) |
|
self._move_exec_to_root(self.build_dir) |
|
|
|
|
|
def _prepare_sources(mname, sources, bdir): |
|
extended_sources = sources.copy() |
|
Path(bdir).mkdir(parents=True, exist_ok=True) |
|
|
|
for source in sources: |
|
if Path(source).exists() and Path(source).is_file(): |
|
shutil.copy(source, bdir) |
|
generated_sources = [ |
|
Path(f"{mname}module.c"), |
|
Path(f"{mname}-f2pywrappers2.f90"), |
|
Path(f"{mname}-f2pywrappers.f"), |
|
] |
|
bdir = Path(bdir) |
|
for generated_source in generated_sources: |
|
if generated_source.exists(): |
|
shutil.copy(generated_source, bdir / generated_source.name) |
|
extended_sources.append(generated_source.name) |
|
generated_source.unlink() |
|
extended_sources = [ |
|
Path(source).name |
|
for source in extended_sources |
|
if not Path(source).suffix == ".pyf" |
|
] |
|
return extended_sources |
|
|
|
|
|
def _get_flags(fc_flags): |
|
flag_values = [] |
|
flag_pattern = re.compile(r"--f(77|90)flags=(.*)") |
|
for flag in fc_flags: |
|
match_result = flag_pattern.match(flag) |
|
if match_result: |
|
values = match_result.group(2).strip().split() |
|
values = [val.strip("'\"") for val in values] |
|
flag_values.extend(values) |
|
|
|
unique_flags = list(dict.fromkeys(flag_values)) |
|
return unique_flags |
|
|