| # Copyright lowRISC contributors. | 
 | # Licensed under the Apache License, Version 2.0, see LICENSE for details. | 
 | # SPDX-License-Identifier: Apache-2.0 | 
 | r""" | 
 | Utility functions common across dvsim. | 
 | """ | 
 |  | 
 | import logging as log | 
 | import os | 
 | import re | 
 | import shlex | 
 | import shutil | 
 | import subprocess | 
 | import sys | 
 | import time | 
 | from collections import OrderedDict | 
 | from datetime import datetime | 
 | from pathlib import Path | 
 |  | 
 | import hjson | 
 | import mistletoe | 
 | from premailer import transform | 
 |  | 
 | # For verbose logging | 
 | VERBOSE = 15 | 
 |  | 
 | # Timestamp format when creating directory backups. | 
 | TS_FORMAT = "%y.%m.%d_%H.%M.%S" | 
 |  | 
 | # Timestamp format when generating reports. | 
 | TS_FORMAT_LONG = "%A %B %d %Y %H:%M:%S UTC" | 
 |  | 
 |  | 
 | # Run a command and get the result. Exit with error if the command did not | 
 | # succeed. This is a simpler version of the run_cmd function below. | 
 | def run_cmd(cmd): | 
 |     (status, output) = subprocess.getstatusoutput(cmd) | 
 |     if status: | 
 |         print(f'cmd {cmd} returned with status {status}', file=sys.stderr) | 
 |         sys.exit(status) | 
 |     return output | 
 |  | 
 |  | 
 | # Run a command with a specified timeout. If the command does not finish before | 
 | # the timeout, then it returns -1. Else it returns the command output. If the | 
 | # command fails, it throws an exception and returns the stderr. | 
 | def run_cmd_with_timeout(cmd, timeout=-1, exit_on_failure=1): | 
 |     args = shlex.split(cmd) | 
 |     p = subprocess.Popen(args, | 
 |                          stdout=subprocess.PIPE, | 
 |                          stderr=subprocess.STDOUT) | 
 |  | 
 |     # If timeout is set, poll for the process to finish until timeout | 
 |     result = "" | 
 |     status = -1 | 
 |     if timeout == -1: | 
 |         p.wait() | 
 |     else: | 
 |         start = time.time() | 
 |         while time.time() - start < timeout: | 
 |             if p.poll() is not None: | 
 |                 break | 
 |             time.sleep(.01) | 
 |  | 
 |     # Capture output and status if cmd exited, else kill it | 
 |     if p.poll() is not None: | 
 |         result = p.communicate()[0] | 
 |         status = p.returncode | 
 |     else: | 
 |         log.error("cmd \"%s\" timed out!", cmd) | 
 |         p.kill() | 
 |  | 
 |     if status != 0: | 
 |         log.error("cmd \"%s\" exited with status %d", cmd, status) | 
 |         if exit_on_failure == 1: | 
 |             sys.exit(status) | 
 |  | 
 |     return (result, status) | 
 |  | 
 |  | 
 | # Parse hjson and return a dict | 
 | def parse_hjson(hjson_file): | 
 |     hjson_cfg_dict = None | 
 |     try: | 
 |         log.debug("Parsing %s", hjson_file) | 
 |         f = open(hjson_file, 'rU') | 
 |         text = f.read() | 
 |         hjson_cfg_dict = hjson.loads(text, use_decimal=True) | 
 |         f.close() | 
 |     except Exception as e: | 
 |         log.fatal( | 
 |             "Failed to parse \"%s\" possibly due to bad path or syntax error.\n%s", | 
 |             hjson_file, e) | 
 |         sys.exit(1) | 
 |     return hjson_cfg_dict | 
 |  | 
 |  | 
 | def _stringify_wildcard_value(value): | 
 |     '''Make sense of a wildcard value as a string (see subst_wildcards) | 
 |  | 
 |     Strings are passed through unchanged. Integer or boolean values are printed | 
 |     as numerical strings. Lists or other sequences have their items printed | 
 |     separated by spaces. | 
 |  | 
 |     ''' | 
 |     if type(value) is str: | 
 |         return value | 
 |  | 
 |     if type(value) in [bool, int]: | 
 |         return str(int(value)) | 
 |  | 
 |     try: | 
 |         return ' '.join(_stringify_wildcard_value(x) for x in value) | 
 |     except TypeError: | 
 |         raise ValueError('Wildcard had value {!r} which is not of a supported ' | 
 |                          'type.'.format(value)) | 
 |  | 
 |  | 
 | def _subst_wildcards(var, mdict, ignored, ignore_error, seen): | 
 |     '''Worker function for subst_wildcards | 
 |  | 
 |     seen is a list of wildcards that have been expanded on the way to this call | 
 |     (used for spotting circular recursion). | 
 |  | 
 |     Returns (expanded, seen_err) where expanded is the new value of the string | 
 |     and seen_err is true if we stopped early because of an ignored error. | 
 |  | 
 |     ''' | 
 |     wildcard_re = re.compile(r"{([A-Za-z0-9\_]+)}") | 
 |  | 
 |     # Work from left to right, expanding each wildcard we find. idx is where we | 
 |     # should start searching (so that we don't keep finding a wildcard that | 
 |     # we've decided to ignore). | 
 |     idx = 0 | 
 |  | 
 |     any_err = False | 
 |  | 
 |     while True: | 
 |         right_str = var[idx:] | 
 |         match = wildcard_re.search(right_str) | 
 |  | 
 |         # If no match, we're done. | 
 |         if match is None: | 
 |             return (var, any_err) | 
 |  | 
 |         name = match.group(1) | 
 |  | 
 |         # If the name should be ignored, skip over it. | 
 |         if name in ignored: | 
 |             idx += match.end() | 
 |             continue | 
 |  | 
 |         # If the name has been seen already, we've spotted circular recursion. | 
 |         # That's not allowed! | 
 |         if name in seen: | 
 |             raise ValueError('String contains circular expansion of ' | 
 |                              'wildcard {!r}.'.format(match.group(0))) | 
 |  | 
 |         # Treat eval_cmd specially | 
 |         if name == 'eval_cmd': | 
 |             cmd = _subst_wildcards(right_str[match.end():], mdict, ignored, | 
 |                                    ignore_error, seen)[0] | 
 |  | 
 |             # Are there any wildcards left in cmd? If not, we can run the | 
 |             # command and we're done. | 
 |             cmd_matches = list(wildcard_re.finditer(cmd)) | 
 |             if not cmd_matches: | 
 |                 var = var[:match.start()] + run_cmd(cmd) | 
 |                 continue | 
 |  | 
 |             # Otherwise, check that each of them is ignored, or that | 
 |             # ignore_error is True. | 
 |             bad_names = False | 
 |             if not ignore_error: | 
 |                 for cmd_match in cmd_matches: | 
 |                     if cmd_match.group(1) not in ignored: | 
 |                         bad_names = True | 
 |  | 
 |             if bad_names: | 
 |                 raise ValueError('Cannot run eval_cmd because the command ' | 
 |                                  'expands to {!r}, which still contains a ' | 
 |                                  'wildcard.'.format(cmd)) | 
 |  | 
 |             # We can't run the command (because it still has wildcards), but we | 
 |             # don't want to report an error either because ignore_error is true | 
 |             # or because each wildcard that's left is ignored. Return the | 
 |             # partially evaluated version. | 
 |             return (var[:idx] + right_str[:match.end()] + cmd, True) | 
 |  | 
 |         # Otherwise, look up name in mdict. | 
 |         value = mdict.get(name) | 
 |  | 
 |         # If the value isn't set, check the environment | 
 |         if value is None: | 
 |             value = os.environ.get(name) | 
 |  | 
 |         if value is None: | 
 |             # Ignore missing values if ignore_error is True. | 
 |             if ignore_error: | 
 |                 idx += match.end() | 
 |                 continue | 
 |  | 
 |             raise ValueError('String to be expanded contains ' | 
 |                              'unknown wildcard, {!r}.'.format(match.group(0))) | 
 |  | 
 |         value = _stringify_wildcard_value(value) | 
 |  | 
 |         # Do any recursive expansion of value, adding name to seen (to avoid | 
 |         # circular recursion). | 
 |         value, saw_err = _subst_wildcards(value, mdict, ignored, ignore_error, | 
 |                                           seen + [name]) | 
 |  | 
 |         # Replace the original match with the result and go around again. If | 
 |         # saw_err, increment idx past what we just inserted. | 
 |         var = (var[:idx] + right_str[:match.start()] + value + | 
 |                right_str[match.end():]) | 
 |         if saw_err: | 
 |             any_err = True | 
 |             idx += match.start() + len(value) | 
 |  | 
 |  | 
 | def subst_wildcards(var, mdict, ignored_wildcards=[], ignore_error=False): | 
 |     '''Substitute any "wildcard" variables in the string var. | 
 |  | 
 |     var is the string to be substituted. mdict is a dictionary mapping | 
 |     variables to strings. ignored_wildcards is a list of wildcards that | 
 |     shouldn't be substituted. ignore_error means to partially evaluate rather | 
 |     than exit on an error. | 
 |  | 
 |     A wildcard is written as a name (alphanumeric, allowing backslash and | 
 |     underscores) surrounded by braces. For example, | 
 |  | 
 |       subst_wildcards('foo {x} baz', {'x': 'bar'}) | 
 |  | 
 |     returns "foo bar baz". Dictionary values can be strings, booleans, integers | 
 |     or lists. For example: | 
 |  | 
 |       subst_wildcards('{a}, {b}, {c}, {d}', | 
 |                       {'a': 'a', 'b': True, 'c': 42, 'd': ['a', 10]}) | 
 |  | 
 |     returns 'a, 1, 42, a 10'. | 
 |  | 
 |     If a wildcard is in ignored_wildcards, it is ignored. For example, | 
 |  | 
 |       subst_wildcards('{a} {b}', {'b': 'bee'}, ignored_wildcards=['a']) | 
 |  | 
 |     returns "{a} bee". | 
 |  | 
 |     If a wildcard appears in var but is not in mdict, the environment is | 
 |     checked for the variable. If the name still isn't found, the default | 
 |     behaviour is to log an error and exit. If ignore_error is True, the | 
 |     wildcard is ignored (as if it appeared in ignore_wildcards). | 
 |  | 
 |     If {eval_cmd} appears in the string and 'eval_cmd' is not in | 
 |     ignored_wildcards then the following text is recursively expanded. The | 
 |     result of this expansion is treated as a command to run and the text is | 
 |     replaced by the output of the command. | 
 |  | 
 |     If a wildcard has been ignored (either because of ignored_wildcards or | 
 |     ignore_error), the command to run in eval_cmd might contain a match for | 
 |     wildcard_re. If ignore_error is True, the command is not run. So | 
 |  | 
 |        subst_wildcards('{eval_cmd}{foo}', {}, ignore_error=True) | 
 |  | 
 |     will return '{eval_cmd}{foo}' unchanged. If ignore_error is False, the | 
 |     function logs an error and exits. | 
 |  | 
 |     Recursion is possible in subst_wildcards. For example, | 
 |  | 
 |       subst_wildcards('{a}', {'a': '{b}', 'b': 'c'}) | 
 |  | 
 |     returns 'c'. Circular recursion is detected, however. So | 
 |  | 
 |       subst_wildcards('{a}', {'a': '{b}', 'b': '{a}'}) | 
 |  | 
 |     will log an error and exit. This error is raised whether or not | 
 |     ignore_error is set. | 
 |  | 
 |     Since subst_wildcards works from left to right, it's possible to compute | 
 |     wildcard names with code like this: | 
 |  | 
 |       subst_wildcards('{a}b}', {'a': 'a {', 'b': 'bee'}) | 
 |  | 
 |     which returns 'a bee'. This is pretty hard to read though, so is probably | 
 |     not a good idea to use. | 
 |  | 
 |     ''' | 
 |     try: | 
 |         return _subst_wildcards(var, mdict, ignored_wildcards, ignore_error, | 
 |                                 [])[0] | 
 |     except ValueError as err: | 
 |         log.error(str(err)) | 
 |         sys.exit(1) | 
 |  | 
 |  | 
 | def find_and_substitute_wildcards(sub_dict, | 
 |                                   full_dict, | 
 |                                   ignored_wildcards=[], | 
 |                                   ignore_error=False): | 
 |     ''' | 
 |     Recursively find key values containing wildcards in sub_dict in full_dict | 
 |     and return resolved sub_dict. | 
 |     ''' | 
 |     for key in sub_dict.keys(): | 
 |         if type(sub_dict[key]) in [dict, OrderedDict]: | 
 |             # Recursively call this funciton in sub-dicts | 
 |             sub_dict[key] = find_and_substitute_wildcards( | 
 |                 sub_dict[key], full_dict, ignored_wildcards, ignore_error) | 
 |  | 
 |         elif type(sub_dict[key]) is list: | 
 |             sub_dict_key_values = list(sub_dict[key]) | 
 |             # Loop through the list of key's values and substitute each var | 
 |             # in case it contains a wildcard | 
 |             for i in range(len(sub_dict_key_values)): | 
 |                 if type(sub_dict_key_values[i]) in [dict, OrderedDict]: | 
 |                     # Recursively call this funciton in sub-dicts | 
 |                     sub_dict_key_values[i] = \ | 
 |                         find_and_substitute_wildcards(sub_dict_key_values[i], | 
 |                                                       full_dict, ignored_wildcards, ignore_error) | 
 |  | 
 |                 elif type(sub_dict_key_values[i]) is str: | 
 |                     sub_dict_key_values[i] = subst_wildcards( | 
 |                         sub_dict_key_values[i], full_dict, ignored_wildcards, | 
 |                         ignore_error) | 
 |  | 
 |             # Set the substituted key values back | 
 |             sub_dict[key] = sub_dict_key_values | 
 |  | 
 |         elif type(sub_dict[key]) is str: | 
 |             sub_dict[key] = subst_wildcards(sub_dict[key], full_dict, | 
 |                                             ignored_wildcards, ignore_error) | 
 |     return sub_dict | 
 |  | 
 |  | 
 | def md_results_to_html(title, css_file, md_text): | 
 |     '''Convert results in md format to html. Add a little bit of styling. | 
 |     ''' | 
 |     html_text = "<!DOCTYPE html>\n" | 
 |     html_text += "<html lang=\"en\">\n" | 
 |     html_text += "<head>\n" | 
 |     if title != "": | 
 |         html_text += "  <title>{}</title>\n".format(title) | 
 |     html_text += "</head>\n" | 
 |     html_text += "<body>\n" | 
 |     html_text += "<div class=\"results\">\n" | 
 |     html_text += mistletoe.markdown(md_text) | 
 |     html_text += "</div>\n" | 
 |     html_text += "</body>\n" | 
 |     html_text += "</html>\n" | 
 |     html_text = htmc_color_pc_cells(html_text) | 
 |     # this function converts css style to inline html style | 
 |     html_text = transform(html_text, | 
 |                           external_styles=css_file, | 
 |                           cssutils_logging_level=log.ERROR) | 
 |     return html_text | 
 |  | 
 |  | 
 | def htmc_color_pc_cells(text): | 
 |     '''This function finds cells in a html table that contain numerical values | 
 |     (and a few known strings) followed by a single space and an identifier. | 
 |     Depending on the identifier, it shades the cell in a specific way. A set of | 
 |     12 color palettes for setting those shades are encoded in ./style.css. | 
 |     These are 'cna' (grey), 'c0' (red), 'c1' ... 'c10' (green). The shade 'cna' | 
 |     is used for items that are maked as 'not applicable'. The shades 'c1' to | 
 |     'c9' form a gradient from red to lime-green to indicate 'levels of | 
 |     completeness'. 'cna' is used for greying out a box for 'not applicable' | 
 |     items, 'c0' is for items that are considered risky (or not yet started) and | 
 |     'c10' for items that have completed successfully, or that are | 
 |     'in good standing'. | 
 |  | 
 |     These are the supported identifiers: %, %u, G, B, E, W, EN, WN. | 
 |     The shading behavior for these is described below. | 
 |  | 
 |     %:  Coloured percentage, where the number in front of the '%' sign is mapped | 
 |         to a color for the cell ranging from red ('c0') to green ('c10'). | 
 |     %u: Uncoloured percentage, where no markup is applied and '%u' is replaced | 
 |         with '%' in the output. | 
 |     G:  This stands for 'Good' and results in a green cell. | 
 |     B:  This stands for 'Bad' and results in a red cell. | 
 |     E:  This stands for 'Errors' and the cell is colored with red if the number | 
 |         in front of the indicator is larger than 0. Otherwise the cell is | 
 |         colored with green. | 
 |     W:  This stands for 'Warnings' and the cell is colored with yellow ('c6') | 
 |         if the number in front of the indicator is larger than 0. Otherwise | 
 |         the cell is colored with green. | 
 |     EN: This stands for 'Errors Negative', which behaves the same as 'E' except | 
 |         that the cell is colored red if the number in front of the indicator is | 
 |         negative. | 
 |     WN: This stands for 'Warnings Negative', which behaves the same as 'W' | 
 |         except that the cell is colored yellow if the number in front of the | 
 |         indicator is negative. | 
 |  | 
 |     N/A items can have any of the following indicators and need not be | 
 |     preceeded with a numerical value: | 
 |  | 
 |     '--', 'NA', 'N.A.', 'N.A', 'N/A', 'na', 'n.a.', 'n.a', 'n/a' | 
 |  | 
 |     ''' | 
 |  | 
 |     # Replace <td> with <td class="color-class"> based on the fp | 
 |     # value. "color-classes" are listed in ./style.css as follows: "cna" | 
 |     # for NA value, "c0" to "c10" for fp value falling between 0.00-9.99, | 
 |     # 10.00-19.99 ... 90.00-99.99, 100.0 respetively. | 
 |     def color_cell(cell, cclass, indicator="%"): | 
 |         op = cell.replace("<td", "<td class=\"" + cclass + "\"") | 
 |         # Remove the indicator. | 
 |         op = re.sub(r"\s*" + indicator + r"\s*", "", op) | 
 |         return op | 
 |  | 
 |     # List of 'not applicable' identifiers. | 
 |     na_list = ['--', 'NA', 'N.A.', 'N.A', 'N/A', 'na', 'n.a.', 'n.a', 'n/a'] | 
 |     na_list_patterns = '|'.join(na_list) | 
 |  | 
 |     # List of floating point patterns: '0', '0.0' & '.0' | 
 |     fp_patterns = r"[\+\-]?\d+\.?\d*" | 
 |  | 
 |     patterns = fp_patterns + '|' + na_list_patterns | 
 |     indicators = "%|%u|G|B|E|W|I|EN|WN" | 
 |     match = re.findall( | 
 |         r"(<td.*>\s*(" + patterns + r")\s+(" + indicators + r")\s*</td>)", | 
 |         text) | 
 |     if len(match) > 0: | 
 |         subst_list = {} | 
 |         fp_nums = [] | 
 |         for item in match: | 
 |             # item is a tuple - first is the full string indicating the table | 
 |             # cell which we want to replace, second is the floating point value. | 
 |             cell = item[0] | 
 |             fp_num = item[1] | 
 |             indicator = item[2] | 
 |             # Skip if fp_num is already processed. | 
 |             if (fp_num, indicator) in fp_nums: | 
 |                 continue | 
 |             fp_nums.append((fp_num, indicator)) | 
 |             if fp_num in na_list: | 
 |                 subst = color_cell(cell, "cna", indicator) | 
 |             else: | 
 |                 # Item is a fp num. | 
 |                 try: | 
 |                     fp = float(fp_num) | 
 |                 except ValueError: | 
 |                     log.error( | 
 |                         "Percentage item \"%s\" in cell \"%s\" is not an " | 
 |                         "integer or a floating point number", fp_num, cell) | 
 |                     continue | 
 |                 # Percentage, colored. | 
 |                 if indicator == "%": | 
 |                     if fp >= 0.0 and fp < 10.0: | 
 |                         subst = color_cell(cell, "c0") | 
 |                     elif fp >= 10.0 and fp < 20.0: | 
 |                         subst = color_cell(cell, "c1") | 
 |                     elif fp >= 20.0 and fp < 30.0: | 
 |                         subst = color_cell(cell, "c2") | 
 |                     elif fp >= 30.0 and fp < 40.0: | 
 |                         subst = color_cell(cell, "c3") | 
 |                     elif fp >= 40.0 and fp < 50.0: | 
 |                         subst = color_cell(cell, "c4") | 
 |                     elif fp >= 50.0 and fp < 60.0: | 
 |                         subst = color_cell(cell, "c5") | 
 |                     elif fp >= 60.0 and fp < 70.0: | 
 |                         subst = color_cell(cell, "c6") | 
 |                     elif fp >= 70.0 and fp < 80.0: | 
 |                         subst = color_cell(cell, "c7") | 
 |                     elif fp >= 80.0 and fp < 90.0: | 
 |                         subst = color_cell(cell, "c8") | 
 |                     elif fp >= 90.0 and fp < 100.0: | 
 |                         subst = color_cell(cell, "c9") | 
 |                     elif fp >= 100.0: | 
 |                         subst = color_cell(cell, "c10") | 
 |                 # Percentage, uncolored. | 
 |                 elif indicator == "%u": | 
 |                     subst = cell.replace("%u", "%") | 
 |                 # Good: green | 
 |                 elif indicator == "G": | 
 |                     subst = color_cell(cell, "c10", indicator) | 
 |                 # Bad: red | 
 |                 elif indicator == "B": | 
 |                     subst = color_cell(cell, "c0", indicator) | 
 |                 # Info, uncolored. | 
 |                 elif indicator == "I": | 
 |                     subst = cell.replace("I", "") | 
 |                 # Bad if positive: red for errors, yellow for warnings, | 
 |                 # otherwise green. | 
 |                 elif indicator in ["E", "W"]: | 
 |                     if fp <= 0: | 
 |                         subst = color_cell(cell, "c10", indicator) | 
 |                     elif indicator == "W": | 
 |                         subst = color_cell(cell, "c6", indicator) | 
 |                     elif indicator == "E": | 
 |                         subst = color_cell(cell, "c0", indicator) | 
 |                 # Bad if negative: red for errors, yellow for warnings, | 
 |                 # otherwise green. | 
 |                 elif indicator in ["EN", "WN"]: | 
 |                     if fp >= 0: | 
 |                         subst = color_cell(cell, "c10", indicator) | 
 |                     elif indicator == "WN": | 
 |                         subst = color_cell(cell, "c6", indicator) | 
 |                     elif indicator == "EN": | 
 |                         subst = color_cell(cell, "c0", indicator) | 
 |             subst_list[cell] = subst | 
 |         for item in subst_list: | 
 |             text = text.replace(item, subst_list[item]) | 
 |     return text | 
 |  | 
 |  | 
 | def print_msg_list(msg_list_title, msg_list, max_msg_count=-1): | 
 |     '''This function prints a list of messages to Markdown. | 
 |  | 
 |     The argument msg_list_title contains a string for the list title, whereas | 
 |     the msg_list argument contains the actual list of message strings. | 
 |     max_msg_count limits the number of messages to be printed (set to negative | 
 |     number to print all messages). | 
 |  | 
 |     Example: | 
 |  | 
 |     print_msg_list("### Tool Warnings", ["Message A", "Message B"], 10) | 
 |     ''' | 
 |     md_results = "" | 
 |     if msg_list: | 
 |         md_results += msg_list_title + "\n" | 
 |         md_results += "```\n" | 
 |         for k, msg in enumerate(msg_list): | 
 |             if k <= max_msg_count or max_msg_count < 0: | 
 |                 md_results += msg + "\n\n" | 
 |             else: | 
 |                 md_results += "Note: %d more messages have been suppressed " % ( | 
 |                     len(msg_list) - max_msg_count) | 
 |                 md_results += "(max_msg_count = %d) \n\n" % (max_msg_count) | 
 |                 break | 
 |         md_results += "```\n" | 
 |     return md_results | 
 |  | 
 |  | 
 | def rm_path(path, ignore_error=False): | 
 |     '''Removes the specified path if it exists. | 
 |  | 
 |     'path' is a Path-like object. If it does not exist, the function simply | 
 |     returns. If 'ignore_error' is set, then exception caught by the remove | 
 |     operation is raised, else it is ignored. | 
 |     ''' | 
 |  | 
 |     exc = None | 
 |     try: | 
 |         os.remove(path) | 
 |     except FileNotFoundError: | 
 |         pass | 
 |     except IsADirectoryError: | 
 |         try: | 
 |             shutil.rmtree(path) | 
 |         except OSError as e: | 
 |             exc = e | 
 |     except OSError as e: | 
 |         exc = e | 
 |  | 
 |     if exc: | 
 |         log.error("Failed to remove {}:\n{}.".format(path, exc)) | 
 |         if not ignore_error: | 
 |             raise exc | 
 |  | 
 |  | 
 | def mk_path(path): | 
 |     '''Create the specified path if it does not exist. | 
 |  | 
 |     'path' is a Path-like object. If it does exist, the function simply | 
 |     returns. If it does not exist, the function creates the path and its | 
 |     parent dictories if necessary. | 
 |     ''' | 
 |     try: | 
 |         Path(path).mkdir(parents=True, exist_ok=True) | 
 |     except PermissionError as e: | 
 |         log.fatal("Failed to create directory {}:\n{}.".format(path, e)) | 
 |         sys.exit(1) | 
 |  | 
 |  | 
 | def mk_symlink(path, link): | 
 |     '''Create a symlink from the given path. | 
 |  | 
 |     'link' is a Path-like object. If it does exist, remove the existing link and | 
 |     create a new symlink with this given path. | 
 |     If it does not exist, the function creates the symlink with the given path. | 
 |     ''' | 
 |     while True: | 
 |         try: | 
 |             os.symlink(path, link) | 
 |             break | 
 |         except FileExistsError: | 
 |             rm_path(link) | 
 |  | 
 |  | 
 | def clean_odirs(odir, max_odirs, ts_format=TS_FORMAT): | 
 |     """Clean previous output directories. | 
 |  | 
 |     When running jobs, we may want to maintain a limited history of | 
 |     previous invocations. This method finds and deletes the output | 
 |     directories at the base of input arg 'odir' with the oldest timestamps, | 
 |     if that limit is reached. It returns a list of directories that | 
 |     remain after deletion. | 
 |     """ | 
 |  | 
 |     odir = Path(odir) | 
 |  | 
 |     if os.path.exists(odir): | 
 |         # If output directory exists, back it up. | 
 |         ts = datetime.fromtimestamp(os.stat(odir).st_ctime).strftime(ts_format) | 
 |         # Prior to Python 3.9, shutil may run into an error when passing in | 
 |         # Path objects (see https://bugs.python.org/issue32689). While this | 
 |         # has been fixed in Python 3.9, string casts are added so that this | 
 |         # also works with older versions. | 
 |         shutil.move(str(odir), str(odir.with_name(ts))) | 
 |  | 
 |     # Get list of past output directories sorted by creation time. | 
 |     pdir = odir.resolve().parent | 
 |     if not pdir.exists(): | 
 |         return [] | 
 |  | 
 |     dirs = sorted([old for old in pdir.iterdir() if (old.is_dir() and | 
 |                                                      old != 'summary')], | 
 |                   key=os.path.getctime, | 
 |                   reverse=True) | 
 |  | 
 |     for old in dirs[max(0, max_odirs - 1):]: | 
 |         shutil.rmtree(old, ignore_errors=True) | 
 |  | 
 |     return [] if max_odirs == 0 else dirs[:max_odirs - 1] | 
 |  | 
 |  | 
 | def check_bool(x): | 
 |     """check_bool checks if input 'x' either a bool or | 
 |        one of the following strings: ["true", "false"] | 
 |         It returns value as Bool type. | 
 |     """ | 
 |     if isinstance(x, bool): | 
 |         return x | 
 |     if not x.lower() in ["true", "false"]: | 
 |         raise RuntimeError("{} is not a boolean value.".format(x)) | 
 |     else: | 
 |         return (x.lower() == "true") | 
 |  | 
 |  | 
 | def check_int(x): | 
 |     """check_int checks if input 'x' is decimal integer. | 
 |         It returns value as an int type. | 
 |     """ | 
 |     if isinstance(x, int): | 
 |         return x | 
 |     if not x.isdecimal(): | 
 |         raise RuntimeError("{} is not a decimal number".format(x)) | 
 |     return int(x) |