Skip to content

Commit e3c6f9d

Browse files
authored
Merge pull request #196 from gilles-peskine-arm/config-checks-generator-framework
Introduce config_checks_generator.py
2 parents 820a16c + 7ca97be commit e3c6f9d

File tree

2 files changed

+242
-0
lines changed

2 files changed

+242
-0
lines changed

scripts/make_generated_files.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"""
1111
import argparse
1212
import filecmp
13+
import os
1314
import shutil
1415
import subprocess
1516
import sys
@@ -66,6 +67,18 @@ def get_generation_script_files(generation_script: str):
6667

6768
return files
6869

70+
COMMON_GENERATION_SCRIPTS = [
71+
]
72+
73+
# Once the script has been added to both Mbed TLS and TF-PSA-Crypto,
74+
# we can include this unconditionally.
75+
# https://github.com/Mbed-TLS/mbedtls/issues/10305
76+
if os.path.exists("scripts/generate_config_checks.py"):
77+
COMMON_GENERATION_SCRIPTS.append(GenerationScript(
78+
Path("scripts/generate_config_checks.py"),
79+
get_generation_script_files("scripts/generate_config_checks.py"),
80+
"", None))
81+
6982
if build_tree.looks_like_tf_psa_crypto_root("."):
7083
TF_PSA_CRYPTO_GENERATION_SCRIPTS = [
7184
GenerationScript(
@@ -244,6 +257,7 @@ def main():
244257
generation_scripts = MBEDTLS_GENERATION_SCRIPTS
245258
else:
246259
raise Exception("No support for Mbed TLS 3.6")
260+
generation_scripts += COMMON_GENERATION_SCRIPTS
247261

248262
if args.list:
249263
files = get_generated_files(generation_scripts)
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
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

Comments
 (0)