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 class MaintainersDatabase:
91 """The database of board status and maintainers."""
94 """Create an empty database."""
97 def get_status(self, target):
98 """Return the status of the given board.
101 Either 'Active' or 'Orphan'
103 if not target in self.database:
104 print >> sys.stderr, "WARNING: no status info for '%s'" % target
107 tmp = self.database[target][0]
108 if tmp.startswith('Maintained'):
110 elif tmp.startswith('Orphan'):
113 print >> sys.stderr, ("WARNING: %s: unknown status for '%s'" %
117 def get_maintainers(self, target):
118 """Return the maintainers of the given board.
120 If the board has two or more maintainers, they are separated
123 if not target in self.database:
124 print >> sys.stderr, "WARNING: no maintainers for '%s'" % target
127 return ':'.join(self.database[target][1])
129 def parse_file(self, file):
130 """Parse the given MAINTAINERS file.
132 This method parses MAINTAINERS and add board status and
133 maintainers information to the database.
136 file: MAINTAINERS file to be parsed
141 for line in open(file):
142 tag, rest = line[:2], line[2:].strip()
144 maintainers.append(rest)
146 # expand wildcard and filter by 'configs/*_defconfig'
147 for f in glob.glob(rest):
148 front, match, rear = f.partition('configs/')
149 if not front and match:
150 front, match, rear = rear.rpartition('_defconfig')
151 if match and not rear:
152 targets.append(front)
156 for target in targets:
157 self.database[target] = (status, maintainers)
162 for target in targets:
163 self.database[target] = (status, maintainers)
165 class DotConfigParser:
167 """A parser of .config file.
169 Each line of the output should have the form of:
170 Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers
171 Most of them are extracted from .config file.
172 MAINTAINERS files are also consulted for Status and Maintainers fields.
175 re_arch = re.compile(r'CONFIG_SYS_ARCH="(.*)"')
176 re_cpu = re.compile(r'CONFIG_SYS_CPU="(.*)"')
177 re_soc = re.compile(r'CONFIG_SYS_SOC="(.*)"')
178 re_vendor = re.compile(r'CONFIG_SYS_VENDOR="(.*)"')
179 re_board = re.compile(r'CONFIG_SYS_BOARD="(.*)"')
180 re_config = re.compile(r'CONFIG_SYS_CONFIG_NAME="(.*)"')
181 re_options = re.compile(r'CONFIG_SYS_EXTRA_OPTIONS="(.*)"')
182 re_list = (('arch', re_arch), ('cpu', re_cpu), ('soc', re_soc),
183 ('vendor', re_vendor), ('board', re_board),
184 ('config', re_config), ('options', re_options))
185 must_fields = ('arch', 'config')
187 def __init__(self, build_dir, output, maintainers_database):
188 """Create a new .config perser.
191 build_dir: Build directory where .config is located
192 output: File object which the result is written to
193 maintainers_database: An instance of class MaintainersDatabase
195 self.dotconfig = os.path.join(build_dir, '.config')
197 self.database = maintainers_database
199 def parse(self, defconfig):
200 """Parse .config file and output one-line database for the given board.
203 defconfig: Board (defconfig) name
206 for line in open(self.dotconfig):
207 if not line.startswith('CONFIG_SYS_'):
209 for (key, pattern) in self.re_list:
210 m = pattern.match(line)
212 fields[key] = m.group(1)
215 # sanity check of '.config' file
216 for field in self.must_fields:
217 if not field in fields:
218 print >> sys.stderr, (
219 "WARNING: '%s' is not defined in '%s'. Skip." %
224 if fields['arch'] == 'arm' and 'cpu' in fields:
225 if fields['cpu'] == 'armv8':
226 fields['arch'] = 'aarch64'
228 target, match, rear = defconfig.partition('_defconfig')
229 assert match and not rear, \
230 '%s : invalid defconfig file name' % defconfig
232 fields['status'] = self.database.get_status(target)
233 fields['maintainers'] = self.database.get_maintainers(target)
235 if 'options' in fields:
236 options = fields['config'] + ':' + \
237 fields['options'].replace(r'\"', '"')
238 elif fields['config'] != target:
239 options = fields['config']
243 self.output.write((' '.join(['%s'] * 9) + '\n') %
246 fields.get('cpu', '-'),
247 fields.get('soc', '-'),
248 fields.get('vendor', '-'),
249 fields.get('board', '-'),
252 fields['maintainers']))
256 """A slot to store a subprocess.
258 Each instance of this class handles one subprocess.
259 This class is useful to control multiple processes
260 for faster processing.
263 def __init__(self, output, maintainers_database, devnull, make_cmd):
264 """Create a new slot.
267 output: File object which the result is written to
268 maintainers_database: An instance of class MaintainersDatabase
270 self.occupied = False
271 self.build_dir = tempfile.mkdtemp()
272 self.devnull = devnull
273 self.make_cmd = make_cmd
274 self.parser = DotConfigParser(self.build_dir, output,
275 maintainers_database)
278 """Delete the working directory"""
279 if not self.occupied:
280 while self.ps.poll() == None:
282 shutil.rmtree(self.build_dir)
284 def add(self, defconfig):
285 """Add a new subprocess to the slot.
287 Fails if the slot is occupied, that is, the current subprocess
291 defconfig: Board (defconfig) name
294 Return True on success or False on fail
298 o = 'O=' + self.build_dir
299 self.ps = subprocess.Popen([self.make_cmd, o, defconfig],
301 self.defconfig = defconfig
306 """Check if the subprocess is running and invoke the .config
307 parser if the subprocess is terminated.
310 Return True if the subprocess is terminated, False otherwise
312 if not self.occupied:
314 if self.ps.poll() == None:
316 if self.ps.poll() == 0:
317 self.parser.parse(self.defconfig)
319 print >> sys.stderr, ("WARNING: failed to process '%s'. skip." %
321 self.occupied = False
326 """Controller of the array of subprocess slots."""
328 def __init__(self, jobs, output, maintainers_database):
329 """Create a new slots controller.
332 jobs: A number of slots to instantiate
333 output: File object which the result is written to
334 maintainers_database: An instance of class MaintainersDatabase
337 devnull = get_devnull()
338 make_cmd = get_make_cmd()
339 for i in range(jobs):
340 self.slots.append(Slot(output, maintainers_database,
343 def add(self, defconfig):
344 """Add a new subprocess if a vacant slot is available.
347 defconfig: Board (defconfig) name
350 Return True on success or False on fail
352 for slot in self.slots:
353 if slot.add(defconfig):
358 """Check if there is a vacant slot.
361 Return True if a vacant slot is found, False if all slots are full
363 for slot in self.slots:
369 """Check if all slots are vacant.
372 Return True if all slots are vacant, False if at least one slot
376 for slot in self.slots:
383 """A class to control the progress indicator."""
388 def __init__(self, total):
389 """Create an instance.
392 total: A number of boards
396 width = get_terminal_columns()
397 width = min(width, self.MAX_WIDTH)
398 width -= self.MIN_WIDTH
406 """Increment the counter and show the progress bar."""
410 arrow_len = self.width * self.cur // self.total
411 msg = '%4d/%d [' % (self.cur, self.total)
412 msg += '=' * arrow_len + '>' + ' ' * (self.width - arrow_len) + ']'
413 sys.stdout.write('\r' + msg)
416 class BoardsFileGenerator:
418 """Generator of boards.cfg."""
421 """Prepare basic things for generating boards.cfg."""
422 # All the defconfig files to be processed
424 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
425 dirpath = dirpath[len(CONFIG_DIR) + 1:]
426 for filename in fnmatch.filter(filenames, '*_defconfig'):
427 if fnmatch.fnmatch(filename, '.*'):
429 defconfigs.append(os.path.join(dirpath, filename))
430 self.defconfigs = defconfigs
431 self.indicator = Indicator(len(defconfigs))
433 # Parse all the MAINTAINERS files
434 maintainers_database = MaintainersDatabase()
435 for (dirpath, dirnames, filenames) in os.walk('.'):
436 if 'MAINTAINERS' in filenames:
437 maintainers_database.parse_file(os.path.join(dirpath,
439 self.maintainers_database = maintainers_database
442 """Delete the incomplete boards.cfg
444 This destructor deletes boards.cfg if the private member 'in_progress'
445 is defined as True. The 'in_progress' member is set to True at the
446 beginning of the generate() method and set to False at its end.
447 So, in_progress==True means generating boards.cfg was terminated
451 if hasattr(self, 'in_progress') and self.in_progress:
453 os.remove(BOARD_FILE)
454 except OSError as exception:
455 # Ignore 'No such file or directory' error
456 if exception.errno != errno.ENOENT:
458 print 'Removed incomplete %s' % BOARD_FILE
460 def generate(self, jobs):
461 """Generate boards.cfg
463 This method sets the 'in_progress' member to True at the beginning
464 and sets it to False on success. The boards.cfg should not be
465 touched before/after this method because 'in_progress' is used
466 to detect the incomplete boards.cfg.
469 jobs: The number of jobs to run simultaneously
472 self.in_progress = True
473 print 'Generating %s ... (jobs: %d)' % (BOARD_FILE, jobs)
475 # Output lines should be piped into the reformat tool
476 reformat_process = subprocess.Popen(REFORMAT_CMD,
477 stdin=subprocess.PIPE,
478 stdout=open(BOARD_FILE, 'w'))
479 pipe = reformat_process.stdin
480 pipe.write(COMMENT_BLOCK)
482 slots = Slots(jobs, pipe, self.maintainers_database)
484 # Main loop to process defconfig files:
485 # Add a new subprocess into a vacant slot.
486 # Sleep if there is no available slot.
487 for defconfig in self.defconfigs:
488 while not slots.add(defconfig):
489 while not slots.available():
490 # No available slot: sleep for a while
491 time.sleep(SLEEP_TIME)
494 # wait until all the subprocesses finish
495 while not slots.empty():
496 time.sleep(SLEEP_TIME)
499 # wait until the reformat tool finishes
500 reformat_process.communicate()
501 if reformat_process.returncode != 0:
502 sys.exit('"%s" failed' % REFORMAT_CMD[0])
504 self.in_progress = False
506 def gen_boards_cfg(jobs):
507 """Generate boards.cfg file.
509 The incomplete boards.cfg is deleted if an error (including
510 the termination by the keyboard interrupt) occurs on the halfway.
513 jobs: The number of jobs to run simultaneously
515 check_top_directory()
516 generator = BoardsFileGenerator()
517 generator.generate(jobs)
520 parser = optparse.OptionParser()
522 parser.add_option('-j', '--jobs',
523 help='the number of jobs to run simultaneously')
524 (options, args) = parser.parse_args()
527 jobs = int(options.jobs)
529 sys.exit('Option -j (--jobs) takes a number')
532 jobs = int(subprocess.Popen(['getconf', '_NPROCESSORS_ONLN'],
533 stdout=subprocess.PIPE).communicate()[0])
534 except (OSError, ValueError):
535 print 'info: failed to get the number of CPUs. Set jobs to 1'
539 if __name__ == '__main__':