Skip to content
VLSI Mentor

Tcl · Module 27

Batch Entry Points

One script decides where the flow tree is, where output goes, where the log goes and what the exit status means. Getting that right takes three fixes, and the second one looks like the whole job.

Intermediate21 min readtcledapart-iiinfrastructurebatchautomation

Part II · Module 27 · Chapter 27.1 · Batch Entry Points

Module 27 is about the shared Tcl a project lives on. It starts with the one script everything else hangs off: the entry point that a human, a Makefile or a job scheduler invokes.

Its job is to leave nothing to the caller.

1. What "decided by the script" means

A driver that works when you run it from the right directory with the right environment is not a driver; it is a note to yourself. The entry point should decide:

  • Where the flow tree is — so it can find its own libraries.
  • Where output goes — one directory per design and stage, created by the script.
  • Where the log goes — inside that directory, not wherever stdout happened to point.
  • What the exit status means — distinct codes for distinct outcomes.

Everything in this chapter is a way of getting one of those wrong.

2. Depending on the launch directory

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# flow/bin/run_naive.tcl
source flow/lib/vars.tcl
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== launched from the flow root ===
--- a driver that assumes the launch directory ---
    cwd        : /private/tmp/m27
    lib dir    : flow/lib   (relative)
    source     : ok
--- a driver that locates itself ---
    cwd        : /private/tmp/m27
    info script: flow/bin/run_safe.tcl
    normalized : /private/tmp/m27/flow/bin/run_safe.tcl
    FLOW_ROOT  : /private/tmp/m27/flow
    source     : ok

=== launched from somewhere else ===
--- a driver that assumes the launch directory ---
    cwd        : /private/tmp
    lib dir    : flow/lib   (relative)
    source     : FAILED — couldn't read file "flow/lib/vars.tcl": no such file or directory
--- a driver that locates itself ---
    cwd        : /private/tmp
    info script: /private/tmp/m27/flow/bin/run_safe.tcl
    normalized : /private/tmp/m27/flow/bin/run_safe.tcl
    FLOW_ROOT  : /private/tmp/m27/flow
    source     : ok

The fix is to ask Tcl where the running script is:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set SCRIPT    [file normalize [info script]]
set BIN_DIR   [file dirname $SCRIPT]
set FLOW_ROOT [file dirname $BIN_DIR]

info script returns the path of the file currently being evaluated (Chapter 14.10), so a script can locate itself without being told. Note the second output block: info script returned a relative path when invoked relatively and an absolute one when invoked absolutely — it reports what was typed. That is why §3 exists.

3. Why file normalize is load-bearing

Drop the normalize and the driver still appears to work:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== without file normalize, launched by a relative path ===
    info script : m27/flow/bin/run_nonorm.tcl
    FLOW_ROOT   : m27/flow
    absolute?   : NO — relative
    source now  : ok
    after cd to : /private/tmp/m27/work/sub
    source again: FAILED — couldn't read file "m27/flow/lib/log.tcl": no such file or directory
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== with file normalize, the same invocation ===
    info script : m27/flow/bin/run_norm.tcl
    FLOW_ROOT   : /private/tmp/m27/flow
    absolute?   : yes
    source now  : ok
    after cd to : /private/tmp/m27/work/sub
    source again: ok

4. Locating yourself is necessary and not sufficient

With FLOW_ROOT correct, the driver's own source calls work from anywhere. So does it work from anywhere?

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== the driver locates ITSELF correctly, so FLOW_ROOT is right ===
    12:set ::FLOW_ROOT [file dirname [file dirname [file normalize [info script]]]]

=== but the paths it HANDS TO THE TOOL are still relative ===
    62:        ::tool::read_design [list rtl/$::DESIGN.sv] $::DESIGN
    65:        ::tool::apply_constraints constraints/$::DESIGN.sdc

=== so: run from the flow root, then from elsewhere ===
    from the flow root      : exit 0
    from an unrelated dir   : exit 1

The fix derives a project root as well, and every handed-on path from it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set ::FLOW_ROOT [file dirname [file dirname [file normalize [info script]]]]
set ::PROJ_ROOT [file dirname $::FLOW_ROOT]
...
::tool::read_design [list [file join $::PROJ_ROOT rtl $::DESIGN.sv]] $::DESIGN
::tool::apply_constraints [file join $::PROJ_ROOT constraints $::DESIGN.sdc]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== with every path derived from the root ===
    13:set ::PROJ_ROOT [file dirname $::FLOW_ROOT]
    66:        ::tool::read_design [list [file join $::PROJ_ROOT rtl $::DESIGN.sv]] $::DESIGN
    69:        ::tool::apply_constraints [file join $::PROJ_ROOT constraints $::DESIGN.sdc]

    from the flow root      : exit 0
    from an unrelated dir   : exit 0
    from the root directory : exit 0

Three launch directories, including / itself, all exit 0. And that is the test: a driver is directory-independent when you have run it from somewhere absurd and it behaved identically, not when you have read it and concluded it should.

5. The output directory

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set ::OUT_DIR [file normalize [file join $::FLOW_ROOT .. $::OUT_DIR $::DESIGN $::STAGE]]
ensure_dir $::OUT_DIR
set LOG [file join $::OUT_DIR run.log]
::flow::log_open $LOG
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== and the outputs, all three ===
    work/core_top/place/run.log
    work/core_top/route/run.log
    work/core_top/synth/run.log

Three decisions worth naming.

The path includes the design and the stage. That is Chapter 26.6 §1's identity argument and Chapter 25.5 §3's collision: a directory named after fewer fields than the run collides, and the collision overwrites evidence silently.

The script creates it. ensure_dir rather than assuming — Chapter 27.7's subject, and the reason a re-run does not fail on a directory that already exists.

The log goes inside it. Not to stdout, not to /tmp, not to wherever the caller redirected. The run's output and the run's log are one artefact, and separating them is how you end up with a report directory nobody can account for.

6. The log a run leaves behind

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== the log a run leaves behind ===
      +0.000s INFO  design core_top  stage synth
      +0.006s INFO  started 2026-10-08 23:07:11
      +0.018s INFO  tcl 8.5.9  host macbook-air.local
      +0.018s INFO  flow root /private/tmp/m27/flow
      +0.018s INFO  out dir   /private/tmp/m27/work/core_top/synth
      +0.018s INFO  step setup: start
      +0.018s INFO  step setup: ok in 0 ms
      +0.018s INFO  step read_design: start
      +0.018s INFO  step read_design: ok in 0 ms
      +0.018s INFO  step constrain: start
      +0.018s INFO  step constrain: ok in 0 ms
      +0.018s INFO  step optimise: start
      +0.018s INFO  step optimise: ok in 0 ms
      +0.018s INFO  step report: start
      +0.018s INFO  step report: ok in 0 ms
      +0.018s INFO  flow completed

The header carries the provenance — design, stage, wall-clock start, interpreter version, host, flow root, output directory — which is Chapter 24.2 §6's argument for the third time in Part II: a result whose origin is unrecorded cannot be compared with anything.

Every step is 0 ms because the tool here is a mock that does nothing. That is honest rather than unfortunate: a runtime report of a mock flow is uninformative, which is why Chapter 27.6 demonstrates timing on a flow that does real work.

The stamps are elapsed time, not wall clock — +0.011s rather than 23:08:50. Chapter 27.6 §3 argues that choice.

7. One exit status per outcome

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== one exit status per outcome ===
    a complete run                                 exit 0
    a required variable missing                    exit 2
        run_flow.tcl: 1 required variable(s) not set:
            STAGE        synth | place | route
    an unknown option                              exit 2
        unknown option '-desgin'
        usage: run_flow.tcl -design NAME -stage STAGE [-out DIR] [-verbose 0|1]
    an unknown tool backend                        exit 2
        run_flow.tcl: unknown backend 'gamma'
    a step that fails: no SDC                      exit 1

2 for a setup problem, 1 for a flow failure, 0 for success. That split is Chapter 25.4 §7's argument: a failing flow belongs to the design team, and a typo'd option, a missing variable or an unknown tool belongs to whoever launched it. One status for both routes every failure to the wrong queue.

8. The shape of the entry point

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# 1. locate ourselves, before anything else
set ::FLOW_ROOT [file dirname [file dirname [file normalize [info script]]]]
set ::PROJ_ROOT [file dirname $::FLOW_ROOT]

# 2. read the command line into the variable contract
foreach {opt val} $::argv { switch -- $opt { ... } }

# 3. the shared setup, then validate the contract
source [file join $::FLOW_ROOT lib setup.tcl]
if {[catch {::flow::validate_vars} e]} { puts stderr "run_flow.tcl: $e" ; exit 2 }

# 4. decide the output and log locations, absolutely
set ::OUT_DIR [file normalize [file join $::FLOW_ROOT .. $::OUT_DIR $::DESIGN $::STAGE]]
ensure_dir $::OUT_DIR
::flow::log_open [file join $::OUT_DIR run.log]

# 5. the flow itself, inside one catch
set code [catch { ::flow::step ... } err]

# 6. one exit status per outcome
if {$code != 0} { ::flow::log ERROR "flow failed: $err" ; ::flow::log_close ; exit 1 }
::flow::log_close
exit 0

The ordering is the content. Locate, parse, validate, decide paths, run, report. Each stage depends only on the ones above it, and nothing before stage 5 touches the design — so every failure up to that point is a flow problem reported as one.

Note ::flow::log_close on both exit paths. A channel left open at exit is usually flushed by the runtime, but relying on that is Chapter 12.6's mistake, and Chapter 27.8 makes it a review item.

9. Exercises

Exercise 1. What four things should a batch entry point decide rather than inherit?

Exercise 2. Why does info script sometimes return a relative path?

Exercise 3. Without file normalize, the first source succeeded and a later one failed. Explain the sequence.

Exercise 4. Why is that worse than failing immediately?

Exercise 5. The driver computed FLOW_ROOT correctly and still failed from another directory. Why?

Exercise 6. Why did that defect only appear once the constraint step checked its file?

Exercise 7. What is the test for directory independence?

Exercise 8. Why does the output directory name include the design and the stage?

Exercise 9. Why is a missing option exit 2 and a failed step exit 1?

Exercise 10. Why was ::tool::use outside the catch a defect, and what was the fix?

Where this fits

Part of the Tcl curriculum.