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() 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 self._conf.load_config(self._tmpfile) 170 171 try_remove(self._tmpfile) 172 self._tmpfile = None 173 174 params = {} 175 176 # Get the value of CONFIG_SYS_ARCH, CONFIG_SYS_CPU, ... etc. 177 # Set '-' if the value is empty. 178 for key, symbol in self._SYMBOL_TABLE.items(): 179 value = self._conf.get_symbol(symbol).get_value() 180 if value: 181 params[key] = value 182 else: 183 params[key] = '-' 184 185 defconfig = os.path.basename(defconfig) 186 params['target'], match, rear = defconfig.partition('_defconfig') 187 assert match and not rear, '%s : invalid defconfig' % defconfig 188 189 # fix-up for aarch64 190 if params['arch'] == 'arm' and params['cpu'] == 'armv8': 191 params['arch'] = 'aarch64' 192 193 # fix-up options field. It should have the form: 194 # <config name>[:comma separated config options] 195 if params['options'] != '-': 196 params['options'] = params['config'] + ':' + \ 197 params['options'].replace(r'\"', '"') 198 elif params['config'] != params['target']: 199 params['options'] = params['config'] 200 201 return params 202 203def scan_defconfigs_for_multiprocess(queue, defconfigs): 204 """Scan defconfig files and queue their board parameters 205 206 This function is intended to be passed to 207 multiprocessing.Process() constructor. 208 209 Arguments: 210 queue: An instance of multiprocessing.Queue(). 211 The resulting board parameters are written into it. 212 defconfigs: A sequence of defconfig files to be scanned. 213 """ 214 kconf_scanner = KconfigScanner() 215 for defconfig in defconfigs: 216 queue.put(kconf_scanner.scan(defconfig)) 217 218def read_queues(queues, params_list): 219 """Read the queues and append the data to the paramers list""" 220 for q in queues: 221 while not q.empty(): 222 params_list.append(q.get()) 223 224def scan_defconfigs(jobs=1): 225 """Collect board parameters for all defconfig files. 226 227 This function invokes multiple processes for faster processing. 228 229 Arguments: 230 jobs: The number of jobs to run simultaneously 231 """ 232 all_defconfigs = [] 233 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR): 234 for filename in fnmatch.filter(filenames, '*_defconfig'): 235 if fnmatch.fnmatch(filename, '.*'): 236 continue 237 all_defconfigs.append(os.path.join(dirpath, filename)) 238 239 total_boards = len(all_defconfigs) 240 processes = [] 241 queues = [] 242 for i in range(jobs): 243 defconfigs = all_defconfigs[total_boards * i / jobs : 244 total_boards * (i + 1) / jobs] 245 q = multiprocessing.Queue(maxsize=-1) 246 p = multiprocessing.Process(target=scan_defconfigs_for_multiprocess, 247 args=(q, defconfigs)) 248 p.start() 249 processes.append(p) 250 queues.append(q) 251 252 # The resulting data should be accumulated to this list 253 params_list = [] 254 255 # Data in the queues should be retrieved preriodically. 256 # Otherwise, the queues would become full and subprocesses would get stuck. 257 while any([p.is_alive() for p in processes]): 258 read_queues(queues, params_list) 259 # sleep for a while until the queues are filled 260 time.sleep(SLEEP_TIME) 261 262 # Joining subprocesses just in case 263 # (All subprocesses should already have been finished) 264 for p in processes: 265 p.join() 266 267 # retrieve leftover data 268 read_queues(queues, params_list) 269 270 return params_list 271 272class MaintainersDatabase: 273 274 """The database of board status and maintainers.""" 275 276 def __init__(self): 277 """Create an empty database.""" 278 self.database = {} 279 280 def get_status(self, target): 281 """Return the status of the given board. 282 283 The board status is generally either 'Active' or 'Orphan'. 284 Display a warning message and return '-' if status information 285 is not found. 286 287 Returns: 288 'Active', 'Orphan' or '-'. 289 """ 290 if not target in self.database: 291 print >> sys.stderr, "WARNING: no status info for '%s'" % target 292 return '-' 293 294 tmp = self.database[target][0] 295 if tmp.startswith('Maintained'): 296 return 'Active' 297 elif tmp.startswith('Supported'): 298 return 'Active' 299 elif tmp.startswith('Orphan'): 300 return 'Orphan' 301 else: 302 print >> sys.stderr, ("WARNING: %s: unknown status for '%s'" % 303 (tmp, target)) 304 return '-' 305 306 def get_maintainers(self, target): 307 """Return the maintainers of the given board. 308 309 Returns: 310 Maintainers of the board. If the board has two or more maintainers, 311 they are separated with colons. 312 """ 313 if not target in self.database: 314 print >> sys.stderr, "WARNING: no maintainers for '%s'" % target 315 return '' 316 317 return ':'.join(self.database[target][1]) 318 319 def parse_file(self, file): 320 """Parse a MAINTAINERS file. 321 322 Parse a MAINTAINERS file and accumulates board status and 323 maintainers information. 324 325 Arguments: 326 file: MAINTAINERS file to be parsed 327 """ 328 targets = [] 329 maintainers = [] 330 status = '-' 331 for line in open(file): 332 # Check also commented maintainers 333 if line[:3] == '#M:': 334 line = line[1:] 335 tag, rest = line[:2], line[2:].strip() 336 if tag == 'M:': 337 maintainers.append(rest) 338 elif tag == 'F:': 339 # expand wildcard and filter by 'configs/*_defconfig' 340 for f in glob.glob(rest): 341 front, match, rear = f.partition('configs/') 342 if not front and match: 343 front, match, rear = rear.rpartition('_defconfig') 344 if match and not rear: 345 targets.append(front) 346 elif tag == 'S:': 347 status = rest 348 elif line == '\n': 349 for target in targets: 350 self.database[target] = (status, maintainers) 351 targets = [] 352 maintainers = [] 353 status = '-' 354 if targets: 355 for target in targets: 356 self.database[target] = (status, maintainers) 357 358def insert_maintainers_info(params_list): 359 """Add Status and Maintainers information to the board parameters list. 360 361 Arguments: 362 params_list: A list of the board parameters 363 """ 364 database = MaintainersDatabase() 365 for (dirpath, dirnames, filenames) in os.walk('.'): 366 if 'MAINTAINERS' in filenames: 367 database.parse_file(os.path.join(dirpath, 'MAINTAINERS')) 368 369 for i, params in enumerate(params_list): 370 target = params['target'] 371 params['status'] = database.get_status(target) 372 params['maintainers'] = database.get_maintainers(target) 373 params_list[i] = params 374 375def format_and_output(params_list, output): 376 """Write board parameters into a file. 377 378 Columnate the board parameters, sort lines alphabetically, 379 and then write them to a file. 380 381 Arguments: 382 params_list: The list of board parameters 383 output: The path to the output file 384 """ 385 FIELDS = ('status', 'arch', 'cpu', 'soc', 'vendor', 'board', 'target', 386 'options', 'maintainers') 387 388 # First, decide the width of each column 389 max_length = dict([ (f, 0) for f in FIELDS]) 390 for params in params_list: 391 for f in FIELDS: 392 max_length[f] = max(max_length[f], len(params[f])) 393 394 output_lines = [] 395 for params in params_list: 396 line = '' 397 for f in FIELDS: 398 # insert two spaces between fields like column -t would 399 line += ' ' + params[f].ljust(max_length[f]) 400 output_lines.append(line.strip()) 401 402 # ignore case when sorting 403 output_lines.sort(key=str.lower) 404 405 with open(output, 'w') as f: 406 f.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n') 407 408def gen_boards_cfg(output, jobs=1, force=False): 409 """Generate a board database file. 410 411 Arguments: 412 output: The name of the output file 413 jobs: The number of jobs to run simultaneously 414 force: Force to generate the output even if it is new 415 """ 416 check_top_directory() 417 418 if not force and output_is_new(output): 419 print "%s is up to date. Nothing to do." % output 420 sys.exit(0) 421 422 params_list = scan_defconfigs(jobs) 423 insert_maintainers_info(params_list) 424 format_and_output(params_list, output) 425 426def main(): 427 try: 428 cpu_count = multiprocessing.cpu_count() 429 except NotImplementedError: 430 cpu_count = 1 431 432 parser = optparse.OptionParser() 433 # Add options here 434 parser.add_option('-f', '--force', action="store_true", default=False, 435 help='regenerate the output even if it is new') 436 parser.add_option('-j', '--jobs', type='int', default=cpu_count, 437 help='the number of jobs to run simultaneously') 438 parser.add_option('-o', '--output', default=OUTPUT_FILE, 439 help='output file [default=%s]' % OUTPUT_FILE) 440 (options, args) = parser.parse_args() 441 442 gen_boards_cfg(options.output, jobs=options.jobs, force=options.force) 443 444if __name__ == '__main__': 445 main() 446