Files
tt-support-tools/precheck/precheck.py
T
Uri Shaked d2d27ab36c fix(precheck): fails on projects in shuttle repository
It can't find info.yaml, since it's looking for it in the parent directory of the gds while info.yaml is in the same directory as the gds.
2024-05-07 09:01:09 +03:00

229 lines
7.6 KiB
Python

#!/usr/bin/env python3
import argparse
import logging
import os
import subprocess
import time
import traceback
import xml.etree.ElementTree as ET
import gdstk
import klayout.db as pya
import klayout.rdb as rdb
import yaml
from klayout_tools import parse_lyp_layers
from pin_check import pin_check
from precheck_failure import PrecheckFailure
PDK_ROOT = os.getenv("PDK_ROOT")
PDK_NAME = os.getenv("PDK_NAME") or "sky130A"
LYP_FILE = f"{PDK_ROOT}/{PDK_NAME}/libs.tech/klayout/tech/{PDK_NAME}.lyp"
REPORTS_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "reports")
if not PDK_ROOT:
logging.error("PDK_ROOT environment variable not set")
exit(1)
def has_sky130_devices(gds: str):
for cell_name in gdstk.read_rawcells(gds):
if cell_name.startswith("sky130_fd_"):
return True
return False
def magic_drc(gds: str, toplevel: str):
logging.info(f"Running magic DRC on {gds} (module={toplevel})")
magic = subprocess.run(
[
"magic",
"-noconsole",
"-dnull",
"-rcfile",
f"{PDK_ROOT}/{PDK_NAME}/libs.tech/magic/{PDK_NAME}.magicrc",
"magic_drc.tcl",
gds,
toplevel,
PDK_ROOT,
f"{REPORTS_PATH}/magic_drc.txt",
f"{REPORTS_PATH}/magic_drc.mag",
],
)
if magic.returncode != 0:
if not has_sky130_devices(gds):
logging.warning("No sky130 devices present - was the design flattened?")
raise PrecheckFailure("Magic DRC failed")
def klayout_drc(gds: str, check: str, script=f"{PDK_NAME}_mr.drc"):
logging.info(f"Running klayout {check} on {gds}")
report_file = f"{REPORTS_PATH}/drc_{check}.xml"
klayout = subprocess.run(
[
"klayout",
"-b",
"-r",
f"tech-files/{script}",
"-rd",
f"{check}=true",
"-rd",
f"input={gds}",
"-rd",
f"report={report_file}",
],
)
if klayout.returncode != 0:
raise PrecheckFailure(f"Klayout {check} failed")
report = rdb.ReportDatabase("DRC")
report.load(report_file)
if report.num_items() > 0:
raise PrecheckFailure(
f"Klayout {check} failed with {report.num_items()} DRC violations"
)
def klayout_zero_area(gds: str):
return klayout_drc(gds, "zero_area", "zeroarea.rb.drc")
def klayout_checks(gds: str):
layout = pya.Layout()
layout.read(gds)
layers = parse_lyp_layers(LYP_FILE)
logging.info("Running top macro name check...")
top_cell = layout.top_cell()
expected_name = os.path.splitext(os.path.basename(gds))[0]
if top_cell.name != expected_name:
raise PrecheckFailure(
f"Top macro name mismatch: expected {expected_name}, got {top_cell.name}"
)
logging.info("Running forbidden layer check...")
forbidden_layers = [
"met5.drawing",
"met5.pin",
"met5.label",
]
for layer in forbidden_layers:
layer_info = layers[layer]
logging.info(f"* Checking {layer_info.name}")
layer_index = layout.find_layer(layer_info.layer, layer_info.data_type)
if layer_index is not None:
raise PrecheckFailure(f"Forbidden layer {layer} found in {gds}")
logging.info("Running prBoundary check...")
layer_info = layers["prBoundary.boundary"]
layer_index = layout.find_layer(layer_info.layer, layer_info.data_type)
if layer_index is None:
calma_index = f"{layer_info.layer}/{layer_info.data_type}"
raise PrecheckFailure(
f"prBoundary.boundary ({calma_index}) layer not found in {gds}"
)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--gds", required=True)
parser.add_argument("--lef", required=False)
parser.add_argument("--template-def", required=False)
parser.add_argument("--top-module", required=False)
args = parser.parse_args()
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logging.info(f"PDK_ROOT: {PDK_ROOT}")
if args.top_module:
top_module = args.top_module
else:
top_module = os.path.splitext(os.path.basename(args.gds))[0]
if args.lef:
lef = args.lef
else:
lef = os.path.splitext(args.gds)[0] + ".lef"
if args.template_def:
template_def = args.template_def
else:
yaml_dir = os.path.dirname(args.gds)
while not os.path.exists(f"{yaml_dir}/info.yaml"):
yaml_dir = os.path.dirname(yaml_dir)
if yaml_dir == "/":
raise PrecheckFailure("info.yaml not found")
yaml_file = f"{yaml_dir}/info.yaml"
yaml_data = yaml.safe_load(open(yaml_file))
logging.info("info.yaml data:" + str(yaml_data))
tiles = yaml_data.get("project", {}).get("tiles", "1x1")
is_analog = yaml_data.get("project", {}).get("analog_pins", 0) > 0
if is_analog:
template_def = f"../def/analog/tt_block_{tiles}_pg_ana.def"
else:
template_def = f"../def/tt_block_{tiles}_pg.def"
logging.info(f"using def template {template_def}")
checks = [
["Magic DRC", lambda: magic_drc(args.gds, top_module)],
["KLayout FEOL", lambda: klayout_drc(args.gds, "feol")],
["KLayout BEOL", lambda: klayout_drc(args.gds, "beol")],
["KLayout offgrid", lambda: klayout_drc(args.gds, "offgrid")],
[
"KLayout pin label overlapping drawing",
lambda: klayout_drc(
args.gds,
"pin_label_purposes_overlapping_drawing",
"pin_label_purposes_overlapping_drawing.rb.drc",
),
],
["KLayout zero area", lambda: klayout_zero_area(args.gds)],
["KLayout Checks", lambda: klayout_checks(args.gds)],
["Pin check", lambda: pin_check(args.gds, lef, template_def, top_module)],
]
testsuite = ET.Element("testsuite", name="Tiny Tapeout Prechecks")
error_count = 0
markdown_table = "# Tiny Tapeout Precheck Results\n\n"
markdown_table += "| Check | Result |\n|-----------|--------|\n"
for [name, check] in checks:
start_time = time.time()
test_case = ET.SubElement(testsuite, "testcase", name=name)
try:
check()
elapsed_time = time.time() - start_time
markdown_table += f"| {name} | ✅ |\n"
test_case.set("time", str(round(elapsed_time, 2)))
except Exception as e:
error_count += 1
elapsed_time = time.time() - start_time
markdown_table += f"| {name} | ❌ Fail: {str(e)} |\n"
test_case.set("time", str(round(elapsed_time, 2)))
error = ET.SubElement(test_case, "error", message=str(e))
error.text = traceback.format_exc()
markdown_table += "\n"
markdown_table += "In case of failure, please reach out on [discord](https://tinytapeout.com/discord) for assistance."
testsuites = ET.Element("testsuites")
testsuites.append(testsuite)
xunit_report = ET.ElementTree(testsuites)
ET.indent(xunit_report, space=" ", level=0)
xunit_report.write(f"{REPORTS_PATH}/results.xml", encoding="unicode")
with open(f"{REPORTS_PATH}/results.md", "w") as f:
f.write(markdown_table)
if error_count > 0:
logging.error(f"Precheck failed for {args.gds}! 😭")
logging.error(f"See {REPORTS_PATH} for more details")
logging.error(f"Markdown report:\n{markdown_table}")
exit(1)
else:
logging.info(f"Precheck passed for {args.gds}! 🎉")
if __name__ == "__main__":
main()