]> git.sur5r.net Git - u-boot/blob - tools/moveconfig.py
tools: moveconfig: extract helper function for user confirmation
[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 ### classes ###
604 class Progress:
605
606     """Progress Indicator"""
607
608     def __init__(self, total):
609         """Create a new progress indicator.
610
611         Arguments:
612           total: A number of defconfig files to process.
613         """
614         self.current = 0
615         self.total = total
616
617     def inc(self):
618         """Increment the number of processed defconfig files."""
619
620         self.current += 1
621
622     def show(self):
623         """Display the progress."""
624         print ' %d defconfigs out of %d\r' % (self.current, self.total),
625         sys.stdout.flush()
626
627 class KconfigParser:
628
629     """A parser of .config and include/autoconf.mk."""
630
631     re_arch = re.compile(r'CONFIG_SYS_ARCH="(.*)"')
632     re_cpu = re.compile(r'CONFIG_SYS_CPU="(.*)"')
633
634     def __init__(self, configs, options, build_dir):
635         """Create a new parser.
636
637         Arguments:
638           configs: A list of CONFIGs to move.
639           options: option flags.
640           build_dir: Build directory.
641         """
642         self.configs = configs
643         self.options = options
644         self.dotconfig = os.path.join(build_dir, '.config')
645         self.autoconf = os.path.join(build_dir, 'include', 'autoconf.mk')
646         self.spl_autoconf = os.path.join(build_dir, 'spl', 'include',
647                                          'autoconf.mk')
648         self.config_autoconf = os.path.join(build_dir, 'include', 'config',
649                                             'auto.conf')
650         self.defconfig = os.path.join(build_dir, 'defconfig')
651
652     def get_cross_compile(self):
653         """Parse .config file and return CROSS_COMPILE.
654
655         Returns:
656           A string storing the compiler prefix for the architecture.
657           Return a NULL string for architectures that do not require
658           compiler prefix (Sandbox and native build is the case).
659           Return None if the specified compiler is missing in your PATH.
660           Caller should distinguish '' and None.
661         """
662         arch = ''
663         cpu = ''
664         for line in open(self.dotconfig):
665             m = self.re_arch.match(line)
666             if m:
667                 arch = m.group(1)
668                 continue
669             m = self.re_cpu.match(line)
670             if m:
671                 cpu = m.group(1)
672
673         if not arch:
674             return None
675
676         # fix-up for aarch64
677         if arch == 'arm' and cpu == 'armv8':
678             arch = 'aarch64'
679
680         return CROSS_COMPILE.get(arch, None)
681
682     def parse_one_config(self, config, dotconfig_lines, autoconf_lines):
683         """Parse .config, defconfig, include/autoconf.mk for one config.
684
685         This function looks for the config options in the lines from
686         defconfig, .config, and include/autoconf.mk in order to decide
687         which action should be taken for this defconfig.
688
689         Arguments:
690           config: CONFIG name to parse.
691           dotconfig_lines: lines from the .config file.
692           autoconf_lines: lines from the include/autoconf.mk file.
693
694         Returns:
695           A tupple of the action for this defconfig and the line
696           matched for the config.
697         """
698         not_set = '# %s is not set' % config
699
700         for line in autoconf_lines:
701             line = line.rstrip()
702             if line.startswith(config + '='):
703                 new_val = line
704                 break
705         else:
706             new_val = not_set
707
708         for line in dotconfig_lines:
709             line = line.rstrip()
710             if line.startswith(config + '=') or line == not_set:
711                 old_val = line
712                 break
713         else:
714             if new_val == not_set:
715                 return (ACTION_NO_ENTRY, config)
716             else:
717                 return (ACTION_NO_ENTRY_WARN, config)
718
719         # If this CONFIG is neither bool nor trisate
720         if old_val[-2:] != '=y' and old_val[-2:] != '=m' and old_val != not_set:
721             # tools/scripts/define2mk.sed changes '1' to 'y'.
722             # This is a problem if the CONFIG is int type.
723             # Check the type in Kconfig and handle it correctly.
724             if new_val[-2:] == '=y':
725                 new_val = new_val[:-1] + '1'
726
727         return (ACTION_NO_CHANGE if old_val == new_val else ACTION_MOVE,
728                 new_val)
729
730     def update_dotconfig(self):
731         """Parse files for the config options and update the .config.
732
733         This function parses the generated .config and include/autoconf.mk
734         searching the target options.
735         Move the config option(s) to the .config as needed.
736
737         Arguments:
738           defconfig: defconfig name.
739
740         Returns:
741           Return a tuple of (updated flag, log string).
742           The "updated flag" is True if the .config was updated, False
743           otherwise.  The "log string" shows what happend to the .config.
744         """
745
746         results = []
747         updated = False
748         suspicious = False
749         rm_files = [self.config_autoconf, self.autoconf]
750
751         if self.options.spl:
752             if os.path.exists(self.spl_autoconf):
753                 autoconf_path = self.spl_autoconf
754                 rm_files.append(self.spl_autoconf)
755             else:
756                 for f in rm_files:
757                     os.remove(f)
758                 return (updated, suspicious,
759                         color_text(self.options.color, COLOR_BROWN,
760                                    "SPL is not enabled.  Skipped.") + '\n')
761         else:
762             autoconf_path = self.autoconf
763
764         with open(self.dotconfig) as f:
765             dotconfig_lines = f.readlines()
766
767         with open(autoconf_path) as f:
768             autoconf_lines = f.readlines()
769
770         for config in self.configs:
771             result = self.parse_one_config(config, dotconfig_lines,
772                                            autoconf_lines)
773             results.append(result)
774
775         log = ''
776
777         for (action, value) in results:
778             if action == ACTION_MOVE:
779                 actlog = "Move '%s'" % value
780                 log_color = COLOR_LIGHT_GREEN
781             elif action == ACTION_NO_ENTRY:
782                 actlog = "%s is not defined in Kconfig.  Do nothing." % value
783                 log_color = COLOR_LIGHT_BLUE
784             elif action == ACTION_NO_ENTRY_WARN:
785                 actlog = "%s is not defined in Kconfig (suspicious).  Do nothing." % value
786                 log_color = COLOR_YELLOW
787                 suspicious = True
788             elif action == ACTION_NO_CHANGE:
789                 actlog = "'%s' is the same as the define in Kconfig.  Do nothing." \
790                          % value
791                 log_color = COLOR_LIGHT_PURPLE
792             elif action == ACTION_SPL_NOT_EXIST:
793                 actlog = "SPL is not enabled for this defconfig.  Skip."
794                 log_color = COLOR_PURPLE
795             else:
796                 sys.exit("Internal Error. This should not happen.")
797
798             log += color_text(self.options.color, log_color, actlog) + '\n'
799
800         with open(self.dotconfig, 'a') as f:
801             for (action, value) in results:
802                 if action == ACTION_MOVE:
803                     f.write(value + '\n')
804                     updated = True
805
806         self.results = results
807         for f in rm_files:
808             os.remove(f)
809
810         return (updated, suspicious, log)
811
812     def check_defconfig(self):
813         """Check the defconfig after savedefconfig
814
815         Returns:
816           Return additional log if moved CONFIGs were removed again by
817           'make savedefconfig'.
818         """
819
820         log = ''
821
822         with open(self.defconfig) as f:
823             defconfig_lines = f.readlines()
824
825         for (action, value) in self.results:
826             if action != ACTION_MOVE:
827                 continue
828             if not value + '\n' in defconfig_lines:
829                 log += color_text(self.options.color, COLOR_YELLOW,
830                                   "'%s' was removed by savedefconfig.\n" %
831                                   value)
832
833         return log
834
835 class Slot:
836
837     """A slot to store a subprocess.
838
839     Each instance of this class handles one subprocess.
840     This class is useful to control multiple threads
841     for faster processing.
842     """
843
844     def __init__(self, configs, options, progress, devnull, make_cmd, reference_src_dir):
845         """Create a new process slot.
846
847         Arguments:
848           configs: A list of CONFIGs to move.
849           options: option flags.
850           progress: A progress indicator.
851           devnull: A file object of '/dev/null'.
852           make_cmd: command name of GNU Make.
853           reference_src_dir: Determine the true starting config state from this
854                              source tree.
855         """
856         self.options = options
857         self.progress = progress
858         self.build_dir = tempfile.mkdtemp()
859         self.devnull = devnull
860         self.make_cmd = (make_cmd, 'O=' + self.build_dir)
861         self.reference_src_dir = reference_src_dir
862         self.parser = KconfigParser(configs, options, self.build_dir)
863         self.state = STATE_IDLE
864         self.failed_boards = set()
865         self.suspicious_boards = set()
866
867     def __del__(self):
868         """Delete the working directory
869
870         This function makes sure the temporary directory is cleaned away
871         even if Python suddenly dies due to error.  It should be done in here
872         because it is guaranteed the destructor is always invoked when the
873         instance of the class gets unreferenced.
874
875         If the subprocess is still running, wait until it finishes.
876         """
877         if self.state != STATE_IDLE:
878             while self.ps.poll() == None:
879                 pass
880         shutil.rmtree(self.build_dir)
881
882     def add(self, defconfig):
883         """Assign a new subprocess for defconfig and add it to the slot.
884
885         If the slot is vacant, create a new subprocess for processing the
886         given defconfig and add it to the slot.  Just returns False if
887         the slot is occupied (i.e. the current subprocess is still running).
888
889         Arguments:
890           defconfig: defconfig name.
891
892         Returns:
893           Return True on success or False on failure
894         """
895         if self.state != STATE_IDLE:
896             return False
897
898         self.defconfig = defconfig
899         self.log = ''
900         self.current_src_dir = self.reference_src_dir
901         self.do_defconfig()
902         return True
903
904     def poll(self):
905         """Check the status of the subprocess and handle it as needed.
906
907         Returns True if the slot is vacant (i.e. in idle state).
908         If the configuration is successfully finished, assign a new
909         subprocess to build include/autoconf.mk.
910         If include/autoconf.mk is generated, invoke the parser to
911         parse the .config and the include/autoconf.mk, moving
912         config options to the .config as needed.
913         If the .config was updated, run "make savedefconfig" to sync
914         it, update the original defconfig, and then set the slot back
915         to the idle state.
916
917         Returns:
918           Return True if the subprocess is terminated, False otherwise
919         """
920         if self.state == STATE_IDLE:
921             return True
922
923         if self.ps.poll() == None:
924             return False
925
926         if self.ps.poll() != 0:
927             self.handle_error()
928         elif self.state == STATE_DEFCONFIG:
929             if self.reference_src_dir and not self.current_src_dir:
930                 self.do_savedefconfig()
931             else:
932                 self.do_autoconf()
933         elif self.state == STATE_AUTOCONF:
934             if self.current_src_dir:
935                 self.current_src_dir = None
936                 self.do_defconfig()
937             else:
938                 self.do_savedefconfig()
939         elif self.state == STATE_SAVEDEFCONFIG:
940             self.update_defconfig()
941         else:
942             sys.exit("Internal Error. This should not happen.")
943
944         return True if self.state == STATE_IDLE else False
945
946     def handle_error(self):
947         """Handle error cases."""
948
949         self.log += color_text(self.options.color, COLOR_LIGHT_RED,
950                                "Failed to process.\n")
951         if self.options.verbose:
952             self.log += color_text(self.options.color, COLOR_LIGHT_CYAN,
953                                    self.ps.stderr.read())
954         self.finish(False)
955
956     def do_defconfig(self):
957         """Run 'make <board>_defconfig' to create the .config file."""
958
959         cmd = list(self.make_cmd)
960         cmd.append(self.defconfig)
961         self.ps = subprocess.Popen(cmd, stdout=self.devnull,
962                                    stderr=subprocess.PIPE,
963                                    cwd=self.current_src_dir)
964         self.state = STATE_DEFCONFIG
965
966     def do_autoconf(self):
967         """Run 'make include/config/auto.conf'."""
968
969         self.cross_compile = self.parser.get_cross_compile()
970         if self.cross_compile is None:
971             self.log += color_text(self.options.color, COLOR_YELLOW,
972                                    "Compiler is missing.  Do nothing.\n")
973             self.finish(False)
974             return
975
976         cmd = list(self.make_cmd)
977         if self.cross_compile:
978             cmd.append('CROSS_COMPILE=%s' % self.cross_compile)
979         cmd.append('KCONFIG_IGNORE_DUPLICATES=1')
980         cmd.append('include/config/auto.conf')
981         self.ps = subprocess.Popen(cmd, stdout=self.devnull,
982                                    stderr=subprocess.PIPE,
983                                    cwd=self.current_src_dir)
984         self.state = STATE_AUTOCONF
985
986     def do_savedefconfig(self):
987         """Update the .config and run 'make savedefconfig'."""
988
989         (updated, suspicious, log) = self.parser.update_dotconfig()
990         if suspicious:
991             self.suspicious_boards.add(self.defconfig)
992         self.log += log
993
994         if not self.options.force_sync and not updated:
995             self.finish(True)
996             return
997         if updated:
998             self.log += color_text(self.options.color, COLOR_LIGHT_GREEN,
999                                    "Syncing by savedefconfig...\n")
1000         else:
1001             self.log += "Syncing by savedefconfig (forced by option)...\n"
1002
1003         cmd = list(self.make_cmd)
1004         cmd.append('savedefconfig')
1005         self.ps = subprocess.Popen(cmd, stdout=self.devnull,
1006                                    stderr=subprocess.PIPE)
1007         self.state = STATE_SAVEDEFCONFIG
1008
1009     def update_defconfig(self):
1010         """Update the input defconfig and go back to the idle state."""
1011
1012         log = self.parser.check_defconfig()
1013         if log:
1014             self.suspicious_boards.add(self.defconfig)
1015             self.log += log
1016         orig_defconfig = os.path.join('configs', self.defconfig)
1017         new_defconfig = os.path.join(self.build_dir, 'defconfig')
1018         updated = not filecmp.cmp(orig_defconfig, new_defconfig)
1019
1020         if updated:
1021             self.log += color_text(self.options.color, COLOR_LIGHT_BLUE,
1022                                    "defconfig was updated.\n")
1023
1024         if not self.options.dry_run and updated:
1025             shutil.move(new_defconfig, orig_defconfig)
1026         self.finish(True)
1027
1028     def finish(self, success):
1029         """Display log along with progress and go to the idle state.
1030
1031         Arguments:
1032           success: Should be True when the defconfig was processed
1033                    successfully, or False when it fails.
1034         """
1035         # output at least 30 characters to hide the "* defconfigs out of *".
1036         log = self.defconfig.ljust(30) + '\n'
1037
1038         log += '\n'.join([ '    ' + s for s in self.log.split('\n') ])
1039         # Some threads are running in parallel.
1040         # Print log atomically to not mix up logs from different threads.
1041         print >> (sys.stdout if success else sys.stderr), log
1042
1043         if not success:
1044             if self.options.exit_on_error:
1045                 sys.exit("Exit on error.")
1046             # If --exit-on-error flag is not set, skip this board and continue.
1047             # Record the failed board.
1048             self.failed_boards.add(self.defconfig)
1049
1050         self.progress.inc()
1051         self.progress.show()
1052         self.state = STATE_IDLE
1053
1054     def get_failed_boards(self):
1055         """Returns a set of failed boards (defconfigs) in this slot.
1056         """
1057         return self.failed_boards
1058
1059     def get_suspicious_boards(self):
1060         """Returns a set of boards (defconfigs) with possible misconversion.
1061         """
1062         return self.suspicious_boards - self.failed_boards
1063
1064 class Slots:
1065
1066     """Controller of the array of subprocess slots."""
1067
1068     def __init__(self, configs, options, progress, reference_src_dir):
1069         """Create a new slots controller.
1070
1071         Arguments:
1072           configs: A list of CONFIGs to move.
1073           options: option flags.
1074           progress: A progress indicator.
1075           reference_src_dir: Determine the true starting config state from this
1076                              source tree.
1077         """
1078         self.options = options
1079         self.slots = []
1080         devnull = get_devnull()
1081         make_cmd = get_make_cmd()
1082         for i in range(options.jobs):
1083             self.slots.append(Slot(configs, options, progress, devnull,
1084                                    make_cmd, reference_src_dir))
1085
1086     def add(self, defconfig):
1087         """Add a new subprocess if a vacant slot is found.
1088
1089         Arguments:
1090           defconfig: defconfig name to be put into.
1091
1092         Returns:
1093           Return True on success or False on failure
1094         """
1095         for slot in self.slots:
1096             if slot.add(defconfig):
1097                 return True
1098         return False
1099
1100     def available(self):
1101         """Check if there is a vacant slot.
1102
1103         Returns:
1104           Return True if at lease one vacant slot is found, False otherwise.
1105         """
1106         for slot in self.slots:
1107             if slot.poll():
1108                 return True
1109         return False
1110
1111     def empty(self):
1112         """Check if all slots are vacant.
1113
1114         Returns:
1115           Return True if all the slots are vacant, False otherwise.
1116         """
1117         ret = True
1118         for slot in self.slots:
1119             if not slot.poll():
1120                 ret = False
1121         return ret
1122
1123     def show_failed_boards(self):
1124         """Display all of the failed boards (defconfigs)."""
1125         boards = set()
1126         output_file = 'moveconfig.failed'
1127
1128         for slot in self.slots:
1129             boards |= slot.get_failed_boards()
1130
1131         if boards:
1132             boards = '\n'.join(boards) + '\n'
1133             msg = "The following boards were not processed due to error:\n"
1134             msg += boards
1135             msg += "(the list has been saved in %s)\n" % output_file
1136             print >> sys.stderr, color_text(self.options.color, COLOR_LIGHT_RED,
1137                                             msg)
1138
1139             with open(output_file, 'w') as f:
1140                 f.write(boards)
1141
1142     def show_suspicious_boards(self):
1143         """Display all boards (defconfigs) with possible misconversion."""
1144         boards = set()
1145         output_file = 'moveconfig.suspicious'
1146
1147         for slot in self.slots:
1148             boards |= slot.get_suspicious_boards()
1149
1150         if boards:
1151             boards = '\n'.join(boards) + '\n'
1152             msg = "The following boards might have been converted incorrectly.\n"
1153             msg += "It is highly recommended to check them manually:\n"
1154             msg += boards
1155             msg += "(the list has been saved in %s)\n" % output_file
1156             print >> sys.stderr, color_text(self.options.color, COLOR_YELLOW,
1157                                             msg)
1158
1159             with open(output_file, 'w') as f:
1160                 f.write(boards)
1161
1162 class ReferenceSource:
1163
1164     """Reference source against which original configs should be parsed."""
1165
1166     def __init__(self, commit):
1167         """Create a reference source directory based on a specified commit.
1168
1169         Arguments:
1170           commit: commit to git-clone
1171         """
1172         self.src_dir = tempfile.mkdtemp()
1173         print "Cloning git repo to a separate work directory..."
1174         subprocess.check_output(['git', 'clone', os.getcwd(), '.'],
1175                                 cwd=self.src_dir)
1176         print "Checkout '%s' to build the original autoconf.mk." % \
1177             subprocess.check_output(['git', 'rev-parse', '--short', commit]).strip()
1178         subprocess.check_output(['git', 'checkout', commit],
1179                                 stderr=subprocess.STDOUT, cwd=self.src_dir)
1180
1181     def __del__(self):
1182         """Delete the reference source directory
1183
1184         This function makes sure the temporary directory is cleaned away
1185         even if Python suddenly dies due to error.  It should be done in here
1186         because it is guaranteed the destructor is always invoked when the
1187         instance of the class gets unreferenced.
1188         """
1189         shutil.rmtree(self.src_dir)
1190
1191     def get_dir(self):
1192         """Return the absolute path to the reference source directory."""
1193
1194         return self.src_dir
1195
1196 def move_config(configs, options):
1197     """Move config options to defconfig files.
1198
1199     Arguments:
1200       configs: A list of CONFIGs to move.
1201       options: option flags
1202     """
1203     if len(configs) == 0:
1204         if options.force_sync:
1205             print 'No CONFIG is specified. You are probably syncing defconfigs.',
1206         else:
1207             print 'Neither CONFIG nor --force-sync is specified. Nothing will happen.',
1208     else:
1209         print 'Move ' + ', '.join(configs),
1210     print '(jobs: %d)\n' % options.jobs
1211
1212     if options.git_ref:
1213         reference_src = ReferenceSource(options.git_ref)
1214         reference_src_dir = reference_src.get_dir()
1215     else:
1216         reference_src_dir = None
1217
1218     if options.defconfigs:
1219         defconfigs = get_matched_defconfigs(options.defconfigs)
1220     else:
1221         defconfigs = get_all_defconfigs()
1222
1223     progress = Progress(len(defconfigs))
1224     slots = Slots(configs, options, progress, reference_src_dir)
1225
1226     # Main loop to process defconfig files:
1227     #  Add a new subprocess into a vacant slot.
1228     #  Sleep if there is no available slot.
1229     for defconfig in defconfigs:
1230         while not slots.add(defconfig):
1231             while not slots.available():
1232                 # No available slot: sleep for a while
1233                 time.sleep(SLEEP_TIME)
1234
1235     # wait until all the subprocesses finish
1236     while not slots.empty():
1237         time.sleep(SLEEP_TIME)
1238
1239     print ''
1240     slots.show_failed_boards()
1241     slots.show_suspicious_boards()
1242
1243 def main():
1244     try:
1245         cpu_count = multiprocessing.cpu_count()
1246     except NotImplementedError:
1247         cpu_count = 1
1248
1249     parser = optparse.OptionParser()
1250     # Add options here
1251     parser.add_option('-c', '--color', action='store_true', default=False,
1252                       help='display the log in color')
1253     parser.add_option('-C', '--commit', action='store_true', default=False,
1254                       help='Create a git commit for the operation')
1255     parser.add_option('-d', '--defconfigs', type='string',
1256                       help='a file containing a list of defconfigs to move')
1257     parser.add_option('-n', '--dry-run', action='store_true', default=False,
1258                       help='perform a trial run (show log with no changes)')
1259     parser.add_option('-e', '--exit-on-error', action='store_true',
1260                       default=False,
1261                       help='exit immediately on any error')
1262     parser.add_option('-s', '--force-sync', action='store_true', default=False,
1263                       help='force sync by savedefconfig')
1264     parser.add_option('-S', '--spl', action='store_true', default=False,
1265                       help='parse config options defined for SPL build')
1266     parser.add_option('-H', '--headers-only', dest='cleanup_headers_only',
1267                       action='store_true', default=False,
1268                       help='only cleanup the headers')
1269     parser.add_option('-j', '--jobs', type='int', default=cpu_count,
1270                       help='the number of jobs to run simultaneously')
1271     parser.add_option('-r', '--git-ref', type='string',
1272                       help='the git ref to clone for building the autoconf.mk')
1273     parser.add_option('-y', '--yes', action='store_true', default=False,
1274                       help="respond 'yes' to any prompts")
1275     parser.add_option('-v', '--verbose', action='store_true', default=False,
1276                       help='show any build errors as boards are built')
1277     parser.usage += ' CONFIG ...'
1278
1279     (options, configs) = parser.parse_args()
1280
1281     if len(configs) == 0 and not options.force_sync:
1282         parser.print_usage()
1283         sys.exit(1)
1284
1285     # prefix the option name with CONFIG_ if missing
1286     configs = [ config if config.startswith('CONFIG_') else 'CONFIG_' + config
1287                 for config in configs ]
1288
1289     check_top_directory()
1290
1291     if not options.cleanup_headers_only:
1292         check_clean_directory()
1293         update_cross_compile(options.color)
1294         move_config(configs, options)
1295
1296     if configs:
1297         cleanup_headers(configs, options)
1298         cleanup_extra_options(configs, options)
1299
1300     if options.commit:
1301         subprocess.call(['git', 'add', '-u'])
1302         if configs:
1303             msg = 'Convert %s %sto Kconfig' % (configs[0],
1304                     'et al ' if len(configs) > 1 else '')
1305             msg += ('\n\nThis converts the following to Kconfig:\n   %s\n' %
1306                     '\n   '.join(configs))
1307         else:
1308             msg = 'configs: Resync with savedefconfig'
1309             msg += '\n\nRsync all defconfig files using moveconfig.py'
1310         subprocess.call(['git', 'commit', '-s', '-m', msg])
1311
1312 if __name__ == '__main__':
1313     main()