_meson.py 8.4 KB

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