mirror of
https://github.com/Mbed-TLS/mbedtls-framework.git
synced 2026-10-05 16:27:23 +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 argparse
|
||||||
import filecmp
|
import filecmp
|
||||||
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@@ -66,6 +67,18 @@ def get_generation_script_files(generation_script: str):
|
|||||||
|
|
||||||
return files
|
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("."):
|
if build_tree.looks_like_tf_psa_crypto_root("."):
|
||||||
TF_PSA_CRYPTO_GENERATION_SCRIPTS = [
|
TF_PSA_CRYPTO_GENERATION_SCRIPTS = [
|
||||||
GenerationScript(
|
GenerationScript(
|
||||||
@@ -244,6 +257,7 @@ def main():
|
|||||||
generation_scripts = MBEDTLS_GENERATION_SCRIPTS
|
generation_scripts = MBEDTLS_GENERATION_SCRIPTS
|
||||||
else:
|
else:
|
||||||
raise Exception("No support for Mbed TLS 3.6")
|
raise Exception("No support for Mbed TLS 3.6")
|
||||||
|
generation_scripts += COMMON_GENERATION_SCRIPTS
|
||||||
|
|
||||||
if args.list:
|
if args.list:
|
||||||
files = get_generated_files(generation_scripts)
|
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