Nettle (NETlist Traversal and Topology Layout Explorer) turns elaborated
Verilog and SystemVerilog into a portable .nettle bundle and opens it as an
interactive schematic in a web browser. Use it to understand synthesized RTL
connectivity, navigate or selectively flatten hierarchy, cross-probe source and
schematic objects, and share a reviewable snapshot without sharing a compiler
environment.
Nettle deliberately separates its portable bundle from viewing. The native CLI runs standalone Slang and Yosys with yosys-slang to normalize a design into a versioned ZIP + Protobuf bundle. Opening a local bundle still validates and renders it entirely in browser memory without uploading it. An optional private-cloud service adds explicit bundle sharing and queued server-side compilation without changing that local workflow.
The GitHub Pages deployment at https://xlsynth.github.io/nettle hosts the
browser viewer with two public examples: the RTL Bedrock CDC FIFO used by the
browser regression and Nettle's schematic diff fixture. You can also
open a local .nettle file; local files are read and rendered entirely in the
browser and are never uploaded. The demo is a static viewer, not a hosted HDL
compiler, and its bundled examples (including their sources) are public.
The demo builds br_counter.nettle from the manifest-pinned upstream
br_counter.sv design and starts the viewer at http://127.0.0.1:8090.
Run all commands from the repository root.
First prepare the demo corpus. This clones Bedrock-RTL, sparse-checks out the
full Git SHA in integration_tests/bedrock-rtl/manifest.json, and keeps the
upstream sources under the ignored target/ directory:
scripts/check-design-corpus.py --prepare-only --corpus bedrock-rtl \
--workspace target/design-corporaThe combined Nettle image is the recommended quickstart. It is
Linux/amd64 because the pinned HDL compiler toolchain does not publish a
Linux/arm64 archive. render leaves the generated bundle in the mounted
checkout and serves it until you stop the foreground container.
docker run --rm --platform linux/amd64 \
-p 127.0.0.1:8090:8080 \
-v "$PWD:/work" -w /work \
ghcr.io/xlsynth/nettle:latest render \
--filelist target/design-corpora/bedrock-rtl/br_counter.f \
--project-root target/design-corpora/bedrock-rtl \
--top br_counter \
--output br_counter.nettleOpen http://127.0.0.1:8090 while the container runs.
Do not open http://0.0.0.0:8080: it is the container's bind address, not a
browser URL. Browsers treat it as an insecure origin and disable the Web Crypto
API Nettle uses to validate bundles.
On Docker Engine for Linux, add --user "$(id -u):$(id -g)" to keep the output
bundle owned by the calling user. The same command works with podman in place
of docker; add --userns=keep-id:uid=10001,gid=10001 and mount the checkout
with :Z when SELinux relabeling is required.
Build the nettle target, then use nettle:latest in place of
ghcr.io/xlsynth/nettle:latest in the command above. First record the build
metadata from the checkout:
NETTLE_BUILD_DATE_UTC="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
NETTLE_BUILD_GIT_SHA="$(git rev-parse HEAD)"
if [ -n "$(git status --porcelain --untracked-files=normal)" ]; then
NETTLE_BUILD_STATE=dirty
elif ! git for-each-ref --contains HEAD --format='%(refname)' \
refs/heads/main refs/remotes |
grep -Eq '(^refs/heads/main$|^refs/remotes/.+/main$)'; then
NETTLE_BUILD_STATE=dev
else
NETTLE_BUILD_STATE=clean
fi
export NETTLE_BUILD_DATE_UTC NETTLE_BUILD_GIT_SHA NETTLE_BUILD_STATEPass those values into the image build:
docker build --platform linux/amd64 \
--build-arg NETTLE_BUILD_DATE_UTC \
--build-arg NETTLE_BUILD_GIT_SHA \
--build-arg NETTLE_BUILD_STATE \
-f Dockerfile --target nettle -t nettle:latest .Nettle publishes one Linux/amd64 image, ghcr.io/xlsynth/nettle, with the
compiler toolchain, static viewer, and hosted service. Use build, view,
render, or host according to the workflow you need.
This path builds and runs Nettle directly on the host. First install the native system dependencies, then verify the HDL compiler toolchain and build Nettle and its static viewer:
scripts/check-toolchain.sh
cargo build --locked
npm ci
npm run buildBuild and view the demo bundle:
cargo run --locked -- render \
--filelist target/design-corpora/bedrock-rtl/br_counter.f \
--project-root target/design-corpora/bedrock-rtl \
--top br_counter \
--output br_counter.nettle \
--web-root web/dist \
--port 8090Open http://127.0.0.1:8090; Nettle opens br_counter.nettle directly.
Press Ctrl-C in the terminal to stop the viewer.
See Build the combined image locally for
the interactive image build command. To run the complete test suite in its
known-good toolchain container, record and export the same build metadata, then
build the root Dockerfile's test target:
docker build --platform linux/amd64 \
--build-arg NETTLE_BUILD_DATE_UTC \
--build-arg NETTLE_BUILD_GIT_SHA \
--build-arg NETTLE_BUILD_STATE \
-f Dockerfile --target test -t nettle-test .Native development works on any host where the required compiler tools are available. On Linux/amd64, the versions below match the known-good container. On macOS/arm64 or Linux/arm64, install or build native host versions of Slang and Yosys with yosys-slang; the pinned standalone Slang Linux release is amd64-only. Install:
- Rust 1.88 or newer. This checkout selects and validates Rust 1.95.0 through
rust-toolchain.toml. - standalone Slang 11 or newer. Slang 11.0 is the known-good release. Nettle checks for the command-file, top selection, JSON diagnostics/AST, define, undefine, and parameter capabilities it uses.
- Yosys with a compatible yosys-slang plugin. Nettle does not claim a numeric
Yosys/plugin version range because the two components must be a compatible
pair; it probes
read_slangand every required plugin capability before compiling. The OSS CAD Suite snapshot dated 2026-06-15 is known good. - Node.js 20.19 or later in the 20.x line, 22.12 or later in the 22.x line, or 24 or newer, plus npm to build the web viewer. Node.js 24 with npm 11 is the known-good configuration.
- A modern web browser to use the viewer. Chromium is additionally required to run the browser end-to-end tests.
The Dockerfile is the source of truth for known-good
toolchains and build environments. Its builder, test, and nettle targets
record the exact Slang release/source commit and checksums, the checksummed OSS
CAD Suite artifact, and the Rust and Node image versions.
Nettle never downloads HDL compilers at runtime. Run
scripts/check-toolchain.sh after installing native compiler tools to exercise
their capabilities end to end.
Install the locked JavaScript dependencies and build the static viewer:
npm ci
npm run buildCargo uses the committed Cargo.lock; use --locked for reproducible native
builds. A typical local development cycle is:
scripts/check-toolchain.sh
cargo build --locked
cargo test --locked
npm test
npm run devThe development viewer is then available at http://127.0.0.1:5173. Use
cargo build --locked --release for an optimized native binary.
Run commands from the repository root unless noted otherwise.
Build the included counter:
cargo run --locked -- build \
--filelist target/design-corpora/bedrock-rtl/br_counter.f \
--project-root target/design-corpora/bedrock-rtl \
--top br_counter \
--output br_counter.nettleThe project root defaults to the filelist's parent. Set a wider root when a
filelist reaches sources through ..; every embedded source must canonicalize
inside that boundary. Nettle embeds only referenced UTF-8 sources up to 4 MiB
and records project-relative paths.
Start the built static viewer and select the bundle in its file picker:
cargo run --locked -- view --web-root web/dist --port 8090Then open http://127.0.0.1:8090. The browser reads the selected file; the
static host never receives it. For frontend development, npm run dev serves
the application at http://127.0.0.1:5173.
Pass a bundle to open it directly:
cargo run --locked -- view br_counter.nettle --web-root web/dist --port 8090This copies the bundle into a private, size-bounded anonymous temporary snapshot, validates that exact copy, and exposes it at a fixed, non-cacheable route. Anonymous storage has no pathname to orphan and is reclaimed by the operating system when its final handle closes, including after abrupt process termination. Keep the default loopback bind for sensitive designs. A non-loopback bind makes the bundle downloadable by reachable clients and therefore requires the deployment's normal authentication and TLS protections.
render combines build and view while leaving the bundle on disk:
cargo run --locked -- render \
--filelist target/design-corpora/bedrock-rtl/br_counter.f \
--project-root target/design-corpora/bedrock-rtl \
--top br_counter \
--output br_counter.nettle \
--web-root web/dist \
--port 8090nettle host serves three explicitly separate workflows from one process:
- open a local
.nettleentirely inside the browser without uploading it; - upload and persist a prebuilt
.nettle, then view or download it through a shareable capability URL; or - upload a source archive, wait in the FIFO build queue, and persist, view, or
download the resulting
.nettle.
Source uploads default to project.f at the archive root. The upload dialog can
instead select any root filelist by its relative archive path, such as
br_counter/filelist.f; nested filelists and every declared project path are
validated beneath the extracted archive root before compilation.
The hosted service runs Slang and Yosys as child processes in the same container. It does not launch nested containers or create Kubernetes workloads. The v1 deployment is exactly one Pod and one container.
First build the combined image, or replace
nettle:latest below with the digest-pinned published image
ghcr.io/xlsynth/nettle@sha256:<published-digest>. Create a persistent Docker
volume and start the service as a background container:
docker volume create nettle-sessions
docker run --detach --platform linux/amd64 \
--name nettle \
--restart unless-stopped \
--user 10001:10001 \
--read-only --cap-drop ALL --security-opt no-new-privileges \
--publish 127.0.0.1:8080:8080 \
--mount type=volume,source=nettle-sessions,target=/data \
--tmpfs /scratch:rw,nosuid,nodev,noexec,size=4g,mode=1777 \
nettle:latest host \
--storage-root=/data \
--scratch-root=/scratch \
--max-upload-bytes=536870912 \
--max-queued-builds=32 \
--build-timeout=600s \
--evict-after=30dOpen http://127.0.0.1:8080. Upload a .nettle bundle or a supported source
archive using the hosted landing page. Azure imports are absent because the
feature is disabled by default. nettle-sessions preserves completed designs
and queued builds across container restarts; /scratch is bounded, temporary
compiler and import storage. Keep only one running container attached to the
session volume.
The combined image already includes the hash-locked boostedblob executable
bbb. It does not include the Azure CLI, sign in on your behalf, or change
network policy. One supported approach is to run az login on the Docker host,
then copy only the Azure login files bbb uses into a protected Docker volume
owned by container UID 10001. Do not mount an owner-only host ~/.azure
directly into the non-root server: bind mounts retain host ownership, so the
server cannot read login files belonging to a different host UID.
If the non-Azure container above is running, stop and remove the container
before starting its Azure-enabled replacement; this does not delete
nettle-sessions:
docker stop nettle
docker rm nettle
az login
docker volume create nettle-azure-cache
docker run --rm --platform linux/amd64 \
--network none --read-only --user 0:0 \
--mount "type=bind,source=$HOME/.azure,target=/host-azure,readonly" \
--mount type=volume,source=nettle-azure-cache,target=/home/nettle \
--entrypoint sh nettle:latest \
-ec 'umask 077
install -d -m 0700 -o 10001 -g 10001 \
/home/nettle/.azure /home/nettle/.config /home/nettle/.config/bbb
test -f /host-azure/msal_token_cache.json ||
test -f /host-azure/accessTokens.json
for file in msal_token_cache.json accessTokens.json azureProfile.json; do
if test -f "/host-azure/$file"; then
install -m 0600 -o 10001 -g 10001 \
"/host-azure/$file" "/home/nettle/.azure/$file"
fi
done'
docker run --detach --platform linux/amd64 \
--name nettle \
--restart unless-stopped \
--user 10001:10001 \
--read-only --cap-drop ALL --security-opt no-new-privileges \
--publish 127.0.0.1:8080:8080 \
--env NETTLE_AZURE_ENABLE=1 \
--mount type=volume,source=nettle-sessions,target=/data \
--mount type=volume,source=nettle-azure-cache,target=/home/nettle \
--tmpfs /scratch:rw,nosuid,nodev,noexec,size=4g,mode=1777 \
nettle:latest host \
--storage-root=/data \
--scratch-root=/scratch \
--max-upload-bytes=536870912 \
--max-queued-builds=32 \
--build-timeout=600s \
--evict-after=30dThe short-lived initialization container uses root only to read the
owner-restricted host cache, install credential copies with mode 0600, and
create private cache directories with mode 0700; its network is disabled and
its root filesystem is read-only. The hosted server still runs as UID 10001
with a read-only root and never mounts the host's Azure credentials. Re-run the
initialization step after renewing the host login when the copied Azure
credentials need to be refreshed.
Verify that the actual container can access your intended Azure account before using the GUI:
docker exec nettle bbb --version
docker exec nettle bbb ls az://account/container/Replace account/container with your own storage account and container.
Alternatively, open docker exec -it nettle sh and configure or diagnose
bbb using your site's approved credential procedure. bbb also supports a
mounted service-principal JSON file selected with
AZURE_APPLICATION_CREDENTIALS, service-principal environment variables, and
storage-account keys. Supply secrets through your site's protected secret
mechanism; do not commit them, put them in blob URLs, expose them in session
links, or assume that az is available inside the image. If your chosen login
or cache needs to write files, mount an appropriately protected writable
directory instead of making the container root writable.
Open http://127.0.0.1:8080, paste an az:// path to a .nettle, .zip,
.tar, .tar.gz, or .tgz blob into the Azure field, and press Enter. Source
archives can optionally specify a relative root filelist. Nettle invokes bbb
to verify the exact blob and its size, copies it into bounded scratch, and
passes it through the same validation, queue, and session pipeline as a normal
upload. A Unix subprocess file-size limit also protects scratch if the remote
blob changes after the size check. The Rust binary contains no Azure SDK and
never acquires or manages Azure credentials. Only the exact value
NETTLE_AZURE_ENABLE=1 enables this workflow.
Check startup, readiness, container logs, and available persistent and scratch storage:
curl --fail http://127.0.0.1:8080/healthz
curl --fail http://127.0.0.1:8080/readyz
docker logs --tail 100 nettle
docker exec nettle df -h /data /scratch
docker volume inspect nettle-sessions
docker system df -v/data is the durable session and queue store; include its Docker volume or
Kubernetes persistent volume in your site's encrypted backup and restore plan.
/scratch is disposable. On Docker Desktop, /scratch is further limited by
the Docker VM's available memory and the size=4g tmpfs ceiling; named-volume
storage consumes the Docker Desktop disk image. Check Docker Desktop's disk
image and memory allocations if either filesystem fills, rather than assuming
free macOS disk space is directly available to the container. Adjust the
scratch size, --max-upload-bytes, queue depth, and build timeout together.
--evict-after=30d automatically removes expired terminal sessions; omit it
only if you intend to retain artifacts indefinitely and monitor storage. Queue
and completed sessions survive restarts, and an interrupted build is recovered
and retried once. Inspect failing sessions and storage before deleting data;
do not modify session directories while the service is running.
To upgrade, build or pull the new digest-pinned image, stop and remove the old
container, and rerun the appropriate start command against the same
nettle-sessions volume. Run
scripts/check-hosted-image.sh nettle:latest before deploying a locally built
image. The smoke test exercises upload, source compilation, persistence,
restart, download, and bundle validation.
Loopback HTTP is suitable only for local evaluation. A shared deployment must terminate HTTPS and restrict reachability to its intended private-cloud user group. Nettle does not authenticate individual users; group authentication at the ingress is optional and site-dependent. Capability URLs are bearer secrets: anyone who can reach the service and has a session URL can view and download its bundle. The UI repeats this fact and the configured retention period before either upload.
The checked-in deploy/kubernetes.yaml is the
recommended private-cloud starting point. It runs a single non-root replica
with Recreate, a PVC backed by the site's encrypted StorageClass, bounded
ephemeral scratch, a read-only root filesystem, dropped capabilities, no
service-account token, and no outbound network. It intentionally leaves
cluster-specific TLS, network access, optional group authentication, hostname,
StorageClass, and image digest configuration to the admin. See
deploy/README.md for setup and operational details.
Build the reference and candidate as independent snapshots, then open schematic diff mode:
cargo run --locked -- compare reference.nettle candidate.nettle \
--matching conservative \
--web-root web/dist \
--port 8090The command copies and validates both inputs in anonymous delete-on-close
storage before starting the viewer and exposes the private snapshots only
through separate non-cacheable startup routes. The two snapshots can consume
up to twice the configured per-bundle temporary-storage ceiling; the operating
system reclaims them when their final handles close, including after abrupt
termination. The default conservative policy accepts exact and uniquely
determined graph correspondence. Pass
--matching aggressive, or switch policy in the viewer, to distinguish
heuristic matches for logic whose compiler-generated identities changed.
The viewer constructs one union graph and lays it out once: reference-only objects are red and dashed, candidate-only objects are green, matched changes are yellow, and unchanged logic remains neutral. Line, outline, and subtle shadow treatments distinguish diff status and heuristic correspondence. The single View menu selects a neutral reference or candidate snapshot, the complete diff overlay, changes-only presentation, or custom status visibility; every view reuses the union geometry. Matching is a structural review aid, not a functional- equivalence result.
Hierarchy navigation and flattening use the same selected policy. Recursive flattening compares child modules before splicing their union through paired boundaries, including one-sided instances, and a policy change recomputes the visible projection. Decoded children are reused while they remain in the bounded provider caches; evicted modules may be decoded again.
The left pane compares the UTF-8 compilation sources embedded in the two
bundles. This is intentionally a bundled source diff, not a complete Git
tree diff: files not included in either snapshot and version-control rename
metadata are unavailable. Different defines, parameters, tops, or compiler
versions are reported separately because they can change the elaborated graph
without changing source text. Diff hunks are labeled source-only only after
neither direct source origins nor indirect evidence anywhere in the reachable
paired hierarchy connects them to a schematic change; selecting a
graph-affecting hunk highlights every directly intersecting object in the
visible overlay.
Builder overrides are repeatable, raw single-line SystemVerilog expressions:
cargo run --locked -- build \
--filelist project.f \
--project-root "$PWD" \
--top top \
--param WIDTH=16 \
--define SYNTHESIS \
--define NUM_HARTS=4 \
--undefine SIMULATION \
--output design.nettleDuplicate names, empty values, invalid identifiers, and define/undefine
conflicts are rejected before compilation. Add --debug-artifacts only when
raw Yosys JSON, Slang AST JSON, and compiler transcripts are needed; those
artifacts can reveal substantially more project detail.
Larger builds can use YAML:
# build.yaml
filelist: filelists/project.f
project_root: .
top: top
output: design.nettle
parameters:
DEPTH: "32"
WIDTH: "16"
defines:
NUM_HARTS: "4"
SYNTHESIS: null
undefines:
- SIMULATION
debug_artifacts: falsecargo run --locked -- build --config build.yamlYAML paths are relative to the configuration file; command-line paths are
relative to the current directory. YAML may also set slang_bin and
yosys_bin. Scalar CLI values override YAML values, while parameter, define,
and undefine collections are combined. Quote expressions and macro values so
YAML preserves them as strings. Unknown fields are rejected. See
INPUT_CONFIG_FILE_FORMAT.md for the complete schema and
merge behavior.
cargo run --locked -- inspect design.nettle
cargo run --locked -- validate design.nettleinspect prints safe summary metadata. validate checks archive membership,
resource limits, hashes, Protobuf payloads, source indexes, and module
index/body consistency without extracting files. See
NETTLE_FILE_FORMAT.md for the normative schema and
compatibility rules.
The unified image can serve the static viewer for existing bundles on linux/amd64:
docker run --rm --platform linux/amd64 -p 127.0.0.1:8090:8080 \
ghcr.io/xlsynth/nettle:latestOn Apple silicon hosts, --platform linux/amd64 selects the published image
architecture.
In a shared deployment, place the static site behind the normal authentication
and HTTPS boundary. Design bytes selected with the file picker remain in each
user's browser. Use the combined image and
deploy/kubernetes.yaml only when upload,
persistence, or server-side compilation is wanted.
scripts/check-rust-docs.py
cargo fmt --all --check
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --locked
cargo test --locked
cargo clippy --all-targets --all-features --locked
npm run lint
npm test
npm run build
npm run check:deployment
npm run test:e2e
scripts/check-toolchain.sh
npm run test:designsThe real-toolchain design regressions are opt-in because they require the external HDL compilers. The Development section shows how to run the complete suite in the known-good container environment.
ARCHITECTURE.mddescribes the software architecture, trust boundaries, data flow, and tool design.NETTLE_FILE_FORMAT.mddefines the.nettleinterchange format and its security rules.INPUT_CONFIG_FILE_FORMAT.mddefines the YAML build configuration schema and command-line merge behavior.RESOURCE_LIMITS_FILE_FORMAT.mddocuments the build-time resource-limit policy shared by the native and browser code.deploy/README.mddocuments the single-Pod hosted-service deployment and its private-cloud responsibilities.
Nettle is licensed under the Apache License 2.0.
Integration tests indirectly use third-party repositories that are referenced by manifests. Third-party sources are not committed into this repository. Nevertheless, manifests identify third-party provenance and applicable license and notice files.

