| #!/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} |