blob: 4f6382bc7ca92c0b80f353537d34fe47f844c3ff [file] [log] [blame]
Tom Rini58214cc2019-09-20 17:42:07 -04001#!/usr/bin/env python3
Tom Rini10e47792018-05-06 17:58:06 -04002# SPDX-License-Identifier: GPL-2.0+
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +09003#
4# Author: Masahiro Yamada <yamada.m@jp.panasonic.com>
5#
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +09006
7"""
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +09008Converter from Kconfig and MAINTAINERS to a board database.
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +09009
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +090010Run 'tools/genboardscfg.py' to create a board database.
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +090011
12Run 'tools/genboardscfg.py -h' for available options.
13"""
14
15import errno
16import fnmatch
17import glob
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +090018import multiprocessing
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +090019import optparse
20import os
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +090021import sys
22import tempfile
23import time
24
Peng Fanf742ed32018-07-24 14:27:18 +080025sys.path.insert(1, os.path.join(os.path.dirname(__file__), 'buildman'))
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +090026import kconfiglib
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +090027
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +090028### constant variables ###
29OUTPUT_FILE = 'boards.cfg'
30CONFIG_DIR = 'configs'
31SLEEP_TIME = 0.03
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +090032COMMENT_BLOCK = '''#
33# List of boards
34# Automatically generated by %s: don't edit
35#
Masahiro Yamada8d141bd2014-08-06 13:42:34 +090036# Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +090037
38''' % __file__
39
40### helper functions ###
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +090041def try_remove(f):
42 """Remove a file ignoring 'No such file or directory' error."""
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +090043 try:
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +090044 os.remove(f)
45 except OSError as exception:
46 # Ignore 'No such file or directory' error
47 if exception.errno != errno.ENOENT:
48 raise
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +090049
50def 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):
Masahiro Yamada880828d2014-08-16 00:59:26 +090054 sys.exit('Please run at the top of source directory.')
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +090055
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +090056def output_is_new(output):
57 """Check if the output file is up to date.
Masahiro Yamadac2d99dd2014-08-25 12:39:47 +090058
59 Returns:
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +090060 True if the given output file exists and is newer than any of
Masahiro Yamadac2d99dd2014-08-25 12:39:47 +090061 *_defconfig, MAINTAINERS and Kconfig*. False otherwise.
62 """
63 try:
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +090064 ctime = os.path.getctime(output)
Masahiro Yamadac2d99dd2014-08-25 12:39:47 +090065 except OSError as exception:
66 if exception.errno == errno.ENOENT:
67 # return False on 'No such file or directory' error
68 return False
69 else:
70 raise
71
72 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
73 for filename in fnmatch.filter(filenames, '*_defconfig'):
74 if fnmatch.fnmatch(filename, '.*'):
75 continue
76 filepath = os.path.join(dirpath, filename)
77 if ctime < os.path.getctime(filepath):
78 return False
79
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'):
85 continue
86 filepath = os.path.join(dirpath, filename)
87 if ctime < os.path.getctime(filepath):
88 return False
89
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +090090 # Detect a board that has been removed since the current board database
Masahiro Yamadac2d99dd2014-08-25 12:39:47 +090091 # was generated
Tom Rini58214cc2019-09-20 17:42:07 -040092 with open(output, encoding="utf-8") as f:
Masahiro Yamadac2d99dd2014-08-25 12:39:47 +090093 for line in f:
94 if line[0] == '#' or line == '\n':
95 continue
96 defconfig = line.split()[6] + '_defconfig'
97 if not os.path.exists(os.path.join(CONFIG_DIR, defconfig)):
98 return False
99
100 return True
101
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900102### classes ###
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900103class KconfigScanner:
104
105 """Kconfig scanner."""
106
107 ### constant variable only used in this class ###
108 _SYMBOL_TABLE = {
109 'arch' : 'SYS_ARCH',
110 'cpu' : 'SYS_CPU',
111 'soc' : 'SYS_SOC',
112 'vendor' : 'SYS_VENDOR',
113 'board' : 'SYS_BOARD',
114 'config' : 'SYS_CONFIG_NAME',
115 'options' : 'SYS_EXTRA_OPTIONS'
116 }
117
118 def __init__(self):
Tom Rini3c5f4152019-09-20 17:42:09 -0400119 """Scan all the Kconfig files and create a Kconfig object."""
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900120 # Define environment variables referenced from Kconfig
121 os.environ['srctree'] = os.getcwd()
122 os.environ['UBOOTVERSION'] = 'dummy'
123 os.environ['KCONFIG_OBJDIR'] = ''
Tom Rini3c5f4152019-09-20 17:42:09 -0400124 self._conf = kconfiglib.Kconfig(warn=False)
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900125
126 def __del__(self):
127 """Delete a leftover temporary file before exit.
128
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.
133 """
134 if hasattr(self, '_tmpfile') and self._tmpfile:
135 try_remove(self._tmpfile)
136
137 def scan(self, defconfig):
138 """Load a defconfig file to obtain board parameters.
139
140 Arguments:
141 defconfig: path to the defconfig file to be processed
142
143 Returns:
144 A dictionary of board parameters. It has a form of:
145 {
146 'arch': <arch_name>,
147 'cpu': <cpu_name>,
148 'soc': <soc_name>,
149 'vendor': <vendor_name>,
150 'board': <board_name>,
151 'target': <target_name>,
152 'config': <config_header_name>,
153 'options': <extra_options>
154 }
155 """
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_')
161 if colon == -1:
162 f.write(line)
163 else:
164 f.write(line[colon + 1:])
165
Tom Rinia6bce032019-09-20 17:42:08 -0400166 self._conf.load_config(self._tmpfile)
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900167 try_remove(self._tmpfile)
168 self._tmpfile = None
169
170 params = {}
171
172 # Get the value of CONFIG_SYS_ARCH, CONFIG_SYS_CPU, ... etc.
173 # Set '-' if the value is empty.
Tom Rini58214cc2019-09-20 17:42:07 -0400174 for key, symbol in list(self._SYMBOL_TABLE.items()):
Tom Rini3c5f4152019-09-20 17:42:09 -0400175 value = self._conf.syms.get(symbol).str_value
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900176 if value:
177 params[key] = value
178 else:
179 params[key] = '-'
180
181 defconfig = os.path.basename(defconfig)
182 params['target'], match, rear = defconfig.partition('_defconfig')
183 assert match and not rear, '%s : invalid defconfig' % defconfig
184
185 # fix-up for aarch64
186 if params['arch'] == 'arm' and params['cpu'] == 'armv8':
187 params['arch'] = 'aarch64'
188
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']
196
197 return params
198
199def scan_defconfigs_for_multiprocess(queue, defconfigs):
200 """Scan defconfig files and queue their board parameters
201
202 This function is intended to be passed to
203 multiprocessing.Process() constructor.
204
205 Arguments:
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.
209 """
210 kconf_scanner = KconfigScanner()
211 for defconfig in defconfigs:
212 queue.put(kconf_scanner.scan(defconfig))
213
214def read_queues(queues, params_list):
215 """Read the queues and append the data to the paramers list"""
216 for q in queues:
217 while not q.empty():
218 params_list.append(q.get())
219
220def scan_defconfigs(jobs=1):
221 """Collect board parameters for all defconfig files.
222
223 This function invokes multiple processes for faster processing.
224
225 Arguments:
226 jobs: The number of jobs to run simultaneously
227 """
228 all_defconfigs = []
229 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
230 for filename in fnmatch.filter(filenames, '*_defconfig'):
231 if fnmatch.fnmatch(filename, '.*'):
232 continue
233 all_defconfigs.append(os.path.join(dirpath, filename))
234
235 total_boards = len(all_defconfigs)
236 processes = []
237 queues = []
238 for i in range(jobs):
Tom Rini58214cc2019-09-20 17:42:07 -0400239 defconfigs = all_defconfigs[total_boards * i // jobs :
240 total_boards * (i + 1) // jobs]
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900241 q = multiprocessing.Queue(maxsize=-1)
242 p = multiprocessing.Process(target=scan_defconfigs_for_multiprocess,
243 args=(q, defconfigs))
244 p.start()
245 processes.append(p)
246 queues.append(q)
247
248 # The resulting data should be accumulated to this list
249 params_list = []
250
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)
257
258 # Joining subprocesses just in case
259 # (All subprocesses should already have been finished)
260 for p in processes:
261 p.join()
262
263 # retrieve leftover data
264 read_queues(queues, params_list)
265
266 return params_list
267
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900268class MaintainersDatabase:
269
270 """The database of board status and maintainers."""
271
272 def __init__(self):
273 """Create an empty database."""
274 self.database = {}
275
276 def get_status(self, target):
277 """Return the status of the given board.
278
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900279 The board status is generally either 'Active' or 'Orphan'.
280 Display a warning message and return '-' if status information
281 is not found.
282
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900283 Returns:
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900284 'Active', 'Orphan' or '-'.
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900285 """
Masahiro Yamadab3c529b2014-08-25 12:39:43 +0900286 if not target in self.database:
Tom Rini58214cc2019-09-20 17:42:07 -0400287 print("WARNING: no status info for '%s'" % target, file=sys.stderr)
Masahiro Yamadab3c529b2014-08-25 12:39:43 +0900288 return '-'
289
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900290 tmp = self.database[target][0]
291 if tmp.startswith('Maintained'):
292 return 'Active'
Lokesh Vutla2ff00bb2017-05-10 16:19:52 +0530293 elif tmp.startswith('Supported'):
294 return 'Active'
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900295 elif tmp.startswith('Orphan'):
296 return 'Orphan'
297 else:
Tom Rini58214cc2019-09-20 17:42:07 -0400298 print(("WARNING: %s: unknown status for '%s'" %
299 (tmp, target)), file=sys.stderr)
Masahiro Yamadab3c529b2014-08-25 12:39:43 +0900300 return '-'
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900301
302 def get_maintainers(self, target):
303 """Return the maintainers of the given board.
304
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900305 Returns:
306 Maintainers of the board. If the board has two or more maintainers,
307 they are separated with colons.
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900308 """
Masahiro Yamadab3c529b2014-08-25 12:39:43 +0900309 if not target in self.database:
Tom Rini58214cc2019-09-20 17:42:07 -0400310 print("WARNING: no maintainers for '%s'" % target, file=sys.stderr)
Masahiro Yamadab3c529b2014-08-25 12:39:43 +0900311 return ''
312
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900313 return ':'.join(self.database[target][1])
314
315 def parse_file(self, file):
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900316 """Parse a MAINTAINERS file.
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900317
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900318 Parse a MAINTAINERS file and accumulates board status and
319 maintainers information.
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900320
321 Arguments:
322 file: MAINTAINERS file to be parsed
323 """
324 targets = []
325 maintainers = []
326 status = '-'
Tom Rini58214cc2019-09-20 17:42:07 -0400327 for line in open(file, encoding="utf-8"):
Masahiro Yamada93624242014-09-16 14:11:49 +0900328 # Check also commented maintainers
329 if line[:3] == '#M:':
330 line = line[1:]
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900331 tag, rest = line[:2], line[2:].strip()
332 if tag == 'M:':
333 maintainers.append(rest)
334 elif tag == 'F:':
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)
342 elif tag == 'S:':
343 status = rest
Masahiro Yamadaba133b22014-08-22 14:10:43 +0900344 elif line == '\n':
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900345 for target in targets:
346 self.database[target] = (status, maintainers)
347 targets = []
348 maintainers = []
349 status = '-'
350 if targets:
351 for target in targets:
352 self.database[target] = (status, maintainers)
353
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900354def insert_maintainers_info(params_list):
355 """Add Status and Maintainers information to the board parameters list.
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900356
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900357 Arguments:
358 params_list: A list of the board parameters
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900359 """
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900360 database = MaintainersDatabase()
361 for (dirpath, dirnames, filenames) in os.walk('.'):
362 if 'MAINTAINERS' in filenames:
363 database.parse_file(os.path.join(dirpath, 'MAINTAINERS'))
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900364
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900365 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
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900370
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900371def format_and_output(params_list, output):
372 """Write board parameters into a file.
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900373
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900374 Columnate the board parameters, sort lines alphabetically,
375 and then write them to a file.
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900376
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900377 Arguments:
378 params_list: The list of board parameters
379 output: The path to the output file
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900380 """
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900381 FIELDS = ('status', 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
382 'options', 'maintainers')
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900383
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900384 # First, decide the width of each column
385 max_length = dict([ (f, 0) for f in FIELDS])
386 for params in params_list:
387 for f in FIELDS:
388 max_length[f] = max(max_length[f], len(params[f]))
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900389
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900390 output_lines = []
391 for params in params_list:
392 line = ''
393 for f in FIELDS:
394 # insert two spaces between fields like column -t would
395 line += ' ' + params[f].ljust(max_length[f])
396 output_lines.append(line.strip())
Masahiro Yamada11748a62014-08-25 12:39:48 +0900397
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900398 # ignore case when sorting
399 output_lines.sort(key=str.lower)
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900400
Tom Rini58214cc2019-09-20 17:42:07 -0400401 with open(output, 'w', encoding="utf-8") as f:
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900402 f.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n')
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900403
Simon Glass11ece152019-12-05 15:59:11 -0700404def gen_boards_cfg(output, jobs=1, force=False, quiet=False):
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900405 """Generate a board database file.
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900406
407 Arguments:
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900408 output: The name of the output file
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900409 jobs: The number of jobs to run simultaneously
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900410 force: Force to generate the output even if it is new
Simon Glass11ece152019-12-05 15:59:11 -0700411 quiet: True to avoid printing a message if nothing needs doing
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900412 """
Masahiro Yamada2b093192014-08-25 12:39:46 +0900413 check_top_directory()
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900414
415 if not force and output_is_new(output):
Simon Glass11ece152019-12-05 15:59:11 -0700416 if not quiet:
417 print("%s is up to date. Nothing to do." % output)
Masahiro Yamadac2d99dd2014-08-25 12:39:47 +0900418 sys.exit(0)
419
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900420 params_list = scan_defconfigs(jobs)
421 insert_maintainers_info(params_list)
422 format_and_output(params_list, output)
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900423
424def main():
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900425 try:
426 cpu_count = multiprocessing.cpu_count()
427 except NotImplementedError:
428 cpu_count = 1
429
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900430 parser = optparse.OptionParser()
431 # Add options here
Masahiro Yamadac2d99dd2014-08-25 12:39:47 +0900432 parser.add_option('-f', '--force', action="store_true", default=False,
433 help='regenerate the output even if it is new')
Masahiro Yamada6a1b97a2014-09-01 19:57:38 +0900434 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)
Simon Glass11ece152019-12-05 15:59:11 -0700438 parser.add_option('-q', '--quiet', action="store_true", help='run silently')
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900439 (options, args) = parser.parse_args()
Masahiro Yamadac2d99dd2014-08-25 12:39:47 +0900440
Simon Glass11ece152019-12-05 15:59:11 -0700441 gen_boards_cfg(options.output, jobs=options.jobs, force=options.force,
442 quiet=options.quiet)
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900443
444if __name__ == '__main__':
445 main()