diff --git a/docs/cli.rst b/docs/cli.rst index 6e94f1c3c..ecaa351d8 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -166,9 +166,9 @@ VUnit automatically detects which simulators are available on the ``PATH`` environment variable and by default selects the first one found. For people who have multiple simulators installed the ``VUNIT_SIMULATOR`` environment variable can be set to one of -``activehdl``, ``rivierapro``, ``ghdl`` or ``modelsim`` to specify -which simulator to use. ``modelsim`` is used for both ModelSim and -Questa as VUnit handles these simulators identically. +``activehdl``, ``rivierapro``, ``ghdl``, ``nvc```, or ``modelsim`` to +specify which simulator to use. ``modelsim`` is used for both ModelSim +and Questa as VUnit handles these simulators identically. In addition to VUnit scanning the ``PATH`` the simulator executable path can be explicitly configured by setting a diff --git a/docs/news.d/1002.feature.rst b/docs/news.d/1002.feature.rst new file mode 100644 index 000000000..9032ad539 --- /dev/null +++ b/docs/news.d/1002.feature.rst @@ -0,0 +1,11 @@ +[GHDL/NVC] Arbitrary waveform viewers are now supported by passing the ``--viewer`` +command line argument. As a consequence, ``ghdl.gtkwave_script.gui`` and +``nvc.gtkwave_script.gui`` are deprecated in favour of ``ghdl.viewer_script.gui`` +and ``nvc.viewer_script.gui``, respectively. The ``--gtkwave-args`` and +``--gtkwave-fmt`` command line arguments are deprecated in favour of ``--viewer-args`` +and ``--viewer-fmt``, respectively. ``ghdl.viewer.gui`` and ``nvc.viewer.gui`` can +be used to set the preferred viewer from the run-file. If no viewer is explicitly +requested, ``gtkwave`` or ``surfer`` is used, in that order. This also means that +VUnit now uses ``surfer`` if ``gtkwave`` is not installed. + +[NVC] It is possible to get VCD waveform files by passing ``--viewer-fmt=vcd``. \ No newline at end of file diff --git a/docs/py/opts.rst b/docs/py/opts.rst index c782ab69d..192a05075 100644 --- a/docs/py/opts.rst +++ b/docs/py/opts.rst @@ -201,9 +201,15 @@ The following simulation options are known. With ``--elaborate``, execute ``ghdl -e`` instead of ``ghdl --elab-run --no-run``. Must be a boolean. -``ghdl.gtkwave_script.gui`` - A user defined TCL-file that is sourced after the design has been loaded in the GUI. +``ghdl.viewer.gui`` + Name of waveform viewer to use. The command line argument ``--viewer`` will have + precedence if provided. If neither is provided, ``gtkwave`` or ``surfer`` will be + used. + +``ghdl.viewer_script.gui`` + A user defined file that is sourced after the design has been loaded in the GUI. For example this can be used to configure the waveform viewer. Must be a string. + There are currently limitations in the HEAD revision of GTKWave that prevent the user from sourcing a list of scripts directly. The following is the current work around to sourcing multiple user TCL-files: @@ -225,3 +231,17 @@ The following simulation options are known. ``nvc.sim_flags`` Extra simulation flags passed to ``nvc -r``. Must be a list of strings. + +``nvc.viewer.gui`` + Name of waveform viewer to use. The command line argument ``--viewer`` will have + precedence if provided. If neither is provided, ``gtkwave`` or ``surfer`` will be + used. + +``nvc.viewer_script.gui`` + A user defined file that is sourced after the design has been loaded in the GUI. + For example this can be used to configure the waveform viewer. Must be a string. + + There are currently limitations in the HEAD revision of GTKWave that prevent the + user from sourcing a list of scripts directly. The following is the current work + around to sourcing multiple user TCL-files: + ``source `` diff --git a/tests/unit/test_ghdl_interface.py b/tests/unit/test_ghdl_interface.py index 9f4c4ca88..335672462 100644 --- a/tests/unit/test_ghdl_interface.py +++ b/tests/unit/test_ghdl_interface.py @@ -27,22 +27,6 @@ class TestGHDLInterface(unittest.TestCase): Test the GHDL interface """ - @mock.patch("vunit.sim_if.ghdl.GHDLInterface.find_executable") - def test_runtime_error_on_missing_gtkwave(self, find_executable): - executables = {} - - def find_executable_side_effect(name): - return executables[name] - - find_executable.side_effect = find_executable_side_effect - - executables["gtkwave"] = ["path"] - GHDLInterface(prefix="prefix", output_path="") - - executables["gtkwave"] = [] - GHDLInterface(prefix="prefix", output_path="") - self.assertRaises(RuntimeError, GHDLInterface, prefix="prefix", output_path="", gui=True) - @mock.patch("subprocess.check_output", autospec=True) def test_parses_llvm_backend(self, check_output): version = b"""\ diff --git a/vunit/sim_if/_viewermixin.py b/vunit/sim_if/_viewermixin.py new file mode 100644 index 000000000..fa0d50a21 --- /dev/null +++ b/vunit/sim_if/_viewermixin.py @@ -0,0 +1,60 @@ +# 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-2024, Lars Asplund lars.anders.asplund@gmail.com +""" +Viewer handling for GHDL and NVC. +""" + + +class ViewerMixin: + """ + Mixin class for handling common viewer functionality for the GHDL and NVC simulators. + """ + + __slots__ = ( + "_gui", + "_viewer", + "_viewer_fmt", + "_viewer_args", + "_gtkwave_available", + "_surfer_available", + ) + + def __init__(self, gui, viewer, viewer_fmt, viewer_args): + self._gui = gui + self._viewer_fmt = viewer_fmt + self._viewer_args = viewer_args + self._viewer = viewer + if gui: + self._gtkwave_available = self.find_executable("gtkwave") + self._surfer_available = self.find_executable("surfer") + + def _get_viewer(self, config): + """ + Determine the waveform viewer to use. + + Falls back to gtkwave or surfer, in that order, if none is provided. + """ + viewer = self._viewer or config.sim_options.get(self.name + ".viewer.gui", None) + + if viewer is None: + if self._gtkwave_available: + viewer = "gtkwave" + elif self._surfer_available: + viewer = "surfer" + else: + raise RuntimeError("No viewer found. GUI not possible. Install GTKWave or Surfer.") + + elif not self.find_executable(viewer): + viewers = [] + if self._gtkwave_available: + viewers += ["gtkwave"] + if self._surfer_available: + viewers += ["surfer"] + addendum = f" The following viewer(s) are found in the path: {', '.join(viewers)}" if viewers else "" + raise RuntimeError( + f"Cannot find the {viewer} executable in the PATH environment variable. GUI not possible.{addendum}" + ) + return viewer diff --git a/vunit/sim_if/ghdl.py b/vunit/sim_if/ghdl.py index 6fd33dee0..6a6b3ca7d 100644 --- a/vunit/sim_if/ghdl.py +++ b/vunit/sim_if/ghdl.py @@ -21,11 +21,12 @@ from ..ostools import Process from . import SimulatorInterface, ListOfStringOption, StringOption, BooleanOption from ..vhdl_standard import VHDL +from ._viewermixin import ViewerMixin LOGGER = logging.getLogger(__name__) -class GHDLInterface(SimulatorInterface): # pylint: disable=too-many-instance-attributes +class GHDLInterface(SimulatorInterface, ViewerMixin): # pylint: disable=too-many-instance-attributes """ Interface for GHDL simulator """ @@ -43,7 +44,9 @@ class GHDLInterface(SimulatorInterface): # pylint: disable=too-many-instance-at sim_options = [ ListOfStringOption("ghdl.sim_flags"), ListOfStringOption("ghdl.elab_flags"), - StringOption("ghdl.gtkwave_script.gui"), + StringOption("ghdl.gtkwave_script.gui"), # Deprecated in v5.1.0 + StringOption("ghdl.viewer_script.gui"), + StringOption("ghdl.viewer.gui"), BooleanOption("ghdl.elab_e"), ] @@ -52,14 +55,16 @@ def add_arguments(parser): """ Add command line arguments """ - group = parser.add_argument_group("ghdl", description="GHDL specific flags") + group = parser.add_argument_group("ghdl/nvc", description="GHDL/NVC specific flags") group.add_argument( + "--viewer-fmt", "--gtkwave-fmt", choices=["vcd", "fst", "ghw"], default=None, - help="Save .vcd, .fst, or .ghw to open in gtkwave", + help="Save .vcd, .fst, or .ghw to open in waveform viewer. NVC does not support ghw.", ) - group.add_argument("--gtkwave-args", default="", help="Arguments to pass to gtkwave") + group.add_argument("--viewer-args", "--gtkwave-args", default="", help="Arguments to pass to waveform viewer") + group.add_argument("--viewer", default=None, help="Waveform viewer to use") @classmethod def from_args(cls, args, output_path, **kwargs): @@ -71,8 +76,9 @@ def from_args(cls, args, output_path, **kwargs): output_path=output_path, prefix=prefix, gui=args.gui, - gtkwave_fmt=args.gtkwave_fmt, - gtkwave_args=args.gtkwave_args, + viewer_fmt=args.viewer_fmt, + viewer_args=args.viewer_args, + viewer=args.viewer, backend=cls.determine_backend(prefix), ) @@ -88,20 +94,23 @@ def __init__( # pylint: disable=too-many-arguments output_path, prefix, gui=False, - gtkwave_fmt=None, - gtkwave_args="", + viewer_fmt=None, + viewer_args="", + viewer=None, backend="llvm", ): SimulatorInterface.__init__(self, output_path, gui) + ViewerMixin.__init__( + self, + gui=gui, + viewer=viewer, + viewer_fmt="ghw" if gui and viewer_fmt is None else viewer_fmt, + viewer_args=viewer_args, + ) + self._prefix = prefix self._project = None - if gui and (not self.find_executable("gtkwave")): - raise RuntimeError("Cannot find the gtkwave executable in the PATH environment variable. GUI not possible") - - self._gui = gui - self._gtkwave_fmt = "ghw" if gui and gtkwave_fmt is None else gtkwave_fmt - self._gtkwave_args = gtkwave_args self._backend = backend self._vhdl_standard = None self._coverage_test_dirs = set() # For gcov @@ -123,7 +132,7 @@ def _get_version_output(cls, prefix): @classmethod def _get_help_output(cls, prefix): """ - Get the output of 'ghdl --version' + Get the output of 'ghdl --help' """ return subprocess.check_output([str(Path(prefix) / cls.executable), "--help"]).decode() @@ -315,11 +324,11 @@ def _get_command( sim += ["--ieee-asserts=disable"] if wave_file: - if self._gtkwave_fmt == "ghw": + if self._viewer_fmt == "ghw": sim += [f"--wave={wave_file!s}"] - elif self._gtkwave_fmt == "vcd": + elif self._viewer_fmt == "vcd": sim += [f"--vcd={wave_file!s}"] - elif self._gtkwave_fmt == "fst": + elif self._viewer_fmt == "fst": sim += [f"--fst={wave_file!s}"] if not ghdl_e: @@ -355,8 +364,8 @@ def simulate(self, output_path, test_suite_name, config, elaborate_only): # pyl ghdl_e = elaborate_only and config.sim_options.get("ghdl.elab_e", False) - if self._gtkwave_fmt is not None: - data_file_name = str(Path(script_path) / f"wave.{self._gtkwave_fmt!s}") + if self._viewer_fmt is not None: + data_file_name = str(Path(script_path) / f"wave.{self._viewer_fmt!s}") if Path(data_file_name).exists(): remove(data_file_name) else: @@ -382,10 +391,17 @@ def simulate(self, output_path, test_suite_name, config, elaborate_only): # pyl except Process.NonZeroExitCode: status = False + if config.sim_options.get(self.name + ".gtkwave_script.gui", None): + LOGGER.warning("%s.gtkwave_script.gui is deprecated and will be removed " + "in a future version, use %s.viewer_script.gui instead", + self.name, self.name) + if self._gui and not elaborate_only: - cmd = ["gtkwave"] + shlex.split(self._gtkwave_args) + [data_file_name] + cmd = [self._get_viewer(config)] + shlex.split(self._viewer_args) + [data_file_name] - init_file = config.sim_options.get(self.name + ".gtkwave_script.gui", None) + init_file = config.sim_options.get( + self.name + ".viewer_script.gui", config.sim_options.get(self.name + ".gtkwave_script.gui", None) + ) if init_file is not None: cmd += ["--script", str(Path(init_file).resolve())] diff --git a/vunit/sim_if/nvc.py b/vunit/sim_if/nvc.py index 32bd9d36c..082c720e1 100644 --- a/vunit/sim_if/nvc.py +++ b/vunit/sim_if/nvc.py @@ -20,12 +20,13 @@ from ..ostools import Process from . import SimulatorInterface, ListOfStringOption, StringOption from . import run_command +from ._viewermixin import ViewerMixin from ..vhdl_standard import VHDL LOGGER = logging.getLogger(__name__) -class NVCInterface(SimulatorInterface): # pylint: disable=too-many-instance-attributes +class NVCInterface(SimulatorInterface, ViewerMixin): # pylint: disable=too-many-instance-attributes """ Interface for NVC simulator """ @@ -45,7 +46,8 @@ class NVCInterface(SimulatorInterface): # pylint: disable=too-many-instance-att ListOfStringOption("nvc.sim_flags"), ListOfStringOption("nvc.elab_flags"), StringOption("nvc.heap_size"), - StringOption("nvc.gtkwave_script.gui"), + StringOption("nvc.viewer_script.gui"), + StringOption("nvc.viewer.gui"), ] @classmethod @@ -59,6 +61,9 @@ def from_args(cls, args, output_path, **kwargs): prefix=prefix, gui=args.gui, num_threads=args.num_threads, + viewer_fmt=args.viewer_fmt, + viewer_args=args.viewer_args, + viewer=args.viewer, ) @classmethod @@ -69,25 +74,19 @@ def find_prefix_from_path(cls): return cls.find_toolchain([cls.executable]) def __init__( # pylint: disable=too-many-arguments - self, - output_path, - prefix, - num_threads, - gui=False, - gtkwave_args="", + self, output_path, prefix, num_threads, gui=False, viewer_fmt=None, viewer_args="", viewer=None ): SimulatorInterface.__init__(self, output_path, gui) + if viewer_fmt == "ghw": + LOGGER.warning("NVC does not support ghw, defaulting to fst") + viewer_fmt = None # Defaults to FST later + ViewerMixin.__init__(self, gui=gui, viewer=viewer, viewer_fmt=viewer_fmt, viewer_args=viewer_args) + self._prefix = prefix self._project = None - if gui and (not self.find_executable("gtkwave")): - raise RuntimeError("Cannot find the gtkwave executable in the PATH environment variable. GUI not possible") - - self._gui = gui - self._gtkwave_args = gtkwave_args self._vhdl_standard = None self._coverage_test_dirs = set() - (major, minor) = self.determine_version(prefix) self._supports_jit = major > 1 or (major == 1 and minor >= 9) @@ -242,7 +241,9 @@ def compile_vhdl_file_command(self, source_file): cmd += [source_file.name] return cmd - def simulate(self, output_path, test_suite_name, config, elaborate_only): # pylint: disable=too-many-branches + def simulate( + self, output_path, test_suite_name, config, elaborate_only + ): # pylint: disable=too-many-branches, disable=too-many-statements """ Simulate with entity as top level using generics """ @@ -252,16 +253,16 @@ def simulate(self, output_path, test_suite_name, config, elaborate_only): # pyl if not script_path.exists(): makedirs(script_path) + libdir = self._project.get_library(config.library_name).directory + cmd = self._get_command(self._vhdl_standard, config.library_name, libdir) + if self._gui: - wave_file = script_path / (f"{config.entity_name}.fst") + wave_file = script_path / (f"{config.entity_name}.{self._viewer_fmt or 'fst'}") if wave_file.exists(): remove(wave_file) else: wave_file = None - libdir = self._project.get_library(config.library_name).directory - cmd = self._get_command(self._vhdl_standard, config.library_name, libdir) - cmd += ["-H", config.sim_options.get("nvc.heap_size", "64m")] cmd += config.sim_options.get("nvc.global_flags", []) @@ -290,6 +291,9 @@ def simulate(self, output_path, test_suite_name, config, elaborate_only): # pyl if wave_file: cmd += [f"--wave={wave_file}"] + if self._viewer_fmt: + cmd += [f"--format={self._viewer_fmt}"] + print(" ".join([f"'{word}'" if " " in word else word for word in cmd])) status = True @@ -300,10 +304,17 @@ def simulate(self, output_path, test_suite_name, config, elaborate_only): # pyl except Process.NonZeroExitCode: status = False + if config.sim_options.get(self.name + ".gtkwave_script.gui", None): + LOGGER.warning("%s.gtkwave_script.gui is deprecated and will be removed " + "in a future version, use %s.viewer_script.gui instead", + self.name, self.name) + if self._gui and not elaborate_only: - cmd = ["gtkwave"] + shlex.split(self._gtkwave_args) + [str(wave_file)] + cmd = [self._get_viewer(config)] + shlex.split(self._viewer_args) + [str(wave_file)] - init_file = config.sim_options.get(self.name + ".gtkwave_script.gui", None) + init_file = config.sim_options.get( + self.name + ".viewer_script.gui", config.sim_options.get(self.name + ".gtkwave_script.gui", None) + ) if init_file is not None: cmd += ["--script", str(Path(init_file).resolve())]