From 92345d663e7d47acdd88ab368a55129a5c468a84 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 20 Apr 2026 10:40:51 -0600 Subject: [PATCH 1/8] Integrate cffi-buildtool into CFFI itself This adds code and documentation that was originally written by Rose Davidson (@inklesspen on GitHub) for the cffi-buildtool project: https://github.com/inklesspen/cffi-buildtool --- .github/workflows/ci.yaml | 10 +- MANIFEST.in | 2 +- doc/source/buildtool.rst | 351 ++++++++++++++++++ doc/source/cdef.rst | 115 +----- doc/source/index.rst | 1 + doc/source/whatsnew.rst | 8 + pyproject.toml | 3 + setup.py | 2 +- src/cffi/buildtool/__init__.py | 26 ++ src/cffi/buildtool/__main__.py | 18 + src/cffi/buildtool/_cli.py | 110 ++++++ src/cffi/buildtool/_gen.py | 58 +++ testing/cffi1/buildtool_example/meson.build | 42 +++ .../cffi1/buildtool_example/pyproject.toml | 10 + .../cffi1/buildtool_example/src/csrc/square.c | 5 + .../cffi1/buildtool_example/src/csrc/square.h | 6 + .../buildtool_example/src/squared/__init__.py | 5 + .../src/squared/_squared_build.py | 13 + testing/cffi1/buildtool_example2/meson.build | 44 +++ .../cffi1/buildtool_example2/pyproject.toml | 10 + .../buildtool_example2/src/csrc/square.c | 5 + .../buildtool_example2/src/csrc/square.h | 6 + .../src/squared/__init__.py | 5 + .../src/squared/squared.cdef.txt | 1 + .../src/squared/squared.csrc.c | 1 + testing/cffi1/test_buildtool.py | 131 +++++++ testing/cffi1/test_buildtool_meson.py | 81 ++++ 27 files changed, 964 insertions(+), 105 deletions(-) create mode 100644 doc/source/buildtool.rst create mode 100644 src/cffi/buildtool/__init__.py create mode 100644 src/cffi/buildtool/__main__.py create mode 100644 src/cffi/buildtool/_cli.py create mode 100644 src/cffi/buildtool/_gen.py create mode 100644 testing/cffi1/buildtool_example/meson.build create mode 100644 testing/cffi1/buildtool_example/pyproject.toml create mode 100644 testing/cffi1/buildtool_example/src/csrc/square.c create mode 100644 testing/cffi1/buildtool_example/src/csrc/square.h create mode 100644 testing/cffi1/buildtool_example/src/squared/__init__.py create mode 100644 testing/cffi1/buildtool_example/src/squared/_squared_build.py create mode 100644 testing/cffi1/buildtool_example2/meson.build create mode 100644 testing/cffi1/buildtool_example2/pyproject.toml create mode 100644 testing/cffi1/buildtool_example2/src/csrc/square.c create mode 100644 testing/cffi1/buildtool_example2/src/csrc/square.h create mode 100644 testing/cffi1/buildtool_example2/src/squared/__init__.py create mode 100644 testing/cffi1/buildtool_example2/src/squared/squared.cdef.txt create mode 100644 testing/cffi1/buildtool_example2/src/squared/squared.csrc.c create mode 100644 testing/cffi1/test_buildtool.py create mode 100644 testing/cffi1/test_buildtool_meson.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fd5b36e9e..ea89afafd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -182,7 +182,7 @@ jobs: CIBW_MUSLLINUX_X86_64_IMAGE: ${{ matrix.musllinux_img || 'musllinux_1_2' }} CIBW_MUSLLINUX_I686_IMAGE: ${{ matrix.musllinux_img || 'musllinux_1_2' }} CIBW_MUSLLINUX_AARCH64_IMAGE: ${{ matrix.musllinux_img || 'musllinux_1_2' }} - CIBW_TEST_REQUIRES: pytest setuptools # 3.12+ no longer includes distutils, just always ensure setuptools is present + CIBW_TEST_REQUIRES: pytest setuptools meson-python ninja # 3.12+ no longer includes distutils, just always ensure setuptools is present CIBW_TEST_COMMAND: PYTHONUNBUFFERED=1 python -m pytest ${{ matrix.test_args || '{project}' }} # default to test all run: | set -eux @@ -268,7 +268,7 @@ jobs: id: build env: CIBW_BUILD: ${{ matrix.spec }} - CIBW_TEST_REQUIRES: pytest setuptools + CIBW_TEST_REQUIRES: pytest setuptools meson-python ninja CIBW_TEST_COMMAND: pip install pip --upgrade; cd {project}; PYTHONUNBUFFERED=1 pytest MACOSX_DEPLOYMENT_TARGET: ${{ matrix.deployment_target || '10.13' }} SDKROOT: ${{ matrix.sdkroot || 'macosx' }} @@ -351,7 +351,7 @@ jobs: id: build env: CIBW_BUILD: ${{ matrix.spec }} - CIBW_TEST_REQUIRES: pytest setuptools + CIBW_TEST_REQUIRES: pytest setuptools meson-python ninja CIBW_TEST_COMMAND: ${{ matrix.test_cmd || 'python -m pytest {package}/src/c' }} # FIXME: /testing takes ~45min on Windows and has some failures... # CIBW_TEST_COMMAND='python -m pytest {package}/src/c {package}/testing' @@ -527,7 +527,7 @@ jobs: - name: build and install run: | - python -m pip install pytest setuptools pytest-run-parallel + python -m pip install pytest setuptools meson-python ninja pytest-run-parallel python -m pip install . - name: run tests under pytest-run-parallel @@ -542,7 +542,7 @@ jobs: - name: build and install run: | - python -m pip install setuptools pytest pytest-run-parallel + python -m pip install setuptools meson-python ninja pytest pytest-run-parallel CFLAGS="-g -O3 -fsanitize=thread" python -m pip install -v . - name: run tests under pytest-run-parallel diff --git a/MANIFEST.in b/MANIFEST.in index a7616ffe9..0590e1e59 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,6 @@ recursive-include src/cffi *.py *.h recursive-include src/c *.c *.h *.asm *.py win64.obj ffi.lib -recursive-include testing *.py *.c *.h +recursive-include testing *.py *.c *.h meson.build pyproject.toml *.txt recursive-include doc *.py *.rst Makefile *.bat recursive-include demo py.cleanup *.py embedding_test.c manual.c include AUTHORS LICENSE setup.py setup_base.py diff --git a/doc/source/buildtool.rst b/doc/source/buildtool.rst new file mode 100644 index 000000000..8d084ad64 --- /dev/null +++ b/doc/source/buildtool.rst @@ -0,0 +1,351 @@ +.. _buildtool_docs: + +========================================= +Building and Distributing CFFI Extensions +========================================= + +.. contents:: + +CFFI ships a small subpackage, :mod:`cffi.buildtool`, together with a +command-line program, ``gen-cffi-src``. Both produce the same output as +:meth:`FFI.emit_c_code`: a ``.c`` source file ready to be compiled into +a CPython extension module. What they add is two convenient front-ends +-- one that executes an existing "build" Python script, and one that +reads a ``cdef`` and C prelude from two files. This tool enables +integrating with and build backend, such as `meson-python +`_, `scikit-build-core +`_, or similar. + +The rest of this page uses meson-python in the examples, but any PEP +517 backend that lets you run a helper program during the build can +drive ``gen-cffi-src`` the same way. + +The ``cffi.buildtool`` subpackage was integrated from the +`cffi-buildtool`_ project by Rose Davidson (@inklesspen on GitHub). + +.. _cffi-buildtool: https://github.com/inklesspen/cffi-buildtool + + +Python API for ``cffi.buildtool`` +================================= + +.. py:module:: cffi.buildtool + +.. py:function:: find_ffi_in_python_script(pysrc, filename, ffivar) + + Execute a Python build script and return the :class:`cffi.FFI` + object it defines. ``pysrc`` is the text of the script, + ``filename`` is used for diagnostics, and ``ffivar`` is the name + the script binds to the :class:`FFI` (or to a callable returning + one -- typical ``ffibuilder`` names are supported). The script is + executed with ``__name__`` set to ``"gen-cffi-src"`` so a trailing + ``if __name__ == "__main__": ffibuilder.compile()`` block is + skipped. + +.. py:function:: make_ffi_from_sources(modulename, cdef, csrc) + + Build an :class:`cffi.FFI` from a ``cdef`` string and a C source + prelude. Equivalent to:: + + ffi = FFI() + ffi.cdef(cdef) + ffi.set_source(modulename, csrc) + +.. py:function:: generate_c_source(ffi) + + Return the C source that :meth:`FFI.emit_c_code` would write for + the given :class:`cffi.FFI`, as a :class:`str`. + + +The ``gen-cffi-src`` Command-line Tool +====================================== + +``gen-cffi-src`` has two subcommands. In both, the final positional +argument is the path to the ``.c`` file to generate. + +.. note:: + + When you drive the build from a build backend, the + ``libraries=``, ``library_dirs=``, ``include_dirs=``, + ``extra_compile_args=`` etc. arguments you pass to + :meth:`FFI.set_source` are *ignored*. Link and include settings are + the build backend's responsibility; for meson-python you express + them through the ``dependencies:`` / ``include_directories:`` + arguments of ``py.extension_module()``. + + +``gen-cffi-src exec-python`` +---------------------------- + +This mode takes the Python build script you would normally run by +hand -- the one the CFFI docs show under "Main mode of usage" -- and +generates the ``.c`` source for you. For example, given this +``_squared_build.py``:: + + from cffi import FFI + + ffibuilder = FFI() + + ffibuilder.cdef("int square(int n);") + + ffibuilder.set_source( + "squared._squared", + '#include "square.h"', + ) + + if __name__ == "__main__": + ffibuilder.compile(verbose=True) + +you run: + +.. code-block:: console + + $ gen-cffi-src exec-python _squared_build.py _squared.c + +If the :class:`cffi.FFI` is bound to a name other than ``ffibuilder``, +pass ``--ffi-var``: + +.. code-block:: console + + $ gen-cffi-src exec-python --ffi-var=make_ffi _squared_build.py _squared.c + +``gen-cffi-src read-sources`` +----------------------------- + +For larger modules, keeping the ``cdef`` and the C source prelude in +separate files tends to be easier to work with -- your editor +treats them as plain C, and presubmit tooling doesn't have to parse +them out of a string literal. + +Given ``squared.cdef.txt``: + +.. code-block:: C + + int square(int n); + +and ``squared.csrc.c``: + +.. code-block:: C + + #include "square.h" + +you run: + +.. code-block:: console + + $ gen-cffi-src read-sources squared._squared squared.cdef.txt squared.csrc.c _squared.c + +The first positional argument is the fully qualified module name that +will be embedded in the generated source (equivalent to the first +argument to :meth:`FFI.set_source`). + + +A Worked Example Using ``meson-python`` +======================================= + +Project layout: + +.. code-block:: text + + squared/ + ├── pyproject.toml + ├── meson.build + └── src/ + ├── squared/ + │ ├── __init__.py + │ └── _squared_build.py + └── csrc/ + ├── square.h + └── square.c + +``pyproject.toml``: + +.. code-block:: toml + + [build-system] + build-backend = 'mesonpy' + requires = ['meson-python', 'cffi'] + + [project] + name = 'squared' + version = '0.1.0' + requires-python = '>=3.9' + dependencies = ['cffi'] + +``meson.build``: + +.. code-block:: meson + + project( + 'squared', + 'c', + version: '0.1.0', + ) + + py = import('python').find_installation(pure: false) + + install_subdir('src/squared', install_dir: py.get_install_dir()) + + gen_cffi_src = find_program('gen-cffi-src') + + square_lib = static_library( + 'square', + 'src/csrc/square.c', + include_directories: include_directories('src/csrc'), + ) + square_dep = declare_dependency( + link_with: square_lib, + include_directories: include_directories('src/csrc'), + ) + + squared_ext_src = custom_target( + 'squared-cffi-src', + command: [ + gen_cffi_src, + 'exec-python', + '@INPUT@', + '@OUTPUT@', + ], + output: '_squared.c', + input: ['src/squared/_squared_build.py'], + ) + + py.extension_module( + '_squared', + squared_ext_src, + subdir: 'squared', + install: true, + dependencies: [square_dep, py.dependency()], + ) + +``src/squared/__init__.py``: + +.. code-block:: python + + from ._squared import ffi, lib + + + def squared(n): + return lib.square(n) + +``src/squared/_squared_build.py``, ``src/csrc/square.h`` and +``src/csrc/square.c`` contain the snippets shown above. + +Build and install the project with any PEP 517 front-end. For +example: + +.. code-block:: console + + $ python -m pip install . + $ python -c "from squared import squared; print(squared(7))" + 49 + +To switch this project to ``read-sources`` mode, replace +``_squared_build.py`` with two files (``_squared.cdef.txt`` and +``_squared.csrc.c``), then change the ``custom_target`` command to: + +.. code-block:: meson + + command: [ + gen_cffi_src, + 'read-sources', + 'squared._squared', + '@INPUT0@', + '@INPUT1@', + '@OUTPUT@', + ], + +and list both files under ``input:``: + +.. code-block:: meson + + input: ['src/squared/_squared.cdef.txt', '_squared.csrc.c'] + +Distributing CFFI Extensions using Setuptools +============================================= + +.. _distutils-setuptools: + + You can (but don't have to) use CFFI's **Distutils** or + **Setuptools integration** when writing a ``setup.py``. For + Distutils (only in out-of-line API mode; deprecated since + Python 3.10): + + .. code-block:: python + + # setup.py (requires CFFI to be installed first) + from distutils.core import setup + + import foo_build # possibly with sys.path tricks to find it + + setup( + ..., + ext_modules=[foo_build.ffibuilder.distutils_extension()], + ) + + For Setuptools (out-of-line only, but works in ABI or API mode; + recommended): + + .. code-block:: python + + # setup.py (with automatic dependency tracking) + from setuptools import setup + + setup( + ..., + setup_requires=["cffi>=1.0.0"], + cffi_modules=["package/foo_build.py:ffibuilder"], + install_requires=["cffi>=1.0.0"], + ) + + Note again that the ``foo_build.py`` example contains the following + lines, which mean that the ``ffibuilder`` is not actually compiled + when ``package.foo_build`` is merely imported---it will be compiled + independently by the Setuptools logic, using compilation parameters + provided by Setuptools: + + .. code-block:: python + + if __name__ == "__main__": # not when running with setuptools + ffibuilder.compile(verbose=True) + +* Note that some bundler tools that try to find all modules used by a + project, like PyInstaller, will miss ``_cffi_backend`` in the + out-of-line mode because your program contains no explicit ``import + cffi`` or ``import _cffi_backend``. You need to add + ``_cffi_backend`` explicitly (as a "hidden import" in PyInstaller, + but it can also be done more generally by adding the line ``import + _cffi_backend`` in your main program). + +Note that CFFI actually contains two different ``FFI`` classes. The +page `Using the ffi/lib objects`_ describes the common functionality. +It is what you get in the ``from package._foo import ffi`` lines above. +On the other hand, the extended ``FFI`` class is the one you get from +``import cffi; ffi_or_ffibuilder = cffi.FFI()``. It has the same +functionality (for in-line use), but also the extra methods described +below (to prepare the FFI). NOTE: We use the name ``ffibuilder`` +instead of ``ffi`` in the out-of-line context, when the code is about +producing a ``_foo.so`` file; this is an attempt to distinguish it +from the different ``ffi`` object that you get by later saying +``from _foo import ffi``. + +.. _`Using the ffi/lib objects`: using.html + +The reason for this split of functionality is that a regular program +using CFFI out-of-line does not need to import the ``cffi`` pure +Python package at all. (Internally it still needs ``_cffi_backend``, +a C extension module that comes with CFFI; this is why CFFI is also +listed in ``install_requires=..`` above. In the future this might be +split into a different PyPI package that only installs +``_cffi_backend``.) + +Note that a few small differences do exist: notably, ``from _foo import +ffi`` returns an object of a type written in C, which does not let you +add random attributes to it (nor does it have all the +underscore-prefixed internal attributes of the Python version). +Similarly, the ``lib`` objects returned by the C version are read-only, +apart from writes to global variables. Also, ``lib.__dict__`` does +not work before version 1.2 or if ``lib`` happens to declare a name +called ``__dict__`` (use instead ``dir(lib)``). The same is true +for ``lib.__class__``, ``lib.__all__`` and ``lib.__name__`` added +in successive versions. diff --git a/doc/source/cdef.rst b/doc/source/cdef.rst index 15be27fac..9a18dc9a8 100644 --- a/doc/source/cdef.rst +++ b/doc/source/cdef.rst @@ -1,9 +1,15 @@ -====================================== -Preparing and Distributing modules -====================================== +========================= +Preparing Wrapper Modules +========================= .. contents:: +.. note:: + + This covers how to create wrapper modules. See :ref:`buildtool_docs` + for instructions on how to integrate with a Python build backend and + distribute wrapper modules. + There are three or four different ways to use CFFI in a project. In order of complexity: @@ -76,92 +82,6 @@ In order of complexity: # use ffi and lib here -.. _distutils-setuptools: - -* Finally, you can (but don't have to) use CFFI's **Distutils** or - **Setuptools integration** when writing a ``setup.py``. For - Distutils (only in out-of-line API mode; deprecated since - Python 3.10): - - .. code-block:: python - - # setup.py (requires CFFI to be installed first) - from distutils.core import setup - - import foo_build # possibly with sys.path tricks to find it - - setup( - ..., - ext_modules=[foo_build.ffibuilder.distutils_extension()], - ) - - For Setuptools (out-of-line only, but works in ABI or API mode; - recommended): - - .. code-block:: python - - # setup.py (with automatic dependency tracking) - from setuptools import setup - - setup( - ..., - setup_requires=["cffi>=1.0.0"], - cffi_modules=["package/foo_build.py:ffibuilder"], - install_requires=["cffi>=1.0.0"], - ) - - Note again that the ``foo_build.py`` example contains the following - lines, which mean that the ``ffibuilder`` is not actually compiled - when ``package.foo_build`` is merely imported---it will be compiled - independently by the Setuptools logic, using compilation parameters - provided by Setuptools: - - .. code-block:: python - - if __name__ == "__main__": # not when running with setuptools - ffibuilder.compile(verbose=True) - -* Note that some bundler tools that try to find all modules used by a - project, like PyInstaller, will miss ``_cffi_backend`` in the - out-of-line mode because your program contains no explicit ``import - cffi`` or ``import _cffi_backend``. You need to add - ``_cffi_backend`` explicitly (as a "hidden import" in PyInstaller, - but it can also be done more generally by adding the line ``import - _cffi_backend`` in your main program). - -Note that CFFI actually contains two different ``FFI`` classes. The -page `Using the ffi/lib objects`_ describes the common functionality. -It is what you get in the ``from package._foo import ffi`` lines above. -On the other hand, the extended ``FFI`` class is the one you get from -``import cffi; ffi_or_ffibuilder = cffi.FFI()``. It has the same -functionality (for in-line use), but also the extra methods described -below (to prepare the FFI). NOTE: We use the name ``ffibuilder`` -instead of ``ffi`` in the out-of-line context, when the code is about -producing a ``_foo.so`` file; this is an attempt to distinguish it -from the different ``ffi`` object that you get by later saying -``from _foo import ffi``. - -.. _`Using the ffi/lib objects`: using.html - -The reason for this split of functionality is that a regular program -using CFFI out-of-line does not need to import the ``cffi`` pure -Python package at all. (Internally it still needs ``_cffi_backend``, -a C extension module that comes with CFFI; this is why CFFI is also -listed in ``install_requires=..`` above. In the future this might be -split into a different PyPI package that only installs -``_cffi_backend``.) - -Note that a few small differences do exist: notably, ``from _foo import -ffi`` returns an object of a type written in C, which does not let you -add random attributes to it (nor does it have all the -underscore-prefixed internal attributes of the Python version). -Similarly, the ``lib`` objects returned by the C version are read-only, -apart from writes to global variables. Also, ``lib.__dict__`` does -not work before version 1.2 or if ``lib`` happens to declare a name -called ``__dict__`` (use instead ``dir(lib)``). The same is true -for ``lib.__class__``, ``lib.__all__`` and ``lib.__name__`` added -in successive versions. - .. _cdef: @@ -935,10 +855,10 @@ steps. and *if* the "stuff" part is big enough that import time is a concern, then rewrite it as described in `the out-of-line but still ABI mode`__ -above. Optionally, see also the `setuptools integration`__ paragraph. +above. Optionally, see also the :ref:`build backend and distrubution +` documentation. .. __: out-of-line-abi_ -.. __: distutils-setuptools_ **API mode** if your CFFI project uses ``ffi.verify()``: @@ -951,16 +871,15 @@ above. Optionally, see also the `setuptools integration`__ paragraph. ffi.cdef("stuff") lib = ffi.verify("real C code") -then you should really rewrite it as described in `the out-of-line, -API mode`__ above. It avoids a number of issues that have caused +then you should really rewrite it as described in `the out-of-line, API +mode`__ above. It avoids a number of issues that have caused ``ffi.verify()`` to grow a number of extra arguments over time. Then -see the `distutils or setuptools`__ paragraph. Also, remember to -remove the ``ext_package=".."`` from your ``setup.py``, which was -sometimes needed with ``verify()`` but is just creating confusion with -``set_source()``. +see the :ref:`build backend and distrubution ` +documentation. Also, remember to remove the ``ext_package=".."`` from +your ``setup.py``, which was sometimes needed with ``verify()`` but is +just creating confusion with ``set_source()``. .. __: out-of-line-api_ -.. __: distutils-setuptools_ The following example should work both with old (pre-1.0) and new versions of CFFI---supporting both is important to run on old diff --git a/doc/source/index.rst b/doc/source/index.rst index 54934f221..f5939d0ef 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -16,4 +16,5 @@ copy-paste from header files or documentation. using ref cdef + buildtool embedding diff --git a/doc/source/whatsnew.rst b/doc/source/whatsnew.rst index db9352d03..df7755ba4 100644 --- a/doc/source/whatsnew.rst +++ b/doc/source/whatsnew.rst @@ -10,8 +10,16 @@ v2.0.0.dev0 with the limited API, so you must set py_limited_api=False when building extensions for the free-threaded build. * Added support for Python 3.14. (`#177`_) +* Added the :mod:`cffi.buildtool` subpackage and the ``gen-cffi-src`` + command-line tool, which let build backends such as `meson-python`_ + generate CFFI extension C source without depending on setuptools. + See :doc:`buildtool` for details. Integrated from the + `cffi-buildtool`_ project by Rose Davidson. * WIP +.. _`meson-python`: https://meson-python.readthedocs.io/ +.. _`cffi-buildtool`: https://github.com/inklesspen/cffi-buildtool + .. _`#177`: https://github.com/python-cffi/cffi/pull/177 .. _`#178`: https://github.com/python-cffi/cffi/pull/178 diff --git a/pyproject.toml b/pyproject.toml index 447621fb0..0f2ac4cd4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,9 @@ maintainers = [ [project.entry-points."distutils.setup_keywords"] cffi_modules = "cffi.setuptools_ext:cffi_modules" +[project.scripts] +gen-cffi-src = "cffi.buildtool._cli:run" + [project.urls] Documentation = "https://cffi.readthedocs.io/" Changelog = "https://cffi.readthedocs.io/en/latest/whatsnew.html" diff --git a/setup.py b/setup.py index c75812c24..29ea5be2b 100644 --- a/setup.py +++ b/setup.py @@ -183,7 +183,7 @@ def has_ext_modules(self): cpython = ('_cffi_backend' not in sys.builtin_module_names) setup( - packages=['cffi'] if cpython else [], + packages=['cffi', 'cffi.buildtool'] if cpython else [], package_dir={"": "src"}, package_data={'cffi': ['_cffi_include.h', 'parse_c_type.h', '_embedding.h', '_cffi_errors.h']} diff --git a/src/cffi/buildtool/__init__.py b/src/cffi/buildtool/__init__.py new file mode 100644 index 000000000..597777d24 --- /dev/null +++ b/src/cffi/buildtool/__init__.py @@ -0,0 +1,26 @@ +"""Helpers for generating CFFI C source without invoking external dependencies. + +This subpackage exposes a small API and a command-line entry point +(``gen-cffi-src``) that build backends can invoke during a build to +produce the ``.c`` source file for a CFFI extension module.: + +* :func:`find_ffi_in_python_script` -- execute an "exec-python" build + script and return the :class:`cffi.FFI` object it defines. +* :func:`make_ffi_from_sources` -- construct an :class:`cffi.FFI` + from a ``cdef`` string and a C source prelude. +* :func:`generate_c_source` -- emit the generated C source for an + :class:`cffi.FFI` as a string. + +""" + +from ._gen import ( + find_ffi_in_python_script, + generate_c_source, + make_ffi_from_sources, +) + +__all__ = [ + 'find_ffi_in_python_script', + 'generate_c_source', + 'make_ffi_from_sources', +] diff --git a/src/cffi/buildtool/__main__.py b/src/cffi/buildtool/__main__.py new file mode 100644 index 000000000..21c976405 --- /dev/null +++ b/src/cffi/buildtool/__main__.py @@ -0,0 +1,18 @@ +# Integrated from the cffi-buildtool project by Rose Davidson +# (https://github.com/inklesspen/cffi-buildtool), MIT-licensed. +""" +Entrypoint module, in case you use `python -m cffi.buildtool`. + + +Why does this file exist, and why __main__? For more info, read: + +- https://www.python.org/dev/peps/pep-0338/ +- https://docs.python.org/2/using/cmdline.html#cmdoption-m +- https://docs.python.org/3/using/cmdline.html#cmdoption-m +""" + + +from ._cli import run + +if __name__ == '__main__': + run() diff --git a/src/cffi/buildtool/_cli.py b/src/cffi/buildtool/_cli.py new file mode 100644 index 000000000..94c414f00 --- /dev/null +++ b/src/cffi/buildtool/_cli.py @@ -0,0 +1,110 @@ +# Integrated from the cffi-buildtool project by Rose Davidson +# (https://github.com/inklesspen/cffi-buildtool), MIT-licensed. +"""Command-line entry point for ``gen-cffi-src``. + +Two subcommands: + +``exec-python`` + Execute a Python build 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`` + Build the :class:`cffi.FFI` from a separate ``cdef`` file and C + source prelude, then emit the generated C source. +""" + +import argparse + +from ._gen import ( + find_ffi_in_python_script, + generate_c_source, + make_ffi_from_sources, +) + + +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) + with output: + output.write(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) + with output: + output.write(generated) + + +parser = argparse.ArgumentParser( + prog='gen-cffi-src', + description='Generate CFFI C source for a build backend (e.g. meson-python).', +) +subparsers = parser.add_subparsers(dest='mode') + +exec_python_parser = subparsers.add_parser( + 'exec-python', + help='Execute a Python script to build 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', + type=argparse.FileType('w', encoding='utf-8'), + help='Output path for the C source', +) + +read_sources_parser = subparsers.add_parser( + 'read-sources', + help='Read cdef and C source prelude files to build 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', + type=argparse.FileType('w', encoding='utf-8'), + 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 args.cdef is 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) diff --git a/src/cffi/buildtool/_gen.py b/src/cffi/buildtool/_gen.py new file mode 100644 index 000000000..4ad2070fb --- /dev/null +++ b/src/cffi/buildtool/_gen.py @@ -0,0 +1,58 @@ +# Integrated from the cffi-buildtool project by Rose Davidson +# (https://github.com/inklesspen/cffi-buildtool), MIT-licensed. +import io + +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 ``"gen-cffi-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. + """ + globs = {'__name__': 'gen-cffi-src'} + _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 + + +def make_ffi_from_sources(modulename, cdef, csrc): + """Build 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() diff --git a/testing/cffi1/buildtool_example/meson.build b/testing/cffi1/buildtool_example/meson.build new file mode 100644 index 000000000..a0c151073 --- /dev/null +++ b/testing/cffi1/buildtool_example/meson.build @@ -0,0 +1,42 @@ +project( + 'squared', + 'c', + version: '0.1.0', + default_options: ['warning_level=2'], +) + +py = import('python').find_installation(pure: false) + +install_subdir('src/squared', install_dir: py.get_install_dir()) + +gen_cffi_src = find_program('gen-cffi-src') + +square_lib = static_library( + 'square', + 'src/csrc/square.c', + include_directories: include_directories('src/csrc'), +) +square_dep = declare_dependency( + link_with: square_lib, + include_directories: include_directories('src/csrc'), +) + +squared_ext_src = custom_target( + 'squared-cffi-src', + command: [ + gen_cffi_src, + 'exec-python', + '@INPUT@', + '@OUTPUT@', + ], + output: '_squared.c', + input: ['src/squared/_squared_build.py'], +) + +py.extension_module( + '_squared', + squared_ext_src, + subdir: 'squared', + install: true, + dependencies: [square_dep, py.dependency()], +) diff --git a/testing/cffi1/buildtool_example/pyproject.toml b/testing/cffi1/buildtool_example/pyproject.toml new file mode 100644 index 000000000..45b21900a --- /dev/null +++ b/testing/cffi1/buildtool_example/pyproject.toml @@ -0,0 +1,10 @@ +[build-system] +build-backend = 'mesonpy' +requires = ['meson-python', 'cffi'] + +[project] +name = 'squared' +version = '0.1.0' +description = 'Small self-contained example project that builds a CFFI extension via meson-python.' +requires-python = '>=3.9' +dependencies = ['cffi'] diff --git a/testing/cffi1/buildtool_example/src/csrc/square.c b/testing/cffi1/buildtool_example/src/csrc/square.c new file mode 100644 index 000000000..05d6092f5 --- /dev/null +++ b/testing/cffi1/buildtool_example/src/csrc/square.c @@ -0,0 +1,5 @@ +#include "square.h" + +int square(int n) { + return n * n; +} diff --git a/testing/cffi1/buildtool_example/src/csrc/square.h b/testing/cffi1/buildtool_example/src/csrc/square.h new file mode 100644 index 000000000..356a57717 --- /dev/null +++ b/testing/cffi1/buildtool_example/src/csrc/square.h @@ -0,0 +1,6 @@ +#ifndef SQUARE_H +#define SQUARE_H + +int square(int n); + +#endif diff --git a/testing/cffi1/buildtool_example/src/squared/__init__.py b/testing/cffi1/buildtool_example/src/squared/__init__.py new file mode 100644 index 000000000..156e67d54 --- /dev/null +++ b/testing/cffi1/buildtool_example/src/squared/__init__.py @@ -0,0 +1,5 @@ +from ._squared import ffi, lib + + +def squared(n): + return lib.square(n) diff --git a/testing/cffi1/buildtool_example/src/squared/_squared_build.py b/testing/cffi1/buildtool_example/src/squared/_squared_build.py new file mode 100644 index 000000000..f44e92c54 --- /dev/null +++ b/testing/cffi1/buildtool_example/src/squared/_squared_build.py @@ -0,0 +1,13 @@ +from cffi import FFI + +ffibuilder = FFI() + +ffibuilder.cdef("int square(int n);") + +ffibuilder.set_source( + "squared._squared", + '#include "square.h"', +) + +if __name__ == "__main__": + ffibuilder.compile(verbose=True) diff --git a/testing/cffi1/buildtool_example2/meson.build b/testing/cffi1/buildtool_example2/meson.build new file mode 100644 index 000000000..644910478 --- /dev/null +++ b/testing/cffi1/buildtool_example2/meson.build @@ -0,0 +1,44 @@ +project( + 'squared', + 'c', + version: '0.1.0', + default_options: ['warning_level=2'], +) + +py = import('python').find_installation(pure: false) + +install_subdir('src/squared', install_dir: py.get_install_dir()) + +gen_cffi_src = find_program('gen-cffi-src') + +square_lib = static_library( + 'square', + 'src/csrc/square.c', + include_directories: include_directories('src/csrc'), +) +square_dep = declare_dependency( + link_with: square_lib, + include_directories: include_directories('src/csrc'), +) + +squared_ext_src = custom_target( + 'squared-cffi-src', + command: [ + gen_cffi_src, + 'read-sources', + 'squared._squared', + '@INPUT0@', + '@INPUT1@', + '@OUTPUT@', + ], + output: '_squared.c', + input: ['src/squared/squared.cdef.txt', 'src/squared/squared.csrc.c'], +) + +py.extension_module( + '_squared', + squared_ext_src, + subdir: 'squared', + install: true, + dependencies: [square_dep, py.dependency()], +) diff --git a/testing/cffi1/buildtool_example2/pyproject.toml b/testing/cffi1/buildtool_example2/pyproject.toml new file mode 100644 index 000000000..45b21900a --- /dev/null +++ b/testing/cffi1/buildtool_example2/pyproject.toml @@ -0,0 +1,10 @@ +[build-system] +build-backend = 'mesonpy' +requires = ['meson-python', 'cffi'] + +[project] +name = 'squared' +version = '0.1.0' +description = 'Small self-contained example project that builds a CFFI extension via meson-python.' +requires-python = '>=3.9' +dependencies = ['cffi'] diff --git a/testing/cffi1/buildtool_example2/src/csrc/square.c b/testing/cffi1/buildtool_example2/src/csrc/square.c new file mode 100644 index 000000000..05d6092f5 --- /dev/null +++ b/testing/cffi1/buildtool_example2/src/csrc/square.c @@ -0,0 +1,5 @@ +#include "square.h" + +int square(int n) { + return n * n; +} diff --git a/testing/cffi1/buildtool_example2/src/csrc/square.h b/testing/cffi1/buildtool_example2/src/csrc/square.h new file mode 100644 index 000000000..356a57717 --- /dev/null +++ b/testing/cffi1/buildtool_example2/src/csrc/square.h @@ -0,0 +1,6 @@ +#ifndef SQUARE_H +#define SQUARE_H + +int square(int n); + +#endif diff --git a/testing/cffi1/buildtool_example2/src/squared/__init__.py b/testing/cffi1/buildtool_example2/src/squared/__init__.py new file mode 100644 index 000000000..156e67d54 --- /dev/null +++ b/testing/cffi1/buildtool_example2/src/squared/__init__.py @@ -0,0 +1,5 @@ +from ._squared import ffi, lib + + +def squared(n): + return lib.square(n) diff --git a/testing/cffi1/buildtool_example2/src/squared/squared.cdef.txt b/testing/cffi1/buildtool_example2/src/squared/squared.cdef.txt new file mode 100644 index 000000000..72afcbd76 --- /dev/null +++ b/testing/cffi1/buildtool_example2/src/squared/squared.cdef.txt @@ -0,0 +1 @@ +int square(int n); \ No newline at end of file diff --git a/testing/cffi1/buildtool_example2/src/squared/squared.csrc.c b/testing/cffi1/buildtool_example2/src/squared/squared.csrc.c new file mode 100644 index 000000000..930ea7ee7 --- /dev/null +++ b/testing/cffi1/buildtool_example2/src/squared/squared.csrc.c @@ -0,0 +1 @@ +#include "square.h" diff --git a/testing/cffi1/test_buildtool.py b/testing/cffi1/test_buildtool.py new file mode 100644 index 000000000..1792060f9 --- /dev/null +++ b/testing/cffi1/test_buildtool.py @@ -0,0 +1,131 @@ +import pytest + +from cffi.buildtool import ( + find_ffi_in_python_script, + generate_c_source, + make_ffi_from_sources, +) +from cffi.buildtool import _cli as buildtool_cli + + +SIMPLE_SCRIPT = """\ +from cffi import FFI + +ffibuilder = FFI() + +ffibuilder.cdef("int square(int n);") + +ffibuilder.set_source("squared._squared", '#include "square.h"') + +something_else = 42 + +if __name__ == "__main__": + ffibuilder.compile(verbose=True) +""" + +CALLABLE_SCRIPT = """\ +from cffi import FFI + +def make_ffi(): + ffibuilder = FFI() + ffibuilder.cdef("int square(int n);") + ffibuilder.set_source("squared._squared", '#include "square.h"') + return ffibuilder + +def something_else(): + return 42 +""" + + +def _dont_exit(_status): + pass + + +def test_find_ffi_simple(): + ffi = find_ffi_in_python_script(SIMPLE_SCRIPT, "_squared_build.py", "ffibuilder") + module_name, csrc, _source_extension, _kwds = ffi._assigned_source + assert module_name == "squared._squared" + assert csrc.strip() == '#include "square.h"' + cdef = "\n".join(ffi._cdefsources) + assert "int square" in cdef + + +def test_find_ffi_callable(): + ffi = find_ffi_in_python_script(CALLABLE_SCRIPT, "_squared_build.py", "make_ffi") + module_name, csrc, _source_extension, _kwds = ffi._assigned_source + assert module_name == "squared._squared" + assert csrc.strip() == '#include "square.h"' + cdef = "\n".join(ffi._cdefsources) + assert "int square" in cdef + + +def test_find_ffi_name_not_found(): + with pytest.raises(NameError, match="'notfound'"): + find_ffi_in_python_script(SIMPLE_SCRIPT, "_squared_build.py", "notfound") + + +def test_find_ffi_wrong_type(): + with pytest.raises(TypeError, match="not an instance of cffi.api.FFI"): + find_ffi_in_python_script(SIMPLE_SCRIPT, "_squared_build.py", "something_else") + + +def test_find_ffi_callable_wrong_type(): + with pytest.raises(TypeError, match="not an instance of cffi.api.FFI"): + find_ffi_in_python_script(CALLABLE_SCRIPT, "_squared_build.py", "something_else") + + +def test_make_ffi_from_sources_and_generate(): + ffi = make_ffi_from_sources( + "squared._squared", + "int square(int n);", + '#include "square.h"', + ) + c_source = generate_c_source(ffi) + assert "square" in c_source + # sanity check: the emitted C source is the thing meson-python will compile + assert "PyInit" in c_source or "_cffi_f_" in c_source + + +def test_cli_exec_python(tmp_path, monkeypatch): + monkeypatch.setattr(buildtool_cli.parser, "exit", _dont_exit) + pyfile = tmp_path / "_squared_build.py" + pyfile.write_text(SIMPLE_SCRIPT) + output = tmp_path / "out.c" + buildtool_cli.run(["exec-python", str(pyfile), str(output)]) + generated = output.read_text() + assert "square" in generated + + +def test_cli_exec_python_ffi_var(tmp_path, monkeypatch): + monkeypatch.setattr(buildtool_cli.parser, "exit", _dont_exit) + pyfile = tmp_path / "_squared_build.py" + pyfile.write_text(CALLABLE_SCRIPT) + output = tmp_path / "out.c" + buildtool_cli.run(["exec-python", "--ffi-var", "make_ffi", str(pyfile), str(output)]) + generated = output.read_text() + assert "square" in generated + + +def test_cli_read_sources(tmp_path, monkeypatch): + monkeypatch.setattr(buildtool_cli.parser, "exit", _dont_exit) + cdef = tmp_path / "squared.cdef.txt" + cdef.write_text("int square(int n);\n") + csrc = tmp_path / "squared.csrc.c" + csrc.write_text('#include "square.h"\n') + output = tmp_path / "out.c" + buildtool_cli.run([ + "read-sources", + "squared._squared", + str(cdef), + str(csrc), + str(output), + ]) + generated = output.read_text() + assert "square" in generated + + +def test_cli_read_sources_same_input_fails(capsys): + with pytest.raises(SystemExit): + buildtool_cli.run(["read-sources", "_squared", "-", "-", "-"]) + _stdout, stderr = capsys.readouterr() + assert "are the same file and should not be" in stderr diff --git a/testing/cffi1/test_buildtool_meson.py b/testing/cffi1/test_buildtool_meson.py new file mode 100644 index 000000000..fa086c70d --- /dev/null +++ b/testing/cffi1/test_buildtool_meson.py @@ -0,0 +1,81 @@ +"""End-to-end test: build a self-contained CFFI extension with meson-python. + +The test provisions a fresh nested venv under ``tmp_path`` using the +stdlib :mod:`venv` module, installs ``cffi`` (from the current source +tree) and ``meson-python`` into it, installs the small example project +that lives under ``testing/cffi1/buildtool_example/``, and then +imports the built extension to confirm it works. + +The test does not use ``uv``. It only relies on the running Python +interpreter having access to ``pip`` (which is true for any venv +created by :mod:`venv`) and on a working C compiler being on ``PATH``. +""" + +import os +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + +import cffi + +pytestmark = [ + pytest.mark.thread_unsafe(reason="spawns subprocesses, slow"), +] + +try: + import mesonpy +except ImportError: + pytest.skip("Test requires meson-python", allow_module_level=True) + + +HERE = Path(__file__).resolve().parent +EXAMPLE_PROJECT = HERE / "buildtool_example" +EXAMPLE_PROJECT2 = HERE / "buildtool_example2" +CFFI_DIR = HERE.parent.parent + + +def _venv_python(venv_dir): + if sys.platform == "win32": + return venv_dir / "Scripts" / "python.exe" + return venv_dir / "bin" / "python" + + +@pytest.mark.parametrize("project", [EXAMPLE_PROJECT, EXAMPLE_PROJECT2]) +def test_meson_python_build(tmp_path, project): + venv_dir = tmp_path / "venv" + subprocess.check_call([sys.executable, "-m", "venv", str(venv_dir)]) + venv_python = _venv_python(venv_dir) + assert venv_python.exists(), venv_python + + # Upgrade pip so --no-build-isolation behaves consistently with recent + # resolver behaviour on older base images. + subprocess.check_call([ + str(venv_python), "-m", "pip", "install", "--upgrade", "pip", + ]) + + # Install build-time deps into the nested venv. + subprocess.check_call([ + str(venv_python), "-m", "pip", "install", "meson-python", CFFI_DIR + ]) + + # Copy the example project so nothing is written back into the + # source tree + project_dir = tmp_path / "project" + shutil.copytree(project, project_dir) + + # --no-build-isolation to ensure the test runs against the CFFI build we want to test + subprocess.check_call([ + str(venv_python), "-m", "pip", "install", + "--no-build-isolation", str(project_dir), + ]) + + # Confirm the built extension imports and behaves as expected. + subprocess.check_call([ + str(venv_python), "-c", + "from squared import squared; " + "assert squared(7) == 49; " + "assert squared(-3) == 9", + ]) From 9293037a1bd788384d000b469fcc43ab8adff5f6 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 19 May 2026 11:50:14 -0600 Subject: [PATCH 2/8] typo fix --- doc/source/buildtool.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/buildtool.rst b/doc/source/buildtool.rst index 8d084ad64..d294a2e94 100644 --- a/doc/source/buildtool.rst +++ b/doc/source/buildtool.rst @@ -12,7 +12,7 @@ command-line program, ``gen-cffi-src``. Both produce the same output as a CPython extension module. What they add is two convenient front-ends -- one that executes an existing "build" Python script, and one that reads a ``cdef`` and C prelude from two files. This tool enables -integrating with and build backend, such as `meson-python +integrating with any build backend, such as `meson-python `_, `scikit-build-core `_, or similar. From 5dad2241dbcc22d08c982d5751f2dae45f0f197a Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 19 May 2026 12:22:23 -0600 Subject: [PATCH 3/8] Use symlinks and give test folders better names --- testing/cffi1/buildtool_example2/pyproject.toml | 10 ---------- testing/cffi1/buildtool_example2/src/csrc/square.c | 5 ----- testing/cffi1/buildtool_example2/src/csrc/square.h | 6 ------ .../cffi1/buildtool_example2/src/squared/__init__.py | 5 ----- .../build_script_example}/meson.build | 0 .../build_script_example/pyproject.toml | 1 + .../build_script_example/src/csrc/square.c | 1 + .../build_script_example/src/csrc/square.h | 1 + .../build_script_example/src/squared/__init__.py | 1 + .../src/squared/_squared_build.py | 0 .../cdef_example}/meson.build | 0 .../buildtool_examples/cdef_example/pyproject.toml | 1 + .../buildtool_examples/cdef_example/src/csrc/square.c | 1 + .../buildtool_examples/cdef_example/src/csrc/square.h | 1 + .../cdef_example/src/squared/__init__.py | 1 + .../cdef_example}/src/squared/squared.cdef.txt | 0 .../cdef_example}/src/squared/squared.csrc.c | 0 .../common}/pyproject.toml | 0 .../common}/src/csrc/square.c | 0 .../common}/src/csrc/square.h | 0 .../common}/src/squared/__init__.py | 0 testing/cffi1/test_buildtool_meson.py | 11 ++++------- 22 files changed, 12 insertions(+), 33 deletions(-) delete mode 100644 testing/cffi1/buildtool_example2/pyproject.toml delete mode 100644 testing/cffi1/buildtool_example2/src/csrc/square.c delete mode 100644 testing/cffi1/buildtool_example2/src/csrc/square.h delete mode 100644 testing/cffi1/buildtool_example2/src/squared/__init__.py rename testing/cffi1/{buildtool_example => buildtool_examples/build_script_example}/meson.build (100%) create mode 120000 testing/cffi1/buildtool_examples/build_script_example/pyproject.toml create mode 120000 testing/cffi1/buildtool_examples/build_script_example/src/csrc/square.c create mode 120000 testing/cffi1/buildtool_examples/build_script_example/src/csrc/square.h create mode 120000 testing/cffi1/buildtool_examples/build_script_example/src/squared/__init__.py rename testing/cffi1/{buildtool_example => buildtool_examples/build_script_example}/src/squared/_squared_build.py (100%) rename testing/cffi1/{buildtool_example2 => buildtool_examples/cdef_example}/meson.build (100%) create mode 120000 testing/cffi1/buildtool_examples/cdef_example/pyproject.toml create mode 120000 testing/cffi1/buildtool_examples/cdef_example/src/csrc/square.c create mode 120000 testing/cffi1/buildtool_examples/cdef_example/src/csrc/square.h create mode 120000 testing/cffi1/buildtool_examples/cdef_example/src/squared/__init__.py rename testing/cffi1/{buildtool_example2 => buildtool_examples/cdef_example}/src/squared/squared.cdef.txt (100%) rename testing/cffi1/{buildtool_example2 => buildtool_examples/cdef_example}/src/squared/squared.csrc.c (100%) rename testing/cffi1/{buildtool_example => buildtool_examples/common}/pyproject.toml (100%) rename testing/cffi1/{buildtool_example => buildtool_examples/common}/src/csrc/square.c (100%) rename testing/cffi1/{buildtool_example => buildtool_examples/common}/src/csrc/square.h (100%) rename testing/cffi1/{buildtool_example => buildtool_examples/common}/src/squared/__init__.py (100%) diff --git a/testing/cffi1/buildtool_example2/pyproject.toml b/testing/cffi1/buildtool_example2/pyproject.toml deleted file mode 100644 index 45b21900a..000000000 --- a/testing/cffi1/buildtool_example2/pyproject.toml +++ /dev/null @@ -1,10 +0,0 @@ -[build-system] -build-backend = 'mesonpy' -requires = ['meson-python', 'cffi'] - -[project] -name = 'squared' -version = '0.1.0' -description = 'Small self-contained example project that builds a CFFI extension via meson-python.' -requires-python = '>=3.9' -dependencies = ['cffi'] diff --git a/testing/cffi1/buildtool_example2/src/csrc/square.c b/testing/cffi1/buildtool_example2/src/csrc/square.c deleted file mode 100644 index 05d6092f5..000000000 --- a/testing/cffi1/buildtool_example2/src/csrc/square.c +++ /dev/null @@ -1,5 +0,0 @@ -#include "square.h" - -int square(int n) { - return n * n; -} diff --git a/testing/cffi1/buildtool_example2/src/csrc/square.h b/testing/cffi1/buildtool_example2/src/csrc/square.h deleted file mode 100644 index 356a57717..000000000 --- a/testing/cffi1/buildtool_example2/src/csrc/square.h +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef SQUARE_H -#define SQUARE_H - -int square(int n); - -#endif diff --git a/testing/cffi1/buildtool_example2/src/squared/__init__.py b/testing/cffi1/buildtool_example2/src/squared/__init__.py deleted file mode 100644 index 156e67d54..000000000 --- a/testing/cffi1/buildtool_example2/src/squared/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from ._squared import ffi, lib - - -def squared(n): - return lib.square(n) diff --git a/testing/cffi1/buildtool_example/meson.build b/testing/cffi1/buildtool_examples/build_script_example/meson.build similarity index 100% rename from testing/cffi1/buildtool_example/meson.build rename to testing/cffi1/buildtool_examples/build_script_example/meson.build diff --git a/testing/cffi1/buildtool_examples/build_script_example/pyproject.toml b/testing/cffi1/buildtool_examples/build_script_example/pyproject.toml new file mode 120000 index 000000000..d6064133e --- /dev/null +++ b/testing/cffi1/buildtool_examples/build_script_example/pyproject.toml @@ -0,0 +1 @@ +../common/pyproject.toml \ No newline at end of file diff --git a/testing/cffi1/buildtool_examples/build_script_example/src/csrc/square.c b/testing/cffi1/buildtool_examples/build_script_example/src/csrc/square.c new file mode 120000 index 000000000..ae7ce3e68 --- /dev/null +++ b/testing/cffi1/buildtool_examples/build_script_example/src/csrc/square.c @@ -0,0 +1 @@ +../../../common/src/csrc/square.c \ No newline at end of file diff --git a/testing/cffi1/buildtool_examples/build_script_example/src/csrc/square.h b/testing/cffi1/buildtool_examples/build_script_example/src/csrc/square.h new file mode 120000 index 000000000..fb6c63f26 --- /dev/null +++ b/testing/cffi1/buildtool_examples/build_script_example/src/csrc/square.h @@ -0,0 +1 @@ +../../../common/src/csrc/square.h \ No newline at end of file diff --git a/testing/cffi1/buildtool_examples/build_script_example/src/squared/__init__.py b/testing/cffi1/buildtool_examples/build_script_example/src/squared/__init__.py new file mode 120000 index 000000000..12d4d27e3 --- /dev/null +++ b/testing/cffi1/buildtool_examples/build_script_example/src/squared/__init__.py @@ -0,0 +1 @@ +../../../common/src/squared/__init__.py \ No newline at end of file diff --git a/testing/cffi1/buildtool_example/src/squared/_squared_build.py b/testing/cffi1/buildtool_examples/build_script_example/src/squared/_squared_build.py similarity index 100% rename from testing/cffi1/buildtool_example/src/squared/_squared_build.py rename to testing/cffi1/buildtool_examples/build_script_example/src/squared/_squared_build.py diff --git a/testing/cffi1/buildtool_example2/meson.build b/testing/cffi1/buildtool_examples/cdef_example/meson.build similarity index 100% rename from testing/cffi1/buildtool_example2/meson.build rename to testing/cffi1/buildtool_examples/cdef_example/meson.build diff --git a/testing/cffi1/buildtool_examples/cdef_example/pyproject.toml b/testing/cffi1/buildtool_examples/cdef_example/pyproject.toml new file mode 120000 index 000000000..d6064133e --- /dev/null +++ b/testing/cffi1/buildtool_examples/cdef_example/pyproject.toml @@ -0,0 +1 @@ +../common/pyproject.toml \ No newline at end of file diff --git a/testing/cffi1/buildtool_examples/cdef_example/src/csrc/square.c b/testing/cffi1/buildtool_examples/cdef_example/src/csrc/square.c new file mode 120000 index 000000000..ae7ce3e68 --- /dev/null +++ b/testing/cffi1/buildtool_examples/cdef_example/src/csrc/square.c @@ -0,0 +1 @@ +../../../common/src/csrc/square.c \ No newline at end of file diff --git a/testing/cffi1/buildtool_examples/cdef_example/src/csrc/square.h b/testing/cffi1/buildtool_examples/cdef_example/src/csrc/square.h new file mode 120000 index 000000000..fb6c63f26 --- /dev/null +++ b/testing/cffi1/buildtool_examples/cdef_example/src/csrc/square.h @@ -0,0 +1 @@ +../../../common/src/csrc/square.h \ No newline at end of file diff --git a/testing/cffi1/buildtool_examples/cdef_example/src/squared/__init__.py b/testing/cffi1/buildtool_examples/cdef_example/src/squared/__init__.py new file mode 120000 index 000000000..12d4d27e3 --- /dev/null +++ b/testing/cffi1/buildtool_examples/cdef_example/src/squared/__init__.py @@ -0,0 +1 @@ +../../../common/src/squared/__init__.py \ No newline at end of file diff --git a/testing/cffi1/buildtool_example2/src/squared/squared.cdef.txt b/testing/cffi1/buildtool_examples/cdef_example/src/squared/squared.cdef.txt similarity index 100% rename from testing/cffi1/buildtool_example2/src/squared/squared.cdef.txt rename to testing/cffi1/buildtool_examples/cdef_example/src/squared/squared.cdef.txt diff --git a/testing/cffi1/buildtool_example2/src/squared/squared.csrc.c b/testing/cffi1/buildtool_examples/cdef_example/src/squared/squared.csrc.c similarity index 100% rename from testing/cffi1/buildtool_example2/src/squared/squared.csrc.c rename to testing/cffi1/buildtool_examples/cdef_example/src/squared/squared.csrc.c diff --git a/testing/cffi1/buildtool_example/pyproject.toml b/testing/cffi1/buildtool_examples/common/pyproject.toml similarity index 100% rename from testing/cffi1/buildtool_example/pyproject.toml rename to testing/cffi1/buildtool_examples/common/pyproject.toml diff --git a/testing/cffi1/buildtool_example/src/csrc/square.c b/testing/cffi1/buildtool_examples/common/src/csrc/square.c similarity index 100% rename from testing/cffi1/buildtool_example/src/csrc/square.c rename to testing/cffi1/buildtool_examples/common/src/csrc/square.c diff --git a/testing/cffi1/buildtool_example/src/csrc/square.h b/testing/cffi1/buildtool_examples/common/src/csrc/square.h similarity index 100% rename from testing/cffi1/buildtool_example/src/csrc/square.h rename to testing/cffi1/buildtool_examples/common/src/csrc/square.h diff --git a/testing/cffi1/buildtool_example/src/squared/__init__.py b/testing/cffi1/buildtool_examples/common/src/squared/__init__.py similarity index 100% rename from testing/cffi1/buildtool_example/src/squared/__init__.py rename to testing/cffi1/buildtool_examples/common/src/squared/__init__.py diff --git a/testing/cffi1/test_buildtool_meson.py b/testing/cffi1/test_buildtool_meson.py index fa086c70d..4e426c2fa 100644 --- a/testing/cffi1/test_buildtool_meson.py +++ b/testing/cffi1/test_buildtool_meson.py @@ -2,13 +2,10 @@ The test provisions a fresh nested venv under ``tmp_path`` using the stdlib :mod:`venv` module, installs ``cffi`` (from the current source -tree) and ``meson-python`` into it, installs the small example project -that lives under ``testing/cffi1/buildtool_example/``, and then +tree) and ``meson-python`` into it, installs one of the small example +projects that live under ``testing/cffi1/buildtool_examples/``, and then imports the built extension to confirm it works. -The test does not use ``uv``. It only relies on the running Python -interpreter having access to ``pip`` (which is true for any venv -created by :mod:`venv`) and on a working C compiler being on ``PATH``. """ import os @@ -32,8 +29,8 @@ HERE = Path(__file__).resolve().parent -EXAMPLE_PROJECT = HERE / "buildtool_example" -EXAMPLE_PROJECT2 = HERE / "buildtool_example2" +EXAMPLE_PROJECT = HERE / "buildtool_examples" / "build_script_example" +EXAMPLE_PROJECT2 = HERE / "buildtool_examples" / "cdef_example" CFFI_DIR = HERE.parent.parent From e25ff27e7c95d0a78308756016ed77bcf7087950 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 9 Jun 2026 11:42:45 -0600 Subject: [PATCH 4/8] Apply suggestions from code review Thanks for the review comments! I'm going to apply the review suggestions that are pending now, pull, and address the remaining comments. Co-authored-by: Matti Picus --- doc/source/buildtool.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/buildtool.rst b/doc/source/buildtool.rst index d294a2e94..a3dcd4c72 100644 --- a/doc/source/buildtool.rst +++ b/doc/source/buildtool.rst @@ -6,10 +6,10 @@ Building and Distributing CFFI Extensions .. contents:: -CFFI ships a small subpackage, :mod:`cffi.buildtool`, together with a +CFFI ships a subpackage, :mod:`cffi.buildtool`, together with a command-line program, ``gen-cffi-src``. Both produce the same output as :meth:`FFI.emit_c_code`: a ``.c`` source file ready to be compiled into -a CPython extension module. What they add is two convenient front-ends +a CPython extension module. They add is two convenient front-ends -- one that executes an existing "build" Python script, and one that reads a ``cdef`` and C prelude from two files. This tool enables integrating with any build backend, such as `meson-python @@ -20,7 +20,7 @@ The rest of this page uses meson-python in the examples, but any PEP 517 backend that lets you run a helper program during the build can drive ``gen-cffi-src`` the same way. -The ``cffi.buildtool`` subpackage was integrated from the +The ``cffi.buildtool`` subpackage was vendored with permission from the `cffi-buildtool`_ project by Rose Davidson (@inklesspen on GitHub). .. _cffi-buildtool: https://github.com/inklesspen/cffi-buildtool From dec64eedaa637ede16dba1b24b3f80c4f005adfc Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 9 Jun 2026 12:50:43 -0600 Subject: [PATCH 5/8] Remove Python interface and consolidate functionality in cffi.buildtool CLI --- doc/source/buildtool.rst | 79 +++----- doc/source/whatsnew.rst | 10 +- pyproject.toml | 3 - setup.py | 2 +- src/cffi/_buildtool.py | 186 ++++++++++++++++++ src/cffi/buildtool.py | 8 + src/cffi/buildtool/__init__.py | 26 --- src/cffi/buildtool/__main__.py | 18 -- src/cffi/buildtool/_cli.py | 110 ----------- src/cffi/buildtool/_gen.py | 58 ------ .../build_script_example/meson.build | 5 +- .../cdef_example/meson.build | 5 +- testing/cffi1/test_buildtool.py | 155 ++++++++------- 13 files changed, 313 insertions(+), 352 deletions(-) create mode 100644 src/cffi/_buildtool.py create mode 100644 src/cffi/buildtool.py delete mode 100644 src/cffi/buildtool/__init__.py delete mode 100644 src/cffi/buildtool/__main__.py delete mode 100644 src/cffi/buildtool/_cli.py delete mode 100644 src/cffi/buildtool/_gen.py diff --git a/doc/source/buildtool.rst b/doc/source/buildtool.rst index a3dcd4c72..95036da25 100644 --- a/doc/source/buildtool.rst +++ b/doc/source/buildtool.rst @@ -6,10 +6,10 @@ Building and Distributing CFFI Extensions .. contents:: -CFFI ships a subpackage, :mod:`cffi.buildtool`, together with a -command-line program, ``gen-cffi-src``. Both produce the same output as +CFFI ships a command-line tool, invoked as ``python -m +cffi.buildtool``, that produces the same output as :meth:`FFI.emit_c_code`: a ``.c`` source file ready to be compiled into -a CPython extension module. They add is two convenient front-ends +a CPython extension module. It adds two convenient front-ends -- one that executes an existing "build" Python script, and one that reads a ``cdef`` and C prelude from two files. This tool enables integrating with any build backend, such as `meson-python @@ -18,50 +18,21 @@ integrating with any build backend, such as `meson-python The rest of this page uses meson-python in the examples, but any PEP 517 backend that lets you run a helper program during the build can -drive ``gen-cffi-src`` the same way. +drive ``python -m cffi.buildtool`` the same way. -The ``cffi.buildtool`` subpackage was vendored with permission from the -`cffi-buildtool`_ project by Rose Davidson (@inklesspen on GitHub). +The command line is the buildtool's only public interface; the +implementation inside the ``cffi`` package is private. The buildtool +was vendored with permission from the `cffi-buildtool`_ project by Rose +Davidson (@inklesspen on GitHub). .. _cffi-buildtool: https://github.com/inklesspen/cffi-buildtool -Python API for ``cffi.buildtool`` -================================= +The ``python -m cffi.buildtool`` Command-line Tool +================================================== -.. py:module:: cffi.buildtool - -.. py:function:: find_ffi_in_python_script(pysrc, filename, ffivar) - - Execute a Python build script and return the :class:`cffi.FFI` - object it defines. ``pysrc`` is the text of the script, - ``filename`` is used for diagnostics, and ``ffivar`` is the name - the script binds to the :class:`FFI` (or to a callable returning - one -- typical ``ffibuilder`` names are supported). The script is - executed with ``__name__`` set to ``"gen-cffi-src"`` so a trailing - ``if __name__ == "__main__": ffibuilder.compile()`` block is - skipped. - -.. py:function:: make_ffi_from_sources(modulename, cdef, csrc) - - Build an :class:`cffi.FFI` from a ``cdef`` string and a C source - prelude. Equivalent to:: - - ffi = FFI() - ffi.cdef(cdef) - ffi.set_source(modulename, csrc) - -.. py:function:: generate_c_source(ffi) - - Return the C source that :meth:`FFI.emit_c_code` would write for - the given :class:`cffi.FFI`, as a :class:`str`. - - -The ``gen-cffi-src`` Command-line Tool -====================================== - -``gen-cffi-src`` has two subcommands. In both, the final positional -argument is the path to the ``.c`` file to generate. +``python -m cffi.buildtool`` has two subcommands. In both, the final +positional argument is the path to the ``.c`` file to generate. .. note:: @@ -74,8 +45,8 @@ argument is the path to the ``.c`` file to generate. arguments of ``py.extension_module()``. -``gen-cffi-src exec-python`` ----------------------------- +``python -m cffi.buildtool exec-python`` +---------------------------------------- This mode takes the Python build script you would normally run by hand -- the one the CFFI docs show under "Main mode of usage" -- and @@ -100,17 +71,21 @@ you run: .. code-block:: console - $ gen-cffi-src exec-python _squared_build.py _squared.c + $ python -m cffi.buildtool exec-python _squared_build.py _squared.c + +The script is executed with ``__name__`` set to ``"cffi.buildtool"``, +so the trailing ``if __name__ == "__main__":`` block is skipped: the +tool only generates the C source and never compiles it. If the :class:`cffi.FFI` is bound to a name other than ``ffibuilder``, pass ``--ffi-var``: .. code-block:: console - $ gen-cffi-src exec-python --ffi-var=make_ffi _squared_build.py _squared.c + $ python -m cffi.buildtool exec-python --ffi-var=make_ffi _squared_build.py _squared.c -``gen-cffi-src read-sources`` ------------------------------ +``python -m cffi.buildtool read-sources`` +----------------------------------------- For larger modules, keeping the ``cdef`` and the C source prelude in separate files tends to be easier to work with -- your editor @@ -133,7 +108,7 @@ you run: .. code-block:: console - $ gen-cffi-src read-sources squared._squared squared.cdef.txt squared.csrc.c _squared.c + $ python -m cffi.buildtool read-sources squared._squared squared.cdef.txt squared.csrc.c _squared.c The first positional argument is the fully qualified module name that will be embedded in the generated source (equivalent to the first @@ -186,8 +161,6 @@ Project layout: install_subdir('src/squared', install_dir: py.get_install_dir()) - gen_cffi_src = find_program('gen-cffi-src') - square_lib = static_library( 'square', 'src/csrc/square.c', @@ -201,7 +174,8 @@ Project layout: squared_ext_src = custom_target( 'squared-cffi-src', command: [ - gen_cffi_src, + py, + '-m', 'cffi.buildtool', 'exec-python', '@INPUT@', '@OUTPUT@', @@ -247,7 +221,8 @@ To switch this project to ``read-sources`` mode, replace .. code-block:: meson command: [ - gen_cffi_src, + py, + '-m', 'cffi.buildtool', 'read-sources', 'squared._squared', '@INPUT0@', diff --git a/doc/source/whatsnew.rst b/doc/source/whatsnew.rst index df7755ba4..a73899535 100644 --- a/doc/source/whatsnew.rst +++ b/doc/source/whatsnew.rst @@ -10,11 +10,11 @@ v2.0.0.dev0 with the limited API, so you must set py_limited_api=False when building extensions for the free-threaded build. * Added support for Python 3.14. (`#177`_) -* Added the :mod:`cffi.buildtool` subpackage and the ``gen-cffi-src`` - command-line tool, which let build backends such as `meson-python`_ - generate CFFI extension C source without depending on setuptools. - See :doc:`buildtool` for details. Integrated from the - `cffi-buildtool`_ project by Rose Davidson. +* Added the ``python -m cffi.buildtool`` command-line tool, which lets + build backends such as `meson-python`_ generate CFFI extension C + source without depending on setuptools. See :doc:`buildtool` for + details. Integrated from the `cffi-buildtool`_ project by Rose + Davidson. * WIP .. _`meson-python`: https://meson-python.readthedocs.io/ diff --git a/pyproject.toml b/pyproject.toml index 0f2ac4cd4..447621fb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,9 +41,6 @@ maintainers = [ [project.entry-points."distutils.setup_keywords"] cffi_modules = "cffi.setuptools_ext:cffi_modules" -[project.scripts] -gen-cffi-src = "cffi.buildtool._cli:run" - [project.urls] Documentation = "https://cffi.readthedocs.io/" Changelog = "https://cffi.readthedocs.io/en/latest/whatsnew.html" diff --git a/setup.py b/setup.py index 29ea5be2b..c75812c24 100644 --- a/setup.py +++ b/setup.py @@ -183,7 +183,7 @@ def has_ext_modules(self): cpython = ('_cffi_backend' not in sys.builtin_module_names) setup( - packages=['cffi', 'cffi.buildtool'] if cpython else [], + packages=['cffi'] if cpython else [], package_dir={"": "src"}, package_data={'cffi': ['_cffi_include.h', 'parse_c_type.h', '_embedding.h', '_cffi_errors.h']} diff --git a/src/cffi/_buildtool.py b/src/cffi/_buildtool.py new file mode 100644 index 000000000..c1e251f87 --- /dev/null +++ b/src/cffi/_buildtool.py @@ -0,0 +1,186 @@ +# 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 ``python -m cffi.buildtool`` command-line tool. + +This module is private; the command line is the only supported +interface. Two subcommands: + +``exec-python`` + Execute a Python build 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`` + Build the :class:`cffi.FFI` from a separate ``cdef`` file and C + source prelude, then emit the generated C source. +""" + +import argparse +import io + +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.buildtool"``, + 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. + """ + globs = {'__name__': 'cffi.buildtool'} + _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 + + +def make_ffi_from_sources(modulename, cdef, csrc): + """Build 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 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) + with output: + output.write(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) + with output: + output.write(generated) + + +parser = argparse.ArgumentParser( + prog='python -m cffi.buildtool', + description='Generate CFFI C source for a build backend (e.g. meson-python).', +) +subparsers = parser.add_subparsers(dest='mode') + +exec_python_parser = subparsers.add_parser( + 'exec-python', + help='Execute a Python script to build 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', + type=argparse.FileType('w', encoding='utf-8'), + help='Output path for the C source', +) + +read_sources_parser = subparsers.add_parser( + 'read-sources', + help='Read cdef and C source prelude files to build 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', + type=argparse.FileType('w', encoding='utf-8'), + 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 args.cdef is 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) diff --git a/src/cffi/buildtool.py b/src/cffi/buildtool.py new file mode 100644 index 000000000..3e01ee144 --- /dev/null +++ b/src/cffi/buildtool.py @@ -0,0 +1,8 @@ +"""Entry point so ``python -m cffi.buildtool`` works. + +The implementation is private; see ``cffi/_buildtool.py``. +""" + +if __name__ == '__main__': + from cffi._buildtool import run + run() \ No newline at end of file diff --git a/src/cffi/buildtool/__init__.py b/src/cffi/buildtool/__init__.py deleted file mode 100644 index 597777d24..000000000 --- a/src/cffi/buildtool/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Helpers for generating CFFI C source without invoking external dependencies. - -This subpackage exposes a small API and a command-line entry point -(``gen-cffi-src``) that build backends can invoke during a build to -produce the ``.c`` source file for a CFFI extension module.: - -* :func:`find_ffi_in_python_script` -- execute an "exec-python" build - script and return the :class:`cffi.FFI` object it defines. -* :func:`make_ffi_from_sources` -- construct an :class:`cffi.FFI` - from a ``cdef`` string and a C source prelude. -* :func:`generate_c_source` -- emit the generated C source for an - :class:`cffi.FFI` as a string. - -""" - -from ._gen import ( - find_ffi_in_python_script, - generate_c_source, - make_ffi_from_sources, -) - -__all__ = [ - 'find_ffi_in_python_script', - 'generate_c_source', - 'make_ffi_from_sources', -] diff --git a/src/cffi/buildtool/__main__.py b/src/cffi/buildtool/__main__.py deleted file mode 100644 index 21c976405..000000000 --- a/src/cffi/buildtool/__main__.py +++ /dev/null @@ -1,18 +0,0 @@ -# Integrated from the cffi-buildtool project by Rose Davidson -# (https://github.com/inklesspen/cffi-buildtool), MIT-licensed. -""" -Entrypoint module, in case you use `python -m cffi.buildtool`. - - -Why does this file exist, and why __main__? For more info, read: - -- https://www.python.org/dev/peps/pep-0338/ -- https://docs.python.org/2/using/cmdline.html#cmdoption-m -- https://docs.python.org/3/using/cmdline.html#cmdoption-m -""" - - -from ._cli import run - -if __name__ == '__main__': - run() diff --git a/src/cffi/buildtool/_cli.py b/src/cffi/buildtool/_cli.py deleted file mode 100644 index 94c414f00..000000000 --- a/src/cffi/buildtool/_cli.py +++ /dev/null @@ -1,110 +0,0 @@ -# Integrated from the cffi-buildtool project by Rose Davidson -# (https://github.com/inklesspen/cffi-buildtool), MIT-licensed. -"""Command-line entry point for ``gen-cffi-src``. - -Two subcommands: - -``exec-python`` - Execute a Python build 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`` - Build the :class:`cffi.FFI` from a separate ``cdef`` file and C - source prelude, then emit the generated C source. -""" - -import argparse - -from ._gen import ( - find_ffi_in_python_script, - generate_c_source, - make_ffi_from_sources, -) - - -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) - with output: - output.write(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) - with output: - output.write(generated) - - -parser = argparse.ArgumentParser( - prog='gen-cffi-src', - description='Generate CFFI C source for a build backend (e.g. meson-python).', -) -subparsers = parser.add_subparsers(dest='mode') - -exec_python_parser = subparsers.add_parser( - 'exec-python', - help='Execute a Python script to build 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', - type=argparse.FileType('w', encoding='utf-8'), - help='Output path for the C source', -) - -read_sources_parser = subparsers.add_parser( - 'read-sources', - help='Read cdef and C source prelude files to build 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', - type=argparse.FileType('w', encoding='utf-8'), - 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 args.cdef is 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) diff --git a/src/cffi/buildtool/_gen.py b/src/cffi/buildtool/_gen.py deleted file mode 100644 index 4ad2070fb..000000000 --- a/src/cffi/buildtool/_gen.py +++ /dev/null @@ -1,58 +0,0 @@ -# Integrated from the cffi-buildtool project by Rose Davidson -# (https://github.com/inklesspen/cffi-buildtool), MIT-licensed. -import io - -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 ``"gen-cffi-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. - """ - globs = {'__name__': 'gen-cffi-src'} - _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 - - -def make_ffi_from_sources(modulename, cdef, csrc): - """Build 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() diff --git a/testing/cffi1/buildtool_examples/build_script_example/meson.build b/testing/cffi1/buildtool_examples/build_script_example/meson.build index a0c151073..2e82677f8 100644 --- a/testing/cffi1/buildtool_examples/build_script_example/meson.build +++ b/testing/cffi1/buildtool_examples/build_script_example/meson.build @@ -9,8 +9,6 @@ py = import('python').find_installation(pure: false) install_subdir('src/squared', install_dir: py.get_install_dir()) -gen_cffi_src = find_program('gen-cffi-src') - square_lib = static_library( 'square', 'src/csrc/square.c', @@ -24,7 +22,8 @@ square_dep = declare_dependency( squared_ext_src = custom_target( 'squared-cffi-src', command: [ - gen_cffi_src, + py, + '-m', 'cffi.buildtool', 'exec-python', '@INPUT@', '@OUTPUT@', diff --git a/testing/cffi1/buildtool_examples/cdef_example/meson.build b/testing/cffi1/buildtool_examples/cdef_example/meson.build index 644910478..7255314fa 100644 --- a/testing/cffi1/buildtool_examples/cdef_example/meson.build +++ b/testing/cffi1/buildtool_examples/cdef_example/meson.build @@ -9,8 +9,6 @@ py = import('python').find_installation(pure: false) install_subdir('src/squared', install_dir: py.get_install_dir()) -gen_cffi_src = find_program('gen-cffi-src') - square_lib = static_library( 'square', 'src/csrc/square.c', @@ -24,7 +22,8 @@ square_dep = declare_dependency( squared_ext_src = custom_target( 'squared-cffi-src', command: [ - gen_cffi_src, + py, + '-m', 'cffi.buildtool', 'read-sources', 'squared._squared', '@INPUT0@', diff --git a/testing/cffi1/test_buildtool.py b/testing/cffi1/test_buildtool.py index 1792060f9..93b3bc2b2 100644 --- a/testing/cffi1/test_buildtool.py +++ b/testing/cffi1/test_buildtool.py @@ -1,11 +1,17 @@ +"""Tests for the ``python -m cffi.buildtool`` command-line tool. + +The command line is the buildtool's only public interface, so these +tests drive it exclusively through subprocesses. +""" + +import subprocess +import sys + import pytest -from cffi.buildtool import ( - find_ffi_in_python_script, - generate_c_source, - make_ffi_from_sources, -) -from cffi.buildtool import _cli as buildtool_cli +pytestmark = [ + pytest.mark.thread_unsafe(reason="spawns subprocesses, slow"), +] SIMPLE_SCRIPT = """\ @@ -37,95 +43,98 @@ def something_else(): """ -def _dont_exit(_status): - pass - - -def test_find_ffi_simple(): - ffi = find_ffi_in_python_script(SIMPLE_SCRIPT, "_squared_build.py", "ffibuilder") - module_name, csrc, _source_extension, _kwds = ffi._assigned_source - assert module_name == "squared._squared" - assert csrc.strip() == '#include "square.h"' - cdef = "\n".join(ffi._cdefsources) - assert "int square" in cdef - - -def test_find_ffi_callable(): - ffi = find_ffi_in_python_script(CALLABLE_SCRIPT, "_squared_build.py", "make_ffi") - module_name, csrc, _source_extension, _kwds = ffi._assigned_source - assert module_name == "squared._squared" - assert csrc.strip() == '#include "square.h"' - cdef = "\n".join(ffi._cdefsources) - assert "int square" in cdef - - -def test_find_ffi_name_not_found(): - with pytest.raises(NameError, match="'notfound'"): - find_ffi_in_python_script(SIMPLE_SCRIPT, "_squared_build.py", "notfound") - - -def test_find_ffi_wrong_type(): - with pytest.raises(TypeError, match="not an instance of cffi.api.FFI"): - find_ffi_in_python_script(SIMPLE_SCRIPT, "_squared_build.py", "something_else") +def _run_buildtool(*args): + return subprocess.run( + [sys.executable, "-m", "cffi.buildtool", *args], + stdin=subprocess.DEVNULL, + capture_output=True, + text=True, + ) -def test_find_ffi_callable_wrong_type(): - with pytest.raises(TypeError, match="not an instance of cffi.api.FFI"): - find_ffi_in_python_script(CALLABLE_SCRIPT, "_squared_build.py", "something_else") +def test_exec_python(tmp_path): + pyfile = tmp_path / "_squared_build.py" + pyfile.write_text(SIMPLE_SCRIPT) + output = tmp_path / "out.c" + proc = _run_buildtool("exec-python", str(pyfile), str(output)) + assert proc.returncode == 0, proc.stderr + generated = output.read_text() + assert "square" in generated + # sanity check: the emitted C source is the thing meson-python will compile + assert "PyInit" in generated or "_cffi_f_" in generated -def test_make_ffi_from_sources_and_generate(): - ffi = make_ffi_from_sources( - "squared._squared", - "int square(int n);", - '#include "square.h"', +def test_exec_python_ffi_var(tmp_path): + pyfile = tmp_path / "_squared_build.py" + pyfile.write_text(CALLABLE_SCRIPT) + output = tmp_path / "out.c" + proc = _run_buildtool( + "exec-python", "--ffi-var", "make_ffi", str(pyfile), str(output) ) - c_source = generate_c_source(ffi) - assert "square" in c_source - # sanity check: the emitted C source is the thing meson-python will compile - assert "PyInit" in c_source or "_cffi_f_" in c_source + assert proc.returncode == 0, proc.stderr + assert "square" in output.read_text() -def test_cli_exec_python(tmp_path, monkeypatch): - monkeypatch.setattr(buildtool_cli.parser, "exit", _dont_exit) +def test_exec_python_name_not_found(tmp_path): pyfile = tmp_path / "_squared_build.py" pyfile.write_text(SIMPLE_SCRIPT) output = tmp_path / "out.c" - buildtool_cli.run(["exec-python", str(pyfile), str(output)]) - generated = output.read_text() - assert "square" in generated + proc = _run_buildtool( + "exec-python", "--ffi-var", "notfound", str(pyfile), str(output) + ) + assert proc.returncode != 0 + assert "NameError" in proc.stderr + assert "'notfound'" in proc.stderr -def test_cli_exec_python_ffi_var(tmp_path, monkeypatch): - monkeypatch.setattr(buildtool_cli.parser, "exit", _dont_exit) +@pytest.mark.parametrize( + "script", [SIMPLE_SCRIPT, CALLABLE_SCRIPT], ids=["simple", "callable"] +) +def test_exec_python_wrong_type(tmp_path, script): pyfile = tmp_path / "_squared_build.py" - pyfile.write_text(CALLABLE_SCRIPT) + pyfile.write_text(script) output = tmp_path / "out.c" - buildtool_cli.run(["exec-python", "--ffi-var", "make_ffi", str(pyfile), str(output)]) - generated = output.read_text() - assert "square" in generated + proc = _run_buildtool( + "exec-python", "--ffi-var", "something_else", str(pyfile), str(output) + ) + assert proc.returncode != 0 + assert "not an instance of cffi.api.FFI" in proc.stderr -def test_cli_read_sources(tmp_path, monkeypatch): - monkeypatch.setattr(buildtool_cli.parser, "exit", _dont_exit) +def test_read_sources(tmp_path): cdef = tmp_path / "squared.cdef.txt" cdef.write_text("int square(int n);\n") csrc = tmp_path / "squared.csrc.c" csrc.write_text('#include "square.h"\n') output = tmp_path / "out.c" - buildtool_cli.run([ - "read-sources", - "squared._squared", - str(cdef), - str(csrc), - str(output), - ]) + proc = _run_buildtool( + "read-sources", "squared._squared", str(cdef), str(csrc), str(output) + ) + assert proc.returncode == 0, proc.stderr generated = output.read_text() assert "square" in generated + assert "PyInit" in generated or "_cffi_f_" in generated + +def test_read_sources_same_input_fails(): + proc = _run_buildtool("read-sources", "_squared", "-", "-", "-") + assert proc.returncode != 0 + assert "are the same file and should not be" in proc.stderr -def test_cli_read_sources_same_input_fails(capsys): - with pytest.raises(SystemExit): - buildtool_cli.run(["read-sources", "_squared", "-", "-", "-"]) - _stdout, stderr = capsys.readouterr() - assert "are the same file and should not be" in stderr + +def test_no_subcommand(): + proc = _run_buildtool() + assert proc.returncode != 0 + assert "a subcommand is required" in proc.stderr + + +def test_import_is_inert(): + # importing the entry-point module must not run the CLI + proc = subprocess.run( + [sys.executable, "-c", "import cffi.buildtool"], + capture_output=True, + text=True, + ) + assert proc.returncode == 0, proc.stderr + assert proc.stdout == "" + assert proc.stderr == "" \ No newline at end of file From 6df3f77958cf4c920fd33ddca6f100e793a6c0eb Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 9 Jun 2026 13:57:40 -0600 Subject: [PATCH 6/8] Respond to code review comments --- doc/source/buildtool.rst | 224 +++++++++--------- doc/source/cdef.rst | 13 +- .../build_script_example/meson.build | 2 +- .../cdef_example/meson.build | 2 +- 4 files changed, 128 insertions(+), 113 deletions(-) diff --git a/doc/source/buildtool.rst b/doc/source/buildtool.rst index 95036da25..adefbe839 100644 --- a/doc/source/buildtool.rst +++ b/doc/source/buildtool.rst @@ -9,48 +9,53 @@ Building and Distributing CFFI Extensions CFFI ships a command-line tool, invoked as ``python -m cffi.buildtool``, that produces the same output as :meth:`FFI.emit_c_code`: a ``.c`` source file ready to be compiled into -a CPython extension module. It adds two convenient front-ends --- one that executes an existing "build" Python script, and one that -reads a ``cdef`` and C prelude from two files. This tool enables +a CPython extension module. This tool enables integrating with any build backend, such as `meson-python `_, `scikit-build-core `_, or similar. -The rest of this page uses meson-python in the examples, but any PEP -517 backend that lets you run a helper program during the build can +The rest of this page uses meson-python in the examples, but any `Python build +backend`_ that lets you run a helper program during the build can drive ``python -m cffi.buildtool`` the same way. -The command line is the buildtool's only public interface; the -implementation inside the ``cffi`` package is private. The buildtool -was vendored with permission from the `cffi-buildtool`_ project by Rose -Davidson (@inklesspen on GitHub). +The only way to use the buildtool functionality is via ``python -m +cffi.buildtool``; the implementation inside the ``cffi`` package is +private. -.. _cffi-buildtool: https://github.com/inklesspen/cffi-buildtool +The implementation is based on the `cffi-buildtool`_ project by Rose Davidson +(`@inklesspen`_ on GitHub). It is included in CFFI with permission of the original +author. +.. _Python build backend: https://packaging.python.org/en/latest/guides/tool-recommendations/#build-backends-for-extension-modules +.. _cffi-buildtool: https://github.com/inklesspen/cffi-buildtool +.. _@inklesspen: https://github.com/inklesspen The ``python -m cffi.buildtool`` Command-line Tool ================================================== -``python -m cffi.buildtool`` has two subcommands. In both, the final -positional argument is the path to the ``.c`` file to generate. +``python -m cffi.buildtool`` has two subcommands. The first, ``exec-python`` is +most useful if you already have a Python script that sets up an FFI +definition. The second, ``read-sources`` is most useful if you are wrapping +a large API surface and want a more structured way to specify a set of FFI +definitions. -.. note:: +``python -m cffi.buildtool exec-python`` +---------------------------------------- - When you drive the build from a build backend, the - ``libraries=``, ``library_dirs=``, ``include_dirs=``, - ``extra_compile_args=`` etc. arguments you pass to - :meth:`FFI.set_source` are *ignored*. Link and include settings are - the build backend's responsibility; for meson-python you express - them through the ``dependencies:`` / ``include_directories:`` - arguments of ``py.extension_module()``. +This mode takes a Python script that dynamically defines an FFI interface and +accompanying C extension source code. The FFI definition script is the same +script you would normally run by hand -- the one the CFFI docs show under +:ref:`real-example`. +Let's say we want to create an extension module that wraps a single C function named +``square``. The ``square`` function has the following signature: -``python -m cffi.buildtool exec-python`` ----------------------------------------- +.. code-block:: C + + int square(int n); -This mode takes the Python build script you would normally run by -hand -- the one the CFFI docs show under "Main mode of usage" -- and -generates the ``.c`` source for you. For example, given this +Let's also say this function definition is exposed inside a header named +`square.h`. We could create a set of FFI bindings for this function given this ``_squared_build.py``:: from cffi import FFI @@ -64,33 +69,53 @@ generates the ``.c`` source for you. For example, given this '#include "square.h"', ) - if __name__ == "__main__": - ffibuilder.compile(verbose=True) - -you run: +To generate the source code for the C extension, you would run: .. code-block:: console $ python -m cffi.buildtool exec-python _squared_build.py _squared.c -The script is executed with ``__name__`` set to ``"cffi.buildtool"``, -so the trailing ``if __name__ == "__main__":`` block is skipped: the -tool only generates the C source and never compiles it. +Many CFFI build scripts have an ``if __name__ == "__main__"`` section +that triggers a compilation step. This is not needed for a +``cffi.buildtool`` script, which does not generate compiled artifacts, +only C source code. It is up to your build-backend of choice +(e.g. meson-python) to run a C compiler and build compiled artifacts. +If the script does have such a section it is harmless: the script is +executed with ``__name__`` set to ``"cffi.buildtool"``, so the block is +skipped and an existing build script works unchanged. + +If the :class:`cffi.FFI` is bound to a name other than ``ffibuilder``, pass +``--ffi-var``. To make that concrete, let's say your build script creates an FFI +object named ``make_ffi``:: + + from cffi import FFI -If the :class:`cffi.FFI` is bound to a name other than ``ffibuilder``, -pass ``--ffi-var``: + make_ffi = FFI() + +In that case, you would pass ``--ffi-var=make_ffi`` to ``cffi.buildtool``: .. code-block:: console $ python -m cffi.buildtool exec-python --ffi-var=make_ffi _squared_build.py _squared.c +.. note:: + + CFFI's setuptools integration supports passing ``libraries=``, + ``library_dirs=``, ``include_dirs=``, and ``extra_compile_args=`` + arguments to :meth:`FFI.set_source`. When using `cffi.buildtool`, + these arguments are *ignored*. Link and include settings are the + build backend's responsibility; for meson-python you would express + them through the ``dependencies``, ``include_directories``, and + ``c_args`` arguments of ``py.extension_module()``. + ``python -m cffi.buildtool read-sources`` ----------------------------------------- -For larger modules, keeping the ``cdef`` and the C source prelude in -separate files tends to be easier to work with -- your editor -treats them as plain C, and presubmit tooling doesn't have to parse -them out of a string literal. +For larger modules, keeping the FFI definition and any necessary C +source prelude in separate files tends to be easier to work with -- +you can configure your editor to treat them as plain C, and write +presubmit tooling that parses the FFI definition directly without +extracting it from a Python script. Given ``squared.cdef.txt``: @@ -104,15 +129,17 @@ and ``squared.csrc.c``: #include "square.h" -you run: +you would run the following command to generate a CFFI extension: .. code-block:: console $ python -m cffi.buildtool read-sources squared._squared squared.cdef.txt squared.csrc.c _squared.c -The first positional argument is the fully qualified module name that -will be embedded in the generated source (equivalent to the first -argument to :meth:`FFI.set_source`). +With all other details left exactly the same as the ``exec-python`` example. + +The first positional argument passed to the ``read-sources`` command is the +fully qualified module name that will be embedded in the generated C source +code (equivalent to the first argument to :meth:`FFI.set_source`). A Worked Example Using ``meson-python`` @@ -135,78 +162,36 @@ Project layout: ``pyproject.toml``: -.. code-block:: toml - - [build-system] - build-backend = 'mesonpy' - requires = ['meson-python', 'cffi'] - - [project] - name = 'squared' - version = '0.1.0' - requires-python = '>=3.9' - dependencies = ['cffi'] +.. literalinclude:: ../../testing/cffi1/buildtool_examples/build_script_example/pyproject.toml + :language: toml ``meson.build``: -.. code-block:: meson - - project( - 'squared', - 'c', - version: '0.1.0', - ) - - py = import('python').find_installation(pure: false) - - install_subdir('src/squared', install_dir: py.get_install_dir()) +.. literalinclude:: ../../testing/cffi1/buildtool_examples/build_script_example/meson.build + :language: meson - square_lib = static_library( - 'square', - 'src/csrc/square.c', - include_directories: include_directories('src/csrc'), - ) - square_dep = declare_dependency( - link_with: square_lib, - include_directories: include_directories('src/csrc'), - ) - - squared_ext_src = custom_target( - 'squared-cffi-src', - command: [ - py, - '-m', 'cffi.buildtool', - 'exec-python', - '@INPUT@', - '@OUTPUT@', - ], - output: '_squared.c', - input: ['src/squared/_squared_build.py'], - ) +``src/squared/__init__.py``: - py.extension_module( - '_squared', - squared_ext_src, - subdir: 'squared', - install: true, - dependencies: [square_dep, py.dependency()], - ) +.. literalinclude:: ../../testing/cffi1/buildtool_examples/build_script_example/src/squared/__init__.py + :language: python -``src/squared/__init__.py``: +``src/squared/_squared_build.py``: -.. code-block:: python +.. literalinclude:: ../../testing/cffi1/buildtool_examples/build_script_example/src/squared/_squared_build.py + :language: python - from ._squared import ffi, lib +``src/csrc/square.h``: +.. literalinclude:: ../../testing/cffi1/buildtool_examples/build_script_example/src/csrc/square.h + :language: C - def squared(n): - return lib.square(n) +``src/csrc/square.c``: -``src/squared/_squared_build.py``, ``src/csrc/square.h`` and -``src/csrc/square.c`` contain the snippets shown above. +.. literalinclude:: ../../testing/cffi1/buildtool_examples/build_script_example/src/csrc/square.c + :language: C -Build and install the project with any PEP 517 front-end. For -example: +Build and install the project with any Python build front-end. For +example, with `pip`, in the root `squared` directory: .. code-block:: console @@ -215,8 +200,35 @@ example: 49 To switch this project to ``read-sources`` mode, replace -``_squared_build.py`` with two files (``_squared.cdef.txt`` and -``_squared.csrc.c``), then change the ``custom_target`` command to: +``_squared_build.py`` with two files, so that the project layout +becomes: + +.. code-block:: text + + squared/ + ├── pyproject.toml + ├── meson.build + └── src/ + ├── squared/ + │ ├── __init__.py + │ ├── squared.cdef.txt + │ └── squared.csrc.c + └── csrc/ + ├── square.h + └── square.c + +The first new file, ``squared.cdef.txt``, contains the FFI definition: + +.. literalinclude:: ../../testing/cffi1/buildtool_examples/cdef_example/src/squared/squared.cdef.txt + :language: python + +and the second, ``squared.csrc.c``, contains the C source prelude: + +.. literalinclude:: ../../testing/cffi1/buildtool_examples/cdef_example/src/squared/squared.csrc.c + :language: python + +then change two spots in the ``meson.build`` file. First, update the ``custom_target`` +``command`` to call ``python -m cffi.buildtool read-sources`` with two input arguments: .. code-block:: meson @@ -230,11 +242,11 @@ To switch this project to ``read-sources`` mode, replace '@OUTPUT@', ], -and list both files under ``input:``: +and then list both of the FFI specification files under ``input``: .. code-block:: meson - input: ['src/squared/_squared.cdef.txt', '_squared.csrc.c'] + input: ['src/squared/squared.cdef.txt', 'src/squared/squared.csrc.c'] Distributing CFFI Extensions using Setuptools ============================================= diff --git a/doc/source/cdef.rst b/doc/source/cdef.rst index 9a18dc9a8..6e028774a 100644 --- a/doc/source/cdef.rst +++ b/doc/source/cdef.rst @@ -6,9 +6,12 @@ Preparing Wrapper Modules .. note:: - This covers how to create wrapper modules. See :ref:`buildtool_docs` - for instructions on how to integrate with a Python build backend and - distribute wrapper modules. + A *wrapper module* is a Python module that uses CFFI to expose + functions and data from a C library, so that the rest of your + program can import it and call the library through the ``ffi`` and + ``lib`` objects. This page covers how to create wrapper modules. + See :ref:`buildtool_docs` for instructions on how to integrate with + a Python build backend and distribute wrapper modules. There are three or four different ways to use CFFI in a project. In order of complexity: @@ -855,7 +858,7 @@ steps. and *if* the "stuff" part is big enough that import time is a concern, then rewrite it as described in `the out-of-line but still ABI mode`__ -above. Optionally, see also the :ref:`build backend and distrubution +above. Optionally, see also the :ref:`build backend and distribution ` documentation. .. __: out-of-line-abi_ @@ -874,7 +877,7 @@ above. Optionally, see also the :ref:`build backend and distrubution then you should really rewrite it as described in `the out-of-line, API mode`__ above. It avoids a number of issues that have caused ``ffi.verify()`` to grow a number of extra arguments over time. Then -see the :ref:`build backend and distrubution ` +see the :ref:`build backend and distribution ` documentation. Also, remember to remove the ``ext_package=".."`` from your ``setup.py``, which was sometimes needed with ``verify()`` but is just creating confusion with ``set_source()``. diff --git a/testing/cffi1/buildtool_examples/build_script_example/meson.build b/testing/cffi1/buildtool_examples/build_script_example/meson.build index 2e82677f8..fe02ff281 100644 --- a/testing/cffi1/buildtool_examples/build_script_example/meson.build +++ b/testing/cffi1/buildtool_examples/build_script_example/meson.build @@ -37,5 +37,5 @@ py.extension_module( squared_ext_src, subdir: 'squared', install: true, - dependencies: [square_dep, py.dependency()], + dependencies: [square_dep], ) diff --git a/testing/cffi1/buildtool_examples/cdef_example/meson.build b/testing/cffi1/buildtool_examples/cdef_example/meson.build index 7255314fa..c3a3483fa 100644 --- a/testing/cffi1/buildtool_examples/cdef_example/meson.build +++ b/testing/cffi1/buildtool_examples/cdef_example/meson.build @@ -39,5 +39,5 @@ py.extension_module( squared_ext_src, subdir: 'squared', install: true, - dependencies: [square_dep, py.dependency()], + dependencies: [square_dep], ) From f8e2c2e4f381b563711ffb3f28329f6593fe6990 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 9 Jun 2026 14:16:29 -0600 Subject: [PATCH 7/8] stop referring to 'build scripts' --- doc/source/buildtool.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/source/buildtool.rst b/doc/source/buildtool.rst index adefbe839..7234eda90 100644 --- a/doc/source/buildtool.rst +++ b/doc/source/buildtool.rst @@ -75,18 +75,18 @@ To generate the source code for the C extension, you would run: $ python -m cffi.buildtool exec-python _squared_build.py _squared.c -Many CFFI build scripts have an ``if __name__ == "__main__"`` section +Many CFFI FFI definition scripts have an ``if __name__ == "__main__"`` section that triggers a compilation step. This is not needed for a ``cffi.buildtool`` script, which does not generate compiled artifacts, only C source code. It is up to your build-backend of choice (e.g. meson-python) to run a C compiler and build compiled artifacts. If the script does have such a section it is harmless: the script is executed with ``__name__`` set to ``"cffi.buildtool"``, so the block is -skipped and an existing build script works unchanged. +skipped and an existing FFI definition script works unchanged. If the :class:`cffi.FFI` is bound to a name other than ``ffibuilder``, pass -``--ffi-var``. To make that concrete, let's say your build script creates an FFI -object named ``make_ffi``:: +``--ffi-var``. To make that concrete, let's say your FFI definition script +creates an FFI object named ``make_ffi``:: from cffi import FFI From 50a29be3abbe18e73cfe7fc9c424e1a14f227aa0 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 10 Jun 2026 15:12:19 -0600 Subject: [PATCH 8/8] Restore the console script --- doc/source/buildtool.rst | 53 ++++++------ doc/source/whatsnew.rst | 10 +-- pyproject.toml | 3 + src/cffi/_buildtool.py | 15 +++- .../build_script_example/meson.build | 5 +- .../cdef_example/meson.build | 5 +- testing/cffi1/test_buildtool.py | 82 +++++++++++++++---- testing/cffi1/test_buildtool_meson.py | 16 +++- 8 files changed, 132 insertions(+), 57 deletions(-) diff --git a/doc/source/buildtool.rst b/doc/source/buildtool.rst index 7234eda90..1f696b279 100644 --- a/doc/source/buildtool.rst +++ b/doc/source/buildtool.rst @@ -6,21 +6,25 @@ Building and Distributing CFFI Extensions .. contents:: -CFFI ships a command-line tool, invoked as ``python -m -cffi.buildtool``, that produces the same output as -:meth:`FFI.emit_c_code`: a ``.c`` source file ready to be compiled into -a CPython extension module. This tool enables +CFFI ships a command-line tool, ``gen-cffi-src``, that produces the +same output as :meth:`FFI.emit_c_code`: a ``.c`` source file ready to +be compiled into a CPython extension module. This tool enables integrating with any build backend, such as `meson-python `_, `scikit-build-core `_, or similar. +Installing CFFI installs the ``gen-cffi-src`` script; running ``python +-m cffi.buildtool`` invokes the same command line and behaves +identically. Use the former where a script with a Python shebang +makes more sense (e.g. cross-compiling) and use the latter when the +console script is not on PATH but a Python interpreter is. + The rest of this page uses meson-python in the examples, but any `Python build backend`_ that lets you run a helper program during the build can -drive ``python -m cffi.buildtool`` the same way. +drive ``gen-cffi-src`` the same way. -The only way to use the buildtool functionality is via ``python -m -cffi.buildtool``; the implementation inside the ``cffi`` package is -private. +The only way to use the buildtool functionality is via this command +line; the implementation inside the ``cffi`` package is private. The implementation is based on the `cffi-buildtool`_ project by Rose Davidson (`@inklesspen`_ on GitHub). It is included in CFFI with permission of the original @@ -30,17 +34,17 @@ author. .. _cffi-buildtool: https://github.com/inklesspen/cffi-buildtool .. _@inklesspen: https://github.com/inklesspen -The ``python -m cffi.buildtool`` Command-line Tool -================================================== +The ``gen-cffi-src`` Command-line Tool +====================================== -``python -m cffi.buildtool`` has two subcommands. The first, ``exec-python`` is +``gen-cffi-src`` has two subcommands. The first, ``exec-python`` is most useful if you already have a Python script that sets up an FFI definition. The second, ``read-sources`` is most useful if you are wrapping a large API surface and want a more structured way to specify a set of FFI definitions. -``python -m cffi.buildtool exec-python`` ----------------------------------------- +``gen-cffi-src exec-python`` +---------------------------- This mode takes a Python script that dynamically defines an FFI interface and accompanying C extension source code. The FFI definition script is the same @@ -73,11 +77,11 @@ To generate the source code for the C extension, you would run: .. code-block:: console - $ python -m cffi.buildtool exec-python _squared_build.py _squared.c + $ gen-cffi-src exec-python _squared_build.py _squared.c Many CFFI FFI definition scripts have an ``if __name__ == "__main__"`` section -that triggers a compilation step. This is not needed for a -``cffi.buildtool`` script, which does not generate compiled artifacts, +that triggers a compilation step. This is not needed for a script run +by ``gen-cffi-src``, which does not generate compiled artifacts, only C source code. It is up to your build-backend of choice (e.g. meson-python) to run a C compiler and build compiled artifacts. If the script does have such a section it is harmless: the script is @@ -92,24 +96,24 @@ creates an FFI object named ``make_ffi``:: make_ffi = FFI() -In that case, you would pass ``--ffi-var=make_ffi`` to ``cffi.buildtool``: +In that case, you would pass ``--ffi-var=make_ffi`` to ``gen-cffi-src``: .. code-block:: console - $ python -m cffi.buildtool exec-python --ffi-var=make_ffi _squared_build.py _squared.c + $ gen-cffi-src exec-python --ffi-var=make_ffi _squared_build.py _squared.c .. note:: CFFI's setuptools integration supports passing ``libraries=``, ``library_dirs=``, ``include_dirs=``, and ``extra_compile_args=`` - arguments to :meth:`FFI.set_source`. When using `cffi.buildtool`, + arguments to :meth:`FFI.set_source`. When using ``gen-cffi-src``, these arguments are *ignored*. Link and include settings are the build backend's responsibility; for meson-python you would express them through the ``dependencies``, ``include_directories``, and ``c_args`` arguments of ``py.extension_module()``. -``python -m cffi.buildtool read-sources`` ------------------------------------------ +``gen-cffi-src read-sources`` +----------------------------- For larger modules, keeping the FFI definition and any necessary C source prelude in separate files tends to be easier to work with -- @@ -133,7 +137,7 @@ you would run the following command to generate a CFFI extension: .. code-block:: console - $ python -m cffi.buildtool read-sources squared._squared squared.cdef.txt squared.csrc.c _squared.c + $ gen-cffi-src read-sources squared._squared squared.cdef.txt squared.csrc.c _squared.c With all other details left exactly the same as the ``exec-python`` example. @@ -228,13 +232,12 @@ and the second, ``squared.csrc.c``, contains the C source prelude: :language: python then change two spots in the ``meson.build`` file. First, update the ``custom_target`` -``command`` to call ``python -m cffi.buildtool read-sources`` with two input arguments: +``command`` to call ``gen-cffi-src read-sources`` with two input arguments: .. code-block:: meson command: [ - py, - '-m', 'cffi.buildtool', + gen_cffi_src, 'read-sources', 'squared._squared', '@INPUT0@', diff --git a/doc/source/whatsnew.rst b/doc/source/whatsnew.rst index a73899535..bb19e312e 100644 --- a/doc/source/whatsnew.rst +++ b/doc/source/whatsnew.rst @@ -10,11 +10,11 @@ v2.0.0.dev0 with the limited API, so you must set py_limited_api=False when building extensions for the free-threaded build. * Added support for Python 3.14. (`#177`_) -* Added the ``python -m cffi.buildtool`` command-line tool, which lets - build backends such as `meson-python`_ generate CFFI extension C - source without depending on setuptools. See :doc:`buildtool` for - details. Integrated from the `cffi-buildtool`_ project by Rose - Davidson. +* Added the ``gen-cffi-src`` command-line tool (also invocable as + ``python -m cffi.buildtool``), which lets build backends such as + `meson-python`_ generate CFFI extension C source without depending + on setuptools. See :doc:`buildtool` for details. Integrated from the + `cffi-buildtool`_ project by Rose Davidson. * WIP .. _`meson-python`: https://meson-python.readthedocs.io/ diff --git a/pyproject.toml b/pyproject.toml index 447621fb0..33ab91eec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,9 @@ maintainers = [ [project.entry-points."distutils.setup_keywords"] cffi_modules = "cffi.setuptools_ext:cffi_modules" +[project.scripts] +gen-cffi-src = "cffi._buildtool:run" + [project.urls] Documentation = "https://cffi.readthedocs.io/" Changelog = "https://cffi.readthedocs.io/en/latest/whatsnew.html" diff --git a/src/cffi/_buildtool.py b/src/cffi/_buildtool.py index c1e251f87..85e3f34d8 100644 --- a/src/cffi/_buildtool.py +++ b/src/cffi/_buildtool.py @@ -25,7 +25,7 @@ # 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 ``python -m cffi.buildtool`` command-line tool. +"""Implementation of the ``gen-cffi-src`` command-line tool. This module is private; the command line is the only supported interface. Two subcommands: @@ -42,6 +42,8 @@ import argparse import io +import os +import sys from .api import FFI @@ -117,8 +119,17 @@ def read_sources(*, output, module_name, cdef_input, csrc_input): output.write(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('gen-cffi-src'): + return 'gen-cffi-src' + return 'python -m cffi.buildtool' + + parser = argparse.ArgumentParser( - prog='python -m cffi.buildtool', + prog=_prog(), description='Generate CFFI C source for a build backend (e.g. meson-python).', ) subparsers = parser.add_subparsers(dest='mode') diff --git a/testing/cffi1/buildtool_examples/build_script_example/meson.build b/testing/cffi1/buildtool_examples/build_script_example/meson.build index fe02ff281..b0e249ef0 100644 --- a/testing/cffi1/buildtool_examples/build_script_example/meson.build +++ b/testing/cffi1/buildtool_examples/build_script_example/meson.build @@ -9,6 +9,8 @@ py = import('python').find_installation(pure: false) install_subdir('src/squared', install_dir: py.get_install_dir()) +gen_cffi_src = find_program('gen-cffi-src') + square_lib = static_library( 'square', 'src/csrc/square.c', @@ -22,8 +24,7 @@ square_dep = declare_dependency( squared_ext_src = custom_target( 'squared-cffi-src', command: [ - py, - '-m', 'cffi.buildtool', + gen_cffi_src, 'exec-python', '@INPUT@', '@OUTPUT@', diff --git a/testing/cffi1/buildtool_examples/cdef_example/meson.build b/testing/cffi1/buildtool_examples/cdef_example/meson.build index c3a3483fa..b9486b174 100644 --- a/testing/cffi1/buildtool_examples/cdef_example/meson.build +++ b/testing/cffi1/buildtool_examples/cdef_example/meson.build @@ -9,6 +9,8 @@ py = import('python').find_installation(pure: false) install_subdir('src/squared', install_dir: py.get_install_dir()) +gen_cffi_src = find_program('gen-cffi-src') + square_lib = static_library( 'square', 'src/csrc/square.c', @@ -22,8 +24,7 @@ square_dep = declare_dependency( squared_ext_src = custom_target( 'squared-cffi-src', command: [ - py, - '-m', 'cffi.buildtool', + gen_cffi_src, 'read-sources', 'squared._squared', '@INPUT0@', diff --git a/testing/cffi1/test_buildtool.py b/testing/cffi1/test_buildtool.py index 93b3bc2b2..72f08417e 100644 --- a/testing/cffi1/test_buildtool.py +++ b/testing/cffi1/test_buildtool.py @@ -1,11 +1,14 @@ -"""Tests for the ``python -m cffi.buildtool`` command-line tool. +"""Tests for the buildtool command-line tool. The command line is the buildtool's only public interface, so these tests drive it exclusively through subprocesses. """ +import os +import shutil import subprocess import sys +import sysconfig import pytest @@ -43,20 +46,48 @@ def something_else(): """ -def _run_buildtool(*args): +def _gen_cffi_src_path(): + """Locate the installed ``gen-cffi-src`` script, or return None.""" + exe = "gen-cffi-src" + (".exe" if sys.platform == "win32" else "") + candidate = os.path.join(sysconfig.get_path("scripts"), exe) + if os.path.exists(candidate): + return candidate + return shutil.which("gen-cffi-src") + + +def _run(argv, *args): return subprocess.run( - [sys.executable, "-m", "cffi.buildtool", *args], + [*argv, *args], stdin=subprocess.DEVNULL, capture_output=True, text=True, ) -def test_exec_python(tmp_path): +# `gen-cffi-src` can also be invoked via `python -m cffi.buildtool`. +# This fixture enables testing both invocations. +@pytest.fixture(params=["module", "script"]) +def run_buildtool(request): + """Run the buildtool CLI in a subprocess, via both invocations.""" + if request.param == "script": + script = _gen_cffi_src_path() + if script is None: + pytest.skip("the gen-cffi-src script is not installed") + argv = [script] + else: + argv = [sys.executable, "-m", "cffi.buildtool"] + + def run(*args): + return _run(argv, *args) + + return run + + +def test_exec_python(tmp_path, run_buildtool): pyfile = tmp_path / "_squared_build.py" pyfile.write_text(SIMPLE_SCRIPT) output = tmp_path / "out.c" - proc = _run_buildtool("exec-python", str(pyfile), str(output)) + proc = run_buildtool("exec-python", str(pyfile), str(output)) assert proc.returncode == 0, proc.stderr generated = output.read_text() assert "square" in generated @@ -64,22 +95,22 @@ def test_exec_python(tmp_path): assert "PyInit" in generated or "_cffi_f_" in generated -def test_exec_python_ffi_var(tmp_path): +def test_exec_python_ffi_var(tmp_path, run_buildtool): pyfile = tmp_path / "_squared_build.py" pyfile.write_text(CALLABLE_SCRIPT) output = tmp_path / "out.c" - proc = _run_buildtool( + proc = run_buildtool( "exec-python", "--ffi-var", "make_ffi", str(pyfile), str(output) ) assert proc.returncode == 0, proc.stderr assert "square" in output.read_text() -def test_exec_python_name_not_found(tmp_path): +def test_exec_python_name_not_found(tmp_path, run_buildtool): pyfile = tmp_path / "_squared_build.py" pyfile.write_text(SIMPLE_SCRIPT) output = tmp_path / "out.c" - proc = _run_buildtool( + proc = run_buildtool( "exec-python", "--ffi-var", "notfound", str(pyfile), str(output) ) assert proc.returncode != 0 @@ -90,24 +121,24 @@ def test_exec_python_name_not_found(tmp_path): @pytest.mark.parametrize( "script", [SIMPLE_SCRIPT, CALLABLE_SCRIPT], ids=["simple", "callable"] ) -def test_exec_python_wrong_type(tmp_path, script): +def test_exec_python_wrong_type(tmp_path, run_buildtool, script): pyfile = tmp_path / "_squared_build.py" pyfile.write_text(script) output = tmp_path / "out.c" - proc = _run_buildtool( + proc = run_buildtool( "exec-python", "--ffi-var", "something_else", str(pyfile), str(output) ) assert proc.returncode != 0 assert "not an instance of cffi.api.FFI" in proc.stderr -def test_read_sources(tmp_path): +def test_read_sources(tmp_path, run_buildtool): cdef = tmp_path / "squared.cdef.txt" cdef.write_text("int square(int n);\n") csrc = tmp_path / "squared.csrc.c" csrc.write_text('#include "square.h"\n') output = tmp_path / "out.c" - proc = _run_buildtool( + proc = run_buildtool( "read-sources", "squared._squared", str(cdef), str(csrc), str(output) ) assert proc.returncode == 0, proc.stderr @@ -116,18 +147,33 @@ def test_read_sources(tmp_path): assert "PyInit" in generated or "_cffi_f_" in generated -def test_read_sources_same_input_fails(): - proc = _run_buildtool("read-sources", "_squared", "-", "-", "-") +def test_read_sources_same_input_fails(run_buildtool): + proc = run_buildtool("read-sources", "_squared", "-", "-", "-") assert proc.returncode != 0 assert "are the same file and should not be" in proc.stderr -def test_no_subcommand(): - proc = _run_buildtool() +def test_no_subcommand(run_buildtool): + proc = run_buildtool() assert proc.returncode != 0 assert "a subcommand is required" in proc.stderr +def test_module_help_names_module_invocation(): + proc = _run([sys.executable, "-m", "cffi.buildtool"], "--help") + assert proc.returncode == 0, proc.stderr + assert proc.stdout.startswith("usage: python -m cffi.buildtool") + + +def test_script_help_names_script_invocation(): + script = _gen_cffi_src_path() + if script is None: + pytest.skip("the gen-cffi-src script is not installed") + proc = _run([script], "--help") + assert proc.returncode == 0, proc.stderr + assert proc.stdout.startswith("usage: gen-cffi-src") + + def test_import_is_inert(): # importing the entry-point module must not run the CLI proc = subprocess.run( @@ -137,4 +183,4 @@ def test_import_is_inert(): ) assert proc.returncode == 0, proc.stderr assert proc.stdout == "" - assert proc.stderr == "" \ No newline at end of file + assert proc.stderr == "" diff --git a/testing/cffi1/test_buildtool_meson.py b/testing/cffi1/test_buildtool_meson.py index 4e426c2fa..8f2937beb 100644 --- a/testing/cffi1/test_buildtool_meson.py +++ b/testing/cffi1/test_buildtool_meson.py @@ -9,6 +9,7 @@ """ import os +import re import shutil import subprocess import sys @@ -63,11 +64,20 @@ def test_meson_python_build(tmp_path, project): project_dir = tmp_path / "project" shutil.copytree(project, project_dir) + # The example meson.build files locate the codegen tool with + # find_program('gen-cffi-src'), which searches PATH. pip only puts + # an environment's scripts directory on PATH for isolated builds, + # so with --no-build-isolation the nested venv's script must be + # made findable by hand. + env = os.environ.copy() + env["PATH"] = str(venv_python.parent) + os.pathsep + env.get("PATH", "") + # --no-build-isolation to ensure the test runs against the CFFI build we want to test - subprocess.check_call([ - str(venv_python), "-m", "pip", "install", + proc = subprocess.run([ + str(venv_python), "-m", "pip", "install", "-v", "--no-build-isolation", str(project_dir), - ]) + ], env=env, capture_output=True, text=True) + assert proc.returncode == 0, proc.stdout + proc.stderr # Confirm the built extension imports and behaves as expected. subprocess.check_call([