Skip to content
VLSI Mentor

Python Part II · Module 18 · VLSI Files, Paths and Project Structures

RTL, Verification and Run Directory Trees

A flow script spends most of its life answering questions about where things are: which files to compile, where last night's logs went, where this run's output should land. Hard-coding the answers works on exactly one project and is the single most common reason a script cannot be reused. The alternative is not cleverness — it is knowing the small number of shapes real projects actually use, understanding the one distinction that governs what a script is allowed to do to each directory, and anchoring everything to a single value so that moving to a new project changes one line rather than forty.

Intermediate14 min readPythonPart IIPathsProject StructureVerification Flow

Part II · Module 18 · Chapter 18.4 · RTL, Verification and Run Directory Trees

1. The Shape

Most RTL projects converge on something close to this, whatever the names:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
project/
├── rtl/              design sources            .sv .v .vhd
│   └── generated/    produced by a tool        DO NOT compile by hand
├── tb/               verification environment  .sv
│   └── tests/        the test library
├── bin/              flow scripts              .py
├── runs/             one directory per run     created, not committed
│   └── smoke_1234/
│       ├── logs/
│       └── waves/
└── reports/          summaries for humans      created, not committed

The names vary — dv/ for tb/, scratch/ or work/ for runs/, src/ for rtl/ — and the shape does not. A design area, a verification area, a place scripts live, a place runs land, and a place summaries land.

2. The Distinction That Matters

Five directories, and they divide into two kinds:

Five blocks arranged around a central anchor. On the left, two committed input directories: rtl, holding design sources, and tb with tests, holding the verification environment. In the centre, the project root, which a script anchors to. On the right, two generated output directories: runs, holding one directory per run, and reports, holding summaries. The script reads from the left and writes to the right.rtl/ — committeddesign sources, read onlytb/ — committedtestbench and tests, readonlyThe project rootthe one value a scriptanchors toruns/ — generatedone directory per run,writtenreports/ — generatedsummaries, writtenreadsreadswriteswrites12
Figure 1 — the split every rule in this module follows from. The left side is committed and read; the right side is generated and written. Which side a directory is on decides what a script may do to it, and Chapter 18.9's deletion rule is this diagram with one arrow removed.

Committed directories are inputs. rtl/, tb/ and bin/ are in version control. A script reads them, discovers files in them, and never writes to them — anything a script creates there will either be committed by accident or deleted by the next git clean.

Generated directories are outputs. runs/ and reports/ are produced by the flow and excluded from version control. A script creates them, writes into them, and — only here — may delete them.

That single distinction is the reason behind most of this module:

ChapterOperates onBecause
18.5 discoverythe committed sideyou find sources; you do not find outputs, you name them
18.7 filelistsreads committed, writes generatedthe filelist is an output describing inputs
18.8 creationthe generated sideyou never mkdir inside rtl/
18.9 cleanuponly the generated sidedeleting a committed file is a different kind of accident

Chapter 18.9's whole safety argument is this diagram with the deletion arrow allowed to point at one side only.

3. Why Hard-Coding Fails

The obvious approach, and what it costs:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
LOGDIR = "/proj/chipA/runs"
RTL = "/proj/chipA/rtl"

print("works on          : chipA, mounted at /proj")
print("fails on          : chipB, a colleague's clone, a CI checkout")
print("fails when        : the project moves to /proj2 or /nfs/proj")
print("number of lines to change:", 2, "per script, times every script")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
works on          : chipA, mounted at /proj
fails on          : chipB, a colleague's clone, a CI checkout
fails when        : the project moves to /proj2 or /nfs/proj
number of lines to change: 2 per script, times every script

The failure is not that absolute paths are wrong — Chapter 18.1 established they are the reliable kind. It is that the project-specific prefix is repeated, so a script encodes a fact about one checkout in forty places.

Relative paths are not the fix either, for the reason Chapter 18.1 gave: rtl/alu.sv works only when the process happens to be standing in the project root.

4. Anchor Once, Derive Everything

The canonical form, and the idea behind the whole chapter:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ROOT = Path(__file__).resolve().parents[1]
RTL, TB, RUNS = ROOT/"rtl", ROOT/"tb", ROOT/"runs"

__file__ is the path of the source file currently executing. resolve() makes it absolute (Chapter 18.1 Section 6), and parents[1] climbs two levels — past the filename, past bin/ — to the project root (Chapter 18.3 Section 4).

So a script in project/bin/check.py computes project/ without being told, and every other path derives from it.

Here it is working, with the script in bin/ exactly as it would be:

Azvya Education Pvt. Ltd.VLSI Mentor
rtl/alu.sv
module alu; endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
tb/tests/smoke.sv
class smoke; endclass
Azvya Education Pvt. Ltd.VLSI Mentor
bin/check.py
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
RTL, TB, RUNS = ROOT / "rtl", ROOT / "tb", ROOT / "runs"

print("the script lives in :", Path(__file__).parent.name)
print("ROOT is its parent  :", ROOT.name == Path(__file__).resolve().parents[1].name)
print()
print("rtl exists          :", RTL.is_dir())
print("a source under it   :", (RTL / "alu.sv").is_file())
print("tb tests exists     :", (TB / "tests").is_dir())
print("runs not yet created:", RUNS.exists())
print()
print("every path is absolute:", RTL.is_absolute(), TB.is_absolute(), RUNS.is_absolute())
print("RTL is inside ROOT    :", RTL.is_relative_to(ROOT))
print("RTL relative to ROOT  :", RTL.relative_to(ROOT))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
the script lives in : bin
ROOT is its parent  : True

rtl exists          : True
a source under it   : True
tb tests exists     : True
runs not yet created: False

every path is absolute: True True True
RTL is inside ROOT    : True
RTL relative to ROOT  : rtl

Note what is printed and what is not. The relationships are shown — that RTL is absolute, that it is inside ROOT, that it is rtl relative to it. The absolute strings are never printed, because they differ on every machine, which is the same property that makes the technique work.

relative_to() is the inverse of / and is genuinely useful for output: a report listing rtl/alu.sv is readable on any machine, where the absolute form is noise.

5. Deriving the Rest

With one anchor, the project's shape becomes a handful of declarations at the top of the file:

Azvya Education Pvt. Ltd.VLSI Mentor
bin/paths.py
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]

RTL = ROOT / "rtl"
TB = ROOT / "tb"
TESTS = TB / "tests"
RUNS = ROOT / "runs"
REPORTS = ROOT / "reports"


def run_dir(test, seed):
    """Where one run's output belongs."""
    return RUNS / f"{test}_{seed}"


def log_path(test, seed):
    return run_dir(test, seed) / "logs" / "sim.log"
Azvya Education Pvt. Ltd.VLSI Mentor
bin/show.py
from paths import ROOT, RTL, TESTS, RUNS, run_dir, log_path

for name, p in [("RTL", RTL), ("TESTS", TESTS), ("RUNS", RUNS)]:
    print(f"{name:<7} {p.relative_to(ROOT)}")

print()
print("run dir :", run_dir("smoke", 1234).relative_to(ROOT))
print("log     :", log_path("smoke", 1234).relative_to(ROOT))
print("waves   :", (run_dir("cdc", 7) / "waves").relative_to(ROOT))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
RTL     rtl
TESTS   tb/tests
RUNS    runs

run dir : runs/smoke_1234
log     : runs/smoke_1234/logs/sim.log
waves   : runs/cdc_7/waves

A paths module imported by every script in bin/ — Chapter 13.5's own-module import, doing the job it exists for. The project's shape is stated once, and a script that needs a log path asks for one rather than building it.

6. There Is No Standard Tree

The honest limit, and it matters because this chapter has just shown a specific shape.

Projects differ. Some put tests under tb/tests/, some under tests/, some under dv/uvm/tests/. Some have rtl/ and some have design/src/rtl/. A script that hard-codes this chapter's tree is no more portable than one that hard-codes /proj/chipA — it has just moved the assumption one level up.

The portable version takes the shape as configuration:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
from pathlib import Path

LAYOUTS = {
    "default": {"rtl": "rtl", "tb": "tb", "tests": "tb/tests", "runs": "runs"},
    "legacy":  {"rtl": "design/src", "tb": "dv", "tests": "dv/uvm/tests", "runs": "scratch"},
}


def project_paths(root, layout="default"):
    shape = LAYOUTS[layout]
    return {name: root / sub for name, sub in shape.items()}


root = Path("/proj/chipA")
for layout in ["default", "legacy"]:
    paths = project_paths(root, layout)
    rendered = {k: str(v.relative_to(root)) for k, v in paths.items()}
    print(f"{layout:<8} {rendered}")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
default  {'rtl': 'rtl', 'tb': 'tb', 'tests': 'tb/tests', 'runs': 'runs'}
legacy   {'rtl': 'design/src', 'tb': 'dv', 'tests': 'dv/uvm/tests', 'runs': 'scratch'}

One function, two projects, and the per-project knowledge is a dictionary rather than scattered through the code. Whether that indirection is worth it is the Chapter 1.4 question again: for a script serving one project, the five constants of Section 5 are right; for a tool a team uses across three, the layout table is.

7. Predict, Run, Observe

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
from pathlib import Path

ROOT = Path("/proj/chipA")
RTL = ROOT / "rtl"
RUNS = ROOT / "runs"
run = RUNS / "smoke_1234"

print("A:", run.relative_to(ROOT))
print("B:", run.relative_to(RUNS))
print("C:", run.is_relative_to(RTL))
print("D:", len(run.parts) - len(ROOT.parts))
print("E:", run.parents[1] == RUNS)
print("F:", (ROOT / "rtl" / ".." / "tb").parts[-3:])
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: runs/smoke_1234
B: smoke_1234
C: False
D: 2
E: False
F: ('rtl', '..', 'tb')

A and B — relative_to answers "relative to what you ask", so the same path has several correct short forms. Choosing the right one is what makes a report readable.

C — a run directory is not inside rtl/, which is the containment check Chapter 18.9 performs before deleting.

D — subtracting parts lengths gives the depth below the root, which is occasionally useful for limiting a traversal.

E is False, and it is the one to check carefully: run.parents[0] is runs, so parents[1] is chipA — one level further than expected. Indexing parents is off-by-one against intuition because it starts at the immediate parent, and that is exactly the kind of mistake that makes parents[1] in the anchoring idiom worth testing rather than assuming.

F keeps the .., confirming Chapter 18.1's point that a .. component survives in parts until something resolves it — so a containment check run against an unresolved path can be fooled by one.

8. Exercises

Exercise 1 — name your tree. Write down your project's five directories using Section 1's categories. Note which names differ.

Exercise 2 — classify them. For each, say committed or generated. Check one against .gitignore.

Exercise 3 — count the hard-codes. Grep a real script for the project prefix. Count the occurrences; that is how many lines change when the project moves.

Exercise 4 — anchor a script. Put a script in bin/ and compute the root with parents[1]. Verify by checking a known file exists relative to it.

Exercise 5 — off by one. For a path three levels deep, print parents[0], parents[1] and parents[2]. Say which one your anchoring idiom needs and why you should test it.

Exercise 6 — a paths module. Write paths.py exporting five constants and two functions. Import it from a second script.

Exercise 7 — relative_to for reports. Print ten discovered files as absolute paths and as paths relative to the root. Say which you would put in a report.

Exercise 8 — the layout table. Write the two-layout version from Section 6 for your project and one you have worked on before. Say whether the indirection is worth it for your case.

9. Summary

  • Projects converge on a shape: design sources, a verification area, a place for scripts, a place runs land, a place summaries land.
  • The names vary and the shape does not.
  • Committed directories are inputs — read, discovered, never written to.
  • Generated directories are outputs — created, written, and the only place deletion is allowed.
  • That split is the reason behind most of this module, including Chapter 18.9's safety rule.
  • Hard-coding a project prefix repeats one fact in forty places, which is the cost, not the absoluteness.
  • Path(__file__).resolve().parents[1] computes the root from the script's own location.
  • Derive everything else from the anchor, in a paths module imported by the rest.
  • relative_to() is the inverse of / and is what makes a report readable on any machine.
  • There is no standard tree, so a tool serving several projects should take the shape as configuration — the anchor is the invariant, not the directory names.

The next chapter finds the files inside those directories without naming them one by one.

Where this fits

Part of the Python curriculum.