mirror of
https://github.com/Mbed-TLS/mbedtls-framework.git
synced 2026-08-06 03:07:47 +00:00
Generate config check headers
Infrastructure to generate config check headers, to be included in
`${PROJECT}_config.c`.
This commit creates a module with the core logic, and support for three
generated files, to be included before the user config, just after the user
config, and after finalizing the user config.
This commit provides a basic `Checker` class. Future commits will add
derived classes for common cases such as options set in the wrong project.
This module is meant to be used in an executable script in each project,
containing the project-specific names to use and the list of options.
Signed-off-by: Gilles Peskine <[email protected]>
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
"""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 sys
|
||||
import textwrap
|
||||
import typing
|
||||
from typing import 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."""
|
||||
#pylint: disable=no-self-use
|
||||
# Derived classes will add content where needed.
|
||||
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}.'
|
||||
|
||||
|
||||
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 to 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})
|
||||
|
||||
/* *INDENT-OFF* */
|
||||
''')
|
||||
self.write_content(out)
|
||||
out.write(f'''
|
||||
/* *INDENT-ON* */
|
||||
|
||||
#endif /* !defined({self.bypass_checks}) */
|
||||
|
||||
/* End of automatically generated {basename} */
|
||||
''')
|
||||
|
||||
|
||||
def generate_header_files(branch_data: BranchData,
|
||||
directory: str,
|
||||
list_only: bool = False) -> None:
|
||||
"""Generate the header files to include before and after *config.h."""
|
||||
for position in Position:
|
||||
generator = HeaderGenerator(branch_data, position)
|
||||
if list_only:
|
||||
print(os.path.join(directory, generator.output_file_name()))
|
||||
else:
|
||||
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('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()
|
||||
generate_header_files(branch_data,
|
||||
options.output_directory,
|
||||
list_only=options.list)
|
||||
Reference in New Issue
Block a user