blob: fc589bee9155159cdbdbcb59fb1558f2fa4953c5 [file] [log] [blame]
# 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