Linux & Bash · Part II · Module 25 · EDA Environment and Tool Access
Checking and Pinning Tool Versions
A result is only reproducible if you know what produced it, and the tool version is the part people record least and need most. This chapter is how to ask a tool its version when no two tools agree on how to ask, how to pin a version so a flow refuses to run against the wrong one, and the measured reason the obvious way of probing for a flag is wrong.
Intermediate21 min readLinuxPart IIVersionsReproducibilityEDA
Part II · Module 25 · Chapter 25.7 · Checking and Pinning Tool Versions
Three months after a tape-out someone asks which simulator version produced a coverage number. If the answer is not in a file, there is no answer.
1. No Two Tools Agree on How to Ask
Four tools are installed here. Every one of them was asked the same three questions, and the statuses were read directly rather than inferred from the messages:
| Tool | -V | --version | -version |
|---|---|---|---|
iverilog | 0 | 1 | 1 |
vvp | 0 | 4 | 3 |
nvc | 1 | 0 | 0 |
simtool | 3 | 3 | 0 |
Three different accepted spellings across four tools, and nvc accepts two. Commercial tools add more: -ID, -v, -ver, --versions, and some require a subcommand.
So the first fact about version checking is that there is no portable version flag, and a script that assumes one is wrong on three of these four tools.
2. Asking Robustly
Try the spellings in order and stop at the first that succeeds:
tool_version() { # $1 = command -> prints one line, or returns 1
local t=$1 f out
for f in -version --version -V -ID -v; do
out=$("$t" "$f" 2>&1) || continue # 2>&1: see the callout
out=$(printf '%s\n' "$out" | head -1)
[ -n "$out" ] || continue
printf '%s\n' "$out"
return 0
done
return 1
}$ tool_version iverilog
Icarus Verilog version 13.0 (stable) (v13_0)
$ tool_version nvc
nvc 1.23.0 (Using LLVM 23.1.1)
$ tool_version simtool
simtool 2024.09 (simlib 1.4.2)Five decisions:
|| continue on the capture, so a rejected flag moves to the next one rather than aborting. That is the whole mechanism.
2>&1, not 2>/dev/null — which is the opposite of what the first version did, for the measured reason in the callout below. A rejected flag's complaint is discarded anyway, because || continue fires on its non-zero status before the output is used.
head -1 after the capture, not in a pipeline with the tool. "$t" "$f" | head -1 would give you head's status — Chapter 12.8, and the single most common way a probe like this silently accepts a failure.
A non-empty check, because a flag can be accepted and print nothing, and an empty version string recorded in a log is worse than none.
And -version first, because it is the spelling most EDA tools use. The order is a heuristic and the correctness does not depend on it — only the number of attempts does.
3. A Version Is Not a Version String
Icarus Verilog version 13.0 (stable) (v13_0) is a sentence. For a comparison you need a field:
$ printf '%s|%s|%s\n' \
> "$(iverilog -V 2>&1 | head -1 | awk '{print $4}')" \
> "$(nvc --version | head -1 | awk '{print $2}')" \
> "$(simtool -version | awk '{print $2}')"
13.0|1.23.0|2024.09Three tools, three different field positions. There is no way around this: every tool prints its version in its own sentence, so extraction is per-tool and belongs in a table rather than in a clever regex.
# the field to take, per tool. Add a row when you add a tool.
version_field() {
case $1 in
(iverilog|vvp) echo 4 ;;
(nvc) echo 2 ;;
(simtool) echo 2 ;;
(*) echo 0 ;; # 0 = we do not know; record the whole line
esac
}The 0 default matters. A tool whose format you have not encoded should have its whole version line recorded rather than a guessed field — because a wrong field recorded confidently is worse than a long string recorded honestly, and Chapter 23.2 §6's rule about a classifier that cannot abstain applies to parsing too.
4. Pinning: Refusing to Run Against the Wrong Version
A pin is a declaration plus a check. The declaration belongs in the project, in a file:
$ cat $PROJECT_ROOT/tools.pinned
simtool 2024.09
iverilog 13.0
nvc 1.23.0And the check belongs at the start of the flow:
#!/usr/bin/env bash
# checkpins — verify every pinned tool is the pinned version
# usage: checkpins [pinfile]
# exit: 0 all match · 1 a mismatch or a missing tool · 2 unusable
set -o nounset
pins=${1:-${PROJECT_ROOT:?set PROJECT_ROOT}/tools.pinned}
[ -r "$pins" ] || { printf 'checkpins: cannot read %s\n' "$pins" >&2; exit 2; }
tool_version() {
local t=$1 f out
for f in -version --version -V -ID -v; do
out=$("$t" "$f" 2>&1) || continue # 2>&1: see the callout
out=$(printf '%s\n' "$out" | head -1)
[ -n "$out" ] || continue
printf '%s\n' "$out"
return 0
done
return 1
}
version_field() {
case $1 in (iverilog|vvp) echo 4 ;; (nvc|simtool) echo 2 ;; (*) echo 0 ;; esac
}
bad=0
while IFS= read -r line || [ -n "$line" ]; do
line=${line%%#*}
set -- $line
[ $# -ge 2 ] || continue
tool=$1 want=$2
path=$(command -v -- "$tool" 2>/dev/null) || {
printf '%-10s MISSING (wanted %s)\n' "$tool" "$want"; bad=1; continue; }
full=$(tool_version "$tool") || {
printf '%-10s NO VERSION (at %s)\n' "$tool" "$path"; bad=1; continue; }
fld=$(version_field "$tool")
if [ "$fld" -eq 0 ]; then
have=$full
else
have=$(printf '%s\n' "$full" | awk -v n="$fld" '{print $n}')
fi
if [ "$have" = "$want" ]; then
printf '%-10s ok %s\n' "$tool" "$have"
else
printf '%-10s MISMATCH have %s, want %s\n' "$tool" "$have" "$want"
printf '%-10s %s\n' '' "$full"
bad=1
fi
done < "$pins"
[ "$bad" -eq 0 ]$ checkpins tools.pinned
simtool ok 2024.09
iverilog ok 13.0
nvc ok 1.23.0
$ echo $?
0And with a deliberately wrong pin:
$ printf 'simtool 2025.03\n' > /tmp/bad.pinned
$ checkpins /tmp/bad.pinned
simtool MISMATCH have 2024.09, want 2025.03
simtool 2024.09 (simlib 1.4.2)
$ echo $?
1Six decisions:
line=${line%%#*} then set -- $line. Stripping comments with parameter expansion forks nothing, and set -- re-splits the line into $1 and $2 using normal word splitting — which is exactly right for a two-column file and avoids read tool want leaving trailing junk in want.
|| [ -n "$line" ] on the read, so a final line with no newline is processed — Chapter 17.5 §4, and a pin file is hand-edited.
A missing tool and an unknown version are distinct lines, not one "FAIL". They have different causes: the first is PATH, the second is an unencoded version format.
The full version line is printed on a mismatch. Three characters of a version number are not enough to debug with; the sentence tells you which build and often which patch.
bad=1 rather than an early exit, so the report lists every problem. A pin check that stops at the first mismatch makes you run it four times.
And the whole-line fallback when the field is unknown, per §3.
5. What Else Belongs in the Record
The tool version alone is not enough to reproduce a result. The minimum useful set, with where each comes from:
| Record | How |
|---|---|
| Tool versions | §2, for every tool the flow invoked |
| Tool paths | command -v, because two installations can share a version string |
| Source revision | git rev-parse HEAD |
| Whether the tree was dirty | git status --porcelain — non-empty means the revision is a lie |
| The filelist used | Chapter 24.5 §4 |
| Defines and options | the command line, verbatim |
| Seed | if the test is random |
| Host, user, directory, locale, umask | Chapter 22.6 §7 |
The dirty-tree check is the one most often missing, and it is the one that makes a revision meaningful:
rev=$(git rev-parse --short HEAD 2>/dev/null || echo unknown)
dirty=$(git status --porcelain 2>/dev/null | grep -c . || true)
[ "$dirty" -eq 0 ] || rev="$rev+$dirty-modified"
printf 'source %s\n' "$rev"A revision with uncommitted changes on top of it does not identify the source that was compiled. Recording a1b2c3d+7-modified is honest; recording a1b2c3d is a false claim that someone will act on three months later.
Chapter 25.8 is the whole record, written into the run directory.
Practice
- Ask three tools for their version with
-V,--versionand-version. Build the status table. - For any flag that printed an error, confirm the status is non-zero. Were any statuses other than 1?
- Write a probe that classifies by message text and find a message it gets wrong.
- Write
tool_versionand run it for every tool in your flow. Any that return 1? - Move the
head -1into a pipeline with the tool and explain what status you now read. - Extract a version field for three tools. Are the field positions the same?
- Try
[ "1.9" \< "1.10" ]and explain the result. - Check whether
sort -Vexists on your machines. All of them? - Write a
tools.pinnedfile for a project and commit it. - Write
checkpinsand run it. Then change one pin and confirm the mismatch and exit 1. - Make a pin for a tool that is not installed and confirm MISSING is distinct from MISMATCH.
- Add a tool whose format
version_fielddoes not know and confirm the whole line is recorded. - Record a revision with uncommitted changes and show how your record distinguishes it.
Check Your Understanding
- How many different accepted version flags across the four tools measured?
- Why must a flag probe classify by exit status rather than message text?
- What did
vvpreturn for its two rejected flags, and why does that matter? - Why must
head -1come after the capture rather than in a pipeline with the tool? - Why does an accepted flag that prints nothing need its own check?
- Why is version-field extraction per-tool rather than a general regex?
- Why does an unknown format record the whole line?
- Why is
[ "1.9" \< "1.10" ]false? - Why prefer an equality check to a minimum-version check?
- Why must
checkpinsask the tool rather than readSIMTOOL_VERSION? - Why record whether the tree was dirty?
6. Remember
- There is no portable version flag. Measured: three different accepted spellings across four tools.
- Classify by exit status, not message text. A text classifier passed
illegal optionas success. - Rejected flags return different non-zero statuses —
vvpgave 4 and 3 — so never test for 1. - Try the spellings in order with
|| continue, capture both streams, and check for an empty result. vvp -Vwrites its version to stderr whileiverilog -Vwrites to stdout — same project, same package. Capture2>&1and let the status be the gate.head -1after the capture, never in a pipeline with the tool, or you readhead's status.- Version-field extraction is per-tool. Keep a table; default to recording the whole line.
- A parser that cannot abstain states something false.
- Version numbers are not strings and not decimals.
[ "1.9" \< "1.10" ]is false;sort -Vis a GNU extension. - Prefer equality to a minimum. Reproducibility is an equality property.
- A pin file is a committed, checked declaration; a setup variable is a choice. You want both.
- The check must ask the tool, or it only restates the intention.
- Report every mismatch, not the first, and print the full version line.
- Record paths as well as versions — two installations can share a version string.
- Record whether the tree was dirty. A bare revision over uncommitted changes is a false claim.
7. What Is Next
Chapter 25.8 closes the module by writing all of it down: the environment a run actually used, captured into the run directory, in a form that lets someone else reproduce it exactly rather than approximately.
Where this fits
Part of the Linux & Bash curriculum.
