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