216 lines
6.9 KiB
Python
216 lines
6.9 KiB
Python
# Integrated from the cffi-buildtool project by Rose Davidson
|
|
# (https://github.com/inklesspen/cffi-buildtool), under the following
|
|
# license:
|
|
#
|
|
# MIT License
|
|
#
|
|
# Copyright (c) 2024, Rose Davidson
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a
|
|
# copy of this software and associated documentation files (the
|
|
# "Software"), to deal in the Software without restriction, including
|
|
# without limitation the rights to use, copy, modify, merge, publish,
|
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
# permit persons to whom the Software is furnished to do so, subject to
|
|
# the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice (including the
|
|
# next paragraph) shall be included in all copies or substantial portions
|
|
# of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
"""Implementation of the ``cffi-gen-src`` command-line tool.
|
|
|
|
This module is private; the command line is the only supported
|
|
interface. Two subcommands:
|
|
|
|
``exec-python``
|
|
Execute a Python script that constructs a :class:`cffi.FFI`
|
|
(the same kind of script that the CFFI docs' "Main mode of usage"
|
|
describes) and emit the generated C source.
|
|
|
|
``read-sources``
|
|
Create the :class:`cffi.FFI` from a separate ``cdef`` file and C
|
|
source prelude, then emit the generated C source.
|
|
"""
|
|
|
|
import argparse
|
|
import io
|
|
import os
|
|
import sys
|
|
|
|
from .api import FFI
|
|
|
|
|
|
def _execfile(pysrc, filename, globs):
|
|
compiled = compile(source=pysrc, filename=filename, mode='exec')
|
|
exec(compiled, globs, globs)
|
|
|
|
|
|
def find_ffi_in_python_script(pysrc, filename, ffivar):
|
|
"""Execute ``pysrc`` and return the :class:`FFI` object it defines.
|
|
|
|
The script is executed with ``__name__`` set to ``"cffi.gen_src"``,
|
|
so a trailing ``if __name__ == "__main__": ffibuilder.compile()``
|
|
block in the script is skipped.
|
|
|
|
``ffivar`` is the name bound by the script to the :class:`FFI`
|
|
object, or to a callable that returns one.
|
|
|
|
Raises :class:`NameError` if the name is not bound by the script,
|
|
or :class:`TypeError` if the name does not resolve to an
|
|
:class:`FFI` instance.
|
|
"""
|
|
filename = os.path.abspath(filename)
|
|
globs = {'__name__': 'cffi.gen_src', '__file__': filename}
|
|
old_path = sys.path[:]
|
|
sys.path.insert(0, os.path.dirname(filename))
|
|
try:
|
|
_execfile(pysrc, filename, globs)
|
|
if ffivar not in globs:
|
|
raise NameError(
|
|
"Expected to find the FFI object with the name %r, "
|
|
"but it was not found." % (ffivar,)
|
|
)
|
|
ffi = globs[ffivar]
|
|
if not isinstance(ffi, FFI) and callable(ffi):
|
|
# Maybe it's a callable that returns a FFI
|
|
ffi = ffi()
|
|
if not isinstance(ffi, FFI):
|
|
raise TypeError(
|
|
"Found an object with the name %r but it was not an "
|
|
"instance of cffi.api.FFI" % (ffivar,)
|
|
)
|
|
return ffi
|
|
finally:
|
|
sys.path[:] = old_path
|
|
|
|
|
|
def make_ffi_from_sources(modulename, cdef, csrc):
|
|
"""Create an :class:`FFI` from ``cdef`` text and a C source prelude."""
|
|
ffibuilder = FFI()
|
|
ffibuilder.cdef(cdef)
|
|
ffibuilder.set_source(modulename, csrc)
|
|
return ffibuilder
|
|
|
|
|
|
def generate_c_source(ffi):
|
|
"""Return the C source that :meth:`FFI.emit_c_code` would write."""
|
|
output = io.StringIO()
|
|
ffi.emit_c_code(output)
|
|
return output.getvalue()
|
|
|
|
|
|
def write_c_source(output, generated):
|
|
if output == '-':
|
|
sys.stdout.write(generated)
|
|
return
|
|
with open(output, 'w', encoding='utf-8') as f:
|
|
f.write(generated)
|
|
|
|
|
|
def same_input_file(file1, file2):
|
|
if file1 is file2:
|
|
return True
|
|
try:
|
|
return os.path.samefile(file1.name, file2.name)
|
|
except OSError:
|
|
return False
|
|
|
|
|
|
def exec_python(*, output, pyfile, ffi_var):
|
|
with pyfile:
|
|
ffi = find_ffi_in_python_script(pyfile.read(), pyfile.name, ffi_var)
|
|
generated = generate_c_source(ffi)
|
|
write_c_source(output, generated)
|
|
|
|
|
|
def read_sources(*, output, module_name, cdef_input, csrc_input):
|
|
with csrc_input, cdef_input:
|
|
csrc = csrc_input.read()
|
|
cdef = cdef_input.read()
|
|
ffi = make_ffi_from_sources(module_name, cdef, csrc)
|
|
generated = generate_c_source(ffi)
|
|
write_c_source(output, generated)
|
|
|
|
|
|
def _prog():
|
|
# The same parser serves both documented invocations; make --help
|
|
# and usage messages show the one that was actually used.
|
|
argv0 = os.path.basename(sys.argv[0]) if sys.argv else ''
|
|
if argv0.startswith('cffi-gen-src'):
|
|
return 'cffi-gen-src'
|
|
return 'python -m cffi.gen_src'
|
|
|
|
|
|
parser = argparse.ArgumentParser(
|
|
prog=_prog(),
|
|
description='Generate CFFI C source for extension modules.',
|
|
)
|
|
subparsers = parser.add_subparsers(dest='mode')
|
|
|
|
exec_python_parser = subparsers.add_parser(
|
|
'exec-python',
|
|
help='Execute a Python script that defines an FFI object',
|
|
)
|
|
exec_python_parser.add_argument(
|
|
'--ffi-var',
|
|
default='ffibuilder',
|
|
help="Name of the FFI object in the Python script; defaults to 'ffibuilder'.",
|
|
)
|
|
exec_python_parser.add_argument(
|
|
'pyfile',
|
|
type=argparse.FileType('r', encoding='utf-8'),
|
|
help='Path to the Python script',
|
|
)
|
|
exec_python_parser.add_argument(
|
|
'output',
|
|
help='Output path for the C source',
|
|
)
|
|
|
|
read_sources_parser = subparsers.add_parser(
|
|
'read-sources',
|
|
help='Read cdef and C source prelude files that define an FFI object',
|
|
)
|
|
read_sources_parser.add_argument(
|
|
'module_name',
|
|
help='Full name of the generated module, including packages',
|
|
)
|
|
read_sources_parser.add_argument(
|
|
'cdef',
|
|
type=argparse.FileType('r', encoding='utf-8'),
|
|
help='File containing C definitions',
|
|
)
|
|
read_sources_parser.add_argument(
|
|
'csrc',
|
|
type=argparse.FileType('r', encoding='utf-8'),
|
|
help='File containing C source prelude',
|
|
)
|
|
read_sources_parser.add_argument(
|
|
'output',
|
|
help='Output path for the C source',
|
|
)
|
|
|
|
|
|
def run(args=None):
|
|
args = parser.parse_args(args=args)
|
|
if args.mode == 'exec-python':
|
|
exec_python(output=args.output, pyfile=args.pyfile, ffi_var=args.ffi_var)
|
|
elif args.mode == 'read-sources':
|
|
if same_input_file(args.cdef, args.csrc):
|
|
parser.error('cdef and csrc are the same file and should not be')
|
|
read_sources(
|
|
output=args.output,
|
|
module_name=args.module_name,
|
|
cdef_input=args.cdef,
|
|
csrc_input=args.csrc,
|
|
)
|
|
else:
|
|
parser.error('a subcommand is required: exec-python or read-sources')
|
|
parser.exit(0)
|