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