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