ARMmbed/mbed-tools

View on GitHub
src/mbed_tools/build/_internal/config/assemble_build_config.py

Summary

Maintainability
A
0 mins
Test Coverage
#
# Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
"""Configuration assembly algorithm."""
import itertools

from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, List, Optional, Set

from mbed_tools.build._internal.config.config import Config
from mbed_tools.build._internal.config import source
from mbed_tools.build._internal.find_files import LabelFilter, RequiresFilter, filter_files, find_files


def assemble_config(target_attributes: dict, search_paths: Iterable[Path], mbed_app_file: Optional[Path]) -> Config:
    """Assemble config for given target and program directory.

    Mbed library and application specific config parameters are parsed from mbed_lib.json and mbed_app.json files
    located in the project source tree.
    The config files contain sets of "labels" which correspond to directory names in the mbed-os source tree. These
    labels are used to determine which mbed_lib.json files to include in the final configuration.

    The mbed_app.json config overrides must be applied after all the mbed_lib.json files have been parsed.
    Unfortunately, mbed_app.json may also contain filter labels to tell us which mbed libs we're depending on.
    This means we have to collect the filter labels from mbed_app.json before parsing any other config files.
    Then we parse all the required mbed_lib config and finally apply the app overrides.

    Args:
        target_attributes: Mapping of target specific config parameters.
        search_paths: Iterable of paths to search for mbed_lib.json files.
        mbed_app_file: The path to mbed_app.json. This can be None.
    """
    mbed_lib_files = list(
        set(
            itertools.chain.from_iterable(
                find_files("mbed_lib.json", path.absolute().resolve()) for path in search_paths
            )
        )
    )
    return _assemble_config_from_sources(target_attributes, mbed_lib_files, mbed_app_file)


def _assemble_config_from_sources(
    target_attributes: dict, mbed_lib_files: List[Path], mbed_app_file: Optional[Path] = None
) -> Config:
    config = Config(source.prepare(target_attributes, source_name="target"))
    previous_filter_data = None
    app_data = None
    if mbed_app_file:
        # We need to obtain the file filter data from mbed_app.json so we can select the correct set of mbed_lib.json
        # files to include in the config. We don't want to update the config object with all of the app settings yet
        # as we won't be able to apply overrides correctly until all relevant mbed_lib.json files have been parsed.
        app_data = source.from_file(
            mbed_app_file, default_name="app", target_filters=FileFilterData.from_config(config).labels
        )
        _get_app_filter_labels(app_data, config)

    current_filter_data = FileFilterData.from_config(config)
    while previous_filter_data != current_filter_data:
        filtered_files = _filter_files(mbed_lib_files, current_filter_data)
        for config_file in filtered_files:
            config.update(source.from_file(config_file, target_filters=current_filter_data.labels))
            # Remove any mbed_lib files we've already visited from the list so we don't parse them multiple times.
            mbed_lib_files.remove(config_file)

        previous_filter_data = current_filter_data
        current_filter_data = FileFilterData.from_config(config)

    # Apply mbed_app.json data last so config parameters are overriden in the correct order.
    if app_data:
        config.update(app_data)

    return config


def _get_app_filter_labels(mbed_app_data: dict, config: Config) -> None:
    requires = mbed_app_data.get("requires")
    if requires:
        config["requires"] = requires

    config.update(_get_file_filter_overrides(mbed_app_data))


def _get_file_filter_overrides(mbed_app_data: dict) -> dict:
    return {"overrides": [override for override in mbed_app_data.get("overrides", []) if override.modifier]}


@dataclass(frozen=True)
class FileFilterData:
    """Data used to navigate mbed-os directories for config files."""

    labels: Set[str]
    features: Set[str]
    components: Set[str]
    requires: Set[str]

    @classmethod
    def from_config(cls, config: Config) -> "FileFilterData":
        """Extract file filters from a Config object."""
        return cls(
            labels=config.get("labels", set()) | config.get("extra_labels", set()),
            features=set(config.get("features", set())),
            components=set(config.get("components", set())),
            requires=set(config.get("requires", set())),
        )


def _filter_files(files: Iterable[Path], filter_data: FileFilterData) -> Iterable[Path]:
    filters = (
        LabelFilter("TARGET", filter_data.labels),
        LabelFilter("FEATURE", filter_data.features),
        LabelFilter("COMPONENT", filter_data.components),
        RequiresFilter(filter_data.requires),
    )
    return filter_files(files, filters)