Skip to content
Open
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
17 changes: 17 additions & 0 deletions examples/embedded_python/README.md
Original file line number Diff line number Diff line change
@@ -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.
39 changes: 8 additions & 31 deletions examples/embedded_python/tb_example.vhd
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -712,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
Expand Down Expand Up @@ -743,8 +722,6 @@ begin
end loop;
-- @formatter:on

-- Revert to old environment variables
unset_tcl_installation;
test_runner_cleanup(runner);
end process;

Expand Down
18 changes: 18 additions & 0 deletions examples/embedded_python/tcl_library.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# 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/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 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)
3 changes: 3 additions & 0 deletions src/vunit_python_bridge/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"])
Expand Down
19 changes: 19 additions & 0 deletions src/vunit_python_bridge/native_library.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions tests/test_python_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
Loading