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