xref: /rk3399_rockchip-uboot/tools/moveconfig.py (revision f3b8e6470cb2d599ce021e3785827a9d4e360db0)
1#!/usr/bin/env python2
2#
3# Author: Masahiro Yamada <yamada.masahiro@socionext.com>
4#
5# SPDX-License-Identifier:	GPL-2.0+
6#
7
8"""
9Move config options from headers to defconfig files.
10
11Since Kconfig was introduced to U-Boot, we have worked on moving
12config options from headers to Kconfig (defconfig).
13
14This tool intends to help this tremendous work.
15
16
17Usage
18-----
19
20First, you must edit the Kconfig to add the menu entries for the configs
21you are moving.
22
23And then run this tool giving CONFIG names you want to move.
24For example, if you want to move CONFIG_CMD_USB and CONFIG_SYS_TEXT_BASE,
25simply type as follows:
26
27  $ tools/moveconfig.py CONFIG_CMD_USB CONFIG_SYS_TEXT_BASE
28
29The tool walks through all the defconfig files and move the given CONFIGs.
30
31The log is also displayed on the terminal.
32
33The log is printed for each defconfig as follows:
34
35<defconfig_name>
36    <action1>
37    <action2>
38    <action3>
39    ...
40
41<defconfig_name> is the name of the defconfig.
42
43<action*> shows what the tool did for that defconfig.
44It looks like one of the following:
45
46 - Move 'CONFIG_... '
47   This config option was moved to the defconfig
48
49 - CONFIG_... is not defined in Kconfig.  Do nothing.
50   The entry for this CONFIG was not found in Kconfig.  The option is not
51   defined in the config header, either.  So, this case can be just skipped.
52
53 - CONFIG_... is not defined in Kconfig (suspicious).  Do nothing.
54   This option is defined in the config header, but its entry was not found
55   in Kconfig.
56   There are two common cases:
57     - You forgot to create an entry for the CONFIG before running
58       this tool, or made a typo in a CONFIG passed to this tool.
59     - The entry was hidden due to unmet 'depends on'.
60   The tool does not know if the result is reasonable, so please check it
61   manually.
62
63 - 'CONFIG_...' is the same as the define in Kconfig.  Do nothing.
64   The define in the config header matched the one in Kconfig.
65   We do not need to touch it.
66
67 - Compiler is missing.  Do nothing.
68   The compiler specified for this architecture was not found
69   in your PATH environment.
70   (If -e option is passed, the tool exits immediately.)
71
72 - Failed to process.
73   An error occurred during processing this defconfig.  Skipped.
74   (If -e option is passed, the tool exits immediately on error.)
75
76Finally, you will be asked, Clean up headers? [y/n]:
77
78If you say 'y' here, the unnecessary config defines are removed
79from the config headers (include/configs/*.h).
80It just uses the regex method, so you should not rely on it.
81Just in case, please do 'git diff' to see what happened.
82
83
84How does it work?
85-----------------
86
87This tool runs configuration and builds include/autoconf.mk for every
88defconfig.  The config options defined in Kconfig appear in the .config
89file (unless they are hidden because of unmet dependency.)
90On the other hand, the config options defined by board headers are seen
91in include/autoconf.mk.  The tool looks for the specified options in both
92of them to decide the appropriate action for the options.  If the given
93config option is found in the .config, but its value does not match the
94one from the board header, the config option in the .config is replaced
95with the define in the board header.  Then, the .config is synced by
96"make savedefconfig" and the defconfig is updated with it.
97
98For faster processing, this tool handles multi-threading.  It creates
99separate build directories where the out-of-tree build is run.  The
100temporary build directories are automatically created and deleted as
101needed.  The number of threads are chosen based on the number of the CPU
102cores of your system although you can change it via -j (--jobs) option.
103
104
105Toolchains
106----------
107
108Appropriate toolchain are necessary to generate include/autoconf.mk
109for all the architectures supported by U-Boot.  Most of them are available
110at the kernel.org site, some are not provided by kernel.org.
111
112The default per-arch CROSS_COMPILE used by this tool is specified by
113the list below, CROSS_COMPILE.  You may wish to update the list to
114use your own.  Instead of modifying the list directly, you can give
115them via environments.
116
117
118Tips and trips
119--------------
120
121To sync only X86 defconfigs:
122
123   ./tools/moveconfig.py -s -d <(grep -l X86 configs/*)
124
125or:
126
127   grep -l X86 configs/* | ./tools/moveconfig.py -s -d -
128
129To process CONFIG_CMD_FPGAD only for a subset of configs based on path match:
130
131   ls configs/{hrcon*,iocon*,strider*} | \
132       ./tools/moveconfig.py -Cy CONFIG_CMD_FPGAD -d -
133
134
135Available options
136-----------------
137
138 -c, --color
139   Surround each portion of the log with escape sequences to display it
140   in color on the terminal.
141
142 -C, --commit
143   Create a git commit with the changes when the operation is complete. A
144   standard commit message is used which may need to be edited.
145
146 -d, --defconfigs
147  Specify a file containing a list of defconfigs to move.  The defconfig
148  files can be given with shell-style wildcards. Use '-' to read from stdin.
149
150 -n, --dry-run
151   Perform a trial run that does not make any changes.  It is useful to
152   see what is going to happen before one actually runs it.
153
154 -e, --exit-on-error
155   Exit immediately if Make exits with a non-zero status while processing
156   a defconfig file.
157
158 -s, --force-sync
159   Do "make savedefconfig" forcibly for all the defconfig files.
160   If not specified, "make savedefconfig" only occurs for cases
161   where at least one CONFIG was moved.
162
163 -S, --spl
164   Look for moved config options in spl/include/autoconf.mk instead of
165   include/autoconf.mk.  This is useful for moving options for SPL build
166   because SPL related options (mostly prefixed with CONFIG_SPL_) are
167   sometimes blocked by CONFIG_SPL_BUILD ifdef conditionals.
168
169 -H, --headers-only
170   Only cleanup the headers; skip the defconfig processing
171
172 -j, --jobs
173   Specify the number of threads to run simultaneously.  If not specified,
174   the number of threads is the same as the number of CPU cores.
175
176 -r, --git-ref
177   Specify the git ref to clone for building the autoconf.mk. If unspecified
178   use the CWD. This is useful for when changes to the Kconfig affect the
179   default values and you want to capture the state of the defconfig from
180   before that change was in effect. If in doubt, specify a ref pre-Kconfig
181   changes (use HEAD if Kconfig changes are not committed). Worst case it will
182   take a bit longer to run, but will always do the right thing.
183
184 -v, --verbose
185   Show any build errors as boards are built
186
187 -y, --yes
188   Instead of prompting, automatically go ahead with all operations. This
189   includes cleaning up headers, CONFIG_SYS_EXTRA_OPTIONS, the config whitelist
190   and the README.
191
192To see the complete list of supported options, run
193
194  $ tools/moveconfig.py -h
195
196"""
197
198import copy
199import difflib
200import filecmp
201import fnmatch
202import glob
203import multiprocessing
204import optparse
205import os
206import re
207import shutil
208import subprocess
209import sys
210import tempfile
211import time
212
213SHOW_GNU_MAKE = 'scripts/show-gnu-make'
214SLEEP_TIME=0.03
215
216# Here is the list of cross-tools I use.
217# Most of them are available at kernel.org
218# (https://www.kernel.org/pub/tools/crosstool/files/bin/), except the following:
219# arc: https://github.com/foss-for-synopsys-dwc-arc-processors/toolchain/releases
220# nds32: http://osdk.andestech.com/packages/nds32le-linux-glibc-v1.tgz
221# nios2: https://sourcery.mentor.com/GNUToolchain/subscription42545
222# sh: http://sourcery.mentor.com/public/gnu_toolchain/sh-linux-gnu
223CROSS_COMPILE = {
224    'arc': 'arc-linux-',
225    'aarch64': 'aarch64-linux-',
226    'arm': 'arm-unknown-linux-gnueabi-',
227    'm68k': 'm68k-linux-',
228    'microblaze': 'microblaze-linux-',
229    'mips': 'mips-linux-',
230    'nds32': 'nds32le-linux-',
231    'nios2': 'nios2-linux-gnu-',
232    'powerpc': 'powerpc-linux-',
233    'sh': 'sh-linux-gnu-',
234    'x86': 'i386-linux-',
235    'xtensa': 'xtensa-linux-'
236}
237
238STATE_IDLE = 0
239STATE_DEFCONFIG = 1
240STATE_AUTOCONF = 2
241STATE_SAVEDEFCONFIG = 3
242
243ACTION_MOVE = 0
244ACTION_NO_ENTRY = 1
245ACTION_NO_ENTRY_WARN = 2
246ACTION_NO_CHANGE = 3
247
248COLOR_BLACK        = '0;30'
249COLOR_RED          = '0;31'
250COLOR_GREEN        = '0;32'
251COLOR_BROWN        = '0;33'
252COLOR_BLUE         = '0;34'
253COLOR_PURPLE       = '0;35'
254COLOR_CYAN         = '0;36'
255COLOR_LIGHT_GRAY   = '0;37'
256COLOR_DARK_GRAY    = '1;30'
257COLOR_LIGHT_RED    = '1;31'
258COLOR_LIGHT_GREEN  = '1;32'
259COLOR_YELLOW       = '1;33'
260COLOR_LIGHT_BLUE   = '1;34'
261COLOR_LIGHT_PURPLE = '1;35'
262COLOR_LIGHT_CYAN   = '1;36'
263COLOR_WHITE        = '1;37'
264
265AUTO_CONF_PATH = 'include/config/auto.conf'
266
267
268### helper functions ###
269def get_devnull():
270    """Get the file object of '/dev/null' device."""
271    try:
272        devnull = subprocess.DEVNULL # py3k
273    except AttributeError:
274        devnull = open(os.devnull, 'wb')
275    return devnull
276
277def check_top_directory():
278    """Exit if we are not at the top of source directory."""
279    for f in ('README', 'Licenses'):
280        if not os.path.exists(f):
281            sys.exit('Please run at the top of source directory.')
282
283def check_clean_directory():
284    """Exit if the source tree is not clean."""
285    for f in ('.config', 'include/config'):
286        if os.path.exists(f):
287            sys.exit("source tree is not clean, please run 'make mrproper'")
288
289def get_make_cmd():
290    """Get the command name of GNU Make.
291
292    U-Boot needs GNU Make for building, but the command name is not
293    necessarily "make". (for example, "gmake" on FreeBSD).
294    Returns the most appropriate command name on your system.
295    """
296    process = subprocess.Popen([SHOW_GNU_MAKE], stdout=subprocess.PIPE)
297    ret = process.communicate()
298    if process.returncode:
299        sys.exit('GNU Make not found')
300    return ret[0].rstrip()
301
302def get_matched_defconfig(line):
303    """Get the defconfig files that match a pattern
304
305    Args:
306        line: Path or filename to match, e.g. 'configs/snow_defconfig' or
307            'k2*_defconfig'. If no directory is provided, 'configs/' is
308            prepended
309
310    Returns:
311        a list of matching defconfig files
312    """
313    dirname = os.path.dirname(line)
314    if dirname:
315        pattern = line
316    else:
317        pattern = os.path.join('configs', line)
318    return glob.glob(pattern) + glob.glob(pattern + '_defconfig')
319
320def get_matched_defconfigs(defconfigs_file):
321    """Get all the defconfig files that match the patterns in a file.
322
323    Args:
324        defconfigs_file: File containing a list of defconfigs to process, or
325            '-' to read the list from stdin
326
327    Returns:
328        A list of paths to defconfig files, with no duplicates
329    """
330    defconfigs = []
331    if defconfigs_file == '-':
332        fd = sys.stdin
333        defconfigs_file = 'stdin'
334    else:
335        fd = open(defconfigs_file)
336    for i, line in enumerate(fd):
337        line = line.strip()
338        if not line:
339            continue # skip blank lines silently
340        matched = get_matched_defconfig(line)
341        if not matched:
342            print >> sys.stderr, "warning: %s:%d: no defconfig matched '%s'" % \
343                                                 (defconfigs_file, i + 1, line)
344
345        defconfigs += matched
346
347    # use set() to drop multiple matching
348    return [ defconfig[len('configs') + 1:]  for defconfig in set(defconfigs) ]
349
350def get_all_defconfigs():
351    """Get all the defconfig files under the configs/ directory."""
352    defconfigs = []
353    for (dirpath, dirnames, filenames) in os.walk('configs'):
354        dirpath = dirpath[len('configs') + 1:]
355        for filename in fnmatch.filter(filenames, '*_defconfig'):
356            defconfigs.append(os.path.join(dirpath, filename))
357
358    return defconfigs
359
360def color_text(color_enabled, color, string):
361    """Return colored string."""
362    if color_enabled:
363        # LF should not be surrounded by the escape sequence.
364        # Otherwise, additional whitespace or line-feed might be printed.
365        return '\n'.join([ '\033[' + color + 'm' + s + '\033[0m' if s else ''
366                           for s in string.split('\n') ])
367    else:
368        return string
369
370def show_diff(a, b, file_path, color_enabled):
371    """Show unidified diff.
372
373    Arguments:
374      a: A list of lines (before)
375      b: A list of lines (after)
376      file_path: Path to the file
377      color_enabled: Display the diff in color
378    """
379
380    diff = difflib.unified_diff(a, b,
381                                fromfile=os.path.join('a', file_path),
382                                tofile=os.path.join('b', file_path))
383
384    for line in diff:
385        if line[0] == '-' and line[1] != '-':
386            print color_text(color_enabled, COLOR_RED, line),
387        elif line[0] == '+' and line[1] != '+':
388            print color_text(color_enabled, COLOR_GREEN, line),
389        else:
390            print line,
391
392def update_cross_compile(color_enabled):
393    """Update per-arch CROSS_COMPILE via environment variables
394
395    The default CROSS_COMPILE values are available
396    in the CROSS_COMPILE list above.
397
398    You can override them via environment variables
399    CROSS_COMPILE_{ARCH}.
400
401    For example, if you want to override toolchain prefixes
402    for ARM and PowerPC, you can do as follows in your shell:
403
404    export CROSS_COMPILE_ARM=...
405    export CROSS_COMPILE_POWERPC=...
406
407    Then, this function checks if specified compilers really exist in your
408    PATH environment.
409    """
410    archs = []
411
412    for arch in os.listdir('arch'):
413        if os.path.exists(os.path.join('arch', arch, 'Makefile')):
414            archs.append(arch)
415
416    # arm64 is a special case
417    archs.append('aarch64')
418
419    for arch in archs:
420        env = 'CROSS_COMPILE_' + arch.upper()
421        cross_compile = os.environ.get(env)
422        if not cross_compile:
423            cross_compile = CROSS_COMPILE.get(arch, '')
424
425        for path in os.environ["PATH"].split(os.pathsep):
426            gcc_path = os.path.join(path, cross_compile + 'gcc')
427            if os.path.isfile(gcc_path) and os.access(gcc_path, os.X_OK):
428                break
429        else:
430            print >> sys.stderr, color_text(color_enabled, COLOR_YELLOW,
431                 'warning: %sgcc: not found in PATH.  %s architecture boards will be skipped'
432                                            % (cross_compile, arch))
433            cross_compile = None
434
435        CROSS_COMPILE[arch] = cross_compile
436
437def extend_matched_lines(lines, matched, pre_patterns, post_patterns, extend_pre,
438                         extend_post):
439    """Extend matched lines if desired patterns are found before/after already
440    matched lines.
441
442    Arguments:
443      lines: A list of lines handled.
444      matched: A list of line numbers that have been already matched.
445               (will be updated by this function)
446      pre_patterns: A list of regular expression that should be matched as
447                    preamble.
448      post_patterns: A list of regular expression that should be matched as
449                     postamble.
450      extend_pre: Add the line number of matched preamble to the matched list.
451      extend_post: Add the line number of matched postamble to the matched list.
452    """
453    extended_matched = []
454
455    j = matched[0]
456
457    for i in matched:
458        if i == 0 or i < j:
459            continue
460        j = i
461        while j in matched:
462            j += 1
463        if j >= len(lines):
464            break
465
466        for p in pre_patterns:
467            if p.search(lines[i - 1]):
468                break
469        else:
470            # not matched
471            continue
472
473        for p in post_patterns:
474            if p.search(lines[j]):
475                break
476        else:
477            # not matched
478            continue
479
480        if extend_pre:
481            extended_matched.append(i - 1)
482        if extend_post:
483            extended_matched.append(j)
484
485    matched += extended_matched
486    matched.sort()
487
488def confirm(options, prompt):
489    if not options.yes:
490        while True:
491            choice = raw_input('{} [y/n]: '.format(prompt))
492            choice = choice.lower()
493            print choice
494            if choice == 'y' or choice == 'n':
495                break
496
497        if choice == 'n':
498            return False
499
500    return True
501
502def cleanup_one_header(header_path, patterns, options):
503    """Clean regex-matched lines away from a file.
504
505    Arguments:
506      header_path: path to the cleaned file.
507      patterns: list of regex patterns.  Any lines matching to these
508                patterns are deleted.
509      options: option flags.
510    """
511    with open(header_path) as f:
512        lines = f.readlines()
513
514    matched = []
515    for i, line in enumerate(lines):
516        if i - 1 in matched and lines[i - 1][-2:] == '\\\n':
517            matched.append(i)
518            continue
519        for pattern in patterns:
520            if pattern.search(line):
521                matched.append(i)
522                break
523
524    if not matched:
525        return
526
527    # remove empty #ifdef ... #endif, successive blank lines
528    pattern_if = re.compile(r'#\s*if(def|ndef)?\W') #  #if, #ifdef, #ifndef
529    pattern_elif = re.compile(r'#\s*el(if|se)\W')   #  #elif, #else
530    pattern_endif = re.compile(r'#\s*endif\W')      #  #endif
531    pattern_blank = re.compile(r'^\s*$')            #  empty line
532
533    while True:
534        old_matched = copy.copy(matched)
535        extend_matched_lines(lines, matched, [pattern_if],
536                             [pattern_endif], True, True)
537        extend_matched_lines(lines, matched, [pattern_elif],
538                             [pattern_elif, pattern_endif], True, False)
539        extend_matched_lines(lines, matched, [pattern_if, pattern_elif],
540                             [pattern_blank], False, True)
541        extend_matched_lines(lines, matched, [pattern_blank],
542                             [pattern_elif, pattern_endif], True, False)
543        extend_matched_lines(lines, matched, [pattern_blank],
544                             [pattern_blank], True, False)
545        if matched == old_matched:
546            break
547
548    tolines = copy.copy(lines)
549
550    for i in reversed(matched):
551        tolines.pop(i)
552
553    show_diff(lines, tolines, header_path, options.color)
554
555    if options.dry_run:
556        return
557
558    with open(header_path, 'w') as f:
559        for line in tolines:
560            f.write(line)
561
562def cleanup_headers(configs, options):
563    """Delete config defines from board headers.
564
565    Arguments:
566      configs: A list of CONFIGs to remove.
567      options: option flags.
568    """
569    if not confirm(options, 'Clean up headers?'):
570        return
571
572    patterns = []
573    for config in configs:
574        patterns.append(re.compile(r'#\s*define\s+%s\W' % config))
575        patterns.append(re.compile(r'#\s*undef\s+%s\W' % config))
576
577    for dir in 'include', 'arch', 'board':
578        for (dirpath, dirnames, filenames) in os.walk(dir):
579            if dirpath == os.path.join('include', 'generated'):
580                continue
581            for filename in filenames:
582                if not fnmatch.fnmatch(filename, '*~'):
583                    cleanup_one_header(os.path.join(dirpath, filename),
584                                       patterns, options)
585
586def cleanup_one_extra_option(defconfig_path, configs, options):
587    """Delete config defines in CONFIG_SYS_EXTRA_OPTIONS in one defconfig file.
588
589    Arguments:
590      defconfig_path: path to the cleaned defconfig file.
591      configs: A list of CONFIGs to remove.
592      options: option flags.
593    """
594
595    start = 'CONFIG_SYS_EXTRA_OPTIONS="'
596    end = '"\n'
597
598    with open(defconfig_path) as f:
599        lines = f.readlines()
600
601    for i, line in enumerate(lines):
602        if line.startswith(start) and line.endswith(end):
603            break
604    else:
605        # CONFIG_SYS_EXTRA_OPTIONS was not found in this defconfig
606        return
607
608    old_tokens = line[len(start):-len(end)].split(',')
609    new_tokens = []
610
611    for token in old_tokens:
612        pos = token.find('=')
613        if not (token[:pos] if pos >= 0 else token) in configs:
614            new_tokens.append(token)
615
616    if new_tokens == old_tokens:
617        return
618
619    tolines = copy.copy(lines)
620
621    if new_tokens:
622        tolines[i] = start + ','.join(new_tokens) + end
623    else:
624        tolines.pop(i)
625
626    show_diff(lines, tolines, defconfig_path, options.color)
627
628    if options.dry_run:
629        return
630
631    with open(defconfig_path, 'w') as f:
632        for line in tolines:
633            f.write(line)
634
635def cleanup_extra_options(configs, options):
636    """Delete config defines in CONFIG_SYS_EXTRA_OPTIONS in defconfig files.
637
638    Arguments:
639      configs: A list of CONFIGs to remove.
640      options: option flags.
641    """
642    if not confirm(options, 'Clean up CONFIG_SYS_EXTRA_OPTIONS?'):
643        return
644
645    configs = [ config[len('CONFIG_'):] for config in configs ]
646
647    defconfigs = get_all_defconfigs()
648
649    for defconfig in defconfigs:
650        cleanup_one_extra_option(os.path.join('configs', defconfig), configs,
651                                 options)
652
653def cleanup_whitelist(configs, options):
654    """Delete config whitelist entries
655
656    Arguments:
657      configs: A list of CONFIGs to remove.
658      options: option flags.
659    """
660    if not confirm(options, 'Clean up whitelist entries?'):
661        return
662
663    with open(os.path.join('scripts', 'config_whitelist.txt')) as f:
664        lines = f.readlines()
665
666    lines = [x for x in lines if x.strip() not in configs]
667
668    with open(os.path.join('scripts', 'config_whitelist.txt'), 'w') as f:
669        f.write(''.join(lines))
670
671def find_matching(patterns, line):
672    for pat in patterns:
673        if pat.search(line):
674            return True
675    return False
676
677def cleanup_readme(configs, options):
678    """Delete config description in README
679
680    Arguments:
681      configs: A list of CONFIGs to remove.
682      options: option flags.
683    """
684    if not confirm(options, 'Clean up README?'):
685        return
686
687    patterns = []
688    for config in configs:
689        patterns.append(re.compile(r'^\s+%s' % config))
690
691    with open('README') as f:
692        lines = f.readlines()
693
694    found = False
695    newlines = []
696    for line in lines:
697        if not found:
698            found = find_matching(patterns, line)
699            if found:
700                continue
701
702        if found and re.search(r'^\s+CONFIG', line):
703            found = False
704
705        if not found:
706            newlines.append(line)
707
708    with open('README', 'w') as f:
709        f.write(''.join(newlines))
710
711
712### classes ###
713class Progress:
714
715    """Progress Indicator"""
716
717    def __init__(self, total):
718        """Create a new progress indicator.
719
720        Arguments:
721          total: A number of defconfig files to process.
722        """
723        self.current = 0
724        self.total = total
725
726    def inc(self):
727        """Increment the number of processed defconfig files."""
728
729        self.current += 1
730
731    def show(self):
732        """Display the progress."""
733        print ' %d defconfigs out of %d\r' % (self.current, self.total),
734        sys.stdout.flush()
735
736class KconfigParser:
737
738    """A parser of .config and include/autoconf.mk."""
739
740    re_arch = re.compile(r'CONFIG_SYS_ARCH="(.*)"')
741    re_cpu = re.compile(r'CONFIG_SYS_CPU="(.*)"')
742
743    def __init__(self, configs, options, build_dir):
744        """Create a new parser.
745
746        Arguments:
747          configs: A list of CONFIGs to move.
748          options: option flags.
749          build_dir: Build directory.
750        """
751        self.configs = configs
752        self.options = options
753        self.dotconfig = os.path.join(build_dir, '.config')
754        self.autoconf = os.path.join(build_dir, 'include', 'autoconf.mk')
755        self.spl_autoconf = os.path.join(build_dir, 'spl', 'include',
756                                         'autoconf.mk')
757        self.config_autoconf = os.path.join(build_dir, AUTO_CONF_PATH)
758        self.defconfig = os.path.join(build_dir, 'defconfig')
759
760    def get_cross_compile(self):
761        """Parse .config file and return CROSS_COMPILE.
762
763        Returns:
764          A string storing the compiler prefix for the architecture.
765          Return a NULL string for architectures that do not require
766          compiler prefix (Sandbox and native build is the case).
767          Return None if the specified compiler is missing in your PATH.
768          Caller should distinguish '' and None.
769        """
770        arch = ''
771        cpu = ''
772        for line in open(self.dotconfig):
773            m = self.re_arch.match(line)
774            if m:
775                arch = m.group(1)
776                continue
777            m = self.re_cpu.match(line)
778            if m:
779                cpu = m.group(1)
780
781        if not arch:
782            return None
783
784        # fix-up for aarch64
785        if arch == 'arm' and cpu == 'armv8':
786            arch = 'aarch64'
787
788        return CROSS_COMPILE.get(arch, None)
789
790    def parse_one_config(self, config, dotconfig_lines, autoconf_lines):
791        """Parse .config, defconfig, include/autoconf.mk for one config.
792
793        This function looks for the config options in the lines from
794        defconfig, .config, and include/autoconf.mk in order to decide
795        which action should be taken for this defconfig.
796
797        Arguments:
798          config: CONFIG name to parse.
799          dotconfig_lines: lines from the .config file.
800          autoconf_lines: lines from the include/autoconf.mk file.
801
802        Returns:
803          A tupple of the action for this defconfig and the line
804          matched for the config.
805        """
806        not_set = '# %s is not set' % config
807
808        for line in autoconf_lines:
809            line = line.rstrip()
810            if line.startswith(config + '='):
811                new_val = line
812                break
813        else:
814            new_val = not_set
815
816        for line in dotconfig_lines:
817            line = line.rstrip()
818            if line.startswith(config + '=') or line == not_set:
819                old_val = line
820                break
821        else:
822            if new_val == not_set:
823                return (ACTION_NO_ENTRY, config)
824            else:
825                return (ACTION_NO_ENTRY_WARN, config)
826
827        # If this CONFIG is neither bool nor trisate
828        if old_val[-2:] != '=y' and old_val[-2:] != '=m' and old_val != not_set:
829            # tools/scripts/define2mk.sed changes '1' to 'y'.
830            # This is a problem if the CONFIG is int type.
831            # Check the type in Kconfig and handle it correctly.
832            if new_val[-2:] == '=y':
833                new_val = new_val[:-1] + '1'
834
835        return (ACTION_NO_CHANGE if old_val == new_val else ACTION_MOVE,
836                new_val)
837
838    def update_dotconfig(self):
839        """Parse files for the config options and update the .config.
840
841        This function parses the generated .config and include/autoconf.mk
842        searching the target options.
843        Move the config option(s) to the .config as needed.
844
845        Arguments:
846          defconfig: defconfig name.
847
848        Returns:
849          Return a tuple of (updated flag, log string).
850          The "updated flag" is True if the .config was updated, False
851          otherwise.  The "log string" shows what happend to the .config.
852        """
853
854        results = []
855        updated = False
856        suspicious = False
857        rm_files = [self.config_autoconf, self.autoconf]
858
859        if self.options.spl:
860            if os.path.exists(self.spl_autoconf):
861                autoconf_path = self.spl_autoconf
862                rm_files.append(self.spl_autoconf)
863            else:
864                for f in rm_files:
865                    os.remove(f)
866                return (updated, suspicious,
867                        color_text(self.options.color, COLOR_BROWN,
868                                   "SPL is not enabled.  Skipped.") + '\n')
869        else:
870            autoconf_path = self.autoconf
871
872        with open(self.dotconfig) as f:
873            dotconfig_lines = f.readlines()
874
875        with open(autoconf_path) as f:
876            autoconf_lines = f.readlines()
877
878        for config in self.configs:
879            result = self.parse_one_config(config, dotconfig_lines,
880                                           autoconf_lines)
881            results.append(result)
882
883        log = ''
884
885        for (action, value) in results:
886            if action == ACTION_MOVE:
887                actlog = "Move '%s'" % value
888                log_color = COLOR_LIGHT_GREEN
889            elif action == ACTION_NO_ENTRY:
890                actlog = "%s is not defined in Kconfig.  Do nothing." % value
891                log_color = COLOR_LIGHT_BLUE
892            elif action == ACTION_NO_ENTRY_WARN:
893                actlog = "%s is not defined in Kconfig (suspicious).  Do nothing." % value
894                log_color = COLOR_YELLOW
895                suspicious = True
896            elif action == ACTION_NO_CHANGE:
897                actlog = "'%s' is the same as the define in Kconfig.  Do nothing." \
898                         % value
899                log_color = COLOR_LIGHT_PURPLE
900            elif action == ACTION_SPL_NOT_EXIST:
901                actlog = "SPL is not enabled for this defconfig.  Skip."
902                log_color = COLOR_PURPLE
903            else:
904                sys.exit("Internal Error. This should not happen.")
905
906            log += color_text(self.options.color, log_color, actlog) + '\n'
907
908        with open(self.dotconfig, 'a') as f:
909            for (action, value) in results:
910                if action == ACTION_MOVE:
911                    f.write(value + '\n')
912                    updated = True
913
914        self.results = results
915        for f in rm_files:
916            os.remove(f)
917
918        return (updated, suspicious, log)
919
920    def check_defconfig(self):
921        """Check the defconfig after savedefconfig
922
923        Returns:
924          Return additional log if moved CONFIGs were removed again by
925          'make savedefconfig'.
926        """
927
928        log = ''
929
930        with open(self.defconfig) as f:
931            defconfig_lines = f.readlines()
932
933        for (action, value) in self.results:
934            if action != ACTION_MOVE:
935                continue
936            if not value + '\n' in defconfig_lines:
937                log += color_text(self.options.color, COLOR_YELLOW,
938                                  "'%s' was removed by savedefconfig.\n" %
939                                  value)
940
941        return log
942
943class Slot:
944
945    """A slot to store a subprocess.
946
947    Each instance of this class handles one subprocess.
948    This class is useful to control multiple threads
949    for faster processing.
950    """
951
952    def __init__(self, configs, options, progress, devnull, make_cmd, reference_src_dir):
953        """Create a new process slot.
954
955        Arguments:
956          configs: A list of CONFIGs to move.
957          options: option flags.
958          progress: A progress indicator.
959          devnull: A file object of '/dev/null'.
960          make_cmd: command name of GNU Make.
961          reference_src_dir: Determine the true starting config state from this
962                             source tree.
963        """
964        self.options = options
965        self.progress = progress
966        self.build_dir = tempfile.mkdtemp()
967        self.devnull = devnull
968        self.make_cmd = (make_cmd, 'O=' + self.build_dir)
969        self.reference_src_dir = reference_src_dir
970        self.parser = KconfigParser(configs, options, self.build_dir)
971        self.state = STATE_IDLE
972        self.failed_boards = set()
973        self.suspicious_boards = set()
974
975    def __del__(self):
976        """Delete the working directory
977
978        This function makes sure the temporary directory is cleaned away
979        even if Python suddenly dies due to error.  It should be done in here
980        because it is guaranteed the destructor is always invoked when the
981        instance of the class gets unreferenced.
982
983        If the subprocess is still running, wait until it finishes.
984        """
985        if self.state != STATE_IDLE:
986            while self.ps.poll() == None:
987                pass
988        shutil.rmtree(self.build_dir)
989
990    def add(self, defconfig):
991        """Assign a new subprocess for defconfig and add it to the slot.
992
993        If the slot is vacant, create a new subprocess for processing the
994        given defconfig and add it to the slot.  Just returns False if
995        the slot is occupied (i.e. the current subprocess is still running).
996
997        Arguments:
998          defconfig: defconfig name.
999
1000        Returns:
1001          Return True on success or False on failure
1002        """
1003        if self.state != STATE_IDLE:
1004            return False
1005
1006        self.defconfig = defconfig
1007        self.log = ''
1008        self.current_src_dir = self.reference_src_dir
1009        self.do_defconfig()
1010        return True
1011
1012    def poll(self):
1013        """Check the status of the subprocess and handle it as needed.
1014
1015        Returns True if the slot is vacant (i.e. in idle state).
1016        If the configuration is successfully finished, assign a new
1017        subprocess to build include/autoconf.mk.
1018        If include/autoconf.mk is generated, invoke the parser to
1019        parse the .config and the include/autoconf.mk, moving
1020        config options to the .config as needed.
1021        If the .config was updated, run "make savedefconfig" to sync
1022        it, update the original defconfig, and then set the slot back
1023        to the idle state.
1024
1025        Returns:
1026          Return True if the subprocess is terminated, False otherwise
1027        """
1028        if self.state == STATE_IDLE:
1029            return True
1030
1031        if self.ps.poll() == None:
1032            return False
1033
1034        if self.ps.poll() != 0:
1035            self.handle_error()
1036        elif self.state == STATE_DEFCONFIG:
1037            if self.reference_src_dir and not self.current_src_dir:
1038                self.do_savedefconfig()
1039            else:
1040                self.do_autoconf()
1041        elif self.state == STATE_AUTOCONF:
1042            if self.current_src_dir:
1043                self.current_src_dir = None
1044                self.do_defconfig()
1045            else:
1046                self.do_savedefconfig()
1047        elif self.state == STATE_SAVEDEFCONFIG:
1048            self.update_defconfig()
1049        else:
1050            sys.exit("Internal Error. This should not happen.")
1051
1052        return True if self.state == STATE_IDLE else False
1053
1054    def handle_error(self):
1055        """Handle error cases."""
1056
1057        self.log += color_text(self.options.color, COLOR_LIGHT_RED,
1058                               "Failed to process.\n")
1059        if self.options.verbose:
1060            self.log += color_text(self.options.color, COLOR_LIGHT_CYAN,
1061                                   self.ps.stderr.read())
1062        self.finish(False)
1063
1064    def do_defconfig(self):
1065        """Run 'make <board>_defconfig' to create the .config file."""
1066
1067        cmd = list(self.make_cmd)
1068        cmd.append(self.defconfig)
1069        self.ps = subprocess.Popen(cmd, stdout=self.devnull,
1070                                   stderr=subprocess.PIPE,
1071                                   cwd=self.current_src_dir)
1072        self.state = STATE_DEFCONFIG
1073
1074    def do_autoconf(self):
1075        """Run 'make AUTO_CONF_PATH'."""
1076
1077        self.cross_compile = self.parser.get_cross_compile()
1078        if self.cross_compile is None:
1079            self.log += color_text(self.options.color, COLOR_YELLOW,
1080                                   "Compiler is missing.  Do nothing.\n")
1081            self.finish(False)
1082            return
1083
1084        cmd = list(self.make_cmd)
1085        if self.cross_compile:
1086            cmd.append('CROSS_COMPILE=%s' % self.cross_compile)
1087        cmd.append('KCONFIG_IGNORE_DUPLICATES=1')
1088        cmd.append(AUTO_CONF_PATH)
1089        self.ps = subprocess.Popen(cmd, stdout=self.devnull,
1090                                   stderr=subprocess.PIPE,
1091                                   cwd=self.current_src_dir)
1092        self.state = STATE_AUTOCONF
1093
1094    def do_savedefconfig(self):
1095        """Update the .config and run 'make savedefconfig'."""
1096
1097        (updated, suspicious, log) = self.parser.update_dotconfig()
1098        if suspicious:
1099            self.suspicious_boards.add(self.defconfig)
1100        self.log += log
1101
1102        if not self.options.force_sync and not updated:
1103            self.finish(True)
1104            return
1105        if updated:
1106            self.log += color_text(self.options.color, COLOR_LIGHT_GREEN,
1107                                   "Syncing by savedefconfig...\n")
1108        else:
1109            self.log += "Syncing by savedefconfig (forced by option)...\n"
1110
1111        cmd = list(self.make_cmd)
1112        cmd.append('savedefconfig')
1113        self.ps = subprocess.Popen(cmd, stdout=self.devnull,
1114                                   stderr=subprocess.PIPE)
1115        self.state = STATE_SAVEDEFCONFIG
1116
1117    def update_defconfig(self):
1118        """Update the input defconfig and go back to the idle state."""
1119
1120        log = self.parser.check_defconfig()
1121        if log:
1122            self.suspicious_boards.add(self.defconfig)
1123            self.log += log
1124        orig_defconfig = os.path.join('configs', self.defconfig)
1125        new_defconfig = os.path.join(self.build_dir, 'defconfig')
1126        updated = not filecmp.cmp(orig_defconfig, new_defconfig)
1127
1128        if updated:
1129            self.log += color_text(self.options.color, COLOR_LIGHT_BLUE,
1130                                   "defconfig was updated.\n")
1131
1132        if not self.options.dry_run and updated:
1133            shutil.move(new_defconfig, orig_defconfig)
1134        self.finish(True)
1135
1136    def finish(self, success):
1137        """Display log along with progress and go to the idle state.
1138
1139        Arguments:
1140          success: Should be True when the defconfig was processed
1141                   successfully, or False when it fails.
1142        """
1143        # output at least 30 characters to hide the "* defconfigs out of *".
1144        log = self.defconfig.ljust(30) + '\n'
1145
1146        log += '\n'.join([ '    ' + s for s in self.log.split('\n') ])
1147        # Some threads are running in parallel.
1148        # Print log atomically to not mix up logs from different threads.
1149        print >> (sys.stdout if success else sys.stderr), log
1150
1151        if not success:
1152            if self.options.exit_on_error:
1153                sys.exit("Exit on error.")
1154            # If --exit-on-error flag is not set, skip this board and continue.
1155            # Record the failed board.
1156            self.failed_boards.add(self.defconfig)
1157
1158        self.progress.inc()
1159        self.progress.show()
1160        self.state = STATE_IDLE
1161
1162    def get_failed_boards(self):
1163        """Returns a set of failed boards (defconfigs) in this slot.
1164        """
1165        return self.failed_boards
1166
1167    def get_suspicious_boards(self):
1168        """Returns a set of boards (defconfigs) with possible misconversion.
1169        """
1170        return self.suspicious_boards - self.failed_boards
1171
1172class Slots:
1173
1174    """Controller of the array of subprocess slots."""
1175
1176    def __init__(self, configs, options, progress, reference_src_dir):
1177        """Create a new slots controller.
1178
1179        Arguments:
1180          configs: A list of CONFIGs to move.
1181          options: option flags.
1182          progress: A progress indicator.
1183          reference_src_dir: Determine the true starting config state from this
1184                             source tree.
1185        """
1186        self.options = options
1187        self.slots = []
1188        devnull = get_devnull()
1189        make_cmd = get_make_cmd()
1190        for i in range(options.jobs):
1191            self.slots.append(Slot(configs, options, progress, devnull,
1192                                   make_cmd, reference_src_dir))
1193
1194    def add(self, defconfig):
1195        """Add a new subprocess if a vacant slot is found.
1196
1197        Arguments:
1198          defconfig: defconfig name to be put into.
1199
1200        Returns:
1201          Return True on success or False on failure
1202        """
1203        for slot in self.slots:
1204            if slot.add(defconfig):
1205                return True
1206        return False
1207
1208    def available(self):
1209        """Check if there is a vacant slot.
1210
1211        Returns:
1212          Return True if at lease one vacant slot is found, False otherwise.
1213        """
1214        for slot in self.slots:
1215            if slot.poll():
1216                return True
1217        return False
1218
1219    def empty(self):
1220        """Check if all slots are vacant.
1221
1222        Returns:
1223          Return True if all the slots are vacant, False otherwise.
1224        """
1225        ret = True
1226        for slot in self.slots:
1227            if not slot.poll():
1228                ret = False
1229        return ret
1230
1231    def show_failed_boards(self):
1232        """Display all of the failed boards (defconfigs)."""
1233        boards = set()
1234        output_file = 'moveconfig.failed'
1235
1236        for slot in self.slots:
1237            boards |= slot.get_failed_boards()
1238
1239        if boards:
1240            boards = '\n'.join(boards) + '\n'
1241            msg = "The following boards were not processed due to error:\n"
1242            msg += boards
1243            msg += "(the list has been saved in %s)\n" % output_file
1244            print >> sys.stderr, color_text(self.options.color, COLOR_LIGHT_RED,
1245                                            msg)
1246
1247            with open(output_file, 'w') as f:
1248                f.write(boards)
1249
1250    def show_suspicious_boards(self):
1251        """Display all boards (defconfigs) with possible misconversion."""
1252        boards = set()
1253        output_file = 'moveconfig.suspicious'
1254
1255        for slot in self.slots:
1256            boards |= slot.get_suspicious_boards()
1257
1258        if boards:
1259            boards = '\n'.join(boards) + '\n'
1260            msg = "The following boards might have been converted incorrectly.\n"
1261            msg += "It is highly recommended to check them manually:\n"
1262            msg += boards
1263            msg += "(the list has been saved in %s)\n" % output_file
1264            print >> sys.stderr, color_text(self.options.color, COLOR_YELLOW,
1265                                            msg)
1266
1267            with open(output_file, 'w') as f:
1268                f.write(boards)
1269
1270class ReferenceSource:
1271
1272    """Reference source against which original configs should be parsed."""
1273
1274    def __init__(self, commit):
1275        """Create a reference source directory based on a specified commit.
1276
1277        Arguments:
1278          commit: commit to git-clone
1279        """
1280        self.src_dir = tempfile.mkdtemp()
1281        print "Cloning git repo to a separate work directory..."
1282        subprocess.check_output(['git', 'clone', os.getcwd(), '.'],
1283                                cwd=self.src_dir)
1284        print "Checkout '%s' to build the original autoconf.mk." % \
1285            subprocess.check_output(['git', 'rev-parse', '--short', commit]).strip()
1286        subprocess.check_output(['git', 'checkout', commit],
1287                                stderr=subprocess.STDOUT, cwd=self.src_dir)
1288
1289    def __del__(self):
1290        """Delete the reference source directory
1291
1292        This function makes sure the temporary directory is cleaned away
1293        even if Python suddenly dies due to error.  It should be done in here
1294        because it is guaranteed the destructor is always invoked when the
1295        instance of the class gets unreferenced.
1296        """
1297        shutil.rmtree(self.src_dir)
1298
1299    def get_dir(self):
1300        """Return the absolute path to the reference source directory."""
1301
1302        return self.src_dir
1303
1304def move_config(configs, options):
1305    """Move config options to defconfig files.
1306
1307    Arguments:
1308      configs: A list of CONFIGs to move.
1309      options: option flags
1310    """
1311    if len(configs) == 0:
1312        if options.force_sync:
1313            print 'No CONFIG is specified. You are probably syncing defconfigs.',
1314        else:
1315            print 'Neither CONFIG nor --force-sync is specified. Nothing will happen.',
1316    else:
1317        print 'Move ' + ', '.join(configs),
1318    print '(jobs: %d)\n' % options.jobs
1319
1320    if options.git_ref:
1321        reference_src = ReferenceSource(options.git_ref)
1322        reference_src_dir = reference_src.get_dir()
1323    else:
1324        reference_src_dir = None
1325
1326    if options.defconfigs:
1327        defconfigs = get_matched_defconfigs(options.defconfigs)
1328    else:
1329        defconfigs = get_all_defconfigs()
1330
1331    progress = Progress(len(defconfigs))
1332    slots = Slots(configs, options, progress, reference_src_dir)
1333
1334    # Main loop to process defconfig files:
1335    #  Add a new subprocess into a vacant slot.
1336    #  Sleep if there is no available slot.
1337    for defconfig in defconfigs:
1338        while not slots.add(defconfig):
1339            while not slots.available():
1340                # No available slot: sleep for a while
1341                time.sleep(SLEEP_TIME)
1342
1343    # wait until all the subprocesses finish
1344    while not slots.empty():
1345        time.sleep(SLEEP_TIME)
1346
1347    print ''
1348    slots.show_failed_boards()
1349    slots.show_suspicious_boards()
1350
1351def main():
1352    try:
1353        cpu_count = multiprocessing.cpu_count()
1354    except NotImplementedError:
1355        cpu_count = 1
1356
1357    parser = optparse.OptionParser()
1358    # Add options here
1359    parser.add_option('-c', '--color', action='store_true', default=False,
1360                      help='display the log in color')
1361    parser.add_option('-C', '--commit', action='store_true', default=False,
1362                      help='Create a git commit for the operation')
1363    parser.add_option('-d', '--defconfigs', type='string',
1364                      help='a file containing a list of defconfigs to move, '
1365                      "one per line (for example 'snow_defconfig') "
1366                      "or '-' to read from stdin")
1367    parser.add_option('-n', '--dry-run', action='store_true', default=False,
1368                      help='perform a trial run (show log with no changes)')
1369    parser.add_option('-e', '--exit-on-error', action='store_true',
1370                      default=False,
1371                      help='exit immediately on any error')
1372    parser.add_option('-s', '--force-sync', action='store_true', default=False,
1373                      help='force sync by savedefconfig')
1374    parser.add_option('-S', '--spl', action='store_true', default=False,
1375                      help='parse config options defined for SPL build')
1376    parser.add_option('-H', '--headers-only', dest='cleanup_headers_only',
1377                      action='store_true', default=False,
1378                      help='only cleanup the headers')
1379    parser.add_option('-j', '--jobs', type='int', default=cpu_count,
1380                      help='the number of jobs to run simultaneously')
1381    parser.add_option('-r', '--git-ref', type='string',
1382                      help='the git ref to clone for building the autoconf.mk')
1383    parser.add_option('-y', '--yes', action='store_true', default=False,
1384                      help="respond 'yes' to any prompts")
1385    parser.add_option('-v', '--verbose', action='store_true', default=False,
1386                      help='show any build errors as boards are built')
1387    parser.usage += ' CONFIG ...'
1388
1389    (options, configs) = parser.parse_args()
1390
1391    if len(configs) == 0 and not options.force_sync:
1392        parser.print_usage()
1393        sys.exit(1)
1394
1395    # prefix the option name with CONFIG_ if missing
1396    configs = [ config if config.startswith('CONFIG_') else 'CONFIG_' + config
1397                for config in configs ]
1398
1399    check_top_directory()
1400
1401    if not options.cleanup_headers_only:
1402        check_clean_directory()
1403        update_cross_compile(options.color)
1404        move_config(configs, options)
1405
1406    if configs:
1407        cleanup_headers(configs, options)
1408        cleanup_extra_options(configs, options)
1409        cleanup_whitelist(configs, options)
1410        cleanup_readme(configs, options)
1411
1412    if options.commit:
1413        subprocess.call(['git', 'add', '-u'])
1414        if configs:
1415            msg = 'Convert %s %sto Kconfig' % (configs[0],
1416                    'et al ' if len(configs) > 1 else '')
1417            msg += ('\n\nThis converts the following to Kconfig:\n   %s\n' %
1418                    '\n   '.join(configs))
1419        else:
1420            msg = 'configs: Resync with savedefconfig'
1421            msg += '\n\nRsync all defconfig files using moveconfig.py'
1422        subprocess.call(['git', 'commit', '-s', '-m', msg])
1423
1424if __name__ == '__main__':
1425    main()
1426