| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| # | |
| # Copyright (C) 2022 F4PGA Authors | |
| # | |
| # 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. | |
| # | |
| # SPDX-License-Identifier: Apache-2.0 | |
| from argparse import ArgumentParser, Namespace | |
| from re import finditer as re_finditer | |
| def p_add_flow_arg(parser: ArgumentParser): | |
| parser.add_argument("-f", "--flow", metavar="flow_path", type=str, help="Path to flow definition file") | |
| def p_setup_build_parser(parser: ArgumentParser): | |
| p_add_flow_arg(parser) | |
| parser.add_argument( | |
| "-t", "--target", metavar="target_name", type=str, help="Perform stages necessary to acquire target" | |
| ) | |
| parser.add_argument( | |
| "-P", "--pretend", action="store_true", help="Show dependency resolution without executing flow" | |
| ) | |
| parser.add_argument("-i", "--info", action="store_true", help="Display info about available targets") | |
| parser.add_argument( | |
| "-c", "--nocache", action="store_true", help="Ignore caching and rebuild everything up to the target." | |
| ) | |
| parser.add_argument("-S", "--stageinfo", nargs=1, metavar="stage_name", help="Display info about stage") | |
| parser.add_argument("-p", "--part", metavar="part_name", help="Name of the target chip") | |
| parser.add_argument("--dep", "-D", action="append", default=[]) | |
| parser.add_argument("--val", "-V", action="append", default=[]) | |
| def p_setup_show_dep_parser(parser: ArgumentParser): | |
| parser.add_argument( | |
| "-p", "--part", metavar="part_name", type=str, help="Name of the part (use to display part-specific values.)" | |
| ) | |
| parser.add_argument( | |
| "-s", | |
| "--stage", | |
| metavar="stage_name", | |
| type=str, | |
| help="Name of the stage (use if you want to set the value only for that stage). Requires `-p`.", | |
| ) | |
| p_add_flow_arg(parser) | |
| def setup_argparser(): | |
| """ | |
| Set up argument parser for the program. | |
| """ | |
| parser = ArgumentParser(description="F4PGA Build System") | |
| parser.add_argument("-v", "--verbose", action="count", default=0) | |
| parser.add_argument("-s", "--silent", action="store_true") | |
| subparsers = parser.add_subparsers(dest="command") | |
| p_setup_build_parser(subparsers.add_parser("build")) | |
| show_dep = subparsers.add_parser("showd", description="Show the value(s) assigned to a dependency") | |
| p_setup_show_dep_parser(show_dep) | |
| return parser | |
| def p_parse_depval(depvalstr: str): | |
| """ | |
| Parse a dependency or value definition in form of: | |
| optional_stage_name.value_or_dependency_name=value | |
| See `p_parse_cli_value` for detail on how to pass different kinds of values. | |
| """ | |
| d = {"name": None, "stage": None, "value": None} | |
| splitted = list(p_unescaped_separated("=", depvalstr)) | |
| if len(splitted) != 2: | |
| raise Exception("Too many components") | |
| pathstr = splitted[0] | |
| valstr = splitted[1] | |
| path_components = pathstr.split(".") | |
| if len(path_components) < 1: | |
| raise Exception("Missing value") | |
| d["name"] = path_components.pop(len(path_components) - 1) | |
| if len(path_components) > 0: | |
| d["stage"] = path_components.pop(0) | |
| if len(path_components) > 0: | |
| raise Exception("Too many path components") | |
| d["value"] = p_parse_cli_value(valstr) | |
| return d | |
| def p_unescaped_matches(regexp: str, s: str, escape_chr="\\"): | |
| """ | |
| Find all occurences of a pattern in a string that contains escape sequences. | |
| Yields pairs of starting and ending indices of the pattern. | |
| """ | |
| noescapes = "" | |
| # We remove all escape sequnces from a string, so it will match only with | |
| # unescaped characters, but to map the results back to the string containing the | |
| # escape sequences, we need to track the offsets by which the characters were | |
| # shifted. | |
| # TODO: This doesn't handle self-escape case | |
| offsets = [] | |
| offset = 0 | |
| for sl in s.split(escape_chr): | |
| noescape = sl[(1 if offset != 0 else 0) :] if len(sl) > 1 else "" | |
| for _ in noescape: | |
| offsets.append(offset) | |
| offset += 2 | |
| noescapes += noescape | |
| if len(offsets) == 0: | |
| return (item for item in []) | |
| last_offset = offsets[-1] | |
| offsets.append(last_offset) | |
| iter = re_finditer(regexp, noescapes) | |
| for m in iter: | |
| start = m.start() | |
| end = m.end() | |
| off1 = start + offsets[start] | |
| off2 = end + offsets[end] | |
| yield off1, off2 | |
| def p_unescaped_separated(regexp: str, s: str, escape_chr="\\"): | |
| """ | |
| Yields substrings of a string that contains escape sequences. | |
| """ | |
| last_end = 0 | |
| for start, end in p_unescaped_matches(regexp, s, escape_chr=escape_chr): | |
| yield s[last_end:start] | |
| last_end = end | |
| if last_end < len(s): | |
| yield s[last_end:] | |
| else: | |
| yield "" | |
| def p_parse_cli_value(s: str): | |
| """ | |
| Parse a value/dependency passed to CLI | |
| CLI values are generated by the following non-contextual grammar: | |
| S -> :str: (string/number value) | |
| S -> [I] | |
| S -> {D} | |
| I -> I,I | |
| I -> S | |
| D -> D,D | |
| D -> K:S | |
| K -> :str: | |
| Starting symbol = S | |
| Terminal symbols: '[', ']', '{', '}', ':', ,',', :str: | |
| (:str: represents any string where terminals are escaped) | |
| TODO: The current implementation of my parser is crippled and is | |
| not able to parse nested structures. Currently there is no real use | |
| case for having nested structures as values, so it's kinda fine atm. | |
| """ | |
| if len(s) == 0: | |
| return None | |
| # List | |
| if s[0] == "[": | |
| if len(s) < 2 or s[len(s) - 1] != "]": | |
| raise Exception("Missing ']' delimiter") | |
| inner = s[1 : (len(s) - 1)] | |
| if inner == "": | |
| return [] | |
| return [p_parse_cli_value(v) for v in p_unescaped_separated(",", inner)] | |
| # Dictionary | |
| if s[0] == "{": | |
| if len(s) < 2 or s[len(s) - 1] != "}": | |
| raise Exception("Missing '}' delimiter") | |
| d = {} | |
| inner = s[1 : (len(s) - 1)] | |
| if inner == "": | |
| return {} | |
| for kv in p_unescaped_separated(",", inner): | |
| k_v = list(p_unescaped_separated(":", kv)) | |
| if len(k_v) < 2: | |
| raise Exception("Missing value in dictionary entry") | |
| if len(k_v) > 2: | |
| raise Exception("Unexpected ':' token") | |
| key = k_v[0] | |
| value = p_parse_cli_value(k_v[1]) | |
| d[key] = value | |
| return d | |
| # Bool hack | |
| if s == "\\True": | |
| return True | |
| if s == "\\False": | |
| return False | |
| # Number hack | |
| if len(s) >= 3 and s[0:1] == "\\N": | |
| return int(s[2:]) | |
| # String | |
| return s.replace("\\", "") | |
| def get_cli_flow_config(args: Namespace, part: str): | |
| def create_defdict(): | |
| return { | |
| "dependencies": {}, | |
| "values": {}, | |
| } | |
| part_flow_config = create_defdict() | |
| def add_entries(arglist: "list[str]", dict_name: str): | |
| for value_def in (p_parse_depval(cliv) for cliv in arglist): | |
| stage = value_def["stage"] | |
| if stage is None: | |
| part_flow_config[dict_name][value_def["name"]] = value_def["value"] | |
| else: | |
| if part_flow_config.get(stage) is None: | |
| part_flow_config[stage] = create_defdict() | |
| part_flow_config[stage][dict_name][value_def["name"]] = value_def["value"] | |
| add_entries(args.dep, "dependencies") | |
| add_entries(args.val, "values") | |
| return {part: part_flow_config} |