1#!/usr/bin/env python2 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 14Python 2.6 or later, but not Python 3.x is necessary to run this script. 15""" 16 17import errno 18import fnmatch 19import glob 20import multiprocessing 21import optparse 22import os 23import sys 24import tempfile 25import time 26 27sys.path.insert(1, os.path.join(os.path.dirname(__file__), 'buildman')) 28import kconfiglib 29 30### constant variables ### 31OUTPUT_FILE = 'boards.cfg' 32CONFIG_DIR = 'configs' 33SLEEP_TIME = 0.03 34COMMENT_BLOCK = '''# 35# List of boards 36# Automatically generated by %s: don't edit 37# 38# Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers 39 40''' % __file__ 41 42### helper functions ### 43def try_remove(f): 44 """Remove a file ignoring 'No such file or directory' error.""" 45 try: 46 os.remove(f) 47 except OSError as exception: 48 # Ignore 'No such file or directory' error 49 if exception.errno != errno.ENOENT: 50 raise 51 52def 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.') 57 58def output_is_new(output): 59 """Check if the output file is up to date. 60 61 Returns: 62 True if the given output file exists and is newer than any of 63 *_defconfig, MAINTAINERS and Kconfig*. False otherwise. 64 """ 65 try: 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 70 return False 71 else: 72 raise 73 74 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR): 75 for filename in fnmatch.filter(filenames, '*_defconfig'): 76 if fnmatch.fnmatch(filename, '.*'): 77 continue 78 filepath = os.path.join(dirpath, filename) 79 if ctime < os.path.getctime(filepath): 80 return False 81 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'): 87 continue 88 filepath = os.path.join(dirpath, filename) 89 if ctime < os.path.getctime(filepath): 90 return False 91 92 # Detect a board that has been removed since the current board database 93 # was generated 94 with open(output) as f: 95 for line in f: 96 if line[0] == '#' or line == '\n': 97 continue 98 defconfig = line.split()[6] + '_defconfig' 99 if not os.path.exists(os.path.join(CONFIG_DIR, defconfig)): 100 return False 101 102 return True 103 104### classes ### 105class KconfigScanner: 106 107 """Kconfig scanner.""" 108 109 ### constant variable only used in this class ### 110 _SYMBOL_TABLE = { 111 'arch' : 'SYS_ARCH', 112 'cpu' : 'SYS_CPU', 113 'soc' : 'SYS_SOC', 114 'vendor' : 'SYS_VENDOR', 115 'board' : 'SYS_BOARD', 116 'config' : 'SYS_CONFIG_NAME', 117 'options' : 'SYS_EXTRA_OPTIONS' 118 } 119 120 def __init__(self): 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) 127 128 def __del__(self): 129 """Delete a leftover temporary file before exit. 130 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. 135 """ 136 if hasattr(self, '_tmpfile') and self._tmpfile: 137 try_remove(self._tmpfile) 138 139 def scan(self, defconfig): 140 """Load a defconfig file to obtain board parameters. 141 142 Arguments: 143 defconfig: path to the defconfig file to be processed 144 145 Returns: 146 A dictionary of board parameters. It has a form of: 147 { 148 'arch': <arch_name>, 149 'cpu': <cpu_name>, 150 'soc': <soc_name>, 151 'vendor': <vendor_name>, 152 'board': <board_name>, 153 'target': <target_name>, 154 'config': <config_header_name>, 155 'options': <extra_options> 156 } 157 """ 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_') 163 if colon == -1: 164 f.write(line) 165 else: 166 f.write(line[colon + 1:]) 167 168 warnings = self._conf.load_config(self._tmpfile) 169 if warnings: 170 for warning in warnings: 171 print '%s: %s' % (defconfig, warning) 172 173 try_remove(self._tmpfile) 174 self._tmpfile = None 175 176 params = {} 177 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() 182 if value: 183 params[key] = value 184 else: 185 params[key] = '-' 186 187 defconfig = os.path.basename(defconfig) 188 params['target'], match, rear = defconfig.partition('_defconfig') 189 assert match and not rear, '%s : invalid defconfig' % defconfig 190 191 # fix-up for aarch64 192 if params['arch'] == 'arm' and params['cpu'] == 'armv8': 193 params['arch'] = 'aarch64' 194 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'] 202 203 return params 204 205def scan_defconfigs_for_multiprocess(queue, defconfigs): 206 """Scan defconfig files and queue their board parameters 207 208 This function is intended to be passed to 209 multiprocessing.Process() constructor. 210 211 Arguments: 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. 215 """ 216 kconf_scanner = KconfigScanner() 217 for defconfig in defconfigs: 218 queue.put(kconf_scanner.scan(defconfig)) 219 220def read_queues(queues, params_list): 221 """Read the queues and append the data to the paramers list""" 222 for q in queues: 223 while not q.empty(): 224 params_list.append(q.get()) 225 226def scan_defconfigs(jobs=1): 227 """Collect board parameters for all defconfig files. 228 229 This function invokes multiple processes for faster processing. 230 231 Arguments: 232 jobs: The number of jobs to run simultaneously 233 """ 234 all_defconfigs = [] 235 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR): 236 for filename in fnmatch.filter(filenames, '*_defconfig'): 237 if fnmatch.fnmatch(filename, '.*'): 238 continue 239 all_defconfigs.append(os.path.join(dirpath, filename)) 240 241 total_boards = len(all_defconfigs) 242 processes = [] 243 queues = [] 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)) 250 p.start() 251 processes.append(p) 252 queues.append(q) 253 254 # The resulting data should be accumulated to this list 255 params_list = [] 256 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) 263 264 # Joining subprocesses just in case 265 # (All subprocesses should already have been finished) 266 for p in processes: 267 p.join() 268 269 # retrieve leftover data 270 read_queues(queues, params_list) 271 272 return params_list 273 274class MaintainersDatabase: 275 276 """The database of board status and maintainers.""" 277 278 def __init__(self): 279 """Create an empty database.""" 280 self.database = {} 281 282 def get_status(self, target): 283 """Return the status of the given board. 284 285 The board status is generally either 'Active' or 'Orphan'. 286 Display a warning message and return '-' if status information 287 is not found. 288 289 Returns: 290 'Active', 'Orphan' or '-'. 291 """ 292 if not target in self.database: 293 print >> sys.stderr, "WARNING: no status info for '%s'" % target 294 return '-' 295 296 tmp = self.database[target][0] 297 if tmp.startswith('Maintained'): 298 return 'Active' 299 elif tmp.startswith('Supported'): 300 return 'Active' 301 elif tmp.startswith('Orphan'): 302 return 'Orphan' 303 else: 304 print >> sys.stderr, ("WARNING: %s: unknown status for '%s'" % 305 (tmp, target)) 306 return '-' 307 308 def get_maintainers(self, target): 309 """Return the maintainers of the given board. 310 311 Returns: 312 Maintainers of the board. If the board has two or more maintainers, 313 they are separated with colons. 314 """ 315 if not target in self.database: 316 print >> sys.stderr, "WARNING: no maintainers for '%s'" % target 317 return '' 318 319 return ':'.join(self.database[target][1]) 320 321 def parse_file(self, file): 322 """Parse a MAINTAINERS file. 323 324 Parse a MAINTAINERS file and accumulates board status and 325 maintainers information. 326 327 Arguments: 328 file: MAINTAINERS file to be parsed 329 """ 330 targets = [] 331 maintainers = [] 332 status = '-' 333 for line in open(file): 334 # Check also commented maintainers 335 if line[:3] == '#M:': 336 line = line[1:] 337 tag, rest = line[:2], line[2:].strip() 338 if tag == 'M:': 339 maintainers.append(rest) 340 elif tag == 'F:': 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) 348 elif tag == 'S:': 349 status = rest 350 elif line == '\n': 351 for target in targets: 352 self.database[target] = (status, maintainers) 353 targets = [] 354 maintainers = [] 355 status = '-' 356 if targets: 357 for target in targets: 358 self.database[target] = (status, maintainers) 359 360def insert_maintainers_info(params_list): 361 """Add Status and Maintainers information to the board parameters list. 362 363 Arguments: 364 params_list: A list of the board parameters 365 """ 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')) 370 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 376 377def format_and_output(params_list, output): 378 """Write board parameters into a file. 379 380 Columnate the board parameters, sort lines alphabetically, 381 and then write them to a file. 382 383 Arguments: 384 params_list: The list of board parameters 385 output: The path to the output file 386 """ 387 FIELDS = ('status', 'arch', 'cpu', 'soc', 'vendor', 'board', 'target', 388 'options', 'maintainers') 389 390 # First, decide the width of each column 391 max_length = dict([ (f, 0) for f in FIELDS]) 392 for params in params_list: 393 for f in FIELDS: 394 max_length[f] = max(max_length[f], len(params[f])) 395 396 output_lines = [] 397 for params in params_list: 398 line = '' 399 for f in FIELDS: 400 # insert two spaces between fields like column -t would 401 line += ' ' + params[f].ljust(max_length[f]) 402 output_lines.append(line.strip()) 403 404 # ignore case when sorting 405 output_lines.sort(key=str.lower) 406 407 with open(output, 'w') as f: 408 f.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n') 409 410def gen_boards_cfg(output, jobs=1, force=False): 411 """Generate a board database file. 412 413 Arguments: 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 417 """ 418 check_top_directory() 419 420 if not force and output_is_new(output): 421 print "%s is up to date. Nothing to do." % output 422 sys.exit(0) 423 424 params_list = scan_defconfigs(jobs) 425 insert_maintainers_info(params_list) 426 format_and_output(params_list, output) 427 428def main(): 429 try: 430 cpu_count = multiprocessing.cpu_count() 431 except NotImplementedError: 432 cpu_count = 1 433 434 parser = optparse.OptionParser() 435 # Add options here 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() 443 444 gen_boards_cfg(options.output, jobs=options.jobs, force=options.force) 445 446if __name__ == '__main__': 447 main() 448