Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions .github/workflows/python_bridge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,16 +125,16 @@ 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:
runs-on: ubuntu-24.04
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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/requirements.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions release_notes/news.d/34.feature.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 7 additions & 1 deletion src/vunit_python_bridge/native_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
28 changes: 24 additions & 4 deletions tests/test_python_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand Down
Loading