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