mirror of
https://github.com/Mbed-TLS/mbedtls-framework.git
synced 2026-08-28 22:09:54 +00:00
Merge pull request #196 from gilles-peskine-arm/config-checks-generator-framework
Introduce config_checks_generator.py
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 "<macro_name> 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)
|
||||
Reference in New Issue
Block a user