mirror of https://github.com/ARMmbed/mbed-os.git
176 lines
6.2 KiB
Python
176 lines
6.2 KiB
Python
#
|
|
# 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
|