diff --git a/scripts/mbedtls_framework/config_history.py b/scripts/mbedtls_framework/config_history.py index b00dcb9db..baee92606 100644 --- a/scripts/mbedtls_framework/config_history.py +++ b/scripts/mbedtls_framework/config_history.py @@ -1,4 +1,6 @@ """Historical information about the library configuration. + +Note: this module is deprecated. Use config_macros.py instead. """ ## Copyright The Mbed TLS Contributors diff --git a/scripts/mbedtls_framework/config_macros.py b/scripts/mbedtls_framework/config_macros.py new file mode 100644 index 000000000..a30fb8af0 --- /dev/null +++ b/scripts/mbedtls_framework/config_macros.py @@ -0,0 +1,160 @@ +"""Information about configuration macros and derived macros.""" + +## Copyright The Mbed TLS Contributors +## SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later + +import glob +import os +import re +from typing import FrozenSet, Iterable, Iterator + +from . import build_tree + + +class ConfigMacros: + """Information about configuration macros and derived macros.""" + + def __init__(self, public: FrozenSet[str], adjusted: FrozenSet[str]) -> None: + self._public = public + self._internal = adjusted - public + + def options(self) -> FrozenSet[str]: + """The set of configuration options in this product.""" + return self._public + + def internal(self) -> FrozenSet[str]: + """The set of internal option-like macros in this product.""" + return self._internal + + @staticmethod + def _load_file(filename: str) -> FrozenSet[str]: + """Load macro names from the given file.""" + with open(filename, encoding='ascii') as input_: + return frozenset(line.strip() + for line in input_) + + +class Current(ConfigMacros): + """Information about config-like macros parsed from the source code.""" + + _SHADOW_FILE = 'scripts/data_files/config-options-current.txt' + + _PUBLIC_CONFIG_HEADERS = [ + 'include/mbedtls/mbedtls_config.h', + 'include/psa/crypto_config.h', + ] + + _ADJUST_CONFIG_HEADERS = [ + 'include/**/*adjust*.h', + 'drivers/*/include/**/*adjust*.h', + ] + + _DEFINE_RE = re.compile(r'[/ ]*# *define *([A-Z_a-z][0-9A-Z_a-z]*)') + + def _list_files(self, patterns: Iterable[str]) -> Iterator[str]: + """Yield files matching the given glob patterns.""" + for pattern in patterns: + yield from glob.glob(os.path.join(self._root, self._submodule, + pattern), + recursive=True) + + def _search_file(self, filename: str) -> Iterator[str]: + """Yield macros defined in the given file.""" + with open(filename, encoding='utf-8') as input_: + for line in input_: + m = self._DEFINE_RE.match(line) + if m: + yield m.group(1) + + def _search_files(self, patterns: Iterable[str]) -> FrozenSet[str]: + """Yield macros defined in files matching the given glob patterns.""" + return frozenset(element + for filename in self._list_files(patterns) + for element in self._search_file(filename)) + + def shadow_file_path(self) -> str: + """The path to the option list shadow file.""" + return os.path.join(self._root, self._submodule, self._SHADOW_FILE) + + def __init__(self, submodule: str = '', + shadow_missing_ok: bool = False) -> None: + """Look for macros defined in the given submodule's source tree. + + If submodule is omitted or empty, look in the root module. + + If shadow_missing_ok is true, treat a missing shadow file as + if it was empty. This is intended for use only when regenerating + the shadow file. + """ + self._root = build_tree.guess_project_root() + self._submodule = submodule + shadow_file = self.shadow_file_path() + try: + public = self._load_file(shadow_file) + except FileNotFoundError: + if not shadow_missing_ok: + raise + public = frozenset() + adjusted = self._search_files(self._ADJUST_CONFIG_HEADERS) + super().__init__(public, adjusted) + + def live_config_options(self) -> FrozenSet[str]: + """Return config options from the config file (as opposed to the shadow file).""" + return self._search_files(self._PUBLIC_CONFIG_HEADERS) + + def is_shadow_file_up_to_date(self) -> bool: + """Whether the config options shadow file is up to date.""" + live = self.live_config_options() + return live == self._public + + def compare_shadow_file(self) -> str: + """Compare the option list shadow file with the live config file. + + Return a string containing the names that are only found in one of + them, in a diff-like format: a line prefixed by ``+`` if the name + is missing from the shadow file, or by ``-`` if the name is only + in the shadow file. + """ + live = self.live_config_options() + diff = [] + for x in sorted(live | self._public): + if x not in live: + diff.append('+' + x + '\n') + elif x not in self._public: + diff.append('-' + x + '\n') + return ''.join(diff) + + def update_shadow_file(self, always_update: bool) -> None: + """Update the shadow file from the live config file. + + If always_update is false and the shadow file already has the desired + content, don't touch it. + """ + if not always_update and self.is_shadow_file_up_to_date(): + return + with open(self.shadow_file_path(), 'w') as out: + for name in sorted(self.live_config_options()): + out.write(name + '\n') + + + +class History(ConfigMacros): + """Information about config-like macros in a previous version. + + Load files created by ``framework/scripts/save_config_history.sh``. + """ + + def _load_history_file(self, basename: str) -> FrozenSet[str]: + """Load macro names from the given file in the history directory.""" + filename = os.path.join(self._history_dir, basename) + return self._load_file(filename) + + def __init__(self, project: str, version: str) -> None: + """Read information about the given project at the given version. + + The information must be present in history files in the framework. + """ + self._history_dir = os.path.join(build_tree.framework_root(), 'history') + public = self._load_history_file(f'config-options-{project}-{version}.txt') + adjusted = self._load_history_file(f'config-adjust-{project}-{version}.txt') + super().__init__(public, adjusted)