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