]> git.kernelconcepts.de Git - karo-tx-uboot.git/blob - tools/genboardscfg.py
Merge branch 'master' of git://git.denx.de/u-boot-arc
[karo-tx-uboot.git] / tools / genboardscfg.py
1 #!/usr/bin/env python2
2 #
3 # Author: Masahiro Yamada <yamada.m@jp.panasonic.com>
4 #
5 # SPDX-License-Identifier:      GPL-2.0+
6 #
7
8 """
9 Converter from Kconfig and MAINTAINERS to boards.cfg
10
11 Run 'tools/genboardscfg.py' to create boards.cfg file.
12
13 Run 'tools/genboardscfg.py -h' for available options.
14
15 This script only works on python 2.6 or later, but not python 3.x.
16 """
17
18 import errno
19 import fnmatch
20 import glob
21 import optparse
22 import os
23 import re
24 import shutil
25 import subprocess
26 import sys
27 import tempfile
28 import time
29
30 BOARD_FILE = 'boards.cfg'
31 CONFIG_DIR = 'configs'
32 REFORMAT_CMD = [os.path.join('tools', 'reformat.py'),
33                 '-i', '-d', '-', '-s', '8']
34 SHOW_GNU_MAKE = 'scripts/show-gnu-make'
35 SLEEP_TIME=0.003
36
37 COMMENT_BLOCK = '''#
38 # List of boards
39 #   Automatically generated by %s: don't edit
40 #
41 # Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers
42
43 ''' % __file__
44
45 ### helper functions ###
46 def get_terminal_columns():
47     """Get the width of the terminal.
48
49     Returns:
50       The width of the terminal, or zero if the stdout is not
51       associated with tty.
52     """
53     try:
54         return shutil.get_terminal_size().columns # Python 3.3~
55     except AttributeError:
56         import fcntl
57         import termios
58         import struct
59         arg = struct.pack('hhhh', 0, 0, 0, 0)
60         try:
61             ret = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, arg)
62         except IOError as exception:
63             # If 'Inappropriate ioctl for device' error occurs,
64             # stdout is probably redirected. Return 0.
65             return 0
66         return struct.unpack('hhhh', ret)[1]
67
68 def get_devnull():
69     """Get the file object of '/dev/null' device."""
70     try:
71         devnull = subprocess.DEVNULL # py3k
72     except AttributeError:
73         devnull = open(os.devnull, 'wb')
74     return devnull
75
76 def check_top_directory():
77     """Exit if we are not at the top of source directory."""
78     for f in ('README', 'Licenses'):
79         if not os.path.exists(f):
80             sys.exit('Please run at the top of source directory.')
81
82 def get_make_cmd():
83     """Get the command name of GNU Make."""
84     process = subprocess.Popen([SHOW_GNU_MAKE], stdout=subprocess.PIPE)
85     ret = process.communicate()
86     if process.returncode:
87         sys.exit('GNU Make not found')
88     return ret[0].rstrip()
89
90 def output_is_new():
91     """Check if the boards.cfg file is up to date.
92
93     Returns:
94       True if the boards.cfg file exists and is newer than any of
95       *_defconfig, MAINTAINERS and Kconfig*.  False otherwise.
96     """
97     try:
98         ctime = os.path.getctime(BOARD_FILE)
99     except OSError as exception:
100         if exception.errno == errno.ENOENT:
101             # return False on 'No such file or directory' error
102             return False
103         else:
104             raise
105
106     for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
107         for filename in fnmatch.filter(filenames, '*_defconfig'):
108             if fnmatch.fnmatch(filename, '.*'):
109                 continue
110             filepath = os.path.join(dirpath, filename)
111             if ctime < os.path.getctime(filepath):
112                 return False
113
114     for (dirpath, dirnames, filenames) in os.walk('.'):
115         for filename in filenames:
116             if (fnmatch.fnmatch(filename, '*~') or
117                 not fnmatch.fnmatch(filename, 'Kconfig*') and
118                 not filename == 'MAINTAINERS'):
119                 continue
120             filepath = os.path.join(dirpath, filename)
121             if ctime < os.path.getctime(filepath):
122                 return False
123
124     # Detect a board that has been removed since the current boards.cfg
125     # was generated
126     with open(BOARD_FILE) as f:
127         for line in f:
128             if line[0] == '#' or line == '\n':
129                 continue
130             defconfig = line.split()[6] + '_defconfig'
131             if not os.path.exists(os.path.join(CONFIG_DIR, defconfig)):
132                 return False
133
134     return True
135
136 ### classes ###
137 class MaintainersDatabase:
138
139     """The database of board status and maintainers."""
140
141     def __init__(self):
142         """Create an empty database."""
143         self.database = {}
144
145     def get_status(self, target):
146         """Return the status of the given board.
147
148         Returns:
149           Either 'Active' or 'Orphan'
150         """
151         if not target in self.database:
152             print >> sys.stderr, "WARNING: no status info for '%s'" % target
153             return '-'
154
155         tmp = self.database[target][0]
156         if tmp.startswith('Maintained'):
157             return 'Active'
158         elif tmp.startswith('Orphan'):
159             return 'Orphan'
160         else:
161             print >> sys.stderr, ("WARNING: %s: unknown status for '%s'" %
162                                   (tmp, target))
163             return '-'
164
165     def get_maintainers(self, target):
166         """Return the maintainers of the given board.
167
168         If the board has two or more maintainers, they are separated
169         with colons.
170         """
171         if not target in self.database:
172             print >> sys.stderr, "WARNING: no maintainers for '%s'" % target
173             return ''
174
175         return ':'.join(self.database[target][1])
176
177     def parse_file(self, file):
178         """Parse the given MAINTAINERS file.
179
180         This method parses MAINTAINERS and add board status and
181         maintainers information to the database.
182
183         Arguments:
184           file: MAINTAINERS file to be parsed
185         """
186         targets = []
187         maintainers = []
188         status = '-'
189         for line in open(file):
190             tag, rest = line[:2], line[2:].strip()
191             if tag == 'M:':
192                 maintainers.append(rest)
193             elif tag == 'F:':
194                 # expand wildcard and filter by 'configs/*_defconfig'
195                 for f in glob.glob(rest):
196                     front, match, rear = f.partition('configs/')
197                     if not front and match:
198                         front, match, rear = rear.rpartition('_defconfig')
199                         if match and not rear:
200                             targets.append(front)
201             elif tag == 'S:':
202                 status = rest
203             elif line == '\n':
204                 for target in targets:
205                     self.database[target] = (status, maintainers)
206                 targets = []
207                 maintainers = []
208                 status = '-'
209         if targets:
210             for target in targets:
211                 self.database[target] = (status, maintainers)
212
213 class DotConfigParser:
214
215     """A parser of .config file.
216
217     Each line of the output should have the form of:
218     Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers
219     Most of them are extracted from .config file.
220     MAINTAINERS files are also consulted for Status and Maintainers fields.
221     """
222
223     re_arch = re.compile(r'CONFIG_SYS_ARCH="(.*)"')
224     re_cpu = re.compile(r'CONFIG_SYS_CPU="(.*)"')
225     re_soc = re.compile(r'CONFIG_SYS_SOC="(.*)"')
226     re_vendor = re.compile(r'CONFIG_SYS_VENDOR="(.*)"')
227     re_board = re.compile(r'CONFIG_SYS_BOARD="(.*)"')
228     re_config = re.compile(r'CONFIG_SYS_CONFIG_NAME="(.*)"')
229     re_options = re.compile(r'CONFIG_SYS_EXTRA_OPTIONS="(.*)"')
230     re_list = (('arch', re_arch), ('cpu', re_cpu), ('soc', re_soc),
231                ('vendor', re_vendor), ('board', re_board),
232                ('config', re_config), ('options', re_options))
233     must_fields = ('arch', 'config')
234
235     def __init__(self, build_dir, output, maintainers_database):
236         """Create a new .config perser.
237
238         Arguments:
239           build_dir: Build directory where .config is located
240           output: File object which the result is written to
241           maintainers_database: An instance of class MaintainersDatabase
242         """
243         self.dotconfig = os.path.join(build_dir, '.config')
244         self.output = output
245         self.database = maintainers_database
246
247     def parse(self, defconfig):
248         """Parse .config file and output one-line database for the given board.
249
250         Arguments:
251           defconfig: Board (defconfig) name
252         """
253         fields = {}
254         for line in open(self.dotconfig):
255             if not line.startswith('CONFIG_SYS_'):
256                 continue
257             for (key, pattern) in self.re_list:
258                 m = pattern.match(line)
259                 if m and m.group(1):
260                     fields[key] = m.group(1)
261                     break
262
263         # sanity check of '.config' file
264         for field in self.must_fields:
265             if not field in fields:
266                 print >> sys.stderr, (
267                     "WARNING: '%s' is not defined in '%s'. Skip." %
268                     (field, defconfig))
269                 return
270
271         # fix-up for aarch64
272         if fields['arch'] == 'arm' and 'cpu' in fields:
273             if fields['cpu'] == 'armv8':
274                 fields['arch'] = 'aarch64'
275
276         target, match, rear = defconfig.partition('_defconfig')
277         assert match and not rear, \
278                                 '%s : invalid defconfig file name' % defconfig
279
280         fields['status'] = self.database.get_status(target)
281         fields['maintainers'] = self.database.get_maintainers(target)
282
283         if 'options' in fields:
284             options = fields['config'] + ':' + \
285                       fields['options'].replace(r'\"', '"')
286         elif fields['config'] != target:
287             options = fields['config']
288         else:
289             options = '-'
290
291         self.output.write((' '.join(['%s'] * 9) + '\n')  %
292                           (fields['status'],
293                            fields['arch'],
294                            fields.get('cpu', '-'),
295                            fields.get('soc', '-'),
296                            fields.get('vendor', '-'),
297                            fields.get('board', '-'),
298                            target,
299                            options,
300                            fields['maintainers']))
301
302 class Slot:
303
304     """A slot to store a subprocess.
305
306     Each instance of this class handles one subprocess.
307     This class is useful to control multiple processes
308     for faster processing.
309     """
310
311     def __init__(self, output, maintainers_database, devnull, make_cmd):
312         """Create a new slot.
313
314         Arguments:
315           output: File object which the result is written to
316           maintainers_database: An instance of class MaintainersDatabase
317           devnull: file object of 'dev/null'
318           make_cmd: the command name of Make
319         """
320         self.build_dir = tempfile.mkdtemp()
321         self.devnull = devnull
322         self.ps = subprocess.Popen([make_cmd, 'O=' + self.build_dir,
323                                     'allnoconfig'], stdout=devnull)
324         self.occupied = True
325         self.parser = DotConfigParser(self.build_dir, output,
326                                       maintainers_database)
327         self.env = os.environ.copy()
328         self.env['srctree'] = os.getcwd()
329         self.env['UBOOTVERSION'] = 'dummy'
330         self.env['KCONFIG_OBJDIR'] = ''
331
332     def __del__(self):
333         """Delete the working directory"""
334         if not self.occupied:
335             while self.ps.poll() == None:
336                 pass
337         shutil.rmtree(self.build_dir)
338
339     def add(self, defconfig):
340         """Add a new subprocess to the slot.
341
342         Fails if the slot is occupied, that is, the current subprocess
343         is still running.
344
345         Arguments:
346           defconfig: Board (defconfig) name
347
348         Returns:
349           Return True on success or False on fail
350         """
351         if self.occupied:
352             return False
353
354         with open(os.path.join(self.build_dir, '.tmp_defconfig'), 'w') as f:
355             for line in open(os.path.join(CONFIG_DIR, defconfig)):
356                 colon = line.find(':CONFIG_')
357                 if colon == -1:
358                     f.write(line)
359                 else:
360                     f.write(line[colon + 1:])
361
362         self.ps = subprocess.Popen([os.path.join('scripts', 'kconfig', 'conf'),
363                                     '--defconfig=.tmp_defconfig', 'Kconfig'],
364                                    stdout=self.devnull,
365                                    cwd=self.build_dir,
366                                    env=self.env)
367
368         self.defconfig = defconfig
369         self.occupied = True
370         return True
371
372     def wait(self):
373         """Wait until the current subprocess finishes."""
374         while self.occupied and self.ps.poll() == None:
375             time.sleep(SLEEP_TIME)
376         self.occupied = False
377
378     def poll(self):
379         """Check if the subprocess is running and invoke the .config
380         parser if the subprocess is terminated.
381
382         Returns:
383           Return True if the subprocess is terminated, False otherwise
384         """
385         if not self.occupied:
386             return True
387         if self.ps.poll() == None:
388             return False
389         if self.ps.poll() == 0:
390             self.parser.parse(self.defconfig)
391         else:
392             print >> sys.stderr, ("WARNING: failed to process '%s'. skip." %
393                                   self.defconfig)
394         self.occupied = False
395         return True
396
397 class Slots:
398
399     """Controller of the array of subprocess slots."""
400
401     def __init__(self, jobs, output, maintainers_database):
402         """Create a new slots controller.
403
404         Arguments:
405           jobs: A number of slots to instantiate
406           output: File object which the result is written to
407           maintainers_database: An instance of class MaintainersDatabase
408         """
409         self.slots = []
410         devnull = get_devnull()
411         make_cmd = get_make_cmd()
412         for i in range(jobs):
413             self.slots.append(Slot(output, maintainers_database,
414                                    devnull, make_cmd))
415         for slot in self.slots:
416             slot.wait()
417
418     def add(self, defconfig):
419         """Add a new subprocess if a vacant slot is available.
420
421         Arguments:
422           defconfig: Board (defconfig) name
423
424         Returns:
425           Return True on success or False on fail
426         """
427         for slot in self.slots:
428             if slot.add(defconfig):
429                 return True
430         return False
431
432     def available(self):
433         """Check if there is a vacant slot.
434
435         Returns:
436           Return True if a vacant slot is found, False if all slots are full
437         """
438         for slot in self.slots:
439             if slot.poll():
440                 return True
441         return False
442
443     def empty(self):
444         """Check if all slots are vacant.
445
446         Returns:
447           Return True if all slots are vacant, False if at least one slot
448           is running
449         """
450         ret = True
451         for slot in self.slots:
452             if not slot.poll():
453                 ret = False
454         return ret
455
456 class Indicator:
457
458     """A class to control the progress indicator."""
459
460     MIN_WIDTH = 15
461     MAX_WIDTH = 70
462
463     def __init__(self, total):
464         """Create an instance.
465
466         Arguments:
467           total: A number of boards
468         """
469         self.total = total
470         self.cur = 0
471         width = get_terminal_columns()
472         width = min(width, self.MAX_WIDTH)
473         width -= self.MIN_WIDTH
474         if width > 0:
475             self.enabled = True
476         else:
477             self.enabled = False
478         self.width = width
479
480     def inc(self):
481         """Increment the counter and show the progress bar."""
482         if not self.enabled:
483             return
484         self.cur += 1
485         arrow_len = self.width * self.cur // self.total
486         msg = '%4d/%d [' % (self.cur, self.total)
487         msg += '=' * arrow_len + '>' + ' ' * (self.width - arrow_len) + ']'
488         sys.stdout.write('\r' + msg)
489         sys.stdout.flush()
490
491 class BoardsFileGenerator:
492
493     """Generator of boards.cfg."""
494
495     def __init__(self):
496         """Prepare basic things for generating boards.cfg."""
497         # All the defconfig files to be processed
498         defconfigs = []
499         for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
500             dirpath = dirpath[len(CONFIG_DIR) + 1:]
501             for filename in fnmatch.filter(filenames, '*_defconfig'):
502                 if fnmatch.fnmatch(filename, '.*'):
503                     continue
504                 defconfigs.append(os.path.join(dirpath, filename))
505         self.defconfigs = defconfigs
506         self.indicator = Indicator(len(defconfigs))
507
508         # Parse all the MAINTAINERS files
509         maintainers_database = MaintainersDatabase()
510         for (dirpath, dirnames, filenames) in os.walk('.'):
511             if 'MAINTAINERS' in filenames:
512                 maintainers_database.parse_file(os.path.join(dirpath,
513                                                              'MAINTAINERS'))
514         self.maintainers_database = maintainers_database
515
516     def __del__(self):
517         """Delete the incomplete boards.cfg
518
519         This destructor deletes boards.cfg if the private member 'in_progress'
520         is defined as True.  The 'in_progress' member is set to True at the
521         beginning of the generate() method and set to False at its end.
522         So, in_progress==True means generating boards.cfg was terminated
523         on the way.
524         """
525
526         if hasattr(self, 'in_progress') and self.in_progress:
527             try:
528                 os.remove(BOARD_FILE)
529             except OSError as exception:
530                 # Ignore 'No such file or directory' error
531                 if exception.errno != errno.ENOENT:
532                     raise
533             print 'Removed incomplete %s' % BOARD_FILE
534
535     def generate(self, jobs):
536         """Generate boards.cfg
537
538         This method sets the 'in_progress' member to True at the beginning
539         and sets it to False on success.  The boards.cfg should not be
540         touched before/after this method because 'in_progress' is used
541         to detect the incomplete boards.cfg.
542
543         Arguments:
544           jobs: The number of jobs to run simultaneously
545         """
546
547         self.in_progress = True
548         print 'Generating %s ...  (jobs: %d)' % (BOARD_FILE, jobs)
549
550         # Output lines should be piped into the reformat tool
551         reformat_process = subprocess.Popen(REFORMAT_CMD,
552                                             stdin=subprocess.PIPE,
553                                             stdout=open(BOARD_FILE, 'w'))
554         pipe = reformat_process.stdin
555         pipe.write(COMMENT_BLOCK)
556
557         slots = Slots(jobs, pipe, self.maintainers_database)
558
559         # Main loop to process defconfig files:
560         #  Add a new subprocess into a vacant slot.
561         #  Sleep if there is no available slot.
562         for defconfig in self.defconfigs:
563             while not slots.add(defconfig):
564                 while not slots.available():
565                     # No available slot: sleep for a while
566                     time.sleep(SLEEP_TIME)
567             self.indicator.inc()
568
569         # wait until all the subprocesses finish
570         while not slots.empty():
571             time.sleep(SLEEP_TIME)
572         print ''
573
574         # wait until the reformat tool finishes
575         reformat_process.communicate()
576         if reformat_process.returncode != 0:
577             sys.exit('"%s" failed' % REFORMAT_CMD[0])
578
579         self.in_progress = False
580
581 def gen_boards_cfg(jobs=1, force=False):
582     """Generate boards.cfg file.
583
584     The incomplete boards.cfg is deleted if an error (including
585     the termination by the keyboard interrupt) occurs on the halfway.
586
587     Arguments:
588       jobs: The number of jobs to run simultaneously
589     """
590     check_top_directory()
591     if not force and output_is_new():
592         print "%s is up to date. Nothing to do." % BOARD_FILE
593         sys.exit(0)
594
595     generator = BoardsFileGenerator()
596     generator.generate(jobs)
597
598 def main():
599     parser = optparse.OptionParser()
600     # Add options here
601     parser.add_option('-j', '--jobs',
602                       help='the number of jobs to run simultaneously')
603     parser.add_option('-f', '--force', action="store_true", default=False,
604                       help='regenerate the output even if it is new')
605     (options, args) = parser.parse_args()
606
607     if options.jobs:
608         try:
609             jobs = int(options.jobs)
610         except ValueError:
611             sys.exit('Option -j (--jobs) takes a number')
612     else:
613         try:
614             jobs = int(subprocess.Popen(['getconf', '_NPROCESSORS_ONLN'],
615                                      stdout=subprocess.PIPE).communicate()[0])
616         except (OSError, ValueError):
617             print 'info: failed to get the number of CPUs. Set jobs to 1'
618             jobs = 1
619
620     gen_boards_cfg(jobs, force=options.force)
621
622 if __name__ == '__main__':
623     main()