summaryrefslogtreecommitdiff
path: root/buildstream/_options/optionpool.py
diff options
context:
space:
mode:
Diffstat (limited to 'buildstream/_options/optionpool.py')
-rw-r--r--buildstream/_options/optionpool.py225
1 files changed, 225 insertions, 0 deletions
diff --git a/buildstream/_options/optionpool.py b/buildstream/_options/optionpool.py
new file mode 100644
index 000000000..3686ee6e3
--- /dev/null
+++ b/buildstream/_options/optionpool.py
@@ -0,0 +1,225 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2017 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 ast
+import jinja2
+from collections import Mapping
+
+from .. import _yaml
+from .. import LoadError, LoadErrorReason
+from .optionbool import OptionBool
+from .optionenum import OptionEnum
+from .optionflags import OptionFlags
+from .optioneltmask import OptionEltMask
+from .optionarch import OptionArch
+
+
+OPTION_TYPES = {
+ OptionBool.OPTION_TYPE: OptionBool,
+ OptionEnum.OPTION_TYPE: OptionEnum,
+ OptionFlags.OPTION_TYPE: OptionFlags,
+ OptionEltMask.OPTION_TYPE: OptionEltMask,
+ OptionArch.OPTION_TYPE: OptionArch,
+}
+
+
+class OptionPool():
+
+ def __init__(self, element_path):
+ self.options = {} # The Options
+ self.variables = None # The Options resolved into typed variables
+
+ # We hold on to the element path for the sake of OptionEltMask
+ self.element_path = element_path
+
+ # jinja2 environment, with default globals cleared out of the way
+ self.environment = jinja2.Environment()
+ self.environment.globals = []
+
+ # load()
+ #
+ # Loads the options described in the project.conf
+ #
+ # Args:
+ # node (dict): The loaded YAML options
+ #
+ def load(self, options):
+
+ for option_name, option_definition in _yaml.node_items(options):
+ opt_type_name = _yaml.node_get(option_definition, str, 'type')
+
+ try:
+ opt_type = OPTION_TYPES[opt_type_name]
+ except KeyError:
+ p = _yaml.node_get_provenance(option_definition, 'type')
+ raise LoadError(LoadErrorReason.INVALID_DATA,
+ "{}: Invalid option type '{}'".format(p, opt_type_name))
+
+ option = opt_type(option_name, option_definition, self)
+ self.options[option_name] = option
+
+ # load_values()
+ #
+ # Loads the option values specified in a key/value
+ # dictionary loaded from YAML, and a list of tuples
+ # collected from the command line
+ #
+ # Args:
+ # node (dict): The loaded YAML options
+ # cli_options (list): A list of (str, str) tuples
+ #
+ def load_values(self, node, cli_options):
+ for option_name, _ in _yaml.node_items(node):
+ try:
+ option = self.options[option_name]
+ except KeyError as e:
+ p = _yaml.node_get_provenance(node, option_name)
+ raise LoadError(LoadErrorReason.INVALID_DATA,
+ "{}: Unknown option '{}' specified".format(p, option_name))
+ option.load_value(node)
+
+ for option_name, option_value in cli_options:
+ try:
+ option = self.options[option_name]
+ except KeyError as e:
+ raise LoadError(LoadErrorReason.INVALID_DATA,
+ "Unknown option '{}' specified on the command line".format(option_name))
+ option.set_value(option_value)
+
+ # resolve()
+ #
+ # Resolves the loaded options, this is just a step which must be
+ # performed after loading all options and their values, and before
+ # ever trying to evaluate an expression
+ #
+ def resolve(self):
+ self.variables = {}
+ for option_name, option in self.options.items():
+ # Delegate one more method for options to
+ # do some last minute validation once any
+ # overrides have been performed.
+ #
+ option.resolve()
+
+ self.variables[option_name] = option.value
+
+ # evaluate()
+ #
+ # Evaluates a jinja2 style expression with the loaded options in context.
+ #
+ # Args:
+ # expression (str): The jinja2 style expression
+ #
+ # Returns:
+ # (bool): Whether the expression resolved to a truthy value or a falsy one.
+ #
+ # Raises:
+ # LoadError: If the expression failed to resolve for any reason
+ #
+ def evaluate(self, expression):
+
+ #
+ # Variables must be resolved at this point.
+ #
+ try:
+ template_string = "{%% if %s %%} True {%% else %%} False {%% endif %%}" % expression
+ template = self.environment.from_string(template_string)
+ context = template.new_context(self.variables, shared=True)
+ result = template.root_render_func(context)
+ evaluated = jinja2.utils.concat(result)
+ val = evaluated.strip()
+
+ if val == "True":
+ return True
+ elif val == "False":
+ return False
+ else: # pragma: nocover
+ raise LoadError(LoadErrorReason.EXPRESSION_FAILED,
+ "Failed to evaluate expression: {}".format(expression))
+ except jinja2.exceptions.TemplateError as e:
+ raise LoadError(LoadErrorReason.EXPRESSION_FAILED,
+ "Failed to evaluate expression ({}): {}".format(expression, e))
+
+ # process_node()
+ #
+ # Args:
+ # node (Mapping): A YAML Loaded dictionary
+ #
+ def process_node(self, node):
+
+ # A conditional will result in composition, which can
+ # in turn add new conditionals to the root.
+ #
+ # Keep processing conditionals on the root node until
+ # all directly nested conditionals are resolved.
+ #
+ while self.process_conditional(node):
+ pass
+
+ # Now recurse into nested dictionaries and lists
+ # and process any indirectly nested conditionals.
+ #
+ for key, value in _yaml.node_items(node):
+ if isinstance(value, Mapping):
+ self.process_node(value)
+ elif isinstance(value, list):
+ self.process_list(value)
+
+ # Recursion assistent for lists, in case there
+ # are lists of lists.
+ #
+ def process_list(self, values):
+ for value in values:
+ if isinstance(value, Mapping):
+ self.process_node(value)
+ elif isinstance(value, list):
+ self.process_list(value)
+
+ # Process a single conditional, resulting in composition
+ # at the root level on the passed node
+ #
+ # Return true if a conditional was processed.
+ #
+ def process_conditional(self, node):
+ conditions = _yaml.node_get(node, list, '(?)', default_value=[]) or None
+
+ if conditions is not None:
+
+ # Collect provenance first, we need to delete the (?) key
+ # before any composition occurs.
+ provenance = [
+ _yaml.node_get_provenance(node, '(?)', indices=[i])
+ for i in range(len(conditions))
+ ]
+ del node['(?)']
+
+ for condition, p in zip(conditions, provenance):
+ tuples = list(_yaml.node_items(condition))
+ if len(tuples) > 1:
+ raise LoadError(LoadErrorReason.INVALID_DATA,
+ "{}: Conditional statement has more than one key".format(p))
+
+ expression, value = tuples[0]
+ if self.evaluate(expression):
+ _yaml.composite_dict(node, value)
+
+ return True
+
+ return False