Hook Authoring Guide
Write custom pre-build hooks that integrate cleanly with the RouteRTL SDK — using the public API instead of reinventing tool discovery.
Quick Start
A pre-build hook is a Python script declared in project.yml:
hooks:
pre_build:
- name: emif-generator
script: tools/gen_emif.py
args: [--output, gen/ip/emif_wrapper.vhd]
expected_outputs:
- gen/ip/emif_wrapper.vhd
watch:
- src/software/**/*.c
- src/software/**/*.h
- CMakeLists.txt
The hook runner (rr project run) executes the script before project
generation and validates that expected_outputs exist afterward.
Source staleness detection (watch)
If watch globs are declared, the runner compares watched file timestamps
against expected_outputs. If all sources are older than the oldest output,
the hook is skipped — saving rebuild time. If any source is newer (or
any output is missing), the hook re-runs.
Without watch, hooks always run (backward-compatible default).
Always declare
watchfor hooks that compile firmware or generate code from source files. A missingwatchfield means synthesis can silently bake stale firmware into the bitstream.
Using the SDK API
Do not call
shutil.which()directly in hook scripts. The SDK providesresolve_tool_binary()which respectshardware.tool_pathfromproject.yml— ensuring the correct vendor tool edition is used.
Finding Vendor Tools
#!/usr/bin/env python3
"""EMIF wrapper generator — pre-build hook example."""
from sdk.api.tools import resolve_tool_binary
# Finds quartus_sh respecting project.yml's hardware.tool_path
quartus = resolve_tool_binary("quartus_sh")
if not quartus:
print("❌ quartus_sh not found")
sys.exit(1)
# Use the resolved path to invoke qsys-generate, ip-generate, etc.
subprocess.run([quartus, "--script", "gen_emif.tcl"], check=True)
Reading Project Configuration
from sdk.api.tools import load_project_config, get_hardware_config
# Load the full config
config = load_project_config()
# Or load from a specific path
config = load_project_config("path/to/project.yml")
# Extract hardware section
hw = get_hardware_config(config)
print(f"Vendor: {hw.get('vendor')}, Part: {hw.get('part')}")
Finding the Project Root
from sdk.api.tools import get_project_root
root = get_project_root() # walks up from CWD to find project.yml
if root:
print(f"Project root: {root}")
API Reference
| Function | Signature | Description |
|---|---|---|
resolve_tool_binary | (name, config=None, project_yml="project.yml") | Find vendor tool binary; checks tool_path first, then PATH |
resolve_tool_path | (config=None, project_yml="project.yml") | Read raw hardware.tool_path from config |
load_project_config | (project_yml="project.yml") | Load project.yml as a dict (empty dict on error) |
get_hardware_config | (config=None, project_yml="project.yml") | Extract hardware section from config |
get_project_root | (start=".") | Walk up from start to find project.yml |
All functions accept an optional config dict parameter. When provided,
the function uses the pre-loaded config instead of reading from disk —
useful for testing and for hooks that need multiple config lookups.
Tool Resolution Order
resolve_tool_binary("quartus_sh") probes in this order:
{tool_path}/quartus_sh— direct{tool_path}/bin/quartus_sh{tool_path}/quartus/bin/quartus_sh{tool_path}/Vivado/bin/quartus_sh{tool_path}/Designer/bin/quartus_sh{tool_path}/Designer/bin64/quartus_shshutil.which("quartus_sh")— fallback to$PATH
The
tool_pathprobing covers Intel (Quartus), Xilinx (Vivado), and Microchip (Libero/Designer) install layouts. If your vendor tool is in a non-standard location, sethardware.tool_pathto the install root directory.
Hook Lifecycle
rr project run │ ├─ Read project.yml ├─ Inject hardware.tool_path into subprocess PATH (P2.102) │ ├─ For each pre_build hook: │ ├─ Check watch vs output mtimes (skip if up-to-date, P1.17) │ ├─ Clean expected_outputs (if clean_before_run: true) │ ├─ Execute script with args │ └─ Validate expected_outputs exist + are fresh (P1.16) │ └─ Exit 0 (all hooks passed) or exit 1 (any hook failed)
Environment: Hook scripts run in a subprocess with tool_path
prepended to PATH. This means shutil.which() will find the
correct vendor tool — but using resolve_tool_binary() is still
recommended for clarity and for hooks that might be invoked outside
the rr project run flow (e.g. directly from a Makefile).
Best Practices
- Use
sdk.api.tools— never callshutil.which()for vendor tools - Declare
expected_outputs— the runner validates them; they're auto-synced to.gitignore - Declare
watchglobs — prevents stale outputs from surviving into synthesis when sources change - Exit non-zero on failure — the runner checks
sys.exit()codes - Accept
--outputarguments — makes hooks testable independently - Keep hooks idempotent — they may run multiple times during development