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