# # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # """Configuration source parser.""" import logging import pathlib from dataclasses import dataclass from typing import Iterable, Any, Optional, List from mbed_tools.lib.json_helpers import decode_json_file from mbed_tools.build.exceptions import InvalidConfigOverride from mbed_tools.lib.python_helpers import flatten_nested logger = logging.getLogger(__name__) def from_file( config_source_file_path: pathlib.Path, target_filters: Iterable[str], default_name: Optional[str] = None ) -> dict: """Load a JSON config file and prepare the contents as a config source.""" return prepare(decode_json_file(config_source_file_path), source_name=default_name, target_filters=target_filters) def prepare( input_data: dict, source_name: Optional[str] = None, target_filters: Optional[Iterable[str]] = None ) -> dict: """Prepare a config source for entry into the Config object. Extracts config and override settings from the source. Flattens these nested dictionaries out into lists of objects which are namespaced in the way the Mbed config system expects. Args: input_data: The raw config JSON object parsed from the config file. source_name: Optional default name to use for namespacing config settings. If the input_data contains a 'name' field, that field is used as the namespace. target_filters: List of filter string used when extracting data from target_overrides section of the config data. Returns: Prepared config source. """ data = input_data.copy() namespace = data.pop("name", source_name) for key in data: data[key] = _sanitise_value(data[key]) if "config" in data: data["config"] = _extract_config_settings(namespace, data["config"]) if "overrides" in data: data["overrides"] = _extract_overrides(namespace, data["overrides"]) if "target_overrides" in data: data["overrides"] = _extract_target_overrides( namespace, data.pop("target_overrides"), target_filters if target_filters is not None else [] ) return data @dataclass class ConfigSetting: """Representation of a config setting. Auto converts any list values to sets for faster operations and de-duplication of values. """ namespace: str name: str value: Any help_text: Optional[str] = None macro_name: Optional[str] = None def __post_init__(self) -> None: """Convert the value to a set if applicable.""" self.value = _sanitise_value(self.value) @dataclass class Override: """Representation of a config override. Checks for _add or _remove modifiers and splits them from the name. """ namespace: str name: str value: Any modifier: Optional[str] = None def __post_init__(self) -> None: """Parse modifiers and convert list values to sets.""" if self.name.endswith("_add") or self.name.endswith("_remove"): self.name, self.modifier = self.name.rsplit("_", maxsplit=1) self.value = _sanitise_value(self.value) def _extract_config_settings(namespace: str, config_data: dict) -> List[ConfigSetting]: settings = [] for name, item in config_data.items(): logger.debug("Extracting config setting from '%s': '%s'='%s'", namespace, name, item) if isinstance(item, dict): macro_name = item.get("macro_name") help_text = item.get("help") value = item.get("value") else: macro_name = None help_text = None value = item setting = ConfigSetting( namespace=namespace, name=name, macro_name=macro_name, help_text=help_text, value=value, ) # If the config item is about a certain component or feature # being present, avoid adding it to the mbed_config.cmake # configuration file. Instead, applications should depend on # the feature or component with target_link_libraries() and the # component's CMake file (in the Mbed OS repo) will create # any necessary macros or definitions. if setting.name == "present": continue settings.append(setting) return settings def _extract_target_overrides( namespace: str, override_data: dict, allowed_target_labels: Iterable[str] ) -> List[Override]: valid_target_data = dict() for target_type in override_data: if target_type == "*" or target_type in allowed_target_labels: valid_target_data.update(override_data[target_type]) return _extract_overrides(namespace, valid_target_data) def _extract_overrides(namespace: str, override_data: dict) -> List[Override]: overrides = [] for name, value in override_data.items(): try: override_namespace, override_name = name.split(".") if override_namespace and override_namespace not in [namespace, "target"] and namespace != "app": raise InvalidConfigOverride( "It is only possible to override config settings defined in an mbed_lib.json from mbed_app.json. " f"An override was defined by the lib `{namespace}` that attempts to override " f"`{override_namespace}.{override_name}`." ) except ValueError: override_namespace = namespace override_name = name overrides.append(Override(namespace=override_namespace, name=override_name, value=value)) return overrides def _sanitise_value(val: Any) -> Any: """Convert list values to sets and return scalar values and strings unchanged. For whatever reason, we allowed config settings to have values of any type available in the JSON spec. The value type can be a list, nested list, str, int, you name it. When we process the config, we want to use sets instead of lists, this is for two reasons: * To take advantage of set operations when we deal with "cumulative" settings. * To prevent any duplicate settings ending up in the final config. """ if isinstance(val, list): return set(flatten_nested(val)) return val