_meson.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. from __future__ import annotations
  2. import os
  3. import errno
  4. import shutil
  5. import subprocess
  6. import sys
  7. import re
  8. from pathlib import Path
  9. from ._backend import Backend
  10. from string import Template
  11. from itertools import chain
  12. class MesonTemplate:
  13. """Template meson build file generation class."""
  14. def __init__(
  15. self,
  16. modulename: str,
  17. sources: list[Path],
  18. deps: list[str],
  19. libraries: list[str],
  20. library_dirs: list[Path],
  21. include_dirs: list[Path],
  22. object_files: list[Path],
  23. linker_args: list[str],
  24. fortran_args: list[str],
  25. build_type: str,
  26. python_exe: str,
  27. ):
  28. self.modulename = modulename
  29. self.build_template_path = (
  30. Path(__file__).parent.absolute() / "meson.build.template"
  31. )
  32. self.sources = sources
  33. self.deps = deps
  34. self.libraries = libraries
  35. self.library_dirs = library_dirs
  36. if include_dirs is not None:
  37. self.include_dirs = include_dirs
  38. else:
  39. self.include_dirs = []
  40. self.substitutions = {}
  41. self.objects = object_files
  42. # Convert args to '' wrapped variant for meson
  43. self.fortran_args = [
  44. f"'{x}'" if not (x.startswith("'") and x.endswith("'")) else x
  45. for x in fortran_args
  46. ]
  47. self.pipeline = [
  48. self.initialize_template,
  49. self.sources_substitution,
  50. self.deps_substitution,
  51. self.include_substitution,
  52. self.libraries_substitution,
  53. self.fortran_args_substitution,
  54. ]
  55. self.build_type = build_type
  56. self.python_exe = python_exe
  57. self.indent = " " * 21
  58. def meson_build_template(self) -> str:
  59. if not self.build_template_path.is_file():
  60. raise FileNotFoundError(
  61. errno.ENOENT,
  62. "Meson build template"
  63. f" {self.build_template_path.absolute()}"
  64. " does not exist.",
  65. )
  66. return self.build_template_path.read_text()
  67. def initialize_template(self) -> None:
  68. self.substitutions["modulename"] = self.modulename
  69. self.substitutions["buildtype"] = self.build_type
  70. self.substitutions["python"] = self.python_exe
  71. def sources_substitution(self) -> None:
  72. self.substitutions["source_list"] = ",\n".join(
  73. [f"{self.indent}'''{source}'''," for source in self.sources]
  74. )
  75. def deps_substitution(self) -> None:
  76. self.substitutions["dep_list"] = f",\n{self.indent}".join(
  77. [f"{self.indent}dependency('{dep}')," for dep in self.deps]
  78. )
  79. def libraries_substitution(self) -> None:
  80. self.substitutions["lib_dir_declarations"] = "\n".join(
  81. [
  82. f"lib_dir_{i} = declare_dependency(link_args : ['''-L{lib_dir}'''])"
  83. for i, lib_dir in enumerate(self.library_dirs)
  84. ]
  85. )
  86. self.substitutions["lib_declarations"] = "\n".join(
  87. [
  88. f"{lib.replace('.','_')} = declare_dependency(link_args : ['-l{lib}'])"
  89. for lib in self.libraries
  90. ]
  91. )
  92. self.substitutions["lib_list"] = f"\n{self.indent}".join(
  93. [f"{self.indent}{lib.replace('.','_')}," for lib in self.libraries]
  94. )
  95. self.substitutions["lib_dir_list"] = f"\n{self.indent}".join(
  96. [f"{self.indent}lib_dir_{i}," for i in range(len(self.library_dirs))]
  97. )
  98. def include_substitution(self) -> None:
  99. self.substitutions["inc_list"] = f",\n{self.indent}".join(
  100. [f"{self.indent}'''{inc}'''," for inc in self.include_dirs]
  101. )
  102. def fortran_args_substitution(self) -> None:
  103. if self.fortran_args:
  104. self.substitutions["fortran_args"] = (
  105. f"{self.indent}fortran_args: [{', '.join(list(self.fortran_args))}],"
  106. )
  107. else:
  108. self.substitutions["fortran_args"] = ""
  109. def generate_meson_build(self):
  110. for node in self.pipeline:
  111. node()
  112. template = Template(self.meson_build_template())
  113. meson_build = template.substitute(self.substitutions)
  114. meson_build = re.sub(r",,", ",", meson_build)
  115. return meson_build
  116. class MesonBackend(Backend):
  117. def __init__(self, *args, **kwargs):
  118. super().__init__(*args, **kwargs)
  119. self.dependencies = self.extra_dat.get("dependencies", [])
  120. self.meson_build_dir = "bbdir"
  121. self.build_type = (
  122. "debug" if any("debug" in flag for flag in self.fc_flags) else "release"
  123. )
  124. self.fc_flags = _get_flags(self.fc_flags)
  125. def _move_exec_to_root(self, build_dir: Path):
  126. walk_dir = Path(build_dir) / self.meson_build_dir
  127. path_objects = chain(
  128. walk_dir.glob(f"{self.modulename}*.so"),
  129. walk_dir.glob(f"{self.modulename}*.pyd"),
  130. )
  131. # Same behavior as distutils
  132. # https://github.com/numpy/numpy/issues/24874#issuecomment-1835632293
  133. for path_object in path_objects:
  134. dest_path = Path.cwd() / path_object.name
  135. if dest_path.exists():
  136. dest_path.unlink()
  137. shutil.copy2(path_object, dest_path)
  138. os.remove(path_object)
  139. def write_meson_build(self, build_dir: Path) -> None:
  140. """Writes the meson build file at specified location"""
  141. meson_template = MesonTemplate(
  142. self.modulename,
  143. self.sources,
  144. self.dependencies,
  145. self.libraries,
  146. self.library_dirs,
  147. self.include_dirs,
  148. self.extra_objects,
  149. self.flib_flags,
  150. self.fc_flags,
  151. self.build_type,
  152. sys.executable,
  153. )
  154. src = meson_template.generate_meson_build()
  155. Path(build_dir).mkdir(parents=True, exist_ok=True)
  156. meson_build_file = Path(build_dir) / "meson.build"
  157. meson_build_file.write_text(src)
  158. return meson_build_file
  159. def _run_subprocess_command(self, command, cwd):
  160. subprocess.run(command, cwd=cwd, check=True)
  161. def run_meson(self, build_dir: Path):
  162. setup_command = ["meson", "setup", self.meson_build_dir]
  163. self._run_subprocess_command(setup_command, build_dir)
  164. compile_command = ["meson", "compile", "-C", self.meson_build_dir]
  165. self._run_subprocess_command(compile_command, build_dir)
  166. def compile(self) -> None:
  167. self.sources = _prepare_sources(self.modulename, self.sources, self.build_dir)
  168. self.write_meson_build(self.build_dir)
  169. self.run_meson(self.build_dir)
  170. self._move_exec_to_root(self.build_dir)
  171. def _prepare_sources(mname, sources, bdir):
  172. extended_sources = sources.copy()
  173. Path(bdir).mkdir(parents=True, exist_ok=True)
  174. # Copy sources
  175. for source in sources:
  176. if Path(source).exists() and Path(source).is_file():
  177. shutil.copy(source, bdir)
  178. generated_sources = [
  179. Path(f"{mname}module.c"),
  180. Path(f"{mname}-f2pywrappers2.f90"),
  181. Path(f"{mname}-f2pywrappers.f"),
  182. ]
  183. bdir = Path(bdir)
  184. for generated_source in generated_sources:
  185. if generated_source.exists():
  186. shutil.copy(generated_source, bdir / generated_source.name)
  187. extended_sources.append(generated_source.name)
  188. generated_source.unlink()
  189. extended_sources = [
  190. Path(source).name
  191. for source in extended_sources
  192. if not Path(source).suffix == ".pyf"
  193. ]
  194. return extended_sources
  195. def _get_flags(fc_flags):
  196. flag_values = []
  197. flag_pattern = re.compile(r"--f(77|90)flags=(.*)")
  198. for flag in fc_flags:
  199. match_result = flag_pattern.match(flag)
  200. if match_result:
  201. values = match_result.group(2).strip().split()
  202. values = [val.strip("'\"") for val in values]
  203. flag_values.extend(values)
  204. # Hacky way to preserve order of flags
  205. unique_flags = list(dict.fromkeys(flag_values))
  206. return unique_flags