Linux & Bash · Part II · Module 24 · Searching Design, Verification and Log Text
Searching Configuration, Filelists and Build Scripts
Half of what a design does is decided outside the design: by defines, include paths and tool options living in build scripts, filelists and headers. This chapter finds them — and measures a case where a command-line define is accepted, exits zero, prints nothing, and has no effect whatsoever.
Intermediate21 min readLinuxPart IIDefinesFilelistsBuild
Part II · Module 24 · Chapter 24.5 · Searching Configuration, Filelists and Build Scripts
"It compiles differently for you than for me" is answered in one of four places, and none of them is the RTL: a define, an include path, a tool option, or which filelist was used.
1. The Four Places a Setting Can Live
| Where | Looks like | Found with |
|---|---|---|
| A header | `define ADDR_W 32 | search *.svh, *.vh |
| A command line | -DADDR_W=32, +define+ADDR_W=32 | search *.sh, *.py, Makefile* |
| A filelist | +define+, +incdir+, -y | search *.f |
| The environment | $ADDR_W read by a script | search for ${ and getenv |
They are searched differently, and the important thing is that a value can be set in more than one of them, in which case something has to win — which §3 measures.
$ /usr/bin/grep -rn '`define' --include='*.svh' --include='*.sv' .
./rtl/include/chip_defs.svh:2:`define CHIP_DEFS_SVH
./rtl/include/chip_defs.svh:3:`define ADDR_W 32
./rtl/include/chip_defs.svh:4:`define DATA_W 64
./rtl/include/chip_defs.svh:5:`define RESET_ACTIVE_LOW 1
./rtl/include/chip_defs.svh:7: `define TIMEOUT_CYCLES 1000
./rtl/include/chip_defs.svh:9: `define TIMEOUT_CYCLES 100000TIMEOUT_CYCLES is defined twice, which is the first thing to notice. Both are real:
$ /usr/bin/grep -n -A4 'ifdef FAST_SIM' rtl/include/chip_defs.svh
6:`ifdef FAST_SIM
7- `define TIMEOUT_CYCLES 1000
8-`else
9- `define TIMEOUT_CYCLES 100000
10-`endifSo the value depends on whether FAST_SIM is set — and FAST_SIM is not set anywhere in the HDL:
$ /usr/bin/grep -rnE '\-D[A-Z_]+|\+define\+' --include='*.sh' --include='*.py' --include='*.f' .
./scripts/run_sim.sh:4:DEFINES="-DADDR_W=32 -DFAST_SIM"
./scripts/regress.py:5: return ["iverilog", "-g2012", "-DFAST_SIM", "-o", f"{test}.vvp"]Both build paths set it. So TIMEOUT_CYCLES is 1000 in every real run and 100000 if you compile by hand — a hundredfold difference in a timeout, decided by a flag that appears nowhere in the design and in a file most engineers never open.
That is the shape of nearly every unexplained compile difference, and the search that finds it is three lines.
2. Include Paths, and the Two Spellings
A `include resolves against a search path, and the path is set outside the file:
$ /usr/bin/grep -rnE '\+incdir\+|\-I[a-z]' --include='*.sh' --include='*.f' .
./filelists/top.f:2:+incdir+rtl/include
./filelists/chip.f:5:+incdir+rtl/include
./scripts/run_sim.sh:5:INCDIRS="-Irtl/include"Three places, two spellings — +incdir+ in the filelists and -I in the shell script. Both point at the same directory here, which is luck rather than design; when they diverge, the compile picks up a different copy of the header, and Chapter 23.3 is how you find out which.
The pattern for "every include directory this flow uses" has to cover both, plus the vendor variants:
/usr/bin/grep -rnE '(\+incdir\+|-I[^[:space:]]|--include-dir|-y[[:space:]])' \
--include='*.sh' --include='*.py' --include='*.f' --include='Makefile*' --include='*.mk' .-I[^[:space:]] rather than -I alone, because a bare -I matches -Idir and also -INFO in prose. And -y[[:space:]] with the space, because -y is a library search path and a bare -y matches far too much.
3. A Define That Is Accepted, Exits Zero, and Does Nothing
Here is the measurement this chapter exists for. A file that prints both values:
`include "chip_defs.svh"
module dc;
initial begin
$display("ADDR_W = %0d", `ADDR_W);
$display("TIMEOUT_CYCLES = %0d", `TIMEOUT_CYCLES);
end
endmoduleWith no -D at all:
$ iverilog -g2012 -Irtl/include -o /tmp/dc.vvp /tmp/dc.sv
$ vvp /tmp/dc.vvp < /dev/null
ADDR_W = 32
TIMEOUT_CYCLES = 100000With -DFAST_SIM, which is what the build scripts pass:
$ iverilog -g2012 -DFAST_SIM -Irtl/include -o /tmp/dc.vvp /tmp/dc.sv
$ vvp /tmp/dc.vvp < /dev/null
ADDR_W = 32
TIMEOUT_CYCLES = 1000The timeout changed by a factor of a hundred and nothing in the design says so. Now try to override the address width:
$ iverilog -g2012 -DADDR_W=8 -Irtl/include -o /tmp/dc.vvp /tmp/dc.sv
$ echo $?
0
$ vvp /tmp/dc.vvp < /dev/null
ADDR_W = 32
TIMEOUT_CYCLES = 100000ADDR_W is 32. The flag was accepted, the compile exited 0, stderr was empty — no redefinition warning of any kind — and the value you asked for was discarded.
4. Which Filelist Was Actually Used
Chapter 23.3 established the filelist as the authority on what is compiled. This chapter's question is which filelist is the authority, because a real project has several:
$ git ls-files | /usr/bin/grep -E '\.f$|filelist'
filelists/abs.f
filelists/bad.f
filelists/chip.f
filelists/crlf.f
filelists/sub.f
filelists/top.fSix, of which one is used by the flow. The answer is in the build script, not in the filelist directory:
$ /usr/bin/grep -rnE '\-[fF][[:space:]]+[^[:space:]]+\.f' --include='*.sh' --include='*.py' --include='*.mk' .
./scripts/run_sim.sh:6:iverilog -g2012 $DEFINES $INCDIRS -o work/sim.vvp -f filelists/chip.fOne line, and it names filelists/chip.f. The other five are drafts, experiments and the nested fragments it includes — which is the normal state of a filelists/ directory and the reason "read the filelist" is ambiguous advice until you have run this search.
Three questions worth asking of the one that is used:
# does it nest?
/usr/bin/grep -nE '^[[:space:]]*-[fF][[:space:]]' filelists/chip.f
# does it set defines or include paths of its own?
/usr/bin/grep -nE '\+define\+|\+incdir\+|^-y' filelists/chip.f
# does it contain variables that must be expanded?
/usr/bin/grep -n '\$' filelists/chip.fThat last one is Chapter 23.3 §2's warning: a $ in a filelist means any simple cross-reference against it may be wrong.
5. Tool Options Nobody Documented
The remaining category: a flag on a command line that changes behaviour and is in no specification.
$ /usr/bin/grep -rnE '^[[:space:]]*[A-Z_]+=' --include='*.sh' scripts
./scripts/run_sim.sh:4:DEFINES="-DADDR_W=32 -DFAST_SIM"
./scripts/run_sim.sh:5:INCDIRS="-Irtl/include"Two variables, both flag collections. The general search is for the tool invocation itself, and then reading every argument:
/usr/bin/grep -rnE '(iverilog|vvp|vcs|xrun|vsim|verilator|nvc)[[:space:]]' \
--include='*.sh' --include='*.py' --include='*.mk' --include='Makefile*' ../scripts/run_sim.sh:6:iverilog -g2012 $DEFINES $INCDIRS -o work/sim.vvp -f filelists/chip.f
./scripts/regress.py:5: return ["iverilog", "-g2012", "-DFAST_SIM", "-o", f"{test}.vvp"]Two invocations, and they do not agree. The shell script passes -DADDR_W=32, -Irtl/include and -f filelists/chip.f; the Python driver passes -DFAST_SIM and no include path or filelist. Whatever regress.py compiles, it is not what run_sim.sh compiles — and a test that passes under one and fails under the other is explained entirely by that line.
Finding a difference like this is the whole point of the chapter, and it needs no cleverness: list every invocation and read the arguments side by side.
6. The Script
#!/usr/bin/env bash
# whyconfig — where does a setting come from, and is it overridable?
#
# usage: whyconfig [<name>]
# with a name: every place it is set
# with none: a survey of the whole build description
# exit: 0 found something · 1 nothing found · 2 unusable
set -o nounset
GREP=${GREP:-/usr/bin/grep}
HDR=(--include='*.svh' --include='*.vh' --include='*.sv' --include='*.v')
BLD=(--include='*.sh' --include='*.bash' --include='*.py' --include='*.f'
--include='*.mk' --include='Makefile*' --include='*.tcl')
sec() { printf -- '\n-- %s\n' "$1"; }
any=0
show() { local o; o=$("$GREP" -rn "$@" . 2>/dev/null | sed 's|^\./||') || return 0
[ -n "$o" ] || return 0; printf '%s\n' "$o" | sed 's/^/ /'; any=1; }
if [ $# -ge 1 ]; then
name=$1
sec "\`define $name" ; show -E "${HDR[@]}" "\`define[[:space:]]+$name[^a-zA-Z0-9_]"
sec "guarded by \`ifndef?" ; show -E "${HDR[@]}" "\`ifndef[[:space:]]+$name[^a-zA-Z0-9_]"
sec "on a command line" ; show -E "${BLD[@]}" "(-D|\+define\+)$name([=[:space:]\"']|$)"
sec "read from the environment"; show -E "${BLD[@]}" "(\\\$\{?$name\}?|getenv\(.$name)"
sec "used in the HDL" ; show -E "${HDR[@]}" "\`$name[^a-zA-Z0-9_]"
else
sec "defines in headers" ; show -E "${HDR[@]}" '`define[[:space:]]'
sec "command-line defines" ; show -E "${BLD[@]}" '(^|[[:space:]"'"'"'])(-D[A-Za-z_]|\+define\+)'
sec "include paths" ; show -E "${BLD[@]}" '(\+incdir\+|-I[^[:space:]]|-y[[:space:]])'
sec "filelists referenced" ; show -E "${BLD[@]}" '\-[fF][[:space:]]+[^[:space:]]+'
sec "tool invocations" ; show -E "${BLD[@]}" '(iverilog|vvp|vcs|xrun|vsim|verilator|nvc)[[:space:]]'
sec "warnings suppressed" ; show -E "${BLD[@]}" '(\-Wno-|\+nowarn|\-suppress|\-nowarn)'
fi
[ "$any" -eq 1 ] || { printf 'whyconfig: nothing found\n' >&2; exit 1; }$ ./whyconfig ADDR_W
-- `define ADDR_W
rtl/include/chip_defs.svh:3:`define ADDR_W 32
-- guarded by `ifndef?
-- on a command line
scripts/run_sim.sh:4:DEFINES="-DADDR_W=32 -DFAST_SIM"
-- read from the environment
-- used in the HDL
rtl/axi/axi_slave.sv:2:module axi_slave #(parameter ADDR_W = `ADDR_W, parameter DATA_W = `DATA_W)Read that output as a diagnosis: defined in a header, set on a command line, and the "guarded by `ifndef" section is empty. Those three facts together say the command-line value is being discarded — which §3 measured and which no single search would have told you.
Six decisions:
An empty section is printed, not skipped. The absence of an `ifndef is the finding, and a script that printed only non-empty sections would hide it. Chapter 23.6 §7 made the same choice for the same reason.
Two extension groups, headers and build files, because the same name means different things in each and mixing them produces a list you cannot read.
show returns 0 on no match, so an empty section does not abort the script under a future set -e — and the any flag carries the overall result instead.
The no-argument form is a survey, so the script is useful before you know which setting to blame. That is the commoner situation.
The -D pattern requires a boundary before the dash, which the first draft did not — and the consequence was measured:
$ printf '%s\n' "/tmp/-Users-engineer-Documents-GitHub/x.sv" | /usr/bin/grep -E '(-D[A-Za-z_]|\+define\+)'
/tmp/-Users-engineer-Documents-GitHub/x.svA filelist of absolute paths matched the "command-line defines" section, because the path contained -Documents and -D was followed by a letter. Four spurious lines in a diagnostic report, which is exactly the kind of noise that makes someone stop trusting the tool. (^|[[:space:]"'])-D[A-Za-z_] rejects the path and still matches DEFINES="-DADDR_W=32".
And warnings suppressed is last, because it is the section you read when everything else looked fine.
Practice
- Find every
`definein a project you work on. Is any name defined twice? - For a define inside a
`ifdef, find out whether the condition is set — and where. - Search your build scripts for
-Dand+define+. Does any value contradict a header? - Reproduce §3: define a macro in a header, pass
-Dfor it, and print the value. Which wins? - Add an
`ifndefguard and repeat. Confirm the override now works and the default still does. - Check
$?and stderr on the unguarded run. What did the tool tell you? - Find every unguarded define in a project. How many could be overridden but cannot?
- Find every include path and both spellings. Do they all point at the same place?
- Count the filelists in your project, then find which one the flow actually uses.
- Check that filelist for nesting, for its own defines, and for a
$. - List every tool invocation in the build description and read the arguments side by side. Do two paths disagree?
- Search for warning-suppression flags. What has the project decided not to be told?
- Write
whyconfigand run it with no argument on an unfamiliar repository.
Check Your Understanding
- What are the four places a setting can live?
- In
chip_a, what decidesTIMEOUT_CYCLES, and where is that decided? - Why does
-DADDR_W=8have no effect, and what does the tool report? - What is the one-line fix, and why does it work?
- Why are both an empty
`ifndefsection and a non-empty-Dsection needed to make the diagnosis? - What are the two common spellings of an include path, and what happens when they diverge?
- How do you find which of six filelists the flow uses?
- Why does a
$in a filelist matter? - Name three flags that change results and are rarely documented.
- Why does
whyconfigprint empty sections?
7. Remember
- Four places a setting lives: a header, a command line, a filelist, the environment.
- A define inside
`ifdefhas two values, and the condition is usually set in a build script the HDL never mentions. - Measured:
-DFAST_SIMchanged a timeout from 100000 to 1000 — a hundredfold, invisible in the design. - An unconditional
`definein a header silently overrides the command line.-DADDR_W=8gave 32, exit 0, empty stderr. `ifndef NAMEaround the define is the fix. Then 8 when overridden, 32 by default.- The diagnosis needs the negative observation — no
`ifndef— as much as the positive ones. - Include paths come in two spellings,
+incdir+and-I; divergence means a different copy of the header. - When the build description and the result disagree, ask the tool — preprocessed output, a dependency file, a compile log.
- Find the filelist in the build script, not in the filelist directory. Six existed; one was used.
- A
$in a filelist invalidates textual comparison. - Search for
-Wno-,+nowarn,-suppressto learn what the project has stopped being told. - List every tool invocation and read the arguments side by side. Two build paths that disagree explain a great deal.
- Print empty sections and say what empty means.
8. What Is Next
Chapter 24.6 closes the module by turning all of this into the shape an actual question needs: not a list of matching lines but a count, a ranked list or a table someone can act on.
Where this fits
Part of the Linux & Bash curriculum.
