From bfe4f189327a07303307ba686680954e6b84c6b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afonso=20Janu=C3=A1rio?= Date: Sun, 30 Aug 2026 01:08:23 +0100 Subject: [PATCH 1/2] Use get_ext_fullpath instead of get_outputs in ffiplatform._build get_outputs() returns every output the build_ext command produced, not just the one we asked it to build. When the Distribution/build_ext state ends up carrying more than one extension (as reported in #246, where the caller's own project already has other native extensions registered), the unconditional [soname] = cmd_obj.get_outputs() unpacking crashes with a bare ValueError instead of naming the extension we actually wanted. get_ext_fullpath(ext.name) asks build_ext directly for the path of the specific extension we passed in, so it no longer depends on exactly one output existing. This also gives a clearer failure mode for #229, where a beginner hit the same unpacking crash with no way to tell what went wrong. --- src/cffi/ffiplatform.py | 2 +- testing/cffi0/test_platform.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/cffi/ffiplatform.py b/src/cffi/ffiplatform.py index adca28f1..3007f246 100644 --- a/src/cffi/ffiplatform.py +++ b/src/cffi/ffiplatform.py @@ -47,7 +47,7 @@ def _build(tmpdir, ext, compiler_verbose=0, debug=None): set_verbosity(compiler_verbose) dist.run_command('build_ext') cmd_obj = dist.get_command_obj('build_ext') - [soname] = cmd_obj.get_outputs() + soname = cmd_obj.get_ext_fullpath(ext.name) finally: set_threshold(old_level) except (CompileError, LinkError) as e: diff --git a/testing/cffi0/test_platform.py b/testing/cffi0/test_platform.py index 55446ec3..2826d2c2 100644 --- a/testing/cffi0/test_platform.py +++ b/testing/cffi0/test_platform.py @@ -1,4 +1,7 @@ +import binascii import os +import pytest +from cffi import FFI from cffi.ffiplatform import maybe_relative_path, flatten @@ -23,3 +26,33 @@ def test_flatten(): assert flatten([4, 5]) == "2l4i5i" assert flatten({4: 5}) == "1d4i5i" assert flatten({"foo": ("bar", "baaz")}) == "1d3sfoo2l3sbar4sbaaz" + +@pytest.mark.thread_unsafe(reason="monkeypatches a shared distutils class method") +def test_compile_with_extra_build_ext_outputs(monkeypatch): + # Some setuptools/distutils versions can make build_ext.get_outputs() + # return more entries than the single extension we asked it to build + # (see https://github.com/python-cffi/cffi/issues/246, which is the + # same underlying unpacking crash reported in + # https://github.com/python-cffi/cffi/issues/229). Unpacking that list + # unconditionally used to raise a confusing + # "ValueError: too many values to unpack". + from cffi._shimmed_dist_utils import build_ext as real_build_ext + + original_get_outputs = real_build_ext.get_outputs + + def get_outputs_with_extra_entry(self): + return list(original_get_outputs(self)) + ['/nonexistent/other.so'] + + monkeypatch.setattr(real_build_ext, 'get_outputs', + get_outputs_with_extra_entry) + + # force a fresh module name/compile every run, so the monkeypatched + # get_outputs() above is actually exercised instead of reusing a + # previously-built module cached under the same checksum-derived name + tag = binascii.hexlify(os.urandom(8)).decode() + + ffi = FFI() + ffi.cdef("double test_platform_extra_outputs(double x);") + csrc = "double test_platform_extra_outputs(double x) { return x + 1.0; }" + lib = ffi.verify(csrc, tag=tag) + assert lib.test_platform_extra_outputs(41.0) == 42.0 From 81d1c53f630a37a2ff2e96d83ef4cd1109d5144a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afonso=20Janu=C3=A1rio?= Date: Tue, 1 Sep 2026 09:57:42 +0100 Subject: [PATCH 2/2] Use os.urandom().hex() instead of binascii for the test tag Simpler, and drops the now-unneeded binascii import. --- testing/cffi0/test_platform.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/testing/cffi0/test_platform.py b/testing/cffi0/test_platform.py index 2826d2c2..ed8c0fcd 100644 --- a/testing/cffi0/test_platform.py +++ b/testing/cffi0/test_platform.py @@ -1,4 +1,3 @@ -import binascii import os import pytest from cffi import FFI @@ -49,7 +48,7 @@ def get_outputs_with_extra_entry(self): # force a fresh module name/compile every run, so the monkeypatched # get_outputs() above is actually exercised instead of reusing a # previously-built module cached under the same checksum-derived name - tag = binascii.hexlify(os.urandom(8)).decode() + tag = os.urandom(8).hex() ffi = FFI() ffi.cdef("double test_platform_extra_outputs(double x);")