-
-
Notifications
You must be signed in to change notification settings - Fork 10.9k
ENH: Meson build system support for F2PY's f2py -c
command
#22225
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a5dfd68
1eacfb0
173a7bb
52762a9
c0689cb
44bec87
81b14f4
7c68149
9ab806a
c70272c
9fde953
4c4b99a
bbb44a7
d26bd6a
9ce2140
bcf7902
568b94d
b3d8a2b
4180fca
a0eab32
528dc4f
d2a72b1
475f4df
aa3d300
885c772
f39eb6d
1df8ee2
15be187
e13faa1
28e5beb
810aad7
bc8dfa9
fc68223
be8a4b0
d7ad3ec
7409f5a
da01606
c2f600f
fd58788
526d374
70352a3
c8fc749
15b7baa
dea01ca
392101b
50e2c85
9f3efd1
e902559
bb41e54
39ef205
5d9f607
9ad8f81
02e9ecd
19b3948
f101211
fc2a708
2e38e83
bbad3ea
80192f9
d0f7d3e
68b79eb
691059e
21cfaf1
30748db
5539245
8226050
4f9c847
f4ab7fa
7782efe
51290b1
69d3a2f
1c8d67e
8cec862
1b2d883
8217138
b3823b1
499c45c
f20254e
68789c5
91b77bf
7762341
6bd3e6c
5fd5123
911fd6a
dac9dd4
b891188
47a9426
5772ec6
0455847
e1f722f
302c77c
f105eec
1cc1ab9
116315e
4b55a17
232decf
257b443
d8061f4
3aa40b7
ac164c7
58fb9c3
e4c5d0b
5f599aa
e9fe1ce
379bb56
f1d93c0
8c0858b
5b99e87
af364d0
2154c47
2803ac5
5a2706f
198895c
6f8ccbb
28414fa
66fd50a
dd08d47
9ab0c9d
706443c
4a114ce
26cc090
ee41f1e
76ab74b
bfd5c86
24d7087
46c466f
29c31ee
64fdd3e
5888fbf
5664a98
a1b8219
cf21444
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
# See: | ||
# https://web.archive.org/web/20140822061353/http://cens.ioc.ee/projects/f2py2e | ||
from numpy.f2py.f2py2e import main | ||
from numpy.f2py.f2pyarg import main | ||
|
||
main() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from .backend import Backend | ||
from .meson_backend import MesonBackend | ||
|
||
backends = { | ||
'meson': MesonBackend | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
from __future__ import annotations | ||
|
||
from pathlib import Path | ||
from abc import ABC, abstractmethod | ||
|
||
import numpy | ||
import numpy.f2py as f2py | ||
|
||
class Backend(ABC): | ||
""" | ||
Superclass for backend compilation plugins to extend. | ||
|
||
""" | ||
|
||
def __init__(self, module_name, fortran_compiler: str, c_compiler: str, | ||
f77exec: Path, f90exec: Path, f77_flags: list[str], | ||
f90_flags: list[str], include_paths: list[Path], | ||
include_dirs: list[Path], external_resources: list[str], | ||
linker_libpath: list[Path], linker_libname: list[str], | ||
define_macros: list[tuple[str, str]], undef_macros: list[str], | ||
debug: bool, opt_flags: list[str], arch_flags: list[str], | ||
no_opt: bool, no_arch: bool) -> None: | ||
""" | ||
The class is initialized with f2py compile options. | ||
The parameters are mappings of f2py compilation flags. | ||
|
||
Parameters | ||
---------- | ||
module_name : str | ||
The name of the module to be compiled. (-m) | ||
fortran_compiler : str | ||
Name of the Fortran compiler to use. (--fcompiler) | ||
c_compiler : str | ||
Name of the C compiler to use. (--ccompiler) | ||
f77exec : Pathlike | ||
Path to the fortran compiler for Fortran 77 files (--f77exec) | ||
f90exec : Pathlike | ||
Path to the fortran compiler for Fortran 90 and above files (--f90exec) | ||
f77_flags : list | ||
List of flags to pass to the fortran compiler for Fortran 77 files (--f77flags) | ||
f90_flags : list | ||
List of flags to pass to the fortran compiler for Fortran 90 and above files (--f90flags) | ||
include_paths : list | ||
Search include files from given directories (--include-paths) | ||
include_dirs : list | ||
Append directory <dir> to the list of directories searched for include files. (-I<dir>) | ||
external_resources : list | ||
Link the extension module with <resource> (--link-<resource>) | ||
linker_libname : list | ||
Use the library when linking. (-l<libname>) | ||
define_macros : list | ||
Define <macro> to <value> if present else define <macro> to true (-D) | ||
undef_macros : list | ||
Undefine <macro> (-U) | ||
linker_libpath : list | ||
Add directory to the list of directories to be searched for `-l`. (-L) | ||
opt_flags : list | ||
Optimization flags to pass to the compiler. (--opt) | ||
arch_flags : list | ||
Architectire specific flags to pass to the compiler (--arch) | ||
no_opt : bool | ||
Disable optimization. (--no-opt) | ||
no_arch : bool | ||
Disable architecture specific optimizations. (--no-arch) | ||
debug : bool | ||
Enable debugging. (--debug) | ||
|
||
""" | ||
self.module_name = module_name | ||
self.fortran_compiler = fortran_compiler | ||
self.c_compiler = c_compiler | ||
self.f77exec = f77exec | ||
self.f90exec = f90exec | ||
self.f77_flags = f77_flags | ||
self.f90_flags = f90_flags | ||
self.include_paths = include_paths | ||
self.include_dirs = include_dirs | ||
self.external_resources = external_resources | ||
self.linker_libpath = linker_libpath | ||
self.linker_libname = linker_libname | ||
self.define_macros = define_macros | ||
self.undef_macros = undef_macros | ||
self.debug = debug | ||
self.opt_flags = opt_flags | ||
self.arch_flags = arch_flags | ||
self.no_opt = no_opt | ||
self.no_arch = no_arch | ||
|
||
def numpy_install_path(self) -> Path: | ||
""" | ||
Returns the install path for numpy. | ||
""" | ||
return Path(numpy.__file__).parent | ||
|
||
def numpy_get_include(self) -> Path: | ||
""" | ||
Returns the include paths for numpy. | ||
""" | ||
return Path(numpy.get_include()) | ||
|
||
def f2py_get_include(self) -> Path: | ||
""" | ||
Returns the include paths for f2py. | ||
""" | ||
return Path(f2py.get_include()) | ||
|
||
@abstractmethod | ||
def compile(self, fortran_sources: Path, c_wrapper: Path, build_dir: Path) -> None: | ||
"""Compile the wrapper.""" | ||
pass |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
from __future__ import annotations | ||
|
||
import os | ||
import errno | ||
import shutil | ||
import subprocess | ||
from pathlib import Path | ||
|
||
from .backend import Backend | ||
from string import Template | ||
|
||
class MesonTemplate: | ||
"""Template meson build file generation class.""" | ||
def __init__(self, module_name: str, numpy_install_path, numpy_get_include: Path, f2py_get_include: Path, wrappers: list[Path], fortran_sources: list[Path], dependencies: list[str], include_path: list[Path], optimization_flags: list[str], architecture_flags: list[str], f77_flags: list[str], f90_flags: list[str], linker_libpath: list[Path], linker_libname: list[str], define_macros: list[tuple[str, str]], undef_macros: list[str]): | ||
self.module_name = module_name | ||
self.numpy_install_path = numpy_install_path | ||
self.build_template_path = numpy_install_path / "f2py" / "backends" / "src" / "meson.build.src" | ||
self.sources = fortran_sources | ||
self.numpy_get_include = numpy_get_include | ||
self.f2py_get_include = f2py_get_include | ||
self.wrappers = wrappers | ||
self.dependencies = dependencies | ||
self.include_directories = include_path | ||
self.substitutions = {} | ||
self.optimization_flags = optimization_flags | ||
self.architecture_flags = architecture_flags | ||
self.fortran_flags = f77_flags + f90_flags | ||
self.linker_libpath = linker_libpath | ||
self.linker_libname = linker_libname | ||
self.define_macros = define_macros | ||
self.undef_macros = undef_macros | ||
self.pipeline = [self.initialize_template, | ||
self.global_flags_substitution, | ||
self.sources_substitution, | ||
self.dependencies_substitution, | ||
self.include_directories_subtitution, | ||
self.linker_substitution, | ||
self.macros_substitution] | ||
|
||
@property | ||
def meson_build_template(self) -> str: | ||
if(not self.build_template_path.is_file()): | ||
raise FileNotFoundError(errno.ENOENT, f"Meson build template {self.build_template_path.absolute()} does not exist.") | ||
return self.build_template_path.read_text() | ||
|
||
def initialize_template(self) -> None: | ||
"""Initialize with module name and external NumPy and F2PY C libraries.""" | ||
self.substitutions['modulename'] = self.module_name | ||
self.substitutions['numpy_get_include'] = self.numpy_get_include.absolute() | ||
self.substitutions['f2py_get_include'] = self.f2py_get_include.absolute() | ||
Comment on lines
+49
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are these necessary? The documentation includes the option to get these dynamically from |
||
|
||
def sources_substitution(self) -> None: | ||
self.substitutions["source_list"] = ",".join(["\'"+str(source.absolute())+"\'" for source in self.sources]) | ||
self.substitutions["wrappers"] = ",".join(["\'"+str(wrapper.absolute())+"\'" for wrapper in self.wrappers]) | ||
|
||
def dependencies_substitution(self) -> None: | ||
self.substitutions["dependencies_list"] = ", ".join([f"dependency('{dependecy}')" for dependecy in self.dependencies]) | ||
|
||
def include_directories_subtitution(self) -> None: | ||
self.substitutions["include_directories_list"] = ", ".join([f"include_directories('{include_directory}')" for include_directory in self.include_directories]) | ||
|
||
def global_flags_substitution(self) -> None: | ||
fortran_compiler_flags = self.fortran_flags + self.optimization_flags + self.architecture_flags | ||
c_compiler_flags = self.optimization_flags + self.architecture_flags | ||
self.substitutions["fortran_global_args"] = fortran_compiler_flags | ||
self.substitutions["c_global_args"] = c_compiler_flags | ||
Comment on lines
+62
to
+66
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Global flags are generally a bad idea, especially since a lot of these might be used as subprojects, it makes more sense to populate the |
||
|
||
def macros_substitution(self) -> None: | ||
self.substitutions["macros"] = "" | ||
if self.define_macros: | ||
self.substitutions["macros"] = ",".join(f"\'-D{macro[0]}={macro[1]}\'" if macro[1] else f"-D{macro[0]}" for macro in self.define_macros) | ||
if self.undef_macros: | ||
self.substitutions["macros"] += "," + ",".join(f"\'-U{macro}\'" for macro in self.undef_macros) | ||
Comment on lines
+68
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These don't pass Similar issue with unsetting the defines. |
||
|
||
def linker_substitution(self) -> None: | ||
self.substitutions["linker_args"] = "" | ||
if self.linker_libpath: | ||
linker_libpath_subs = ",".join(f"-L{libpath}" for libpath in self.linker_libpath) | ||
self.substitutions["linker_args"] += linker_libpath_subs | ||
if self.linker_libname: | ||
linker_libname_subs = ",".join(f"-l{libname}" for libname in self.linker_libname) | ||
self.substitutions["linker_args"] += f",{linker_libname_subs}" | ||
Comment on lines
+75
to
+82
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same issue as above. |
||
|
||
def generate_meson_build(self) -> str: | ||
for node in self.pipeline: | ||
node() | ||
template = Template(self.meson_build_template) | ||
return template.substitute(self.substitutions) | ||
|
||
|
||
class MesonBackend(Backend): | ||
|
||
def __init__(self, module_name: str = 'untitled', fortran_compiler: str = None, c_compiler: str = None, f77exec: Path = None, f90exec: Path = None, f77_flags: list[str] = None, f90_flags: list[str] = None, include_paths: list[Path] = None, include_dirs: list[Path] = None, external_resources: list[str] = None, linker_libpath: list[Path] = None, linker_libname: list[str] = None, define_macros: list[tuple[str, str]] = None, undef_macros: list[str] = None, debug: bool = False, opt_flags: list[str] = None, arch_flags: list[str] = None, no_opt: bool = False, no_arch: bool = False) -> None: | ||
self.meson_build_dir = "builddir" | ||
if f77_flags is None: | ||
f77_flags = [] | ||
if include_paths is None: | ||
include_paths = [] | ||
if include_dirs is None: | ||
include_dirs = [] | ||
if external_resources is None: | ||
external_resources = [] | ||
if linker_libpath is None: | ||
linker_libpath = [] | ||
if linker_libname is None: | ||
linker_libname = [] | ||
if define_macros is None: | ||
define_macros = [] | ||
if undef_macros is None: | ||
undef_macros = [] | ||
if f77_flags is None: | ||
f77_flags = [] | ||
if f90_flags is None: | ||
f90_flags = [] | ||
if opt_flags is None: | ||
opt_flags = [] | ||
if arch_flags is None: | ||
arch_flags = [] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These flags don't quite fit anymore (the difference between F77 and F90 is not needed, nor are the exact paths to the compiler; the |
||
super().__init__(module_name, fortran_compiler, c_compiler, f77exec, f90exec, f77_flags, f90_flags, include_paths, include_dirs, external_resources, linker_libpath, linker_libname, define_macros, undef_macros, debug, opt_flags, arch_flags, no_opt, no_arch) | ||
|
||
self.wrappers: list[Path] = [] | ||
self.fortran_sources: list[Path] = [] | ||
self.template = Template(self.fortran_sources) | ||
|
||
def _get_optimization_level(self): | ||
if self.no_arch and not self.no_opt : | ||
return 2 | ||
elif self.no_opt : | ||
return 0 | ||
return 3 | ||
|
||
def _set_environment_variables(self) -> None: | ||
if self.fortran_compiler: | ||
os.putenv("FC", self.fortran_compiler) | ||
elif self.f77exec: | ||
os.putenv("FC", self.f77exec) | ||
elif self.f90exec: | ||
os.putenv("FC", self.f90exec) | ||
if self.c_compiler: | ||
os.putenv("CC", self.c_compiler) | ||
|
||
def _move_exec_to_root(self, build_dir: Path): | ||
walk_dir = build_dir / self.meson_build_dir | ||
path_objects = walk_dir.glob(f"{self.module_name}*.so") | ||
for path_object in path_objects: | ||
shutil.move(path_object, Path.cwd()) | ||
|
||
def _get_build_command(self): | ||
return ["meson", "setup", self.meson_build_dir, "-Ddebug=true" if self.debug else "-Ddebug=false", f"-Doptimization={str(self._get_optimization_level())}"] | ||
|
||
def load_wrapper(self, wrappers: list[Path]) -> None: | ||
self.wrappers = wrappers | ||
|
||
def load_sources(self, fortran_sources: list[Path]) -> None: | ||
for fortran_source in fortran_sources: | ||
fortran_source = Path(fortran_source) | ||
if not fortran_source.is_file(): | ||
raise FileNotFoundError(errno.ENOENT, f"{fortran_source.absolute()} does not exist.") | ||
self.fortran_sources.append(fortran_source) | ||
|
||
def write_meson_build(self, build_dir: Path) -> None: | ||
"""Writes the meson build file at specified location""" | ||
meson_template = MesonTemplate(self.module_name, super().numpy_install_path(), self.numpy_get_include(), self.f2py_get_include(), self.wrappers, self.fortran_sources, self.external_resources, self.include_paths+self.include_dirs, optimization_flags=self.opt_flags, architecture_flags=self.arch_flags, f77_flags=self.f77_flags, f90_flags=self.f90_flags, linker_libpath=self.linker_libpath, linker_libname=self.linker_libname, define_macros=self.define_macros, undef_macros=self.undef_macros) | ||
src = meson_template.generate_meson_build() | ||
meson_build_file = build_dir / "meson.build" | ||
meson_build_file.write_text(src) | ||
return meson_build_file | ||
|
||
def run_meson(self, build_dir: Path): | ||
self._set_environment_variables() | ||
completed_process = subprocess.run(self._get_build_command(), cwd=build_dir) | ||
if(completed_process.returncode != 0): | ||
raise subprocess.CalledProcessError(completed_process.returncode, completed_process.args) | ||
completed_process = subprocess.run(["meson", "compile", "-C", self.meson_build_dir], cwd=build_dir) | ||
if(completed_process.returncode != 0): | ||
raise subprocess.CalledProcessError(completed_process.returncode, completed_process.args) | ||
|
||
def compile(self, f77_sources: list[Path], f90_sources:list[Path], object_files: list[Path], wrappers: list[Path], build_dir: Path) -> None: | ||
self.load_wrapper(wrappers) | ||
self.load_sources(f77_sources + f90_sources + object_files) | ||
self.write_meson_build(build_dir) | ||
self.run_meson(build_dir) | ||
self._move_exec_to_root(build_dir) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
project('${modulename}', 'c', | ||
version : '0.1', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you want to set the minimum There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Corrected it. |
||
default_options : ['warning_level=2']) | ||
|
||
add_languages('fortran') | ||
|
||
add_global_arguments(${c_global_args}, language: 'c') | ||
add_global_arguments(${fortran_global_args}, language: 'fortran') | ||
|
||
py_mod = import('python') | ||
py3 = py_mod.find_installation('python3') | ||
py3_dep = py3.dependency() | ||
message(py3.path()) | ||
message(py3.get_install_dir()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should have |
||
|
||
incdir_numpy = '${numpy_get_include}' | ||
|
||
incdir_f2py = '${f2py_get_include}' | ||
|
||
inc_np = include_directories(incdir_numpy, incdir_f2py) | ||
|
||
py3.extension_module('${modulename}', ${wrappers}, ${source_list}, incdir_f2py+'/fortranobject.c', include_directories: [inc_np, ${include_directories_list}], dependencies : [py3_dep, ${dependencies_list}], link_args : '${linker_args}', c_args : [${macros}], install : true) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's tabs here instead of spaces. @NamamiShanker can you please check your editor settings and ensure tabs are always converted to spaces?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure @rgommers. I corrected this file and the others.