232 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			232 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
|  | import errno | ||
|  | import os | ||
|  | import re | ||
|  | import shutil | ||
|  | import subprocess | ||
|  | import sys | ||
|  | from itertools import chain | ||
|  | from pathlib import Path | ||
|  | from string import Template | ||
|  | 
 | ||
|  | from ._backend import Backend | ||
|  | 
 | ||
|  | 
 | ||
|  | 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 | ||
|  |         # Convert args to '' wrapped variant for meson | ||
|  |         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(list(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 = meson_build.replace(",,", ",") | ||
|  |         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"), | ||
|  |             walk_dir.glob(f"{self.modulename}*.dll"), | ||
|  |         ) | ||
|  |         # Same behavior as distutils | ||
|  |         # https://github.com/numpy/numpy/issues/24874#issuecomment-1835632293 | ||
|  |         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) | ||
|  |     # Copy sources | ||
|  |     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) | ||
|  |     # Hacky way to preserve order of flags | ||
|  |     unique_flags = list(dict.fromkeys(flag_values)) | ||
|  |     return unique_flags |