blob: 1b306934b87056584fc1fa6e37475eb9982304f0 [file] [log] [blame]
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright 2021 Efabless Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
See <openlane_root>/docs/source/using_or_issue.md.
This script is intended for use by bug reporters and maintainers and is not part of the flow.
"""
import os
import re
import sys
import glob
import shutil
import pathlib
import argparse
from typing import List
from collections import deque
from os.path import join, abspath, dirname, basename, isdir, relpath
openlane_path = abspath(dirname(dirname(__file__)))
parser = argparse.ArgumentParser(description="OpenROAD Issue Packager")
parser.add_argument(
"--or-script",
"-s",
required=True,
help="Path to the OpenROAD script causing the failure: i.e. ./scripts/openroad/antenna_check.tcl, ./scripts/openroad/pdn.tcl, etc. [required]",
)
parser.add_argument(
"--pdk-root",
required=(os.getenv("PDK_ROOT") is None),
default=os.getenv("PDK_ROOT"),
help="Path to the PDK root [required if environment variable PDK_ROOT is not set]",
)
parser.add_argument(
"--run-path",
"-r",
default=None,
help="The run path. If not specified, the script will attempt to discern it from the input_def path.",
)
parser.add_argument(
"--output",
"-o",
default="./out.def",
help="Name of def file to be generated [default: ./out.def]",
)
parser.add_argument(
"--verbose",
action="store_true",
default=False,
help="Verbose output of all found environment variables.",
)
parser.add_argument(
"--netlist",
"-n",
action="store_true",
default=False,
help="Use the netlist as an input instead of a def file. Useful for evaluating some scripts such as floorplan.tcl.",
)
parser.add_argument("--output-dir", default=None, help="Output to this directory.")
parser.add_argument(
"input",
help="Name of input into the OR script (usually denoted by environment variable CURRENT_NETLIST or CURRENT_DEF: get it from the logs) [required]",
)
args = parser.parse_args()
OPEN_SOURCE_PDKS = ["sky130A"]
print(
"""
or_issue.py OpenROAD Issue Packager
EFABLESS CORPORATION AND ALL AUTHORS OF THE OPENLANE PROJECT SHALL NOT BE HELD
LIABLE FOR ANY LEAKS THAT MAY OCCUR TO ANY PROPRIETARY DATA AS A RESULT OF USING
THIS SCRIPT. THIS SCRIPT IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND.
BY USING THIS SCRIPT, YOU ACKNOWLEDGE THAT YOU FULLY UNDERSTAND THIS DISCLAIMER
AND ALL IT ENTAILS.
""",
file=sys.stderr,
)
script_path = abspath(args.or_script)
pdk_root = abspath(args.pdk_root)
or_scripts_path = join(openlane_path, "scripts", "openroad")
use_netlist = args.netlist
input_file = abspath(args.input)
run_path = None
if args.run_path is not None:
run_path = abspath(args.run_path)
else:
current_dir = dirname(input_file)
while current_dir != "/":
if "config.tcl" in os.listdir(current_dir):
run_path = current_dir
break
current_dir = dirname(current_dir)
if run_path is None:
print(
f"[ERR] No run path provided and {input_file} is not in the run path.",
file=sys.stderr,
)
exit(os.EX_USAGE)
else:
print(f"[INF] Resolved run path to {run_path}.")
save_def = args.output
verbose = args.verbose
if not os.path.exists(input_file):
print(f"[ERR] {input_file} not found.", file=sys.stderr)
exit(os.EX_NOINPUT)
if not script_path.startswith(or_scripts_path):
print(
f"[ERR] The OpenROAD script {script_path} does not appear to be in {or_scripts_path}.",
file=sys.stderr,
)
exit(os.EX_CONFIG)
if not os.path.exists(run_path) and os.path.isdir(run_path):
print(f"[ERR] The run path {run_path} is not a valid folder.", file=sys.stderr)
exit(os.EX_CONFIG)
run_name = basename(run_path)
# Phase 1: Read All Environment Variables
# pdk_config = join(args.pdk_root, "sky130A", "libs.tech", "openlane", "config.tcl")
print("Parsing config file(s)…", file=sys.stderr)
run_config = join(run_path, "config.tcl")
env = {}
def read_env(config_path: str, from_path: str, input_env={}) -> dict:
rx = r"\s*set\s*::env\((.+?)\)\s*(.+)"
env = input_env.copy()
string_data = ""
try:
string_data = open(config_path).read()
except FileNotFoundError:
print(
f"❌ File {config_path} not found. The path {from_path} may have been specified incorrectly.",
file=sys.stderr,
)
exit(os.EX_NOINPUT)
# Process \ at ends of lines, remove semicolons
entries = string_data.split("\n")
i = 0
while i < len(entries):
if not entries[i].endswith("\\"):
if entries[i].endswith(";"):
entries[i] = entries[i][:-1]
i += 1
continue
entries[i] = entries[i][:-1] + entries[i + 1]
del entries[i + 1]
for entry in entries:
match = re.match(rx, entry)
if match is None:
continue
name = match[1]
value = match[2]
# remove double quotes/{}
value = value.strip('"')
value = value.strip("{}")
env[name] = value
return env
env = read_env(run_config, "Run Path") # , read_env(pdk_config, "PDK Root"))
# Cannot be reliably read from config.tcl
input_key = "CURRENT_DEF"
if use_netlist:
input_key = "CURRENT_NETLIST"
env[input_key] = input_file
env["SAVE_DEF"] = save_def
# Phase 2: Set up destination folder
script_basename = basename(args.or_script)[:-4]
destination_folder = args.output_dir or abspath(
join(".", "_build", f"{run_name}_{script_basename}_packaged")
)
print(f"Setting up {destination_folder}…", file=sys.stderr)
def mkdirp(path):
return pathlib.Path(path).mkdir(parents=True, exist_ok=True)
try:
shutil.rmtree(destination_folder)
except FileNotFoundError:
pass
mkdirp(destination_folder)
# Phase 3: Process TCL Scripts To Find Full List Of Files
tcls_to_process = deque([script_path])
def shift(deque):
try:
return deque.popleft()
except Exception:
return None
or_script_counter = 0
def get_or_script():
global or_script_counter
value = f"OR_SCRIPT_{or_script_counter}"
or_script_counter += 1
return value
env_keys_used = set()
tcls = set()
current = shift(tcls_to_process)
while current is not None:
env_key = get_or_script()
env_keys_used.add(env_key)
env[env_key] = current
try:
script = open(current).read()
for key, value in env.items():
key_accessor = re.compile(rf"((\$::env\({re.escape(key)}\))([/\-\w\.]*))")
for use in key_accessor.findall(script):
use: List[str]
full, accessor, extra = use
env_keys_used.add(key)
value_substituted = full.replace(accessor, value)
if value_substituted.endswith(".tcl") or value_substituted.endswith(
".sdc"
):
if value_substituted not in tcls:
tcls.add(value_substituted)
tcls_to_process.append(value_substituted)
except Exception:
print(
f"[WRN] {current} was not found, might be a product. Skipping",
file=sys.stderr,
)
current = shift(tcls_to_process)
# Phase 4: Copy The Files
final_env = {}
pdk_path = join(destination_folder, "pdk")
openlane_misc_path = join(destination_folder, "openlane")
warnings = []
def copy(frm, to):
parents = dirname(to)
mkdirp(parents)
def do_copy():
if isdir(frm):
shutil.copytree(frm, to)
else:
shutil.copyfile(frm, to)
try:
incomplete_matches = glob.glob(frm + "*")
if len(incomplete_matches) == 0:
raise Exception()
elif len(incomplete_matches) != 1 or incomplete_matches[0] != frm:
# Prefix For Other Files
for match in incomplete_matches:
if match == frm:
# If it's both a file AND a prefix for other files
do_copy()
else:
new_frm = match
new_to = to + new_frm[len(frm) :]
copy(new_frm, new_to)
else:
do_copy()
except Exception as e:
warnings.append(f"[WRN] Couldn't copy {frm}: {e}. Skipped.")
if verbose:
print("\nProcessing environment variables…\n---", file=sys.stderr)
for key in env_keys_used:
value = env[key]
if verbose:
print(f"{key}: {value}")
if value == input_file:
final_path = join(destination_folder, "in.def")
from_path = value
copy(from_path, final_path)
final_env[input_key] = "./in.def"
elif value.startswith(run_path):
relative = relpath(value, run_path)
final_value = join(".", relative)
final_path = join(destination_folder, final_value)
from_path = value
copy(from_path, final_path)
final_env[key] = final_value
elif value.startswith(pdk_root):
nonfree_warning = True
value_components = value.split(os.path.sep)
for pdk in OPEN_SOURCE_PDKS:
if pdk in value_components:
nonfree_warning = False
if nonfree_warning:
warnings.append(
f"[WRN] {value} appears to be a confidential PDK file. ENSURE THAT YOU INSPECT THE RESULTS."
)
relative = relpath(value, pdk_root)
final_value = join("pdk", relative)
final_path = join(destination_folder, final_value)
copy(value, final_path)
final_env[key] = final_value
elif value.startswith("/openlane"):
relative = relpath(value, "/openlane")
final_value = join("openlane", relative)
final_path = join(destination_folder, final_value)
from_path = value.replace("/openlane", openlane_path)
if value != "/openlane/scripts": # Too many files to copy otherwise
copy(from_path, final_path)
final_env[key] = final_value
elif value.startswith("/"):
final_value = value[1:]
final_path = join(destination_folder, final_value)
copy(value, final_path)
final_env[key] = final_value
else:
final_env[key] = value
if verbose:
print("---\n", file=sys.stderr)
for warning in warnings:
print(warning)
print("\n")
# Phase 5: Create Environment Set/Run Files
run_shell = join(destination_folder, "run.sh")
with open(run_shell, "w") as f:
env_list = "\n".join(
[f"export {key}='{value}';" for key, value in final_env.items()]
)
f.write(
f"""\
#!/bin/sh
dir=$(cd -P -- "$(dirname -- "$0")" && pwd -P)
cd $dir;
{env_list}
OPENROAD_BIN=${{OPENROAD_BIN:-openroad}}
$OPENROAD_BIN -exit $OR_SCRIPT_0
"""
)
os.chmod(run_shell, 0o755)
run_tcl = join(destination_folder, "run.tcl")
with open(run_tcl, "w") as f:
env_list = "\n".join(
[f"set ::env({key}) {{{value}}};" for key, value in final_env.items()]
)
f.write(
f"""\
#!/usr/bin/env openroad
{env_list}
source $::env(OR_SCRIPT_0)
"""
)
os.chmod(run_tcl, 0o755)
gdb_env = join(destination_folder, "env.gdb")
with open(gdb_env, "w") as f:
env_list = "\n".join([f"set env {key} {value}" for key, value in final_env.items()])
f.write(
f"""\
{env_list}
"""
)
lldb_env = join(destination_folder, "env.lldb")
with open(lldb_env, "w") as f:
env_list = "\n".join([f"env {key}={value}" for key, value in final_env.items()])
f.write(
f"""\
{env_list}
"""
)
print("[FIN] Done.", file=sys.stderr)
print(destination_folder)