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.
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'
37 # Automatically generated by %s: don't edit
39 # Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers
43 ### helper functions ###
44 def get_terminal_columns():
45 """Get the width of the terminal.
48 The width of the terminal, or zero if the stdout is not
52 return shutil.get_terminal_size().columns # Python 3.3~
53 except AttributeError:
57 arg = struct.pack('hhhh', 0, 0, 0, 0)
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.
64 return struct.unpack('hhhh', ret)[1]
67 """Get the file object of '/dev/null' device."""
69 devnull = subprocess.DEVNULL # py3k
70 except AttributeError:
71 devnull = open(os.devnull, 'wb')
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.')
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()
89 """Check if the boards.cfg file is up to date.
92 True if the boards.cfg file exists and is newer than any of
93 *_defconfig, MAINTAINERS and Kconfig*. False otherwise.
96 ctime = os.path.getctime(BOARD_FILE)
97 except OSError as exception:
98 if exception.errno == errno.ENOENT:
99 # return False on 'No such file or directory' error
104 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
105 for filename in fnmatch.filter(filenames, '*_defconfig'):
106 if fnmatch.fnmatch(filename, '.*'):
108 filepath = os.path.join(dirpath, filename)
109 if ctime < os.path.getctime(filepath):
112 for (dirpath, dirnames, filenames) in os.walk('.'):
113 for filename in filenames:
114 if (fnmatch.fnmatch(filename, '*~') or
115 not fnmatch.fnmatch(filename, 'Kconfig*') and
116 not filename == 'MAINTAINERS'):
118 filepath = os.path.join(dirpath, filename)
119 if ctime < os.path.getctime(filepath):
122 # Detect a board that has been removed since the current boards.cfg
124 with open(BOARD_FILE) as f:
126 if line[0] == '#' or line == '\n':
128 defconfig = line.split()[6] + '_defconfig'
129 if not os.path.exists(os.path.join(CONFIG_DIR, defconfig)):
135 class MaintainersDatabase:
137 """The database of board status and maintainers."""
140 """Create an empty database."""
143 def get_status(self, target):
144 """Return the status of the given board.
147 Either 'Active' or 'Orphan'
149 if not target in self.database:
150 print >> sys.stderr, "WARNING: no status info for '%s'" % target
153 tmp = self.database[target][0]
154 if tmp.startswith('Maintained'):
156 elif tmp.startswith('Orphan'):
159 print >> sys.stderr, ("WARNING: %s: unknown status for '%s'" %
163 def get_maintainers(self, target):
164 """Return the maintainers of the given board.
166 If the board has two or more maintainers, they are separated
169 if not target in self.database:
170 print >> sys.stderr, "WARNING: no maintainers for '%s'" % target
173 return ':'.join(self.database[target][1])
175 def parse_file(self, file):
176 """Parse the given MAINTAINERS file.
178 This method parses MAINTAINERS and add board status and
179 maintainers information to the database.
182 file: MAINTAINERS file to be parsed
187 for line in open(file):
188 tag, rest = line[:2], line[2:].strip()
190 maintainers.append(rest)
192 # expand wildcard and filter by 'configs/*_defconfig'
193 for f in glob.glob(rest):
194 front, match, rear = f.partition('configs/')
195 if not front and match:
196 front, match, rear = rear.rpartition('_defconfig')
197 if match and not rear:
198 targets.append(front)
202 for target in targets:
203 self.database[target] = (status, maintainers)
208 for target in targets:
209 self.database[target] = (status, maintainers)
211 class DotConfigParser:
213 """A parser of .config file.
215 Each line of the output should have the form of:
216 Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers
217 Most of them are extracted from .config file.
218 MAINTAINERS files are also consulted for Status and Maintainers fields.
221 re_arch = re.compile(r'CONFIG_SYS_ARCH="(.*)"')
222 re_cpu = re.compile(r'CONFIG_SYS_CPU="(.*)"')
223 re_soc = re.compile(r'CONFIG_SYS_SOC="(.*)"')
224 re_vendor = re.compile(r'CONFIG_SYS_VENDOR="(.*)"')
225 re_board = re.compile(r'CONFIG_SYS_BOARD="(.*)"')
226 re_config = re.compile(r'CONFIG_SYS_CONFIG_NAME="(.*)"')
227 re_options = re.compile(r'CONFIG_SYS_EXTRA_OPTIONS="(.*)"')
228 re_list = (('arch', re_arch), ('cpu', re_cpu), ('soc', re_soc),
229 ('vendor', re_vendor), ('board', re_board),
230 ('config', re_config), ('options', re_options))
231 must_fields = ('arch', 'config')
233 def __init__(self, build_dir, output, maintainers_database):
234 """Create a new .config perser.
237 build_dir: Build directory where .config is located
238 output: File object which the result is written to
239 maintainers_database: An instance of class MaintainersDatabase
241 self.dotconfig = os.path.join(build_dir, '.config')
243 self.database = maintainers_database
245 def parse(self, defconfig):
246 """Parse .config file and output one-line database for the given board.
249 defconfig: Board (defconfig) name
252 for line in open(self.dotconfig):
253 if not line.startswith('CONFIG_SYS_'):
255 for (key, pattern) in self.re_list:
256 m = pattern.match(line)
258 fields[key] = m.group(1)
261 # sanity check of '.config' file
262 for field in self.must_fields:
263 if not field in fields:
264 print >> sys.stderr, (
265 "WARNING: '%s' is not defined in '%s'. Skip." %
270 if fields['arch'] == 'arm' and 'cpu' in fields:
271 if fields['cpu'] == 'armv8':
272 fields['arch'] = 'aarch64'
274 target, match, rear = defconfig.partition('_defconfig')
275 assert match and not rear, \
276 '%s : invalid defconfig file name' % defconfig
278 fields['status'] = self.database.get_status(target)
279 fields['maintainers'] = self.database.get_maintainers(target)
281 if 'options' in fields:
282 options = fields['config'] + ':' + \
283 fields['options'].replace(r'\"', '"')
284 elif fields['config'] != target:
285 options = fields['config']
289 self.output.write((' '.join(['%s'] * 9) + '\n') %
292 fields.get('cpu', '-'),
293 fields.get('soc', '-'),
294 fields.get('vendor', '-'),
295 fields.get('board', '-'),
298 fields['maintainers']))
302 """A slot to store a subprocess.
304 Each instance of this class handles one subprocess.
305 This class is useful to control multiple processes
306 for faster processing.
309 def __init__(self, output, maintainers_database, devnull, make_cmd):
310 """Create a new slot.
313 output: File object which the result is written to
314 maintainers_database: An instance of class MaintainersDatabase
315 devnull: file object of 'dev/null'
316 make_cmd: the command name of Make
318 self.build_dir = tempfile.mkdtemp()
319 self.devnull = devnull
320 self.ps = subprocess.Popen([make_cmd, 'O=' + self.build_dir,
321 'allnoconfig'], stdout=devnull)
323 self.parser = DotConfigParser(self.build_dir, output,
324 maintainers_database)
325 self.env = os.environ.copy()
326 self.env['srctree'] = os.getcwd()
327 self.env['UBOOTVERSION'] = 'dummy'
328 self.env['KCONFIG_OBJDIR'] = ''
331 """Delete the working directory"""
332 if not self.occupied:
333 while self.ps.poll() == None:
335 shutil.rmtree(self.build_dir)
337 def add(self, defconfig):
338 """Add a new subprocess to the slot.
340 Fails if the slot is occupied, that is, the current subprocess
344 defconfig: Board (defconfig) name
347 Return True on success or False on fail
352 with open(os.path.join(self.build_dir, '.tmp_defconfig'), 'w') as f:
353 for line in open(os.path.join(CONFIG_DIR, defconfig)):
354 colon = line.find(':CONFIG_')
358 f.write(line[colon + 1:])
360 self.ps = subprocess.Popen([os.path.join('scripts', 'kconfig', 'conf'),
361 '--defconfig=.tmp_defconfig', 'Kconfig'],
366 self.defconfig = defconfig
371 """Wait until the current subprocess finishes."""
372 while self.occupied and self.ps.poll() == None:
373 time.sleep(SLEEP_TIME)
374 self.occupied = False
377 """Check if the subprocess is running and invoke the .config
378 parser if the subprocess is terminated.
381 Return True if the subprocess is terminated, False otherwise
383 if not self.occupied:
385 if self.ps.poll() == None:
387 if self.ps.poll() == 0:
388 self.parser.parse(self.defconfig)
390 print >> sys.stderr, ("WARNING: failed to process '%s'. skip." %
392 self.occupied = False
397 """Controller of the array of subprocess slots."""
399 def __init__(self, jobs, output, maintainers_database):
400 """Create a new slots controller.
403 jobs: A number of slots to instantiate
404 output: File object which the result is written to
405 maintainers_database: An instance of class MaintainersDatabase
408 devnull = get_devnull()
409 make_cmd = get_make_cmd()
410 for i in range(jobs):
411 self.slots.append(Slot(output, maintainers_database,
413 for slot in self.slots:
416 def add(self, defconfig):
417 """Add a new subprocess if a vacant slot is available.
420 defconfig: Board (defconfig) name
423 Return True on success or False on fail
425 for slot in self.slots:
426 if slot.add(defconfig):
431 """Check if there is a vacant slot.
434 Return True if a vacant slot is found, False if all slots are full
436 for slot in self.slots:
442 """Check if all slots are vacant.
445 Return True if all slots are vacant, False if at least one slot
449 for slot in self.slots:
456 """A class to control the progress indicator."""
461 def __init__(self, total):
462 """Create an instance.
465 total: A number of boards
469 width = get_terminal_columns()
470 width = min(width, self.MAX_WIDTH)
471 width -= self.MIN_WIDTH
479 """Increment the counter and show the progress bar."""
483 arrow_len = self.width * self.cur // self.total
484 msg = '%4d/%d [' % (self.cur, self.total)
485 msg += '=' * arrow_len + '>' + ' ' * (self.width - arrow_len) + ']'
486 sys.stdout.write('\r' + msg)
489 class BoardsFileGenerator:
491 """Generator of boards.cfg."""
494 """Prepare basic things for generating boards.cfg."""
495 # All the defconfig files to be processed
497 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
498 dirpath = dirpath[len(CONFIG_DIR) + 1:]
499 for filename in fnmatch.filter(filenames, '*_defconfig'):
500 if fnmatch.fnmatch(filename, '.*'):
502 defconfigs.append(os.path.join(dirpath, filename))
503 self.defconfigs = defconfigs
504 self.indicator = Indicator(len(defconfigs))
506 # Parse all the MAINTAINERS files
507 maintainers_database = MaintainersDatabase()
508 for (dirpath, dirnames, filenames) in os.walk('.'):
509 if 'MAINTAINERS' in filenames:
510 maintainers_database.parse_file(os.path.join(dirpath,
512 self.maintainers_database = maintainers_database
515 """Delete the incomplete boards.cfg
517 This destructor deletes boards.cfg if the private member 'in_progress'
518 is defined as True. The 'in_progress' member is set to True at the
519 beginning of the generate() method and set to False at its end.
520 So, in_progress==True means generating boards.cfg was terminated
524 if hasattr(self, 'in_progress') and self.in_progress:
526 os.remove(BOARD_FILE)
527 except OSError as exception:
528 # Ignore 'No such file or directory' error
529 if exception.errno != errno.ENOENT:
531 print 'Removed incomplete %s' % BOARD_FILE
533 def generate(self, jobs):
534 """Generate boards.cfg
536 This method sets the 'in_progress' member to True at the beginning
537 and sets it to False on success. The boards.cfg should not be
538 touched before/after this method because 'in_progress' is used
539 to detect the incomplete boards.cfg.
542 jobs: The number of jobs to run simultaneously
545 self.in_progress = True
546 print 'Generating %s ... (jobs: %d)' % (BOARD_FILE, jobs)
548 # Output lines should be piped into the reformat tool
549 reformat_process = subprocess.Popen(REFORMAT_CMD,
550 stdin=subprocess.PIPE,
551 stdout=open(BOARD_FILE, 'w'))
552 pipe = reformat_process.stdin
553 pipe.write(COMMENT_BLOCK)
555 slots = Slots(jobs, pipe, self.maintainers_database)
557 # Main loop to process defconfig files:
558 # Add a new subprocess into a vacant slot.
559 # Sleep if there is no available slot.
560 for defconfig in self.defconfigs:
561 while not slots.add(defconfig):
562 while not slots.available():
563 # No available slot: sleep for a while
564 time.sleep(SLEEP_TIME)
567 # wait until all the subprocesses finish
568 while not slots.empty():
569 time.sleep(SLEEP_TIME)
572 # wait until the reformat tool finishes
573 reformat_process.communicate()
574 if reformat_process.returncode != 0:
575 sys.exit('"%s" failed' % REFORMAT_CMD[0])
577 self.in_progress = False
579 def gen_boards_cfg(jobs=1, force=False):
580 """Generate boards.cfg file.
582 The incomplete boards.cfg is deleted if an error (including
583 the termination by the keyboard interrupt) occurs on the halfway.
586 jobs: The number of jobs to run simultaneously
588 check_top_directory()
589 if not force and output_is_new():
590 print "%s is up to date. Nothing to do." % BOARD_FILE
593 generator = BoardsFileGenerator()
594 generator.generate(jobs)
597 parser = optparse.OptionParser()
599 parser.add_option('-j', '--jobs',
600 help='the number of jobs to run simultaneously')
601 parser.add_option('-f', '--force', action="store_true", default=False,
602 help='regenerate the output even if it is new')
603 (options, args) = parser.parse_args()
607 jobs = int(options.jobs)
609 sys.exit('Option -j (--jobs) takes a number')
612 jobs = int(subprocess.Popen(['getconf', '_NPROCESSORS_ONLN'],
613 stdout=subprocess.PIPE).communicate()[0])
614 except (OSError, ValueError):
615 print 'info: failed to get the number of CPUs. Set jobs to 1'
618 gen_boards_cfg(jobs, force=options.force)
620 if __name__ == '__main__':