Skip to content
VLSI Mentor

Perl Part II · Module 17 · Engineering Files and Directory Automation

RTL, Verification and Run-Directory Trees

Every project names its directories slightly differently, which makes a script written against one project's names useless on the next. The way out is to stop thinking about names and think about roles, because there are only four and every project has all of them: things you read and must never write, things you create fresh and delete afterwards, things you write and keep, and things you must walk past without looking inside. Once a script classifies a directory by role, the names become a short configuration rather than assumptions scattered through the code — and a script that knows a directory is disposable can safely delete it, which Chapter 17.6 shows is the difference between cleanup and disaster.

Intermediate10 min readPerlPart IIProject Structure

Part II · Module 17 · Chapter 17.2 · RTL, Verification and Run-Directory Trees

1. Four Roles, Not Seven Names

Figure 17.2.1 — What a script may do with each kind of directory

Four roles for directories in a project tree: read-only sources and tools, disposable per-run directories, kept outputs, and trees to skip entirely when walking.a project treeclassify before touchingread-onlyrtl/ tb/ scripts/ lists/disposableruns/ — safe to deletekept, or skippedreports/ vs third_party/12
The names change between projects; the four roles do not. Sources and tools are read and never written, so a script that writes into them has a bug. A run directory is created fresh for one run and deleted afterwards, which is the only kind of directory a script may safely remove. Outputs are written once and kept, because somebody will compare them against last week's. Everything else — vendor code, generated trees, version-control metadata — must be walked past rather than into.

The four roles, stated as rules a script can follow:

Read-only. rtl/, tb/, scripts/, lists/. A script reads these and never writes into them. If a tool wants to put a generated file somewhere, it does not go here.

Disposable. runs/<something>. Created for one run, deleted when nobody needs the evidence. This is the only role a script may delete, and Chapter 17.6 is about proving a path really has this role before removing it.

Kept. reports/. Written by the flow and compared against previous runs. Never deleted automatically, because the comparison is the point.

Skipped. third_party/, rtl/generated/, .git/, .svn/. Present in the tree and not part of the design you are collecting. Chapter 17.4 is entirely about walking past these.

2. Measure a Tree's Shape

Azvya Education Pvt. Ltd.VLSI Mentor
tree-shape.pl
use strict;
use warnings;
use File::Find;
use File::Temp qw(tempdir);
use File::Spec;
use File::Path qw(make_path);

# ---- a SAMPLE project tree (no claim that any project uses these exact names)
my $root = File::Spec->catdir(tempdir(CLEANUP => 1), "proj");
my @file = qw(
  rtl/alu.v rtl/core/cpu.sv rtl/sub/fifo.v rtl/generated/regs.sv
  tb/env/env.sv tb/tests/t1.sv
  runs/alu_1/sim.log runs/alu_2/logs/sim.log
  reports/setup.rpt scripts/run.pl lists/nightly.list
  third_party/uvm/uvm.sv
);
for my $rel (@file) {
    my @part = split m{/}, $rel;
    my $name = pop @part;
    make_path(File::Spec->catdir($root, @part));
    open my $w, ">", File::Spec->catfile($root, @part, $name) or die "cannot write: $!\n";
    print $w "// $rel\n";
    close $w;
}

# ---- the roles, as a table a script can consult instead of guessing
my %role = (
  rtl         => [ "sources",  "read, never write"         ],
  tb          => [ "sources",  "read, never write"         ],
  scripts     => [ "tools",    "read, never write"         ],
  lists       => [ "inputs",   "read, sometimes generate"  ],
  runs        => [ "per-run",  "create and delete"         ],
  reports     => [ "outputs",  "write, keep"               ],
  third_party => [ "external", "SKIP when walking sources" ],
);

opendir my $dh, $root or die "cannot read $root: $!\n";
my @top = sort grep { !/^\.\.?$/ && -d File::Spec->catdir($root, $_) } readdir $dh;
closedir $dh;

printf "%-14s %-10s %-27s %s\n", "DIRECTORY", "ROLE", "WHAT A SCRIPT MAY DO", "FILES";
for my $d (@top) {
    my ($r, $what) = @{ $role{$d} // [ "unknown", "ask before touching" ] };
    my $n = 0;
    find(sub { $n++ if -f $_ }, File::Spec->catdir($root, $d));
    printf "%-14s %-10s %-27s %d\n", "$d/", $r, $what, $n;
}

print "\nthe sub-shapes that matter:\n";
for my $p (["rtl/generated",     "generated sources — skip, they are rebuilt"],
           ["runs/alu_1",        "one directory per test and seed"],
           ["runs/alu_2/logs",   "logs live under the run directory"]) {
    printf "  %-18s %-7s %s\n", $p->[0],
        (-d File::Spec->catdir($root, split m{/}, $p->[0]) ? "exists" : "absent"), $p->[1];
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
DIRECTORY      ROLE       WHAT A SCRIPT MAY DO        FILES
lists/         inputs     read, sometimes generate    1
reports/       outputs    write, keep                 1
rtl/           sources    read, never write           4
runs/          per-run    create and delete           2
scripts/       tools      read, never write           1
tb/            sources    read, never write           2
third_party/   external   SKIP when walking sources   1

the sub-shapes that matter:
  rtl/generated      exists  generated sources — skip, they are rebuilt
  runs/alu_1         exists  one directory per test and seed
  runs/alu_2/logs    exists  logs live under the run directory

Notice that the table is sorted and the readdir result is passed through sort explicitly. That is not tidiness — readdir returns entries in whatever order the filesystem gives them, which is not alphabetical and not stable between machines. Chapter 17.8 measures what that does to a report.

Notice too that an unrecognised directory gets unknown — ask before touching, not a guess. A script that assumes an unfamiliar directory is disposable is one bad assumption away from deleting somebody's work.

3. The Sub-Shapes That Matter

Three patterns inside those directories come up on every project.

runs/<test>_<seed>/ — one directory per test and seed, because the same test at a different seed is a different run with its own log. A flow that reuses one directory per test cannot keep two seeds' evidence, and the second run silently overwrites the first. Chapter 17.5 builds these.

runs/<run>/logs/ and runs/<run>/waves/ — the run directory has internal structure, created before the tool starts. A simulator told to write logs/sim.log into a directory that does not exist fails in a way that looks like a tool problem.

rtl/generated/ — sources produced by another step: register maps from a spec, configuration packages, wrappers. They are real inputs to compilation and must not be collected by a source walk, because the walk runs before they are regenerated. Chapter 17.4 handles the skipping; Chapter 17.9 handles noticing they are stale.

4. Surviving a Project That Names Things Differently

The point of roles is that the names become data. Put them in one place:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
my %TREE = (
    sources => [ "rtl", "tb" ],          # read-only, walked for files
    tools   => [ "scripts", "bin" ],     # read-only
    inputs  => [ "lists" ],
    runs    => [ "runs" ],               # the ONLY deletable role
    outputs => [ "reports" ],
    skip    => [ "third_party", "generated", ".git", ".svn", "backup" ],
);

Three properties that gives you:

One place to change for a new project. The code asks "which directories are sources?" and gets an answer; it never contains the string rtl.

Several directories per role. sources is a list, which handles the tb/ versus tests/ problem directly.

A skip list the walker consults, which is exactly what Chapter 17.4 needs and means the skip rules are visible rather than buried in a return if inside a callback.

And one check worth running at startup, per Chapter 15.3's argument for validating before working:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
for my $role (qw(sources tools inputs)) {
    for my $d (@{ $TREE{$role} }) {
        my $p = File::Spec->catdir($ROOT, $d);
        warn "warning: $role directory not found: $d\n" unless -d $p;
    }
}

A warn rather than a die, because a project legitimately may not have every directory — but a warn that names the missing one turns "no files found" into "tb does not exist here", which is a much shorter conversation.

5. What To Remember

  • Names change between projects; the four roles do not.
  • Read-only: rtl/, tb/, scripts/, lists/. A script that writes here has a bug.
  • Disposable: runs/<...>. The only role a script may delete — and only after proving it (17.6).
  • Kept: reports/. Never deleted automatically, because comparison against last week is the point.
  • Skipped: third_party/, rtl/generated/, .git/, .svn/. In the tree, not part of the design (17.4).
  • Measured: a classifier reports the role and file count per directory, and an unrecognised one gets unknown — ask before touching rather than a guess.
  • readdir order is not alphabetical and not stable — always sort it (17.8).
  • runs/<test>_<seed>/ — per test and seed, or two seeds overwrite each other.
  • Create logs/ and waves/ before the tool runs, or the failure looks like a tool problem.
  • rtl/generated/ is a real input that a source walk must skip, because the walk runs before it is rebuilt.
  • tb/ versus tests/ is not reliable — take a list of source roots.
  • The run area is often a separate filesystem, so give it its own override: $ENV{RUN_AREA} //.
  • Put the names in one %TREE table so the code never contains the string rtl.
  • Warn, do not die, for a missing directory — but name it, so "no files found" becomes "tb does not exist here".
  • Never infer disposability from a name. work/ and scratch/ are sometimes neither.

Chapter 17.3 starts using the tree. Finding every source file sounds like a solved problem until you measure what glob actually does — and one widely-used pattern turns out not to recurse at all.

Where this fits

Part of the Perl curriculum.