VUnit Packages¶
HDL code can be distributed as a Python package and added to a VUnit project
with add_package().
A VUnit package must be installed and importable, and its root directory must
contain a vunit_pkg.toml manifest. Namespace packages are not currently
supported.
To use a package, add it in the project’s run script:
from vunit import VUnit
vu = VUnit.from_argv()
vu.add_vhdl_builtins()
vu.add_package("foo")
vu.main()
The manifest specifies the package’s source files, version requirements, and compilation options. For example:
[package]
# The package requires VUnit 5.0.0.dev15 or later and VHDL 2008 or later.
requires = 'vunit >= "5.0.0.dev15" and vhdl >= "2008"'
# The default library for the package's sources.
library = "foo_lib"
# Default compile options for all sources in the package. These options are optional and can also be overridden for
# individual source entries.
compile_option = [
["ghdl.a_flags", ["-fpsl"]],
["nvc.a_flags", ["--psl"]]
]
[[package.sources]]
include = ["hdl/src/*.vhd"]
[[package.sources]]
include = ["hdl/compat/*.vhd"]
# Overrides the default library foo_lib for this source entry.
library = "foo_compat"
[[package.sources]]
include = ["hdl/ghdl_extras_pkg.vhd", "hdl/ghdl_extras_pkg-body.vhd"]
# Conditionally includes this source entry based on the environment.
when = 'vhdl >= "2019" and simulator == "ghdl" and ghdl_version >= "6.0.0"'
The requires expression in the example allows the package to be used with
VUnit 5.0.0.dev15 or later and VHDL 2008 or later. Each [[package.sources]]
table defines a source entry, and its include patterns are relative to the
package root. The first entry adds files matching hdl/src/*.vhd to the
default library, foo_lib. The second adds hdl/compat/*.vhd to
foo_compat instead.
The final source entry is added only when all parts of its when condition
match: the selected VHDL standard is 2019 or later, the simulator is GHDL, and
the GHDL version is 6.0.0 or later. Otherwise, that entry’s source files are
skipped.
The package-level compile_option values shown apply to every source entry.
Each option is a two-element array containing an option name and an array of
string values. A source-level option, if specified, overrides the package-level
option with the same name for that entry.
The supported manifest fields are:
Field |
Location |
Meaning |
|---|---|---|
|
|
Environment expression that must match before the package can be used. |
|
|
Default library for package sources. Required when |
|
|
Repeated source tables. Each table must contain |
|
|
Optional environment expression that controls whether this source entry is included. |
|
|
Optional library override for this source entry. VUnit creates the library if needed. |
|
|
Array of option-name/value-array pairs. Source-level options override package-level options with the same name. |
|
|
Optional Python setup function, specified as |
|
|
Deprecated. Use |
|
|
Deprecated. Use |
The requires and when expressions support the markers vunit,
vhdl, simulator, and the simulator version markers
activehdl_version, ghdl_version, modelsim_version,
nvc_version, and rivierapro_version. Values must be quoted strings.
Comparisons use ==, !=, <, <=, >, or >=. A simulator
version marker has a value only when its corresponding simulator is selected.
If a marker has no value, equality and inequality comparisons are supported
(== is false and != is true); ordering comparisons are unavailable.
Comparisons can be combined with and and or, and parentheses can group
them. and has higher precedence than or; parentheses can override
that precedence. These operators short-circuit: and skips its second
comparison when the first is false, and or skips it when the first is
true. For example, in simulator == "ghdl" and ghdl_version >= "6.0.0",
the GHDL version is checked only if GHDL is selected. If another simulator is
selected, the first comparison is false, so the version marker is not
evaluated while it has no value.
requires determines whether the package can be used in the current
environment. when determines whether a particular source entry is
included. A source entry without when is included whenever the package
can be used.
The deprecated requires-vunit and requires-vhdl fields are still
supported, but new manifests should use requires.
VUnit uses the project’s VHDL standard when it satisfies requires. If not,
VUnit selects the closest supported VHDL standard that makes the full
requires expression match. A warning is issued when a different standard
is selected, and the package is compiled with that standard. For example, if
the project uses VHDL 2008 and the manifest has
requires = 'vhdl == "1993"', the package is compiled with VHDL 1993. If the
project uses VHDL 2019 and the manifest has requires = 'vhdl <= "2008"',
VUnit selects VHDL 2008, the closest supported standard that satisfies the
requirement.
Manifest validation is strict: unknown fields and values of the wrong type are rejected.
Publishing¶
It is recommended to publish VUnit packages on PyPI and add the vunitpkg
keyword to the package metadata in pyproject.toml. This makes VUnit
packages easier to identify and discover:
[project]
keywords = ["vunitpkg"]
Setup Function¶
Packages can be defined entirely in vunit_pkg.toml. For packages that need additional setup, such as building a native library, generating sources, or configuring simulator options, the optional setup key specifies a Python function in the format “module:function”:
[package]
library = "foo_lib"
setup = "foo.vunit_setup:setup"
[[package.sources]]
include = ["hdl/src/*.vhd"]
The project must explicitly allow the setup function to run:
vu.add_package("foo", allow_setup=True)
If a package declares a setup function and allow_setup=True is not
provided, adding the package fails with an error identifying the function
that would have run. This makes permission to execute package setup code
explicit in the project’s run script. Packages that only list HDL sources
do not need a setup function.
After adding the sources listed in the manifest, VUnit imports the specified
module and calls the setup function with a
PackageContext:
def setup(context):
build_dir = context.output_path / "foo"
build_native_library(context.package_root / "c", build_dir)
context.add_source_files("foo_lib", build_dir / "*.vhd")
The context provides the package root, the default library created for the package, the VHDL standard used for its sources, the VUnit output path, the run script path, and information about the selected simulator. Exceptions raised by the setup function are reported as package errors.
A package that builds against a simulator installation needs information about that installation during setup, before VUnit creates the simulator interface. The context exposes this information through the following attributes:
context.simulator_classis the selected simulator interface class.context.simulator_nameis the selected simulator’s name.context.simulator_prefixis the directory containing the selected simulator’s executables.context.simulator_backendidentifies the installation’s backend, where applicable. For GHDL, this is the code generator:"mcode","llvm","llvm-jit", or"gcc". For simulators without a backend distinction, it isNone.
Both simulator_class and simulator_name are None when no simulator
is found.
Simulator Hooks¶
A package’s HDL sources may depend on a native library or generated files. The simulator may need additional flags or environment variables to locate and use these dependencies.
Use context.register_simulator_hooks(simulator_name, ...) to register
functions that supply these settings for a particular simulator. Hooks
receive the simulator interface, so they can adapt their results to its
configuration. They apply only to the project in which they are registered,
that is to the tests of the VUnit object the package is added to. Creating
another VUnit object neither removes them nor gives them to the new object.
The following example builds a native library and registers hooks to load it with NVC or link it with GHDL:
def setup(context):
library = build_native_library(
context.package_root / "c",
context.output_path / "foo",
)
context.register_simulator_hooks(
"nvc",
run_flags=lambda simulator_interface: [f"--load={library}"],
)
context.register_simulator_hooks(
"ghdl",
elab_flags=lambda simulator_interface: [f"-Wl,{library}"],
)
The GHDL hook above assumes a backend that links native libraries during elaboration. Backend-specific hooks are described below.
Four hook types are available, with support depending on the simulator:
Hook |
Return value |
Supported simulators |
|---|---|---|
|
Additional flags for test elaboration. |
GHDL, NVC, and ModelSim/Questa. ModelSim/Questa passes these flags
to |
|
Additional flags for test simulation. |
All simulators. For simulators based on |
|
Additional command-line flags for the simulator process started by VUnit. |
ModelSim/Questa and Riviera-PRO, for both persistent and batch
|
|
The simulation environment, based on the environment supplied
in |
GHDL and NVC, and the |
For simulators based on vsim, run_flags and process_flags target
different commands:
run_flagsadds options to thevsimcommand inside the generated do-file. Use it for options that control what is loaded into the simulation.process_flagsadds options to the command line used to start thevsimprocess. Use it for options that configure the process itself.
For example, Questa’s -noautoldlibpath option prevents bundled runtime
libraries from being added to the dynamic library search path. It is only
honoured on the process command line, so it belongs in process_flags.
VUnit evaluates process_flags and run_env when it constructs the
simulator interface. These hooks must therefore be registered before the
interface is created. Registering them in a package setup function meets
this requirement: setup runs during
add_package(), before
main() creates the interface.
Hooks can also inspect the simulator installation through the interface
they receive. The simulator_interface.prefix attribute contains the
directory of the simulator executables, matching
context.simulator_prefix from the setup function.
For GHDL, GHDLInterface.backend identifies the code generator:
"mcode", "llvm", "llvm-jit", or "gcc". This determines how
native libraries are bound to the design. The "llvm" and "gcc"
backends link them during elaboration; the other backends load them at
runtime.
A hook can use this information to return flags only for the relevant
backends. For example, with library referring to the built native
library, this hook adds a linker search path only for "llvm" and
"gcc":
def elab_flags(simulator_interface):
if simulator_interface.backend not in ("llvm", "gcc"):
return []
return [f"-Wl,-L{library.parent}"]