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 tmp = self.database[target][0]
104 if tmp.startswith('Maintained'):
106 elif tmp.startswith('Orphan'):
109 print >> sys.stderr, 'Error: %s: unknown status' % tmp
111 def get_maintainers(self, target):
112 """Return the maintainers of the given board.
114 If the board has two or more maintainers, they are separated
117 return ':'.join(self.database[target][1])
119 def parse_file(self, file):
120 """Parse the given MAINTAINERS file.
122 This method parses MAINTAINERS and add board status and
123 maintainers information to the database.
126 file: MAINTAINERS file to be parsed
131 for line in open(file):
132 tag, rest = line[:2], line[2:].strip()
134 maintainers.append(rest)
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)
146 for target in targets:
147 self.database[target] = (status, maintainers)
152 for target in targets:
153 self.database[target] = (status, maintainers)
155 class DotConfigParser:
157 """A parser of .config file.
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.
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')
177 def __init__(self, build_dir, output, maintainers_database):
178 """Create a new .config perser.
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
185 self.dotconfig = os.path.join(build_dir, '.config')
187 self.database = maintainers_database
189 def parse(self, defconfig):
190 """Parse .config file and output one-line database for the given board.
193 defconfig: Board (defconfig) name
196 for line in open(self.dotconfig):
197 if not line.startswith('CONFIG_SYS_'):
199 for (key, pattern) in self.re_list:
200 m = pattern.match(line)
202 fields[key] = m.group(1)
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))
211 if fields['arch'] == 'arm' and 'cpu' in fields:
212 if fields['cpu'] == 'armv8':
213 fields['arch'] = 'aarch64'
215 target, match, rear = defconfig.partition('_defconfig')
216 assert match and not rear, \
217 '%s : invalid defconfig file name' % defconfig
219 fields['status'] = self.database.get_status(target)
220 fields['maintainers'] = self.database.get_maintainers(target)
222 if 'options' in fields:
223 options = fields['config'] + ':' + \
224 fields['options'].replace(r'\"', '"')
225 elif fields['config'] != target:
226 options = fields['config']
230 self.output.write((' '.join(['%s'] * 9) + '\n') %
233 fields.get('cpu', '-'),
234 fields.get('soc', '-'),
235 fields.get('vendor', '-'),
236 fields.get('board', '-'),
239 fields['maintainers']))
243 """A slot to store a subprocess.
245 Each instance of this class handles one subprocess.
246 This class is useful to control multiple processes
247 for faster processing.
250 def __init__(self, output, maintainers_database, devnull, make_cmd):
251 """Create a new slot.
254 output: File object which the result is written to
255 maintainers_database: An instance of class MaintainersDatabase
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)
265 """Delete the working directory"""
266 shutil.rmtree(self.build_dir)
268 def add(self, defconfig):
269 """Add a new subprocess to the slot.
271 Fails if the slot is occupied, that is, the current subprocess
275 defconfig: Board (defconfig) name
278 Return True on success or False on fail
282 o = 'O=' + self.build_dir
283 self.ps = subprocess.Popen([self.make_cmd, o, defconfig],
285 self.defconfig = defconfig
290 """Check if the subprocess is running and invoke the .config
291 parser if the subprocess is terminated.
294 Return True if the subprocess is terminated, False otherwise
296 if not self.occupied:
298 if self.ps.poll() == None:
300 self.parser.parse(self.defconfig)
301 self.occupied = False
306 """Controller of the array of subprocess slots."""
308 def __init__(self, jobs, output, maintainers_database):
309 """Create a new slots controller.
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
317 devnull = get_devnull()
318 make_cmd = get_make_cmd()
319 for i in range(jobs):
320 self.slots.append(Slot(output, maintainers_database,
323 def add(self, defconfig):
324 """Add a new subprocess if a vacant slot is available.
327 defconfig: Board (defconfig) name
330 Return True on success or False on fail
332 for slot in self.slots:
333 if slot.add(defconfig):
338 """Check if there is a vacant slot.
341 Return True if a vacant slot is found, False if all slots are full
343 for slot in self.slots:
349 """Check if all slots are vacant.
352 Return True if all slots are vacant, False if at least one slot
356 for slot in self.slots:
363 """A class to control the progress indicator."""
368 def __init__(self, total):
369 """Create an instance.
372 total: A number of boards
376 width = get_terminal_columns()
377 width = min(width, self.MAX_WIDTH)
378 width -= self.MIN_WIDTH
386 """Increment the counter and show the progress bar."""
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)
396 def __gen_boards_cfg(jobs):
397 """Generate boards.cfg file.
400 jobs: The number of jobs to run simultaneously
403 The incomplete boards.cfg is left over when an error (including
404 the termination by the keyboard interrupt) occurs on the halfway.
406 check_top_directory()
407 print 'Generating %s ... (jobs: %d)' % (BOARD_FILE, jobs)
409 # All the defconfig files to be processed
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 if fnmatch.fnmatch(filename, '.*'):
416 defconfigs.append(os.path.join(dirpath, filename))
418 # Parse all the MAINTAINERS files
419 maintainers_database = MaintainersDatabase()
420 for (dirpath, dirnames, filenames) in os.walk('.'):
421 if 'MAINTAINERS' in filenames:
422 maintainers_database.parse_file(os.path.join(dirpath,
425 # Output lines should be piped into the reformat tool
426 reformat_process = subprocess.Popen(REFORMAT_CMD, stdin=subprocess.PIPE,
427 stdout=open(BOARD_FILE, 'w'))
428 pipe = reformat_process.stdin
429 pipe.write(COMMENT_BLOCK)
431 indicator = Indicator(len(defconfigs))
432 slots = Slots(jobs, pipe, maintainers_database)
434 # Main loop to process defconfig files:
435 # Add a new subprocess into a vacant slot.
436 # Sleep if there is no available slot.
437 for defconfig in defconfigs:
438 while not slots.add(defconfig):
439 while not slots.available():
440 # No available slot: sleep for a while
441 time.sleep(SLEEP_TIME)
444 # wait until all the subprocesses finish
445 while not slots.empty():
446 time.sleep(SLEEP_TIME)
449 # wait until the reformat tool finishes
450 reformat_process.communicate()
451 if reformat_process.returncode != 0:
452 sys.exit('"%s" failed' % REFORMAT_CMD[0])
454 def gen_boards_cfg(jobs):
455 """Generate boards.cfg file.
457 The incomplete boards.cfg is deleted if an error (including
458 the termination by the keyboard interrupt) occurs on the halfway.
461 jobs: The number of jobs to run simultaneously
464 __gen_boards_cfg(jobs)
466 # We should remove incomplete boards.cfg
468 os.remove(BOARD_FILE)
469 except OSError as exception:
470 # Ignore 'No such file or directory' error
471 if exception.errno != errno.ENOENT:
476 parser = optparse.OptionParser()
478 parser.add_option('-j', '--jobs',
479 help='the number of jobs to run simultaneously')
480 (options, args) = parser.parse_args()
483 jobs = int(options.jobs)
485 sys.exit('Option -j (--jobs) takes a number')
488 jobs = int(subprocess.Popen(['getconf', '_NPROCESSORS_ONLN'],
489 stdout=subprocess.PIPE).communicate()[0])
490 except (OSError, ValueError):
491 print 'info: failed to get the number of CPUs. Set jobs to 1'
495 if __name__ == '__main__':