Clone of official tools

Revision:
8:a8ac6ed29081
Child:
10:2511036308b8
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/config.py	Tue Jun 07 11:35:02 2016 +0100
@@ -0,0 +1,325 @@
+"""
+mbed SDK
+Copyright (c) 2016 ARM Limited
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+"""
+
+# Implementation of mbed configuration mechanism
+from copy import deepcopy
+from collections import OrderedDict
+from tools.utils import json_file_to_dict, ToolException
+from tools.targets import Target
+import os
+
+# Base class for all configuration exceptions
+class ConfigException(Exception):
+    pass
+
+# This class keeps information about a single configuration parameter
+class ConfigParameter:
+    # name: the name of the configuration parameter
+    # data: the data associated with the configuration parameter
+    # unit_name: the unit (target/library/application) that defines this parameter
+    # unit_ kind: the kind of the unit ("target", "library" or "application")
+    def __init__(self, name, data, unit_name, unit_kind):
+        self.name = self.get_full_name(name, unit_name, unit_kind, allow_prefix = False)
+        self.defined_by = self.get_display_name(unit_name, unit_kind)
+        self.set_by = self.defined_by
+        self.help_text = data.get("help", None)
+        self.value = data.get("value", None)
+        self.required = data.get("required", False)
+        self.macro_name = data.get("macro_name", "MBED_CONF_%s" % self.sanitize(self.name.upper()))
+
+    # Return the full (prefixed) name of a parameter.
+    # If the parameter already has a prefix, check if it is valid
+    # name: the simple (unqualified) name of the parameter
+    # unit_name: the unit (target/library/application) that defines this parameter
+    # unit_kind: the kind of the unit ("target", "library" or "application")
+    # label: the name of the label in the 'target_config_overrides' section (optional)
+    # allow_prefix: True to allo the original name to have a prefix, False otherwise
+    @staticmethod
+    def get_full_name(name, unit_name, unit_kind, label = None, allow_prefix = True):
+        if name.find('.') == -1: # the name is not prefixed
+            if unit_kind == "target":
+                prefix = "target."
+            elif unit_kind == "application":
+                prefix = "app."
+            else:
+                prefix = unit_name + '.'
+            return prefix + name
+        # The name has a prefix, so check if it is valid
+        if not allow_prefix:
+            raise ConfigException("Invalid parameter name '%s' in '%s'" % (name, ConfigParameter.get_display_name(unit_name, unit_kind, label)))
+        temp = name.split(".")
+        # Check if the parameter syntax is correct (must be unit_name.parameter_name)
+        if len(temp) != 2:
+            raise ConfigException("Invalid parameter name '%s' in '%s'" % (name, ConfigParameter.get_display_name(unit_name, unit_kind, label)))
+        prefix = temp[0]
+        # Check if the given parameter prefix matches the expected prefix
+        if (unit_kind == "library" and prefix != unit_name) or (unit_kind == "target" and prefix != "target"):
+            raise ConfigException("Invalid prefix '%s' for parameter name '%s' in '%s'" % (prefix, name, ConfigParameter.get_display_name(unit_name, unit_kind, label)))
+        return name
+
+    # Return the name displayed for a unit when interogating the origin
+    # and the last set place of a parameter
+    # unit_name: the unit (target/library/application) that defines this parameter
+    # unit_kind: the kind of the unit ("target", "library" or "application")
+    # label: the name of the label in the 'target_config_overrides' section (optional)
+    @staticmethod
+    def get_display_name(unit_name, unit_kind, label = None):
+        if unit_kind == "target":
+            return "target:" + unit_name
+        elif unit_kind == "application":
+            return "application%s" % ("[%s]" % label if label else "")
+        else: # library
+            return "library:%s%s" % (unit_name, "[%s]" % label if label else "")
+
+    # "Sanitize" a name so that it is a valid C macro name
+    # Currently it simply replaces '.' and '-' with '_'
+    # name: the un-sanitized name.
+    @staticmethod
+    def sanitize(name):
+        return name.replace('.', '_').replace('-', '_')
+
+    # Sets a value for this parameter, remember the place where it was set
+    # value: the value of the parameter
+    # unit_name: the unit (target/library/application) that defines this parameter
+    # unit_ kind: the kind of the unit ("target", "library" or "application")
+    # label: the name of the label in the 'target_config_overrides' section (optional)
+    def set_value(self, value, unit_name, unit_kind, label = None):
+        self.value = value
+        self.set_by = self.get_display_name(unit_name, unit_kind, label)
+
+    # Return the string representation of this configuration parameter
+    def __str__(self):
+        if self.value is not None:
+            return '%s = %s (macro name: "%s")' % (self.name, self.value, self.macro_name)
+        else:
+            return '%s has no value' % self.name
+
+    # Return a verbose description of this configuration paramater as a string
+    def get_verbose_description(self):
+        desc = "Name: %s%s\n" % (self.name, " (required parameter)" if self.required else "")
+        if self.help_text:
+            desc += "    Description: %s\n" % self.help_text
+        desc += "    Defined by: %s\n" % self.defined_by
+        if not self.value:
+            return desc + "    No value set"
+        desc += "    Macro name: %s\n" % self.macro_name
+        desc += "    Value: %s (set by %s)" % (self.value, self.set_by)
+        return desc
+
+# A representation of a configuration macro. It handles both macros without a value (MACRO)
+# and with a value (MACRO=VALUE)
+class ConfigMacro:
+    def __init__(self, name, unit_name, unit_kind):
+        self.name = name
+        self.defined_by = ConfigParameter.get_display_name(unit_name, unit_kind)
+        if name.find("=") != -1:
+            tmp = name.split("=")
+            if len(tmp) != 2:
+                raise ValueError("Invalid macro definition '%s' in '%s'" % (name, self.defined_by))
+            self.macro_name = tmp[0]
+        else:
+            self.macro_name = name
+
+# 'Config' implements the mbed configuration mechanism
+class Config:
+    # Libraries and applications have different names for their configuration files
+    __mbed_app_config_name = "mbed_app.json"
+    __mbed_lib_config_name = "mbed_lib.json"
+
+    # Allowed keys in configuration dictionaries
+    # (targets can have any kind of keys, so this validation is not applicable to them)
+    __allowed_keys = {
+        "library": set(["name", "config", "target_overrides", "macros", "__config_path"]),
+        "application": set(["config", "custom_targets", "target_overrides", "macros", "__config_path"])
+    }
+
+    # The initialization arguments for Config are:
+    #     target: the name of the mbed target used for this configuration instance
+    #     top_level_dirs: a list of top level source directories (where mbed_abb_config.json could be found)
+    # __init__ will look for the application configuration file in top_level_dirs.
+    # If found once, it'll parse it and check if it has a custom_targets function.
+    # If it does, it'll update the list of targets if need.
+    # If found more than once, an exception is raised
+    # top_level_dirs can be None (in this case, mbed_app_config.json will not be searched)
+    def __init__(self, target, top_level_dirs = []):
+        app_config_location = None
+        for s in (top_level_dirs or []):
+            full_path = os.path.join(s, self.__mbed_app_config_name)
+            if os.path.isfile(full_path):
+                if app_config_location is not None:
+                    raise ConfigException("Duplicate '%s' file in '%s' and '%s'" % (self.__mbed_app_config_name, app_config_location, full_path))
+                else:
+                    app_config_location = full_path
+        self.app_config_data = json_file_to_dict(app_config_location) if app_config_location else {}
+        # Check the keys in the application configuration data
+        unknown_keys = set(self.app_config_data.keys()) - self.__allowed_keys["application"]
+        if unknown_keys:
+            raise ConfigException("Unknown key(s) '%s' in %s" % (",".join(unknown_keys), self.__mbed_app_config_name))
+        # Update the list of targets with the ones defined in the application config, if applicable
+        Target.add_py_targets(self.app_config_data.get("custom_targets", {}))
+        self.lib_config_data = {}
+        # Make sure that each config is processed only once
+        self.processed_configs = {}
+        self.target = target if isinstance(target, str) else target.name
+        self.target_labels = Target.get_target(self.target).get_labels()
+
+    # Add one or more configuration files
+    def add_config_files(self, flist):
+        for f in flist:
+            if not f.endswith(self.__mbed_lib_config_name):
+                continue
+            full_path = os.path.normpath(os.path.abspath(f))
+            # Check that we didn't already process this file
+            if self.processed_configs.has_key(full_path):
+                continue
+            self.processed_configs[full_path] = True
+            # Read the library configuration and add a "__full_config_path" attribute to it
+            cfg = json_file_to_dict(f)
+            cfg["__config_path"] = full_path
+            # If there's already a configuration for a module with the same name, exit with error
+            if self.lib_config_data.has_key(cfg["name"]):
+                raise ConfigException("Library name '%s' is not unique (defined in '%s' and '%s')" % (cfg["name"], full_path, self.lib_config_data[cfg["name"]]["__config_path"]))
+            self.lib_config_data[cfg["name"]] = cfg
+
+    # Helper function: process a "config_parameters" section in either a target, a library or the application
+    # data: a dictionary with the configuration parameters
+    # params: storage for the discovered configuration parameters
+    # unit_name: the unit (target/library/application) that defines this parameter
+    # unit_kind: the kind of the unit ("target", "library" or "application")
+    def _process_config_parameters(self, data, params, unit_name, unit_kind):
+        for name, v in data.items():
+            full_name = ConfigParameter.get_full_name(name, unit_name, unit_kind)
+            # If the parameter was already defined, raise an error
+            if full_name in params:
+                raise ConfigException("Parameter name '%s' defined in both '%s' and '%s'" % (name, ConfigParameter.get_display_name(unit_name, unit_kind), params[full_name].defined_by))
+            # Otherwise add it to the list of known parameters
+            # If "v" is not a dictionary, this is a shortcut definition, otherwise it is a full definition
+            params[full_name] = ConfigParameter(name, v if isinstance(v, dict) else {"value": v}, unit_name, unit_kind)
+        return params
+
+    # Helper function: process "config_parameters" and "target_config_overrides" in a given dictionary
+    # data: the configuration data of the library/appliation
+    # params: storage for the discovered configuration parameters
+    # unit_name: the unit (library/application) that defines this parameter
+    # unit_kind: the kind of the unit ("library" or "application")
+    def _process_config_and_overrides(self, data, params, unit_name, unit_kind):
+        self._process_config_parameters(data.get("config", {}), params, unit_name, unit_kind)
+        for label, overrides in data.get("target_overrides", {}).items():
+            # If the label is defined by the target or it has the special value "*", process the overrides
+            if (label == '*') or (label in self.target_labels):
+                for name, v in overrides.items():
+                    # Get the full name of the parameter
+                    full_name = ConfigParameter.get_full_name(name, unit_name, unit_kind, label)
+                    # If an attempt is made to override a parameter that isn't defined, raise an error
+                    if not full_name in params:
+                        raise ConfigException("Attempt to override undefined parameter '%s' in '%s'" % (full_name, ConfigParameter.get_display_name(unit_name, unit_kind, label)))
+                    params[full_name].set_value(v, unit_name, unit_kind, label)
+        return params
+
+    # Read and interpret configuration data defined by targets
+    def get_target_config_data(self):
+        # We consider the resolution order for our target and sort it by level reversed,
+        # so that we first look at the top level target (the parent), then its direct children,
+        # then the children's children and so on, until we reach self.target
+        # TODO: this might not work so well in some multiple inheritance scenarios
+        # At each step, look at two keys of the target data:
+        #   - config_parameters: used to define new configuration parameters
+        #   - config_overrides: used to override already defined configuration parameters
+        params, json_data = {}, Target.get_json_target_data()
+        resolution_order = [e[0] for e in sorted(Target.get_target(self.target).resolution_order, key = lambda e: e[1], reverse = True)]
+        for tname in resolution_order:
+            # Read the target data directly from its description
+            t = json_data[tname]
+            # Process definitions first
+            self._process_config_parameters(t.get("config", {}), params, tname, "target")
+            # Then process overrides
+            for name, v in t.get("overrides", {}).items():
+                full_name = ConfigParameter.get_full_name(name, tname, "target")
+                # If the parameter name is not defined or if there isn't a path from this target to the target where the
+                # parameter was defined in the target inheritance tree, raise an error
+                # We need to use 'defined_by[7:]' to remove the "target:" prefix from defined_by
+                if (not full_name in params) or (not params[full_name].defined_by[7:] in Target.get_target(tname).resolution_order_names):
+                    raise ConfigException("Attempt to override undefined parameter '%s' in '%s'" % (name, ConfigParameter.get_display_name(tname, "target")))
+                # Otherwise update the value of the parameter
+                params[full_name].set_value(v, tname, "target")
+        return params
+
+    # Helper function: process a macro definition, checking for incompatible duplicate definitions
+    # mlist: list of macro names to process
+    # macros: dictionary with currently discovered macros
+    # unit_name: the unit (library/application) that defines this macro
+    # unit_kind: the kind of the unit ("library" or "application")
+    def _process_macros(self, mlist, macros, unit_name, unit_kind):
+        for mname in mlist:
+            m = ConfigMacro(mname, unit_name, unit_kind)
+            if (m.macro_name in macros) and (macros[m.macro_name].name != mname):
+                # Found an incompatible definition of the macro in another module, so raise an error
+                full_unit_name = ConfigParameter.get_display_name(unit_name, unit_kind)
+                raise ConfigException("Macro '%s' defined in both '%s' and '%s' with incompatible values" % (m.macro_name, macros[m.macro_name].defined_by, full_unit_name))
+            macros[m.macro_name] = m
+
+    # Read and interpret configuration data defined by libs
+    # It is assumed that "add_config_files" above was already called and the library configuration data
+    # exists in self.lib_config_data
+    def get_lib_config_data(self):
+        all_params, macros = {}, {}
+        for lib_name, lib_data in self.lib_config_data.items():
+            unknown_keys = set(lib_data.keys()) - self.__allowed_keys["library"]
+            if unknown_keys:
+                raise ConfigException("Unknown key(s) '%s' in %s" % (",".join(unknown_keys), lib_name))
+            all_params.update(self._process_config_and_overrides(lib_data, {}, lib_name, "library"))
+            self._process_macros(lib_data.get("macros", []), macros, lib_name, "library")
+        return all_params, macros
+
+    # Read and interpret the configuration data defined by the target
+    # The target can override any configuration parameter, as well as define its own configuration data
+    # params: the dictionary with configuration parameters found so far (in the target and in libraries)
+    # macros: the list of macros defined in the configuration
+    def get_app_config_data(self, params, macros):
+        app_cfg = self.app_config_data
+        # The application can have a "config_parameters" and a "target_config_overrides" section just like a library
+        self._process_config_and_overrides(app_cfg, params, "app", "application")
+        # The application can also defined macros
+        self._process_macros(app_cfg.get("macros", []), macros, "app", "application")
+
+    # Return the configuration data in two parts:
+    #   - params: a dictionary with (name, ConfigParam) entries
+    #   - macros: the list of macros defined with "macros" in libraries and in the application
+    def get_config_data(self):
+        all_params = self.get_target_config_data()
+        lib_params, macros = self.get_lib_config_data()
+        all_params.update(lib_params)
+        self.get_app_config_data(all_params, macros)
+        return all_params, [m.name for m in macros.values()]
+
+    # Helper: verify if there are any required parameters without a value in 'params'
+    def _check_required_parameters(self, params):
+        for p in params.values():
+            if p.required and (p.value is None):
+                raise ConfigException("Required parameter '%s' defined by '%s' doesn't have a value" % (p.name, p.defined_by))
+
+    # Return the macro definitions generated for a dictionary of configuration parameters
+    # params: a dictionary of (name, ConfigParameters instance) mappings
+    @staticmethod
+    def parameters_to_macros(params):
+        return ['%s=%s' % (m.macro_name, m.value) for m in params.values() if m.value is not None]
+
+    # Return the configuration data converted to a list of C macros
+    def get_config_data_macros(self):
+        params, macros = self.get_config_data()
+        self._check_required_parameters(params)
+        return macros + self.parameters_to_macros(params)