summaryrefslogtreecommitdiff
path: root/buildstream/_variables.py
diff options
context:
space:
mode:
authorTristan Van Berkom <tristan.vanberkom@codethink.co.uk>2017-01-05 15:45:49 -0500
committerTristan Van Berkom <tristan.vanberkom@codethink.co.uk>2017-01-06 17:18:49 -0500
commiteb8af73ef50244c4d3a0968a15250d53ac6f3a41 (patch)
treec277abb4da5c0717b512b533909b2e19db1ab343 /buildstream/_variables.py
parentc340c4a1dc1107cd4acff594fe36bf810cabb31f (diff)
downloadbuildstream-eb8af73ef50244c4d3a0968a15250d53ac6f3a41.tar.gz
_variables.py: A new helper object for variable contexts
A helper object for resolving a loaded and composited dictionary of variables so that variable values themselves are expanded and errors properly detected. Object provides a subst() method allowing strings to be substituted with the variable context.
Diffstat (limited to 'buildstream/_variables.py')
-rw-r--r--buildstream/_variables.py170
1 files changed, 170 insertions, 0 deletions
diff --git a/buildstream/_variables.py b/buildstream/_variables.py
new file mode 100644
index 000000000..f325db714
--- /dev/null
+++ b/buildstream/_variables.py
@@ -0,0 +1,170 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2016 Codethink Limited
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+#
+# Authors:
+# Tristan Van Berkom <tristan.vanberkom@codethink.co.uk>
+
+import re
+
+from . import LoadError, LoadErrorReason
+from . import _yaml
+
+# Variables are allowed to have dashes here
+#
+VARIABLE_MATCH = r'\%\{([a-zA-Z][a-zA-Z0-9_-]*)\}'
+
+
+# The Variables helper object will resolve the variable references in
+# the given dictionary, expecting that any dictionary values which contain
+# variable references can be resolved from the same dictionary.
+#
+# Each Element creates it's own Variables instance to track the configured
+# variable settings for the element.
+#
+# Args:
+# node (dict): A node loaded and composited with yaml tools
+#
+# Raises:
+# LoadError, if unresolved variables occur.
+#
+class Variables():
+
+ def __init__(self, node):
+
+ self.original = node
+ self.variables = self.resolve(node)
+
+ # subst():
+ #
+ # Substitutes any variables in 'string' and returns the result.
+ #
+ # Args:
+ # (string): The string to substitute
+ #
+ # Returns:
+ # (string): The new string with any substitutions made
+ #
+ # Raises:
+ # LoadError, if the string contains unresolved variable references.
+ #
+ def subst(self, string):
+ substitute, unmatched = self.subst_internal(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_internal(self, string, variables):
+
+ def subst_callback(match):
+ nonlocal variables
+ nonlocal unmatched
+
+ token = match.group(0)
+ varname = match.group(1)
+
+ value = _yaml.node_get(variables, str, varname)
+ 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)
+ else:
+ # Return unmodified token
+ unmatched += [varname]
+ value = token
+
+ return value
+
+ unmatched = []
+ replacement = re.sub(VARIABLE_MATCH, subst_callback, string)
+
+ return (replacement, unmatched)
+
+ # 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.
+ def resolve(self, node):
+ variables = node
+
+ # 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 variables.items():
+ if key == _yaml.PROVENANCE_KEY:
+ continue
+
+ resolved_var, item_unmatched = self.subst_internal(value, variables)
+ 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 len(unmatched) > 0:
+ 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%s" % summary)
+
+ last_unmatched = unmatched
+
+ return resolved
+
+ # Helper function to fetch information about the node referring to a variable
+ #
+ def find_references(self, varname):
+ fullname = '%{' + varname + '}'
+ for key, value in self.original.items():
+ if key == _yaml.PROVENANCE_KEY:
+ continue
+
+ if fullname in value:
+ provenance = _yaml.node_get_provenance(self.original, key)
+ yield (key, provenance)