3 # Author: Masahiro Yamada <yamada.m@jp.panasonic.com>
5 # SPDX-License-Identifier: GPL-2.0+
9 Converter from Kconfig and MAINTAINERS to boards.cfg
11 Run 'tools/genboardscfg.py' to create boards.cfg file.
13 Run 'tools/genboardscfg.py -h' for available options.
15 This script only works on python 2.6 or later, but not python 3.x.
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'
39 # Automatically generated by %s: don't edit
41 # Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers
45 ### helper functions ###
46 def get_terminal_columns():
47 """Get the width of the terminal.
50 The width of the terminal, or zero if the stdout is not
54 return shutil.get_terminal_size().columns # Python 3.3~
55 except AttributeError:
59 arg = struct.pack('hhhh', 0, 0, 0, 0)
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.
66 return struct.unpack('hhhh', ret)[1]
69 """Get the file object of '/dev/null' device."""
71 devnull = subprocess.DEVNULL # py3k
72 except AttributeError:
73 devnull = open(os.devnull, 'wb')
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.')
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()
91 """Check if the boards.cfg file is up to date.
94 True if the boards.cfg file exists and is newer than any of
95 *_defconfig, MAINTAINERS and Kconfig*. False otherwise.
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
106 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
107 for filename in fnmatch.filter(filenames, '*_defconfig'):
108 if fnmatch.fnmatch(filename, '.*'):
110 filepath = os.path.join(dirpath, filename)
111 if ctime < os.path.getctime(filepath):
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'):
120 filepath = os.path.join(dirpath, filename)
121 if ctime < os.path.getctime(filepath):
124 # Detect a board that has been removed since the current boards.cfg
126 with open(BOARD_FILE) as f:
128 if line[0] == '#' or line == '\n':
130 defconfig = line.split()[6] + '_defconfig'
131 if not os.path.exists(os.path.join(CONFIG_DIR, defconfig)):
137 class MaintainersDatabase:
139 """The database of board status and maintainers."""
142 """Create an empty database."""
145 def get_status(self, target):
146 """Return the status of the given board.
149 Either 'Active' or 'Orphan'
151 if not target in self.database:
152 print >> sys.stderr, "WARNING: no status info for '%s'" % target
155 tmp = self.database[target][0]
156 if tmp.startswith('Maintained'):
158 elif tmp.startswith('Orphan'):
161 print >> sys.stderr, ("WARNING: %s: unknown status for '%s'" %
165 def get_maintainers(self, target):
166 """Return the maintainers of the given board.
168 If the board has two or more maintainers, they are separated
171 if not target in self.database:
172 print >> sys.stderr, "WARNING: no maintainers for '%s'" % target
175 return ':'.join(self.database[target][1])
177 def parse_file(self, file):
178 """Parse the given MAINTAINERS file.
180 This method parses MAINTAINERS and add board status and
181 maintainers information to the database.
184 file: MAINTAINERS file to be parsed
189 for line in open(file):
190 tag, rest = line[:2], line[2:].strip()
192 maintainers.append(rest)
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)
204 for target in targets:
205 self.database[target] = (status, maintainers)
210 for target in targets:
211 self.database[target] = (status, maintainers)
213 class DotConfigParser:
215 """A parser of .config file.
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.
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')
235 def __init__(self, build_dir, output, maintainers_database):
236 """Create a new .config perser.
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
243 self.dotconfig = os.path.join(build_dir, '.config')
245 self.database = maintainers_database
247 def parse(self, defconfig):
248 """Parse .config file and output one-line database for the given board.
251 defconfig: Board (defconfig) name
254 for line in open(self.dotconfig):
255 if not line.startswith('CONFIG_SYS_'):
257 for (key, pattern) in self.re_list:
258 m = pattern.match(line)
260 fields[key] = m.group(1)
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." %
272 if fields['arch'] == 'arm' and 'cpu' in fields:
273 if fields['cpu'] == 'armv8':
274 fields['arch'] = 'aarch64'
276 target, match, rear = defconfig.partition('_defconfig')
277 assert match and not rear, \
278 '%s : invalid defconfig file name' % defconfig
280 fields['status'] = self.database.get_status(target)
281 fields['maintainers'] = self.database.get_maintainers(target)
283 if 'options' in fields:
284 options = fields['config'] + ':' + \
285 fields['options'].replace(r'\"', '"')
286 elif fields['config'] != target:
287 options = fields['config']
291 self.output.write((' '.join(['%s'] * 9) + '\n') %
294 fields.get('cpu', '-'),
295 fields.get('soc', '-'),
296 fields.get('vendor', '-'),
297 fields.get('board', '-'),
300 fields['maintainers']))
304 """A slot to store a subprocess.
306 Each instance of this class handles one subprocess.
307 This class is useful to control multiple processes
308 for faster processing.
311 def __init__(self, output, maintainers_database, devnull, make_cmd):
312 """Create a new slot.
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
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)
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'] = ''
333 """Delete the working directory"""
334 if not self.occupied:
335 while self.ps.poll() == None:
337 shutil.rmtree(self.build_dir)
339 def add(self, defconfig):
340 """Add a new subprocess to the slot.
342 Fails if the slot is occupied, that is, the current subprocess
346 defconfig: Board (defconfig) name
349 Return True on success or False on fail
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_')
360 f.write(line[colon + 1:])
362 self.ps = subprocess.Popen([os.path.join('scripts', 'kconfig', 'conf'),
363 '--defconfig=.tmp_defconfig', 'Kconfig'],
368 self.defconfig = defconfig
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
379 """Check if the subprocess is running and invoke the .config
380 parser if the subprocess is terminated.
383 Return True if the subprocess is terminated, False otherwise
385 if not self.occupied:
387 if self.ps.poll() == None:
389 if self.ps.poll() == 0:
390 self.parser.parse(self.defconfig)
392 print >> sys.stderr, ("WARNING: failed to process '%s'. skip." %
394 self.occupied = False
399 """Controller of the array of subprocess slots."""
401 def __init__(self, jobs, output, maintainers_database):
402 """Create a new slots controller.
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
410 devnull = get_devnull()
411 make_cmd = get_make_cmd()
412 for i in range(jobs):
413 self.slots.append(Slot(output, maintainers_database,
415 for slot in self.slots:
418 def add(self, defconfig):
419 """Add a new subprocess if a vacant slot is available.
422 defconfig: Board (defconfig) name
425 Return True on success or False on fail
427 for slot in self.slots:
428 if slot.add(defconfig):
433 """Check if there is a vacant slot.
436 Return True if a vacant slot is found, False if all slots are full
438 for slot in self.slots:
444 """Check if all slots are vacant.
447 Return True if all slots are vacant, False if at least one slot
451 for slot in self.slots:
458 """A class to control the progress indicator."""
463 def __init__(self, total):
464 """Create an instance.
467 total: A number of boards
471 width = get_terminal_columns()
472 width = min(width, self.MAX_WIDTH)
473 width -= self.MIN_WIDTH
481 """Increment the counter and show the progress bar."""
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)
491 class BoardsFileGenerator:
493 """Generator of boards.cfg."""
496 """Prepare basic things for generating boards.cfg."""
497 # All the defconfig files to be processed
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, '.*'):
504 defconfigs.append(os.path.join(dirpath, filename))
505 self.defconfigs = defconfigs
506 self.indicator = Indicator(len(defconfigs))
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,
514 self.maintainers_database = maintainers_database
517 """Delete the incomplete boards.cfg
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
526 if hasattr(self, 'in_progress') and self.in_progress:
528 os.remove(BOARD_FILE)
529 except OSError as exception:
530 # Ignore 'No such file or directory' error
531 if exception.errno != errno.ENOENT:
533 print 'Removed incomplete %s' % BOARD_FILE
535 def generate(self, jobs):
536 """Generate boards.cfg
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.
544 jobs: The number of jobs to run simultaneously
547 self.in_progress = True
548 print 'Generating %s ... (jobs: %d)' % (BOARD_FILE, jobs)
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)
557 slots = Slots(jobs, pipe, self.maintainers_database)
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)
569 # wait until all the subprocesses finish
570 while not slots.empty():
571 time.sleep(SLEEP_TIME)
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])
579 self.in_progress = False
581 def gen_boards_cfg(jobs=1, force=False):
582 """Generate boards.cfg file.
584 The incomplete boards.cfg is deleted if an error (including
585 the termination by the keyboard interrupt) occurs on the halfway.
588 jobs: The number of jobs to run simultaneously
590 check_top_directory()
591 if not force and output_is_new():
592 print "%s is up to date. Nothing to do." % BOARD_FILE
595 generator = BoardsFileGenerator()
596 generator.generate(jobs)
599 parser = optparse.OptionParser()
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()
609 jobs = int(options.jobs)
611 sys.exit('Option -j (--jobs) takes a number')
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'
620 gen_boards_cfg(jobs, force=options.force)
622 if __name__ == '__main__':