rules_vunit: VUnit Testbenches Under Bazel
rules_vunit packages VUnit, the unit-testing framework for VHDL, as a
hermetic Bazel module.
Testbenches written against VUnit check, run, and logging libraries execute as
standard bazel test targets.
In cocoapuffs-fpga, the module manages approximately thirty
*.test.vhdl testbenches evaluated by the NVC simulator.
The primary architectural choice in rules_vunit is what it omits: the Python
orchestrator.
Separating library from orchestrator
VUnit consists of two distinct components.
The first component is a VHDL runtime library (vunit_lib), containing check
assertions, test runner primitives, and logging utilities.
The second component is a Python test orchestrator (run.py), which scans the
source tree, computes compilation order, invokes simulators, and aggregates test
results.
Under Bazel, the Python orchestrator duplicates capabilities that the build
system already provides.
Filesystem scanning duplicates target graph declarations.
Compilation order tracking duplicates dependency graphs.
Incremental rebuilding duplicates the Bazel action cache.
Parallel test execution and logging duplicate native bazel test facilities.
Running run.py inside a Bazel action would create conflicting build systems.
Therefore, rules_vunit packages only the VHDL library.
A testbench declares @rules_vunit//tool.nvc:vunit_lib in its deps, and Bazel
manages compilation and test execution natively.
Omitting run.py requires one adjustment: multi-configuration testbenches must
be declared as distinct Bazel targets.
Integration tests in the module demonstrate this approach by instantiating one
testbench across two architecture configurations.
Expressing test configurations as explicit targets allows Bazel to cache, shard,
and report test outcomes individually.
Compiling the VHDL library
Compiling vunit_lib independently requires resolving internal dependencies
without the Python runner.
Package declarations and component definitions form an intricate dependency
hierarchy that VUnit’s orchestrator normally calculates dynamically.
Packaging the library required mapping these dependencies into explicit Bazel
compilation rules.
VUnit sources also vary across VHDL standards.
Language standards 1993/2002 and 2008/2019 require different source files.
The module defines a --vhdl_standard build flag with associated
config_setting targets.
Downstream rules use select() statements on these settings, allowing an entire
project to switch language standards with a single command-line flag.
A subsequent update added VUnit’s verification component library on top of the
core framework.
Because the underlying NVC simulator builds from source, registering
rules_vunit provisions an LLVM toolchain.
Compiling the simulator from pinned source code ensures that VHDL unit tests
rely on a reproducible clang compiler rather than host binaries.
In cocoapuffs-fpga
Testbenches follow a standard structure: a vhdl_test rule specifying
@rules_vunit//tool.nvc:vunit_lib in deps.
This pattern covers Wishbone IP blocks, TileLink bridges, general utility units,
and an SD-card model.
Two implementation details matter:
- The Vivado synthesis flow does not use testbench assertions.
The project provides a lightweight
vunit_stubs.vhdlfile to satisfy synthesis dependencies without compiling the full framework. - The VHDL standard declared on test targets must align with the VUnit library.
The repository synchronizes these settings across targets using
select()expressions.
Verification combines two complementary libraries: VUnit provides test execution
and assertions, while OSVVM (rules_osvvm) supplies functional coverage and
constrained randomization.
Prior art and references
- Upstream VUnit, whose VHDL support library this module packages.
- cocotb, an alternative coroutine-based verification framework in Python.
- OSVVM, the Open Source VHDL Verification Methodology library.
- bazel_rules_hdl, providing broader EDA tooling context in Bazel.
This post is part of the build system series behind cocoapuffs-fpga,
beginning with rules_vivado and rules_ghdl.
rules_vunit is published in my Bazel registry, with source code at
filmil/bazel_rules_vunit.