Skip to content
VLSI Mentor

Linux & Bash · Part II · Module 33 · Debugging Failed Jobs and Broken Flows

Missing, Stale or Wrong Environment

A variable can be unset, it can point at something that has gone, or it can point at something real that is not what you meant — and those three go wrong in increasing order of damage and decreasing order of noise. This chapter measures all three against two real project areas, then builds the check that separates them before any tool starts.

Advanced25 min readLinuxPart IIDebuggingEnvironmentValidation

Part II · Module 33 · Chapter 33.4 · Missing, Stale or Wrong Environment

Chapter 33.3 listed the environment as the fourth of four differences and found it the weakest suspect for a script you run yourself. That is true for whether a variable arrives. It is not true for what the variable says, which is this chapter, and which produces the quietest failure in the module.

1. Three Ways, Not One

A flow wrapper that trusts a project root — the shape of almost every real one:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash
set -u
test=${1:-smoke}; d=${2:?need an outdir}
mkdir -p "$d"
( cd "$d" && exec vvp "$PROJ_ROOT/logs/sim.vvp" +TESTNAME="$test" +SEED=1 +CYCLES=200 ) > "$d/run.log" 2>&1
printf '%s\n' "$?" > "$d/rc"

The fixture has two complete project areas. proj_a is chip_a, whose arbiter grants the wrong agent first; proj_b is chip_b, whose arbiter is fixed. Each has its own filelist, its own built image, and a PROJECT file naming itself.

Running the same test, prio0, four ways:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  case      script rc  vvp rc  log
  missing   0          1       flow2.sh: line 6: PROJ_ROOT: unbound variable
  stale     0          255     <R>/proj_deleted/logs/sim.vvp: Unable to open input file.
  wrong_a   0          0       TB  test=prio0 seed=1 cycles=200 verbosity=LOW   (TEST FAILED)
  wrong_b   0          0       TB  test=prio0 seed=1 cycles=200 verbosity=LOW   (TEST PASSED)

Read the first column before anything else. The script exited 0 in all four cases, including the two where no simulation ran at all.

2. Severity Is Inverse to Loudness

The three kinds, ordered by how much damage they do:

Missing — the loudest, the least damaging. Something says unbound variable or a path appears as /logs/sim.vvp with an obviously absent prefix. Nothing ran, and it is clear that nothing ran. Minutes.

Stale — louder still, and still not damaging. The variable points at an area that has been archived, renamed or deleted:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
<R>/proj_deleted/logs/sim.vvp: Unable to open input file.

Exit 255, a full path in the message, and the path is visibly wrong once you read it. This is the best failure of the three, and it is worth noticing that it is better than the "handled" case in Section 1 — because the tool's own error message names the file it could not open, while set -u's message names only a variable.

Wrong — silent, and the only one that matters. The variable points at a real project area with a real filelist and a real built image. Everything works:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  -- intended: chip_a, whose arbiter is broken --
    PROJ_ROOT=<R>/proj_a
    ERROR t=35 gnt=10 expected 01   TEST FAILED
  -- actual:   chip_b, whose arbiter is fixed --
    PROJ_ROOT=<R>/proj_b
    TEST PASSED

The same command, two valid roots, opposite verdicts. The second run produced a PASS for a design that was never tested, there is no error anywhere, the exit status is 0, the log is well-formed, and the only evidence is a path in a line nobody compares against anything.

how it announces itselfwhat it costswho notices
missingunbound variable, or a path with a gap in itminutesthe first run
staleUnable to open input file, with the pathminutesthe first run
wrongnothing at alla signed-off blocknobody

This is the same inversion Chapter 33.1 found between a genuinely broken design and a stale image: the failure modes that produce a clean, confident, well-formed result are the expensive ones, and they are expensive precisely because every check downstream passes.

3. The Check

Three failures need three statuses, because they need three different people:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash
# checkenv EXPECTED_PROJECT -- assert the environment before any work starts.
# Three distinct failures, three distinct statuses, because they need three
# different people:
#   3  a required variable is not set          -> whoever launched this
#   4  it points at something that is not there -> whoever moved it
#   5  it points at the WRONG project           -> the dangerous one
set -u
want=${1:-}
[ -n "$want" ] || { echo "checkenv: name the project you expect" >&2; exit 2; }
fail() { printf 'checkenv: %s\n' "$1" >&2; exit "$2"; }

# 1. set, and not empty. ${X:?} would exit 1, which collides with a test failure.
[ -n "${PROJ_ROOT:-}" ] || fail "PROJ_ROOT is not set" 3

# 2. it resolves to a directory that exists
[ -d "$PROJ_ROOT" ] || fail "PROJ_ROOT points at '$PROJ_ROOT', which is not a directory" 4
for f in cfg/files.f logs/sim.vvp PROJECT; do
  [ -r "$PROJ_ROOT/$f" ] || fail "PROJ_ROOT has no $f -- this does not look like a project area" 4
done

# 3. it is the project you meant. A valid-but-wrong root is the silent failure:
#    it produces a real run, a real log and a real verdict about another design.
got=$(tr -d ' \n' < "$PROJ_ROOT/PROJECT")
[ "$got" = "$want" ] || fail "PROJ_ROOT is the '$got' project; this flow expects '$want'" 5

printf 'checkenv: %s at %s\n' "$got" "$PROJ_ROOT" >&2

Every branch, exercised:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  CASE                         rc     MESSAGE
  missing                      3      checkenv: PROJ_ROOT is not set
  empty string                 3      checkenv: PROJ_ROOT is not set
  stale (gone)                 4      checkenv: PROJ_ROOT points at '<R>/proj_deleted', which is…
  a dir, not a project         4      checkenv: PROJ_ROOT has no cfg/files.f -- this does not lo…
  WRONG project                5      checkenv: PROJ_ROOT is the 'chip_b' project; this flow exp…
  correct                      0      checkenv: chip_a at <R>/proj_a

Four decisions in that script are worth more than the rest.

It checks the identity, not just the existence

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
got=$(tr -d ' \n' < "$PROJ_ROOT/PROJECT")
[ "$got" = "$want" ] || fail "PROJ_ROOT is the '$got' project; this flow expects '$want'" 5

Existence checks catch missing and stale, which were already loud. Only an identity check catches wrong, and the identity has to come from inside the area — a PROJECT file, a manifest, a version — rather than from the path, because the path is the thing that is wrong.

A check written against the path instead, case $PROJ_ROOT in */chip_a) ok;;, fails in both directions: a legitimate area reached through a symlink or an automounter has a different path, and a copied area has the right path shape with the wrong contents inside.

Empty is treated as missing

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[ -n "${PROJ_ROOT:-}" ] || fail "PROJ_ROOT is not set" 3

PROJ_ROOT="" passes set -u — it is set. And then "$PROJ_ROOT/logs/sim.vvp" is /logs/sim.vvp, an absolute path into the root of the filesystem, which on an unlucky machine exists. Chapter 20.3 makes the general case; [ -n ] rather than a bare set check is what covers it, and the measured row empty string → 3 is the proof that the branch works.

The statuses avoid 1

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#   3  a required variable is not set          -> whoever launched this
#   4  it points at something that is not there -> whoever moved it
#   5  it points at the WRONG project           -> the dangerous one

Not ${PROJ_ROOT:?message}, which is the idiomatic one-liner and exits 1 — the value Chapter 32.3 reserved for a failing test. A setup problem reported as 1 is a setup problem that a regression driver counts as a design failure, which is Module 31's measured collision between ${VAR:?} and a FAIL status.

And all of them stay under 126, per Chapter 21.4.

It refuses when it is not told what to expect

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[ -n "$want" ] || { echo "checkenv: name the project you expect" >&2; exit 2; }

A check called with no expectation cannot do the thing it exists for. Defaulting to "whatever is there" would make the dangerous case pass silently — the check would validate the environment against itself.

4. Where the Check Goes

At the top of the flow, before anything expensive, and before the run directory is created:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash
set -u
root=$(cd "$(dirname "$0")/.." && pwd)
"$root/bin/checkenv" chip_a || exit $?        # pass the status through unchanged

Three properties of that line.

|| exit $? rather than || exit 1. The whole point of three statuses is that they reach the caller. Collapsing them at the first boundary is the mistake Chapter 32.7 spent a chapter on.

Before the run directory exists. A flow that creates runs/smoke_s1/ and then discovers the environment is wrong has left a directory that Chapter 32.4's collector will classify as NOSTATUS — a run that started and produced no verdict. The environment failure is now disguised as a different, more worrying problem.

And its message goes to standard error, un-redirected. Section 1's whole lesson: a diagnostic captured into a run's log is a diagnostic attributed to the tool.

5. Diagnosing One That Already Happened

A failure has arrived and you suspect the environment. In order:

Is the variable in the run's record at all? Chapter 32.2's env.txt and Chapter 33.1's cmd.args exist for this. If the flow recorded nothing about its environment, that is the first thing to fix, because every other step is now guesswork.

Does the recorded path still mean the same thing? This is the stale-and-wrong question, and it needs the identity rather than the path:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ grep '^PROJ_ROOT=' regr/…/env.txt
PROJ_ROOT=/proj/chip_a
$ cat /proj/chip_a/PROJECT
chip_b

Which happens more often than it sounds, because areas get re-pointed, symlinks get re-aimed, and a mount gets remapped — and none of those changes the path recorded in last night's env.txt.

Compare the whole environment, not the variable you suspect. Chapter 33.3's harness, applied to the record:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ diff <(sort regr/last-good/env.txt) <(sort regr/last-night/env.txt)

Which is the one step that finds the variable you were not thinking about — and it only works if both runs recorded it, which is Section 4's argument for recording more than you expect to read.

And when there is no record, reproduce the environment rather than reasoning about it. env -i for a scheduled job (Chapter 32.8 measured five PATH entries and no simulator), and /bin/bash -c for a shell difference.

Practice

  1. Write a flow wrapper that trusts a root variable, and run it with the variable unset.
  2. Check the script's exit status in that case, and then the tool's.
  3. Find where set -u's message went, and say who gets blamed for it.
  4. Explain why the subshell dying did not stop the script.
  5. Point the variable at a directory that does not exist and compare the message.
  6. Build two project areas whose designs differ, each with its own image.
  7. Run the same test against both and compare the verdicts.
  8. Say what evidence in the second run's output reveals the mistake.
  9. Rank the three kinds by damage, and then by loudness.
  10. Write the check with one status per kind.
  11. Give it an empty string and confirm it is treated as missing.
  12. Give it a directory that is not a project and confirm a different status.
  13. Point it at the wrong project and confirm the identity check fires.
  14. Check the identity from the path instead, and construct the two cases where that fails.
  15. Use ${VAR:?} instead and work out which status it returns and what that collides with.
  16. Call the check with no expectation and confirm it refuses.
  17. Put the check before the run directory is created, then move it after, and see what the collector reports.
  18. Pass the status through with || exit $?, then collapse it to exit 1 and say what is lost.
  19. List the variables in your flow and apply the "would anything else notice" test to each.
  20. Find a recorded PROJ_ROOT from an old run and check whether it still means the same thing.
  21. Diff two runs' whole environments and find a variable you were not suspecting.

Check Your Understanding

  1. What did the script's exit status say in all four cases?
  2. set -u fired on the missing variable. Name the three things that went wrong anyway.
  3. Why does the shell's diagnostic end up attributed to the tool?
  4. Which shell did set -u exit, and what did the parent do next?
  5. Why is rc=1 there indistinguishable from a failing test?
  6. Rank missing, stale and wrong by damage and by loudness.
  7. Why is the stale failure better than the "handled" missing one?
  8. What exactly did the two valid roots produce?
  9. What is the only evidence that the wrong project was used?
  10. What general inversion does this share with Chapter 33.1?
  11. Why must the identity come from inside the area rather than from the path?
  12. Give the two cases where a path-based identity check fails.
  13. Why is an empty string treated as missing, and what does "$PROJ_ROOT/logs/sim.vvp" become?
  14. Why avoid status 1, and what does ${VAR:?} return?
  15. Why does the check refuse when given no expectation?
  16. Why || exit $? rather than || exit 1?
  17. Why must the check run before the run directory is created?
  18. Why must its message not be redirected?
  19. Which four kinds of variable are worth asserting, and what do they have in common?
  20. What is the trap in checking too much?
  21. State the single test for whether a variable belongs in the check.
  22. What are the four diagnostic steps for a failure that already happened?
  23. Why can a recorded path stop meaning the same thing?
  24. Which diagnostic step finds the variable you were not thinking about, and what does it require?

6. Remember

  • Three kinds, not one: missing, stale, and wrong. Severity is inverse to loudness.
  • The script exited 0 in all four measured cases, including the two where nothing ran.
  • set -u fired and did not help: its message went into run.log, the subshell died while the parent continued, and the recorded 1 looks like a tool error.
  • A guard whose diagnostic is redirected into an artifact is a guard whose diagnostic is lost.
  • The stale failure is the best of the three, because the tool names the file it could not open.
  • A valid-but-wrong root produced TEST PASSED for a design that was never tested. Same command, two roots, opposite verdicts, no error anywhere.
  • Only an identity check catches it — read a marker from inside the area, never infer it from the path.
  • Treat an empty string as missing. PROJ_ROOT="" passes set -u and makes the image path /logs/sim.vvp.
  • One status per kind, none of them 1, and ${VAR:?} exits 1.
  • Refuse when not told what to expect, or the check validates the environment against itself.
  • Run it before the run directory is created, pass the status through with || exit $?, and keep its message on un-redirected stderr.
  • Assert only the variables whose wrongness would be silent — a check that is wrong gets commented out.
  • To diagnose one after the fact: diff the whole recorded environment, which only works if it was recorded.

7. What Is Next

The environment is now asserted rather than assumed. Chapter 33.5 takes on the defect class behind more flow bugs than any other — and the one Chapter 33.3 already met twice, where one line built two arguments in one shell and four in another.

Where this fits

Part of the Linux & Bash curriculum.