From 705f0f269d3a101b856f6f5e970cf7a3292d7326 Mon Sep 17 00:00:00 2001 From: Sebastian Hellgren Date: Sun, 27 Sep 2026 19:07:11 +0200 Subject: [PATCH] Find the Tcl/Tk of the Python installation in every simulator Standalone Python builds (python-build-standalone) keep Tcl/Tk next to libpython on Linux. Only the python executable finds them, through its RPATH, so tkinter failed to load in any simulator embedding the interpreter. The libraries have no SONAME, so loading them from Python first does not help. The package setup now adds that directory to LD_LIBRARY_PATH of the VUnit process, like add_python_dll_to_path does for PATH on Windows, and every simulator process inherits it. The embedded_python example pointed TCL_LIBRARY and TK_LIBRARY at a tcl8.6/tk8.6 directory under sys.prefix whether it existed or not. In a virtual environment sys.prefix is not the Python installation, and a Python with Tcl 9 has no tcl8.6 directory, so Tcl stopped finding its own libraries. unset_tcl_installation did not restore anything. The logic moves to tcl_installation.py: it uses sys.base_prefix, accepts any Tcl version, only sets a variable to a directory that has the Tcl or Tk scripts, and restores the previous values. Co-Authored-By: Claude Opus 5.5 --- examples/embedded_python/tb_example.vhd | 21 ++-------- examples/embedded_python/tcl_installation.py | 44 ++++++++++++++++++++ src/vunit_python_bridge/__init__.py | 3 ++ src/vunit_python_bridge/native_library.py | 19 +++++++++ tests/test_python_bridge.py | 26 ++++++++++++ 5 files changed, 96 insertions(+), 17 deletions(-) create mode 100644 examples/embedded_python/tcl_installation.py diff --git a/examples/embedded_python/tb_example.vhd b/examples/embedded_python/tb_example.vhd index df63c80..e8a8774 100644 --- a/examples/embedded_python/tb_example.vhd +++ b/examples/embedded_python/tb_example.vhd @@ -71,29 +71,16 @@ begin variable coefficients : real_vector(0 to 2); variable table : integer_vector_ptr_t; + -- The Tcl/Tk installation of the Python installation, see tcl_installation.py procedure set_tcl_installation is begin - exec("from os import environ"); - exec("from sys import prefix"); - exec("from pathlib import Path"); - exec("old_environ = environ"); - exec( - "if (Path(prefix) / 'lib' / 'tcl8.6').exists():" + - " environ['TCL_LIBRARY'] = str(Path(prefix) / 'lib' / 'tcl8.6')" + - "else:" + - " environ['TCL_LIBRARY'] = str(Path(prefix) / 'tcl' / 'tcl8.6')" - ); - exec( - "if (Path(prefix) / 'lib' / 'tk8.6').exists():" + - " environ['TK_LIBRARY'] = str(Path(prefix) / 'lib' / 'tk8.6')" + - "else:" + - " environ['TK_LIBRARY'] = str(Path(prefix) / 'tcl' / 'tk8.6')" - ); + import_module_from_file(join(tb_path(runner_cfg), "tcl_installation.py"), "tcl_installation"); + exec("tcl_installation.set_tcl_installation()"); end; procedure unset_tcl_installation is begin - exec("environ = old_environ"); + exec("tcl_installation.unset_tcl_installation()"); end; procedure query_if(expr : boolean; check_result : check_result_t) is diff --git a/examples/embedded_python/tcl_installation.py b/examples/embedded_python/tcl_installation.py new file mode 100644 index 0000000..c4511f2 --- /dev/null +++ b/examples/embedded_python/tcl_installation.py @@ -0,0 +1,44 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. +# +# Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com + +""" +The Tcl/Tk installation for the tests using tkinter (PySimpleGUI dialogs, Matplotlib windows), +imported from VHDL with import_module_from_file. + +A simulator with a Tcl of its own, like Riviera-PRO, can make tkinter look for the Tcl/Tk script +libraries of that Tcl rather than those of the Python installation. set_tcl_installation points +TCL_LIBRARY and TK_LIBRARY at the libraries of the Python installation, of whatever Tcl version it +has, when it has them in its lib (Linux, macOS) or tcl (Windows) directory. Otherwise it leaves +them alone, for Tcl to find its libraries itself. +""" + +import os +import sys +from pathlib import Path + +# The variable, the directory name pattern and a script every such directory has +LIBRARIES = [("TCL_LIBRARY", "tcl[0-9]*", "init.tcl"), ("TK_LIBRARY", "tk[0-9]*", "tk.tcl")] + +_saved = {} + + +def set_tcl_installation(): + for name, pattern, script in LIBRARIES: + _saved.setdefault(name, os.environ.get(name)) + for directory in (Path(sys.base_prefix) / "lib", Path(sys.base_prefix) / "tcl"): + found = [path for path in sorted(directory.glob(pattern), reverse=True) if (path / script).is_file()] + if found: + os.environ[name] = str(found[0]) + break + + +def unset_tcl_installation(): + for name, value in _saved.items(): + if value is None: + os.environ.pop(name, None) + else: + os.environ[name] = value + _saved.clear() diff --git a/src/vunit_python_bridge/__init__.py b/src/vunit_python_bridge/__init__.py index 1bec107..40809c0 100644 --- a/src/vunit_python_bridge/__init__.py +++ b/src/vunit_python_bridge/__init__.py @@ -58,6 +58,7 @@ def setup(context): # pylint: disable=import-outside-toplevel from .bridge import setup as setup_bridge from .foreign_application import setup_vhpi_application + from .native_library import add_python_libraries_to_library_path from . import simulator_hooks simulator_name = context.simulator_name @@ -73,6 +74,8 @@ def setup(context): f"it has no foreign language interface for {simulator_name}" ) + add_python_libraries_to_library_path() + if interface == "VHPI": # Riviera-PRO/Active-HDL, the simulators served by the VHPI application context.add_source_files(context.library.name, [VHDL_PATH / "python_pkg_vhpi.vhd"]) diff --git a/src/vunit_python_bridge/native_library.py b/src/vunit_python_bridge/native_library.py index c331f24..e171583 100644 --- a/src/vunit_python_bridge/native_library.py +++ b/src/vunit_python_bridge/native_library.py @@ -406,6 +406,25 @@ def add_python_dll_to_path() -> None: os.environ["PATH"] = os.pathsep.join([item for item in paths if item] + [directory]) +def add_python_libraries_to_library_path() -> None: + """ + Standalone builds of Python (python-build-standalone) keep the shared libraries their extension + modules load, Tcl/Tk for tkinter, next to libpython. On Linux only the python executable finds + them, through its RPATH, not a simulator embedding the interpreter. They have no SONAME either, + so loading them from Python first does not help: the simulator processes VUnit starts must find + them through LD_LIBRARY_PATH. + """ + # ponytail: changes LD_LIBRARY_PATH of the whole VUnit process, only for its children to find Tcl/Tk + if not sys.platform.startswith("linux"): + return + directory = Path(sys.base_prefix) / "lib" + if not any(directory.glob("libtcl*.so")): + return + paths = os.environ.get("LD_LIBRARY_PATH", "").split(os.pathsep) + if str(directory) not in paths: + os.environ["LD_LIBRARY_PATH"] = os.pathsep.join([item for item in paths if item] + [str(directory)]) + + def _build_windows_library(root: Path, simulator_prefix: Optional[Path] = None) -> Path: """ Build the library on Windows with gcc (see windows_gcc) against the headers and the import diff --git a/tests/test_python_bridge.py b/tests/test_python_bridge.py index 8563c0d..6a17c89 100644 --- a/tests/test_python_bridge.py +++ b/tests/test_python_bridge.py @@ -880,6 +880,32 @@ def test_add_python_dll_to_path_appends_once(self): native_library.add_python_dll_to_path() self.assertEqual(os.environ["PATH"], os.pathsep.join(["first", "pydir"])) + def test_add_python_libraries_to_library_path_only_for_a_python_with_its_own_tcl(self): + with create_tempdir() as tempdir: + lib = tempdir / "lib" + lib.mkdir() + with ( + mock.patch("sys.platform", "linux"), + mock.patch("sys.base_prefix", str(tempdir)), + mock.patch.dict(os.environ, {"LD_LIBRARY_PATH": "first"}), + ): + # A distribution Python: Tcl/Tk are in the system library directories + native_library.add_python_libraries_to_library_path() + self.assertEqual(os.environ["LD_LIBRARY_PATH"], "first") + # A standalone build: Tcl/Tk next to libpython, added once + (lib / "libtcl9.0.so").touch() + native_library.add_python_libraries_to_library_path() + native_library.add_python_libraries_to_library_path() + self.assertEqual(os.environ["LD_LIBRARY_PATH"], os.pathsep.join(["first", str(lib)])) + for platform in ("darwin", "win32"): + with ( + mock.patch("sys.platform", platform), + mock.patch("sys.base_prefix", str(tempdir)), + mock.patch.dict(os.environ, {"LD_LIBRARY_PATH": "first"}), + ): + native_library.add_python_libraries_to_library_path() + self.assertEqual(os.environ["LD_LIBRARY_PATH"], "first", platform) + class TestWindows64Bit(unittest.TestCase): """