text: #!/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/or_antenna_check.tcl, ./scripts/openroad/or_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('input', help='Name of def file input into the OR script (usually denoted by environment variable CURRENT_DEF: get it from the logs) [required]')
  args = parser.parse_args()
  
  OPEN_SOURCE_PDKS = ["sky130A"]
  print("""
  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")
  current_def = abspath(args.input)
  run_path = None
  if args.run_path is not None:
      run_path = abspath(args.run_path)
  else:
      current_dir = dirname(current_def)
      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"❌ No run path provided and {current_def} is not in the run path.", file=sys.stderr)
          exit(os.EX_USAGE)
      else:
          print(f"ℹ Resolved run path to {run_path}.")
  
  save_def = args.output
  verbose = args.verbose
  
  if not os.path.exists(current_def):
      print(f"❌ {current_def} not found.", file=sys.stderr)
      exit(os.EX_NOINPUT)
  
  if not script_path.startswith(or_scripts_path):
      print(f"❌ 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"❌ 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(f"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
          if value.startswith('"') and value.endswith('"'): 
              value = value[1:-1]
          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
  env["CURRENT_DEF"] = current_def
  env["SAVE_DEF"] = save_def
  
  
  # Phase 2: Set up destination folder
  script_basename = basename(args.or_script)[:-4]
  destination_folder = 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:
          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
  
      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"):
                  if value_substituted not in tcls:
                      tcls.add(value_substituted)
                      tcls_to_process.append(value_substituted)
  
      current = shift(tcls_to_process)
  
  # Phase 4: Copy The Files
  final_env_pairs = []
  
  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"ℹ 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 == current_def:
          final_path = join(destination_folder, "in.def")
          from_path = value
          copy(from_path, final_path)
          final_env_pairs.append(("CURRENT_DEF", "./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_pairs.append((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"⚠ CAUTION: {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_pairs.append((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_pairs.append((key, final_value))
      elif value.startswith("/"):
          final_value = value[1:]
          final_path = join(destination_folder, final_value)
          copy(value, final_path)
          final_env_pairs.append((key, final_value))
      else:
          final_env_pairs.append((key, value))  
  if verbose:
      print("---\n", file=sys.stderr)
  
  for warning in warnings:
      print(warning)
  print('\n')
      
  # Phase 5: Create Run Files
  run_raw = join(destination_folder, "run")
  with open(run_raw, "w") as f:
      env_list = "\n".join([f"export {key}='{value}';" for key, value in final_env_pairs])
      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_raw, 0o755)
  
  # Phase 6: Tarball and output
  last_output = f"{destination_folder}.tar.gz"
  
  os.system(f"tar -cvC {destination_folder} . | gzip > {last_output}")
  
  print("⭕️ Done.", file=sys.stderr)
  print(last_output)
  