diff --git a/scripts/make_generated_files.py b/scripts/make_generated_files.py index 1ca0f2da7..cd60b087f 100755 --- a/scripts/make_generated_files.py +++ b/scripts/make_generated_files.py @@ -10,6 +10,7 @@ Generate the TF-PSA-Crypto generated files """ import argparse import filecmp +import os import shutil import subprocess import sys @@ -66,6 +67,18 @@ def get_generation_script_files(generation_script: str): return files +COMMON_GENERATION_SCRIPTS = [ +] + +# Once the script has been added to both Mbed TLS and TF-PSA-Crypto, +# we can include this unconditionally. +# https://github.com/Mbed-TLS/mbedtls/issues/10305 +if os.path.exists("scripts/generate_config_checks.py"): + COMMON_GENERATION_SCRIPTS.append(GenerationScript( + Path("scripts/generate_config_checks.py"), + get_generation_script_files("scripts/generate_config_checks.py"), + "", None)) + if build_tree.looks_like_tf_psa_crypto_root("."): TF_PSA_CRYPTO_GENERATION_SCRIPTS = [ GenerationScript( @@ -244,6 +257,7 @@ def main(): generation_scripts = MBEDTLS_GENERATION_SCRIPTS else: raise Exception("No support for Mbed TLS 3.6") + generation_scripts += COMMON_GENERATION_SCRIPTS if args.list: files = get_generated_files(generation_scripts) diff --git a/scripts/mbedtls_framework/config_checks_generator.py b/scripts/mbedtls_framework/config_checks_generator.py new file mode 100644 index 000000000..edca2f730 --- /dev/null +++ b/scripts/mbedtls_framework/config_checks_generator.py @@ -0,0 +1,228 @@ +"""Generate C preprocessor code to check for bad configurations. + +The headers are meant to be included in a specific way in PROJECT_config.c. +See framework/docs/architecture/config-check-framework.md for information. +""" + +## Copyright The Mbed TLS Contributors +## SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later + +import argparse +import enum +import os +import re +import sys +import textwrap +import typing +from typing import Iterator, List + +from . import build_tree +from . import typing_util + + +class Position(enum.Enum): + BEFORE = 0 # Before build_info.h + USER = 1 # Just after reading PROJECT_CONFIG_FILE (*config.h) and PROJECT_USER_CONFIG_FILE + FINAL = 2 # After *adjust*.h and the rest of build_info.h + + +class Checker: + """Description of checks for one option. + + By default, this class triggers an error if the option is set after + reading the user configuration. To change the behavior, override + the methods `before`, `user` and `final` as needed. + """ + + def __init__(self, name: str, suggestion: str = '') -> None: + """Construct a checker for the given preprocessor macro name. + + If suggestion is given, it is appended to the error message. + It should be a short sentence intended for human readers. + This sentence follows a sentence like " is not + a valid configuration option". + """ + self.name = name + self.suggestion = suggestion + + def _basic_message(self) -> str: + """The first sentence of the message to display on error. + + It should end with a full stop or other sentence-ending punctuation. + """ + return f'{self.name} is not a valid configuration option.' + + def message(self) -> str: + """The message to display on error.""" + message = self._basic_message() + if self.suggestion: + message += ' Suggestion: ' + self.suggestion + return message + + def _quoted_message(self) -> str: + """Quote message() in double quotes. Useful for #error directives.""" + return ('"' + + str.replace(str.replace(self.message(), + '\\', '\\\\'), + '"', '\\"') + + '"') + + def before(self, _prefix: str) -> str: + """C code to inject before including the config.""" + #pylint: disable=no-self-use + # Derived classes will add content where needed. + return '' + + def user(self, _prefix: str) -> str: + """C code to inject immediately after including the user config.""" + return f''' + #if defined({self.name}) + # error {self._quoted_message()} + #endif + ''' + + def final(self, _prefix: str) -> str: + """C code to inject after finalizing the config.""" + #pylint: disable=no-self-use + # Derived classes will add content where needed. + return '' + + def code(self, position: Position, prefix: str) -> str: + """C code to inject at the given position. + + Use the given prefix for auxiliary macro names. + """ + methods = { + Position.BEFORE: self.before, + Position.USER: self.user, + Position.FINAL: self.final, + } + method = methods[position] + snippet = method(prefix) + return textwrap.dedent(snippet) + + +class Internal(Checker): + """Checker for an internal-only option.""" + + +class Removed(Checker): + """Checker for an option that has been removed.""" + + def __init__(self, name: str, version: str, suggestion: str = '') -> None: + super().__init__(name, suggestion) + self.version = version + + def _basic_message(self) -> str: + """The first sentence of the message to display on error. + + It should end with a full stop or other sentence-ending punctuation. + """ + return f'{self.name} was removed in {self.version}.' + + def user(self, prefix: str) -> str: + """C code to inject immediately after including the user config.""" + # A removed option is forbidden, just like an internal option. + # But since we're checking a macro that is not defined anywhere, + # we need to tell check_names.py that this is a false positive. + code = super().user(prefix) + return re.sub(rf'^ *# *\w+.*\b{self.name}\b.*$', + lambda m: m.group(0) + ' //no-check-names', + code, flags=re.M) + + +class BranchData(typing.NamedTuple): + """The relevant aspects of the configuration on a branch.""" + + # Subdirectory where the generated headers will be located. + header_directory: str + + # Prefix used for the generated headers' basename. + header_prefix: str + + # Prefix used for C preprocessor macros. + project_cpp_prefix: str + + # Options to check + checkers: List[Checker] + + +class HeaderGenerator: + """Generate a header to include before or after the user config.""" + + def __init__(self, branch_data: BranchData, position: Position) -> None: + self.branch_data = branch_data + self.position = position + self.prefix = branch_data.project_cpp_prefix + '_CONFIG_CHECK' + self.bypass_checks = self.prefix + '_BYPASS' + + def write_stanza(self, out: typing_util.Writable, checker: Checker) -> None: + """Write the part of the output corresponding to one config option.""" + code = checker.code(self.position, self.prefix) + out.write(code) + + def write_content(self, out: typing_util.Writable) -> None: + """Write the output for all config options to be processed.""" + for checker in self.branch_data.checkers: + self.write_stanza(out, checker) + + def output_file_name(self) -> str: + """The base name of the output file.""" + return ''.join([self.branch_data.header_prefix, + 'config_check_', + self.position.name.lower(), + '.h']) + + def write(self, directory: str) -> None: + """Write the whole output file.""" + basename = self.output_file_name() + with open(os.path.join(directory, basename), 'w') as out: + out.write(f'''\ +/* {basename} (generated part of {self.branch_data.header_prefix}config.c). */ +/* Automatically generated by {os.path.basename(sys.argv[0])}. Do not edit! */ + +#if !defined({self.bypass_checks}) //no-check-names + +/* *INDENT-OFF* */ +''') + self.write_content(out) + out.write(f''' +/* *INDENT-ON* */ + +#endif /* !defined({self.bypass_checks}) */ //no-check-names + +/* End of automatically generated {basename} */ +''') + + +def generate_header_files(branch_data: BranchData, + directory: str, + list_only: bool = False) -> Iterator[str]: + """Generate the header files to include before and after *config.h.""" + for position in Position: + generator = HeaderGenerator(branch_data, position) + yield os.path.join(directory, generator.output_file_name()) + if not list_only: + generator.write(directory) + + +def main(branch_data: BranchData) -> None: + root = build_tree.guess_project_root() + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--list', action='store_true', + help='List generated files and exit') + parser.add_argument('--list-for-cmake', action='store_true', + help='List generated files in CMake-friendly format and exit') + parser.add_argument('output_directory', metavar='DIR', nargs='?', + default=os.path.join(root, branch_data.header_directory), + help='output file location (default: %(default)s)') + options = parser.parse_args() + list_only = options.list or options.list_for_cmake + output_files = generate_header_files(branch_data, + options.output_directory, + list_only=list_only) + if options.list_for_cmake: + sys.stdout.write(';'.join(output_files)) + elif options.list: + for filename in output_files: + print(filename)