kelvin.zhang | ac22e65 | 2021-10-18 15:09:21 +0800 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | |
| 3 | # Writes/updates the zephyr/.config configuration file by merging configuration |
| 4 | # files passed as arguments, e.g. board *_defconfig and application prj.conf |
| 5 | # files. |
| 6 | # |
| 7 | # When fragments haven't changed, zephyr/.config is both the input and the |
| 8 | # output, which just updates it. This is handled in the CMake files. |
| 9 | # |
| 10 | # Also does various checks (most via Kconfiglib warnings). |
| 11 | |
| 12 | import argparse |
| 13 | import os |
| 14 | import sys |
| 15 | import textwrap |
| 16 | |
| 17 | # Zephyr doesn't use tristate symbols. They're supported here just to make the |
| 18 | # script a bit more generic. |
| 19 | from kconfiglib import Kconfig, split_expr, expr_value, expr_str, BOOL, \ |
| 20 | TRISTATE, TRI_TO_STR, AND |
| 21 | |
| 22 | |
| 23 | def main(): |
| 24 | args = parse_args() |
| 25 | |
| 26 | if args.zephyr_base: |
| 27 | os.environ['ZEPHYR_BASE'] = args.zephyr_base |
| 28 | |
| 29 | print("Parsing " + args.kconfig_file) |
| 30 | kconf = Kconfig(args.kconfig_file, warn_to_stderr=False, |
| 31 | suppress_traceback=True) |
| 32 | |
| 33 | if args.handwritten_input_configs: |
| 34 | # Warn for assignments to undefined symbols, but only for handwritten |
| 35 | # fragments, to avoid warnings-turned-errors when using an old |
| 36 | # configuration file together with updated Kconfig files |
| 37 | kconf.warn_assign_undef = True |
| 38 | |
| 39 | # prj.conf may override settings from the board configuration, so |
| 40 | # disable warnings about symbols being assigned more than once |
| 41 | kconf.warn_assign_override = False |
| 42 | kconf.warn_assign_redun = False |
| 43 | |
| 44 | # Load configuration files |
| 45 | print(kconf.load_config(args.configs_in[0])) |
| 46 | for config in args.configs_in[1:]: |
| 47 | # replace=False creates a merged configuration |
| 48 | print(kconf.load_config(config, replace=False)) |
| 49 | |
| 50 | if args.handwritten_input_configs: |
| 51 | # Check that there are no assignments to promptless symbols, which |
| 52 | # have no effect. |
| 53 | # |
| 54 | # This only makes sense when loading handwritten fragments and not when |
| 55 | # loading zephyr/.config, because zephyr/.config is configuration |
| 56 | # output and also assigns promptless symbols. |
| 57 | check_no_promptless_assign(kconf) |
| 58 | |
| 59 | # Print warnings for symbols that didn't get the assigned value. Only |
| 60 | # do this for handwritten input too, to avoid likely unhelpful warnings |
| 61 | # when using an old configuration and updating Kconfig files. |
| 62 | check_assigned_sym_values(kconf) |
| 63 | check_assigned_choice_values(kconf) |
| 64 | |
| 65 | # Hack: Force all symbols to be evaluated, to catch warnings generated |
| 66 | # during evaluation. Wait till the end to write the actual output files, so |
| 67 | # that we don't generate any output if there are warnings-turned-errors. |
| 68 | # |
| 69 | # Kconfiglib caches calculated symbol values internally, so this is still |
| 70 | # fast. |
| 71 | kconf.write_config(os.devnull) |
| 72 | |
| 73 | if kconf.warnings: |
| 74 | # Put a blank line between warnings to make them easier to read |
| 75 | for warning in kconf.warnings: |
| 76 | print("\n" + warning, file=sys.stderr) |
| 77 | |
| 78 | # Turn all warnings into errors, so that e.g. assignments to undefined |
| 79 | # Kconfig symbols become errors. |
| 80 | # |
| 81 | # A warning is generated by this script whenever a symbol gets a |
| 82 | # different value than the one it was assigned. Keep that one as just a |
| 83 | # warning for now. |
| 84 | err("Aborting due to Kconfig warnings") |
| 85 | |
| 86 | # Write the merged configuration and the C header |
| 87 | print(kconf.write_config(args.config_out)) |
| 88 | print(kconf.write_autoconf(args.header_out)) |
| 89 | |
| 90 | # Write the list of parsed Kconfig files to a file |
| 91 | write_kconfig_filenames(kconf, args.kconfig_list_out) |
| 92 | |
| 93 | |
| 94 | def check_no_promptless_assign(kconf): |
| 95 | # Checks that no promptless symbols are assigned |
| 96 | |
| 97 | for sym in kconf.unique_defined_syms: |
| 98 | if sym.user_value is not None and promptless(sym): |
| 99 | err(f"""\ |
| 100 | {sym.name_and_loc} is assigned in a configuration file, but is not directly |
| 101 | user-configurable (has no prompt). It gets its value indirectly from other |
| 102 | symbols. """ + SYM_INFO_HINT.format(sym)) |
| 103 | |
| 104 | |
| 105 | def check_assigned_sym_values(kconf): |
| 106 | # Verifies that the values assigned to symbols "took" (matches the value |
| 107 | # the symbols actually got), printing warnings otherwise. Choice symbols |
| 108 | # are checked separately, in check_assigned_choice_values(). |
| 109 | |
| 110 | for sym in kconf.unique_defined_syms: |
| 111 | if sym.choice: |
| 112 | continue |
| 113 | |
| 114 | user_value = sym.user_value |
| 115 | if user_value is None: |
| 116 | continue |
| 117 | |
| 118 | # Tristate values are represented as 0, 1, 2. Having them as "n", "m", |
| 119 | # "y" is more convenient here, so convert. |
| 120 | if sym.type in (BOOL, TRISTATE): |
| 121 | user_value = TRI_TO_STR[user_value] |
| 122 | |
| 123 | if user_value != sym.str_value: |
| 124 | msg = f"{sym.name_and_loc} was assigned the value '{user_value}'" \ |
| 125 | f" but got the value '{sym.str_value}'. " |
| 126 | |
| 127 | # List any unsatisfied 'depends on' dependencies in the warning |
| 128 | mdeps = missing_deps(sym) |
| 129 | if mdeps: |
| 130 | expr_strs = [] |
| 131 | for expr in mdeps: |
| 132 | estr = expr_str(expr) |
| 133 | if isinstance(expr, tuple): |
| 134 | # Add () around dependencies that aren't plain symbols. |
| 135 | # Gives '(FOO || BAR) (=n)' instead of |
| 136 | # 'FOO || BAR (=n)', which might be clearer. |
| 137 | estr = f"({estr})" |
| 138 | expr_strs.append(f"{estr} " |
| 139 | f"(={TRI_TO_STR[expr_value(expr)]})") |
| 140 | |
| 141 | msg += "Check these unsatisfied dependencies: " + \ |
| 142 | ", ".join(expr_strs) + ". " |
| 143 | |
| 144 | warn(msg + SYM_INFO_HINT.format(sym)) |
| 145 | |
| 146 | |
| 147 | def missing_deps(sym): |
| 148 | # check_assigned_sym_values() helper for finding unsatisfied dependencies. |
| 149 | # |
| 150 | # Given direct dependencies |
| 151 | # |
| 152 | # depends on <expr> && <expr> && ... && <expr> |
| 153 | # |
| 154 | # on 'sym' (which can also come from e.g. a surrounding 'if'), returns a |
| 155 | # list of all <expr>s with a value less than the value 'sym' was assigned |
| 156 | # ("less" instead of "not equal" just to be general and handle tristates, |
| 157 | # even though Zephyr doesn't use them). |
| 158 | # |
| 159 | # For string/int/hex symbols, just looks for <expr> = n. |
| 160 | # |
| 161 | # Note that <expr>s can be something more complicated than just a symbol, |
| 162 | # like 'FOO || BAR' or 'FOO = "string"'. |
| 163 | |
| 164 | deps = split_expr(sym.direct_dep, AND) |
| 165 | |
| 166 | if sym.type in (BOOL, TRISTATE): |
| 167 | return [dep for dep in deps if expr_value(dep) < sym.user_value] |
| 168 | # string/int/hex |
| 169 | return [dep for dep in deps if expr_value(dep) == 0] |
| 170 | |
| 171 | |
| 172 | def check_assigned_choice_values(kconf): |
| 173 | # Verifies that any choice symbols that were selected (by setting them to |
| 174 | # y) ended up as the selection, printing warnings otherwise. |
| 175 | # |
| 176 | # We check choice symbols separately to avoid warnings when two different |
| 177 | # choice symbols within the same choice are set to y. This might happen if |
| 178 | # a choice selection from a board defconfig is overridden in a prj.conf, |
| 179 | # for example. The last choice symbol set to y becomes the selection (and |
| 180 | # all other choice symbols get the value n). |
| 181 | # |
| 182 | # Without special-casing choices, we'd detect that the first symbol set to |
| 183 | # y ended up as n, and print a spurious warning. |
| 184 | |
| 185 | for choice in kconf.unique_choices: |
| 186 | if choice.user_selection and \ |
| 187 | choice.user_selection is not choice.selection: |
| 188 | |
| 189 | warn(f"""\ |
| 190 | The choice symbol {choice.user_selection.name_and_loc} was selected (set =y), |
| 191 | but {choice.selection.name_and_loc if choice.selection else "no symbol"} ended |
| 192 | up as the choice selection. """ + SYM_INFO_HINT.format(choice.user_selection)) |
| 193 | |
| 194 | |
| 195 | # Hint on where to find symbol information. Used like |
| 196 | # SYM_INFO_HINT.format(sym). |
| 197 | SYM_INFO_HINT = """\ |
| 198 | See http://docs.zephyrproject.org/latest/reference/kconfig/CONFIG_{0.name}.html |
| 199 | and/or look up {0.name} in the menuconfig/guiconfig interface. The Application |
| 200 | Development Primer, Setting Configuration Values, and Kconfig - Tips and Best |
| 201 | Practices sections of the manual might be helpful too.\ |
| 202 | """ |
| 203 | |
| 204 | |
| 205 | def promptless(sym): |
| 206 | # Returns True if 'sym' has no prompt. Since the symbol might be defined in |
| 207 | # multiple locations, we need to check all locations. |
| 208 | |
| 209 | return not any(node.prompt for node in sym.nodes) |
| 210 | |
| 211 | |
| 212 | def write_kconfig_filenames(kconf, kconfig_list_path): |
| 213 | # Writes a sorted list with the absolute paths of all parsed Kconfig files |
| 214 | # to 'kconfig_list_path'. The paths are realpath()'d, and duplicates are |
| 215 | # removed. This file is used by CMake to look for changed Kconfig files. It |
| 216 | # needs to be deterministic. |
| 217 | |
| 218 | with open(kconfig_list_path, 'w') as out: |
| 219 | for path in sorted({os.path.realpath(os.path.join(kconf.srctree, path)) |
| 220 | for path in kconf.kconfig_filenames}): |
| 221 | print(path, file=out) |
| 222 | |
| 223 | |
| 224 | def parse_args(): |
| 225 | parser = argparse.ArgumentParser() |
| 226 | |
| 227 | parser.add_argument("--handwritten-input-configs", |
| 228 | action="store_true", |
| 229 | help="Assume the input configuration fragments are " |
| 230 | "handwritten fragments and do additional checks " |
| 231 | "on them, like no promptless symbols being " |
| 232 | "assigned") |
| 233 | parser.add_argument("--zephyr-base", |
| 234 | help="Path to current Zephyr installation") |
| 235 | parser.add_argument("kconfig_file", |
| 236 | help="Top-level Kconfig file") |
| 237 | parser.add_argument("config_out", |
| 238 | help="Output configuration file") |
| 239 | parser.add_argument("header_out", |
| 240 | help="Output header file") |
| 241 | parser.add_argument("kconfig_list_out", |
| 242 | help="Output file for list of parsed Kconfig files") |
| 243 | parser.add_argument("configs_in", |
| 244 | nargs="+", |
| 245 | help="Input configuration fragments. Will be merged " |
| 246 | "together.") |
| 247 | |
| 248 | return parser.parse_args() |
| 249 | |
| 250 | |
| 251 | def warn(msg): |
| 252 | # Use a large fill() width to try to avoid linebreaks in the symbol |
| 253 | # reference link, and add some extra newlines to set the message off from |
| 254 | # surrounding text (this usually gets printed as part of spammy CMake |
| 255 | # output) |
| 256 | print("\n" + textwrap.fill("warning: " + msg, 100) + "\n", file=sys.stderr) |
| 257 | |
| 258 | |
| 259 | def err(msg): |
| 260 | sys.exit("\n" + textwrap.fill("error: " + msg, 100) + "\n") |
| 261 | |
| 262 | |
| 263 | if __name__ == "__main__": |
| 264 | main() |