1#!/usr/bin/env python3 2# SPDX-License-Identifier: GPL-2.0+ 3# 4# Author: Masahiro Yamada <yamada.m@jp.panasonic.com> 5# 6 7""" 8Converter from Kconfig and MAINTAINERS to a board database. 9 10Run 'tools/genboardscfg.py' to create a board database. 11 12Run 'tools/genboardscfg.py -h' for available options. 13""" 14 15import errno 16import fnmatch 17import glob 18import multiprocessing 19import optparse 20import os 21import sys 22import tempfile 23import time 24 25from buildman import kconfiglib 26 27### constant variables ### 28OUTPUT_FILE = 'boards.cfg' 29CONFIG_DIR = 'configs' 30SLEEP_TIME = 0.03 31COMMENT_BLOCK = '''# 32# List of boards 33# Automatically generated by %s: don't edit 34# 35# Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers 36 37''' % __file__ 38 39### helper functions ### 40def try_remove(f): 41 """Remove a file ignoring 'No such file or directory' error.""" 42 try: 43 os.remove(f) 44 except OSError as exception: 45 # Ignore 'No such file or directory' error 46 if exception.errno != errno.ENOENT: 47 raise 48 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): 53 sys.exit('Please run at the top of source directory.') 54 55def output_is_new(output): 56 """Check if the output file is up to date. 57 58 Returns: 59 True if the given output file exists and is newer than any of 60 *_defconfig, MAINTAINERS and Kconfig*. False otherwise. 61 """ 62 try: 63 ctime = os.path.getctime(output) 64 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 89 # Detect a board that has been removed since the current board database 90 # was generated 91 with open(output, encoding="utf-8") as f: 92 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 101### classes ### 102class 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', 114 'options' : 'SYS_EXTRA_OPTIONS' 115 } 116 117 def __init__(self): 118 """Scan all the Kconfig files and create a Kconfig object.""" 119 # Define environment variables referenced from Kconfig 120 os.environ['srctree'] = os.getcwd() 121 os.environ['UBOOTVERSION'] = 'dummy' 122 os.environ['KCONFIG_OBJDIR'] = '' 123 self._conf = kconfiglib.Kconfig(warn=False) 124 125 def __del__(self): 126 """Delete a leftover temporary file before exit. 127 128 The scan() method of this class creates a temporay file and deletes 129 it on success. If scan() method throws an exception on the way, 130 the temporary file might be left over. In that case, it should be 131 deleted in this destructor. 132 """ 133 if hasattr(self, '_tmpfile') and self._tmpfile: 134 try_remove(self._tmpfile) 135 136 def scan(self, defconfig): 137 """Load a defconfig file to obtain board parameters. 138 139 Arguments: 140 defconfig: path to the defconfig file to be processed 141 142 Returns: 143 A dictionary of board parameters. It has a form of: 144 { 145 'arch': <arch_name>, 146 'cpu': <cpu_name>, 147 'soc': <soc_name>, 148 'vendor': <vendor_name>, 149 'board': <board_name>, 150 'target': <target_name>, 151 'config': <config_header_name>, 152 'options': <extra_options> 153 } 154 """ 155 # strip special prefixes and save it in a temporary file 156 fd, self._tmpfile = tempfile.mkstemp() 157 with os.fdopen(fd, 'w') as f: 158 for line in open(defconfig): 159 colon = line.find(':CONFIG_') 160 if colon == -1: 161 f.write(line) 162 else: 163 f.write(line[colon + 1:]) 164 165 self._conf.load_config(self._tmpfile) 166 try_remove(self._tmpfile) 167 self._tmpfile = None 168 169 params = {} 170 171 # Get the value of CONFIG_SYS_ARCH, CONFIG_SYS_CPU, ... etc. 172 # Set '-' if the value is empty. 173 for key, symbol in list(self._SYMBOL_TABLE.items()): 174 value = self._conf.syms.get(symbol).str_value 175 if value: 176 params[key] = value 177 else: 178 params[key] = '-' 179 180 defconfig = os.path.basename(defconfig) 181 params['target'], match, rear = defconfig.partition('_defconfig') 182 assert match and not rear, '%s : invalid defconfig' % defconfig 183 184 # fix-up for aarch64 185 if params['arch'] == 'arm' and params['cpu'] == 'armv8': 186 params['arch'] = 'aarch64' 187 188 # fix-up options field. It should have the form: 189 # <config name>[:comma separated config options] 190 if params['options'] != '-': 191 params['options'] = params['config'] + ':' + \ 192 params['options'].replace(r'\"', '"') 193 elif params['config'] != params['target']: 194 params['options'] = params['config'] 195 196 return params 197 198def scan_defconfigs_for_multiprocess(queue, defconfigs): 199 """Scan defconfig files and queue their board parameters 200 201 This function is intended to be passed to 202 multiprocessing.Process() constructor. 203 204 Arguments: 205 queue: An instance of multiprocessing.Queue(). 206 The resulting board parameters are written into it. 207 defconfigs: A sequence of defconfig files to be scanned. 208 """ 209 kconf_scanner = KconfigScanner() 210 for defconfig in defconfigs: 211 queue.put(kconf_scanner.scan(defconfig)) 212 213def read_queues(queues, params_list): 214 """Read the queues and append the data to the paramers list""" 215 for q in queues: 216 while not q.empty(): 217 params_list.append(q.get()) 218 219def scan_defconfigs(jobs=1): 220 """Collect board parameters for all defconfig files. 221 222 This function invokes multiple processes for faster processing. 223 224 Arguments: 225 jobs: The number of jobs to run simultaneously 226 """ 227 all_defconfigs = [] 228 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR): 229 for filename in fnmatch.filter(filenames, '*_defconfig'): 230 if fnmatch.fnmatch(filename, '.*'): 231 continue 232 all_defconfigs.append(os.path.join(dirpath, filename)) 233 234 total_boards = len(all_defconfigs) 235 processes = [] 236 queues = [] 237 for i in range(jobs): 238 defconfigs = all_defconfigs[total_boards * i // jobs : 239 total_boards * (i + 1) // jobs] 240 q = multiprocessing.Queue(maxsize=-1) 241 p = multiprocessing.Process(target=scan_defconfigs_for_multiprocess, 242 args=(q, defconfigs)) 243 p.start() 244 processes.append(p) 245 queues.append(q) 246 247 # The resulting data should be accumulated to this list 248 params_list = [] 249 250 # Data in the queues should be retrieved preriodically. 251 # Otherwise, the queues would become full and subprocesses would get stuck. 252 while any([p.is_alive() for p in processes]): 253 read_queues(queues, params_list) 254 # sleep for a while until the queues are filled 255 time.sleep(SLEEP_TIME) 256 257 # Joining subprocesses just in case 258 # (All subprocesses should already have been finished) 259 for p in processes: 260 p.join() 261 262 # retrieve leftover data 263 read_queues(queues, params_list) 264 265 return params_list 266 267class MaintainersDatabase: 268 269 """The database of board status and maintainers.""" 270 271 def __init__(self): 272 """Create an empty database.""" 273 self.database = {} 274 275 def get_status(self, target): 276 """Return the status of the given board. 277 278 The board status is generally either 'Active' or 'Orphan'. 279 Display a warning message and return '-' if status information 280 is not found. 281 282 Returns: 283 'Active', 'Orphan' or '-'. 284 """ 285 if not target in self.database: 286 print("WARNING: no status info for '%s'" % target, file=sys.stderr) 287 return '-' 288 289 tmp = self.database[target][0] 290 if tmp.startswith('Maintained'): 291 return 'Active' 292 elif tmp.startswith('Supported'): 293 return 'Active' 294 elif tmp.startswith('Orphan'): 295 return 'Orphan' 296 else: 297 print(("WARNING: %s: unknown status for '%s'" % 298 (tmp, target)), file=sys.stderr) 299 return '-' 300 301 def get_maintainers(self, target): 302 """Return the maintainers of the given board. 303 304 Returns: 305 Maintainers of the board. If the board has two or more maintainers, 306 they are separated with colons. 307 """ 308 if not target in self.database: 309 print("WARNING: no maintainers for '%s'" % target, file=sys.stderr) 310 return '' 311 312 return ':'.join(self.database[target][1]) 313 314 def parse_file(self, file): 315 """Parse a MAINTAINERS file. 316 317 Parse a MAINTAINERS file and accumulates board status and 318 maintainers information. 319 320 Arguments: 321 file: MAINTAINERS file to be parsed 322 """ 323 targets = [] 324 maintainers = [] 325 status = '-' 326 for line in open(file, encoding="utf-8"): 327 # Check also commented maintainers 328 if line[:3] == '#M:': 329 line = line[1:] 330 tag, rest = line[:2], line[2:].strip() 331 if tag == 'M:': 332 maintainers.append(rest) 333 elif tag == 'F:': 334 # expand wildcard and filter by 'configs/*_defconfig' 335 for f in glob.glob(rest): 336 front, match, rear = f.partition('configs/') 337 if not front and match: 338 front, match, rear = rear.rpartition('_defconfig') 339 if match and not rear: 340 targets.append(front) 341 elif tag == 'S:': 342 status = rest 343 elif line == '\n': 344 for target in targets: 345 self.database[target] = (status, maintainers) 346 targets = [] 347 maintainers = [] 348 status = '-' 349 if targets: 350 for target in targets: 351 self.database[target] = (status, maintainers) 352 353def insert_maintainers_info(params_list): 354 """Add Status and Maintainers information to the board parameters list. 355 356 Arguments: 357 params_list: A list of the board parameters 358 """ 359 database = MaintainersDatabase() 360 for (dirpath, dirnames, filenames) in os.walk('.'): 361 if 'MAINTAINERS' in filenames: 362 database.parse_file(os.path.join(dirpath, 'MAINTAINERS')) 363 364 for i, params in enumerate(params_list): 365 target = params['target'] 366 params['status'] = database.get_status(target) 367 params['maintainers'] = database.get_maintainers(target) 368 params_list[i] = params 369 370def format_and_output(params_list, output): 371 """Write board parameters into a file. 372 373 Columnate the board parameters, sort lines alphabetically, 374 and then write them to a file. 375 376 Arguments: 377 params_list: The list of board parameters 378 output: The path to the output file 379 """ 380 FIELDS = ('status', 'arch', 'cpu', 'soc', 'vendor', 'board', 'target', 381 'options', 'maintainers') 382 383 # First, decide the width of each column 384 max_length = dict([ (f, 0) for f in FIELDS]) 385 for params in params_list: 386 for f in FIELDS: 387 max_length[f] = max(max_length[f], len(params[f])) 388 389 output_lines = [] 390 for params in params_list: 391 line = '' 392 for f in FIELDS: 393 # insert two spaces between fields like column -t would 394 line += ' ' + params[f].ljust(max_length[f]) 395 output_lines.append(line.strip()) 396 397 # ignore case when sorting 398 output_lines.sort(key=str.lower) 399 400 with open(output, 'w', encoding="utf-8") as f: 401 f.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n') 402 403def gen_boards_cfg(output, jobs=1, force=False, quiet=False): 404 """Generate a board database file. 405 406 Arguments: 407 output: The name of the output file 408 jobs: The number of jobs to run simultaneously 409 force: Force to generate the output even if it is new 410 quiet: True to avoid printing a message if nothing needs doing 411 """ 412 check_top_directory() 413 414 if not force and output_is_new(output): 415 if not quiet: 416 print("%s is up to date. Nothing to do." % output) 417 sys.exit(0) 418 419 params_list = scan_defconfigs(jobs) 420 insert_maintainers_info(params_list) 421 format_and_output(params_list, output) 422 423def main(): 424 try: 425 cpu_count = multiprocessing.cpu_count() 426 except NotImplementedError: 427 cpu_count = 1 428 429 parser = optparse.OptionParser() 430 # Add options here 431 parser.add_option('-f', '--force', action="store_true", default=False, 432 help='regenerate the output even if it is new') 433 parser.add_option('-j', '--jobs', type='int', default=cpu_count, 434 help='the number of jobs to run simultaneously') 435 parser.add_option('-o', '--output', default=OUTPUT_FILE, 436 help='output file [default=%s]' % OUTPUT_FILE) 437 parser.add_option('-q', '--quiet', action="store_true", help='run silently') 438 (options, args) = parser.parse_args() 439 440 gen_boards_cfg(options.output, jobs=options.jobs, force=options.force, 441 quiet=options.quiet) 442 443if __name__ == '__main__': 444 main() 445