]> git.kernelconcepts.de Git - karo-tx-uboot.git/blob - tools/genboardscfg.py
tools/genboardscfg.py: no exception if columns undetectable
[karo-tx-uboot.git] / tools / genboardscfg.py
1 #!/usr/bin/env python
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
16 import errno
17 import fnmatch
18 import glob
19 import optparse
20 import os
21 import re
22 import shutil
23 import subprocess
24 import sys
25 import tempfile
26 import time
27
28 BOARD_FILE = 'boards.cfg'
29 CONFIG_DIR = 'configs'
30 REFORMAT_CMD = [os.path.join('tools', 'reformat.py'),
31                 '-i', '-d', '-', '-s', '8']
32 SHOW_GNU_MAKE = 'scripts/show-gnu-make'
33 SLEEP_TIME=0.03
34
35 COMMENT_BLOCK = '''#
36 # List of boards
37 #   Automatically generated by %s: don't edit
38 #
39 # Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers
40
41 ''' % __file__
42
43 ### helper functions ###
44 def get_terminal_columns():
45     """Get the width of the terminal.
46
47     Returns:
48       The width of the terminal, or zero if the stdout is not
49       associated with tty.
50     """
51     try:
52         return shutil.get_terminal_size().columns # Python 3.3~
53     except AttributeError:
54         import fcntl
55         import termios
56         import struct
57         arg = struct.pack('hhhh', 0, 0, 0, 0)
58         try:
59             ret = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, arg)
60         except IOError as exception:
61             # If 'Inappropriate ioctl for device' error occurs,
62             # stdout is probably redirected. Return 0.
63             return 0
64         return struct.unpack('hhhh', ret)[1]
65
66 def get_devnull():
67     """Get the file object of '/dev/null' device."""
68     try:
69         devnull = subprocess.DEVNULL # py3k
70     except AttributeError:
71         devnull = open(os.devnull, 'wb')
72     return devnull
73
74 def check_top_directory():
75     """Exit if we are not at the top of source directory."""
76     for f in ('README', 'Licenses'):
77         if not os.path.exists(f):
78             sys.exit('Please run at the top of source directory.')
79
80 def get_make_cmd():
81     """Get the command name of GNU Make."""
82     process = subprocess.Popen([SHOW_GNU_MAKE], stdout=subprocess.PIPE)
83     ret = process.communicate()
84     if process.returncode:
85         sys.exit('GNU Make not found')
86     return ret[0].rstrip()
87
88 ### classes ###
89 class MaintainersDatabase:
90
91     """The database of board status and maintainers."""
92
93     def __init__(self):
94         """Create an empty database."""
95         self.database = {}
96
97     def get_status(self, target):
98         """Return the status of the given board.
99
100         Returns:
101           Either 'Active' or 'Orphan'
102         """
103         tmp = self.database[target][0]
104         if tmp.startswith('Maintained'):
105             return 'Active'
106         elif tmp.startswith('Orphan'):
107             return 'Orphan'
108         else:
109             print >> sys.stderr, 'Error: %s: unknown status' % tmp
110
111     def get_maintainers(self, target):
112         """Return the maintainers of the given board.
113
114         If the board has two or more maintainers, they are separated
115         with colons.
116         """
117         return ':'.join(self.database[target][1])
118
119     def parse_file(self, file):
120         """Parse the given MAINTAINERS file.
121
122         This method parses MAINTAINERS and add board status and
123         maintainers information to the database.
124
125         Arguments:
126           file: MAINTAINERS file to be parsed
127         """
128         targets = []
129         maintainers = []
130         status = '-'
131         for line in open(file):
132             tag, rest = line[:2], line[2:].strip()
133             if tag == 'M:':
134                 maintainers.append(rest)
135             elif tag == 'F:':
136                 # expand wildcard and filter by 'configs/*_defconfig'
137                 for f in glob.glob(rest):
138                     front, match, rear = f.partition('configs/')
139                     if not front and match:
140                         front, match, rear = rear.rpartition('_defconfig')
141                         if match and not rear:
142                             targets.append(front)
143             elif tag == 'S:':
144                 status = rest
145             elif line == '\n' and targets:
146                 for target in targets:
147                     self.database[target] = (status, maintainers)
148                 targets = []
149                 maintainers = []
150                 status = '-'
151         if targets:
152             for target in targets:
153                 self.database[target] = (status, maintainers)
154
155 class DotConfigParser:
156
157     """A parser of .config file.
158
159     Each line of the output should have the form of:
160     Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers
161     Most of them are extracted from .config file.
162     MAINTAINERS files are also consulted for Status and Maintainers fields.
163     """
164
165     re_arch = re.compile(r'CONFIG_SYS_ARCH="(.*)"')
166     re_cpu = re.compile(r'CONFIG_SYS_CPU="(.*)"')
167     re_soc = re.compile(r'CONFIG_SYS_SOC="(.*)"')
168     re_vendor = re.compile(r'CONFIG_SYS_VENDOR="(.*)"')
169     re_board = re.compile(r'CONFIG_SYS_BOARD="(.*)"')
170     re_config = re.compile(r'CONFIG_SYS_CONFIG_NAME="(.*)"')
171     re_options = re.compile(r'CONFIG_SYS_EXTRA_OPTIONS="(.*)"')
172     re_list = (('arch', re_arch), ('cpu', re_cpu), ('soc', re_soc),
173                ('vendor', re_vendor), ('board', re_board),
174                ('config', re_config), ('options', re_options))
175     must_fields = ('arch', 'config')
176
177     def __init__(self, build_dir, output, maintainers_database):
178         """Create a new .config perser.
179
180         Arguments:
181           build_dir: Build directory where .config is located
182           output: File object which the result is written to
183           maintainers_database: An instance of class MaintainersDatabase
184         """
185         self.dotconfig = os.path.join(build_dir, '.config')
186         self.output = output
187         self.database = maintainers_database
188
189     def parse(self, defconfig):
190         """Parse .config file and output one-line database for the given board.
191
192         Arguments:
193           defconfig: Board (defconfig) name
194         """
195         fields = {}
196         for line in open(self.dotconfig):
197             if not line.startswith('CONFIG_SYS_'):
198                 continue
199             for (key, pattern) in self.re_list:
200                 m = pattern.match(line)
201                 if m and m.group(1):
202                     fields[key] = m.group(1)
203                     break
204
205         # sanity check of '.config' file
206         for field in self.must_fields:
207             if not field in fields:
208                 sys.exit('Error: %s is not defined in %s' % (field, defconfig))
209
210         # fix-up for aarch64
211         if fields['arch'] == 'arm' and 'cpu' in fields:
212             if fields['cpu'] == 'armv8':
213                 fields['arch'] = 'aarch64'
214
215         target, match, rear = defconfig.partition('_defconfig')
216         assert match and not rear, \
217                                 '%s : invalid defconfig file name' % defconfig
218
219         fields['status'] = self.database.get_status(target)
220         fields['maintainers'] = self.database.get_maintainers(target)
221
222         if 'options' in fields:
223             options = fields['config'] + ':' + \
224                       fields['options'].replace(r'\"', '"')
225         elif fields['config'] != target:
226             options = fields['config']
227         else:
228             options = '-'
229
230         self.output.write((' '.join(['%s'] * 9) + '\n')  %
231                           (fields['status'],
232                            fields['arch'],
233                            fields.get('cpu', '-'),
234                            fields.get('soc', '-'),
235                            fields.get('vendor', '-'),
236                            fields.get('board', '-'),
237                            target,
238                            options,
239                            fields['maintainers']))
240
241 class Slot:
242
243     """A slot to store a subprocess.
244
245     Each instance of this class handles one subprocess.
246     This class is useful to control multiple processes
247     for faster processing.
248     """
249
250     def __init__(self, output, maintainers_database, devnull, make_cmd):
251         """Create a new slot.
252
253         Arguments:
254           output: File object which the result is written to
255           maintainers_database: An instance of class MaintainersDatabase
256         """
257         self.occupied = False
258         self.build_dir = tempfile.mkdtemp()
259         self.devnull = devnull
260         self.make_cmd = make_cmd
261         self.parser = DotConfigParser(self.build_dir, output,
262                                       maintainers_database)
263
264     def __del__(self):
265         """Delete the working directory"""
266         shutil.rmtree(self.build_dir)
267
268     def add(self, defconfig):
269         """Add a new subprocess to the slot.
270
271         Fails if the slot is occupied, that is, the current subprocess
272         is still running.
273
274         Arguments:
275           defconfig: Board (defconfig) name
276
277         Returns:
278           Return True on success or False on fail
279         """
280         if self.occupied:
281             return False
282         o = 'O=' + self.build_dir
283         self.ps = subprocess.Popen([self.make_cmd, o, defconfig],
284                                    stdout=self.devnull)
285         self.defconfig = defconfig
286         self.occupied = True
287         return True
288
289     def poll(self):
290         """Check if the subprocess is running and invoke the .config
291         parser if the subprocess is terminated.
292
293         Returns:
294           Return True if the subprocess is terminated, False otherwise
295         """
296         if not self.occupied:
297             return True
298         if self.ps.poll() == None:
299             return False
300         self.parser.parse(self.defconfig)
301         self.occupied = False
302         return True
303
304 class Slots:
305
306     """Controller of the array of subprocess slots."""
307
308     def __init__(self, jobs, output, maintainers_database):
309         """Create a new slots controller.
310
311         Arguments:
312           jobs: A number of slots to instantiate
313           output: File object which the result is written to
314           maintainers_database: An instance of class MaintainersDatabase
315         """
316         self.slots = []
317         devnull = get_devnull()
318         make_cmd = get_make_cmd()
319         for i in range(jobs):
320             self.slots.append(Slot(output, maintainers_database,
321                                    devnull, make_cmd))
322
323     def add(self, defconfig):
324         """Add a new subprocess if a vacant slot is available.
325
326         Arguments:
327           defconfig: Board (defconfig) name
328
329         Returns:
330           Return True on success or False on fail
331         """
332         for slot in self.slots:
333             if slot.add(defconfig):
334                 return True
335         return False
336
337     def available(self):
338         """Check if there is a vacant slot.
339
340         Returns:
341           Return True if a vacant slot is found, False if all slots are full
342         """
343         for slot in self.slots:
344             if slot.poll():
345                 return True
346         return False
347
348     def empty(self):
349         """Check if all slots are vacant.
350
351         Returns:
352           Return True if all slots are vacant, False if at least one slot
353           is running
354         """
355         ret = True
356         for slot in self.slots:
357             if not slot.poll():
358                 ret = False
359         return ret
360
361 class Indicator:
362
363     """A class to control the progress indicator."""
364
365     MIN_WIDTH = 15
366     MAX_WIDTH = 70
367
368     def __init__(self, total):
369         """Create an instance.
370
371         Arguments:
372           total: A number of boards
373         """
374         self.total = total
375         self.cur = 0
376         width = get_terminal_columns()
377         width = min(width, self.MAX_WIDTH)
378         width -= self.MIN_WIDTH
379         if width > 0:
380             self.enabled = True
381         else:
382             self.enabled = False
383         self.width = width
384
385     def inc(self):
386         """Increment the counter and show the progress bar."""
387         if not self.enabled:
388             return
389         self.cur += 1
390         arrow_len = self.width * self.cur // self.total
391         msg = '%4d/%d [' % (self.cur, self.total)
392         msg += '=' * arrow_len + '>' + ' ' * (self.width - arrow_len) + ']'
393         sys.stdout.write('\r' + msg)
394         sys.stdout.flush()
395
396 def __gen_boards_cfg(jobs):
397     """Generate boards.cfg file.
398
399     Arguments:
400       jobs: The number of jobs to run simultaneously
401
402     Note:
403       The incomplete boards.cfg is left over when an error (including
404       the termination by the keyboard interrupt) occurs on the halfway.
405     """
406     check_top_directory()
407     print 'Generating %s ...  (jobs: %d)' % (BOARD_FILE, jobs)
408
409     # All the defconfig files to be processed
410     defconfigs = []
411     for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
412         dirpath = dirpath[len(CONFIG_DIR) + 1:]
413         for filename in fnmatch.filter(filenames, '*_defconfig'):
414             defconfigs.append(os.path.join(dirpath, filename))
415
416     # Parse all the MAINTAINERS files
417     maintainers_database = MaintainersDatabase()
418     for (dirpath, dirnames, filenames) in os.walk('.'):
419         if 'MAINTAINERS' in filenames:
420             maintainers_database.parse_file(os.path.join(dirpath,
421                                                          'MAINTAINERS'))
422
423     # Output lines should be piped into the reformat tool
424     reformat_process = subprocess.Popen(REFORMAT_CMD, stdin=subprocess.PIPE,
425                                         stdout=open(BOARD_FILE, 'w'))
426     pipe = reformat_process.stdin
427     pipe.write(COMMENT_BLOCK)
428
429     indicator = Indicator(len(defconfigs))
430     slots = Slots(jobs, pipe, maintainers_database)
431
432     # Main loop to process defconfig files:
433     #  Add a new subprocess into a vacant slot.
434     #  Sleep if there is no available slot.
435     for defconfig in defconfigs:
436         while not slots.add(defconfig):
437             while not slots.available():
438                 # No available slot: sleep for a while
439                 time.sleep(SLEEP_TIME)
440         indicator.inc()
441
442     # wait until all the subprocesses finish
443     while not slots.empty():
444         time.sleep(SLEEP_TIME)
445     print ''
446
447     # wait until the reformat tool finishes
448     reformat_process.communicate()
449     if reformat_process.returncode != 0:
450         sys.exit('"%s" failed' % REFORMAT_CMD[0])
451
452 def gen_boards_cfg(jobs):
453     """Generate boards.cfg file.
454
455     The incomplete boards.cfg is deleted if an error (including
456     the termination by the keyboard interrupt) occurs on the halfway.
457
458     Arguments:
459       jobs: The number of jobs to run simultaneously
460     """
461     try:
462         __gen_boards_cfg(jobs)
463     except:
464         # We should remove incomplete boards.cfg
465         try:
466             os.remove(BOARD_FILE)
467         except OSError as exception:
468             # Ignore 'No such file or directory' error
469             if exception.errno != errno.ENOENT:
470                 raise
471         raise
472
473 def main():
474     parser = optparse.OptionParser()
475     # Add options here
476     parser.add_option('-j', '--jobs',
477                       help='the number of jobs to run simultaneously')
478     (options, args) = parser.parse_args()
479     if options.jobs:
480         try:
481             jobs = int(options.jobs)
482         except ValueError:
483             sys.exit('Option -j (--jobs) takes a number')
484     else:
485         try:
486             jobs = int(subprocess.Popen(['getconf', '_NPROCESSORS_ONLN'],
487                                      stdout=subprocess.PIPE).communicate()[0])
488         except (OSError, ValueError):
489             print 'info: failed to get the number of CPUs. Set jobs to 1'
490             jobs = 1
491     gen_boards_cfg(jobs)
492
493 if __name__ == '__main__':
494     main()