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