File: config_checks_generator.py

package info (click to toggle)
mbedtls 3.6.5-0.1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 51,488 kB
  • sloc: ansic: 164,842; sh: 25,443; python: 15,512; makefile: 3,131; perl: 1,043; tcl: 4
file content (294 lines) | stat: -rw-r--r-- 10,524 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
"""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, Optional

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 SubprojectInternal(Checker):
    """Checker for an internal macro of a subproject."""

    # must be overridden in child classes
    SUBPROJECT = None #type: Optional[str]

    def __init__(self, name: str, suggestion: str = '') -> None:
        super().__init__(name, suggestion)
        if self.SUBPROJECT is None:
            raise ValueError('No subproject specified for ' + name)
        self.subproject = self.SUBPROJECT #type: str

    def _basic_message(self) -> str:
        return f'{self.name} is an internal macro of {self.subproject} and may not be configured.'

    def before(self, prefix: str) -> str:
        return f'''
        #if defined({self.name})
        #  define {prefix}_WASSET_{self.name} 1
        #else
        #  define {prefix}_WASSET_{self.name} 0
        #endif
        #undef {self.name}
        '''

    def user(self, prefix: str) -> str:
        return f'''
        #if defined({self.name})
        #  error {self._quoted_message()}
        #endif
        #if {prefix}_WASSET_{self.name}
        #  define {self.name}
        #endif
        #undef {prefix}_WASSET_{self.name}
        '''


class SubprojectOption(SubprojectInternal):
    """Checker for a configuration option of a subproject."""

    def _basic_message(self) -> str:
        return f'{self.name} must be configured in {self.subproject}.'

    def user(self, prefix: str) -> str:
        return f'''
        #if defined({self.name}) && !{prefix}_WASSET_{self.name}
        #  error {self._quoted_message()}
        #endif
        #if {prefix}_WASSET_{self.name}
        #  define {self.name}
        #endif
        #undef {prefix}_WASSET_{self.name}
        '''


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()
    # Is root the current directory? The safe default is no, so compare
    # the paths, rather than calling `os.samefile()` which can have false
    # positives and can fail in edge cases.
    if root == os.getcwd():
        # Be nice and use a relative path when it's simple to do so.
        # (build_tree.guess_project_root() should probably do this, actually.)
        # This is not only nice to humans, but also necessary for
        # `make_generated_files.py --root DIR --check`: it calls
        # this script with `--list` and expects a path that is relative
        # to DIR, not an absolute path that is under the project root.
        root = os.curdir
    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 = list(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)