Skip to content
VLSI Mentor

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
iverilog011
vvp043
nvc100
simtool330

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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.09

Three 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ cat $PROJECT_ROOT/tools.pinned
simtool 2024.09
iverilog 13.0
nvc 1.23.0

And the check belongs at the start of the flow:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/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 ]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ checkpins tools.pinned
simtool    ok        2024.09
iverilog   ok        13.0
nvc        ok        1.23.0
$ echo $?
0

And with a deliberately wrong pin:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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 $?
1

Six 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:

RecordHow
Tool versions§2, for every tool the flow invoked
Tool pathscommand -v, because two installations can share a version string
Source revisiongit rev-parse HEAD
Whether the tree was dirtygit status --porcelain — non-empty means the revision is a lie
The filelist usedChapter 24.5 §4
Defines and optionsthe command line, verbatim
Seedif the test is random
Host, user, directory, locale, umaskChapter 22.6 §7

The dirty-tree check is the one most often missing, and it is the one that makes a revision meaningful:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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

  1. Ask three tools for their version with -V, --version and -version. Build the status table.
  2. For any flag that printed an error, confirm the status is non-zero. Were any statuses other than 1?
  3. Write a probe that classifies by message text and find a message it gets wrong.
  4. Write tool_version and run it for every tool in your flow. Any that return 1?
  5. Move the head -1 into a pipeline with the tool and explain what status you now read.
  6. Extract a version field for three tools. Are the field positions the same?
  7. Try [ "1.9" \< "1.10" ] and explain the result.
  8. Check whether sort -V exists on your machines. All of them?
  9. Write a tools.pinned file for a project and commit it.
  10. Write checkpins and run it. Then change one pin and confirm the mismatch and exit 1.
  11. Make a pin for a tool that is not installed and confirm MISSING is distinct from MISMATCH.
  12. Add a tool whose format version_field does not know and confirm the whole line is recorded.
  13. Record a revision with uncommitted changes and show how your record distinguishes it.

Check Your Understanding

  1. How many different accepted version flags across the four tools measured?
  2. Why must a flag probe classify by exit status rather than message text?
  3. What did vvp return for its two rejected flags, and why does that matter?
  4. Why must head -1 come after the capture rather than in a pipeline with the tool?
  5. Why does an accepted flag that prints nothing need its own check?
  6. Why is version-field extraction per-tool rather than a general regex?
  7. Why does an unknown format record the whole line?
  8. Why is [ "1.9" \< "1.10" ] false?
  9. Why prefer an equality check to a minimum-version check?
  10. Why must checkpins ask the tool rather than read SIMTOOL_VERSION?
  11. 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 option as success.
  • Rejected flags return different non-zero statuses — vvp gave 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 -V writes its version to stderr while iverilog -V writes to stdout — same project, same package. Capture 2>&1 and let the status be the gate.
  • head -1 after the capture, never in a pipeline with the tool, or you read head'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 -V is 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.