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