diff --git a/buildstream/_variables.py b/buildstream/_variables.py index aa988c7e5a607880adfe5111372e851a971cdd92..04d5a99e0d951f3d27f1fa8f8d36919a3011e0b9 100644 --- a/buildstream/_variables.py +++ b/buildstream/_variables.py @@ -25,7 +25,7 @@ from . import _yaml # Variables are allowed to have dashes here # -_VARIABLE_MATCH = r'\%\{([a-zA-Z][a-zA-Z0-9_-]*)\}' +PARSE_EXPANSION = re.compile(r"\%\{([a-zA-Z][a-zA-Z0-9_-]*)\}") # The Variables helper object will resolve the variable references in @@ -46,7 +46,7 @@ class Variables(): def __init__(self, node): self.original = node - self.newexp = self._resolve_exp(node) + self.newexp = self._resolve(node) self._sanity_check() def get(self, var): @@ -69,130 +69,35 @@ class Variables(): # LoadError, if the string contains unresolved variable references. # def subst(self, string): -# real = self.real_subst(string) - exp = self.subst_exp(string) -# if real != exp: -# raise LoadError(LoadErrorReason.UNRESOLVED_VARIABLE, "Oh dear!") -# return real - return exp - - def real_subst(self, string): - substitute, unmatched, _ = self._subst(string, self.variables) - unmatched = list(set(unmatched)) - if unmatched: - if len(unmatched) == 1: - message = "Unresolved variable '{var}'".format(var=unmatched[0]) - else: - message = "Unresolved variables: " - for unmatch in unmatched: - if unmatched.index(unmatch) > 0: - message += ', ' - message += unmatch - - raise LoadError(LoadErrorReason.UNRESOLVED_VARIABLE, message) - - return substitute - - def _subst(self, string, variables): - - def subst_callback(match): - nonlocal variables - nonlocal unmatched - nonlocal matched - - token = match.group(0) - varname = match.group(1) + exp = _parse_expstr(string) - value = _yaml.node_get(variables, str, varname, default_value=None) - if value is not None: - # We have to check if the inner string has variables - # and return unmatches for those - unmatched += re.findall(_VARIABLE_MATCH, value) - matched += [varname] - else: - # Return unmodified token - unmatched += [varname] - value = token + try: + return _expand_expstr(self.newexp, exp) + except KeyError: + unmatched = [] - return value + for v in exp[1][1::2]: + if v not in self.newexp: + unmatched.append(v) - matched = [] - unmatched = [] - replacement = re.sub(_VARIABLE_MATCH, subst_callback, string) + if unmatched: + if len(unmatched) == 1: + message = "Unresolved variable '{var}'".format(var=unmatched[0]) + else: + message = "Unresolved variables: " + for unmatch in unmatched: + if unmatched.index(unmatch) > 0: + message += ', ' + message += unmatch - return (replacement, unmatched, matched) + raise LoadError(LoadErrorReason.UNRESOLVED_VARIABLE, message) + raise # Variable resolving code # - # Here we substitute variables for values (resolve variables) repeatedly - # in a dictionary, each time creating a new dictionary until there is no - # more unresolved variables to resolve, or, until resolving further no - # longer resolves anything, in which case we throw an exception. + # Here we resolve all of our inputs into a dictionary, ready for use + # in subst() def _resolve(self, node): - variables = node - - # Special case, if notparallel is specified in the variables for this - # element, then override max-jobs to be 1. - # Initialize it as a string as all variables are processed as strings. - # - if _yaml.node_get(variables, bool, 'notparallel', default_value=False): - variables['max-jobs'] = str(1) - - # Resolve the dictionary once, reporting the new dictionary with things - # substituted in it, and reporting unmatched tokens. - # - def resolve_one(variables): - unmatched = [] - resolved = {} - - for key, value in _yaml.node_items(variables): - - # Ensure stringness of the value before substitution - value = _yaml.node_get(variables, str, key) - - resolved_var, item_unmatched, matched = self._subst(value, variables) - - if _wrap_variable(key) in resolved_var: - referenced_through = find_recursive_variable(key, matched, variables) - raise LoadError(LoadErrorReason.RECURSIVE_VARIABLE, - "{}: ".format(_yaml.node_get_provenance(variables, key)) + - ("Variable '{}' expands to contain a reference to itself. " + - "Perhaps '{}' contains '{}").format(key, referenced_through, _wrap_variable(key))) - - resolved[key] = resolved_var - unmatched += item_unmatched - - # Carry over provenance - resolved[_yaml.PROVENANCE_KEY] = variables[_yaml.PROVENANCE_KEY] - return (resolved, unmatched) - - # Resolve it until it's resolved or broken - # - resolved = variables - unmatched = ['dummy'] - last_unmatched = ['dummy'] - while unmatched: - resolved, unmatched = resolve_one(resolved) - - # Lists of strings can be compared like this - if unmatched == last_unmatched: - # We've got the same result twice without matching everything, - # something is undeclared or cyclic, compose a summary. - # - summary = '' - for unmatch in set(unmatched): - for var, provenance in self._find_references(unmatch): - line = " unresolved variable '{unmatched}' in declaration of '{variable}' at: {provenance}\n" - summary += line.format(unmatched=unmatch, variable=var, provenance=provenance) - - raise LoadError(LoadErrorReason.UNRESOLVED_VARIABLE, - "Failed to resolve one or more variable:\n{}".format(summary)) - - last_unmatched = unmatched - - return resolved - - def _resolve_exp(self, node): # Special case, if notparallel is specified in the variables for this # element, then override max-jobs to be 1. # Initialize it as a string as all variables are processed as strings. @@ -206,13 +111,17 @@ class Variables(): ret[sys.intern(key)] = _parse_expstr(value) return ret + # Sanity checks on the resolved variables dictionary + # + # Here we check for loops and for missing inputs which might cause the + # resolved dictionary of inputs to not be sufficient. def _sanity_check(self): - # Ensure that every variable we could expand here has its targets resolved + # First the check for anything unresolvable summary = [] wants = {} for k, es in self.newexp.items(): wants[k] = wants.get(k, set()) - for var in _exp_deps(es): + for var in es[1][1::2]: wants[k].add(var) if var not in self.newexp: line = " unresolved variable '{unmatched}' in declaration of '{variable}' at: {provenance}" @@ -222,6 +131,7 @@ class Variables(): raise LoadError(LoadErrorReason.UNRESOLVED_VARIABLE, "Failed to resolve one or more variable:\n{}\n".format("\n".join(summary))) + # And now the cycle checks def cycle_check(exp, visited, cleared): for var in _exp_deps(exp): if var in cleared: @@ -230,116 +140,67 @@ class Variables(): raise LoadError(LoadErrorReason.RECURSIVE_VARIABLE, "{}: ".format(_yaml.node_get_provenance(self.original, var)) + ("Variable '{}' expands to contain a reference to itself. " + - "Perhaps '{}' contains '{}").format(var, visited[-1], _wrap_variable(var))) + "Perhaps '{}' contains '%{{{}}}").format(var, visited[-1], var)) visited.append(var) cycle_check(self.newexp[var], visited, cleared) visited.pop() cleared.add(var) cleared = set() - for k,es in self.newexp.items(): + for k, es in self.newexp.items(): if k not in cleared: cycle_check(es, [k], cleared) + # resolved(): + # + # Perform any substitutions necessary and return a dictionary of all + # inputs fully resolved. + # + # Returns: + # (dict): A dictionary mapping variable name to resolved content + # def resolved(self): + # Make a fresh dict of the resolved expansions ret = {} for k, es in self.newexp.items(): ret[k] = _expand_expstr(self.newexp, es) return ret - - def subst_exp(self, string): - exp = _parse_expstr(string) - - try: - return _expand_expstr(self.newexp, exp) - except KeyError: - unmatched = [] - for v in _exp_deps(exp): - if v not in self.newexp: - unmatched.append(v) - - if unmatched: - if len(unmatched) == 1: - message = "Unresolved variable '{var}'".format(var=unmatched[0]) - else: - message = "Unresolved variables: " - for unmatch in unmatched: - if unmatched.index(unmatch) > 0: - message += ', ' - message += unmatch - - raise LoadError(LoadErrorReason.UNRESOLVED_VARIABLE, message) - raise - - # Helper function to fetch information about the node referring to a variable - # - def _find_references(self, varname): - fullname = _wrap_variable(varname) - for key, value in _yaml.node_items(self.original): - if fullname in value: - provenance = _yaml.node_get_provenance(self.original, key) - yield (key, provenance) - - -def find_recursive_variable(variable, matched_variables, all_vars): - matched_values = (_yaml.node_get(all_vars, str, key) for key in matched_variables) - for key, value in zip(matched_variables, matched_values): - if _wrap_variable(variable) in value: - return key - # We failed to find a recursive variable - return None - - -def _wrap_variable(var): - return "%{" + var + "}" - -## Stuff for new style expansion strings - -PARSE_EXPANSION = re.compile(r"\%\{([a-zA-Z][a-zA-Z0-9_-]*)\}") - -def __parse_expstr(s): - spl = PARSE_EXPANSION.split(s) - if spl[-1] == '': - spl = spl[:-1] - return (len(spl), [sys.intern(s) for s in spl]) - -def _exp_deps(es): - for i, s in enumerate(es[1]): - if i % 2 == 1: - yield s +# Cache for the parsed expansion strings. While this is nominally +# something which might "waste" memory, in reality each of these +# will live as long as the element which uses it, which is the +# vast majority of the memory usage across the execution of BuildStream. PARSE_CACHE = {} -def _parse_expstr(s): + +# Helper to parse a string into an expansion string tuple, caching +# the results so that future parse requests don't need to think about +# the string +def _parse_expstr(instr): try: - return PARSE_CACHE[s] + return PARSE_CACHE[instr] except KeyError: - PARSE_CACHE[s] = __parse_expstr(s) - return PARSE_CACHE[s] + spl = PARSE_EXPANSION.split(instr) + if spl[-1] == '': + spl = spl[:-1] + PARSE_CACHE[instr] = (len(spl), [sys.intern(s) for s in spl]) + return PARSE_CACHE[instr] -#def _expand_expstr(content, value): -# ret = [] -# expstack = [(value,0)] -# while expstack: -# ((elen,expanded), idx) = expstack.pop() -# ret.append(expanded[idx]) -# idx += 1 -# if idx < elen: -# if elen > idx+1: -# expstack.append(((elen,expanded), idx+1)) -# expstack.append((content[expanded[idx]], 0)) -# return "".join(ret) -def __expand(content, value): - (elen, bits) = value - idx = 0 - while idx < elen: - yield bits[idx] - idx += 1 - if idx < elen: - yield from __expand(content, content[bits[idx]]) - idx += 1 - -def _expand_expstr(content, value): - return "".join(__expand(content, value)) +# Helper to expand a given top level expansion string tuple in the context +# of the given dictionary of expansion strings. +# +# Note: Will raise KeyError if any expansion is missing +def _expand_expstr(content, topvalue): + def __expand(value): + (elen, bits) = value + idx = 0 + while idx < elen: + yield bits[idx] + idx += 1 + if idx < elen: + yield from __expand(content[bits[idx]]) + idx += 1 + + return "".join(__expand(topvalue))