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