Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
extbuild.py251 linesDownload Raw Back to _private
1"""
2Build a c-extension module on-the-fly in tests.
3See build_and_import_extensions for usage hints
4
5"""
6
7import os
8import pathlib
9import subprocess
10import sys
11import sysconfig
12import textwrap
13
14__all__ = ['build_and_import_extension', 'compile_extension_module']
15
16
17def build_and_import_extension(
18        modname, functions, *, prologue="", build_dir=None,
19        include_dirs=None, more_init=""):
20    """
21    Build and imports a c-extension module `modname` from a list of function
22    fragments `functions`.
23
24
25    Parameters
26    ----------
27    functions : list of fragments
28        Each fragment is a sequence of func_name, calling convention, snippet.
29    prologue : string
30        Code to precede the rest, usually extra ``#include`` or ``#define``
31        macros.
32    build_dir : pathlib.Path
33        Where to build the module, usually a temporary directory
34    include_dirs : list
35        Extra directories to find include files when compiling
36    more_init : string
37        Code to appear in the module PyMODINIT_FUNC
38
39    Returns
40    -------
41    out: module
42        The module will have been loaded and is ready for use
43
44    Examples
45    --------
46    >>> functions = [("test_bytes", "METH_O", \"\"\"
47        if ( !PyBytesCheck(args)) {
48            Py_RETURN_FALSE;
49        }
50        Py_RETURN_TRUE;
51    \"\"\")]
52    >>> mod = build_and_import_extension("testme", functions)
53    >>> assert not mod.test_bytes('abc')
54    >>> assert mod.test_bytes(b'abc')
55    """
56    if include_dirs is None:
57        include_dirs = []
58    body = prologue + _make_methods(functions, modname)
59    init = """
60    PyObject *mod = PyModule_Create(&moduledef);
61    #ifdef Py_GIL_DISABLED
62    PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED);
63    #endif
64           """
65    if not build_dir:
66        build_dir = pathlib.Path('.')
67    if more_init:
68        init += """#define INITERROR return NULL
69                """
70        init += more_init
71    init += "\nreturn mod;"
72    source_string = _make_source(modname, init, body)
73    mod_so = compile_extension_module(
74        modname, build_dir, include_dirs, source_string)
75    import importlib.util
76    spec = importlib.util.spec_from_file_location(modname, mod_so)
77    foo = importlib.util.module_from_spec(spec)
78    spec.loader.exec_module(foo)
79    return foo
80
81
82def compile_extension_module(
83        name, builddir, include_dirs,
84        source_string, libraries=None, library_dirs=None):
85    """
86    Build an extension module and return the filename of the resulting
87    native code file.
88
89    Parameters
90    ----------
91    name : string
92        name of the module, possibly including dots if it is a module inside a
93        package.
94    builddir : pathlib.Path
95        Where to build the module, usually a temporary directory
96    include_dirs : list
97        Extra directories to find include files when compiling
98    libraries : list
99        Libraries to link into the extension module
100    library_dirs: list
101        Where to find the libraries, ``-L`` passed to the linker
102    """
103    modname = name.split('.')[-1]
104    dirname = builddir / name
105    dirname.mkdir(exist_ok=True)
106    cfile = _convert_str_to_file(source_string, dirname)
107    include_dirs = include_dirs or []
108    libraries = libraries or []
109    library_dirs = library_dirs or []
110
111    return _c_compile(
112        cfile, outputfilename=dirname / modname,
113        include_dirs=include_dirs, libraries=libraries,
114        library_dirs=library_dirs,
115        )
116
117
118def _convert_str_to_file(source, dirname):
119    """Helper function to create a file ``source.c`` in `dirname` that contains
120    the string in `source`. Returns the file name
121    """
122    filename = dirname / 'source.c'
123    with filename.open('w') as f:
124        f.write(str(source))
125    return filename
126
127
128def _make_methods(functions, modname):
129    """ Turns the name, signature, code in functions into complete functions
130    and lists them in a methods_table. Then turns the methods_table into a
131    ``PyMethodDef`` structure and returns the resulting code fragment ready
132    for compilation
133    """
134    methods_table = []
135    codes = []
136    for funcname, flags, code in functions:
137        cfuncname = f"{modname}_{funcname}"
138        if 'METH_KEYWORDS' in flags:
139            signature = '(PyObject *self, PyObject *args, PyObject *kwargs)'
140        else:
141            signature = '(PyObject *self, PyObject *args)'
142        methods_table.append(
143            "{\"%s\", (PyCFunction)%s, %s}," % (funcname, cfuncname, flags))
144        func_code = f"""
145        static PyObject* {cfuncname}{signature}
146        {{
147        {code}
148        }}
149        """
150        codes.append(func_code)
151
152    body = "\n".join(codes) + """
153    static PyMethodDef methods[] = {
154    %(methods)s
155    { NULL }
156    };
157    static struct PyModuleDef moduledef = {
158        PyModuleDef_HEAD_INIT,
159        "%(modname)s",  /* m_name */
160        NULL,           /* m_doc */
161        -1,             /* m_size */
162        methods,        /* m_methods */
163    };
164    """ % {'methods': '\n'.join(methods_table), 'modname': modname}
165    return body
166
167
168def _make_source(name, init, body):
169    """ Combines the code fragments into source code ready to be compiled
170    """
171    code = """
172    #include <Python.h>
173
174    %(body)s
175
176    PyMODINIT_FUNC
177    PyInit_%(name)s(void) {
178    %(init)s
179    }
180    """ % {
181        'name': name, 'init': init, 'body': body,
182    }
183    return code
184
185
186def _c_compile(cfile, outputfilename, include_dirs, libraries,
187               library_dirs):
188    link_extra = []
189    if sys.platform == 'win32':
190        compile_extra = ["/we4013"]
191        link_extra.append('/DEBUG')  # generate .pdb file
192    elif sys.platform.startswith('linux'):
193        compile_extra = [
194            "-O0", "-g", "-Werror=implicit-function-declaration", "-fPIC"]
195    else:
196        compile_extra = []
197
198    return build(
199        cfile, outputfilename,
200        compile_extra, link_extra,
201        include_dirs, libraries, library_dirs)
202
203
204def build(cfile, outputfilename, compile_extra, link_extra,
205          include_dirs, libraries, library_dirs):
206    "use meson to build"
207
208    build_dir = cfile.parent / "build"
209    os.makedirs(build_dir, exist_ok=True)
210    with open(cfile.parent / "meson.build", "wt") as fid:
211        link_dirs = ['-L' + d for d in library_dirs]
212        fid.write(textwrap.dedent(f"""\
213            project('foo', 'c')
214            py = import('python').find_installation(pure: false)
215            py.extension_module(
216                '{outputfilename.parts[-1]}',
217                '{cfile.parts[-1]}',
218                c_args: {compile_extra},
219                link_args: {link_dirs},
220                include_directories: {include_dirs},
221            )
222        """))
223    native_file_name = cfile.parent / ".mesonpy-native-file.ini"
224    with open(native_file_name, "wt") as fid:
225        fid.write(textwrap.dedent(f"""\
226            [binaries]
227            python = '{sys.executable}'
228        """))
229    if sys.platform == "win32":
230        subprocess.check_call(["meson", "setup",
231                               "--buildtype=release",
232                               "--vsenv", ".."],
233                              cwd=build_dir,
234                              )
235    else:
236        subprocess.check_call(["meson", "setup", "--vsenv",
237                               "..", f'--native-file={os.fspath(native_file_name)}'],
238                              cwd=build_dir
239                              )
240
241    so_name = outputfilename.parts[-1] + get_so_suffix()
242    subprocess.check_call(["meson", "compile"], cwd=build_dir)
243    os.rename(str(build_dir / so_name), cfile.parent / so_name)
244    return cfile.parent / so_name
245
246
247def get_so_suffix():
248    ret = sysconfig.get_config_var('EXT_SUFFIX')
249    assert ret
250    return ret
251 
codekingpro/portable-devtools · Team Ai