Running Builds in CI#
A SiliconCompiler build is an ordinary Python script, so any CI system that runs Python runs it. The interesting part is not getting it to run once – it is getting it to run in minutes rather than an hour, and to fail for the right reasons.
Start with what costs nothing#
Not every check needs EDA tools. The cheapest gates catch most breakage and run in seconds on a stock runner:
- run: pip install siliconcompiler
- run: python -c "from mydesign import design; assert design().check_filepaths()"
- run: python lint.py
Linting needs no tools at all – the default linter ships as a Python package. Formal checks and simulation need a simulator but no PDK. Run these on every push, and save the full flow for something less frequent.
The two things that make it practical#
Do not install tools per job. Use the Docker image, which has them already:
jobs:
build:
runs-on: ubuntu-latest
container: ghcr.io/siliconcompiler/sc_runner:latest
steps:
- uses: actions/checkout@v4
- run: python make.py
Pinning :latest is a trap, though: the image drifts from the release you
meant, and you end up debugging a tool version nobody chose. Ask
SiliconCompiler which image to use. It publishes a reusable workflow that
resolves the exact digest for a tool set, which you call rather than reimplement:
jobs:
image:
uses: siliconcompiler/siliconcompiler/.github/workflows/[email protected]
with:
sc_version: v0.38.2 # 'latest' resolves to the newest release
tool: runner # SiliconCompiler + the tools; see below
build:
needs: image
runs-on: ubuntu-latest
container: ${{ needs.image.outputs.sc_tool }}
steps:
- uses: actions/checkout@v4
- run: python make.py
Pin both: the @ ref decides which workflow definition runs, and
sc_version decides which image it resolves. A tag is the readable choice; a
commit SHA is the strict one, since a tag can be moved. latest and @main
are convenient and reproducible only until someone releases.
sc_tool comes back fully qualified – ghcr.io/siliconcompiler/sc_runner:v0.38.2
for runner, or a digest-pinned sc_tools:af23682… for tools – so the
job moves forward only when you change sc_version. Behind
docker_image.yml
is setup/docker/builder.py, which derives the image from the tool versions
pinned in _tools.json, so the container and the tool pins cannot disagree.
Two images are published and they are not interchangeable:
Image |
Contains |
|---|---|
|
|
|
|
Getting this backwards is the easy mistake: sc_tools followed by
python make.py fails on ImportError: siliconcompiler.
Or build your own with sc-install and reuse it. Either way the install happens once, not once per job.
Cache the data. PDKs, libraries and git-fetched sources are downloaded into the cache on first use. Point it somewhere your CI can restore:
- uses: actions/cache@v4
with:
path: ~/.sc/cache
key: sc-cache-${{ hashFiles('**/make.py') }}
- run: python make.py
~/.sc/cache is the default location, so caching that path is enough. To put
it somewhere else, set it in the script – there is no environment variable for
it:
project.option.set_cachedir("/mnt/ci-cache")
Without this every job re-downloads the PDK, which usually costs more than the build.
Failing for the right reason#
Project.run() raises on a failed run, so a build that cannot complete
fails the job by itself. What it will not do is fail because the result got
worse – that is yours to assert. Read the metrics from the history object
run() returns, not from the live project, which is reset when the job ends:
history = project.run()
project.summary()
slack = history.get("metric", "setupslack", step="route", index="0")
assert slack >= 0, f"timing did not close: {slack}"
For anything beyond a couple of assertions, checklists are the structured form – a named set of criteria checked against recorded metrics, which is a better artifact than a wall of asserts and produces the same pass/fail.
Tip
Record the numbers, not just the verdict. Publishing
build/<design>/<jobname>/<design>.pkg.json as an artifact means a
regression can be diffed against the last good run rather than re-argued.
Directory structures says what else is worth
keeping.
Keeping it fast#
Split the flow.
[option,from]/[option,to]run synthesis on every push and the full flow nightly.Bound the parallelism. CI runners are small; a flow built with
_npwidths that suit a workstation will thrash. See[option,scheduler,maxnodes]and Parallel Job Execution.Give each configuration its own jobname so results do not overwrite each other – Multi-Job Flows.
A worked reference#
SiliconCompiler’s own CI does all of the above: it runs inside a prebuilt tools container, restores the data cache between jobs, and splits fast checks from the nightly EDA runs. The workflows under .github/workflows are the reference implementation, and are worth copying from rather than starting fresh.
See also
When a Run Fails – what to collect from a CI failure
so it can be diagnosed without re-running it, and sc-issue for packaging
a reproduction out of a build directory.