From a4c8aff79b659c9bcfe1e62523da32ae81828f58 Mon Sep 17 00:00:00 2001 From: Sebastian Hellgren Date: Sun, 27 Sep 2026 10:32:58 +0200 Subject: [PATCH 1/6] Fix the plot tests of the example with a Tk window On Windows in a virtual environment, Tk found no Tcl scripts: the example pointed TCL_LIBRARY to sys.prefix, the virtual environment, instead of the Python installation, and Tcl looks next to the executable, the simulator. tcl_library.py now finds the scripts under sys.base_prefix, for any Tcl version and layout, and sets nothing for a Tcl 9 that carries them in its library. The revert of the variables was a no-op and is gone. Test simple plot waited for its window to be closed, which hangs whenever Tk works; it now closes the window after 5 s like Test advanced plot. Co-Authored-By: Claude Opus 5.5 --- examples/embedded_python/tb_example.vhd | 37 ++++--------------------- examples/embedded_python/tcl_library.py | 21 ++++++++++++++ 2 files changed, 27 insertions(+), 31 deletions(-) create mode 100644 examples/embedded_python/tcl_library.py diff --git a/examples/embedded_python/tb_example.vhd b/examples/embedded_python/tb_example.vhd index df63c80..d653d5d 100644 --- a/examples/embedded_python/tb_example.vhd +++ b/examples/embedded_python/tb_example.vhd @@ -71,31 +71,6 @@ begin variable coefficients : real_vector(0 to 2); variable table : integer_vector_ptr_t; - 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')" - ); - end; - - procedure unset_tcl_installation is - begin - exec("environ = old_environ"); - end; - procedure query_if(expr : boolean; check_result : check_result_t) is variable logger : logger_t; variable log_level : log_level_t; @@ -163,10 +138,8 @@ begin begin test_runner_setup(runner, runner_cfg); - -- To avoid mixup with the Riviera-PRO TCL installation I had to - -- set the TCL_LIBRARY and TK_LIBRARY environment variables - -- to the Python installation. TODO: Find a better way if possible - set_tcl_installation; + -- For the tests showing a Tk window: the Tcl/Tk of the Python installation + import_module_from_file(join(tb_path(runner_cfg), "tcl_library.py"), "tcl_library"); show(display_handler, debug); @@ -717,6 +690,10 @@ begin exec("from matplotlib import pyplot as plt"); -- Matplotlib is installed with pip install matplotlib exec("fig = plt.figure()"); exec("plt.plot([1,2,3,4,5], [1,2,3,4,5])"); + -- Close the window after 5 s, or plt.show() waits for it to be closed + exec("timer = fig.canvas.new_timer(interval=5000)"); + exec("timer.add_callback(plt.close)"); + exec("timer.start()"); exec("plt.show()"); elsif run("Test advanced plot") then @@ -743,8 +720,6 @@ begin end loop; -- @formatter:on - -- Revert to old environment variables - unset_tcl_installation; test_runner_cleanup(runner); end process; diff --git a/examples/embedded_python/tcl_library.py b/examples/embedded_python/tcl_library.py new file mode 100644 index 0000000..ff000ce --- /dev/null +++ b/examples/embedded_python/tcl_library.py @@ -0,0 +1,21 @@ +# 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 + +""" +Points Tcl and Tk to the scripts of the Python installation, for the tests of +tb_example.vhd that show a Tk window. Tcl looks for its scripts next to the +executable, which is the simulator here, so it finds none (NVC, GHDL) or those +of the simulator (Riviera-PRO). A Tcl 9 that carries its scripts in its library +has none to point to. +""" + +from os import environ +from pathlib import Path +from sys import base_prefix + +for var, pattern in [("TCL_LIBRARY", "*/tcl[0-9]*/init.tcl"), ("TK_LIBRARY", "*/tk[0-9]*/tk.tcl")]: + for script in Path(base_prefix).glob(pattern): + environ[var] = str(script.parent) From 13ea3ee5b3273dbf10985a8c569611f1b2dd1e57 Mon Sep 17 00:00:00 2001 From: Sebastian Hellgren Date: Sun, 27 Sep 2026 10:42:37 +0200 Subject: [PATCH 2/6] Keep Test simple plot waiting for its window to be closed It is how the test shows the plot; the timer changed that. Co-Authored-By: Claude Opus 5.5 --- examples/embedded_python/tb_example.vhd | 4 ---- 1 file changed, 4 deletions(-) diff --git a/examples/embedded_python/tb_example.vhd b/examples/embedded_python/tb_example.vhd index d653d5d..fc30d15 100644 --- a/examples/embedded_python/tb_example.vhd +++ b/examples/embedded_python/tb_example.vhd @@ -690,10 +690,6 @@ begin exec("from matplotlib import pyplot as plt"); -- Matplotlib is installed with pip install matplotlib exec("fig = plt.figure()"); exec("plt.plot([1,2,3,4,5], [1,2,3,4,5])"); - -- Close the window after 5 s, or plt.show() waits for it to be closed - exec("timer = fig.canvas.new_timer(interval=5000)"); - exec("timer.add_callback(plt.close)"); - exec("timer.start()"); exec("plt.show()"); elsif run("Test advanced plot") then From 2f1f8dec490b362cad276d64efbcaf3e383433de Mon Sep 17 00:00:00 2001 From: Sebastian Hellgren Date: Sun, 27 Sep 2026 11:26:54 +0200 Subject: [PATCH 3/6] Simplify the docstring of tcl_library.py Co-Authored-By: Claude Opus 5.5 --- examples/embedded_python/tcl_library.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/examples/embedded_python/tcl_library.py b/examples/embedded_python/tcl_library.py index ff000ce..62a489e 100644 --- a/examples/embedded_python/tcl_library.py +++ b/examples/embedded_python/tcl_library.py @@ -5,11 +5,8 @@ # Copyright (c) 2014-2026, Lars Asplund lars.anders.asplund@gmail.com """ -Points Tcl and Tk to the scripts of the Python installation, for the tests of -tb_example.vhd that show a Tk window. Tcl looks for its scripts next to the -executable, which is the simulator here, so it finds none (NVC, GHDL) or those -of the simulator (Riviera-PRO). A Tcl 9 that carries its scripts in its library -has none to point to. +Points Tcl/Tk to the scripts of the Python installation. Tcl looks for them next +to the executable, which here is the simulator. """ from os import environ From 23f5d47f27a4b06845f664a9c891fed0512888ea Mon Sep 17 00:00:00 2001 From: Sebastian Hellgren Date: Sun, 27 Sep 2026 11:54:51 +0200 Subject: [PATCH 4/6] Note the Tcl version clash of the plot tests on Windows Co-Authored-By: Claude Opus 5.5 --- examples/embedded_python/tb_example.vhd | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/examples/embedded_python/tb_example.vhd b/examples/embedded_python/tb_example.vhd index fc30d15..09c48c6 100644 --- a/examples/embedded_python/tb_example.vhd +++ b/examples/embedded_python/tb_example.vhd @@ -685,6 +685,12 @@ begin -- Simulators have a limited set of capabilities when it comes to -- Visualize simulation output beyond signal waveforms. Python has -- almost endless capabilities + -- + -- On Windows, a simulator with a Tcl of its own, like NVC or Questa, can + -- crash when matplotlib opens a Tk window and Python has another Tcl + -- version: Pillow and matplotlib use the first Tcl they find in the + -- process. Use a Python with the Tcl version of the simulator, or another + -- backend, for example MPLBACKEND=QtAgg after pip install PySide6. --------------------------------------------------------------------- elsif run("Test simple plot") then exec("from matplotlib import pyplot as plt"); -- Matplotlib is installed with pip install matplotlib From e34372085b70a4cff7ae74e378b72b102d46dd3d Mon Sep 17 00:00:00 2001 From: Sebastian Hellgren Date: Sun, 27 Sep 2026 19:10:13 +0200 Subject: [PATCH 5/6] Find the Tcl/Tk libraries 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: ImportError: libtcl9tk9.0.so: cannot open shared object file 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. Co-Authored-By: Claude Opus 5.5 --- src/vunit_python_bridge/__init__.py | 3 +++ src/vunit_python_bridge/native_library.py | 19 +++++++++++++++++ tests/test_python_bridge.py | 26 +++++++++++++++++++++++ 3 files changed, 48 insertions(+) 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): """ From c15c203f69267227f5f0c8bc24c67b0b7bd6280e Mon Sep 17 00:00:00 2001 From: Sebastian Hellgren Date: Sun, 27 Sep 2026 19:14:40 +0200 Subject: [PATCH 6/6] Add a README to the embedded_python example How to run the tests that need no extra Python packages and no user input, and that the PySimpleGUI tests wait for answers to their dialogs. Co-Authored-By: Claude Opus 5.5 --- examples/embedded_python/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 examples/embedded_python/README.md diff --git a/examples/embedded_python/README.md b/examples/embedded_python/README.md new file mode 100644 index 0000000..f37882c --- /dev/null +++ b/examples/embedded_python/README.md @@ -0,0 +1,17 @@ +# Embedded Python example + +`tb_example.vhd` calls Python from a VHDL testbench: `exec` and `eval`, function calls, NumPy +arrays, Python models and verification components, error reporting and plots. See the +[user guide](../../docs/user_guide.rst) for the API. + +## Running + +```bash +python run.py --without-attributes .expected_failure --without-attributes .optional_deps +``` + +runs every test that needs no extra Python packages and no user input. Without the options, +`run.py` also runs the tests that fail by design and says which packages the others need. + +The three PySimpleGUI tests are interactive: they open dialogs and wait for an answer, so they +need a display and someone to answer them.