Reproducible, source-pinned binary releases of the open-source EDA toolchain DFHDL depends on, packaged as a set of Apptainer images and run via Scalapptainer.
DFHDL runs these tools through DFTools by default; users can opt into local
installations with --tools-location local. The DFTools release cycle is
independent of the DFHDL library release: bumping a tool pin rebuilds and retests
only the image(s) that contain it, and publishes only if the tests pass.
Tools are clustered by shared heavy dependency / hard linkage, so a tool bump rebuilds the smallest possible image:
| image | tools | notes |
|---|---|---|
| synth-verilog | yosys (in-tree slang SV frontend), eqy | no VHDL/LLVM — stays lean |
| synth-vhdl | yosys + ghdl (frontend) + ghdl-yosys-plugin | yosys -m ghdl; carries LLVM |
| pnr | nextpnr-ecp5, nextpnr-himbaechel, ecppack, gowin_pack | consumes yosys JSON |
| pnr-xilinx | nextpnr-xilinx, bbasm, fasm2frames, xc7frames2bit, prjxray-db | openXC7; Xilinx 7-series only⁴ |
| sim-llvm | nvc + ghdl (simulator) | the two VHDL sims share one LLVM |
| sim-verilator | verilator (+ g++/make/perl) | keeps a C++ build env at runtime¹ |
| sim-iverilog | iverilog, vvp | small, self-contained |
| sim-xezim | xezim | Rust SV simulator; built in lockstep with xezim-core |
| wavegen | surfer, gtkwave (+ vcd2fst/fst2vcd/… converters), wavecrux³ | GUI; X11-forwarded |
| program | openFPGALoader | small |
| hmi | ffmpeg, fpga-isv | media + interactive-sim viewer GUI (X11); single-stage² |
¹ Verilator emits C++ that is compiled into the simulation at use-time inside the
image, so per Verilator's docs g++/make/perl and the libfl/zlib/lz4
dev headers are runtime requirements (only verilator's own autoconf/flex/bison
are stripped).
² hmi bundles ffmpeg (prebuilt Ubuntu archive package, unpinned — floats with ubuntu:24.04)
and fpga-isv (the interactive-sim-viewer Python/Tk GUI, source-pinned via FPGA_ISV_REV and
installed into the shared venv). Both are pure-Python/wheels or prebuilt, so there is no build
toolchain to strip and it stays a single stage — the exception to the two-stage build-from-source
pattern below. fpga-isv is a GUI, X11-forwarded like wavegen.
³ wavecrux is the upstream-prebuilt WaveCrux AppImage (closed source during its public
beta), pinned by version and sha256 (WAVECRUX_REV/WAVECRUX_SHA256) and extracted into
the image at build time (no FUSE needed at runtime). Upstream ships x86_64 only, so it is
present only in the linux-x64 wavegen image; the aarch64 image carries the source-built
viewers alone. Revisit both (and the pin style) when the canonical
Ferrite-Engineering/wavecrux source repo
opens post-beta.
⁴ openXC7 is a fork of nextpnr (openXC7/nextpnr-xilinx, built with -DARCH=xilinx), not an
arch of the YosysHQ tree in pnr, so it cannot share that image; it also carries a ~500 MB
prjxray bit database that ECP5/Gowin users should not pay for. Keeping them apart means a Trellis
or Apicula pin bump never rebuilds this image, and vice versa. The per-part chip databases nextpnr
places against are not in the image (90-670 MB each): they are generated by
scripts/chipdb.sc and published as their own assets, see below.
NEXTPNR_XILINX_REV must stay at or after 6680df93: below it create_clock does not propagate
past the input BUFG and every build silently reports "PASS at 12.00 MHz".
ghdl appears in two images on purpose: the synthesis frontend ghdl
(synth-vhdl, plugin-ABI-bound to yosys) and the simulator ghdl (sim-llvm) are
pinned independently (GHDL_SYNTH_REV / GHDL_SIM_REV), so a simulator bump never
perturbs the synthesis image and vice versa. Proprietary tools (Vivado, Quartus,
Diamond, Gowin Designer, QuestaSim) are out of scope — used only via
--tools-location local.
| path | purpose |
|---|---|
pins.env |
single source of truth for source revisions (tag/commit per tool) |
images/*.def |
one multi-stage Apptainer recipe per image |
build/common.sh |
shared build helpers (clone, venv, manifest) |
build/strip-runtime.sh |
prune build-only artifacts; strip binaries |
scripts/build.sc |
build.sc <image> [dest.sif] — Scalapptainer build driver |
scripts/test.sc |
test.sc probe <image> <sif> / test.sc dfhdl <sif-dir> <dfhdl-dir> |
scripts/affected-images.sh |
maps a diff to the affected image(s) |
scripts/chipdb.sc |
generate the nextpnr-xilinx chip databases from a built pnr-xilinx image |
scripts/lockfile.sc |
rename sifs and chipdbs to immutable names + (re)generate dftools.lock.json |
.github/workflows/release.yml |
detect → build+test → DFHDL gate → publish |
Every .def (except hmi, single-stage — see ² above) is a two-stage
build: a heavy build stage installs full toolchains and
installs the tools into /opt/dftools; the prefix is pruned and stripped
(build/strip-runtime.sh), then a minimal final stage copies only /opt/dftools
and installs just the runtime shared libraries (no compilers, sources, or headers —
except sim-verilator, which intentionally keeps a C++ build env). The base
(ubuntu:24.04, ~28 MB) is the only OS overhead per image; SIF has no cross-image
layer sharing, so each image carries its own slim base — kept small relative to the
tool payloads (LLVM, yosys, trellis DB, …).
The LLVM backend is used for ghdl/nvc so the recipes build on both linux-x64
and linux-arm64.
Every tool is built from a specific tag or commit in pins.env — never a moving
branch. Change one line to bump a tool; CI rebuilds only the affected image(s). After
the first green build, harden branch-head commit pins to exact SHAs.
Requires Apptainer (native Linux) or, on Windows/macOS, the WSL2/Lima backend that Scalapptainer provisions. From the repository root:
scala-cli run scripts/build.sc -- synth-verilog # real root (CI/Docker)
DFTOOLS_NONROOT=1 scala-cli run scripts/build.sc -- synth-verilog # unprivileged dev build
scala-cli run scripts/test.sc -- probe synth-verilog dftools-synth-verilog.sifEach release tag holds a dftools.lock.json plus the per-image
assets. Asset names are immutable and content-addressed —
dftools-<image>-<arch>-<sha12>.sif (+ .sha256 and .MANIFEST.txt) — so a published
asset is never mutated in place: a changed image lands under a new name and old tags stay
valid.
The lockfile is the tag's only version-keyed artifact. It maps each image+arch to the sha256 of its sif and the asset carrying those bytes:
{ "tag": "v0.2.0",
"images": { "sim-verilator": { "linux-x64": { "sha256": "ab12…",
"asset": "dftools-sim-verilator-linux-x64-ab12….sif" } } },
"chipdbs": { "xc7a100tcsg324": { "sha256": "cd34…",
"asset": "dftools-chipdb-xc7a100tcsg324-cd34….bin.gz" } } }The chipdbs section carries the nextpnr-xilinx chip databases the same way, with two differences:
a chipdb is byte-identical on every platform, so it is not keyed by arch, and it is published
gzipped while the recorded digest is of the decompressed bytes, which is what DFHDL caches and
mmaps. A chipdb is only valid for the nextpnr-xilinx build that produced it, so the set is
regenerated exactly when the pnr-xilinx image is, and carried forward untouched otherwise.
DFHDL bundles this lockfile at build time and resolves/caches each image by sha256, not by tag — so bumping the tag re-downloads only the image(s) whose digest actually changed, and the rest of the set is a cache hit. Only rebuilt images are refreshed on a re-publish; the lockfile is regenerated (seeded from the prior one on the tag) to point at the current set.
DFTools packaging (recipes, scripts, CI) is Apache-2.0. Bundled tools retain their own upstream licenses.