]> git.ipfire.org Git - u-boot.git/blob - tools/genboardscfg.py
Merge branch 'master' of git://git.denx.de/u-boot-arc
[u-boot.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()