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