SDK Behavior Contract
Status: Living specification — maintained alongside the codebase.
This document is the single truth source for what the RouteRTL SDK must do automatically and what the consumer is responsible for. Use it to validate SDK behavior, write tests, and guide new features.
1. Source Discovery
1.1 Explicit Sources
| Input | Source | Resolution |
|---|---|---|
project.yml → paths.sources | Consumer project | Relative to project root |
build_env.mk → SRC_DIR | Generated by project_manager.py | Takes priority over project.yml |
--src CLI argument | Direct invocation | Absolute or relative paths |
1.2 Auto-Injected Sources
| Path | Condition | Purpose |
|---|---|---|
SDK src/units/ | Pip-installed, no vendor/routertl submodule | Utility entities (edge_detector, infilter, etc.) |
Parity Rule: Every entity/package available in the submodule workflow (
vendor/routertl/src/units/) must be equally discoverable in the pip workflow. If a consumer canentity tich.Xin submodule mode, it must also work in pip mode without extra configuration.
1.3 Gitignore Filtering
- Files inside
.gitignore-matched directories are excluded by default - Exception: Files under an explicitly-requested
--srcpath are always included (prevents pip SDK paths like.venv/site-packages/...from being filtered) - Disable with
--no-respect-gitignore
1.4 Gitignore Management (Pre-Build Outputs)
rr ws update-configsyncshooks.pre_build[*].expected_outputsinto.gitignoreusing an idempotent sentinel block- If outputs are added/changed, the sentinel block is replaced (not duplicated)
- If hooks are removed, the sentinel block is cleaned up
- Paths containing
..or starting with/are silently skipped (safety) - No-op if
.gitignoredoesn't exist (consumer must create it viarr init)
1.5 Path Resolution and Source Scanning
The paths.* fields control directory scanning (rr ws update),
not the actual build file list (that is sources.syn).
| Path Field | Scan Method | Extensions | Filter |
|---|---|---|---|
paths.sources | Recursive (os.walk) | *.vhd, *.v, *.sv | None |
paths.simulation | Recursive (os.walk) | *.vhd, *.v, *.sv | tb_* prefix only |
paths.constraints | Recursive (os.walk) | Vendor-aware (.xdc, .sdc, .lpf, .pcf) | None |
Multi-directory union: When paths.sources is a list, all directories
are scanned and results are deduplicated against existing sources.syn
entries before insertion. Files retain their relative project-root paths.
1.6 RTL Testbenches vs. Python Tests
| Concept | Field | File Pattern | Populates |
|---|---|---|---|
| RTL testbench wrappers | paths.simulation | tb_*.vhd, tb_*.v, tb_*.sv | sources.sim |
| Python Cocotb tests | simulation.test_dir | test_*.py | hooks.pre_commit.tests: auto |
Contract: These are separate file types serving separate purposes.
A tb_* VHDL file wraps the DUT for HDL simulation; a test_*.py file
drives the simulation via Cocotb's Python API. Consumers must populate both
for a complete verification flow.
1.7 Auto-Sources Merge Semantics
When auto_sources: true, the DependencyResolver crawls from
project.top_module and appends resolved files to sources.syn.
Contract:
- Resolved files must not replace existing
sources.synentries - Duplicates must be skipped silently
- Results must be cached in
.routertl_cache/dependencies.json
The resolver uses
paths.sourcesfor its search directories (supports both string and list forms). Falls back tosrc/if unset.
1.8 Configuration Precedence Chain
Values flow through multiple layers. Later layers override earlier ones:
SDK defaults → project.yml → build_env.mk (?= only) → CLI arguments
| Example | project.yml | build_env.mk | CLI |
|---|---|---|---|
| Source dirs | paths.sources: src | SRC_DIR ?= src | --src hw/rtl |
| Tool version | tool_version: "2024.1" | VIVADO_VER ?= 2024.1 | N/A |
| Tool version override | vivado_version: "2024.2" | VIVADO_VER := 2024.2 | N/A |
?=(conditional assignment) only takes effect if the variable is not already set.:=(unconditional) always overrides. Vendor-specific version fields emit:=to guarantee precedence over the generictool_version.
2. Dependency Management
2.1 XPM Library (Vendor Primitives)
The XPM library provides VHDL simulation models for Xilinx primitives
(FIFOs, CDCs, etc.). Any unit that uses library xpm; use xpm.vcomponents.all;
requires these stubs.
| Stage | Current Behavior | Expected Behavior |
|---|---|---|
| Declaration | project.yml → dependencies.xpm_vhdl | Same |
| Fetch | rr deps fetch clones repo | Same |
| Export | project_manager.py → build_env.mk → XPM_FILES | Same |
| Linting | hardware.py reads build_env.mk, passes --xpm | ✅ Auto-resolves (P1.49) |
| Pip install | ✅ Fallback reads project.yml directly (P1.49) | ✅ Resolved |
2.2 Unisim Library (Xilinx Primitives)
The unisim library provides VHDL simulation models for Xilinx I/O and
clock primitives (BUFG, IBUFDS, MMCM, etc.). SDK utility wrappers
(unisim_bufg_wrap.vhd, unisim_ibufds_wrap.vhd, unisim_mmcm_wrap.vhd)
use library unisim; use unisim.vcomponents.all;.
| Stage | Current Behavior | Expected Behavior |
|---|---|---|
| Declaration | Not standardized | project.yml → dependencies.unisim |
| Fetch | rr eda install-unisim | Same |
| Linting | ✅ Auto-compile via vendor_libraries cascade (P1.50) | ✅ Resolved |
Resolved (P1.50): The linting command now calls
ensure_ghdl_unisim()fromvendor_libraries.pybefore invoking the smart linter. The 5-tier cascade (precompiled → Vivado → cached → download → actionable warning) handles all environments automatically.
2.3 Custom Libraries
Custom libraries (e.g. tich) are auto-discovered from source code via
library <name>; declarations and entity <lib>.X instantiations.
Contract:
- Auto-discovery must find all library members without manual config
- Package pre-compilation into the library must precede entity compilation
- Topological sort must handle intra-library dependencies
- Manual
libraries.jsonoverrides act as extensions, not requirements
2.4 Transitive Dependencies
The rr deps fetch system resolves transitive dependencies from ip.yml
manifests. Each dependency declares:
dependencies:
xpm_vhdl:
url: https://github.com/fransschreuder/xpm_vhdl.git
fallback_url: https://github.com/djmazure/xpm_vhdl.git
path: libs/xpm_vhdl
library: xpm # maps to XPM_FILES in build_env.mk
Contract: If a dependency has library: xpm, its source files
must be exported as XPM_FILES and precompiled before any consumer
source that references library xpm.
2.5 Vendor and Simulator Selection
Vendors: The SDK recognizes xilinx, intel, altera, lattice,
and microchip. altera and intel are treated as equivalent — both
resolve to the Quartus toolchain. As of 2025, Intel divested the FPGA
business and Altera recovered its original name, so altera is the
preferred value for new projects.
Simulators: The SDK supports nvc, ghdl, questa, verilator,
icarus, and riviera.
| Simulator | Languages | Default For |
|---|---|---|
nvc | VHDL, mixed | VHDL-only projects |
ghdl | VHDL, mixed | — |
questa | VHDL, SV, mixed | — |
verilator | Verilog, SystemVerilog | Verilog-only projects |
icarus | Verilog, SystemVerilog | — |
riviera | VHDL, SV, mixed | — |
Auto-selection: When no simulation.simulator is configured and the
design contains only Verilog/SystemVerilog sources with no VHDL, the SDK
auto-switches to verilator. Conversely, if the configured simulator is
verilator but VHDL sources are detected, the SDK emits a warning.
3. Linting Pipeline
3.1 Phase Ordering
Phase 1: PRECOMPILE → XPM stubs, utility pkgs, project pkgs Phase 1.5: COMPILE_LIB → Custom libraries (topo-sorted) Phase 2a: LINT (VHDL) → Hierarchy files via GHDL Phase 2b: LINT (SV) → SystemVerilog files via Verilator
Contract: Each phase must complete before the next begins. Failure in Phase 1 or 1.5 must halt the pipeline with a clear diagnostic.
3.2 Incremental Compilation
- GHDL work directories persist in
sim/work/ - Files are skipped when their timestamp matches the cached
.cfcatalog rr linting --cleanorrm -rf sim/work/forces full recompilation- Stale work directories are auto-detected via
.cfhealth check
3.3 Exit Code Contract (Dual-Mode Architecture)
smart_linter.pyalways exits 0 regardless of lint result. The actual status is written tosim/lint.status. Consumers must readsim/lint.statusfor CI integration.
Why exit 0? The linter is invoked by both Make targets and the
Python CLI. Make aborts the entire recipe on non-zero exit, preventing
multi-hierarchy analysis. The exit 0 + status-file pattern lets all
hierarchies complete before the caller inspects the result.
Three consumers read sim/lint.status:
rr linting— reads the file and sets the CLI exit codetools/githooks/pre-commit— gates commits on status ≠ 0 (P1.33)project-pre-commit— consumer hook, same gate (P1.33)
Do NOT change exit_with_status() to propagate the real exit code
without updating all three consumers.
4. Pip vs. Submodule Parity
Every CLI command must produce identical functional results regardless of install method.
| Capability | Submodule | Pip | Parity Status |
|---|---|---|---|
rr linting | ✅ Full pipeline | ✅ With P1.48 fix | ✅ Parity |
rr info --forest | ✅ | ✅ | ✅ Parity |
rr hierarchy | ✅ | ✅ | ✅ Parity |
rr sim | ✅ | ✅ | ✅ Parity |
rr doctor | ✅ | ✅ | ✅ Parity |
XPM stubs for library xpm | ✅ via build_env.mk | ✅ via P1.49 fallback | ✅ Parity |
Unisim stubs for library unisim | ✅ Auto-compile (P1.50) | ✅ Auto-compile (P1.50) | ✅ Parity |
build_env.mk generation | ✅ Auto via Make | ⚠️ Manual via rr ws update-config | ⚠️ Partial |
| Custom library auto-discovery | ✅ | ✅ | ✅ Parity |
| SDK utility entities | ✅ via project.yml sources | ✅ via P1.48 auto-injection | ✅ Parity |
5. Error Taxonomy
5.1 SDK Bugs (Must Fix)
Failures where the SDK silently produces wrong results or fails without actionable diagnostics:
- Missing entity in auto-discovered library (source file not in
--src) Unisim stubs needed but not precompiled (P1.50 — resolved)- Custom library package compiled after entity that depends on it
5.2 Consumer-Fixable Issues
Failures that require consumer action, with clear error messages:
- GHDL/Verilator/Icarus not installed →
rr doctordiagnostic - Missing
project.yml→rr initsuggestion - Uncloned dependency →
rr deps fetchsuggestion - Invalid entity name →
_clean_top()validation
5.3 Environment Limitations
Known limitations that cannot be fixed by either SDK or consumer:
- GHDL+Cocotb 2.0+
HierarchyArrayObjecterrors on scalar signals - Vendor tools not available (no license, no installation)
6. Test Oracle Rules
Each contract clause maps to a testable assertion:
| Contract | Test Type | Location |
|---|---|---|
| SDK units auto-injected for pip | Integration | test_pip_install.sh + rr linting |
| XPM files precompiled when declared | Unit | test_regression.py::TestDependencyResolution |
| Custom libs auto-discovered | Unit | test_dependency_resolver.py |
| Topo sort within library | Unit | test_smart_linter.py |
| Pip = Submodule parity | Integration | test_pip_install.sh |
sim/lint.status written | Unit | test_smart_linter.py::TestLintStatus |
Gitignore exemption for --src | Unit | test_dependency_resolver.py |
Pre-build outputs synced to .gitignore | Unit | test_regression.py::test_gitignore_prebuild_sync |
| All imports declared in pyproject.toml | Audit | §7 dependency audit |
7. Python Dependency Contract
Every third-party package imported anywhere in the shipped SDK
(sdk/, sim/, routertl_core/, src/, tcl/) must be
declared in pyproject.toml → [project].dependencies.
No silent crashes. If a pip consumer runs
pip install routertland then invokes anyrrcommand or simulation flow, it must work without additionalpip installsteps. There are no optional simulation dependencies — cocotb is the simulation engine.
7.1 Declared Dependencies (Audit: 2026-03-13)
| Package | Min Version | Used By | Purpose |
|---|---|---|---|
click | ≥8.0.0 | sdk/cli/ | CLI framework |
rich | ≥13.0 | sdk/cli/, routertl_core/ | Terminal UI |
rich-click | ≥1.7.0 | sdk/cli/ | Rich-enhanced Click |
pyyaml | ≥6.0 | sdk/engine/, sdk/cli/ | YAML parsing |
docker | ≥7.0.0 | sdk/cli/commands/docker/ | Docker SDK |
jinja2 | ≥3.1.0 | sdk/cli/commands/ci.py | Template engine |
pathspec | ≥0.11.0 | routertl_core/ | Gitignore matching |
tomli | ≥1.1.0 | sdk/cli/ (Python <3.11) | TOML backport |
cocotb | ≥2.0 | sim/cocotb/ (118 imports) | Simulation engine |
7.2 Vendored / Editable Dependencies
| Package | Source | Mechanism |
|---|---|---|
cocotbext-i2c | sim/cocotb/packages/cocotbext-i2c/ | pip_bootstrap.py editable install |
7.3 Dev-Only (Not Shipped)
| Package | Scope |
|---|---|
pytest | Test runner |
ruff | Linter |
shellcheck | Shell linter |
mkdocs + plugins | Documentation |
8. Hook API Contract
Pre-build hook scripts must use sdk.api.tools for vendor tool
resolution instead of calling shutil.which() directly.
8.1 Public API Surface
| Function | Module | Purpose |
|---|---|---|
resolve_tool_binary(name) | sdk.api.tools | Find vendor tool; probes tool_path then PATH |
resolve_tool_path() | sdk.api.tools | Read raw hardware.tool_path from project.yml |
load_project_config() | sdk.api.tools | Load project.yml as a dict |
get_hardware_config() | sdk.api.tools | Extract hardware section |
get_project_root() | sdk.api.tools | Walk up to find project.yml |
8.2 Resolution Contract
resolve_tool_binary("quartus_sh") must:
- Check
hardware.tool_pathfromproject.ymlfirst - Probe vendor-specific subdirectories (
bin/,quartus/bin/,Vivado/bin/,Designer/bin/,Designer/bin64/) - Fall back to
shutil.which()only iftool_pathis unset or the binary is not found under it
Contract: The tool_path binary must always take priority over
whatever is on system PATH. This prevents edition mismatches (e.g.
Quartus Pro on PATH vs Standard in project.yml).
8.3 Backward Compatibility
sdk.cli.commands.license re-exports resolve_tool_binary for
backward compatibility. Existing code using the old import path
continues to work. New code should import from sdk.api.tools.
| Oracle | Test | Location |
|---|---|---|
| All 5 functions importable | Import contract | test_tool_api.py::TestImportContracts |
tool_path priority over PATH | Resolution order | test_tool_api.py::TestResolveToolBinary |
| Backward compat re-export | Import alias | test_tool_api.py::test_backward_compat_license_reexport |
check_vendor_tool uses sdk.api.tools | Guard test | test_package_separation.py::TestDoctorMockTargets |