Hardware engineers are often behind software engineers on continuous integration practices. A simulator version mismatch between a contributor's laptop and a colleague's workstation can cause a simulation to fail, pass incorrectly, or produce different waveforms with no obvious explanation. Verilaxi uses Docker and GitHub Actions to eliminate this problem: anyone who clones the repository and runs make inside the container gets identical results, on any operating system, every time.
The problem: tool version sensitivity
Verilaxi is validated against Verilator 5.046 specifically. Older 5.x releases packaged by Linux distributions may fail to parse parts of the testbench or silently miscompile coroutine-dependent tasks. There is no warning — the simulation either crashes or produces wrong results. Without a pinned environment, every new contributor or CI runner is a potential source of noise.
The same applies to Yosys for synthesis. Version differences in the synthesis tool can produce different resource counts or fail on specific constructs, making it impossible to compare synthesis results across machines.
The Dockerfile
The Dockerfile builds a minimal Ubuntu-based image with Verilator 5.046 and Yosys compiled from source, plus the C++17 toolchain and make. Pinning to a specific commit or release tag in the Dockerfile is what guarantees reproducibility.
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
build-essential git autoconf flex bison \
libfl-dev libfl2 zlib1g-dev help2man \
python3 cmake ccache numactl perl \
tcl-dev libreadline-dev libffi-dev pkg-config \
&& rm -rf /var/lib/apt/lists/*
# Verilator 5.046 from source
RUN git clone https://github.com/verilator/verilator.git /verilator && \
cd /verilator && git checkout v5.046 && \
autoconf && ./configure && make -j$(nproc) && make install
# Yosys from source
RUN git clone https://github.com/YosysHQ/yosys.git /yosys && \
cd /yosys && make -j$(nproc) && make install
WORKDIR /workspace
Simplified Dockerfile for verilaxi
The image is built once. After that, every simulation and synthesis run uses the same binary. No README section saying "install Verilator 5.046 manually" is needed.
Running simulations in the container
The repository is volume-mounted into the container at runtime. This means the source files, Makefile, and output directories are all on the host filesystem — no copying in or out is needed, and the results (logs, FST waveforms) appear directly in the working directory.
Each part of the command has a specific purpose. docker build -t verilaxi . creates a reusable image named verilaxi. In the run commands, --rm deletes the container after the command finishes, -it makes the shell interactive, -v "$PWD":/workspace mounts the current repository into the container, and -w /workspace makes that mounted directory the working directory before running make. The final tokens are just the command executed inside the container.
# Build the image (once)
docker build -t verilaxi .
# Run a simulation
docker run --rm -it -v "$PWD":/workspace -w /workspace verilaxi \
make run TESTNAME=dma TESTTYPE=3 READY_PROB=80
# Run synthesis
docker run --rm -it -v "$PWD":/workspace -w /workspace verilaxi \
make synth SYNTH_NAME=axis_afifo SYNTH_TARGET=artix7
# Check tool versions
docker run --rm -it -v "$PWD":/workspace -w /workspace verilaxi \
bash -lc "verilator --version && yosys -V"
Running simulations and synthesis inside the verilaxi container
The --rm flag removes the container after the run. The -v "$PWD":/workspace flag mounts the current directory. On macOS and WSL2, a Linux-specific OBJ_DIR avoids trying to execute host-compiled binaries inside the container.
So a command such as docker run --rm -it -v "$PWD":/workspace -w /workspace verilaxi make run TESTNAME=dma TESTTYPE=3 READY_PROB=80 literally means: start a temporary container from the verilaxi image, mount this checkout into it, enter that directory, and run the normal project Makefile with the chosen test parameters.
# macOS or WSL2: use a separate obj_dir for the Linux container build
docker run --rm -it -v "$PWD":/workspace -w /workspace verilaxi \
make run OBJ_DIR=work/obj_dir_linux TESTNAME=axis_afifo \
FRAME_FIFO=1 TESTTYPE=1 SRC_BP=1 SINK_BP=1
Cross-platform: separate OBJ_DIR prevents host and container build artefacts from colliding
The sweep script
scripts/sweep.sh is a convenience regression runner that exercises the full simulation and synthesis matrices in a single command. It is designed to be run both locally and inside CI.
# Run all simulations
scripts/sweep.sh sim
# Run synthesis for all targets
scripts/sweep.sh synth both
# Run everything
scripts/sweep.sh all both
Sweep script entry points
The simulation sweep covers all AXIS tests (register, FIFO, AFIFO across multiple TESTTYPE and backpressure combinations), axil_register, and DMA/CDMA runs with READY_PROB=70. The synthesis sweep covers generic and Artix-7 targets for all synthesisable modules. Because waveform and log filenames are parameter-aware (dma_tt3_rp80.fst, axis_afifo_ff1_tt1_src1_sink1.fst), sweep runs across different parameter combinations never overwrite each other.
The point of the sweep is not just convenience. Hardware bugs often hide in the corners of a parameter matrix: one FIFO mode works but another fails, one level of backpressure is fine but another exposes a handshake bug, or one synthesis target accepts a construct that another rejects. Running a matrix forces the design through combinations that a single hand-picked smoke test would miss.
That is also why the script is valuable in CI. Instead of asking every contributor to remember the right list of commands, the project encodes the matrix once in scripts/sweep.sh and reuses it everywhere. Local debug, Docker runs, and GitHub Actions all execute the same regression intent, which keeps the project much easier to trust.
GitHub Actions CI
The CI workflow runs automatically on every push and pull request. It reuses the same Docker image so the CI environment matches the local development environment exactly.
The workflow YAML is in .github/workflows/. The key steps are: checkout the repository, build the Docker image, and run the sweep script inside the container.
name: verilaxi CI
on: [push, pull_request]
jobs:
simulate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t verilaxi .
- name: Run simulation sweep
run: |
docker run --rm -v "$PWD":/workspace -w /workspace verilaxi \
./scripts/sweep.sh sim
synthesise:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: docker build -t verilaxi .
- name: Run synthesis sweep
run: |
docker run --rm -v "$PWD":/workspace -w /workspace verilaxi \
./scripts/sweep.sh synth both
GitHub Actions workflow for verilaxi
Any regression — a simulation that no longer terminates, an assertion that fires, or a synthesis failure — is caught immediately on every push. Contributors do not need to run the full sweep locally before opening a pull request.
What this gives you
The combination of a pinned Docker environment, a sweep script, and a GitHub Actions workflow eliminates an entire class of "it works here but not there" problems that are common in hardware projects. The repository README no longer needs a section explaining which tool versions to install or which OS was tested. The container is the environment definition, and CI proves it works on every commit.
The full Dockerfile, sweep script, and CI workflow are available in the verilaxi repository.
Read next:
Building a Verilator Testbench for AXI Designs for the simulation harness that the container and CI jobs are actually running.
Implementation pointers in verilaxi: Dockerfile, scripts/sweep.sh, and .github/workflows/ci.yml.
Also available in GitHub.