Files
OpenLane/scripts/or_issue.py
Mohamed Gaber b1cf21ab16 Rework Tcl String Escaping (#1782)
+ `scripts/config/tcl.py:escape_quoted_string` escapes strings for placement inside quotes for Tcl
~ Generated Tcl files now use quotes instead of {} for strings, using above
~ `Tcl.py` no longer ruins variables via overzealous escaping
~ `set_log` renamed to `set_and_log`, stupid arguments removed
~ `save_state` rewritten to rely on initial copy of `::env` array instead of spaghetti mess
~ Make 1506 test more comprehensive
2023-05-02 13:39:56 +03:00

416 lines
13 KiB
Python
Executable File

#!/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.
"""
import os
import re
import sys
import glob
import shutil
import pathlib
import textwrap
from typing import Callable, List, Dict
from collections import deque
from os.path import join, abspath, dirname, basename, isdir, relpath
import click
from config.tcl import read_tcl_env, escape_quoted_string
openlane_path = abspath(dirname(dirname(__file__)))
ws = re.compile(r"\s+")
@click.command()
@click.option(
"--script",
"-s",
required=True,
help="Path to the Tcl script causing the failure: i.e. ./scripts/openroad/antenna_check.tcl, ./scripts/magic/drc.tcl, etc. [required]",
)
@click.option(
"--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]",
)
@click.option(
"--run-path",
"-r",
default=None,
help="The run path. If not specified, the script will attempt to discern it from the input file's path.",
)
@click.option(
"--output",
"-o",
"save_def",
default="./out.def",
help="Name of def file to be generated [default: ./out.def]",
)
@click.option(
"--output-db",
"-O",
"save_odb",
default="./out.odb",
help="Name of odb file to be generated [default: ./out.odb]",
)
@click.option(
"--output-netlist",
"-O",
"save_netlist",
default="./out.v",
help="Name of verilog netlist to be generated [default: ./out.v]",
)
@click.option(
"--verbose/--not-verbose",
default=False,
help="Verbose output of all found environment variables.",
)
@click.option(
"--input-type",
type=click.Choice(["def", "netlist", "odb", "n/a"]),
default="odb",
help="Determine type of input file.",
)
@click.option("--output-dir", default=None, help="Output to this directory.")
@click.option(
"--tool",
type=click.Choice(["sta", "openroad", "magic", "yosys"]),
help="The tool used for the desired Tcl script. [required]",
)
@click.argument("input_file")
def issue(
script,
pdk_root,
run_path,
save_def,
save_odb,
save_netlist,
verbose,
input_type,
output_dir,
tool,
input_file,
):
"""
Issue packager for Tcl-based tools (currently: OpenROAD, Magic)
The or_ prefix is an artifact name because this used to only work with OpenROAD.
Usage: or_issue.py [OPTIONS] <input_file>
input_file: Name of input into the script (usually denoted by environment variable CURRENT_NETLIST or CURRENT_DEF: get it from the logs)
"""
print(
textwrap.dedent(
"""\
OpenLane TCL 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_abs_path = abspath(script)
if run_path is not None:
run_path = abspath(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}.")
if not os.path.exists(input_file):
print(f"[ERR] {input_file} not found.", file=sys.stderr)
exit(os.EX_NOINPUT)
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
print("Parsing config file(s)…", file=sys.stderr)
run_config = join(run_path, "config.tcl")
env = read_tcl_env(run_config)
# Cannot be reliably read from config.tcl
input_key = "CURRENT_NOTHING"
if input_type == "def":
input_key = "CURRENT_DEF"
env["SAVE_DEF"] = save_def
if input_type == "odb":
input_key = "CURRENT_ODB"
env["SAVE_ODB"] = save_odb
env["SAVE_DEF"] = save_def
if input_type == "netlist":
input_key = "CURRENT_NETLIST"
env["SAVE_NETLIST"] = save_netlist
env[input_key] = input_file
# Phase 2: Set up destination folder
script_basename = basename(script_abs_path)[:-4]
destination_folder = 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_abs_path])
def shift(deque):
try:
return deque.popleft()
except Exception:
return None
script_counter = 0
def get_script_key():
nonlocal script_counter
value = f"PACKAGED_SCRIPT_{script_counter}"
script_counter += 1
return value
env_keys_used = set()
if tool == "magic":
env_keys_used.add("MAGIC_MAGICRC")
tcls = set()
current = shift(tcls_to_process)
while current is not None:
env_key = get_script_key()
env_keys_used.add(env_key)
env[env_key] = current
try:
script_abs_path = open(current).read()
if verbose:
print(f"Processing {current}...", file=sys.stderr)
for key, value in env.items():
key_accessor = re.compile(
rf"((\$::env\({re.escape(key)}\))([/\-\w\.]*))"
)
for use in key_accessor.findall(script_abs_path):
use: List[str]
full, accessor, extra = use
env_keys_used.add(key)
if verbose:
print(f"Found {accessor}", file=sys.stderr)
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 = {}
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:
full_value = env[key]
final_env[key] = ""
if verbose:
print(f"Processing {key}: {full_value}", file=sys.stderr)
for split_value in ws.split(full_value):
if split_value.startswith(run_path):
final_env[key] = ""
relative = relpath(split_value, run_path)
final_value = join(".", relative)
final_path = join(destination_folder, final_value)
from_path = split_value
copy(from_path, final_path)
final_env[key] += f"{final_value} "
elif split_value.startswith(pdk_root):
relative = relpath(split_value, pdk_root)
final_value = join("pdk", relative)
final_path = join(destination_folder, final_value)
copy(split_value, final_path)
final_env[key] += f"{final_value} "
elif split_value.startswith("/openlane"):
relative = relpath(split_value, "/openlane")
final_value = join("openlane", relative)
final_path = join(destination_folder, final_value)
from_path = split_value.replace("/openlane", openlane_path)
if (
split_value != "/openlane/scripts"
): # Too many files to copy otherwise
copy(from_path, final_path)
final_env[key] += f"{final_value} "
elif split_value.startswith("/") and not split_value.startswith(
"/dev"
): # /dev/null, /dev/stdout, /dev/stderr, etc should still work
final_value = split_value[1:]
final_path = join(destination_folder, final_value)
copy(split_value, final_path)
final_env[key] += f"{final_value} "
else:
final_env[key] += f"{split_value} "
final_env[key] = final_env[key].rstrip()
if verbose:
print("---\n", file=sys.stderr)
for warning in warnings:
print(warning)
print("\n")
# Phase 5: Create Environment Set/Run Files
def env_list(
format_string: str = "{key}={value}",
env: Dict[str, str] = final_env,
indent: int = 0,
value_process: Callable[[str], str] = lambda x: x,
) -> str:
array = []
for key, value in sorted(env.items()):
array.append(format_string.format(key=key, value=value_process(value)))
value = f"\n{' ' * indent}".join(array)
return value
run_shell = join(destination_folder, "run.sh")
with open(run_shell, "w") as f:
run_cmd = None
if tool == "openroad":
run_cmd = "$TOOL_BIN -exit $PACKAGED_SCRIPT_0"
elif tool == "magic":
run_cmd = "$TOOL_BIN -dnull -noconsole -rcfile $MAGIC_MAGICRC < $PACKAGED_SCRIPT_0"
elif tool == "sta":
run_cmd = "$TOOL_BIN -exit $PACKAGED_SCRIPT_0"
elif tool == "yosys":
run_cmd = "$TOOL_BIN -c $PACKAGED_SCRIPT_0"
f.write(
textwrap.dedent(
f"""\
#!/bin/sh
dir=$(cd -P -- "$(dirname -- "$0")" && pwd -P)
cd $dir;
{env_list("export {key}='{value}';", indent=4)}
TOOL_BIN=${{TOOL_BIN:-{tool}}}
{run_cmd}
"""
)
)
os.chmod(run_shell, 0o755)
if tool == "openroad":
run_tcl = join(destination_folder, "run.tcl")
with open(run_tcl, "w") as f:
f.write(
textwrap.dedent(
f"""\
#!/usr/bin/env openroad
{env_list('set ::env({key}) "{value}";', indent=5, value_process=escape_quoted_string)}
source $::env(PACKAGED_SCRIPT_0)
"""
)
)
os.chmod(run_tcl, 0o755)
gdb_env = join(destination_folder, "env.gdb")
with open(gdb_env, "w") as f:
f.write(env_list("set env {key} {value}"))
lldb_env = join(destination_folder, "env.lldb")
with open(lldb_env, "w") as f:
f.write(env_list("env {key}={value}"))
print("Done.", file=sys.stderr)
print(destination_folder)
if __name__ == "__main__":
issue()