3 # Author: Masahiro Yamada <yamada.m@jp.panasonic.com>
5 # SPDX-License-Identifier: GPL-2.0+
9 Converter from Kconfig and MAINTAINERS to a board database.
11 Run 'tools/genboardscfg.py' to create a board database.
13 Run 'tools/genboardscfg.py -h' for available options.
15 Python 2.6 or later, but not Python 3.x is necessary to run this script.
21 import multiprocessing
28 sys.path.append(os.path.join(os.path.dirname(__file__), 'buildman'))
31 ### constant variables ###
32 OUTPUT_FILE = 'boards.cfg'
33 CONFIG_DIR = 'configs'
37 # Automatically generated by %s: don't edit
39 # Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers
43 ### helper functions ###
45 """Remove a file ignoring 'No such file or directory' error."""
48 except OSError as exception:
49 # Ignore 'No such file or directory' error
50 if exception.errno != errno.ENOENT:
53 def check_top_directory():
54 """Exit if we are not at the top of source directory."""
55 for f in ('README', 'Licenses'):
56 if not os.path.exists(f):
57 sys.exit('Please run at the top of source directory.')
59 def output_is_new(output):
60 """Check if the output file is up to date.
63 True if the given output file exists and is newer than any of
64 *_defconfig, MAINTAINERS and Kconfig*. False otherwise.
67 ctime = os.path.getctime(output)
68 except OSError as exception:
69 if exception.errno == errno.ENOENT:
70 # return False on 'No such file or directory' error
75 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
76 for filename in fnmatch.filter(filenames, '*_defconfig'):
77 if fnmatch.fnmatch(filename, '.*'):
79 filepath = os.path.join(dirpath, filename)
80 if ctime < os.path.getctime(filepath):
83 for (dirpath, dirnames, filenames) in os.walk('.'):
84 for filename in filenames:
85 if (fnmatch.fnmatch(filename, '*~') or
86 not fnmatch.fnmatch(filename, 'Kconfig*') and
87 not filename == 'MAINTAINERS'):
89 filepath = os.path.join(dirpath, filename)
90 if ctime < os.path.getctime(filepath):
93 # Detect a board that has been removed since the current board database
95 with open(output) as f:
97 if line[0] == '#' or line == '\n':
99 defconfig = line.split()[6] + '_defconfig'
100 if not os.path.exists(os.path.join(CONFIG_DIR, defconfig)):
106 class KconfigScanner:
108 """Kconfig scanner."""
110 ### constant variable only used in this class ###
115 'vendor' : 'SYS_VENDOR',
116 'board' : 'SYS_BOARD',
117 'config' : 'SYS_CONFIG_NAME',
118 'options' : 'SYS_EXTRA_OPTIONS'
122 """Scan all the Kconfig files and create a Config object."""
123 # Define environment variables referenced from Kconfig
124 os.environ['srctree'] = os.getcwd()
125 os.environ['UBOOTVERSION'] = 'dummy'
126 os.environ['KCONFIG_OBJDIR'] = ''
127 self._conf = kconfiglib.Config(print_warnings=False)
130 """Delete a leftover temporary file before exit.
132 The scan() method of this class creates a temporay file and deletes
133 it on success. If scan() method throws an exception on the way,
134 the temporary file might be left over. In that case, it should be
135 deleted in this destructor.
137 if hasattr(self, '_tmpfile') and self._tmpfile:
138 try_remove(self._tmpfile)
140 def scan(self, defconfig):
141 """Load a defconfig file to obtain board parameters.
144 defconfig: path to the defconfig file to be processed
147 A dictionary of board parameters. It has a form of:
152 'vendor': <vendor_name>,
153 'board': <board_name>,
154 'target': <target_name>,
155 'config': <config_header_name>,
156 'options': <extra_options>
159 # strip special prefixes and save it in a temporary file
160 fd, self._tmpfile = tempfile.mkstemp()
161 with os.fdopen(fd, 'w') as f:
162 for line in open(defconfig):
163 colon = line.find(':CONFIG_')
167 f.write(line[colon + 1:])
169 warnings = self._conf.load_config(self._tmpfile)
171 for warning in warnings:
172 print '%s: %s' % (defconfig, warning)
174 try_remove(self._tmpfile)
179 # Get the value of CONFIG_SYS_ARCH, CONFIG_SYS_CPU, ... etc.
180 # Set '-' if the value is empty.
181 for key, symbol in self._SYMBOL_TABLE.items():
182 value = self._conf.get_symbol(symbol).get_value()
188 defconfig = os.path.basename(defconfig)
189 params['target'], match, rear = defconfig.partition('_defconfig')
190 assert match and not rear, '%s : invalid defconfig' % defconfig
193 if params['arch'] == 'arm' and params['cpu'] == 'armv8':
194 params['arch'] = 'aarch64'
196 # fix-up options field. It should have the form:
197 # <config name>[:comma separated config options]
198 if params['options'] != '-':
199 params['options'] = params['config'] + ':' + \
200 params['options'].replace(r'\"', '"')
201 elif params['config'] != params['target']:
202 params['options'] = params['config']
206 def scan_defconfigs_for_multiprocess(queue, defconfigs):
207 """Scan defconfig files and queue their board parameters
209 This function is intended to be passed to
210 multiprocessing.Process() constructor.
213 queue: An instance of multiprocessing.Queue().
214 The resulting board parameters are written into it.
215 defconfigs: A sequence of defconfig files to be scanned.
217 kconf_scanner = KconfigScanner()
218 for defconfig in defconfigs:
219 queue.put(kconf_scanner.scan(defconfig))
221 def read_queues(queues, params_list):
222 """Read the queues and append the data to the paramers list"""
225 params_list.append(q.get())
227 def scan_defconfigs(jobs=1):
228 """Collect board parameters for all defconfig files.
230 This function invokes multiple processes for faster processing.
233 jobs: The number of jobs to run simultaneously
236 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
237 for filename in fnmatch.filter(filenames, '*_defconfig'):
238 if fnmatch.fnmatch(filename, '.*'):
240 all_defconfigs.append(os.path.join(dirpath, filename))
242 total_boards = len(all_defconfigs)
245 for i in range(jobs):
246 defconfigs = all_defconfigs[total_boards * i / jobs :
247 total_boards * (i + 1) / jobs]
248 q = multiprocessing.Queue(maxsize=-1)
249 p = multiprocessing.Process(target=scan_defconfigs_for_multiprocess,
250 args=(q, defconfigs))
255 # The resulting data should be accumulated to this list
258 # Data in the queues should be retrieved preriodically.
259 # Otherwise, the queues would become full and subprocesses would get stuck.
260 while any([p.is_alive() for p in processes]):
261 read_queues(queues, params_list)
262 # sleep for a while until the queues are filled
263 time.sleep(SLEEP_TIME)
265 # Joining subprocesses just in case
266 # (All subprocesses should already have been finished)
270 # retrieve leftover data
271 read_queues(queues, params_list)
275 class MaintainersDatabase:
277 """The database of board status and maintainers."""
280 """Create an empty database."""
283 def get_status(self, target):
284 """Return the status of the given board.
286 The board status is generally either 'Active' or 'Orphan'.
287 Display a warning message and return '-' if status information
291 'Active', 'Orphan' or '-'.
293 if not target in self.database:
294 print >> sys.stderr, "WARNING: no status info for '%s'" % target
297 tmp = self.database[target][0]
298 if tmp.startswith('Maintained'):
300 elif tmp.startswith('Supported'):
302 elif tmp.startswith('Orphan'):
305 print >> sys.stderr, ("WARNING: %s: unknown status for '%s'" %
309 def get_maintainers(self, target):
310 """Return the maintainers of the given board.
313 Maintainers of the board. If the board has two or more maintainers,
314 they are separated with colons.
316 if not target in self.database:
317 print >> sys.stderr, "WARNING: no maintainers for '%s'" % target
320 return ':'.join(self.database[target][1])
322 def parse_file(self, file):
323 """Parse a MAINTAINERS file.
325 Parse a MAINTAINERS file and accumulates board status and
326 maintainers information.
329 file: MAINTAINERS file to be parsed
334 for line in open(file):
335 # Check also commented maintainers
336 if line[:3] == '#M:':
338 tag, rest = line[:2], line[2:].strip()
340 maintainers.append(rest)
342 # expand wildcard and filter by 'configs/*_defconfig'
343 for f in glob.glob(rest):
344 front, match, rear = f.partition('configs/')
345 if not front and match:
346 front, match, rear = rear.rpartition('_defconfig')
347 if match and not rear:
348 targets.append(front)
352 for target in targets:
353 self.database[target] = (status, maintainers)
358 for target in targets:
359 self.database[target] = (status, maintainers)
361 def insert_maintainers_info(params_list):
362 """Add Status and Maintainers information to the board parameters list.
365 params_list: A list of the board parameters
367 database = MaintainersDatabase()
368 for (dirpath, dirnames, filenames) in os.walk('.'):
369 if 'MAINTAINERS' in filenames:
370 database.parse_file(os.path.join(dirpath, 'MAINTAINERS'))
372 for i, params in enumerate(params_list):
373 target = params['target']
374 params['status'] = database.get_status(target)
375 params['maintainers'] = database.get_maintainers(target)
376 params_list[i] = params
378 def format_and_output(params_list, output):
379 """Write board parameters into a file.
381 Columnate the board parameters, sort lines alphabetically,
382 and then write them to a file.
385 params_list: The list of board parameters
386 output: The path to the output file
388 FIELDS = ('status', 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
389 'options', 'maintainers')
391 # First, decide the width of each column
392 max_length = dict([ (f, 0) for f in FIELDS])
393 for params in params_list:
395 max_length[f] = max(max_length[f], len(params[f]))
398 for params in params_list:
401 # insert two spaces between fields like column -t would
402 line += ' ' + params[f].ljust(max_length[f])
403 output_lines.append(line.strip())
405 # ignore case when sorting
406 output_lines.sort(key=str.lower)
408 with open(output, 'w') as f:
409 f.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n')
411 def gen_boards_cfg(output, jobs=1, force=False):
412 """Generate a board database file.
415 output: The name of the output file
416 jobs: The number of jobs to run simultaneously
417 force: Force to generate the output even if it is new
419 check_top_directory()
421 if not force and output_is_new(output):
422 print "%s is up to date. Nothing to do." % output
425 params_list = scan_defconfigs(jobs)
426 insert_maintainers_info(params_list)
427 format_and_output(params_list, output)
431 cpu_count = multiprocessing.cpu_count()
432 except NotImplementedError:
435 parser = optparse.OptionParser()
437 parser.add_option('-f', '--force', action="store_true", default=False,
438 help='regenerate the output even if it is new')
439 parser.add_option('-j', '--jobs', type='int', default=cpu_count,
440 help='the number of jobs to run simultaneously')
441 parser.add_option('-o', '--output', default=OUTPUT_FILE,
442 help='output file [default=%s]' % OUTPUT_FILE)
443 (options, args) = parser.parse_args()
445 gen_boards_cfg(options.output, jobs=options.jobs, force=options.force)
447 if __name__ == '__main__':