xref: /rk3399_rockchip-uboot/tools/moveconfig.py (revision f2f6981a149572675ec7e59a33fb414440364739)
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 color_text(color_enabled, color, string):
270    """Return colored string."""
271    if color_enabled:
272        # LF should not be surrounded by the escape sequence.
273        # Otherwise, additional whitespace or line-feed might be printed.
274        return '\n'.join([ '\033[' + color + 'm' + s + '\033[0m' if s else ''
275                           for s in string.split('\n') ])
276    else:
277        return string
278
279def show_diff(a, b, file_path):
280    """Show unidified diff.
281
282    Arguments:
283      a: A list of lines (before)
284      b: A list of lines (after)
285      file_path: Path to the file
286    """
287
288    diff = difflib.unified_diff(a, b,
289                                fromfile=os.path.join('a', file_path),
290                                tofile=os.path.join('b', file_path))
291
292    for line in diff:
293        print line,
294
295def update_cross_compile(color_enabled):
296    """Update per-arch CROSS_COMPILE via environment variables
297
298    The default CROSS_COMPILE values are available
299    in the CROSS_COMPILE list above.
300
301    You can override them via environment variables
302    CROSS_COMPILE_{ARCH}.
303
304    For example, if you want to override toolchain prefixes
305    for ARM and PowerPC, you can do as follows in your shell:
306
307    export CROSS_COMPILE_ARM=...
308    export CROSS_COMPILE_POWERPC=...
309
310    Then, this function checks if specified compilers really exist in your
311    PATH environment.
312    """
313    archs = []
314
315    for arch in os.listdir('arch'):
316        if os.path.exists(os.path.join('arch', arch, 'Makefile')):
317            archs.append(arch)
318
319    # arm64 is a special case
320    archs.append('aarch64')
321
322    for arch in archs:
323        env = 'CROSS_COMPILE_' + arch.upper()
324        cross_compile = os.environ.get(env)
325        if not cross_compile:
326            cross_compile = CROSS_COMPILE.get(arch, '')
327
328        for path in os.environ["PATH"].split(os.pathsep):
329            gcc_path = os.path.join(path, cross_compile + 'gcc')
330            if os.path.isfile(gcc_path) and os.access(gcc_path, os.X_OK):
331                break
332        else:
333            print >> sys.stderr, color_text(color_enabled, COLOR_YELLOW,
334                 'warning: %sgcc: not found in PATH.  %s architecture boards will be skipped'
335                                            % (cross_compile, arch))
336            cross_compile = None
337
338        CROSS_COMPILE[arch] = cross_compile
339
340def extend_matched_lines(lines, matched, pre_patterns, post_patterns, extend_pre,
341                         extend_post):
342    """Extend matched lines if desired patterns are found before/after already
343    matched lines.
344
345    Arguments:
346      lines: A list of lines handled.
347      matched: A list of line numbers that have been already matched.
348               (will be updated by this function)
349      pre_patterns: A list of regular expression that should be matched as
350                    preamble.
351      post_patterns: A list of regular expression that should be matched as
352                     postamble.
353      extend_pre: Add the line number of matched preamble to the matched list.
354      extend_post: Add the line number of matched postamble to the matched list.
355    """
356    extended_matched = []
357
358    j = matched[0]
359
360    for i in matched:
361        if i == 0 or i < j:
362            continue
363        j = i
364        while j in matched:
365            j += 1
366        if j >= len(lines):
367            break
368
369        for p in pre_patterns:
370            if p.search(lines[i - 1]):
371                break
372        else:
373            # not matched
374            continue
375
376        for p in post_patterns:
377            if p.search(lines[j]):
378                break
379        else:
380            # not matched
381            continue
382
383        if extend_pre:
384            extended_matched.append(i - 1)
385        if extend_post:
386            extended_matched.append(j)
387
388    matched += extended_matched
389    matched.sort()
390
391def cleanup_one_header(header_path, patterns, dry_run):
392    """Clean regex-matched lines away from a file.
393
394    Arguments:
395      header_path: path to the cleaned file.
396      patterns: list of regex patterns.  Any lines matching to these
397                patterns are deleted.
398      dry_run: make no changes, but still display log.
399    """
400    with open(header_path) as f:
401        lines = f.readlines()
402
403    matched = []
404    for i, line in enumerate(lines):
405        for pattern in patterns:
406            if pattern.search(line):
407                matched.append(i)
408                break
409
410    if not matched:
411        return
412
413    # remove empty #ifdef ... #endif, successive blank lines
414    pattern_if = re.compile(r'#\s*if(def|ndef)?\W') #  #if, #ifdef, #ifndef
415    pattern_elif = re.compile(r'#\s*el(if|se)\W')   #  #elif, #else
416    pattern_endif = re.compile(r'#\s*endif\W')      #  #endif
417    pattern_blank = re.compile(r'^\s*$')            #  empty line
418
419    while True:
420        old_matched = copy.copy(matched)
421        extend_matched_lines(lines, matched, [pattern_if],
422                             [pattern_endif], True, True)
423        extend_matched_lines(lines, matched, [pattern_elif],
424                             [pattern_elif, pattern_endif], True, False)
425        extend_matched_lines(lines, matched, [pattern_if, pattern_elif],
426                             [pattern_blank], False, True)
427        extend_matched_lines(lines, matched, [pattern_blank],
428                             [pattern_elif, pattern_endif], True, False)
429        extend_matched_lines(lines, matched, [pattern_blank],
430                             [pattern_blank], True, False)
431        if matched == old_matched:
432            break
433
434    tolines = copy.copy(lines)
435
436    for i in reversed(matched):
437        tolines.pop(i)
438
439    show_diff(lines, tolines, header_path)
440
441    if dry_run:
442        return
443
444    with open(header_path, 'w') as f:
445        for line in tolines:
446            f.write(line)
447
448def cleanup_headers(configs, dry_run):
449    """Delete config defines from board headers.
450
451    Arguments:
452      configs: A list of CONFIGs to remove.
453      dry_run: make no changes, but still display log.
454    """
455    while True:
456        choice = raw_input('Clean up headers? [y/n]: ').lower()
457        print choice
458        if choice == 'y' or choice == 'n':
459            break
460
461    if choice == 'n':
462        return
463
464    patterns = []
465    for config in configs:
466        patterns.append(re.compile(r'#\s*define\s+%s\W' % config))
467        patterns.append(re.compile(r'#\s*undef\s+%s\W' % config))
468
469    for dir in 'include', 'arch', 'board':
470        for (dirpath, dirnames, filenames) in os.walk(dir):
471            if dirpath == os.path.join('include', 'generated'):
472                continue
473            for filename in filenames:
474                if not fnmatch.fnmatch(filename, '*~'):
475                    cleanup_one_header(os.path.join(dirpath, filename),
476                                       patterns, dry_run)
477
478### classes ###
479class Progress:
480
481    """Progress Indicator"""
482
483    def __init__(self, total):
484        """Create a new progress indicator.
485
486        Arguments:
487          total: A number of defconfig files to process.
488        """
489        self.current = 0
490        self.total = total
491
492    def inc(self):
493        """Increment the number of processed defconfig files."""
494
495        self.current += 1
496
497    def show(self):
498        """Display the progress."""
499        print ' %d defconfigs out of %d\r' % (self.current, self.total),
500        sys.stdout.flush()
501
502class KconfigParser:
503
504    """A parser of .config and include/autoconf.mk."""
505
506    re_arch = re.compile(r'CONFIG_SYS_ARCH="(.*)"')
507    re_cpu = re.compile(r'CONFIG_SYS_CPU="(.*)"')
508
509    def __init__(self, configs, options, build_dir):
510        """Create a new parser.
511
512        Arguments:
513          configs: A list of CONFIGs to move.
514          options: option flags.
515          build_dir: Build directory.
516        """
517        self.configs = configs
518        self.options = options
519        self.dotconfig = os.path.join(build_dir, '.config')
520        self.autoconf = os.path.join(build_dir, 'include', 'autoconf.mk')
521        self.config_autoconf = os.path.join(build_dir, 'include', 'config',
522                                            'auto.conf')
523        self.defconfig = os.path.join(build_dir, 'defconfig')
524
525    def get_cross_compile(self):
526        """Parse .config file and return CROSS_COMPILE.
527
528        Returns:
529          A string storing the compiler prefix for the architecture.
530          Return a NULL string for architectures that do not require
531          compiler prefix (Sandbox and native build is the case).
532          Return None if the specified compiler is missing in your PATH.
533          Caller should distinguish '' and None.
534        """
535        arch = ''
536        cpu = ''
537        for line in open(self.dotconfig):
538            m = self.re_arch.match(line)
539            if m:
540                arch = m.group(1)
541                continue
542            m = self.re_cpu.match(line)
543            if m:
544                cpu = m.group(1)
545
546        if not arch:
547            return None
548
549        # fix-up for aarch64
550        if arch == 'arm' and cpu == 'armv8':
551            arch = 'aarch64'
552
553        return CROSS_COMPILE.get(arch, None)
554
555    def parse_one_config(self, config, dotconfig_lines, autoconf_lines):
556        """Parse .config, defconfig, include/autoconf.mk for one config.
557
558        This function looks for the config options in the lines from
559        defconfig, .config, and include/autoconf.mk in order to decide
560        which action should be taken for this defconfig.
561
562        Arguments:
563          config: CONFIG name to parse.
564          dotconfig_lines: lines from the .config file.
565          autoconf_lines: lines from the include/autoconf.mk file.
566
567        Returns:
568          A tupple of the action for this defconfig and the line
569          matched for the config.
570        """
571        not_set = '# %s is not set' % config
572
573        for line in dotconfig_lines:
574            line = line.rstrip()
575            if line.startswith(config + '=') or line == not_set:
576                old_val = line
577                break
578        else:
579            return (ACTION_NO_ENTRY, config)
580
581        for line in autoconf_lines:
582            line = line.rstrip()
583            if line.startswith(config + '='):
584                new_val = line
585                break
586        else:
587            new_val = not_set
588
589        # If this CONFIG is neither bool nor trisate
590        if old_val[-2:] != '=y' and old_val[-2:] != '=m' and old_val != not_set:
591            # tools/scripts/define2mk.sed changes '1' to 'y'.
592            # This is a problem if the CONFIG is int type.
593            # Check the type in Kconfig and handle it correctly.
594            if new_val[-2:] == '=y':
595                new_val = new_val[:-1] + '1'
596
597        return (ACTION_NO_CHANGE if old_val == new_val else ACTION_MOVE,
598                new_val)
599
600    def update_dotconfig(self):
601        """Parse files for the config options and update the .config.
602
603        This function parses the generated .config and include/autoconf.mk
604        searching the target options.
605        Move the config option(s) to the .config as needed.
606
607        Arguments:
608          defconfig: defconfig name.
609
610        Returns:
611          Return a tuple of (updated flag, log string).
612          The "updated flag" is True if the .config was updated, False
613          otherwise.  The "log string" shows what happend to the .config.
614        """
615
616        results = []
617        updated = False
618
619        with open(self.dotconfig) as f:
620            dotconfig_lines = f.readlines()
621
622        with open(self.autoconf) as f:
623            autoconf_lines = f.readlines()
624
625        for config in self.configs:
626            result = self.parse_one_config(config, dotconfig_lines,
627                                           autoconf_lines)
628            results.append(result)
629
630        log = ''
631
632        for (action, value) in results:
633            if action == ACTION_MOVE:
634                actlog = "Move '%s'" % value
635                log_color = COLOR_LIGHT_GREEN
636            elif action == ACTION_NO_ENTRY:
637                actlog = "%s is not defined in Kconfig.  Do nothing." % value
638                log_color = COLOR_LIGHT_BLUE
639            elif action == ACTION_NO_CHANGE:
640                actlog = "'%s' is the same as the define in Kconfig.  Do nothing." \
641                         % value
642                log_color = COLOR_LIGHT_PURPLE
643            else:
644                sys.exit("Internal Error. This should not happen.")
645
646            log += color_text(self.options.color, log_color, actlog) + '\n'
647
648        with open(self.dotconfig, 'a') as f:
649            for (action, value) in results:
650                if action == ACTION_MOVE:
651                    f.write(value + '\n')
652                    updated = True
653
654        self.results = results
655        os.remove(self.config_autoconf)
656        os.remove(self.autoconf)
657
658        return (updated, log)
659
660    def check_defconfig(self):
661        """Check the defconfig after savedefconfig
662
663        Returns:
664          Return additional log if moved CONFIGs were removed again by
665          'make savedefconfig'.
666        """
667
668        log = ''
669
670        with open(self.defconfig) as f:
671            defconfig_lines = f.readlines()
672
673        for (action, value) in self.results:
674            if action != ACTION_MOVE:
675                continue
676            if not value + '\n' in defconfig_lines:
677                log += color_text(self.options.color, COLOR_YELLOW,
678                                  "'%s' was removed by savedefconfig.\n" %
679                                  value)
680
681        return log
682
683class Slot:
684
685    """A slot to store a subprocess.
686
687    Each instance of this class handles one subprocess.
688    This class is useful to control multiple threads
689    for faster processing.
690    """
691
692    def __init__(self, configs, options, progress, devnull, make_cmd, reference_src_dir):
693        """Create a new process slot.
694
695        Arguments:
696          configs: A list of CONFIGs to move.
697          options: option flags.
698          progress: A progress indicator.
699          devnull: A file object of '/dev/null'.
700          make_cmd: command name of GNU Make.
701          reference_src_dir: Determine the true starting config state from this
702                             source tree.
703        """
704        self.options = options
705        self.progress = progress
706        self.build_dir = tempfile.mkdtemp()
707        self.devnull = devnull
708        self.make_cmd = (make_cmd, 'O=' + self.build_dir)
709        self.reference_src_dir = reference_src_dir
710        self.parser = KconfigParser(configs, options, self.build_dir)
711        self.state = STATE_IDLE
712        self.failed_boards = []
713        self.suspicious_boards = []
714
715    def __del__(self):
716        """Delete the working directory
717
718        This function makes sure the temporary directory is cleaned away
719        even if Python suddenly dies due to error.  It should be done in here
720        because it is guaranteed the destructor is always invoked when the
721        instance of the class gets unreferenced.
722
723        If the subprocess is still running, wait until it finishes.
724        """
725        if self.state != STATE_IDLE:
726            while self.ps.poll() == None:
727                pass
728        shutil.rmtree(self.build_dir)
729
730    def add(self, defconfig):
731        """Assign a new subprocess for defconfig and add it to the slot.
732
733        If the slot is vacant, create a new subprocess for processing the
734        given defconfig and add it to the slot.  Just returns False if
735        the slot is occupied (i.e. the current subprocess is still running).
736
737        Arguments:
738          defconfig: defconfig name.
739
740        Returns:
741          Return True on success or False on failure
742        """
743        if self.state != STATE_IDLE:
744            return False
745
746        self.defconfig = defconfig
747        self.log = ''
748        self.current_src_dir = self.reference_src_dir
749        self.do_defconfig()
750        return True
751
752    def poll(self):
753        """Check the status of the subprocess and handle it as needed.
754
755        Returns True if the slot is vacant (i.e. in idle state).
756        If the configuration is successfully finished, assign a new
757        subprocess to build include/autoconf.mk.
758        If include/autoconf.mk is generated, invoke the parser to
759        parse the .config and the include/autoconf.mk, moving
760        config options to the .config as needed.
761        If the .config was updated, run "make savedefconfig" to sync
762        it, update the original defconfig, and then set the slot back
763        to the idle state.
764
765        Returns:
766          Return True if the subprocess is terminated, False otherwise
767        """
768        if self.state == STATE_IDLE:
769            return True
770
771        if self.ps.poll() == None:
772            return False
773
774        if self.ps.poll() != 0:
775            self.handle_error()
776        elif self.state == STATE_DEFCONFIG:
777            if self.reference_src_dir and not self.current_src_dir:
778                self.do_savedefconfig()
779            else:
780                self.do_autoconf()
781        elif self.state == STATE_AUTOCONF:
782            if self.current_src_dir:
783                self.current_src_dir = None
784                self.do_defconfig()
785            else:
786                self.do_savedefconfig()
787        elif self.state == STATE_SAVEDEFCONFIG:
788            self.update_defconfig()
789        else:
790            sys.exit("Internal Error. This should not happen.")
791
792        return True if self.state == STATE_IDLE else False
793
794    def handle_error(self):
795        """Handle error cases."""
796
797        self.log += color_text(self.options.color, COLOR_LIGHT_RED,
798                               "Failed to process.\n")
799        if self.options.verbose:
800            self.log += color_text(self.options.color, COLOR_LIGHT_CYAN,
801                                   self.ps.stderr.read())
802        self.finish(False)
803
804    def do_defconfig(self):
805        """Run 'make <board>_defconfig' to create the .config file."""
806
807        cmd = list(self.make_cmd)
808        cmd.append(self.defconfig)
809        self.ps = subprocess.Popen(cmd, stdout=self.devnull,
810                                   stderr=subprocess.PIPE,
811                                   cwd=self.current_src_dir)
812        self.state = STATE_DEFCONFIG
813
814    def do_autoconf(self):
815        """Run 'make include/config/auto.conf'."""
816
817        self.cross_compile = self.parser.get_cross_compile()
818        if self.cross_compile is None:
819            self.log += color_text(self.options.color, COLOR_YELLOW,
820                                   "Compiler is missing.  Do nothing.\n")
821            self.finish(False)
822            return
823
824        cmd = list(self.make_cmd)
825        if self.cross_compile:
826            cmd.append('CROSS_COMPILE=%s' % self.cross_compile)
827        cmd.append('KCONFIG_IGNORE_DUPLICATES=1')
828        cmd.append('include/config/auto.conf')
829        self.ps = subprocess.Popen(cmd, stdout=self.devnull,
830                                   stderr=subprocess.PIPE,
831                                   cwd=self.current_src_dir)
832        self.state = STATE_AUTOCONF
833
834    def do_savedefconfig(self):
835        """Update the .config and run 'make savedefconfig'."""
836
837        (updated, log) = self.parser.update_dotconfig()
838        self.log += log
839
840        if not self.options.force_sync and not updated:
841            self.finish(True)
842            return
843        if updated:
844            self.log += color_text(self.options.color, COLOR_LIGHT_GREEN,
845                                   "Syncing by savedefconfig...\n")
846        else:
847            self.log += "Syncing by savedefconfig (forced by option)...\n"
848
849        cmd = list(self.make_cmd)
850        cmd.append('savedefconfig')
851        self.ps = subprocess.Popen(cmd, stdout=self.devnull,
852                                   stderr=subprocess.PIPE)
853        self.state = STATE_SAVEDEFCONFIG
854
855    def update_defconfig(self):
856        """Update the input defconfig and go back to the idle state."""
857
858        log = self.parser.check_defconfig()
859        if log:
860            self.suspicious_boards.append(self.defconfig)
861            self.log += log
862        orig_defconfig = os.path.join('configs', self.defconfig)
863        new_defconfig = os.path.join(self.build_dir, 'defconfig')
864        updated = not filecmp.cmp(orig_defconfig, new_defconfig)
865
866        if updated:
867            self.log += color_text(self.options.color, COLOR_LIGHT_BLUE,
868                                   "defconfig was updated.\n")
869
870        if not self.options.dry_run and updated:
871            shutil.move(new_defconfig, orig_defconfig)
872        self.finish(True)
873
874    def finish(self, success):
875        """Display log along with progress and go to the idle state.
876
877        Arguments:
878          success: Should be True when the defconfig was processed
879                   successfully, or False when it fails.
880        """
881        # output at least 30 characters to hide the "* defconfigs out of *".
882        log = self.defconfig.ljust(30) + '\n'
883
884        log += '\n'.join([ '    ' + s for s in self.log.split('\n') ])
885        # Some threads are running in parallel.
886        # Print log atomically to not mix up logs from different threads.
887        print >> (sys.stdout if success else sys.stderr), log
888
889        if not success:
890            if self.options.exit_on_error:
891                sys.exit("Exit on error.")
892            # If --exit-on-error flag is not set, skip this board and continue.
893            # Record the failed board.
894            self.failed_boards.append(self.defconfig)
895
896        self.progress.inc()
897        self.progress.show()
898        self.state = STATE_IDLE
899
900    def get_failed_boards(self):
901        """Returns a list of failed boards (defconfigs) in this slot.
902        """
903        return self.failed_boards
904
905    def get_suspicious_boards(self):
906        """Returns a list of boards (defconfigs) with possible misconversion.
907        """
908        return self.suspicious_boards
909
910class Slots:
911
912    """Controller of the array of subprocess slots."""
913
914    def __init__(self, configs, options, progress, reference_src_dir):
915        """Create a new slots controller.
916
917        Arguments:
918          configs: A list of CONFIGs to move.
919          options: option flags.
920          progress: A progress indicator.
921          reference_src_dir: Determine the true starting config state from this
922                             source tree.
923        """
924        self.options = options
925        self.slots = []
926        devnull = get_devnull()
927        make_cmd = get_make_cmd()
928        for i in range(options.jobs):
929            self.slots.append(Slot(configs, options, progress, devnull,
930                                   make_cmd, reference_src_dir))
931
932    def add(self, defconfig):
933        """Add a new subprocess if a vacant slot is found.
934
935        Arguments:
936          defconfig: defconfig name to be put into.
937
938        Returns:
939          Return True on success or False on failure
940        """
941        for slot in self.slots:
942            if slot.add(defconfig):
943                return True
944        return False
945
946    def available(self):
947        """Check if there is a vacant slot.
948
949        Returns:
950          Return True if at lease one vacant slot is found, False otherwise.
951        """
952        for slot in self.slots:
953            if slot.poll():
954                return True
955        return False
956
957    def empty(self):
958        """Check if all slots are vacant.
959
960        Returns:
961          Return True if all the slots are vacant, False otherwise.
962        """
963        ret = True
964        for slot in self.slots:
965            if not slot.poll():
966                ret = False
967        return ret
968
969    def show_failed_boards(self):
970        """Display all of the failed boards (defconfigs)."""
971        boards = []
972        output_file = 'moveconfig.failed'
973
974        for slot in self.slots:
975            boards += slot.get_failed_boards()
976
977        if boards:
978            boards = '\n'.join(boards) + '\n'
979            msg = "The following boards were not processed due to error:\n"
980            msg += boards
981            msg += "(the list has been saved in %s)\n" % output_file
982            print >> sys.stderr, color_text(self.options.color, COLOR_LIGHT_RED,
983                                            msg)
984
985            with open(output_file, 'w') as f:
986                f.write(boards)
987
988    def show_suspicious_boards(self):
989        """Display all boards (defconfigs) with possible misconversion."""
990        boards = []
991        output_file = 'moveconfig.suspicious'
992
993        for slot in self.slots:
994            boards += slot.get_suspicious_boards()
995
996        if boards:
997            boards = '\n'.join(boards) + '\n'
998            msg = "The following boards might have been converted incorrectly.\n"
999            msg += "It is highly recommended to check them manually:\n"
1000            msg += boards
1001            msg += "(the list has been saved in %s)\n" % output_file
1002            print >> sys.stderr, color_text(self.options.color, COLOR_YELLOW,
1003                                            msg)
1004
1005            with open(output_file, 'w') as f:
1006                f.write(boards)
1007
1008class ReferenceSource:
1009
1010    """Reference source against which original configs should be parsed."""
1011
1012    def __init__(self, commit):
1013        """Create a reference source directory based on a specified commit.
1014
1015        Arguments:
1016          commit: commit to git-clone
1017        """
1018        self.src_dir = tempfile.mkdtemp()
1019        print "Cloning git repo to a separate work directory..."
1020        subprocess.check_output(['git', 'clone', os.getcwd(), '.'],
1021                                cwd=self.src_dir)
1022        print "Checkout '%s' to build the original autoconf.mk." % \
1023            subprocess.check_output(['git', 'rev-parse', '--short', commit]).strip()
1024        subprocess.check_output(['git', 'checkout', commit],
1025                                stderr=subprocess.STDOUT, cwd=self.src_dir)
1026
1027    def __del__(self):
1028        """Delete the reference source directory
1029
1030        This function makes sure the temporary directory is cleaned away
1031        even if Python suddenly dies due to error.  It should be done in here
1032        because it is guaranteed the destructor is always invoked when the
1033        instance of the class gets unreferenced.
1034        """
1035        shutil.rmtree(self.src_dir)
1036
1037    def get_dir(self):
1038        """Return the absolute path to the reference source directory."""
1039
1040        return self.src_dir
1041
1042def move_config(configs, options):
1043    """Move config options to defconfig files.
1044
1045    Arguments:
1046      configs: A list of CONFIGs to move.
1047      options: option flags
1048    """
1049    if len(configs) == 0:
1050        if options.force_sync:
1051            print 'No CONFIG is specified. You are probably syncing defconfigs.',
1052        else:
1053            print 'Neither CONFIG nor --force-sync is specified. Nothing will happen.',
1054    else:
1055        print 'Move ' + ', '.join(configs),
1056    print '(jobs: %d)\n' % options.jobs
1057
1058    if options.git_ref:
1059        reference_src = ReferenceSource(options.git_ref)
1060        reference_src_dir = reference_src.get_dir()
1061    else:
1062        reference_src_dir = None
1063
1064    if options.defconfigs:
1065        defconfigs = [line.strip() for line in open(options.defconfigs)]
1066        for i, defconfig in enumerate(defconfigs):
1067            if not defconfig.endswith('_defconfig'):
1068                defconfigs[i] = defconfig + '_defconfig'
1069            if not os.path.exists(os.path.join('configs', defconfigs[i])):
1070                sys.exit('%s - defconfig does not exist. Stopping.' %
1071                         defconfigs[i])
1072    else:
1073        # All the defconfig files to be processed
1074        defconfigs = []
1075        for (dirpath, dirnames, filenames) in os.walk('configs'):
1076            dirpath = dirpath[len('configs') + 1:]
1077            for filename in fnmatch.filter(filenames, '*_defconfig'):
1078                defconfigs.append(os.path.join(dirpath, filename))
1079
1080    progress = Progress(len(defconfigs))
1081    slots = Slots(configs, options, progress, reference_src_dir)
1082
1083    # Main loop to process defconfig files:
1084    #  Add a new subprocess into a vacant slot.
1085    #  Sleep if there is no available slot.
1086    for defconfig in defconfigs:
1087        while not slots.add(defconfig):
1088            while not slots.available():
1089                # No available slot: sleep for a while
1090                time.sleep(SLEEP_TIME)
1091
1092    # wait until all the subprocesses finish
1093    while not slots.empty():
1094        time.sleep(SLEEP_TIME)
1095
1096    print ''
1097    slots.show_failed_boards()
1098    slots.show_suspicious_boards()
1099
1100def main():
1101    try:
1102        cpu_count = multiprocessing.cpu_count()
1103    except NotImplementedError:
1104        cpu_count = 1
1105
1106    parser = optparse.OptionParser()
1107    # Add options here
1108    parser.add_option('-c', '--color', action='store_true', default=False,
1109                      help='display the log in color')
1110    parser.add_option('-d', '--defconfigs', type='string',
1111                      help='a file containing a list of defconfigs to move')
1112    parser.add_option('-n', '--dry-run', action='store_true', default=False,
1113                      help='perform a trial run (show log with no changes)')
1114    parser.add_option('-e', '--exit-on-error', action='store_true',
1115                      default=False,
1116                      help='exit immediately on any error')
1117    parser.add_option('-s', '--force-sync', action='store_true', default=False,
1118                      help='force sync by savedefconfig')
1119    parser.add_option('-H', '--headers-only', dest='cleanup_headers_only',
1120                      action='store_true', default=False,
1121                      help='only cleanup the headers')
1122    parser.add_option('-j', '--jobs', type='int', default=cpu_count,
1123                      help='the number of jobs to run simultaneously')
1124    parser.add_option('-r', '--git-ref', type='string',
1125                      help='the git ref to clone for building the autoconf.mk')
1126    parser.add_option('-v', '--verbose', action='store_true', default=False,
1127                      help='show any build errors as boards are built')
1128    parser.usage += ' CONFIG ...'
1129
1130    (options, configs) = parser.parse_args()
1131
1132    if len(configs) == 0 and not options.force_sync:
1133        parser.print_usage()
1134        sys.exit(1)
1135
1136    # prefix the option name with CONFIG_ if missing
1137    configs = [ config if config.startswith('CONFIG_') else 'CONFIG_' + config
1138                for config in configs ]
1139
1140    check_top_directory()
1141
1142    if not options.cleanup_headers_only:
1143        check_clean_directory()
1144        update_cross_compile(options.color)
1145        move_config(configs, options)
1146
1147    if configs:
1148        cleanup_headers(configs, options.dry_run)
1149
1150if __name__ == '__main__':
1151    main()
1152