blob: 8a159180ff5cb799524099945b1502e6ca0762bd [file] [log] [blame]
#!/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}