fix(plugin): bundle Python dependencies for standalone deployment
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user