2 # SPDX-License-Identifier: GPL-2.0+
4 # Author: Masahiro Yamada <yamada.m@jp.panasonic.com>
8 Converter from Kconfig and MAINTAINERS to a board database.
10 Run 'tools/genboardscfg.py' to create a board database.
12 Run 'tools/genboardscfg.py -h' for available options.
14 Python 2.6 or later, but not Python 3.x is necessary to run this script.
20 import multiprocessing
27 sys.path.insert(1, os.path.join(os.path.dirname(__file__), 'buildman'))
30 ### constant variables ###
31 OUTPUT_FILE = 'boards.cfg'
32 CONFIG_DIR = 'configs'
36 # Automatically generated by %s: don't edit
38 # Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers
42 ### helper functions ###
44 """Remove a file ignoring 'No such file or directory' error."""
47 except OSError as exception:
48 # Ignore 'No such file or directory' error
49 if exception.errno != errno.ENOENT:
52 def check_top_directory():
53 """Exit if we are not at the top of source directory."""
54 for f in ('README', 'Licenses'):
55 if not os.path.exists(f):
56 sys.exit('Please run at the top of source directory.')
58 def output_is_new(output):
59 """Check if the output file is up to date.
62 True if the given output file exists and is newer than any of
63 *_defconfig, MAINTAINERS and Kconfig*. False otherwise.
66 ctime = os.path.getctime(output)
67 except OSError as exception:
68 if exception.errno == errno.ENOENT:
69 # return False on 'No such file or directory' error
74 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
75 for filename in fnmatch.filter(filenames, '*_defconfig'):
76 if fnmatch.fnmatch(filename, '.*'):
78 filepath = os.path.join(dirpath, filename)
79 if ctime < os.path.getctime(filepath):
82 for (dirpath, dirnames, filenames) in os.walk('.'):
83 for filename in filenames:
84 if (fnmatch.fnmatch(filename, '*~') or
85 not fnmatch.fnmatch(filename, 'Kconfig*') and
86 not filename == 'MAINTAINERS'):
88 filepath = os.path.join(dirpath, filename)
89 if ctime < os.path.getctime(filepath):
92 # Detect a board that has been removed since the current board database
94 with open(output) as f:
96 if line[0] == '#' or line == '\n':
98 defconfig = line.split()[6] + '_defconfig'
99 if not os.path.exists(os.path.join(CONFIG_DIR, defconfig)):
105 class KconfigScanner:
107 """Kconfig scanner."""
109 ### constant variable only used in this class ###
114 'vendor' : 'SYS_VENDOR',
115 'board' : 'SYS_BOARD',
116 'config' : 'SYS_CONFIG_NAME',
117 'options' : 'SYS_EXTRA_OPTIONS'
121 """Scan all the Kconfig files and create a Config object."""
122 # Define environment variables referenced from Kconfig
123 os.environ['srctree'] = os.getcwd()
124 os.environ['UBOOTVERSION'] = 'dummy'
125 os.environ['KCONFIG_OBJDIR'] = ''
126 self._conf = kconfiglib.Config(print_warnings=False)
129 """Delete a leftover temporary file before exit.
131 The scan() method of this class creates a temporay file and deletes
132 it on success. If scan() method throws an exception on the way,
133 the temporary file might be left over. In that case, it should be
134 deleted in this destructor.
136 if hasattr(self, '_tmpfile') and self._tmpfile:
137 try_remove(self._tmpfile)
139 def scan(self, defconfig):
140 """Load a defconfig file to obtain board parameters.
143 defconfig: path to the defconfig file to be processed
146 A dictionary of board parameters. It has a form of:
151 'vendor': <vendor_name>,
152 'board': <board_name>,
153 'target': <target_name>,
154 'config': <config_header_name>,
155 'options': <extra_options>
158 # strip special prefixes and save it in a temporary file
159 fd, self._tmpfile = tempfile.mkstemp()
160 with os.fdopen(fd, 'w') as f:
161 for line in open(defconfig):
162 colon = line.find(':CONFIG_')
166 f.write(line[colon + 1:])
168 warnings = self._conf.load_config(self._tmpfile)
170 for warning in warnings:
171 print '%s: %s' % (defconfig, warning)
173 try_remove(self._tmpfile)
178 # Get the value of CONFIG_SYS_ARCH, CONFIG_SYS_CPU, ... etc.
179 # Set '-' if the value is empty.
180 for key, symbol in self._SYMBOL_TABLE.items():
181 value = self._conf.get_symbol(symbol).get_value()
187 defconfig = os.path.basename(defconfig)
188 params['target'], match, rear = defconfig.partition('_defconfig')
189 assert match and not rear, '%s : invalid defconfig' % defconfig
192 if params['arch'] == 'arm' and params['cpu'] == 'armv8':
193 params['arch'] = 'aarch64'
195 # fix-up options field. It should have the form:
196 # <config name>[:comma separated config options]
197 if params['options'] != '-':
198 params['options'] = params['config'] + ':' + \
199 params['options'].replace(r'\"', '"')
200 elif params['config'] != params['target']:
201 params['options'] = params['config']
205 def scan_defconfigs_for_multiprocess(queue, defconfigs):
206 """Scan defconfig files and queue their board parameters
208 This function is intended to be passed to
209 multiprocessing.Process() constructor.
212 queue: An instance of multiprocessing.Queue().
213 The resulting board parameters are written into it.
214 defconfigs: A sequence of defconfig files to be scanned.
216 kconf_scanner = KconfigScanner()
217 for defconfig in defconfigs:
218 queue.put(kconf_scanner.scan(defconfig))
220 def read_queues(queues, params_list):
221 """Read the queues and append the data to the paramers list"""
224 params_list.append(q.get())
226 def scan_defconfigs(jobs=1):
227 """Collect board parameters for all defconfig files.
229 This function invokes multiple processes for faster processing.
232 jobs: The number of jobs to run simultaneously
235 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
236 for filename in fnmatch.filter(filenames, '*_defconfig'):
237 if fnmatch.fnmatch(filename, '.*'):
239 all_defconfigs.append(os.path.join(dirpath, filename))
241 total_boards = len(all_defconfigs)
244 for i in range(jobs):
245 defconfigs = all_defconfigs[total_boards * i / jobs :
246 total_boards * (i + 1) / jobs]
247 q = multiprocessing.Queue(maxsize=-1)
248 p = multiprocessing.Process(target=scan_defconfigs_for_multiprocess,
249 args=(q, defconfigs))
254 # The resulting data should be accumulated to this list
257 # Data in the queues should be retrieved preriodically.
258 # Otherwise, the queues would become full and subprocesses would get stuck.
259 while any([p.is_alive() for p in processes]):
260 read_queues(queues, params_list)
261 # sleep for a while until the queues are filled
262 time.sleep(SLEEP_TIME)
264 # Joining subprocesses just in case
265 # (All subprocesses should already have been finished)
269 # retrieve leftover data
270 read_queues(queues, params_list)
274 class MaintainersDatabase:
276 """The database of board status and maintainers."""
279 """Create an empty database."""
282 def get_status(self, target):
283 """Return the status of the given board.
285 The board status is generally either 'Active' or 'Orphan'.
286 Display a warning message and return '-' if status information
290 'Active', 'Orphan' or '-'.
292 if not target in self.database:
293 print >> sys.stderr, "WARNING: no status info for '%s'" % target
296 tmp = self.database[target][0]
297 if tmp.startswith('Maintained'):
299 elif tmp.startswith('Supported'):
301 elif tmp.startswith('Orphan'):
304 print >> sys.stderr, ("WARNING: %s: unknown status for '%s'" %
308 def get_maintainers(self, target):
309 """Return the maintainers of the given board.
312 Maintainers of the board. If the board has two or more maintainers,
313 they are separated with colons.
315 if not target in self.database:
316 print >> sys.stderr, "WARNING: no maintainers for '%s'" % target
319 return ':'.join(self.database[target][1])
321 def parse_file(self, file):
322 """Parse a MAINTAINERS file.
324 Parse a MAINTAINERS file and accumulates board status and
325 maintainers information.
328 file: MAINTAINERS file to be parsed
333 for line in open(file):
334 # Check also commented maintainers
335 if line[:3] == '#M:':
337 tag, rest = line[:2], line[2:].strip()
339 maintainers.append(rest)
341 # expand wildcard and filter by 'configs/*_defconfig'
342 for f in glob.glob(rest):
343 front, match, rear = f.partition('configs/')
344 if not front and match:
345 front, match, rear = rear.rpartition('_defconfig')
346 if match and not rear:
347 targets.append(front)
351 for target in targets:
352 self.database[target] = (status, maintainers)
357 for target in targets:
358 self.database[target] = (status, maintainers)
360 def insert_maintainers_info(params_list):
361 """Add Status and Maintainers information to the board parameters list.
364 params_list: A list of the board parameters
366 database = MaintainersDatabase()
367 for (dirpath, dirnames, filenames) in os.walk('.'):
368 if 'MAINTAINERS' in filenames:
369 database.parse_file(os.path.join(dirpath, 'MAINTAINERS'))
371 for i, params in enumerate(params_list):
372 target = params['target']
373 params['status'] = database.get_status(target)
374 params['maintainers'] = database.get_maintainers(target)
375 params_list[i] = params
377 def format_and_output(params_list, output):
378 """Write board parameters into a file.
380 Columnate the board parameters, sort lines alphabetically,
381 and then write them to a file.
384 params_list: The list of board parameters
385 output: The path to the output file
387 FIELDS = ('status', 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
388 'options', 'maintainers')
390 # First, decide the width of each column
391 max_length = dict([ (f, 0) for f in FIELDS])
392 for params in params_list:
394 max_length[f] = max(max_length[f], len(params[f]))
397 for params in params_list:
400 # insert two spaces between fields like column -t would
401 line += ' ' + params[f].ljust(max_length[f])
402 output_lines.append(line.strip())
404 # ignore case when sorting
405 output_lines.sort(key=str.lower)
407 with open(output, 'w') as f:
408 f.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n')
410 def gen_boards_cfg(output, jobs=1, force=False):
411 """Generate a board database file.
414 output: The name of the output file
415 jobs: The number of jobs to run simultaneously
416 force: Force to generate the output even if it is new
418 check_top_directory()
420 if not force and output_is_new(output):
421 print "%s is up to date. Nothing to do." % output
424 params_list = scan_defconfigs(jobs)
425 insert_maintainers_info(params_list)
426 format_and_output(params_list, output)
430 cpu_count = multiprocessing.cpu_count()
431 except NotImplementedError:
434 parser = optparse.OptionParser()
436 parser.add_option('-f', '--force', action="store_true", default=False,
437 help='regenerate the output even if it is new')
438 parser.add_option('-j', '--jobs', type='int', default=cpu_count,
439 help='the number of jobs to run simultaneously')
440 parser.add_option('-o', '--output', default=OUTPUT_FILE,
441 help='output file [default=%s]' % OUTPUT_FILE)
442 (options, args) = parser.parse_args()
444 gen_boards_cfg(options.output, jobs=options.jobs, force=options.force)
446 if __name__ == '__main__':