Loading...
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 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 | # SPDX-License-Identifier: GPL-2.0+ # Copyright 2022 Google LLC # Written by Simon Glass <sjg@chromium.org> # """Utility functions for dealing with Kconfig .config files""" import os import re import tempfile from u_boot_pylib import command from u_boot_pylib import tools class Config: """Holds information about configuration settings for a board.""" def __init__(self, config_filename, target): self.target = target self.config = {} for fname in config_filename: self.config[fname] = {} def add(self, fname, key, value): """Add a configuration value Args: fname (str): Filename to add to (e.g. '.config') key (str): Config key (e.g. 'CONFIG_DM') value (str): Config value (e.g. 'y') """ self.config[fname][key] = value def __hash__(self): val = 0 for _, config in self.config.items(): for key, value in config.items(): print(key, value) val = val ^ hash(key) & hash(value) return val RE_LINE = re.compile(r'(# )?CONFIG_([A-Z0-9_]+)(=(.*)| is not set)') RE_CFG = re.compile(r'(~?)(CONFIG_)?([A-Z0-9_]+)(=.*)?') def make_cfg_line(opt, adj): """Make a new config line for an option Args: opt (str): Option to process, without CONFIG_ prefix adj (str): Adjustment to make (C is config option without prefix): C to enable C ~C to disable C C=val to set the value of C (val must have quotes if C is a string Kconfig) Returns: str: New line to use, one of: CONFIG_opt=y - option is enabled # CONFIG_opt is not set - option is disabled CONFIG_opt=val - option is getting a new value (val is in quotes if this is a string) """ if adj[0] == '~': return f'# CONFIG_{opt} is not set' if '=' in adj: return f'CONFIG_{adj}' return f'CONFIG_{opt}=y' def adjust_cfg_line(line, adjust_cfg, done=None): """Make an adjustment to a single of line from a .config file This processes a .config line, producing a new line if a change for this CONFIG is requested in adjust_cfg Args: line (str): line to process, e.g. '# CONFIG_FRED is not set' or 'CONFIG_FRED=y' or 'CONFIG_FRED=0x123' or 'CONFIG_FRED="fred"' adjust_cfg (dict of str): Changes to make to .config file before building: key: str config to change, without the CONFIG_ prefix, e.g. FRED value: str change to make (C is config option without prefix): C to enable C ~C to disable C C=val to set the value of C (val must have quotes if C is a string Kconfig) done (set of set): Adds the config option to this set if it is changed in some way. This is used to track which ones have been processed. None to skip. Returns: tuple: str: New string for this line (maybe unchanged) str: Adjustment string that was used """ out_line = line m_line = RE_LINE.match(line) adj = None if m_line: _, opt, _, _ = m_line.groups() adj = adjust_cfg.get(opt) if adj: out_line = make_cfg_line(opt, adj) if done is not None: done.add(opt) return out_line, adj def adjust_cfg_lines(lines, adjust_cfg): """Make adjustments to a list of lines from a .config file Args: lines (list of str): List of lines to process adjust_cfg (dict of str): Changes to make to .config file before building: key: str config to change, without the CONFIG_ prefix, e.g. FRED value: str change to make (C is config option without prefix): C to enable C ~C to disable C C=val to set the value of C (val must have quotes if C is a string Kconfig) Returns: list of str: New list of lines resulting from the processing """ out_lines = [] done = set() for line in lines: out_line, _ = adjust_cfg_line(line, adjust_cfg, done) out_lines.append(out_line) for opt in adjust_cfg: if opt not in done: adj = adjust_cfg.get(opt) out_line = make_cfg_line(opt, adj) out_lines.append(out_line) return out_lines def adjust_cfg_file(fname, adjust_cfg): """Make adjustments to a .config file Args: fname (str): Filename of .config file to change adjust_cfg (dict of str): Changes to make to .config file before building: key: str config to change, without the CONFIG_ prefix, e.g. FRED value: str change to make (C is config option without prefix): C to enable C ~C to disable C C=val to set the value of C (val must have quotes if C is a string Kconfig) """ lines = tools.read_file(fname, binary=False).splitlines() out_lines = adjust_cfg_lines(lines, adjust_cfg) out = '\n'.join(out_lines) + '\n' tools.write_file(fname, out, binary=False) def convert_list_to_dict(adjust_cfg_list): """Convert a list of config changes into the dict used by adjust_cfg_file() Args: adjust_cfg_list (list of str): List of changes to make to .config file before building. Each is one of (where C is the config option with or without the CONFIG_ prefix). Items can be comma-separated. C to enable C ~C to disable C C=val to set the value of C (val must have quotes if C is a string Kconfig Returns: dict of str: Changes to make to .config file before building: key: str config to change, without the CONFIG_ prefix, e.g. FRED value: str change to make (C is config option without prefix): C to enable C ~C to disable C C=val to set the value of C (val must have quotes if C is a string Kconfig) Raises: ValueError: if an item in adjust_cfg_list has invalid syntax """ result = {} for item in adjust_cfg_list or []: # Split by comma to support comma-separated values for cfg in item.split(','): cfg = cfg.strip() if not cfg: continue m_cfg = RE_CFG.match(cfg) if not m_cfg: raise ValueError(f"Invalid CONFIG adjustment '{cfg}'") negate, _, opt, val = m_cfg.groups() result[opt] = f'%s{opt}%s' % (negate or '', val or '') return result def check_cfg_lines(lines, adjust_cfg): """Check that lines do not conflict with the requested changes If a line enables a CONFIG which was requested to be disabled, etc., then this is an error. This function finds such errors. Args: lines (list of str): List of lines to process adjust_cfg (dict of str): Changes to make to .config file before building: key: str config to change, without the CONFIG_ prefix, e.g. FRED value: str change to make (C is config option without prefix): C to enable C ~C to disable C C=val to set the value of C (val must have quotes if C is a string Kconfig) Returns: list of tuple: list of errors, each a tuple: str: cfg adjustment requested str: line of the config that conflicts """ bad = [] done = set() for line in lines: out_line, adj = adjust_cfg_line(line, adjust_cfg, done) if out_line != line: bad.append([adj, line]) for opt in adjust_cfg: if opt not in done: adj = adjust_cfg.get(opt) out_line = make_cfg_line(opt, adj) bad.append([adj, f'Missing expected line: {out_line}']) return bad def check_cfg_file(fname, adjust_cfg): """Check that a config file has been adjusted according to adjust_cfg Args: fname (str): Filename of .config file to change adjust_cfg (dict of str): Changes to make to .config file before building: key: str config to change, without the CONFIG_ prefix, e.g. FRED value: str change to make (C is config option without prefix): C to enable C ~C to disable C C=val to set the value of C (val must have quotes if C is a string Kconfig) Returns: str: None if OK, else an error string listing the problems """ lines = tools.read_file(fname, binary=False).splitlines() bad_cfgs = check_cfg_lines(lines, adjust_cfg) if bad_cfgs: out = [f'{cfg:20} {line}' for cfg, line in bad_cfgs] content = '\\n'.join(out) return f''' Some CONFIG adjustments did not take effect. This may be because the request CONFIGs do not exist or conflict with others. Failed adjustments: {content} ''' return None def process_config(fname, squash_config_y): """Read in a .config, autoconf.mk or autoconf.h file This function handles all config file types. It ignores comments and any #defines which don't start with CONFIG_. Args: fname (str): Filename to read squash_config_y (bool): If True, replace 'y' values with '1' Returns: dict: Dictionary with: key: Config name (e.g. CONFIG_DM) value: Config value (e.g. '1') """ config = {} if os.path.exists(fname): with open(fname, encoding='utf-8') as fd: for line in fd: line = line.strip() if line.startswith('#define'): values = line[8:].split(' ', 1) if len(values) > 1: key, value = values else: key = values[0] value = '1' if squash_config_y else '' if not key.startswith('CONFIG_'): continue elif not line or line[0] in ['#', '*', '/']: continue else: key, value = line.split('=', 1) if squash_config_y and value == 'y': value = '1' config[key] = value return config def adjust_cfg_to_fragment(adjust_cfg): """Convert adjust_cfg dict to config fragment content Args: adjust_cfg (dict): Changes to make to .config file. Keys are config names (without CONFIG_ prefix), values are the setting. Format matches make_cfg_line(): ~... - disable the option ...=val - set the option to val (val contains full assignment) other - enable the option with =y Returns: str: Config fragment content suitable for merge_config.sh """ lines = [] for opt, val in adjust_cfg.items(): if val.startswith('~'): lines.append(f'# CONFIG_{opt} is not set') elif '=' in val: lines.append(f'CONFIG_{val}') else: lines.append(f'CONFIG_{opt}=y') return '\n'.join(lines) + '\n' if lines else '' def run_merge_config(src_dir, out_dir, cfg_file, adjust_cfg, env): """Run merge_config.sh to apply config changes with Kconfig resolution This uses scripts/kconfig/merge_config.sh to merge config fragments into the .config file, then runs 'make alldefconfig' to resolve all Kconfig dependencies including 'imply' and 'select'. To properly resolve 'imply' relationships, we must use a minimal defconfig as the base (not the full .config). The full .config contains '# CONFIG_xxx is not set' lines which count as "specified" and prevent imply from taking effect. Using savedefconfig output ensures only explicitly set options are in the base, allowing imply to work. Args: src_dir (str): Source directory (containing scripts/kconfig) out_dir (str): Output directory containing .config cfg_file (str): Path to the .config file adjust_cfg (dict): Config changes to apply env (dict): Environment variables Returns: CommandResult: Result of the merge_config.sh operation """ # Create a temporary fragment file with the config changes fragment_content = adjust_cfg_to_fragment(adjust_cfg) with tempfile.NamedTemporaryFile(mode='w', suffix='.config', delete=False) as frag: frag.write(fragment_content) frag_path = frag.name # Create a minimal defconfig from the current .config # This is necessary for 'imply' to work - the full .config has # '# CONFIG_xxx is not set' lines that prevent imply from taking effect # # Convert paths to be relative to src_dir since commands run with # cwd=src_dir if src_dir and out_dir: rel_out_dir = os.path.relpath(out_dir, src_dir) rel_cfg_file = os.path.relpath(cfg_file, src_dir) else: rel_out_dir = out_dir or '.' rel_cfg_file = cfg_file defconfig_path = os.path.join(rel_out_dir, 'defconfig') make_cmd = ['make', f'O={rel_out_dir}' if rel_out_dir != '.' else None, f'KCONFIG_CONFIG={rel_cfg_file}', 'savedefconfig'] make_cmd = [x for x in make_cmd if x] # Remove None elements result = command.run_one(*make_cmd, cwd=src_dir, env=env, capture=True, capture_stderr=True) if result.return_code: if os.path.exists(frag_path): os.unlink(frag_path) return result try: # Run merge_config.sh with the minimal defconfig as base # -O sets output dir; defconfig is the base, fragment is merged merge_script = os.path.join('scripts', 'kconfig', 'merge_config.sh') cmd = [merge_script, '-O', rel_out_dir, defconfig_path, frag_path] result = command.run_one(*cmd, cwd=src_dir, env=env, capture=True, capture_stderr=True) finally: # Clean up temporary files if os.path.exists(frag_path): os.unlink(frag_path) return result |