blob: ecdc166d7b96e0c1a40a34ab232504274f032106 [file] [log] [blame]
Tom Rini3bc14092019-09-20 17:42:07 -04001#!/usr/bin/env python3
Tom Rini83d290c2018-05-06 17:58:06 -04002# SPDX-License-Identifier: GPL-2.0+
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +09003#
4# Author: Masahiro Yamada <yamada.m@jp.panasonic.com>
5#
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +09006
7"""
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +09008Converter from Kconfig and MAINTAINERS to a board database.
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +09009
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +090010Run 'tools/genboardscfg.py' to create a board database.
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +090011
12Run 'tools/genboardscfg.py -h' for available options.
13"""
14
15import errno
16import fnmatch
17import glob
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +090018import multiprocessing
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +090019import optparse
20import os
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +090021import sys
22import tempfile
23import time
24
Simon Glassb4fa9492020-04-17 18:09:05 -060025from buildman import kconfiglib
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +090026
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +090027### constant variables ###
28OUTPUT_FILE = 'boards.cfg'
29CONFIG_DIR = 'configs'
30SLEEP_TIME = 0.03
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +090031COMMENT_BLOCK = '''#
32# List of boards
33# Automatically generated by %s: don't edit
34#
Masahiro Yamadaca418dd2014-08-06 13:42:34 +090035# Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +090036
37''' % __file__
38
39### helper functions ###
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +090040def try_remove(f):
41 """Remove a file ignoring 'No such file or directory' error."""
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +090042 try:
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +090043 os.remove(f)
44 except OSError as exception:
45 # Ignore 'No such file or directory' error
46 if exception.errno != errno.ENOENT:
47 raise
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +090048
49def check_top_directory():
50 """Exit if we are not at the top of source directory."""
51 for f in ('README', 'Licenses'):
52 if not os.path.exists(f):
Masahiro Yamada31e21412014-08-16 00:59:26 +090053 sys.exit('Please run at the top of source directory.')
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +090054
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +090055def output_is_new(output):
56 """Check if the output file is up to date.
Masahiro Yamadad1bf4af2014-08-25 12:39:47 +090057
58 Returns:
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +090059 True if the given output file exists and is newer than any of
Masahiro Yamadad1bf4af2014-08-25 12:39:47 +090060 *_defconfig, MAINTAINERS and Kconfig*. False otherwise.
61 """
62 try:
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +090063 ctime = os.path.getctime(output)
Masahiro Yamadad1bf4af2014-08-25 12:39:47 +090064 except OSError as exception:
65 if exception.errno == errno.ENOENT:
66 # return False on 'No such file or directory' error
67 return False
68 else:
69 raise
70
71 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
72 for filename in fnmatch.filter(filenames, '*_defconfig'):
73 if fnmatch.fnmatch(filename, '.*'):
74 continue
75 filepath = os.path.join(dirpath, filename)
76 if ctime < os.path.getctime(filepath):
77 return False
78
79 for (dirpath, dirnames, filenames) in os.walk('.'):
80 for filename in filenames:
81 if (fnmatch.fnmatch(filename, '*~') or
82 not fnmatch.fnmatch(filename, 'Kconfig*') and
83 not filename == 'MAINTAINERS'):
84 continue
85 filepath = os.path.join(dirpath, filename)
86 if ctime < os.path.getctime(filepath):
87 return False
88
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +090089 # Detect a board that has been removed since the current board database
Masahiro Yamadad1bf4af2014-08-25 12:39:47 +090090 # was generated
Tom Rini3bc14092019-09-20 17:42:07 -040091 with open(output, encoding="utf-8") as f:
Masahiro Yamadad1bf4af2014-08-25 12:39:47 +090092 for line in f:
93 if line[0] == '#' or line == '\n':
94 continue
95 defconfig = line.split()[6] + '_defconfig'
96 if not os.path.exists(os.path.join(CONFIG_DIR, defconfig)):
97 return False
98
99 return True
100
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900101### classes ###
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +0900102class KconfigScanner:
103
104 """Kconfig scanner."""
105
106 ### constant variable only used in this class ###
107 _SYMBOL_TABLE = {
108 'arch' : 'SYS_ARCH',
109 'cpu' : 'SYS_CPU',
110 'soc' : 'SYS_SOC',
111 'vendor' : 'SYS_VENDOR',
112 'board' : 'SYS_BOARD',
113 'config' : 'SYS_CONFIG_NAME',
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +0900114 }
115
116 def __init__(self):
Tom Rini65e05dd2019-09-20 17:42:09 -0400117 """Scan all the Kconfig files and create a Kconfig object."""
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +0900118 # Define environment variables referenced from Kconfig
119 os.environ['srctree'] = os.getcwd()
120 os.environ['UBOOTVERSION'] = 'dummy'
121 os.environ['KCONFIG_OBJDIR'] = ''
Tom Rini65e05dd2019-09-20 17:42:09 -0400122 self._conf = kconfiglib.Kconfig(warn=False)
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +0900123
124 def __del__(self):
125 """Delete a leftover temporary file before exit.
126
127 The scan() method of this class creates a temporay file and deletes
128 it on success. If scan() method throws an exception on the way,
129 the temporary file might be left over. In that case, it should be
130 deleted in this destructor.
131 """
132 if hasattr(self, '_tmpfile') and self._tmpfile:
133 try_remove(self._tmpfile)
134
135 def scan(self, defconfig):
136 """Load a defconfig file to obtain board parameters.
137
138 Arguments:
139 defconfig: path to the defconfig file to be processed
140
141 Returns:
142 A dictionary of board parameters. It has a form of:
143 {
144 'arch': <arch_name>,
145 'cpu': <cpu_name>,
146 'soc': <soc_name>,
147 'vendor': <vendor_name>,
148 'board': <board_name>,
149 'target': <target_name>,
150 'config': <config_header_name>,
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +0900151 }
152 """
153 # strip special prefixes and save it in a temporary file
154 fd, self._tmpfile = tempfile.mkstemp()
155 with os.fdopen(fd, 'w') as f:
156 for line in open(defconfig):
157 colon = line.find(':CONFIG_')
158 if colon == -1:
159 f.write(line)
160 else:
161 f.write(line[colon + 1:])
162
Tom Rini5e7c8a32019-09-20 17:42:08 -0400163 self._conf.load_config(self._tmpfile)
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +0900164 try_remove(self._tmpfile)
165 self._tmpfile = None
166
167 params = {}
168
169 # Get the value of CONFIG_SYS_ARCH, CONFIG_SYS_CPU, ... etc.
170 # Set '-' if the value is empty.
Tom Rini3bc14092019-09-20 17:42:07 -0400171 for key, symbol in list(self._SYMBOL_TABLE.items()):
Tom Rini65e05dd2019-09-20 17:42:09 -0400172 value = self._conf.syms.get(symbol).str_value
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +0900173 if value:
174 params[key] = value
175 else:
176 params[key] = '-'
177
178 defconfig = os.path.basename(defconfig)
179 params['target'], match, rear = defconfig.partition('_defconfig')
180 assert match and not rear, '%s : invalid defconfig' % defconfig
181
182 # fix-up for aarch64
183 if params['arch'] == 'arm' and params['cpu'] == 'armv8':
184 params['arch'] = 'aarch64'
185
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +0900186 return params
187
188def scan_defconfigs_for_multiprocess(queue, defconfigs):
189 """Scan defconfig files and queue their board parameters
190
191 This function is intended to be passed to
192 multiprocessing.Process() constructor.
193
194 Arguments:
195 queue: An instance of multiprocessing.Queue().
196 The resulting board parameters are written into it.
197 defconfigs: A sequence of defconfig files to be scanned.
198 """
199 kconf_scanner = KconfigScanner()
200 for defconfig in defconfigs:
201 queue.put(kconf_scanner.scan(defconfig))
202
203def read_queues(queues, params_list):
204 """Read the queues and append the data to the paramers list"""
205 for q in queues:
206 while not q.empty():
207 params_list.append(q.get())
208
209def scan_defconfigs(jobs=1):
210 """Collect board parameters for all defconfig files.
211
212 This function invokes multiple processes for faster processing.
213
214 Arguments:
215 jobs: The number of jobs to run simultaneously
216 """
217 all_defconfigs = []
218 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
219 for filename in fnmatch.filter(filenames, '*_defconfig'):
220 if fnmatch.fnmatch(filename, '.*'):
221 continue
222 all_defconfigs.append(os.path.join(dirpath, filename))
223
224 total_boards = len(all_defconfigs)
225 processes = []
226 queues = []
227 for i in range(jobs):
Tom Rini3bc14092019-09-20 17:42:07 -0400228 defconfigs = all_defconfigs[total_boards * i // jobs :
229 total_boards * (i + 1) // jobs]
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +0900230 q = multiprocessing.Queue(maxsize=-1)
231 p = multiprocessing.Process(target=scan_defconfigs_for_multiprocess,
232 args=(q, defconfigs))
233 p.start()
234 processes.append(p)
235 queues.append(q)
236
237 # The resulting data should be accumulated to this list
238 params_list = []
239
240 # Data in the queues should be retrieved preriodically.
241 # Otherwise, the queues would become full and subprocesses would get stuck.
242 while any([p.is_alive() for p in processes]):
243 read_queues(queues, params_list)
244 # sleep for a while until the queues are filled
245 time.sleep(SLEEP_TIME)
246
247 # Joining subprocesses just in case
248 # (All subprocesses should already have been finished)
249 for p in processes:
250 p.join()
251
252 # retrieve leftover data
253 read_queues(queues, params_list)
254
255 return params_list
256
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900257class MaintainersDatabase:
258
259 """The database of board status and maintainers."""
260
261 def __init__(self):
262 """Create an empty database."""
263 self.database = {}
264
265 def get_status(self, target):
266 """Return the status of the given board.
267
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +0900268 The board status is generally either 'Active' or 'Orphan'.
269 Display a warning message and return '-' if status information
270 is not found.
271
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900272 Returns:
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +0900273 'Active', 'Orphan' or '-'.
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900274 """
Masahiro Yamadab8828e82014-08-25 12:39:43 +0900275 if not target in self.database:
Tom Rini3bc14092019-09-20 17:42:07 -0400276 print("WARNING: no status info for '%s'" % target, file=sys.stderr)
Masahiro Yamadab8828e82014-08-25 12:39:43 +0900277 return '-'
278
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900279 tmp = self.database[target][0]
280 if tmp.startswith('Maintained'):
281 return 'Active'
Lokesh Vutladee7c682017-05-10 16:19:52 +0530282 elif tmp.startswith('Supported'):
283 return 'Active'
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900284 elif tmp.startswith('Orphan'):
285 return 'Orphan'
286 else:
Tom Rini3bc14092019-09-20 17:42:07 -0400287 print(("WARNING: %s: unknown status for '%s'" %
288 (tmp, target)), file=sys.stderr)
Masahiro Yamadab8828e82014-08-25 12:39:43 +0900289 return '-'
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900290
291 def get_maintainers(self, target):
292 """Return the maintainers of the given board.
293
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +0900294 Returns:
295 Maintainers of the board. If the board has two or more maintainers,
296 they are separated with colons.
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900297 """
Masahiro Yamadab8828e82014-08-25 12:39:43 +0900298 if not target in self.database:
Tom Rini3bc14092019-09-20 17:42:07 -0400299 print("WARNING: no maintainers for '%s'" % target, file=sys.stderr)
Masahiro Yamadab8828e82014-08-25 12:39:43 +0900300 return ''
301
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900302 return ':'.join(self.database[target][1])
303
304 def parse_file(self, file):
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +0900305 """Parse a MAINTAINERS file.
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900306
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +0900307 Parse a MAINTAINERS file and accumulates board status and
308 maintainers information.
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900309
310 Arguments:
311 file: MAINTAINERS file to be parsed
312 """
313 targets = []
314 maintainers = []
315 status = '-'
Tom Rini3bc14092019-09-20 17:42:07 -0400316 for line in open(file, encoding="utf-8"):
Masahiro Yamada5dff8442014-09-16 14:11:49 +0900317 # Check also commented maintainers
318 if line[:3] == '#M:':
319 line = line[1:]
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900320 tag, rest = line[:2], line[2:].strip()
321 if tag == 'M:':
322 maintainers.append(rest)
323 elif tag == 'F:':
324 # expand wildcard and filter by 'configs/*_defconfig'
325 for f in glob.glob(rest):
326 front, match, rear = f.partition('configs/')
327 if not front and match:
328 front, match, rear = rear.rpartition('_defconfig')
329 if match and not rear:
330 targets.append(front)
331 elif tag == 'S:':
332 status = rest
Masahiro Yamada9c2d60c2014-08-22 14:10:43 +0900333 elif line == '\n':
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900334 for target in targets:
335 self.database[target] = (status, maintainers)
336 targets = []
337 maintainers = []
338 status = '-'
339 if targets:
340 for target in targets:
341 self.database[target] = (status, maintainers)
342
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +0900343def insert_maintainers_info(params_list):
344 """Add Status and Maintainers information to the board parameters list.
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900345
346 Arguments:
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +0900347 params_list: A list of the board parameters
348 """
349 database = MaintainersDatabase()
350 for (dirpath, dirnames, filenames) in os.walk('.'):
351 if 'MAINTAINERS' in filenames:
352 database.parse_file(os.path.join(dirpath, 'MAINTAINERS'))
353
354 for i, params in enumerate(params_list):
355 target = params['target']
356 params['status'] = database.get_status(target)
357 params['maintainers'] = database.get_maintainers(target)
358 params_list[i] = params
359
360def format_and_output(params_list, output):
361 """Write board parameters into a file.
362
363 Columnate the board parameters, sort lines alphabetically,
364 and then write them to a file.
365
366 Arguments:
367 params_list: The list of board parameters
368 output: The path to the output file
369 """
370 FIELDS = ('status', 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
Tom Rinieeec0002022-03-24 17:18:06 -0400371 'maintainers')
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +0900372
373 # First, decide the width of each column
374 max_length = dict([ (f, 0) for f in FIELDS])
375 for params in params_list:
376 for f in FIELDS:
377 max_length[f] = max(max_length[f], len(params[f]))
378
379 output_lines = []
380 for params in params_list:
381 line = ''
382 for f in FIELDS:
383 # insert two spaces between fields like column -t would
384 line += ' ' + params[f].ljust(max_length[f])
385 output_lines.append(line.strip())
386
387 # ignore case when sorting
388 output_lines.sort(key=str.lower)
389
Tom Rini3bc14092019-09-20 17:42:07 -0400390 with open(output, 'w', encoding="utf-8") as f:
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +0900391 f.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n')
392
Simon Glass69bbdd12019-12-05 15:59:11 -0700393def gen_boards_cfg(output, jobs=1, force=False, quiet=False):
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +0900394 """Generate a board database file.
395
396 Arguments:
397 output: The name of the output file
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900398 jobs: The number of jobs to run simultaneously
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +0900399 force: Force to generate the output even if it is new
Simon Glass69bbdd12019-12-05 15:59:11 -0700400 quiet: True to avoid printing a message if nothing needs doing
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900401 """
Masahiro Yamada79d45d32014-08-25 12:39:46 +0900402 check_top_directory()
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +0900403
404 if not force and output_is_new(output):
Simon Glass69bbdd12019-12-05 15:59:11 -0700405 if not quiet:
406 print("%s is up to date. Nothing to do." % output)
Masahiro Yamadad1bf4af2014-08-25 12:39:47 +0900407 sys.exit(0)
408
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +0900409 params_list = scan_defconfigs(jobs)
410 insert_maintainers_info(params_list)
411 format_and_output(params_list, output)
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900412
413def main():
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +0900414 try:
415 cpu_count = multiprocessing.cpu_count()
416 except NotImplementedError:
417 cpu_count = 1
418
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900419 parser = optparse.OptionParser()
420 # Add options here
Masahiro Yamadad1bf4af2014-08-25 12:39:47 +0900421 parser.add_option('-f', '--force', action="store_true", default=False,
422 help='regenerate the output even if it is new')
Andre Przywara5ecdd522022-01-11 15:34:50 +0000423 parser.add_option('-j', '--jobs', type='int', default=min(cpu_count, 240),
Masahiro Yamadaf6c8f382014-09-01 19:57:38 +0900424 help='the number of jobs to run simultaneously')
425 parser.add_option('-o', '--output', default=OUTPUT_FILE,
426 help='output file [default=%s]' % OUTPUT_FILE)
Simon Glass69bbdd12019-12-05 15:59:11 -0700427 parser.add_option('-q', '--quiet', action="store_true", help='run silently')
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900428 (options, args) = parser.parse_args()
Masahiro Yamadad1bf4af2014-08-25 12:39:47 +0900429
Simon Glass69bbdd12019-12-05 15:59:11 -0700430 gen_boards_cfg(options.output, jobs=options.jobs, force=options.force,
431 quiet=options.quiet)
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900432
433if __name__ == '__main__':
434 main()