CAD & Methodology · lesson 3

Flow Scripting with Tcl and Python

Tcl as the tool control language, keeping configuration apart from scripts, and a Python wrapper that generates Tcl, runs a tool and checks logs and exit codes.

9 min read · Free, no login

Lesson 2 described the flow as a chain of stages. This lesson looks at what a single stage is made of: a tool, a script that tells the tool what to do, a configuration that says which design and which settings, and a small program around the tool that starts it, records what it did and decides whether it succeeded.

Almost every EDA tool is controlled through Tcl, so Tcl is where we start. We then separate configuration from scripts, and finally wrap the tool in a short Python program that generates a Tcl file, launches the tool in batch mode, and turns the result into a clear pass or fail.

Reading time: about 9 minutes.

3.1 Why Tcl is the control language

Most commercial synthesis, place-and-route and timing tools, and open-source tools such as OpenROAD and OpenSTA, embed a Tcl interpreter. Every action available in the GUI is also a Tcl command, and SDC itself is a set of Tcl commands. Thus, when the tool reads a script, it is simply running a Tcl program in which the tool's own commands (read_liberty, global_placement, report_checks, and so on) sit alongside ordinary Tcl.

This has a useful consequence. A flow engineer does not need a different language for each tool; the same few Tcl constructs โ€“ variables, conditionals, loops, procedures and error handling โ€“ work in all of them. Only the tool-specific commands change. Yosys is a partial exception: it has its own script format, but it can also run Tcl scripts with yosys -c.

3.2 The Tcl you need for flow work

Flow scripts use a small subset of Tcl. The constructs below cover most of what is needed.

set DESIGN  "aes_core"              ;# variable assignment
puts "Running $DESIGN"              ;# substitution with $
set corners {ss_0p72v_125c ff_0p88v_m40c}
foreach c $corners { puts "corner: $c" }

if {[file exists $env(RUN_DIR)/syn.v]} {
    puts "netlist found"
} else {
    puts "FLOW-ERROR: netlist missing"
    exit 1                          ;# non-zero exit = failure
}

proc report_stage {name} {          ;# a reusable procedure
    puts "=== stage $name done at [clock format [clock seconds]]"
}
source scripts/common.tcl           ;# run another Tcl file here

Two points deserve attention. First, square brackets run a command and substitute its result, so [file exists ...] returns 0 or 1. Second, exit with a non-zero value ends the tool with a failing exit status. Section 3.6 shows why that matters.

Error handling uses catch. When a tool command fails, it raises a Tcl error; catch traps it and returns 1, so the script can report a clear message instead of stopping in an unclear state.

catch script [result_variable]

if {[catch {global_placement -density $DENSITY} msg]} {
    puts "FLOW-ERROR: global placement failed: $msg"
    exit 1
}

3.3 Separating configuration from scripts

Let us consider a team that brings up a second block by copying the first block's placement script and editing the density and file names inside it. After a few months there are ten slightly different copies, and a fix made in one never reaches the others. This is the most common way a flow decays.

The remedy is to keep two kinds of file apart. Stage scripts describe how a stage works and are identical for every design. Configuration files describe one design: its name, its files, its clock period, its target utilisation. A new block then needs a new configuration file and nothing else. A configuration might look like this:

{
  "design":        "aes_core",
  "flow_dir":      "/proj/flow/v3.2",
  "run_dir":       "runs/2026-11-05_a",
  "verilog":       "results/aes_core.syn.v",
  "sdc":           "constraints/aes_core.sdc",
  "place_density": 0.65,
  "clock_period":  2.0
}

JSON, YAML or a plain Tcl file of set commands all work. JSON and YAML have the advantage that Python and CI systems can read them directly; a Tcl file has the advantage that the tool can source it without any translation. Many flows keep the source of truth in JSON or YAML and generate a Tcl file from it, which is what the wrapper in section 3.5 does.

3.4 A stage script that reads configuration

The stage script never names a design. It expects certain variables to exist and uses them. The example below is a placement stage for OpenROAD.

# scripts/place.tcl -- expects DESIGN, DENSITY, PREV_DB, RUN_DIR
read_db $PREV_DB

if {[catch {global_placement -density $DENSITY} msg]} {
    puts "FLOW-ERROR: global placement failed: $msg"
    exit 1
}
detailed_placement
check_placement

write_db $RUN_DIR/$DESIGN.place.odb
puts "FLOW-INFO: placement complete"

Notice the convention of printing FLOW-ERROR and FLOW-INFO prefixes. The tool's own messages have their own format (OpenROAD, for example, prints messages of the form [ERROR GPL-nnnn], with a tool prefix and a message number), and a fixed prefix for the flow's messages makes them easy to find in the log later.

3.5 A Python wrapper around the tool

Python is the usual choice for the layer around the tool, because it handles files, configuration formats, processes and text easily. The wrapper has three jobs: generate the per-run Tcl file, launch the tool and capture its output, and decide the result.

stage script scripts/place.tcl config.json design settings wrapper.py writes Tcl, runs run Tcl file set vars, source EDA tool batch mode log + exit code stdout, rc wrapper checks rc, ERROR lines PASS / FAIL status file source read back Configuration and scripts stay separate; only the run Tcl file is per run
One stage, end to end. The configuration and the stage script never change between runs of the same block; the wrapper writes a small run file that sets variables and sources the stage script. Notice that the decision on pass or fail is made by the wrapper, from both the exit code and the log, not by the tool alone.
#!/usr/bin/env python3
"""run_stage.py STAGE CONFIG -- run one flow stage and report pass/fail."""
import json, subprocess, sys, time
from pathlib import Path

def run_stage(stage, prev_db, cfg):
    run_dir = Path(cfg["run_dir"]) / stage
    run_dir.mkdir(parents=True, exist_ok=True)

    # 1. generate the per-run Tcl file
    tcl = run_dir / f"{stage}.tcl"
    tcl.write_text(
        f'set DESIGN  "{cfg["design"]}"\n'
        f'set DENSITY {cfg["place_density"]}\n'
        f'set PREV_DB "{prev_db}"\n'
        f'set RUN_DIR "{run_dir}"\n'
        f'source {cfg["flow_dir"]}/scripts/{stage}.tcl\n')

    # 2. launch the tool in batch mode, capture everything in a log
    log = run_dir / f"{stage}.log"
    t0 = time.time()
    with log.open("w") as fh:
        rc = subprocess.run(["openroad", "-no_init", "-exit", str(tcl)],
                            stdout=fh, stderr=subprocess.STDOUT).returncode

    # 3. decide: exit code AND error lines in the log
    lines  = log.read_text().splitlines()
    errors = [l for l in lines if l.startswith(("[ERROR", "FLOW-ERROR"))]
    ok = (rc == 0) and not errors
    status = "PASS" if ok else "FAIL"
    (run_dir / f"{stage}.status").write_text(
        f"{status} rc={rc} errors={len(errors)} "
        f"runtime={time.time() - t0:.0f}s\n")
    for e in errors[:5]:
        print(e)
    return 0 if ok else 1

if __name__ == "__main__":
    stage, cfg_file = sys.argv[1], sys.argv[2]
    cfg = json.loads(Path(cfg_file).read_text())
    prev = Path(cfg["run_dir"]) / "floorplan" / f'{cfg["design"]}.floorplan.odb'
    sys.exit(run_stage(stage, prev, cfg))

The options on the OpenROAD command line are standard: -no_init skips the user's startup file, so a personal setting cannot change a flow run, and -exit makes the tool quit after the script instead of waiting at its prompt. Most commercial tools have equivalent options for batch execution and for naming the log file; the exact names differ, so check the tool's help.

3.6 Logging and exit codes

Every program on Linux returns an exit status when it ends: zero for success, non-zero for failure. Shells, make (lesson 4), batch schedulers (lesson 6) and CI systems (lesson 8) all use this number to decide whether to continue. If a stage returns zero after failing, the whole chain above it continues on bad data.

Unfortunately, EDA tools do not always return a non-zero status when something goes wrong. A tool might print an error, skip the command, carry on and exit with zero. That is why the wrapper checks both the exit code and the log. It is recommended to be explicit in three places:

  • In the stage script, call exit 1 on any condition the flow treats as fatal.
  • In the wrapper, scan the log for error patterns and fail if any are found, even when the exit code is zero.
  • In the wrapper, return a non-zero status itself, so that whatever called it sees the failure.

The status file written by the wrapper is small but valuable. It records the result, the error count and the run time in one line that a later script can read without parsing the full log. Lesson 7 builds QoR tables from such files and from the tool reports.

Worked example: a failure that the exit code alone would miss

Assumptions: the configuration names a netlist file that was moved. The tool reports the missing file as an error, but โ€“ as some tools do โ€“ continues and finally exits with status 0.

With a shell script that only checks the exit code, placement "passes", writes an empty database, and routing fails an hour later with an unrelated-looking message. With the wrapper above, the log contains a line starting with [ERROR, so errors is non-empty, the status becomes FAIL, the first error line is printed, and the wrapper exits with 1. The problem is reported at the stage where it happened, within seconds.

3.7 Interview questions

Why is Tcl used to control EDA tools? Most tools embed a Tcl interpreter and expose every action as a Tcl command, and SDC is itself Tcl. The same variables, loops, procedures and catch work in every tool, so only the tool-specific commands differ.

Why separate configuration from stage scripts? So that every design runs the same scripts. A new block needs only a configuration file, and a fix to a stage script reaches every block at once instead of one copy.

Why check the log as well as the exit code? Some tools print errors and still exit with zero. Checking only the exit code lets a broken stage pass and moves the failure downstream, where it is harder to diagnose.

What does catch return? 0 if the script ran without error and 1 (or another non-zero code) if it raised an error; the optional variable receives the result or the error message.

Key takeaways

  • Tcl is the common control language of EDA tools; a small subset (variables, if, foreach, proc, source, catch, exit) covers most flow work.
  • Stage scripts describe how a stage works and never name a design; configuration files describe one design.
  • A Python wrapper generates the per-run Tcl, launches the tool in batch mode, and captures a complete log.
  • Pass or fail is decided from both the exit code and error lines in the log, and the wrapper returns a non-zero status on failure.
  • A one-line status file per stage makes later reporting (lesson 7) and regressions (lesson 8) simple.