]> git.sur5r.net Git - u-boot/blob - tools/moveconfig.py
tools: moveconfig: fix cleanup of defines across multiple lines
[u-boot] / 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         if i - 1 in matched and lines[i - 1][-2:] == '\\\n':
412             matched.append(i)
413             continue
414         for pattern in patterns:
415             if pattern.search(line):
416                 matched.append(i)
417                 break
418
419     if not matched:
420         return
421
422     # remove empty #ifdef ... #endif, successive blank lines
423     pattern_if = re.compile(r'#\s*if(def|ndef)?\W') #  #if, #ifdef, #ifndef
424     pattern_elif = re.compile(r'#\s*el(if|se)\W')   #  #elif, #else
425     pattern_endif = re.compile(r'#\s*endif\W')      #  #endif
426     pattern_blank = re.compile(r'^\s*$')            #  empty line
427
428     while True:
429         old_matched = copy.copy(matched)
430         extend_matched_lines(lines, matched, [pattern_if],
431                              [pattern_endif], True, True)
432         extend_matched_lines(lines, matched, [pattern_elif],
433                              [pattern_elif, pattern_endif], True, False)
434         extend_matched_lines(lines, matched, [pattern_if, pattern_elif],
435                              [pattern_blank], False, True)
436         extend_matched_lines(lines, matched, [pattern_blank],
437                              [pattern_elif, pattern_endif], True, False)
438         extend_matched_lines(lines, matched, [pattern_blank],
439                              [pattern_blank], True, False)
440         if matched == old_matched:
441             break
442
443     tolines = copy.copy(lines)
444
445     for i in reversed(matched):
446         tolines.pop(i)
447
448     show_diff(lines, tolines, header_path, options.color)
449
450     if options.dry_run:
451         return
452
453     with open(header_path, 'w') as f:
454         for line in tolines:
455             f.write(line)
456
457 def cleanup_headers(configs, options):
458     """Delete config defines from board headers.
459
460     Arguments:
461       configs: A list of CONFIGs to remove.
462       options: option flags.
463     """
464     while True:
465         choice = raw_input('Clean up headers? [y/n]: ').lower()
466         print choice
467         if choice == 'y' or choice == 'n':
468             break
469
470     if choice == 'n':
471         return
472
473     patterns = []
474     for config in configs:
475         patterns.append(re.compile(r'#\s*define\s+%s\W' % config))
476         patterns.append(re.compile(r'#\s*undef\s+%s\W' % config))
477
478     for dir in 'include', 'arch', 'board':
479         for (dirpath, dirnames, filenames) in os.walk(dir):
480             if dirpath == os.path.join('include', 'generated'):
481                 continue
482             for filename in filenames:
483                 if not fnmatch.fnmatch(filename, '*~'):
484                     cleanup_one_header(os.path.join(dirpath, filename),
485                                        patterns, options)
486
487 ### classes ###
488 class Progress:
489
490     """Progress Indicator"""
491
492     def __init__(self, total):
493         """Create a new progress indicator.
494
495         Arguments:
496           total: A number of defconfig files to process.
497         """
498         self.current = 0
499         self.total = total
500
501     def inc(self):
502         """Increment the number of processed defconfig files."""
503
504         self.current += 1
505
506     def show(self):
507         """Display the progress."""
508         print ' %d defconfigs out of %d\r' % (self.current, self.total),
509         sys.stdout.flush()
510
511 class KconfigParser:
512
513     """A parser of .config and include/autoconf.mk."""
514
515     re_arch = re.compile(r'CONFIG_SYS_ARCH="(.*)"')
516     re_cpu = re.compile(r'CONFIG_SYS_CPU="(.*)"')
517
518     def __init__(self, configs, options, build_dir):
519         """Create a new parser.
520
521         Arguments:
522           configs: A list of CONFIGs to move.
523           options: option flags.
524           build_dir: Build directory.
525         """
526         self.configs = configs
527         self.options = options
528         self.dotconfig = os.path.join(build_dir, '.config')
529         self.autoconf = os.path.join(build_dir, 'include', 'autoconf.mk')
530         self.config_autoconf = os.path.join(build_dir, 'include', 'config',
531                                             'auto.conf')
532         self.defconfig = os.path.join(build_dir, 'defconfig')
533
534     def get_cross_compile(self):
535         """Parse .config file and return CROSS_COMPILE.
536
537         Returns:
538           A string storing the compiler prefix for the architecture.
539           Return a NULL string for architectures that do not require
540           compiler prefix (Sandbox and native build is the case).
541           Return None if the specified compiler is missing in your PATH.
542           Caller should distinguish '' and None.
543         """
544         arch = ''
545         cpu = ''
546         for line in open(self.dotconfig):
547             m = self.re_arch.match(line)
548             if m:
549                 arch = m.group(1)
550                 continue
551             m = self.re_cpu.match(line)
552             if m:
553                 cpu = m.group(1)
554
555         if not arch:
556             return None
557
558         # fix-up for aarch64
559         if arch == 'arm' and cpu == 'armv8':
560             arch = 'aarch64'
561
562         return CROSS_COMPILE.get(arch, None)
563
564     def parse_one_config(self, config, dotconfig_lines, autoconf_lines):
565         """Parse .config, defconfig, include/autoconf.mk for one config.
566
567         This function looks for the config options in the lines from
568         defconfig, .config, and include/autoconf.mk in order to decide
569         which action should be taken for this defconfig.
570
571         Arguments:
572           config: CONFIG name to parse.
573           dotconfig_lines: lines from the .config file.
574           autoconf_lines: lines from the include/autoconf.mk file.
575
576         Returns:
577           A tupple of the action for this defconfig and the line
578           matched for the config.
579         """
580         not_set = '# %s is not set' % config
581
582         for line in dotconfig_lines:
583             line = line.rstrip()
584             if line.startswith(config + '=') or line == not_set:
585                 old_val = line
586                 break
587         else:
588             return (ACTION_NO_ENTRY, config)
589
590         for line in autoconf_lines:
591             line = line.rstrip()
592             if line.startswith(config + '='):
593                 new_val = line
594                 break
595         else:
596             new_val = not_set
597
598         # If this CONFIG is neither bool nor trisate
599         if old_val[-2:] != '=y' and old_val[-2:] != '=m' and old_val != not_set:
600             # tools/scripts/define2mk.sed changes '1' to 'y'.
601             # This is a problem if the CONFIG is int type.
602             # Check the type in Kconfig and handle it correctly.
603             if new_val[-2:] == '=y':
604                 new_val = new_val[:-1] + '1'
605
606         return (ACTION_NO_CHANGE if old_val == new_val else ACTION_MOVE,
607                 new_val)
608
609     def update_dotconfig(self):
610         """Parse files for the config options and update the .config.
611
612         This function parses the generated .config and include/autoconf.mk
613         searching the target options.
614         Move the config option(s) to the .config as needed.
615
616         Arguments:
617           defconfig: defconfig name.
618
619         Returns:
620           Return a tuple of (updated flag, log string).
621           The "updated flag" is True if the .config was updated, False
622           otherwise.  The "log string" shows what happend to the .config.
623         """
624
625         results = []
626         updated = False
627
628         with open(self.dotconfig) as f:
629             dotconfig_lines = f.readlines()
630
631         with open(self.autoconf) as f:
632             autoconf_lines = f.readlines()
633
634         for config in self.configs:
635             result = self.parse_one_config(config, dotconfig_lines,
636                                            autoconf_lines)
637             results.append(result)
638
639         log = ''
640
641         for (action, value) in results:
642             if action == ACTION_MOVE:
643                 actlog = "Move '%s'" % value
644                 log_color = COLOR_LIGHT_GREEN
645             elif action == ACTION_NO_ENTRY:
646                 actlog = "%s is not defined in Kconfig.  Do nothing." % value
647                 log_color = COLOR_LIGHT_BLUE
648             elif action == ACTION_NO_CHANGE:
649                 actlog = "'%s' is the same as the define in Kconfig.  Do nothing." \
650                          % value
651                 log_color = COLOR_LIGHT_PURPLE
652             else:
653                 sys.exit("Internal Error. This should not happen.")
654
655             log += color_text(self.options.color, log_color, actlog) + '\n'
656
657         with open(self.dotconfig, 'a') as f:
658             for (action, value) in results:
659                 if action == ACTION_MOVE:
660                     f.write(value + '\n')
661                     updated = True
662
663         self.results = results
664         os.remove(self.config_autoconf)
665         os.remove(self.autoconf)
666
667         return (updated, log)
668
669     def check_defconfig(self):
670         """Check the defconfig after savedefconfig
671
672         Returns:
673           Return additional log if moved CONFIGs were removed again by
674           'make savedefconfig'.
675         """
676
677         log = ''
678
679         with open(self.defconfig) as f:
680             defconfig_lines = f.readlines()
681
682         for (action, value) in self.results:
683             if action != ACTION_MOVE:
684                 continue
685             if not value + '\n' in defconfig_lines:
686                 log += color_text(self.options.color, COLOR_YELLOW,
687                                   "'%s' was removed by savedefconfig.\n" %
688                                   value)
689
690         return log
691
692 class Slot:
693
694     """A slot to store a subprocess.
695
696     Each instance of this class handles one subprocess.
697     This class is useful to control multiple threads
698     for faster processing.
699     """
700
701     def __init__(self, configs, options, progress, devnull, make_cmd, reference_src_dir):
702         """Create a new process slot.
703
704         Arguments:
705           configs: A list of CONFIGs to move.
706           options: option flags.
707           progress: A progress indicator.
708           devnull: A file object of '/dev/null'.
709           make_cmd: command name of GNU Make.
710           reference_src_dir: Determine the true starting config state from this
711                              source tree.
712         """
713         self.options = options
714         self.progress = progress
715         self.build_dir = tempfile.mkdtemp()
716         self.devnull = devnull
717         self.make_cmd = (make_cmd, 'O=' + self.build_dir)
718         self.reference_src_dir = reference_src_dir
719         self.parser = KconfigParser(configs, options, self.build_dir)
720         self.state = STATE_IDLE
721         self.failed_boards = []
722         self.suspicious_boards = []
723
724     def __del__(self):
725         """Delete the working directory
726
727         This function makes sure the temporary directory is cleaned away
728         even if Python suddenly dies due to error.  It should be done in here
729         because it is guaranteed the destructor is always invoked when the
730         instance of the class gets unreferenced.
731
732         If the subprocess is still running, wait until it finishes.
733         """
734         if self.state != STATE_IDLE:
735             while self.ps.poll() == None:
736                 pass
737         shutil.rmtree(self.build_dir)
738
739     def add(self, defconfig):
740         """Assign a new subprocess for defconfig and add it to the slot.
741
742         If the slot is vacant, create a new subprocess for processing the
743         given defconfig and add it to the slot.  Just returns False if
744         the slot is occupied (i.e. the current subprocess is still running).
745
746         Arguments:
747           defconfig: defconfig name.
748
749         Returns:
750           Return True on success or False on failure
751         """
752         if self.state != STATE_IDLE:
753             return False
754
755         self.defconfig = defconfig
756         self.log = ''
757         self.current_src_dir = self.reference_src_dir
758         self.do_defconfig()
759         return True
760
761     def poll(self):
762         """Check the status of the subprocess and handle it as needed.
763
764         Returns True if the slot is vacant (i.e. in idle state).
765         If the configuration is successfully finished, assign a new
766         subprocess to build include/autoconf.mk.
767         If include/autoconf.mk is generated, invoke the parser to
768         parse the .config and the include/autoconf.mk, moving
769         config options to the .config as needed.
770         If the .config was updated, run "make savedefconfig" to sync
771         it, update the original defconfig, and then set the slot back
772         to the idle state.
773
774         Returns:
775           Return True if the subprocess is terminated, False otherwise
776         """
777         if self.state == STATE_IDLE:
778             return True
779
780         if self.ps.poll() == None:
781             return False
782
783         if self.ps.poll() != 0:
784             self.handle_error()
785         elif self.state == STATE_DEFCONFIG:
786             if self.reference_src_dir and not self.current_src_dir:
787                 self.do_savedefconfig()
788             else:
789                 self.do_autoconf()
790         elif self.state == STATE_AUTOCONF:
791             if self.current_src_dir:
792                 self.current_src_dir = None
793                 self.do_defconfig()
794             else:
795                 self.do_savedefconfig()
796         elif self.state == STATE_SAVEDEFCONFIG:
797             self.update_defconfig()
798         else:
799             sys.exit("Internal Error. This should not happen.")
800
801         return True if self.state == STATE_IDLE else False
802
803     def handle_error(self):
804         """Handle error cases."""
805
806         self.log += color_text(self.options.color, COLOR_LIGHT_RED,
807                                "Failed to process.\n")
808         if self.options.verbose:
809             self.log += color_text(self.options.color, COLOR_LIGHT_CYAN,
810                                    self.ps.stderr.read())
811         self.finish(False)
812
813     def do_defconfig(self):
814         """Run 'make <board>_defconfig' to create the .config file."""
815
816         cmd = list(self.make_cmd)
817         cmd.append(self.defconfig)
818         self.ps = subprocess.Popen(cmd, stdout=self.devnull,
819                                    stderr=subprocess.PIPE,
820                                    cwd=self.current_src_dir)
821         self.state = STATE_DEFCONFIG
822
823     def do_autoconf(self):
824         """Run 'make include/config/auto.conf'."""
825
826         self.cross_compile = self.parser.get_cross_compile()
827         if self.cross_compile is None:
828             self.log += color_text(self.options.color, COLOR_YELLOW,
829                                    "Compiler is missing.  Do nothing.\n")
830             self.finish(False)
831             return
832
833         cmd = list(self.make_cmd)
834         if self.cross_compile:
835             cmd.append('CROSS_COMPILE=%s' % self.cross_compile)
836         cmd.append('KCONFIG_IGNORE_DUPLICATES=1')
837         cmd.append('include/config/auto.conf')
838         self.ps = subprocess.Popen(cmd, stdout=self.devnull,
839                                    stderr=subprocess.PIPE,
840                                    cwd=self.current_src_dir)
841         self.state = STATE_AUTOCONF
842
843     def do_savedefconfig(self):
844         """Update the .config and run 'make savedefconfig'."""
845
846         (updated, log) = self.parser.update_dotconfig()
847         self.log += log
848
849         if not self.options.force_sync and not updated:
850             self.finish(True)
851             return
852         if updated:
853             self.log += color_text(self.options.color, COLOR_LIGHT_GREEN,
854                                    "Syncing by savedefconfig...\n")
855         else:
856             self.log += "Syncing by savedefconfig (forced by option)...\n"
857
858         cmd = list(self.make_cmd)
859         cmd.append('savedefconfig')
860         self.ps = subprocess.Popen(cmd, stdout=self.devnull,
861                                    stderr=subprocess.PIPE)
862         self.state = STATE_SAVEDEFCONFIG
863
864     def update_defconfig(self):
865         """Update the input defconfig and go back to the idle state."""
866
867         log = self.parser.check_defconfig()
868         if log:
869             self.suspicious_boards.append(self.defconfig)
870             self.log += log
871         orig_defconfig = os.path.join('configs', self.defconfig)
872         new_defconfig = os.path.join(self.build_dir, 'defconfig')
873         updated = not filecmp.cmp(orig_defconfig, new_defconfig)
874
875         if updated:
876             self.log += color_text(self.options.color, COLOR_LIGHT_BLUE,
877                                    "defconfig was updated.\n")
878
879         if not self.options.dry_run and updated:
880             shutil.move(new_defconfig, orig_defconfig)
881         self.finish(True)
882
883     def finish(self, success):
884         """Display log along with progress and go to the idle state.
885
886         Arguments:
887           success: Should be True when the defconfig was processed
888                    successfully, or False when it fails.
889         """
890         # output at least 30 characters to hide the "* defconfigs out of *".
891         log = self.defconfig.ljust(30) + '\n'
892
893         log += '\n'.join([ '    ' + s for s in self.log.split('\n') ])
894         # Some threads are running in parallel.
895         # Print log atomically to not mix up logs from different threads.
896         print >> (sys.stdout if success else sys.stderr), log
897
898         if not success:
899             if self.options.exit_on_error:
900                 sys.exit("Exit on error.")
901             # If --exit-on-error flag is not set, skip this board and continue.
902             # Record the failed board.
903             self.failed_boards.append(self.defconfig)
904
905         self.progress.inc()
906         self.progress.show()
907         self.state = STATE_IDLE
908
909     def get_failed_boards(self):
910         """Returns a list of failed boards (defconfigs) in this slot.
911         """
912         return self.failed_boards
913
914     def get_suspicious_boards(self):
915         """Returns a list of boards (defconfigs) with possible misconversion.
916         """
917         return self.suspicious_boards
918
919 class Slots:
920
921     """Controller of the array of subprocess slots."""
922
923     def __init__(self, configs, options, progress, reference_src_dir):
924         """Create a new slots controller.
925
926         Arguments:
927           configs: A list of CONFIGs to move.
928           options: option flags.
929           progress: A progress indicator.
930           reference_src_dir: Determine the true starting config state from this
931                              source tree.
932         """
933         self.options = options
934         self.slots = []
935         devnull = get_devnull()
936         make_cmd = get_make_cmd()
937         for i in range(options.jobs):
938             self.slots.append(Slot(configs, options, progress, devnull,
939                                    make_cmd, reference_src_dir))
940
941     def add(self, defconfig):
942         """Add a new subprocess if a vacant slot is found.
943
944         Arguments:
945           defconfig: defconfig name to be put into.
946
947         Returns:
948           Return True on success or False on failure
949         """
950         for slot in self.slots:
951             if slot.add(defconfig):
952                 return True
953         return False
954
955     def available(self):
956         """Check if there is a vacant slot.
957
958         Returns:
959           Return True if at lease one vacant slot is found, False otherwise.
960         """
961         for slot in self.slots:
962             if slot.poll():
963                 return True
964         return False
965
966     def empty(self):
967         """Check if all slots are vacant.
968
969         Returns:
970           Return True if all the slots are vacant, False otherwise.
971         """
972         ret = True
973         for slot in self.slots:
974             if not slot.poll():
975                 ret = False
976         return ret
977
978     def show_failed_boards(self):
979         """Display all of the failed boards (defconfigs)."""
980         boards = []
981         output_file = 'moveconfig.failed'
982
983         for slot in self.slots:
984             boards += slot.get_failed_boards()
985
986         if boards:
987             boards = '\n'.join(boards) + '\n'
988             msg = "The following boards were not processed due to error:\n"
989             msg += boards
990             msg += "(the list has been saved in %s)\n" % output_file
991             print >> sys.stderr, color_text(self.options.color, COLOR_LIGHT_RED,
992                                             msg)
993
994             with open(output_file, 'w') as f:
995                 f.write(boards)
996
997     def show_suspicious_boards(self):
998         """Display all boards (defconfigs) with possible misconversion."""
999         boards = []
1000         output_file = 'moveconfig.suspicious'
1001
1002         for slot in self.slots:
1003             boards += slot.get_suspicious_boards()
1004
1005         if boards:
1006             boards = '\n'.join(boards) + '\n'
1007             msg = "The following boards might have been converted incorrectly.\n"
1008             msg += "It is highly recommended to check them manually:\n"
1009             msg += boards
1010             msg += "(the list has been saved in %s)\n" % output_file
1011             print >> sys.stderr, color_text(self.options.color, COLOR_YELLOW,
1012                                             msg)
1013
1014             with open(output_file, 'w') as f:
1015                 f.write(boards)
1016
1017 class ReferenceSource:
1018
1019     """Reference source against which original configs should be parsed."""
1020
1021     def __init__(self, commit):
1022         """Create a reference source directory based on a specified commit.
1023
1024         Arguments:
1025           commit: commit to git-clone
1026         """
1027         self.src_dir = tempfile.mkdtemp()
1028         print "Cloning git repo to a separate work directory..."
1029         subprocess.check_output(['git', 'clone', os.getcwd(), '.'],
1030                                 cwd=self.src_dir)
1031         print "Checkout '%s' to build the original autoconf.mk." % \
1032             subprocess.check_output(['git', 'rev-parse', '--short', commit]).strip()
1033         subprocess.check_output(['git', 'checkout', commit],
1034                                 stderr=subprocess.STDOUT, cwd=self.src_dir)
1035
1036     def __del__(self):
1037         """Delete the reference source directory
1038
1039         This function makes sure the temporary directory is cleaned away
1040         even if Python suddenly dies due to error.  It should be done in here
1041         because it is guaranteed the destructor is always invoked when the
1042         instance of the class gets unreferenced.
1043         """
1044         shutil.rmtree(self.src_dir)
1045
1046     def get_dir(self):
1047         """Return the absolute path to the reference source directory."""
1048
1049         return self.src_dir
1050
1051 def move_config(configs, options):
1052     """Move config options to defconfig files.
1053
1054     Arguments:
1055       configs: A list of CONFIGs to move.
1056       options: option flags
1057     """
1058     if len(configs) == 0:
1059         if options.force_sync:
1060             print 'No CONFIG is specified. You are probably syncing defconfigs.',
1061         else:
1062             print 'Neither CONFIG nor --force-sync is specified. Nothing will happen.',
1063     else:
1064         print 'Move ' + ', '.join(configs),
1065     print '(jobs: %d)\n' % options.jobs
1066
1067     if options.git_ref:
1068         reference_src = ReferenceSource(options.git_ref)
1069         reference_src_dir = reference_src.get_dir()
1070     else:
1071         reference_src_dir = None
1072
1073     if options.defconfigs:
1074         defconfigs = [line.strip() for line in open(options.defconfigs)]
1075         for i, defconfig in enumerate(defconfigs):
1076             if not defconfig.endswith('_defconfig'):
1077                 defconfigs[i] = defconfig + '_defconfig'
1078             if not os.path.exists(os.path.join('configs', defconfigs[i])):
1079                 sys.exit('%s - defconfig does not exist. Stopping.' %
1080                          defconfigs[i])
1081     else:
1082         # All the defconfig files to be processed
1083         defconfigs = []
1084         for (dirpath, dirnames, filenames) in os.walk('configs'):
1085             dirpath = dirpath[len('configs') + 1:]
1086             for filename in fnmatch.filter(filenames, '*_defconfig'):
1087                 defconfigs.append(os.path.join(dirpath, filename))
1088
1089     progress = Progress(len(defconfigs))
1090     slots = Slots(configs, options, progress, reference_src_dir)
1091
1092     # Main loop to process defconfig files:
1093     #  Add a new subprocess into a vacant slot.
1094     #  Sleep if there is no available slot.
1095     for defconfig in defconfigs:
1096         while not slots.add(defconfig):
1097             while not slots.available():
1098                 # No available slot: sleep for a while
1099                 time.sleep(SLEEP_TIME)
1100
1101     # wait until all the subprocesses finish
1102     while not slots.empty():
1103         time.sleep(SLEEP_TIME)
1104
1105     print ''
1106     slots.show_failed_boards()
1107     slots.show_suspicious_boards()
1108
1109 def main():
1110     try:
1111         cpu_count = multiprocessing.cpu_count()
1112     except NotImplementedError:
1113         cpu_count = 1
1114
1115     parser = optparse.OptionParser()
1116     # Add options here
1117     parser.add_option('-c', '--color', action='store_true', default=False,
1118                       help='display the log in color')
1119     parser.add_option('-d', '--defconfigs', type='string',
1120                       help='a file containing a list of defconfigs to move')
1121     parser.add_option('-n', '--dry-run', action='store_true', default=False,
1122                       help='perform a trial run (show log with no changes)')
1123     parser.add_option('-e', '--exit-on-error', action='store_true',
1124                       default=False,
1125                       help='exit immediately on any error')
1126     parser.add_option('-s', '--force-sync', action='store_true', default=False,
1127                       help='force sync by savedefconfig')
1128     parser.add_option('-H', '--headers-only', dest='cleanup_headers_only',
1129                       action='store_true', default=False,
1130                       help='only cleanup the headers')
1131     parser.add_option('-j', '--jobs', type='int', default=cpu_count,
1132                       help='the number of jobs to run simultaneously')
1133     parser.add_option('-r', '--git-ref', type='string',
1134                       help='the git ref to clone for building the autoconf.mk')
1135     parser.add_option('-v', '--verbose', action='store_true', default=False,
1136                       help='show any build errors as boards are built')
1137     parser.usage += ' CONFIG ...'
1138
1139     (options, configs) = parser.parse_args()
1140
1141     if len(configs) == 0 and not options.force_sync:
1142         parser.print_usage()
1143         sys.exit(1)
1144
1145     # prefix the option name with CONFIG_ if missing
1146     configs = [ config if config.startswith('CONFIG_') else 'CONFIG_' + config
1147                 for config in configs ]
1148
1149     check_top_directory()
1150
1151     if not options.cleanup_headers_only:
1152         check_clean_directory()
1153         update_cross_compile(options.color)
1154         move_config(configs, options)
1155
1156     if configs:
1157         cleanup_headers(configs, options)
1158
1159 if __name__ == '__main__':
1160     main()