From adac26e92bae52b9164b4ef813b3d5082a3dfff7 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 15 Oct 2026 19:22:05 +0200 Subject: [PATCH 1/2] Use the shared libpython of conda-forge Pythons conda-forge links the python executable statically, so sysconfig reports LDLIBRARY=libpython3.X.a and Py_ENABLE_SHARED=0, but it ships libpython3.X.so in LIBDIR too. The bridge now uses that library and raises the error for a static Python only when there is none. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- docs/requirements.rst | 2 ++ release_notes/news.d/34.feature.md | 1 + src/vunit_python_bridge/native_library.py | 8 ++++++- tests/test_python_bridge.py | 28 +++++++++++++++++++---- 5 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 release_notes/news.d/34.feature.md diff --git a/README.md b/README.md index e7d6582..ad88b08 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ check(call_integer_vector("gain", arg(integer_vector'(1, 2))) = integer_vector'( - VUnit 5.0.0.dev12 or later (`vunit_hdl` on PyPI), which pip installs with the package. - VHDL-2008 or later. - CPython 3.10 or later, standard (GIL) build, with a shared `libpython` (`--enable-shared`), which - is what distribution Pythons, `actions/setup-python`, `uv` and `pyenv` provide by default. + is what distribution Pythons, `actions/setup-python`, `uv`, `pyenv` and conda-forge provide by default. - Linux and macOS: a C compiler (`cc`, `gcc` or `clang`, or `CC`) and the Python development headers (for example the `python3-dev` package). The bridge library is compiled on first use and cached. - Windows: a 64-bit CPython from python.org (or compatible). The package ships prebuilt DLLs diff --git a/docs/requirements.rst b/docs/requirements.rst index cbd520a..b271469 100644 --- a/docs/requirements.rst +++ b/docs/requirements.rst @@ -17,6 +17,8 @@ Requirements :ref:`python_bridge:native`. Python must provide a shared ``libpython`` (``--enable-shared``), which is the case for distribution Pythons, ``actions/setup-python``, ``uv`` and ``pyenv`` builds with default settings. + conda-forge Pythons link the ``python`` executable statically but also + ship the shared library, which the bridge uses. * Windows: a 64-bit CPython from python.org (or compatible, such as ``actions/setup-python`` and ``uv``). No compiler is needed for NVC and GHDL when the package includes the prebuilt DLLs. Otherwise, and always for diff --git a/release_notes/news.d/34.feature.md b/release_notes/news.d/34.feature.md new file mode 100644 index 0000000..ede6f94 --- /dev/null +++ b/release_notes/news.d/34.feature.md @@ -0,0 +1 @@ +conda-forge Pythons are supported: their `python` executable links Python statically, but the bridge now uses the shared `libpython` they also ship. diff --git a/src/vunit_python_bridge/native_library.py b/src/vunit_python_bridge/native_library.py index 55f51b4..c29a655 100644 --- a/src/vunit_python_bridge/native_library.py +++ b/src/vunit_python_bridge/native_library.py @@ -229,16 +229,22 @@ def _python_library() -> Path: Regular --enable-shared builds name it in LDLIBRARY (libpython3.X.so, .dylib); macOS framework builds name the framework binary instead (Python.framework/Versions/3.X/Python). + conda-forge links its python executable statically but ships the shared library in LIBDIR too. """ ldlibrary = sysconfig.get_config_var("LDLIBRARY") or "" framework = sysconfig.get_config_var("PYTHONFRAMEWORK") or "" version = f"{sys.version_info[0]}.{sys.version_info[1]}{getattr(sys, 'abiflags', '')}" shared = bool(sysconfig.get_config_var("Py_ENABLE_SHARED")) or bool(framework) if not shared or not (framework or ldlibrary.endswith((".so", ".dylib")) or ".so." in ldlibrary): + suffix = ".dylib" if sys.platform == "darwin" else ".so" + shared_library = Path(sysconfig.get_config_var("LIBDIR") or "") / f"libpython{version}{suffix}" + if shared_library.is_file(): + return shared_library.resolve() raise PythonBridgeError( f"VHDL Python support requires a CPython built with a shared Python library (--enable-shared). " f"{sys.executable} links Python statically " - f"(LDLIBRARY={ldlibrary!r}, Py_ENABLE_SHARED={sysconfig.get_config_var('Py_ENABLE_SHARED')!r})." + f"(LDLIBRARY={ldlibrary!r}, Py_ENABLE_SHARED={sysconfig.get_config_var('Py_ENABLE_SHARED')!r}) " + f"and there is no {shared_library}." ) prefix = Path(sys.base_prefix) candidates = [Path(sysconfig.get_config_var("LIBDIR") or "") / ldlibrary] diff --git a/tests/test_python_bridge.py b/tests/test_python_bridge.py index 750379b..a8e5582 100644 --- a/tests/test_python_bridge.py +++ b/tests/test_python_bridge.py @@ -725,7 +725,10 @@ def fake_get_paths(): with self.assertRaisesRegex(RuntimeError, "Python.h"): self._setup() - def test_static_only_python_raises(self): + def _static_python(self, libdir): + """ + sysconfig of a statically linked Python with libdir as LIBDIR + """ real_get_config_var = native_library.sysconfig.get_config_var def fake_get_config_var(name): @@ -734,11 +737,28 @@ def fake_get_config_var(name): return 0 if name == "PYTHONFRAMEWORK": return "" + if name == "LIBDIR": + return str(libdir) return real_get_config_var(name) - with mock.patch("vunit_python_bridge.native_library.sysconfig.get_config_var", side_effect=fake_get_config_var): - with self.assertRaisesRegex(RuntimeError, "shared Python library"): - self._setup() + return mock.patch.object(native_library.sysconfig, "get_config_var", side_effect=fake_get_config_var) + + def test_static_only_python_raises(self): + libdir = self.tempdir / "static lib" + libdir.mkdir() + with self._static_python(libdir): + with self.assertRaisesRegex(RuntimeError, f"shared Python library.*no {re.escape(str(libdir))}"): + native_library._python_library() + + def test_static_python_with_a_shared_library_uses_it(self): + # conda-forge: the python executable is static, LIBDIR has libpython3.X.so too + libdir = self.tempdir / "conda lib" + libdir.mkdir() + version = f"{sys.version_info[0]}.{sys.version_info[1]}{getattr(sys, 'abiflags', '')}" + library = libdir / f"libpython{version}{'.dylib' if sys.platform == 'darwin' else '.so'}" + library.write_bytes(b"") + with self._static_python(libdir): + self.assertEqual(native_library._python_library(), library.resolve()) def test_free_threaded_python_raises(self): real_get_config_var = native_library.sysconfig.get_config_var From 2c6355ef6ba48a77f9aed1adb1995c5e2ba4d922 Mon Sep 17 00:00:00 2001 From: Sebastian Abrahamsson Date: Thu, 15 Oct 2026 19:22:31 +0200 Subject: [PATCH 2/2] CI: conda-forge Python Co-Authored-By: Claude Opus 5.5 --- .github/workflows/python_bridge.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python_bridge.yml b/.github/workflows/python_bridge.yml index 4d7ecc2..743e49b 100644 --- a/.github/workflows/python_bridge.yml +++ b/.github/workflows/python_bridge.yml @@ -125,8 +125,8 @@ jobs: # # Linux with the Pythons users have besides python.org's: the one of the distribution -# (libpython in the multiarch directory), the one uv installs (python-build-standalone) -# and the one pyenv builds from source +# (libpython in the multiarch directory), the one uv installs (python-build-standalone), +# the one pyenv builds from source and conda-forge's (python executable linked statically) # pythons: @@ -134,7 +134,7 @@ jobs: strategy: fail-fast: false matrix: - python: [apt, uv, pyenv] + python: [apt, uv, pyenv, conda] name: 'ubuntu-24.04 · Python from ${{ matrix.python }} · nvc' env: VUNIT_SIMULATOR: nvc @@ -167,6 +167,11 @@ jobs: git clone --depth 1 https://github.com/pyenv/pyenv.git "$RUNNER_TEMP/pyenv" PYENV_ROOT="$RUNNER_TEMP/pyenv" "$RUNNER_TEMP/pyenv/bin/pyenv" install 3.13 echo "PY=$(ls -d "$RUNNER_TEMP"/pyenv/versions/3.13*/bin/python3)" >> "$GITHUB_ENV" ;; + conda) + curl -fsSL -o miniforge.sh https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-x86_64.sh + bash miniforge.sh -b -p "$RUNNER_TEMP/miniforge" + "$RUNNER_TEMP/miniforge/bin/conda" create -q -y -p "$RUNNER_TEMP/conda" python=3.13 + echo "PY=$RUNNER_TEMP/conda/bin/python" >> "$GITHUB_ENV" ;; esac # As on python.org's Python: outside the checkout, with a space in the path