# Copyright 2019-2021 Portmod Authors
# Distributed under the terms of the GNU General Public License v3
import re
from typing import AbstractSet, Any, Callable, List, Optional, Type
from portmodlib.atom import Atom, InvalidAtom, useflag_re
from portmodlib.l10n import l10n
class UseParserError(Exception):
"""Error indicating a failure to parse a use-conditional string"""
[docs]def use_reduce(
depstr: str,
uselist: AbstractSet[str] = set(),
masklist: AbstractSet[str] = set(),
matchall: bool = False,
excludeall: AbstractSet[str] = set(),
is_src_uri: bool = False,
opconvert: bool = False,
flat: bool = False,
is_valid_flag: Optional[Callable[[str], bool]] = None,
token_class: Optional[Type] = None,
matchnone: bool = False,
) -> List:
"""
Takes a dep string and reduces the use? conditionals out, leaving an array
with subarrays. All redundant brackets are removed.
Adapted from portage's use_reduce
args:
depstr: depstring
uselist: List of use enabled flags
masklist: List of masked flags (always treated as disabled)
param matchall: Treat all conditionals as active. Used by inquisitor.
excludeall: List of flags for which negated conditionals are always treated
as inactive.
is_src_uri: Indicates if depstr represents a SRC_URI
opconvert: Put every operator as first element into it's argument list
flat: Create a flat list of all tokens
is_valid_flag: Function that decides if a given use flag might be used in
use conditionals
token_class: Convert all non operator tokens into this class
matchnone: Treat all conditionals as inactive. Used by digestgen().
returns:
The use reduced depend array
"""
if opconvert and flat:
raise ValueError(
"portage.dep.use_reduce: 'opconvert' and 'flat' are mutually exclusive"
)
if matchall and matchnone:
raise ValueError(
"portage.dep.use_reduce: 'matchall' and 'matchnone' are mutually exclusive"
)
def is_active(conditional):
"""
Decides if a given use conditional is active.
"""
if conditional.startswith("!"):
flag = conditional[1:-1]
is_negated = True
else:
flag = conditional[:-1]
is_negated = False
if is_valid_flag:
if not is_valid_flag(flag):
msg = (
"USE flag '{}' referenced in conditional '{}' "
"is not in IUSE".format(flag, conditional)
)
raise UseParserError(msg)
else:
if useflag_re.match(flag) is None:
raise UseParserError(
"invalid use flag '{}' in conditional '{}'".format(
flag, conditional
)
)
if is_negated and flag in excludeall:
return False
if flag in masklist:
return is_negated
if matchall:
return True
if matchnone:
return False
return (flag in uselist and not is_negated) or (
flag not in uselist and is_negated
)
def missing_white_space_check(token, pos):
"""
Used to generate good error messages for invalid tokens.
"""
for x in (")", "(", "||"):
if token.startswith(x) or token.endswith(x):
raise UseParserError(
"missing whitespace around '%s' at '%s', token %s"
% (x, token, pos + 1)
)
mysplit = depstr.split()
# Count the bracket level.
level = 0
# We parse into a stack.
# Every time we hit a '(', a new empty list is appended to the stack.
# When we hit a ')', the last list in the stack is merged with list one level up.
stack: List[List[Any]] = [[]]
# Set need_bracket to True after use conditionals or ||. Other tokens need to ensure
# that need_bracket is not True.
need_bracket = False
# Set need_simple_token to True after a SRC_URI arrow. Other tokens need to ensure
# that need_simple_token is not True.
need_simple_token = False
for pos, token in enumerate(mysplit):
if token == "(":
if need_simple_token:
raise UseParserError(
"expected: file name, got: '%s', token %s" % (token, pos + 1)
)
if len(mysplit) >= pos + 2 and mysplit[pos + 1] == ")":
raise UseParserError(
"expected: dependency string, got: ')', token %s" % (pos + 1,)
)
need_bracket = False
stack.append([])
level += 1
elif token == ")":
if need_bracket:
raise UseParserError(
"expected: '(', got: '%s', token %s" % (token, pos + 1)
)
if need_simple_token:
raise UseParserError(
"expected: file name, got: '%s', token %s" % (token, pos + 1)
)
if level > 0:
level -= 1
ll = stack.pop()
is_single = (
len(ll) == 1
or (opconvert and ll and ll[0] == "||")
or (not opconvert and len(ll) == 2 and ll[0] == "||")
)
ignore = False
if flat:
# In 'flat' mode, we simply merge all lists into a single large one.
if stack[level] and stack[level][-1][-1] == "?":
# The last token before the '(' that matches the current ')'
# was a use conditional. The conditional is removed in any case.
# Merge the current list if needed.
if is_active(stack[level][-1]):
stack[level].pop()
stack[level].extend(ll)
else:
stack[level].pop()
else:
stack[level].extend(ll)
continue
if stack[level] and isinstance(stack[level][-1], str):
if stack[level][-1] == "||" and not ll:
# Optimize: || ( ) -> .
ll.append((token_class or str)("empty-any-of"))
stack[level].pop()
elif stack[level][-1][-1] == "?":
# The last token before the '(' that matches the current ')'
# was a use conditional, remove it and decide if we
# have to keep the current list.
if not is_active(stack[level][-1]):
ignore = True
stack[level].pop()
def ends_in_any_of_dep(k):
return k >= 0 and stack[k] and stack[k][-1] == "||"
def last_any_of_operator_level(k):
# Returns the level of the last || operator if it is in effect for
# the current level. It is not in effect, if there is a level, that
# ends in a non-operator. This is almost equivalent to
# stack[level][-1]=="||", expect that it skips empty levels.
while k >= 0:
if stack[k] and isinstance(stack[k][-1], str):
if stack[k][-1] == "||":
return k
elif stack[k][-1][-1] != "?":
return -1
k -= 1
return -1
def special_append():
"""
Use extend instead of append if possible.
This kills all redundant brackets.
"""
if is_single:
# Either [A], [[...]] or [|| [...]]
if ll[0] == "||" and ends_in_any_of_dep(level - 1):
if opconvert:
stack[level].extend(ll[1:])
else:
stack[level].extend(ll[1])
elif len(ll) == 1 and isinstance(ll[0], list):
# l = [[...]]
last = last_any_of_operator_level(level - 1)
if last == -1:
if (
opconvert
and isinstance(ll[0], list)
and ll[0]
and ll[0][0] == "||"
):
stack[level].append(ll[0])
else:
stack[level].extend(ll[0])
else:
if opconvert and ll[0] and ll[0][0] == "||":
stack[level].extend(ll[0][1:])
else:
stack[level].append(ll[0])
else:
stack[level].extend(ll)
else:
if opconvert and stack[level] and stack[level][-1] == "||":
stack[level][-1] = ["||"] + ll
else:
stack[level].append(ll)
if ll and not ignore:
# The current list is not empty and we don't want to ignore it
# because of an inactive use conditional.
if not ends_in_any_of_dep(level - 1) and not ends_in_any_of_dep(
level
):
# Optimize: ( ( ... ) ) -> ( ... ).
# Make sure there is no '||' hanging around.
stack[level].extend(ll)
elif not stack[level]:
# An '||' in the level above forces us to keep to brackets.
special_append()
elif is_single and ends_in_any_of_dep(level):
# Optimize: || ( A ) -> A, || ( || ( ... ) ) -> || ( ... )
stack[level].pop()
special_append()
elif ends_in_any_of_dep(level) and ends_in_any_of_dep(level - 1):
# Optimize: || ( A || ( B C ) ) -> || ( A B C )
stack[level].pop()
stack[level].extend(ll)
else:
if opconvert and ends_in_any_of_dep(level):
# In opconvert mode, we have to move the operator from the
# level above into the current list.
stack[level].pop()
stack[level].append(["||"] + ll)
else:
special_append()
else:
raise UseParserError(
"no matching '%s' for '%s', token %s" % ("(", ")", pos + 1)
)
elif token == "||":
if is_src_uri:
raise UseParserError(
"any-of dependencies are not allowed in SRC_URI: token %s"
% (pos + 1,)
)
if need_bracket:
raise UseParserError(
"expected: '(', got: '%s', token %s" % (token, pos + 1)
)
need_bracket = True
stack[level].append(token)
elif token == "->":
if need_simple_token:
raise UseParserError(
"expected: file name, got: '%s', token %s" % (token, pos + 1)
)
if not is_src_uri:
raise UseParserError(
"SRC_URI arrow are only allowed in SRC_URI: token %s" % (pos + 1,)
)
need_simple_token = True
stack[level].append(token)
else:
if need_bracket:
raise UseParserError(
"expected: '(', got: '%s', token %s" % (token, pos + 1)
)
if need_simple_token and "/" in token:
# The last token was a SRC_URI arrow,
# make sure we have a simple file name.
raise UseParserError(
"expected: file name, got: '%s', token %s" % (token, pos + 1)
)
if token[-1] == "?":
need_bracket = True
else:
need_simple_token = False
if token_class and not is_src_uri:
# Add a hack for SRC_URI here, to avoid conditional code at the
# consumer level
try:
token = token_class(token)
except InvalidAtom as e:
missing_white_space_check(token, pos)
raise UseParserError(
"Invalid atom (%s), token %s" % (e, pos + 1)
)
except SystemExit:
raise
except UseParserError:
missing_white_space_check(token, pos)
raise UseParserError(
"Invalid token '%s', token %s" % (token, pos + 1)
)
if not matchall and isinstance(token, Atom):
token = token.evaluate_conditionals(uselist)
stack[level].append(token)
if level != 0:
raise UseParserError("Missing '%s' at end of string" % (")",))
if need_bracket:
raise UseParserError("Missing '%s' at end of string" % ("(",))
if need_simple_token:
raise UseParserError("Missing file name at end of string")
return stack[0]
def check_required_use(
required_use: str, use: AbstractSet[str], iuse_match: Callable[[str], bool]
) -> bool:
"""
Adapted from portage's check_required_use
Checks if the use flags listed in 'use' satisfy all
constraints specified in 'constraints'.
args:
required_use: REQUIRED_USE string
use: Enabled use flags
iuse_match: Callable that takes a single flag argument and returns
True if the flag is matched, false otherwise,
returns:
Indicates if REQUIRED_USE constraints are satisfied
"""
valid_operators = ("||", "^^", "??")
def is_active(token):
if token.startswith("!"):
flag = token[1:]
is_negated = True
else:
flag = token
is_negated = False
if not flag or not iuse_match(flag):
raise UseParserError("USE flag '%s' is not in IUSE" % (flag,))
return (flag in use and not is_negated) or (flag not in use and is_negated)
def is_satisfied(operator, argument):
if operator == "||":
return True in argument
elif operator == "^^":
return argument.count(True) == 1
elif operator == "??":
return argument.count(True) <= 1
elif operator[-1] == "?":
return False not in argument
mysplit = required_use.split()
level = 0
stack: List[List[str]] = [[]]
need_bracket = False
for token in mysplit:
if token == "(":
need_bracket = False
stack.append([])
level += 1
elif token == ")":
if need_bracket:
raise UseParserError("malformed syntax: '%s'" % required_use)
if level > 0:
level -= 1
ll = stack.pop()
op = None
if stack[level]:
if stack[level][-1] in valid_operators:
op = stack[level].pop()
satisfied = is_satisfied(op, ll)
stack[level].append(satisfied)
elif (
not isinstance(stack[level][-1], bool)
and stack[level][-1][-1] == "?"
):
op = stack[level].pop()
if is_active(op[:-1]):
satisfied = is_satisfied(op, ll)
stack[level].append(satisfied)
else:
continue
if op is None:
satisfied = False not in ll
if ll:
stack[level].append(satisfied)
else:
raise UseParserError("malformed syntax: '%s'" % required_use)
elif token in valid_operators:
if need_bracket:
raise UseParserError("malformed syntax: '%s'" % required_use)
need_bracket = True
stack[level].append(token)
else:
if need_bracket:
raise UseParserError("malformed syntax: '%s'" % required_use)
if token[-1] == "?":
need_bracket = True
stack[level].append(token)
else:
satisfied = is_active(token)
stack[level].append(satisfied)
if level != 0 or need_bracket:
raise UseParserError("malformed syntax: '%s'" % required_use)
return False not in stack[0]
def parse_usestr(usestr: str, token_class: Type = str):
"""
Adapted from portage's check_required_use
Converts usestring into structure containing lists of tokens,
possibly preceeded by an operator that can be one of || ?? ^^ or a use conditional
"""
valid_operators = ("||", "^^", "??")
mysplit = usestr.split()
level = 0
stack: List[List[Any]] = [[]]
need_bracket = False
for token in mysplit:
if token == "(":
need_bracket = False
stack.append([])
level += 1
elif token == ")":
if need_bracket:
raise UseParserError(f"malformed syntax: '{usestr}'")
if level > 0:
level -= 1
ll = stack.pop()
stack[level].append(ll)
else:
raise UseParserError(f"malformed syntax: '{usestr}'")
elif token in valid_operators:
if need_bracket:
raise UseParserError(f"malformed syntax: '{usestr}'")
need_bracket = True
stack[level].append(token)
else:
if need_bracket:
raise UseParserError(f"malformed syntax: '{usestr}'")
if token[-1] == "?":
need_bracket = True
stack[level].append(token)
else:
stack[level].append(token_class(token))
if level != 0 or need_bracket:
raise UseParserError(f"malformed syntax: '{usestr}'")
def opconvert(tokenlist):
index = 0
while index < len(tokenlist):
token = tokenlist[index]
if (
token in valid_operators
or isinstance(token, str)
and token.endswith("?")
):
tokenlist[index] = [token] + opconvert(tokenlist[index + 1])
del tokenlist[index + 1]
index += 1
return tokenlist
# Nested ||, ^^, and ?? groups can be combined
# use-conditionals cannot (tokens ending in ?)
def flatten(parent):
index = 0
while index < len(parent):
token = parent[index]
if isinstance(token, list):
token = flatten(token)
if token[0] in valid_operators and token[0] == parent[0]:
parent += token[1:]
del parent[index]
index -= 1
elif (
token[0] not in valid_operators
and parent[0] not in valid_operators
and not token[0].endswith("?")
):
parent += token
del parent[index]
index -= 1
index += 1
return parent
return flatten(opconvert(stack[0]))
def human_readable_required_use(required_use: List[Any]) -> str:
"""Takes a (recursive) list of tokens and turns it into a readable string"""
if isinstance(required_use[0], list):
string = "( " + human_readable_required_use(required_use[0]) + " )"
else:
string = required_use[0]
for token in required_use[1:]:
if isinstance(token, list):
string += " ( " + human_readable_required_use(token) + " )"
else:
string += " " + token
return re.sub(
r"\( (\!?\w+\?)", # Move use-conditionals in front of brackets
r"\1 (",
string.replace("( ^^", l10n("exactly-one-of") + " (")
.replace("( ||", l10n("any-of") + " (")
.replace("( ??", l10n("at-most-one-of") + " ("),
)