| # Copyright 2022 The ChromiumOS Authors | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| """Parser for ebuild dependency specifications. | |
| This is the foundation for parsing many conditional ebuild variables. | |
| https://projects.gentoo.org/pms/8/pms.html#x1-730008.2 | |
| """ | |
| from typing import Callable, List, NamedTuple, Optional | |
| class Error(Exception): | |
| """Base error class for the module.""" | |
| class PmsNameError(Error): | |
| """A token name is invalid.""" | |
| class PmsSyntaxError(Error): | |
| """The syntax is invalid.""" | |
| class NestedOrUnsupportedError(Error): | |
| """Using ||() directly within ||() doesn't make sense.""" | |
| def _dedupe_in_order(iterable) -> List: | |
| """Dedupe elements while maintaining order.""" | |
| # This uses Python dict insertion order that is guaranteed in newer | |
| # versions. | |
| return dict((x, None) for x in iterable).keys() | |
| class Node(NamedTuple): | |
| """A single element.""" | |
| name: str | |
| def reduce( | |
| self, | |
| use_flags: Optional[set] = None, | |
| anyof_reduce: Optional[Callable] = None, | |
| flatten_allof: Optional[bool] = True, | |
| ): # pylint: disable=unused-argument | |
| return [self.name] | |
| def __str__(self): | |
| return self.name | |
| class RootNode: | |
| """A group of nodes.""" | |
| def __init__(self): | |
| self.children = [] | |
| def reduce( | |
| self, | |
| use_flags: Optional[set] = None, | |
| anyof_reduce: Optional[Callable] = None, | |
| flatten_allof: Optional[bool] = True, | |
| ): | |
| return list( | |
| _dedupe_in_order( | |
| x | |
| for child in self.children | |
| for x in child.reduce(use_flags, anyof_reduce, flatten_allof) | |
| ) | |
| ) | |
| def __str__(self): | |
| return " ".join(_dedupe_in_order(str(x) for x in self.children)) | |
| class AllOfNode(RootNode): | |
| """A group of nodes that should be kept together.""" | |
| def reduce( | |
| self, | |
| use_flags: Optional[set] = None, | |
| anyof_reduce: Optional[Callable] = None, | |
| flatten_allof: Optional[bool] = True, | |
| ): | |
| ret = super().reduce(use_flags, anyof_reduce, flatten_allof) | |
| if not flatten_allof: | |
| ret = (tuple(ret),) | |
| return ret | |
| def __str__(self): | |
| return f"( {super().__str__()} )" | |
| class AnyOfNode(RootNode): | |
| """A group of optional/alternative nodes.""" | |
| def reduce( | |
| self, | |
| use_flags: Optional[set] = None, | |
| anyof_reduce: Optional[Callable] = None, | |
| flatten_allof: Optional[bool] = True, | |
| ): | |
| choices = super().reduce(use_flags, anyof_reduce, False) | |
| if anyof_reduce: | |
| return [anyof_reduce(choices)] | |
| # Just peel off the first group. If there are any tuples (which come | |
| # from AllOfNodes), we need to flatten. | |
| def _flatten(eles): | |
| for e in eles: | |
| if isinstance(e, tuple): | |
| yield from _flatten(e) | |
| else: | |
| yield e | |
| return list(_flatten(choices[:1])) | |
| def __str__(self): | |
| return f"|| ( {super().__str__()} )" | |
| class UseNode(RootNode): | |
| """A conditional node.""" | |
| def __init__(self, flag: str): | |
| super().__init__() | |
| self.flag = flag | |
| def reduce( | |
| self, | |
| use_flags: Optional[set] = None, | |
| anyof_reduce: Optional[Callable] = None, | |
| flatten_allof: Optional[bool] = True, | |
| ): | |
| if use_flags is None: | |
| use_flags = [] | |
| if self.flag.startswith("!"): | |
| test = self.flag[1:] not in use_flags | |
| else: | |
| test = self.flag in use_flags | |
| return ( | |
| super().reduce(use_flags, anyof_reduce, flatten_allof) | |
| if test | |
| else [] | |
| ) | |
| def __str__(self): | |
| return ( | |
| f"{self.flag}? ( " + " ".join(str(x) for x in self.children) + " )" | |
| ) | |
| def parse(data: str, check_token: Optional[Callable] = None) -> RootNode: | |
| """Parse a PMS dependency specification into a structure.""" | |
| root = RootNode() | |
| cur = root | |
| stack = [root] | |
| tokens = iter(data.split()) | |
| for token in tokens: | |
| if token == "||": | |
| if cur is AnyOfNode: | |
| # We could support this, but it doesn't make sense in general. | |
| # (A || (B || C)) is simply (A || B || C). | |
| raise NestedOrUnsupportedError( | |
| f'Redundant nested OR deps are not supported: "{data}"' | |
| ) | |
| node = AnyOfNode() | |
| cur.children.append(node) | |
| stack.append(node) | |
| cur = node | |
| token = next(tokens) | |
| if token != "(": | |
| raise PmsSyntaxError(f'Expected "(": "{data}"') | |
| elif token == "(": | |
| node = AllOfNode() | |
| cur.children.append(node) | |
| stack.append(node) | |
| cur = node | |
| elif token == ")": | |
| stack.pop() | |
| if not stack: | |
| raise PmsSyntaxError(f'Unexpected ")": "{data}"') | |
| cur = stack[-1] | |
| elif token.endswith("?"): | |
| # USE flag conditional. | |
| node = UseNode(token[:-1]) | |
| cur.children.append(node) | |
| stack.append(node) | |
| cur = node | |
| token = next(tokens) | |
| if token != "(": | |
| raise PmsSyntaxError(f'Expected "(": "{data}"') | |
| elif not check_token or check_token(token): | |
| cur.children.append(Node(token)) | |
| else: | |
| raise PmsNameError(f'Invalid token name: "{token}"') | |
| # Make sure all '(...)' nodes finished processing. | |
| if len(stack) != 1: | |
| raise PmsSyntaxError(f'Incomplete specification: "{data}"') | |
| return root |