8000 ENH: ``meson`` backend for ``f2py`` (#24532) · numpy/numpy@fedc834 · GitHub
[go: up one dir, main page]

Skip to content

Commit fedc834

Browse files
HaoZekeNamamiShankerrgommers
authored
ENH: meson backend for f2py (#24532)
* FIX: Import f2py2e rather than f2py for run_main * FIX: Import f2py2e instead of f2py * ENH: Add F2PY back-end work from gh-22225 Co-authored-by: NamamiShanker <NamamiShanker@users.noreply.github.com> * ENH: Add meson skeleton from gh-2225 Co-authored-by: NamamiShanker <NamamiShanker@users.noreply.github.com> * MAINT: Trim backend.py down to f2py2e flags * ENH: Add a factory function for backends * ENH: Add a distutils backend * ENH: Handle --backends in f2py Defaults to distutils for now * DOC: Add some minor comments in f2py2e * MAINT: Refactor and rework meson.build.src * MAINT: Add objects * MAINT: Cleanup distutils backend * MAINT: Refactor to add everything back to backend Necessary for the meson.build for now. Refactors / cleanup needs better argument handling in f2py2e * MAINT: Fix overly long line * BUG: Construct wrappers for meson backend * MAINT: Rework, simplify template massively * ENH: Truncate meson.build to skeleton only * MAINT: Minor backend housekeeping, name changes * MAINT: Less absolute paths, update setup.py [f2py] * MAINT: Move f2py module name functionality Previously part of np.distutils * ENH: Handle .pyf files * TST: Fix typo in isoFortranEnvMap.f90 * MAINT: Typo in f2py2e support for pyf files * DOC: Add release note for --backend * MAINT: Conditional switch for Python 3.12 [f2py] * MAINT: No absolute paths in backend [f2py-meson] The files are copied over anyway, this makes it easier to extend the generated skeleton * MAINT: Prettier generated meson.build files [f2py] * ENH: Add meson's dependency(blah) to f2py * DOC: Document the new flag * MAINT: Simplify and rename backend template [f2py] Co-authored-by: rgommers <rgommers@users.noreply.github.com> * ENH: Support build_type via --debug [f2py-meson] * MAINT,DOC: Reduce warn,rework doc [f2py-meson] Co-authored-by: rgommers <rgommers@users.noreply.github.com> * ENH: Rework deps: to --dep calls [f2py-meson] Also shows how incremental updates to the parser can be done. * MAINT,DOC: Add --backend to argparse, add docs * MAINT: Rename meson template [f2py-meson] * MAINT: Add meson.build for f2py Should address #22225 (comment) * BLD: remove duplicate f2py handling in meson.build files --------- Co-authored-by: Namami Shanker <namami2011@gmail.com> Co-authored-by: NamamiShanker <NamamiShanker@users.noreply.github.com> Co-authored-by: rgommers <rgommers@users.noreply.github.com> Co-authored-by: Ralf Gommers <ralf.gommers@gmail.com>
1 parent dc2ff12 commit fedc834

File tree

11 files changed

+451
-59
lines changed

11 files changed

+451
-59
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
``meson`` backend for ``f2py``
2+
------------------------------
3+
``f2py`` in compile mode (i.e. ``f2py -c``) now accepts the ``--backend meson`` option. This is the default option
4+
for Python ``3.12`` on-wards. Older versions will still default to ``--backend
5+
distutils``.
6+
7+
To support this in realistic use-cases, in compile mode ``f2py`` takes a
8+
``--dep`` flag one or many times which maps to ``dependency()`` calls in the
9+
``meson`` backend, and does nothing in the ``distutils`` backend.
10+
11+
12+
There are no changes for users of ``f2py`` only as a code generator, i.e. without ``-c``.

numpy/distutils/command/build_src.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -539,8 +539,8 @@ def f2py_sources(self, sources, extension):
539539
if (self.force or newer_group(depends, target_file, 'newer')) \
540540
and not skip_f2py:
541541
log.info("f2py: %s" % (source))
542-
import numpy.f2py
543-
numpy.f2py.run_main(f2py_options
542+
from numpy.f2py import f2py2e
543+
f2py2e.run_main(f2py_options
544544
+ ['--build-dir', target_dir, source])
545545
else:
546546
log.debug(" skipping '%s' f2py interface (up-to-date)" % (source))
@@ -558,8 +558,8 @@ def f2py_sources(self, sources, extension):
558558
and not skip_f2py:
559559
log.info("f2py:> %s" % (target_file))
560560
self.mkpath(target_dir)
561-
import numpy.f2py
562-
numpy.f2py.run_main(f2py_options + ['--lower',
561+
from numpy.f2py import f2py2e
562+
f2py2e.run_main(f2py_options + ['--lower',
563563
'--build-dir', target_dir]+\
564564
['-m', ext_name]+f_sources)
565565
else:

numpy/f2py/_backends/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
def f2py_build_generator(name):
2+
if name == "meson":
3+
from ._meson import MesonBackend
4+
return MesonBackend
5+
elif name == "distutils":
6+
from ._distutils import DistutilsBackend
7+
return DistutilsBackend
8+
else:
9+
raise ValueError(f"Unknown backend: {name}")

numpy/f2py/_backends/_backend.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from __future__ import annotations
2+
3+
from abc import ABC, abstractmethod
4+
5+
6+
class Backend(ABC):
7+
def __init__(
8+
self,
9+
modulename,
10+
sources,
11+
extra_objects,
12+
build_dir,
13+
include_dirs,
14+
library_dirs,
15+
libraries,
16+
define_macros,
17+
undef_macros,
18+
f2py_flags,
19+
sysinfo_flags,
20+
fc_flags,
21+
flib_flags,
22+
setup_flags,
23+
remove_build_dir,
24+
extra_dat,
25+
):
26+
self.modulename = modulename
27+
self.sources = sources
28+
self.extra_objects = extra_objects
29+
self.build_dir = build_dir
30+
self.include_dirs = include_dirs
31+
self.library_dirs = library_dirs
32+
self.libraries = libraries
33+
self.define_macros = define_macros
34+
self.undef_macros = undef_macros
35+
self.f2py_flags = f2py_flags
36+
self.sysinfo_flags = sysinfo_flags
37+
self.fc_flags = fc_flags
38+
self.flib_flags = flib_flags
39+
self.setup_flags = setup_flags
40+
self.remove_build_dir = remove_build_dir
41+
self.extra_dat = extra_dat
42+
43+
@abstractmethod
44+
def compile(self) -> None:
45+
"""Compile the wrapper."""
46+
pass

numpy/f2py/_backends/_distutils.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from ._backend import Backend
2+
3+
from numpy.distutils.core import setup, Extension
4+
from numpy.distutils.system_info import get_info
5+
from numpy.distutils.misc_util import dict_append
6+
from numpy.exceptions import VisibleDeprecationWarning
7+
import os
8+
import sys
9+
import shutil
10+
import warnings
11+
12+
13+
class DistutilsBackend(Backend):
14+
def __init__(sef, *args, **kwargs):
15+
warnings.warn(
16+
"distutils has been deprecated since NumPy 1.26."
17+
"Use the Meson backend instead, or generate wrappers"
18+
"without -c and use a custom build script",
19+
VisibleDeprecationWarning,
20+
stacklevel=2,
21+
)
22+
super().__init__(*args, **kwargs)
23+
24+
def compile(self):
25+
num_info = {}
26+
if num_info:
27+
self.include_dirs.extend(num_info.get("include_dirs", []))
28+
ext_args = {
29+
"name": self.modulename,
30+
"sources": self.sources,
31+
"include_dirs": self.include_dirs,
32+
"library_dirs": self.library_dirs,
33+
"libraries": self.libraries,
34+
"define_macros": self.define_macros,
35+
"undef_macros": self.undef_macros,
36+
"extra_objects": self.extra_objects,
37+
"f2py_options": self.f2py_flags,
38+
}
39+
40+
if self.sysinfo_flags:
41+
for n in self.sysinfo_flags:
42+
i = get_info(n)
43+
if not i:
44+
print(
45+
f"No {repr(n)} resources found"
46+
"in system (try `f2py --help-link`)"
47+
)
48+
dict_append(ext_args, **i)
49+
50+
ext = Extension(**ext_args)
51+
52+
sys.argv = [sys.argv[0]] + self.setup_flags
53+
sys.argv.extend(
54+
[
55+
"build",
56+
"--build-temp",
57+
self.build_dir,
58+
"--build-base",
59+
self.build_dir,
60+
"--build-platlib",
61+
".",
62+
"--disable-optimization",
63+
]
64+
)
65+
66+
if self.fc_flags:
67+
sys.argv.extend(["config_fc"] + self.fc_flags)
68+
if self.flib_flags:
69+
sys.argv.extend(["build_ext"] + self.flib_flags)
70+
71+
setup(ext_modules=[ext])
72+
73+
if self.remove_build_dir and os.path.exists(self.build_dir):
74+
print(f"Removing build directory {self.build_dir}")
75+
shutil.rmtree(self.build_dir)

numpy/f2py/_backends/_meson.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
from __future__ import annotations
2+
3+
import errno
4+
import shutil
5+
import subprocess
6+
from pathlib import Path
7+
8+
from ._backend import Backend
9+
from string import Template
10+
11+
import warnings
12+
13+
14+
class MesonTemplate:
15+
"""Template meson build file generation class."""
16+
17+
def __init__(
18+
self,
19+
modulename: str,
20+
sources: list[Path],
21+
deps: list[str],
22+
object_files: list[Path],
23+
linker_args: list[str],
24+
c_args: list[str],
25+
build_type: 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.substitutions = {}
34+
self.objects = object_files
35+
self.pipeline = [
36+
self.initialize_template,
37+
self.sources_substitution,
38+
self.deps_substitution,
39+
]
40+
self.build_type = build_type
41+
42+
def meson_build_template(self) -> str:
43+
if not self.build_template_path.is_file():
44+
raise FileNotFoundError(
45+
errno.ENOENT,
46+
"Meson build template"
47+
f" {self.build_template_path.absolute()}"
48+
" does not exist.",
49+
)
50+
return self.build_template_path.read_text()
51+
52+
def initialize_template(self) -> None:
53+
self.substitutions["modulename"] = self.modulename
54+
self.substitutions["buildtype"] = self.build_type
55+
56+
def sources_substitution(self) -> None:
57+
indent = " " * 21
58+
self.substitutions["source_list"] = f",\n{indent}".join(
59+
[f"'{source}'" for source in self.sources]
60+
)
61+
62+
def deps_substitution(self) -> None:
63+
indent = " " * 21
64+
self.substitutions["dep_list"] = f",\n{indent}".join(
65+
[f"dependency('{dep}')" for dep in self.deps]
66+
)
67+
68+
def generate_meson_build(self):
69+
for node in self.pipeline:
70+
node()
71+
template = Template(self.meson_build_template())
72+
return template.substitute(self.substitutions)
73+
74+
75+
class MesonBackend(Backend):
76+
def __init__(self, *args, **kwargs):
77+
super().__init__(*args, **kwargs)
78+
self.dependencies = self.extra_dat.get("dependencies", [])
79+
self.meson_build_dir = "bbdir"
80+
self.build_type = (
81+
"debug" if any("debug" in flag for flag in self.fc_flags) else "release"
82+
)
83+
84+
def _move_exec_to_root(self, build_dir: Path):
85+
walk_dir = Path(build_dir) / self.meson_build_dir
86+
path_objects = walk_dir.glob(f"{self.modulename}*.so")
87+
for path_object in path_objects:
88+
shutil.move(path_object, Path.cwd())
89+
90+
def _get_build_command(self):
91+
return [
92+
"meson",
93+
"setup",
94+
self.meson_build_dir,
95+
]
96+
97+
def write_meson_build(self, build_dir: Path) -> None:
98+
"""Writes the meson build file at specified location"""
99+
meson_template = MesonTemplate(
100+
self.modulename,
101+
self.sources,
102+
self.dependencies,
103+
self.extra_objects,
104+
self.flib_flags,
105+
self.fc_flags,
106+
self.build_type,
107+
)
108+
src = meson_template.generate_meson_build()
109+
Path(build_dir).mkdir(parents=True, exist_ok=True)
110+
meson_build_file = Path(build_dir) / "meson.build"
111+
meson_build_file.write_text(src)
112+
return meson_build_file
113+
114+
def run_meson(self, build_dir: Path):
115+
completed_process = subprocess.run(self._get_build_command(), cwd=build_dir)
116+
if completed_process.returncode != 0:
117+
raise subprocess.CalledProcessError(
118+
completed_process.returncode, completed_process.args
119+
)
120+
completed_process = subprocess.run(
121+
["meson", "compile", "-C", self.meson_build_dir], cwd=build_dir
122+
)
123+
if completed_process.returncode != 0:
124+
raise subprocess.CalledProcessError(
125+
completed_process.returncode, completed_process.args
126+
)
127+
128+
def compile(self) -> None:
129+
self.sources = _prepare_sources(self.modulename, self.sources, self.build_dir)
130+
self.write_meson_build(self.build_dir)
131+
self.run_meson(self.build_dir)
132+
self._move_exec_to_root(self.build_dir)
133+
134+
135+
def _prepare_sources(mname, sources, bdir):
136+
extended_sources = sources.copy()
137+
Path(bdir).mkdir(parents=True, exist_ok=True)
138+
# Copy sources
139+
for source in sources:
140+
shutil.copy(source, bdir)
141+
generated_sources = [
142+
Path(f"{mname}module.c"),
143+
Path(f"{mname}-f2pywrappers2.f90"),
144+
Path(f"{mname}-f2pywrappers.f"),
145+
]
146+
bdir = Path(bdir)
147+
for generated_source in generated_sources:
148+
if generated_source.exists():
149+
shutil.copy(generated_source, bdir / generated_source.name)
150+
extended_sources.append(generated_source.name)
151+
generated_source.unlink()
152+
extended_sources = [
153+
Path(source).name
154+
for source in extended_sources
155+
if not Path(source).suffix == ".pyf"
156+
]
157+
return extended_sources
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
project('${modulename}',
2+
['c', 'fortran'],
3+
version : '0.1',
4+
meson_version: '>= 1.1.0',
5+
default_options : [
6+
'warning_level=1',
7+
'buildtype=${buildtype}'
8+
])
9+
10+
py = import('python').find_installation(pure: false)
11+
py_dep = py.dependency()
12+
13+
incdir_numpy = run_command(py,
14+
['-c', 'import os; os.chdir(".."); import numpy; print(numpy.get_include())'],
15+
check : true
16+
).stdout().strip()
17+
18+
incdir_f2py = run_command(py,
19+
['-c', 'import os; os.chdir(".."); import numpy.f2py; print(numpy.f2py.get_include())'],
20+
check : true
21+
).stdout().strip()
22+
23+
inc_np = include_directories(incdir_numpy)
24+
np_dep = declare_dependency(include_directories: inc_np)
25+
26+
incdir_f2py = incdir_numpy / '..' / '..' / 'f2py' / 'src'
27+
inc_f2py = include_directories(incdir_f2py)
28+
fortranobject_c = incdir_f2py / 'fortranobject.c'
29+
30+
inc_np = include_directories(incdir_numpy, incdir_f2py)
31+
32+
py.extension_module('${modulename}',
33+
[
34+
${source_list},
35+
fortranobject_c
36+
],
37+
include_directories: [inc_np],
38+
dependencies : [
39+
py_dep,
40+
${dep_list}
41+
],
42+
install : true)

0 commit comments

Comments
 (0)
0