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