|
| 1 | +"""Generate C preprocessor code to check for bad configurations. |
| 2 | +
|
| 3 | +The headers are meant to be included in a specific way in PROJECT_config.c. |
| 4 | +See framework/docs/architecture/config-check-framework.md for information. |
| 5 | +""" |
| 6 | + |
| 7 | +## Copyright The Mbed TLS Contributors |
| 8 | +## SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later |
| 9 | + |
| 10 | +import argparse |
| 11 | +import enum |
| 12 | +import os |
| 13 | +import re |
| 14 | +import sys |
| 15 | +import textwrap |
| 16 | +import typing |
| 17 | +from typing import Iterator, List |
| 18 | + |
| 19 | +from . import build_tree |
| 20 | +from . import typing_util |
| 21 | + |
| 22 | + |
| 23 | +class Position(enum.Enum): |
| 24 | + BEFORE = 0 # Before build_info.h |
| 25 | + USER = 1 # Just after reading PROJECT_CONFIG_FILE (*config.h) and PROJECT_USER_CONFIG_FILE |
| 26 | + FINAL = 2 # After *adjust*.h and the rest of build_info.h |
| 27 | + |
| 28 | + |
| 29 | +class Checker: |
| 30 | + """Description of checks for one option. |
| 31 | +
|
| 32 | + By default, this class triggers an error if the option is set after |
| 33 | + reading the user configuration. To change the behavior, override |
| 34 | + the methods `before`, `user` and `final` as needed. |
| 35 | + """ |
| 36 | + |
| 37 | + def __init__(self, name: str, suggestion: str = '') -> None: |
| 38 | + """Construct a checker for the given preprocessor macro name. |
| 39 | +
|
| 40 | + If suggestion is given, it is appended to the error message. |
| 41 | + It should be a short sentence intended for human readers. |
| 42 | + This sentence follows a sentence like "<macro_name> is not |
| 43 | + a valid configuration option". |
| 44 | + """ |
| 45 | + self.name = name |
| 46 | + self.suggestion = suggestion |
| 47 | + |
| 48 | + def _basic_message(self) -> str: |
| 49 | + """The first sentence of the message to display on error. |
| 50 | +
|
| 51 | + It should end with a full stop or other sentence-ending punctuation. |
| 52 | + """ |
| 53 | + return f'{self.name} is not a valid configuration option.' |
| 54 | + |
| 55 | + def message(self) -> str: |
| 56 | + """The message to display on error.""" |
| 57 | + message = self._basic_message() |
| 58 | + if self.suggestion: |
| 59 | + message += ' Suggestion: ' + self.suggestion |
| 60 | + return message |
| 61 | + |
| 62 | + def _quoted_message(self) -> str: |
| 63 | + """Quote message() in double quotes. Useful for #error directives.""" |
| 64 | + return ('"' + |
| 65 | + str.replace(str.replace(self.message(), |
| 66 | + '\\', '\\\\'), |
| 67 | + '"', '\\"') + |
| 68 | + '"') |
| 69 | + |
| 70 | + def before(self, _prefix: str) -> str: |
| 71 | + """C code to inject before including the config.""" |
| 72 | + #pylint: disable=no-self-use |
| 73 | + # Derived classes will add content where needed. |
| 74 | + return '' |
| 75 | + |
| 76 | + def user(self, _prefix: str) -> str: |
| 77 | + """C code to inject immediately after including the user config.""" |
| 78 | + return f''' |
| 79 | + #if defined({self.name}) |
| 80 | + # error {self._quoted_message()} |
| 81 | + #endif |
| 82 | + ''' |
| 83 | + |
| 84 | + def final(self, _prefix: str) -> str: |
| 85 | + """C code to inject after finalizing the config.""" |
| 86 | + #pylint: disable=no-self-use |
| 87 | + # Derived classes will add content where needed. |
| 88 | + return '' |
| 89 | + |
| 90 | + def code(self, position: Position, prefix: str) -> str: |
| 91 | + """C code to inject at the given position. |
| 92 | +
|
| 93 | + Use the given prefix for auxiliary macro names. |
| 94 | + """ |
| 95 | + methods = { |
| 96 | + Position.BEFORE: self.before, |
| 97 | + Position.USER: self.user, |
| 98 | + Position.FINAL: self.final, |
| 99 | + } |
| 100 | + method = methods[position] |
| 101 | + snippet = method(prefix) |
| 102 | + return textwrap.dedent(snippet) |
| 103 | + |
| 104 | + |
| 105 | +class Internal(Checker): |
| 106 | + """Checker for an internal-only option.""" |
| 107 | + |
| 108 | + |
| 109 | +class Removed(Checker): |
| 110 | + """Checker for an option that has been removed.""" |
| 111 | + |
| 112 | + def __init__(self, name: str, version: str, suggestion: str = '') -> None: |
| 113 | + super().__init__(name, suggestion) |
| 114 | + self.version = version |
| 115 | + |
| 116 | + def _basic_message(self) -> str: |
| 117 | + """The first sentence of the message to display on error. |
| 118 | +
|
| 119 | + It should end with a full stop or other sentence-ending punctuation. |
| 120 | + """ |
| 121 | + return f'{self.name} was removed in {self.version}.' |
| 122 | + |
| 123 | + def user(self, prefix: str) -> str: |
| 124 | + """C code to inject immediately after including the user config.""" |
| 125 | + # A removed option is forbidden, just like an internal option. |
| 126 | + # But since we're checking a macro that is not defined anywhere, |
| 127 | + # we need to tell check_names.py that this is a false positive. |
| 128 | + code = super().user(prefix) |
| 129 | + return re.sub(rf'^ *# *\w+.*\b{self.name}\b.*$', |
| 130 | + lambda m: m.group(0) + ' //no-check-names', |
| 131 | + code, flags=re.M) |
| 132 | + |
| 133 | + |
| 134 | +class BranchData(typing.NamedTuple): |
| 135 | + """The relevant aspects of the configuration on a branch.""" |
| 136 | + |
| 137 | + # Subdirectory where the generated headers will be located. |
| 138 | + header_directory: str |
| 139 | + |
| 140 | + # Prefix used for the generated headers' basename. |
| 141 | + header_prefix: str |
| 142 | + |
| 143 | + # Prefix used for C preprocessor macros. |
| 144 | + project_cpp_prefix: str |
| 145 | + |
| 146 | + # Options to check |
| 147 | + checkers: List[Checker] |
| 148 | + |
| 149 | + |
| 150 | +class HeaderGenerator: |
| 151 | + """Generate a header to include before or after the user config.""" |
| 152 | + |
| 153 | + def __init__(self, branch_data: BranchData, position: Position) -> None: |
| 154 | + self.branch_data = branch_data |
| 155 | + self.position = position |
| 156 | + self.prefix = branch_data.project_cpp_prefix + '_CONFIG_CHECK' |
| 157 | + self.bypass_checks = self.prefix + '_BYPASS' |
| 158 | + |
| 159 | + def write_stanza(self, out: typing_util.Writable, checker: Checker) -> None: |
| 160 | + """Write the part of the output corresponding to one config option.""" |
| 161 | + code = checker.code(self.position, self.prefix) |
| 162 | + out.write(code) |
| 163 | + |
| 164 | + def write_content(self, out: typing_util.Writable) -> None: |
| 165 | + """Write the output for all config options to be processed.""" |
| 166 | + for checker in self.branch_data.checkers: |
| 167 | + self.write_stanza(out, checker) |
| 168 | + |
| 169 | + def output_file_name(self) -> str: |
| 170 | + """The base name of the output file.""" |
| 171 | + return ''.join([self.branch_data.header_prefix, |
| 172 | + 'config_check_', |
| 173 | + self.position.name.lower(), |
| 174 | + '.h']) |
| 175 | + |
| 176 | + def write(self, directory: str) -> None: |
| 177 | + """Write the whole output file.""" |
| 178 | + basename = self.output_file_name() |
| 179 | + with open(os.path.join(directory, basename), 'w') as out: |
| 180 | + out.write(f'''\ |
| 181 | +/* {basename} (generated part of {self.branch_data.header_prefix}config.c). */ |
| 182 | +/* Automatically generated by {os.path.basename(sys.argv[0])}. Do not edit! */ |
| 183 | +
|
| 184 | +#if !defined({self.bypass_checks}) //no-check-names |
| 185 | +
|
| 186 | +/* *INDENT-OFF* */ |
| 187 | +''') |
| 188 | + self.write_content(out) |
| 189 | + out.write(f''' |
| 190 | +/* *INDENT-ON* */ |
| 191 | +
|
| 192 | +#endif /* !defined({self.bypass_checks}) */ //no-check-names |
| 193 | +
|
| 194 | +/* End of automatically generated {basename} */ |
| 195 | +''') |
| 196 | + |
| 197 | + |
| 198 | +def generate_header_files(branch_data: BranchData, |
| 199 | + directory: str, |
| 200 | + list_only: bool = False) -> Iterator[str]: |
| 201 | + """Generate the header files to include before and after *config.h.""" |
| 202 | + for position in Position: |
| 203 | + generator = HeaderGenerator(branch_data, position) |
| 204 | + yield os.path.join(directory, generator.output_file_name()) |
| 205 | + if not list_only: |
| 206 | + generator.write(directory) |
| 207 | + |
| 208 | + |
| 209 | +def main(branch_data: BranchData) -> None: |
| 210 | + root = build_tree.guess_project_root() |
| 211 | + parser = argparse.ArgumentParser(description=__doc__) |
| 212 | + parser.add_argument('--list', action='store_true', |
| 213 | + help='List generated files and exit') |
| 214 | + parser.add_argument('--list-for-cmake', action='store_true', |
| 215 | + help='List generated files in CMake-friendly format and exit') |
| 216 | + parser.add_argument('output_directory', metavar='DIR', nargs='?', |
| 217 | + default=os.path.join(root, branch_data.header_directory), |
| 218 | + help='output file location (default: %(default)s)') |
| 219 | + options = parser.parse_args() |
| 220 | + list_only = options.list or options.list_for_cmake |
| 221 | + output_files = generate_header_files(branch_data, |
| 222 | + options.output_directory, |
| 223 | + list_only=list_only) |
| 224 | + if options.list_for_cmake: |
| 225 | + sys.stdout.write(';'.join(output_files)) |
| 226 | + elif options.list: |
| 227 | + for filename in output_files: |
| 228 | + print(filename) |
0 commit comments