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.
18 import multiprocessing
25 sys.path.insert(1, os.path.join(os.path.dirname(__file__), 'buildman'))
28 ### constant variables ###
29 OUTPUT_FILE = 'boards.cfg'
30 CONFIG_DIR = 'configs'
34 # Automatically generated by %s: don't edit
36 # Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers
40 ### helper functions ###
42 """Remove a file ignoring 'No such file or directory' error."""
45 except OSError as exception:
46 # Ignore 'No such file or directory' error
47 if exception.errno != errno.ENOENT:
50 def check_top_directory():
51 """Exit if we are not at the top of source directory."""
52 for f in ('README', 'Licenses'):
53 if not os.path.exists(f):
54 sys.exit('Please run at the top of source directory.')
56 def output_is_new(output):
57 """Check if the output file is up to date.
60 True if the given output file exists and is newer than any of
61 *_defconfig, MAINTAINERS and Kconfig*. False otherwise.
64 ctime = os.path.getctime(output)
65 except OSError as exception:
66 if exception.errno == errno.ENOENT:
67 # return False on 'No such file or directory' error
72 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
73 for filename in fnmatch.filter(filenames, '*_defconfig'):
74 if fnmatch.fnmatch(filename, '.*'):
76 filepath = os.path.join(dirpath, filename)
77 if ctime < os.path.getctime(filepath):
80 for (dirpath, dirnames, filenames) in os.walk('.'):
81 for filename in filenames:
82 if (fnmatch.fnmatch(filename, '*~') or
83 not fnmatch.fnmatch(filename, 'Kconfig*') and
84 not filename == 'MAINTAINERS'):
86 filepath = os.path.join(dirpath, filename)
87 if ctime < os.path.getctime(filepath):
90 # Detect a board that has been removed since the current board database
92 with open(output, encoding="utf-8") as f:
94 if line[0] == '#' or line == '\n':
96 defconfig = line.split()[6] + '_defconfig'
97 if not os.path.exists(os.path.join(CONFIG_DIR, defconfig)):
103 class KconfigScanner:
105 """Kconfig scanner."""
107 ### constant variable only used in this class ###
112 'vendor' : 'SYS_VENDOR',
113 'board' : 'SYS_BOARD',
114 'config' : 'SYS_CONFIG_NAME',
115 'options' : 'SYS_EXTRA_OPTIONS'
119 """Scan all the Kconfig files and create a Kconfig object."""
120 # Define environment variables referenced from Kconfig
121 os.environ['srctree'] = os.getcwd()
122 os.environ['UBOOTVERSION'] = 'dummy'
123 os.environ['KCONFIG_OBJDIR'] = ''
124 self._conf = kconfiglib.Kconfig(warn=False)
127 """Delete a leftover temporary file before exit.
129 The scan() method of this class creates a temporay file and deletes
130 it on success. If scan() method throws an exception on the way,
131 the temporary file might be left over. In that case, it should be
132 deleted in this destructor.
134 if hasattr(self, '_tmpfile') and self._tmpfile:
135 try_remove(self._tmpfile)
137 def scan(self, defconfig):
138 """Load a defconfig file to obtain board parameters.
141 defconfig: path to the defconfig file to be processed
144 A dictionary of board parameters. It has a form of:
149 'vendor': <vendor_name>,
150 'board': <board_name>,
151 'target': <target_name>,
152 'config': <config_header_name>,
153 'options': <extra_options>
156 # strip special prefixes and save it in a temporary file
157 fd, self._tmpfile = tempfile.mkstemp()
158 with os.fdopen(fd, 'w') as f:
159 for line in open(defconfig):
160 colon = line.find(':CONFIG_')
164 f.write(line[colon + 1:])
166 self._conf.load_config(self._tmpfile)
167 try_remove(self._tmpfile)
172 # Get the value of CONFIG_SYS_ARCH, CONFIG_SYS_CPU, ... etc.
173 # Set '-' if the value is empty.
174 for key, symbol in list(self._SYMBOL_TABLE.items()):
175 value = self._conf.syms.get(symbol).str_value
181 defconfig = os.path.basename(defconfig)
182 params['target'], match, rear = defconfig.partition('_defconfig')
183 assert match and not rear, '%s : invalid defconfig' % defconfig
186 if params['arch'] == 'arm' and params['cpu'] == 'armv8':
187 params['arch'] = 'aarch64'
189 # fix-up options field. It should have the form:
190 # <config name>[:comma separated config options]
191 if params['options'] != '-':
192 params['options'] = params['config'] + ':' + \
193 params['options'].replace(r'\"', '"')
194 elif params['config'] != params['target']:
195 params['options'] = params['config']
199 def scan_defconfigs_for_multiprocess(queue, defconfigs):
200 """Scan defconfig files and queue their board parameters
202 This function is intended to be passed to
203 multiprocessing.Process() constructor.
206 queue: An instance of multiprocessing.Queue().
207 The resulting board parameters are written into it.
208 defconfigs: A sequence of defconfig files to be scanned.
210 kconf_scanner = KconfigScanner()
211 for defconfig in defconfigs:
212 queue.put(kconf_scanner.scan(defconfig))
214 def read_queues(queues, params_list):
215 """Read the queues and append the data to the paramers list"""
218 params_list.append(q.get())
220 def scan_defconfigs(jobs=1):
221 """Collect board parameters for all defconfig files.
223 This function invokes multiple processes for faster processing.
226 jobs: The number of jobs to run simultaneously
229 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
230 for filename in fnmatch.filter(filenames, '*_defconfig'):
231 if fnmatch.fnmatch(filename, '.*'):
233 all_defconfigs.append(os.path.join(dirpath, filename))
235 total_boards = len(all_defconfigs)
238 for i in range(jobs):
239 defconfigs = all_defconfigs[total_boards * i // jobs :
240 total_boards * (i + 1) // jobs]
241 q = multiprocessing.Queue(maxsize=-1)
242 p = multiprocessing.Process(target=scan_defconfigs_for_multiprocess,
243 args=(q, defconfigs))
248 # The resulting data should be accumulated to this list
251 # Data in the queues should be retrieved preriodically.
252 # Otherwise, the queues would become full and subprocesses would get stuck.
253 while any([p.is_alive() for p in processes]):
254 read_queues(queues, params_list)
255 # sleep for a while until the queues are filled
256 time.sleep(SLEEP_TIME)
258 # Joining subprocesses just in case
259 # (All subprocesses should already have been finished)
263 # retrieve leftover data
264 read_queues(queues, params_list)
268 class MaintainersDatabase:
270 """The database of board status and maintainers."""
273 """Create an empty database."""
276 def get_status(self, target):
277 """Return the status of the given board.
279 The board status is generally either 'Active' or 'Orphan'.
280 Display a warning message and return '-' if status information
284 'Active', 'Orphan' or '-'.
286 if not target in self.database:
287 print("WARNING: no status info for '%s'" % target, file=sys.stderr)
290 tmp = self.database[target][0]
291 if tmp.startswith('Maintained'):
293 elif tmp.startswith('Supported'):
295 elif tmp.startswith('Orphan'):
298 print(("WARNING: %s: unknown status for '%s'" %
299 (tmp, target)), file=sys.stderr)
302 def get_maintainers(self, target):
303 """Return the maintainers of the given board.
306 Maintainers of the board. If the board has two or more maintainers,
307 they are separated with colons.
309 if not target in self.database:
310 print("WARNING: no maintainers for '%s'" % target, file=sys.stderr)
313 return ':'.join(self.database[target][1])
315 def parse_file(self, file):
316 """Parse a MAINTAINERS file.
318 Parse a MAINTAINERS file and accumulates board status and
319 maintainers information.
322 file: MAINTAINERS file to be parsed
327 for line in open(file, encoding="utf-8"):
328 # Check also commented maintainers
329 if line[:3] == '#M:':
331 tag, rest = line[:2], line[2:].strip()
333 maintainers.append(rest)
335 # expand wildcard and filter by 'configs/*_defconfig'
336 for f in glob.glob(rest):
337 front, match, rear = f.partition('configs/')
338 if not front and match:
339 front, match, rear = rear.rpartition('_defconfig')
340 if match and not rear:
341 targets.append(front)
345 for target in targets:
346 self.database[target] = (status, maintainers)
351 for target in targets:
352 self.database[target] = (status, maintainers)
354 def insert_maintainers_info(params_list):
355 """Add Status and Maintainers information to the board parameters list.
358 params_list: A list of the board parameters
360 database = MaintainersDatabase()
361 for (dirpath, dirnames, filenames) in os.walk('.'):
362 if 'MAINTAINERS' in filenames:
363 database.parse_file(os.path.join(dirpath, 'MAINTAINERS'))
365 for i, params in enumerate(params_list):
366 target = params['target']
367 params['status'] = database.get_status(target)
368 params['maintainers'] = database.get_maintainers(target)
369 params_list[i] = params
371 def format_and_output(params_list, output):
372 """Write board parameters into a file.
374 Columnate the board parameters, sort lines alphabetically,
375 and then write them to a file.
378 params_list: The list of board parameters
379 output: The path to the output file
381 FIELDS = ('status', 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
382 'options', 'maintainers')
384 # First, decide the width of each column
385 max_length = dict([ (f, 0) for f in FIELDS])
386 for params in params_list:
388 max_length[f] = max(max_length[f], len(params[f]))
391 for params in params_list:
394 # insert two spaces between fields like column -t would
395 line += ' ' + params[f].ljust(max_length[f])
396 output_lines.append(line.strip())
398 # ignore case when sorting
399 output_lines.sort(key=str.lower)
401 with open(output, 'w', encoding="utf-8") as f:
402 f.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n')
404 def gen_boards_cfg(output, jobs=1, force=False, quiet=False):
405 """Generate a board database file.
408 output: The name of the output file
409 jobs: The number of jobs to run simultaneously
410 force: Force to generate the output even if it is new
411 quiet: True to avoid printing a message if nothing needs doing
413 check_top_directory()
415 if not force and output_is_new(output):
417 print("%s is up to date. Nothing to do." % output)
420 params_list = scan_defconfigs(jobs)
421 insert_maintainers_info(params_list)
422 format_and_output(params_list, output)
426 cpu_count = multiprocessing.cpu_count()
427 except NotImplementedError:
430 parser = optparse.OptionParser()
432 parser.add_option('-f', '--force', action="store_true", default=False,
433 help='regenerate the output even if it is new')
434 parser.add_option('-j', '--jobs', type='int', default=cpu_count,
435 help='the number of jobs to run simultaneously')
436 parser.add_option('-o', '--output', default=OUTPUT_FILE,
437 help='output file [default=%s]' % OUTPUT_FILE)
438 parser.add_option('-q', '--quiet', action="store_true", help='run silently')
439 (options, args) = parser.parse_args()
441 gen_boards_cfg(options.output, jobs=options.jobs, force=options.force,
444 if __name__ == '__main__':