uboot/tools/moveconfig.py
<<
>>
Prefs
   1#!/usr/bin/env python3
   2# SPDX-License-Identifier: GPL-2.0+
   3#
   4# Author: Masahiro Yamada <yamada.masahiro@socionext.com>
   5#
   6
   7"""
   8Move config options from headers to defconfig files.
   9
  10See doc/develop/moveconfig.rst for documentation.
  11"""
  12
  13from argparse import ArgumentParser
  14import asteval
  15import collections
  16from contextlib import ExitStack
  17import copy
  18import difflib
  19import doctest
  20import filecmp
  21import fnmatch
  22import glob
  23import multiprocessing
  24import os
  25import queue
  26import re
  27import shutil
  28import subprocess
  29import sys
  30import tempfile
  31import threading
  32import time
  33import unittest
  34
  35from buildman import bsettings
  36from buildman import kconfiglib
  37from buildman import toolchain
  38
  39SHOW_GNU_MAKE = 'scripts/show-gnu-make'
  40SLEEP_TIME=0.03
  41
  42STATE_IDLE = 0
  43STATE_DEFCONFIG = 1
  44STATE_AUTOCONF = 2
  45STATE_SAVEDEFCONFIG = 3
  46
  47ACTION_MOVE = 0
  48ACTION_NO_ENTRY = 1
  49ACTION_NO_ENTRY_WARN = 2
  50ACTION_NO_CHANGE = 3
  51
  52COLOR_BLACK        = '0;30'
  53COLOR_RED          = '0;31'
  54COLOR_GREEN        = '0;32'
  55COLOR_BROWN        = '0;33'
  56COLOR_BLUE         = '0;34'
  57COLOR_PURPLE       = '0;35'
  58COLOR_CYAN         = '0;36'
  59COLOR_LIGHT_GRAY   = '0;37'
  60COLOR_DARK_GRAY    = '1;30'
  61COLOR_LIGHT_RED    = '1;31'
  62COLOR_LIGHT_GREEN  = '1;32'
  63COLOR_YELLOW       = '1;33'
  64COLOR_LIGHT_BLUE   = '1;34'
  65COLOR_LIGHT_PURPLE = '1;35'
  66COLOR_LIGHT_CYAN   = '1;36'
  67COLOR_WHITE        = '1;37'
  68
  69AUTO_CONF_PATH = 'include/config/auto.conf'
  70CONFIG_DATABASE = 'moveconfig.db'
  71
  72CONFIG_LEN = len('CONFIG_')
  73
  74SIZES = {
  75    'SZ_1':    0x00000001, 'SZ_2':    0x00000002,
  76    'SZ_4':    0x00000004, 'SZ_8':    0x00000008,
  77    'SZ_16':   0x00000010, 'SZ_32':   0x00000020,
  78    'SZ_64':   0x00000040, 'SZ_128':  0x00000080,
  79    'SZ_256':  0x00000100, 'SZ_512':  0x00000200,
  80    'SZ_1K':   0x00000400, 'SZ_2K':   0x00000800,
  81    'SZ_4K':   0x00001000, 'SZ_8K':   0x00002000,
  82    'SZ_16K':  0x00004000, 'SZ_32K':  0x00008000,
  83    'SZ_64K':  0x00010000, 'SZ_128K': 0x00020000,
  84    'SZ_256K': 0x00040000, 'SZ_512K': 0x00080000,
  85    'SZ_1M':   0x00100000, 'SZ_2M':   0x00200000,
  86    'SZ_4M':   0x00400000, 'SZ_8M':   0x00800000,
  87    'SZ_16M':  0x01000000, 'SZ_32M':  0x02000000,
  88    'SZ_64M':  0x04000000, 'SZ_128M': 0x08000000,
  89    'SZ_256M': 0x10000000, 'SZ_512M': 0x20000000,
  90    'SZ_1G':   0x40000000, 'SZ_2G':   0x80000000,
  91    'SZ_4G':  0x100000000
  92}
  93
  94RE_REMOVE_DEFCONFIG = re.compile(r'(.*)_defconfig')
  95
  96### helper functions ###
  97def remove_defconfig(defc):
  98    """Drop the _defconfig suffix on a string
  99
 100    Args:
 101        defc (str): String to convert
 102
 103    Returns:
 104        str: string with the '_defconfig' suffix removed
 105    """
 106    return RE_REMOVE_DEFCONFIG.match(defc)[1]
 107
 108def check_top_directory():
 109    """Exit if we are not at the top of source directory."""
 110    for fname in 'README', 'Licenses':
 111        if not os.path.exists(fname):
 112            sys.exit('Please run at the top of source directory.')
 113
 114def check_clean_directory():
 115    """Exit if the source tree is not clean."""
 116    for fname in '.config', 'include/config':
 117        if os.path.exists(fname):
 118            sys.exit("source tree is not clean, please run 'make mrproper'")
 119
 120def get_make_cmd():
 121    """Get the command name of GNU Make.
 122
 123    U-Boot needs GNU Make for building, but the command name is not
 124    necessarily "make". (for example, "gmake" on FreeBSD).
 125    Returns the most appropriate command name on your system.
 126    """
 127    with subprocess.Popen([SHOW_GNU_MAKE], stdout=subprocess.PIPE) as proc:
 128        ret = proc.communicate()
 129        if proc.returncode:
 130            sys.exit('GNU Make not found')
 131    return ret[0].rstrip()
 132
 133def get_matched_defconfig(line):
 134    """Get the defconfig files that match a pattern
 135
 136    Args:
 137        line (str): Path or filename to match, e.g. 'configs/snow_defconfig' or
 138            'k2*_defconfig'. If no directory is provided, 'configs/' is
 139            prepended
 140
 141    Returns:
 142        list of str: a list of matching defconfig files
 143    """
 144    dirname = os.path.dirname(line)
 145    if dirname:
 146        pattern = line
 147    else:
 148        pattern = os.path.join('configs', line)
 149    return glob.glob(pattern) + glob.glob(pattern + '_defconfig')
 150
 151def get_matched_defconfigs(defconfigs_file):
 152    """Get all the defconfig files that match the patterns in a file.
 153
 154    Args:
 155        defconfigs_file (str): File containing a list of defconfigs to process,
 156            or '-' to read the list from stdin
 157
 158    Returns:
 159        list of str: A list of paths to defconfig files, with no duplicates
 160    """
 161    defconfigs = []
 162    with ExitStack() as stack:
 163        if defconfigs_file == '-':
 164            inf = sys.stdin
 165            defconfigs_file = 'stdin'
 166        else:
 167            inf = stack.enter_context(open(defconfigs_file, encoding='utf-8'))
 168        for i, line in enumerate(inf):
 169            line = line.strip()
 170            if not line:
 171                continue # skip blank lines silently
 172            if ' ' in line:
 173                line = line.split(' ')[0]  # handle 'git log' input
 174            matched = get_matched_defconfig(line)
 175            if not matched:
 176                print(f"warning: {defconfigs_file}:{i + 1}: no defconfig matched '{line}'",
 177                      file=sys.stderr)
 178
 179            defconfigs += matched
 180
 181    # use set() to drop multiple matching
 182    return [defconfig[len('configs') + 1:]  for defconfig in set(defconfigs)]
 183
 184def get_all_defconfigs():
 185    """Get all the defconfig files under the configs/ directory.
 186
 187    Returns:
 188        list of str: List of paths to defconfig files
 189    """
 190    defconfigs = []
 191    for (dirpath, _, filenames) in os.walk('configs'):
 192        dirpath = dirpath[len('configs') + 1:]
 193        for filename in fnmatch.filter(filenames, '*_defconfig'):
 194            defconfigs.append(os.path.join(dirpath, filename))
 195
 196    return defconfigs
 197
 198def color_text(color_enabled, color, string):
 199    """Return colored string."""
 200    if color_enabled:
 201        # LF should not be surrounded by the escape sequence.
 202        # Otherwise, additional whitespace or line-feed might be printed.
 203        return '\n'.join([ '\033[' + color + 'm' + s + '\033[0m' if s else ''
 204                           for s in string.split('\n') ])
 205    return string
 206
 207def show_diff(alines, blines, file_path, color_enabled):
 208    """Show unidified diff.
 209
 210    Args:
 211       alines (list of str): A list of lines (before)
 212       blines (list of str): A list of lines (after)
 213       file_path (str): Path to the file
 214       color_enabled (bool): Display the diff in color
 215    """
 216    diff = difflib.unified_diff(alines, blines,
 217                                fromfile=os.path.join('a', file_path),
 218                                tofile=os.path.join('b', file_path))
 219
 220    for line in diff:
 221        if line.startswith('-') and not line.startswith('--'):
 222            print(color_text(color_enabled, COLOR_RED, line))
 223        elif line.startswith('+') and not line.startswith('++'):
 224            print(color_text(color_enabled, COLOR_GREEN, line))
 225        else:
 226            print(line)
 227
 228def extend_matched_lines(lines, matched, pre_patterns, post_patterns,
 229                         extend_pre, extend_post):
 230    """Extend matched lines if desired patterns are found before/after already
 231    matched lines.
 232
 233    Args:
 234      lines (list of str): list of lines handled.
 235      matched (list of int): list of line numbers that have been already
 236          matched (will be updated by this function)
 237      pre_patterns (list of re.Pattern): list of regular expression that should
 238          be matched as preamble
 239      post_patterns (list of re.Pattern): list of regular expression that should
 240          be matched as postamble
 241      extend_pre (bool): Add the line number of matched preamble to the matched
 242          list
 243      extend_post (bool): Add the line number of matched postamble to the
 244          matched list
 245    """
 246    extended_matched = []
 247
 248    j = matched[0]
 249
 250    for i in matched:
 251        if i == 0 or i < j:
 252            continue
 253        j = i
 254        while j in matched:
 255            j += 1
 256        if j >= len(lines):
 257            break
 258
 259        for pat in pre_patterns:
 260            if pat.search(lines[i - 1]):
 261                break
 262        else:
 263            # not matched
 264            continue
 265
 266        for pat in post_patterns:
 267            if pat.search(lines[j]):
 268                break
 269        else:
 270            # not matched
 271            continue
 272
 273        if extend_pre:
 274            extended_matched.append(i - 1)
 275        if extend_post:
 276            extended_matched.append(j)
 277
 278    matched += extended_matched
 279    matched.sort()
 280
 281def confirm(args, prompt):
 282    """Ask the user to confirm something
 283
 284    Args:
 285        args (Namespace ): program arguments
 286
 287    Returns:
 288        bool: True to confirm, False to cancel/stop
 289    """
 290    if not args.yes:
 291        while True:
 292            choice = input(f'{prompt} [y/n]: ')
 293            choice = choice.lower()
 294            print(choice)
 295            if choice in ('y', 'n'):
 296                break
 297
 298        if choice == 'n':
 299            return False
 300
 301    return True
 302
 303def write_file(fname, data):
 304    """Write data to a file
 305
 306    Args:
 307        fname (str): Filename to write to
 308        data (list of str): Lines to write (with or without trailing newline);
 309            or str to write
 310    """
 311    with open(fname, 'w', encoding='utf-8') as out:
 312        if isinstance(data, list):
 313            for line in data:
 314                print(line.rstrip('\n'), file=out)
 315        else:
 316            out.write(data)
 317
 318def read_file(fname, as_lines=True, skip_unicode=False):
 319    """Read a file and return the contents
 320
 321    Args:
 322        fname (str): Filename to read from
 323        as_lines: Return file contents as a list of lines
 324        skip_unicode (bool): True to report unicode errors and continue
 325
 326    Returns:
 327        iter of str: List of ;ines from the file with newline removed; str if
 328            as_lines is False with newlines intact; or None if a unicode error
 329            occurred
 330
 331    Raises:
 332        UnicodeDecodeError: Unicode error occurred when reading
 333    """
 334    with open(fname, encoding='utf-8') as inf:
 335        try:
 336            if as_lines:
 337                return [line.rstrip('\n') for line in inf.readlines()]
 338            else:
 339                return inf.read()
 340        except UnicodeDecodeError as e:
 341            if not skip_unicode:
 342                raises
 343            print("Failed on file %s': %s" % (fname, e))
 344            return None
 345
 346def cleanup_empty_blocks(header_path, args):
 347    """Clean up empty conditional blocks
 348
 349    Args:
 350      header_path (str): path to the cleaned file.
 351      args (Namespace): program arguments
 352    """
 353    pattern = re.compile(r'^\s*#\s*if.*$\n^\s*#\s*endif.*$\n*', flags=re.M)
 354    data = read_file(header_path, as_lines=False, skip_unicode=True)
 355    if data is None:
 356        return
 357
 358    new_data = pattern.sub('\n', data)
 359
 360    show_diff(data.splitlines(True), new_data.splitlines(True), header_path,
 361              args.color)
 362
 363    if args.dry_run:
 364        return
 365
 366    if new_data != data:
 367        write_file(header_path, new_data)
 368
 369def cleanup_one_header(header_path, patterns, args):
 370    """Clean regex-matched lines away from a file.
 371
 372    Args:
 373      header_path: path to the cleaned file.
 374      patterns: list of regex patterns.  Any lines matching to these
 375                patterns are deleted.
 376      args (Namespace): program arguments
 377    """
 378    lines = read_file(header_path, skip_unicode=True)
 379    if lines is None:
 380        return
 381
 382    matched = []
 383    for i, line in enumerate(lines):
 384        if i - 1 in matched and lines[i - 1].endswith('\\'):
 385            matched.append(i)
 386            continue
 387        for pattern in patterns:
 388            if pattern.search(line):
 389                matched.append(i)
 390                break
 391
 392    if not matched:
 393        return
 394
 395    # remove empty #ifdef ... #endif, successive blank lines
 396    pattern_if = re.compile(r'#\s*if(def|ndef)?\b') #  #if, #ifdef, #ifndef
 397    pattern_elif = re.compile(r'#\s*el(if|se)\b')   #  #elif, #else
 398    pattern_endif = re.compile(r'#\s*endif\b')      #  #endif
 399    pattern_blank = re.compile(r'^\s*$')            #  empty line
 400
 401    while True:
 402        old_matched = copy.copy(matched)
 403        extend_matched_lines(lines, matched, [pattern_if],
 404                             [pattern_endif], True, True)
 405        extend_matched_lines(lines, matched, [pattern_elif],
 406                             [pattern_elif, pattern_endif], True, False)
 407        extend_matched_lines(lines, matched, [pattern_if, pattern_elif],
 408                             [pattern_blank], False, True)
 409        extend_matched_lines(lines, matched, [pattern_blank],
 410                             [pattern_elif, pattern_endif], True, False)
 411        extend_matched_lines(lines, matched, [pattern_blank],
 412                             [pattern_blank], True, False)
 413        if matched == old_matched:
 414            break
 415
 416    tolines = copy.copy(lines)
 417
 418    for i in reversed(matched):
 419        tolines.pop(i)
 420
 421    show_diff(lines, tolines, header_path, args.color)
 422
 423    if args.dry_run:
 424        return
 425
 426    write_file(header_path, tolines)
 427
 428def cleanup_headers(configs, args):
 429    """Delete config defines from board headers.
 430
 431    Args:
 432      configs: A list of CONFIGs to remove.
 433      args (Namespace): program arguments
 434    """
 435    if not confirm(args, 'Clean up headers?'):
 436        return
 437
 438    patterns = []
 439    for config in configs:
 440        patterns.append(re.compile(r'#\s*define\s+%s\b' % config))
 441        patterns.append(re.compile(r'#\s*undef\s+%s\b' % config))
 442
 443    for dir in 'include', 'arch', 'board':
 444        for (dirpath, dirnames, filenames) in os.walk(dir):
 445            if dirpath == os.path.join('include', 'generated'):
 446                continue
 447            for filename in filenames:
 448                if not filename.endswith(('~', '.dts', '.dtsi', '.bin',
 449                                          '.elf','.aml','.dat')):
 450                    header_path = os.path.join(dirpath, filename)
 451                    # This file contains UTF-16 data and no CONFIG symbols
 452                    if header_path == 'include/video_font_data.h':
 453                        continue
 454                    cleanup_one_header(header_path, patterns, args)
 455                    cleanup_empty_blocks(header_path, args)
 456
 457def cleanup_one_extra_option(defconfig_path, configs, args):
 458    """Delete config defines in CONFIG_SYS_EXTRA_OPTIONS in one defconfig file.
 459
 460    Args:
 461      defconfig_path: path to the cleaned defconfig file.
 462      configs: A list of CONFIGs to remove.
 463      args (Namespace): program arguments
 464    """
 465
 466    start = 'CONFIG_SYS_EXTRA_OPTIONS="'
 467    end = '"'
 468
 469    lines = read_file(defconfig_path)
 470
 471    for i, line in enumerate(lines):
 472        if line.startswith(start) and line.endswith(end):
 473            break
 474    else:
 475        # CONFIG_SYS_EXTRA_OPTIONS was not found in this defconfig
 476        return
 477
 478    old_tokens = line[len(start):-len(end)].split(',')
 479    new_tokens = []
 480
 481    for token in old_tokens:
 482        pos = token.find('=')
 483        if not (token[:pos] if pos >= 0 else token) in configs:
 484            new_tokens.append(token)
 485
 486    if new_tokens == old_tokens:
 487        return
 488
 489    tolines = copy.copy(lines)
 490
 491    if new_tokens:
 492        tolines[i] = start + ','.join(new_tokens) + end
 493    else:
 494        tolines.pop(i)
 495
 496    show_diff(lines, tolines, defconfig_path, args.color)
 497
 498    if args.dry_run:
 499        return
 500
 501    write_file(defconfig_path, tolines)
 502
 503def cleanup_extra_options(configs, args):
 504    """Delete config defines in CONFIG_SYS_EXTRA_OPTIONS in defconfig files.
 505
 506    Args:
 507      configs: A list of CONFIGs to remove.
 508      args (Namespace): program arguments
 509    """
 510    if not confirm(args, 'Clean up CONFIG_SYS_EXTRA_OPTIONS?'):
 511        return
 512
 513    configs = [ config[len('CONFIG_'):] for config in configs ]
 514
 515    defconfigs = get_all_defconfigs()
 516
 517    for defconfig in defconfigs:
 518        cleanup_one_extra_option(os.path.join('configs', defconfig), configs,
 519                                 args)
 520
 521def cleanup_whitelist(configs, args):
 522    """Delete config whitelist entries
 523
 524    Args:
 525      configs: A list of CONFIGs to remove.
 526      args (Namespace): program arguments
 527    """
 528    if not confirm(args, 'Clean up whitelist entries?'):
 529        return
 530
 531    lines = read_file(os.path.join('scripts', 'config_whitelist.txt'))
 532
 533    lines = [x for x in lines if x.strip() not in configs]
 534
 535    write_file(os.path.join('scripts', 'config_whitelist.txt'), lines)
 536
 537def find_matching(patterns, line):
 538    for pat in patterns:
 539        if pat.search(line):
 540            return True
 541    return False
 542
 543def cleanup_readme(configs, args):
 544    """Delete config description in README
 545
 546    Args:
 547      configs: A list of CONFIGs to remove.
 548      args (Namespace): program arguments
 549    """
 550    if not confirm(args, 'Clean up README?'):
 551        return
 552
 553    patterns = []
 554    for config in configs:
 555        patterns.append(re.compile(r'^\s+%s' % config))
 556
 557    lines = read_file('README')
 558
 559    found = False
 560    newlines = []
 561    for line in lines:
 562        if not found:
 563            found = find_matching(patterns, line)
 564            if found:
 565                continue
 566
 567        if found and re.search(r'^\s+CONFIG', line):
 568            found = False
 569
 570        if not found:
 571            newlines.append(line)
 572
 573    write_file('README', newlines)
 574
 575def try_expand(line):
 576    """If value looks like an expression, try expanding it
 577    Otherwise just return the existing value
 578    """
 579    if line.find('=') == -1:
 580        return line
 581
 582    try:
 583        aeval = asteval.Interpreter( usersyms=SIZES, minimal=True )
 584        cfg, val = re.split("=", line)
 585        val= val.strip('\"')
 586        if re.search(r'[*+-/]|<<|SZ_+|\(([^\)]+)\)', val):
 587            newval = hex(aeval(val))
 588            print('\tExpanded expression %s to %s' % (val, newval))
 589            return cfg+'='+newval
 590    except:
 591        print('\tFailed to expand expression in %s' % line)
 592
 593    return line
 594
 595
 596### classes ###
 597class Progress:
 598
 599    """Progress Indicator"""
 600
 601    def __init__(self, total):
 602        """Create a new progress indicator.
 603
 604        Args:
 605          total: A number of defconfig files to process.
 606        """
 607        self.current = 0
 608        self.total = total
 609
 610    def inc(self):
 611        """Increment the number of processed defconfig files."""
 612
 613        self.current += 1
 614
 615    def show(self):
 616        """Display the progress."""
 617        print(' %d defconfigs out of %d\r' % (self.current, self.total), end=' ')
 618        sys.stdout.flush()
 619
 620
 621class KconfigScanner:
 622    """Kconfig scanner."""
 623
 624    def __init__(self):
 625        """Scan all the Kconfig files and create a Config object."""
 626        # Define environment variables referenced from Kconfig
 627        os.environ['srctree'] = os.getcwd()
 628        os.environ['UBOOTVERSION'] = 'dummy'
 629        os.environ['KCONFIG_OBJDIR'] = ''
 630        self.conf = kconfiglib.Kconfig()
 631
 632
 633class KconfigParser:
 634
 635    """A parser of .config and include/autoconf.mk."""
 636
 637    re_arch = re.compile(r'CONFIG_SYS_ARCH="(.*)"')
 638    re_cpu = re.compile(r'CONFIG_SYS_CPU="(.*)"')
 639
 640    def __init__(self, configs, args, build_dir):
 641        """Create a new parser.
 642
 643        Args:
 644          configs: A list of CONFIGs to move.
 645          args (Namespace): program arguments
 646          build_dir: Build directory.
 647        """
 648        self.configs = configs
 649        self.args = args
 650        self.dotconfig = os.path.join(build_dir, '.config')
 651        self.autoconf = os.path.join(build_dir, 'include', 'autoconf.mk')
 652        self.spl_autoconf = os.path.join(build_dir, 'spl', 'include',
 653                                         'autoconf.mk')
 654        self.config_autoconf = os.path.join(build_dir, AUTO_CONF_PATH)
 655        self.defconfig = os.path.join(build_dir, 'defconfig')
 656
 657    def get_arch(self):
 658        """Parse .config file and return the architecture.
 659
 660        Returns:
 661          Architecture name (e.g. 'arm').
 662        """
 663        arch = ''
 664        cpu = ''
 665        for line in read_file(self.dotconfig):
 666            m = self.re_arch.match(line)
 667            if m:
 668                arch = m.group(1)
 669                continue
 670            m = self.re_cpu.match(line)
 671            if m:
 672                cpu = m.group(1)
 673
 674        if not arch:
 675            return None
 676
 677        # fix-up for aarch64
 678        if arch == 'arm' and cpu == 'armv8':
 679            arch = 'aarch64'
 680
 681        return arch
 682
 683    def parse_one_config(self, config, dotconfig_lines, autoconf_lines):
 684        """Parse .config, defconfig, include/autoconf.mk for one config.
 685
 686        This function looks for the config options in the lines from
 687        defconfig, .config, and include/autoconf.mk in order to decide
 688        which action should be taken for this defconfig.
 689
 690        Args:
 691          config: CONFIG name to parse.
 692          dotconfig_lines: lines from the .config file.
 693          autoconf_lines: lines from the include/autoconf.mk file.
 694
 695        Returns:
 696          A tupple of the action for this defconfig and the line
 697          matched for the config.
 698        """
 699        not_set = '# %s is not set' % config
 700
 701        for line in autoconf_lines:
 702            line = line.rstrip()
 703            if line.startswith(config + '='):
 704                new_val = line
 705                break
 706        else:
 707            new_val = not_set
 708
 709        new_val = try_expand(new_val)
 710
 711        for line in dotconfig_lines:
 712            line = line.rstrip()
 713            if line.startswith(config + '=') or line == not_set:
 714                old_val = line
 715                break
 716        else:
 717            if new_val == not_set:
 718                return (ACTION_NO_ENTRY, config)
 719            else:
 720                return (ACTION_NO_ENTRY_WARN, config)
 721
 722        # If this CONFIG is neither bool nor trisate
 723        if old_val[-2:] != '=y' and old_val[-2:] != '=m' and old_val != not_set:
 724            # tools/scripts/define2mk.sed changes '1' to 'y'.
 725            # This is a problem if the CONFIG is int type.
 726            # Check the type in Kconfig and handle it correctly.
 727            if new_val[-2:] == '=y':
 728                new_val = new_val[:-1] + '1'
 729
 730        return (ACTION_NO_CHANGE if old_val == new_val else ACTION_MOVE,
 731                new_val)
 732
 733    def update_dotconfig(self):
 734        """Parse files for the config options and update the .config.
 735
 736        This function parses the generated .config and include/autoconf.mk
 737        searching the target options.
 738        Move the config option(s) to the .config as needed.
 739
 740        Args:
 741          defconfig: defconfig name.
 742
 743        Returns:
 744          Return a tuple of (updated flag, log string).
 745          The "updated flag" is True if the .config was updated, False
 746          otherwise.  The "log string" shows what happend to the .config.
 747        """
 748
 749        results = []
 750        updated = False
 751        suspicious = False
 752        rm_files = [self.config_autoconf, self.autoconf]
 753
 754        if self.args.spl:
 755            if os.path.exists(self.spl_autoconf):
 756                autoconf_path = self.spl_autoconf
 757                rm_files.append(self.spl_autoconf)
 758            else:
 759                for f in rm_files:
 760                    os.remove(f)
 761                return (updated, suspicious,
 762                        color_text(self.args.color, COLOR_BROWN,
 763                                   "SPL is not enabled.  Skipped.") + '\n')
 764        else:
 765            autoconf_path = self.autoconf
 766
 767        dotconfig_lines = read_file(self.dotconfig)
 768
 769        autoconf_lines = read_file(autoconf_path)
 770
 771        for config in self.configs:
 772            result = self.parse_one_config(config, dotconfig_lines,
 773                                           autoconf_lines)
 774            results.append(result)
 775
 776        log = ''
 777
 778        for (action, value) in results:
 779            if action == ACTION_MOVE:
 780                actlog = "Move '%s'" % value
 781                log_color = COLOR_LIGHT_GREEN
 782            elif action == ACTION_NO_ENTRY:
 783                actlog = '%s is not defined in Kconfig.  Do nothing.' % value
 784                log_color = COLOR_LIGHT_BLUE
 785            elif action == ACTION_NO_ENTRY_WARN:
 786                actlog = '%s is not defined in Kconfig (suspicious).  Do nothing.' % value
 787                log_color = COLOR_YELLOW
 788                suspicious = True
 789            elif action == ACTION_NO_CHANGE:
 790                actlog = "'%s' is the same as the define in Kconfig.  Do nothing." \
 791                         % value
 792                log_color = COLOR_LIGHT_PURPLE
 793            elif action == ACTION_SPL_NOT_EXIST:
 794                actlog = 'SPL is not enabled for this defconfig.  Skip.'
 795                log_color = COLOR_PURPLE
 796            else:
 797                sys.exit('Internal Error. This should not happen.')
 798
 799            log += color_text(self.args.color, log_color, actlog) + '\n'
 800
 801        with open(self.dotconfig, 'a', encoding='utf-8') as out:
 802            for (action, value) in results:
 803                if action == ACTION_MOVE:
 804                    out.write(value + '\n')
 805                    updated = True
 806
 807        self.results = results
 808        for f in rm_files:
 809            os.remove(f)
 810
 811        return (updated, suspicious, log)
 812
 813    def check_defconfig(self):
 814        """Check the defconfig after savedefconfig
 815
 816        Returns:
 817          Return additional log if moved CONFIGs were removed again by
 818          'make savedefconfig'.
 819        """
 820
 821        log = ''
 822
 823        defconfig_lines = read_file(self.defconfig)
 824
 825        for (action, value) in self.results:
 826            if action != ACTION_MOVE:
 827                continue
 828            if not value in defconfig_lines:
 829                log += color_text(self.args.color, COLOR_YELLOW,
 830                                  "'%s' was removed by savedefconfig.\n" %
 831                                  value)
 832
 833        return log
 834
 835
 836class DatabaseThread(threading.Thread):
 837    """This thread processes results from Slot threads.
 838
 839    It collects the data in the master config directary. There is only one
 840    result thread, and this helps to serialise the build output.
 841    """
 842    def __init__(self, config_db, db_queue):
 843        """Set up a new result thread
 844
 845        Args:
 846            builder: Builder which will be sent each result
 847        """
 848        threading.Thread.__init__(self)
 849        self.config_db = config_db
 850        self.db_queue= db_queue
 851
 852    def run(self):
 853        """Called to start up the result thread.
 854
 855        We collect the next result job and pass it on to the build.
 856        """
 857        while True:
 858            defconfig, configs = self.db_queue.get()
 859            self.config_db[defconfig] = configs
 860            self.db_queue.task_done()
 861
 862
 863class Slot:
 864
 865    """A slot to store a subprocess.
 866
 867    Each instance of this class handles one subprocess.
 868    This class is useful to control multiple threads
 869    for faster processing.
 870    """
 871
 872    def __init__(self, toolchains, configs, args, progress, devnull,
 873                 make_cmd, reference_src_dir, db_queue):
 874        """Create a new process slot.
 875
 876        Args:
 877          toolchains: Toolchains object containing toolchains.
 878          configs: A list of CONFIGs to move.
 879          args: Program arguments
 880          progress: A progress indicator.
 881          devnull: A file object of '/dev/null'.
 882          make_cmd: command name of GNU Make.
 883          reference_src_dir: Determine the true starting config state from this
 884                             source tree.
 885          db_queue: output queue to write config info for the database
 886        """
 887        self.toolchains = toolchains
 888        self.args = args
 889        self.progress = progress
 890        self.build_dir = tempfile.mkdtemp()
 891        self.devnull = devnull
 892        self.make_cmd = (make_cmd, 'O=' + self.build_dir)
 893        self.reference_src_dir = reference_src_dir
 894        self.db_queue = db_queue
 895        self.parser = KconfigParser(configs, args, self.build_dir)
 896        self.state = STATE_IDLE
 897        self.failed_boards = set()
 898        self.suspicious_boards = set()
 899
 900    def __del__(self):
 901        """Delete the working directory
 902
 903        This function makes sure the temporary directory is cleaned away
 904        even if Python suddenly dies due to error.  It should be done in here
 905        because it is guaranteed the destructor is always invoked when the
 906        instance of the class gets unreferenced.
 907
 908        If the subprocess is still running, wait until it finishes.
 909        """
 910        if self.state != STATE_IDLE:
 911            while self.ps.poll() == None:
 912                pass
 913        shutil.rmtree(self.build_dir)
 914
 915    def add(self, defconfig):
 916        """Assign a new subprocess for defconfig and add it to the slot.
 917
 918        If the slot is vacant, create a new subprocess for processing the
 919        given defconfig and add it to the slot.  Just returns False if
 920        the slot is occupied (i.e. the current subprocess is still running).
 921
 922        Args:
 923          defconfig: defconfig name.
 924
 925        Returns:
 926          Return True on success or False on failure
 927        """
 928        if self.state != STATE_IDLE:
 929            return False
 930
 931        self.defconfig = defconfig
 932        self.log = ''
 933        self.current_src_dir = self.reference_src_dir
 934        self.do_defconfig()
 935        return True
 936
 937    def poll(self):
 938        """Check the status of the subprocess and handle it as needed.
 939
 940        Returns True if the slot is vacant (i.e. in idle state).
 941        If the configuration is successfully finished, assign a new
 942        subprocess to build include/autoconf.mk.
 943        If include/autoconf.mk is generated, invoke the parser to
 944        parse the .config and the include/autoconf.mk, moving
 945        config options to the .config as needed.
 946        If the .config was updated, run "make savedefconfig" to sync
 947        it, update the original defconfig, and then set the slot back
 948        to the idle state.
 949
 950        Returns:
 951          Return True if the subprocess is terminated, False otherwise
 952        """
 953        if self.state == STATE_IDLE:
 954            return True
 955
 956        if self.ps.poll() == None:
 957            return False
 958
 959        if self.ps.poll() != 0:
 960            self.handle_error()
 961        elif self.state == STATE_DEFCONFIG:
 962            if self.reference_src_dir and not self.current_src_dir:
 963                self.do_savedefconfig()
 964            else:
 965                self.do_autoconf()
 966        elif self.state == STATE_AUTOCONF:
 967            if self.current_src_dir:
 968                self.current_src_dir = None
 969                self.do_defconfig()
 970            elif self.args.build_db:
 971                self.do_build_db()
 972            else:
 973                self.do_savedefconfig()
 974        elif self.state == STATE_SAVEDEFCONFIG:
 975            self.update_defconfig()
 976        else:
 977            sys.exit('Internal Error. This should not happen.')
 978
 979        return True if self.state == STATE_IDLE else False
 980
 981    def handle_error(self):
 982        """Handle error cases."""
 983
 984        self.log += color_text(self.args.color, COLOR_LIGHT_RED,
 985                               'Failed to process.\n')
 986        if self.args.verbose:
 987            self.log += color_text(self.args.color, COLOR_LIGHT_CYAN,
 988                                   self.ps.stderr.read().decode())
 989        self.finish(False)
 990
 991    def do_defconfig(self):
 992        """Run 'make <board>_defconfig' to create the .config file."""
 993
 994        cmd = list(self.make_cmd)
 995        cmd.append(self.defconfig)
 996        self.ps = subprocess.Popen(cmd, stdout=self.devnull,
 997                                   stderr=subprocess.PIPE,
 998                                   cwd=self.current_src_dir)
 999        self.state = STATE_DEFCONFIG
1000
1001    def do_autoconf(self):
1002        """Run 'make AUTO_CONF_PATH'."""
1003
1004        arch = self.parser.get_arch()
1005        try:
1006            toolchain = self.toolchains.Select(arch)
1007        except ValueError:
1008            self.log += color_text(self.args.color, COLOR_YELLOW,
1009                    "Tool chain for '%s' is missing.  Do nothing.\n" % arch)
1010            self.finish(False)
1011            return
1012        env = toolchain.MakeEnvironment(False)
1013
1014        cmd = list(self.make_cmd)
1015        cmd.append('KCONFIG_IGNORE_DUPLICATES=1')
1016        cmd.append(AUTO_CONF_PATH)
1017        self.ps = subprocess.Popen(cmd, stdout=self.devnull, env=env,
1018                                   stderr=subprocess.PIPE,
1019                                   cwd=self.current_src_dir)
1020        self.state = STATE_AUTOCONF
1021
1022    def do_build_db(self):
1023        """Add the board to the database"""
1024        configs = {}
1025        for line in read_file(os.path.join(self.build_dir, AUTO_CONF_PATH)):
1026            if line.startswith('CONFIG'):
1027                config, value = line.split('=', 1)
1028                configs[config] = value.rstrip()
1029        self.db_queue.put([self.defconfig, configs])
1030        self.finish(True)
1031
1032    def do_savedefconfig(self):
1033        """Update the .config and run 'make savedefconfig'."""
1034
1035        (updated, suspicious, log) = self.parser.update_dotconfig()
1036        if suspicious:
1037            self.suspicious_boards.add(self.defconfig)
1038        self.log += log
1039
1040        if not self.args.force_sync and not updated:
1041            self.finish(True)
1042            return
1043        if updated:
1044            self.log += color_text(self.args.color, COLOR_LIGHT_GREEN,
1045                                   'Syncing by savedefconfig...\n')
1046        else:
1047            self.log += 'Syncing by savedefconfig (forced by option)...\n'
1048
1049        cmd = list(self.make_cmd)
1050        cmd.append('savedefconfig')
1051        self.ps = subprocess.Popen(cmd, stdout=self.devnull,
1052                                   stderr=subprocess.PIPE)
1053        self.state = STATE_SAVEDEFCONFIG
1054
1055    def update_defconfig(self):
1056        """Update the input defconfig and go back to the idle state."""
1057
1058        log = self.parser.check_defconfig()
1059        if log:
1060            self.suspicious_boards.add(self.defconfig)
1061            self.log += log
1062        orig_defconfig = os.path.join('configs', self.defconfig)
1063        new_defconfig = os.path.join(self.build_dir, 'defconfig')
1064        updated = not filecmp.cmp(orig_defconfig, new_defconfig)
1065
1066        if updated:
1067            self.log += color_text(self.args.color, COLOR_LIGHT_BLUE,
1068                                   'defconfig was updated.\n')
1069
1070        if not self.args.dry_run and updated:
1071            shutil.move(new_defconfig, orig_defconfig)
1072        self.finish(True)
1073
1074    def finish(self, success):
1075        """Display log along with progress and go to the idle state.
1076
1077        Args:
1078          success: Should be True when the defconfig was processed
1079                   successfully, or False when it fails.
1080        """
1081        # output at least 30 characters to hide the "* defconfigs out of *".
1082        log = self.defconfig.ljust(30) + '\n'
1083
1084        log += '\n'.join([ '    ' + s for s in self.log.split('\n') ])
1085        # Some threads are running in parallel.
1086        # Print log atomically to not mix up logs from different threads.
1087        print(log, file=(sys.stdout if success else sys.stderr))
1088
1089        if not success:
1090            if self.args.exit_on_error:
1091                sys.exit('Exit on error.')
1092            # If --exit-on-error flag is not set, skip this board and continue.
1093            # Record the failed board.
1094            self.failed_boards.add(self.defconfig)
1095
1096        self.progress.inc()
1097        self.progress.show()
1098        self.state = STATE_IDLE
1099
1100    def get_failed_boards(self):
1101        """Returns a set of failed boards (defconfigs) in this slot.
1102        """
1103        return self.failed_boards
1104
1105    def get_suspicious_boards(self):
1106        """Returns a set of boards (defconfigs) with possible misconversion.
1107        """
1108        return self.suspicious_boards - self.failed_boards
1109
1110class Slots:
1111
1112    """Controller of the array of subprocess slots."""
1113
1114    def __init__(self, toolchains, configs, args, progress,
1115                 reference_src_dir, db_queue):
1116        """Create a new slots controller.
1117
1118        Args:
1119          toolchains: Toolchains object containing toolchains.
1120          configs: A list of CONFIGs to move.
1121          args: Program arguments
1122          progress: A progress indicator.
1123          reference_src_dir: Determine the true starting config state from this
1124                             source tree.
1125          db_queue: output queue to write config info for the database
1126        """
1127        self.args = args
1128        self.slots = []
1129        devnull = subprocess.DEVNULL
1130        make_cmd = get_make_cmd()
1131        for i in range(args.jobs):
1132            self.slots.append(Slot(toolchains, configs, args, progress,
1133                                   devnull, make_cmd, reference_src_dir,
1134                                   db_queue))
1135
1136    def add(self, defconfig):
1137        """Add a new subprocess if a vacant slot is found.
1138
1139        Args:
1140          defconfig: defconfig name to be put into.
1141
1142        Returns:
1143          Return True on success or False on failure
1144        """
1145        for slot in self.slots:
1146            if slot.add(defconfig):
1147                return True
1148        return False
1149
1150    def available(self):
1151        """Check if there is a vacant slot.
1152
1153        Returns:
1154          Return True if at lease one vacant slot is found, False otherwise.
1155        """
1156        for slot in self.slots:
1157            if slot.poll():
1158                return True
1159        return False
1160
1161    def empty(self):
1162        """Check if all slots are vacant.
1163
1164        Returns:
1165          Return True if all the slots are vacant, False otherwise.
1166        """
1167        ret = True
1168        for slot in self.slots:
1169            if not slot.poll():
1170                ret = False
1171        return ret
1172
1173    def show_failed_boards(self):
1174        """Display all of the failed boards (defconfigs)."""
1175        boards = set()
1176        output_file = 'moveconfig.failed'
1177
1178        for slot in self.slots:
1179            boards |= slot.get_failed_boards()
1180
1181        if boards:
1182            boards = '\n'.join(boards) + '\n'
1183            msg = 'The following boards were not processed due to error:\n'
1184            msg += boards
1185            msg += '(the list has been saved in %s)\n' % output_file
1186            print(color_text(self.args.color, COLOR_LIGHT_RED,
1187                                            msg), file=sys.stderr)
1188
1189            write_file(output_file, boards)
1190
1191    def show_suspicious_boards(self):
1192        """Display all boards (defconfigs) with possible misconversion."""
1193        boards = set()
1194        output_file = 'moveconfig.suspicious'
1195
1196        for slot in self.slots:
1197            boards |= slot.get_suspicious_boards()
1198
1199        if boards:
1200            boards = '\n'.join(boards) + '\n'
1201            msg = 'The following boards might have been converted incorrectly.\n'
1202            msg += 'It is highly recommended to check them manually:\n'
1203            msg += boards
1204            msg += '(the list has been saved in %s)\n' % output_file
1205            print(color_text(self.args.color, COLOR_YELLOW,
1206                                            msg), file=sys.stderr)
1207
1208            write_file(output_file, boards)
1209
1210class ReferenceSource:
1211
1212    """Reference source against which original configs should be parsed."""
1213
1214    def __init__(self, commit):
1215        """Create a reference source directory based on a specified commit.
1216
1217        Args:
1218          commit: commit to git-clone
1219        """
1220        self.src_dir = tempfile.mkdtemp()
1221        print('Cloning git repo to a separate work directory...')
1222        subprocess.check_output(['git', 'clone', os.getcwd(), '.'],
1223                                cwd=self.src_dir)
1224        print("Checkout '%s' to build the original autoconf.mk." % \
1225            subprocess.check_output(['git', 'rev-parse', '--short', commit]).strip())
1226        subprocess.check_output(['git', 'checkout', commit],
1227                                stderr=subprocess.STDOUT, cwd=self.src_dir)
1228
1229    def __del__(self):
1230        """Delete the reference source directory
1231
1232        This function makes sure the temporary directory is cleaned away
1233        even if Python suddenly dies due to error.  It should be done in here
1234        because it is guaranteed the destructor is always invoked when the
1235        instance of the class gets unreferenced.
1236        """
1237        shutil.rmtree(self.src_dir)
1238
1239    def get_dir(self):
1240        """Return the absolute path to the reference source directory."""
1241
1242        return self.src_dir
1243
1244def move_config(toolchains, configs, args, db_queue):
1245    """Move config options to defconfig files.
1246
1247    Args:
1248      configs: A list of CONFIGs to move.
1249      args: Program arguments
1250    """
1251    if len(configs) == 0:
1252        if args.force_sync:
1253            print('No CONFIG is specified. You are probably syncing defconfigs.', end=' ')
1254        elif args.build_db:
1255            print('Building %s database' % CONFIG_DATABASE)
1256        else:
1257            print('Neither CONFIG nor --force-sync is specified. Nothing will happen.', end=' ')
1258    else:
1259        print('Move ' + ', '.join(configs), end=' ')
1260    print('(jobs: %d)\n' % args.jobs)
1261
1262    if args.git_ref:
1263        reference_src = ReferenceSource(args.git_ref)
1264        reference_src_dir = reference_src.get_dir()
1265    else:
1266        reference_src_dir = None
1267
1268    if args.defconfigs:
1269        defconfigs = get_matched_defconfigs(args.defconfigs)
1270    else:
1271        defconfigs = get_all_defconfigs()
1272
1273    progress = Progress(len(defconfigs))
1274    slots = Slots(toolchains, configs, args, progress, reference_src_dir,
1275                  db_queue)
1276
1277    # Main loop to process defconfig files:
1278    #  Add a new subprocess into a vacant slot.
1279    #  Sleep if there is no available slot.
1280    for defconfig in defconfigs:
1281        while not slots.add(defconfig):
1282            while not slots.available():
1283                # No available slot: sleep for a while
1284                time.sleep(SLEEP_TIME)
1285
1286    # wait until all the subprocesses finish
1287    while not slots.empty():
1288        time.sleep(SLEEP_TIME)
1289
1290    print('')
1291    slots.show_failed_boards()
1292    slots.show_suspicious_boards()
1293
1294def find_kconfig_rules(kconf, config, imply_config):
1295    """Check whether a config has a 'select' or 'imply' keyword
1296
1297    Args:
1298        kconf: Kconfiglib.Kconfig object
1299        config: Name of config to check (without CONFIG_ prefix)
1300        imply_config: Implying config (without CONFIG_ prefix) which may or
1301            may not have an 'imply' for 'config')
1302
1303    Returns:
1304        Symbol object for 'config' if found, else None
1305    """
1306    sym = kconf.syms.get(imply_config)
1307    if sym:
1308        for sel, cond in (sym.selects + sym.implies):
1309            if sel.name == config:
1310                return sym
1311    return None
1312
1313def check_imply_rule(kconf, config, imply_config):
1314    """Check if we can add an 'imply' option
1315
1316    This finds imply_config in the Kconfig and looks to see if it is possible
1317    to add an 'imply' for 'config' to that part of the Kconfig.
1318
1319    Args:
1320        kconf: Kconfiglib.Kconfig object
1321        config: Name of config to check (without CONFIG_ prefix)
1322        imply_config: Implying config (without CONFIG_ prefix) which may or
1323            may not have an 'imply' for 'config')
1324
1325    Returns:
1326        tuple:
1327            filename of Kconfig file containing imply_config, or None if none
1328            line number within the Kconfig file, or 0 if none
1329            message indicating the result
1330    """
1331    sym = kconf.syms.get(imply_config)
1332    if not sym:
1333        return 'cannot find sym'
1334    nodes = sym.nodes
1335    if len(nodes) != 1:
1336        return '%d locations' % len(nodes)
1337    node = nodes[0]
1338    fname, linenum = node.filename, node.linenr
1339    cwd = os.getcwd()
1340    if cwd and fname.startswith(cwd):
1341        fname = fname[len(cwd) + 1:]
1342    file_line = ' at %s:%d' % (fname, linenum)
1343    data = read_file(fname)
1344    if data[linenum - 1] != 'config %s' % imply_config:
1345        return None, 0, 'bad sym format %s%s' % (data[linenum], file_line)
1346    return fname, linenum, 'adding%s' % file_line
1347
1348def add_imply_rule(config, fname, linenum):
1349    """Add a new 'imply' option to a Kconfig
1350
1351    Args:
1352        config: config option to add an imply for (without CONFIG_ prefix)
1353        fname: Kconfig filename to update
1354        linenum: Line number to place the 'imply' before
1355
1356    Returns:
1357        Message indicating the result
1358    """
1359    file_line = ' at %s:%d' % (fname, linenum)
1360    data = read_file(fname)
1361    linenum -= 1
1362
1363    for offset, line in enumerate(data[linenum:]):
1364        if line.strip().startswith('help') or not line:
1365            data.insert(linenum + offset, '\timply %s' % config)
1366            write_file(fname, data)
1367            return 'added%s' % file_line
1368
1369    return 'could not insert%s'
1370
1371(IMPLY_MIN_2, IMPLY_TARGET, IMPLY_CMD, IMPLY_NON_ARCH_BOARD) = (
1372    1, 2, 4, 8)
1373
1374IMPLY_FLAGS = {
1375    'min2': [IMPLY_MIN_2, 'Show options which imply >2 boards (normally >5)'],
1376    'target': [IMPLY_TARGET, 'Allow CONFIG_TARGET_... options to imply'],
1377    'cmd': [IMPLY_CMD, 'Allow CONFIG_CMD_... to imply'],
1378    'non-arch-board': [
1379        IMPLY_NON_ARCH_BOARD,
1380        'Allow Kconfig options outside arch/ and /board/ to imply'],
1381}
1382
1383
1384def read_database():
1385    """Read in the config database
1386
1387    Returns:
1388        tuple:
1389            set of all config options seen (each a str)
1390            set of all defconfigs seen (each a str)
1391            dict of configs for each defconfig:
1392                key: defconfig name, e.g. "MPC8548CDS_legacy_defconfig"
1393                value: dict:
1394                    key: CONFIG option
1395                    value: Value of option
1396            dict of defconfigs for each config:
1397                key: CONFIG option
1398                value: set of boards using that option
1399
1400    """
1401    configs = {}
1402
1403    # key is defconfig name, value is dict of (CONFIG_xxx, value)
1404    config_db = {}
1405
1406    # Set of all config options we have seen
1407    all_configs = set()
1408
1409    # Set of all defconfigs we have seen
1410    all_defconfigs = set()
1411
1412    defconfig_db = collections.defaultdict(set)
1413    for line in read_file(CONFIG_DATABASE):
1414        line = line.rstrip()
1415        if not line:  # Separator between defconfigs
1416            config_db[defconfig] = configs
1417            all_defconfigs.add(defconfig)
1418            configs = {}
1419        elif line[0] == ' ':  # CONFIG line
1420            config, value = line.strip().split('=', 1)
1421            configs[config] = value
1422            defconfig_db[config].add(defconfig)
1423            all_configs.add(config)
1424        else:  # New defconfig
1425            defconfig = line
1426
1427    return all_configs, all_defconfigs, config_db, defconfig_db
1428
1429
1430def do_imply_config(config_list, add_imply, imply_flags, skip_added,
1431                    check_kconfig=True, find_superset=False):
1432    """Find CONFIG options which imply those in the list
1433
1434    Some CONFIG options can be implied by others and this can help to reduce
1435    the size of the defconfig files. For example, CONFIG_X86 implies
1436    CONFIG_CMD_IRQ, so we can put 'imply CMD_IRQ' under 'config X86' and
1437    all x86 boards will have that option, avoiding adding CONFIG_CMD_IRQ to
1438    each of the x86 defconfig files.
1439
1440    This function uses the moveconfig database to find such options. It
1441    displays a list of things that could possibly imply those in the list.
1442    The algorithm ignores any that start with CONFIG_TARGET since these
1443    typically refer to only a few defconfigs (often one). It also does not
1444    display a config with less than 5 defconfigs.
1445
1446    The algorithm works using sets. For each target config in config_list:
1447        - Get the set 'defconfigs' which use that target config
1448        - For each config (from a list of all configs):
1449            - Get the set 'imply_defconfig' of defconfigs which use that config
1450            -
1451            - If imply_defconfigs contains anything not in defconfigs then
1452              this config does not imply the target config
1453
1454    Params:
1455        config_list: List of CONFIG options to check (each a string)
1456        add_imply: Automatically add an 'imply' for each config.
1457        imply_flags: Flags which control which implying configs are allowed
1458           (IMPLY_...)
1459        skip_added: Don't show options which already have an imply added.
1460        check_kconfig: Check if implied symbols already have an 'imply' or
1461            'select' for the target config, and show this information if so.
1462        find_superset: True to look for configs which are a superset of those
1463            already found. So for example if CONFIG_EXYNOS5 implies an option,
1464            but CONFIG_EXYNOS covers a larger set of defconfigs and also
1465            implies that option, this will drop the former in favour of the
1466            latter. In practice this option has not proved very used.
1467
1468    Note the terminoloy:
1469        config - a CONFIG_XXX options (a string, e.g. 'CONFIG_CMD_EEPROM')
1470        defconfig - a defconfig file (a string, e.g. 'configs/snow_defconfig')
1471    """
1472    kconf = KconfigScanner().conf if check_kconfig else None
1473    if add_imply and add_imply != 'all':
1474        add_imply = add_imply.split(',')
1475
1476    all_configs, all_defconfigs, config_db, defconfig_db = read_database()
1477
1478    # Work through each target config option in turn, independently
1479    for config in config_list:
1480        defconfigs = defconfig_db.get(config)
1481        if not defconfigs:
1482            print('%s not found in any defconfig' % config)
1483            continue
1484
1485        # Get the set of defconfigs without this one (since a config cannot
1486        # imply itself)
1487        non_defconfigs = all_defconfigs - defconfigs
1488        num_defconfigs = len(defconfigs)
1489        print('%s found in %d/%d defconfigs' % (config, num_defconfigs,
1490                                                len(all_configs)))
1491
1492        # This will hold the results: key=config, value=defconfigs containing it
1493        imply_configs = {}
1494        rest_configs = all_configs - set([config])
1495
1496        # Look at every possible config, except the target one
1497        for imply_config in rest_configs:
1498            if 'ERRATUM' in imply_config:
1499                continue
1500            if not imply_flags & IMPLY_CMD:
1501                if 'CONFIG_CMD' in imply_config:
1502                    continue
1503            if not imply_flags & IMPLY_TARGET:
1504                if 'CONFIG_TARGET' in imply_config:
1505                    continue
1506
1507            # Find set of defconfigs that have this config
1508            imply_defconfig = defconfig_db[imply_config]
1509
1510            # Get the intersection of this with defconfigs containing the
1511            # target config
1512            common_defconfigs = imply_defconfig & defconfigs
1513
1514            # Get the set of defconfigs containing this config which DO NOT
1515            # also contain the taret config. If this set is non-empty it means
1516            # that this config affects other defconfigs as well as (possibly)
1517            # the ones affected by the target config. This means it implies
1518            # things we don't want to imply.
1519            not_common_defconfigs = imply_defconfig & non_defconfigs
1520            if not_common_defconfigs:
1521                continue
1522
1523            # If there are common defconfigs, imply_config may be useful
1524            if common_defconfigs:
1525                skip = False
1526                if find_superset:
1527                    for prev in list(imply_configs.keys()):
1528                        prev_count = len(imply_configs[prev])
1529                        count = len(common_defconfigs)
1530                        if (prev_count > count and
1531                            (imply_configs[prev] & common_defconfigs ==
1532                            common_defconfigs)):
1533                            # skip imply_config because prev is a superset
1534                            skip = True
1535                            break
1536                        elif count > prev_count:
1537                            # delete prev because imply_config is a superset
1538                            del imply_configs[prev]
1539                if not skip:
1540                    imply_configs[imply_config] = common_defconfigs
1541
1542        # Now we have a dict imply_configs of configs which imply each config
1543        # The value of each dict item is the set of defconfigs containing that
1544        # config. Rank them so that we print the configs that imply the largest
1545        # number of defconfigs first.
1546        ranked_iconfigs = sorted(imply_configs,
1547                            key=lambda k: len(imply_configs[k]), reverse=True)
1548        kconfig_info = ''
1549        cwd = os.getcwd()
1550        add_list = collections.defaultdict(list)
1551        for iconfig in ranked_iconfigs:
1552            num_common = len(imply_configs[iconfig])
1553
1554            # Don't bother if there are less than 5 defconfigs affected.
1555            if num_common < (2 if imply_flags & IMPLY_MIN_2 else 5):
1556                continue
1557            missing = defconfigs - imply_configs[iconfig]
1558            missing_str = ', '.join(missing) if missing else 'all'
1559            missing_str = ''
1560            show = True
1561            if kconf:
1562                sym = find_kconfig_rules(kconf, config[CONFIG_LEN:],
1563                                         iconfig[CONFIG_LEN:])
1564                kconfig_info = ''
1565                if sym:
1566                    nodes = sym.nodes
1567                    if len(nodes) == 1:
1568                        fname, linenum = nodes[0].filename, nodes[0].linenr
1569                        if cwd and fname.startswith(cwd):
1570                            fname = fname[len(cwd) + 1:]
1571                        kconfig_info = '%s:%d' % (fname, linenum)
1572                        if skip_added:
1573                            show = False
1574                else:
1575                    sym = kconf.syms.get(iconfig[CONFIG_LEN:])
1576                    fname = ''
1577                    if sym:
1578                        nodes = sym.nodes
1579                        if len(nodes) == 1:
1580                            fname, linenum = nodes[0].filename, nodes[0].linenr
1581                            if cwd and fname.startswith(cwd):
1582                                fname = fname[len(cwd) + 1:]
1583                    in_arch_board = not sym or (fname.startswith('arch') or
1584                                                fname.startswith('board'))
1585                    if (not in_arch_board and
1586                        not imply_flags & IMPLY_NON_ARCH_BOARD):
1587                        continue
1588
1589                    if add_imply and (add_imply == 'all' or
1590                                      iconfig in add_imply):
1591                        fname, linenum, kconfig_info = (check_imply_rule(kconf,
1592                                config[CONFIG_LEN:], iconfig[CONFIG_LEN:]))
1593                        if fname:
1594                            add_list[fname].append(linenum)
1595
1596            if show and kconfig_info != 'skip':
1597                print('%5d : %-30s%-25s %s' % (num_common, iconfig.ljust(30),
1598                                              kconfig_info, missing_str))
1599
1600        # Having collected a list of things to add, now we add them. We process
1601        # each file from the largest line number to the smallest so that
1602        # earlier additions do not affect our line numbers. E.g. if we added an
1603        # imply at line 20 it would change the position of each line after
1604        # that.
1605        for fname, linenums in add_list.items():
1606            for linenum in sorted(linenums, reverse=True):
1607                add_imply_rule(config[CONFIG_LEN:], fname, linenum)
1608
1609def defconfig_matches(configs, re_match):
1610    """Check if any CONFIG option matches a regex
1611
1612    The match must be complete, i.e. from the start to end of the CONFIG option.
1613
1614    Args:
1615        configs (dict): Dict of CONFIG options:
1616            key: CONFIG option
1617            value: Value of option
1618        re_match (re.Pattern): Match to check
1619
1620    Returns:
1621        bool: True if any CONFIG matches the regex
1622    """
1623    for cfg in configs:
1624        m_cfg = re_match.match(cfg)
1625        if m_cfg and m_cfg.span()[1] == len(cfg):
1626            return True
1627    return False
1628
1629def do_find_config(config_list):
1630    """Find boards with a given combination of CONFIGs
1631
1632    Params:
1633        config_list: List of CONFIG options to check (each a regex consisting
1634            of a config option, with or without a CONFIG_ prefix. If an option
1635            is preceded by a tilde (~) then it must be false, otherwise it must
1636            be true)
1637    """
1638    all_configs, all_defconfigs, config_db, defconfig_db = read_database()
1639
1640    # Get the whitelist
1641    adhoc_configs = set(read_file('scripts/config_whitelist.txt'))
1642
1643    # Start with all defconfigs
1644    out = all_defconfigs
1645
1646    # Work through each config in turn
1647    adhoc = []
1648    for item in config_list:
1649        # Get the real config name and whether we want this config or not
1650        cfg = item
1651        want = True
1652        if cfg[0] == '~':
1653            want = False
1654            cfg = cfg[1:]
1655
1656        if cfg in adhoc_configs:
1657            adhoc.append(cfg)
1658            continue
1659
1660        # Search everything that is still in the running. If it has a config
1661        # that we want, or doesn't have one that we don't, add it into the
1662        # running for the next stage
1663        in_list = out
1664        out = set()
1665        re_match = re.compile(cfg)
1666        for defc in in_list:
1667            has_cfg = defconfig_matches(config_db[defc], re_match)
1668            if has_cfg == want:
1669                out.add(defc)
1670    if adhoc:
1671        print(f"Error: Not in Kconfig: %s" % ' '.join(adhoc))
1672    else:
1673        print(f'{len(out)} matches')
1674        print(' '.join([remove_defconfig(item) for item in out]))
1675
1676
1677def prefix_config(cfg):
1678    """Prefix a config with CONFIG_ if needed
1679
1680    This handles ~ operator, which indicates that the CONFIG should be disabled
1681
1682    >>> prefix_config('FRED')
1683    'CONFIG_FRED'
1684    >>> prefix_config('CONFIG_FRED')
1685    'CONFIG_FRED'
1686    >>> prefix_config('~FRED')
1687    '~CONFIG_FRED'
1688    >>> prefix_config('~CONFIG_FRED')
1689    '~CONFIG_FRED'
1690    >>> prefix_config('A123')
1691    'CONFIG_A123'
1692    """
1693    op = ''
1694    if cfg[0] == '~':
1695        op = cfg[0]
1696        cfg = cfg[1:]
1697    if not cfg.startswith('CONFIG_'):
1698        cfg = 'CONFIG_' + cfg
1699    return op + cfg
1700
1701
1702def main():
1703    try:
1704        cpu_count = multiprocessing.cpu_count()
1705    except NotImplementedError:
1706        cpu_count = 1
1707
1708    epilog = '''Move config options from headers to defconfig files. See
1709doc/develop/moveconfig.rst for documentation.'''
1710
1711    parser = ArgumentParser(epilog=epilog)
1712    # Add arguments here
1713    parser.add_argument('-a', '--add-imply', type=str, default='',
1714                      help='comma-separated list of CONFIG options to add '
1715                      "an 'imply' statement to for the CONFIG in -i")
1716    parser.add_argument('-A', '--skip-added', action='store_true', default=False,
1717                      help="don't show options which are already marked as "
1718                      'implying others')
1719    parser.add_argument('-b', '--build-db', action='store_true', default=False,
1720                      help='build a CONFIG database')
1721    parser.add_argument('-c', '--color', action='store_true', default=False,
1722                      help='display the log in color')
1723    parser.add_argument('-C', '--commit', action='store_true', default=False,
1724                      help='Create a git commit for the operation')
1725    parser.add_argument('-d', '--defconfigs', type=str,
1726                      help='a file containing a list of defconfigs to move, '
1727                      "one per line (for example 'snow_defconfig') "
1728                      "or '-' to read from stdin")
1729    parser.add_argument('-e', '--exit-on-error', action='store_true',
1730                      default=False,
1731                      help='exit immediately on any error')
1732    parser.add_argument('-f', '--find', action='store_true', default=False,
1733                      help='Find boards with a given config combination')
1734    parser.add_argument('-H', '--headers-only', dest='cleanup_headers_only',
1735                      action='store_true', default=False,
1736                      help='only cleanup the headers')
1737    parser.add_argument('-i', '--imply', action='store_true', default=False,
1738                      help='find options which imply others')
1739    parser.add_argument('-I', '--imply-flags', type=str, default='',
1740                      help="control the -i option ('help' for help")
1741    parser.add_argument('-j', '--jobs', type=int, default=cpu_count,
1742                      help='the number of jobs to run simultaneously')
1743    parser.add_argument('-n', '--dry-run', action='store_true', default=False,
1744                      help='perform a trial run (show log with no changes)')
1745    parser.add_argument('-r', '--git-ref', type=str,
1746                      help='the git ref to clone for building the autoconf.mk')
1747    parser.add_argument('-s', '--force-sync', action='store_true', default=False,
1748                      help='force sync by savedefconfig')
1749    parser.add_argument('-S', '--spl', action='store_true', default=False,
1750                      help='parse config options defined for SPL build')
1751    parser.add_argument('-t', '--test', action='store_true', default=False,
1752                      help='run unit tests')
1753    parser.add_argument('-y', '--yes', action='store_true', default=False,
1754                      help="respond 'yes' to any prompts")
1755    parser.add_argument('-v', '--verbose', action='store_true', default=False,
1756                      help='show any build errors as boards are built')
1757    parser.add_argument('configs', nargs='*')
1758
1759    args = parser.parse_args()
1760    configs = args.configs
1761
1762    if args.test:
1763        sys.argv = [sys.argv[0]]
1764        fail, count = doctest.testmod()
1765        if fail:
1766            return 1
1767        unittest.main()
1768
1769    if not any((len(configs), args.force_sync, args.build_db, args.imply,
1770                args.find)):
1771        parser.print_usage()
1772        sys.exit(1)
1773
1774    # prefix the option name with CONFIG_ if missing
1775    configs = [prefix_config(cfg) for cfg in configs]
1776
1777    check_top_directory()
1778
1779    if args.imply:
1780        imply_flags = 0
1781        if args.imply_flags == 'all':
1782            imply_flags = -1
1783
1784        elif args.imply_flags:
1785            for flag in args.imply_flags.split(','):
1786                bad = flag not in IMPLY_FLAGS
1787                if bad:
1788                    print("Invalid flag '%s'" % flag)
1789                if flag == 'help' or bad:
1790                    print("Imply flags: (separate with ',')")
1791                    for name, info in IMPLY_FLAGS.items():
1792                        print(' %-15s: %s' % (name, info[1]))
1793                    parser.print_usage()
1794                    sys.exit(1)
1795                imply_flags |= IMPLY_FLAGS[flag][0]
1796
1797        do_imply_config(configs, args.add_imply, imply_flags, args.skip_added)
1798        return
1799
1800    if args.find:
1801        do_find_config(configs)
1802        return
1803
1804    config_db = {}
1805    db_queue = queue.Queue()
1806    t = DatabaseThread(config_db, db_queue)
1807    t.setDaemon(True)
1808    t.start()
1809
1810    if not args.cleanup_headers_only:
1811        check_clean_directory()
1812        bsettings.Setup('')
1813        toolchains = toolchain.Toolchains()
1814        toolchains.GetSettings()
1815        toolchains.Scan(verbose=False)
1816        move_config(toolchains, configs, args, db_queue)
1817        db_queue.join()
1818
1819    if configs:
1820        cleanup_headers(configs, args)
1821        cleanup_extra_options(configs, args)
1822        cleanup_whitelist(configs, args)
1823        cleanup_readme(configs, args)
1824
1825    if args.commit:
1826        subprocess.call(['git', 'add', '-u'])
1827        if configs:
1828            msg = 'Convert %s %sto Kconfig' % (configs[0],
1829                    'et al ' if len(configs) > 1 else '')
1830            msg += ('\n\nThis converts the following to Kconfig:\n   %s\n' %
1831                    '\n   '.join(configs))
1832        else:
1833            msg = 'configs: Resync with savedefconfig'
1834            msg += '\n\nRsync all defconfig files using moveconfig.py'
1835        subprocess.call(['git', 'commit', '-s', '-m', msg])
1836
1837    if args.build_db:
1838        with open(CONFIG_DATABASE, 'w', encoding='utf-8') as fd:
1839            for defconfig, configs in config_db.items():
1840                fd.write('%s\n' % defconfig)
1841                for config in sorted(configs.keys()):
1842                    fd.write('   %s=%s\n' % (config, configs[config]))
1843                fd.write('\n')
1844
1845if __name__ == '__main__':
1846    sys.exit(main())
1847