1# SPDX-License-Identifier: GPL-2.0+ 2# Copyright (c) 2012 The Chromium OS Authors. 3# Author: Simon Glass <sjg@chromium.org> 4# Author: Masahiro Yamada <yamada.m@jp.panasonic.com> 5 6"""Maintains a list of boards and allows them to be selected""" 7 8from collections import OrderedDict 9import errno 10import fnmatch 11import glob 12import multiprocessing 13import os 14import re 15import sys 16import tempfile 17import time 18 19from buildman import board 20from buildman import kconfiglib 21 22from u_boot_pylib.terminal import print_clear, tprint 23 24### constant variables ### 25OUTPUT_FILE = 'boards.cfg' 26CONFIG_DIR = 'configs' 27SLEEP_TIME = 0.03 28COMMENT_BLOCK = f'''# 29# List of boards 30# Automatically generated by {__file__}: don't edit 31# 32# Status, Arch, CPU, SoC, Vendor, Board, Target, Config, Maintainers 33 34''' 35 36 37def try_remove(fname): 38 """Remove a file ignoring 'No such file or directory' error. 39 40 Args: 41 fname (str): Filename to remove 42 43 Raises: 44 OSError: output file exists but could not be removed 45 """ 46 try: 47 os.remove(fname) 48 except OSError as exception: 49 # Ignore 'No such file or directory' error 50 if exception.errno != errno.ENOENT: 51 raise 52 53 54def output_is_new(output, config_dir, srcdir): 55 """Check if the output file is up to date. 56 57 Looks at defconfig and Kconfig files to make sure none is newer than the 58 output file. Also ensures that the boards.cfg does not mention any removed 59 boards. 60 61 Args: 62 output (str): Filename to check 63 config_dir (str): Directory containing defconfig files 64 srcdir (str): Directory containing Kconfig and MAINTAINERS files 65 66 Returns: 67 True if the given output file exists and is newer than any of 68 *_defconfig, MAINTAINERS and Kconfig*. False otherwise. 69 70 Raises: 71 OSError: output file exists but could not be opened 72 """ 73 # pylint: disable=too-many-branches 74 try: 75 ctime = os.path.getctime(output) 76 except OSError as exception: 77 if exception.errno == errno.ENOENT: 78 # return False on 'No such file or directory' error 79 return False 80 raise 81 82 for (dirpath, _, filenames) in os.walk(config_dir): 83 for filename in fnmatch.filter(filenames, '*_defconfig'): 84 if fnmatch.fnmatch(filename, '.*'): 85 continue 86 filepath = os.path.join(dirpath, filename) 87 if ctime < os.path.getctime(filepath): 88 return False 89 90 for (dirpath, _, filenames) in os.walk(srcdir): 91 for filename in filenames: 92 if (fnmatch.fnmatch(filename, '*~') or 93 not fnmatch.fnmatch(filename, 'Kconfig*') and 94 not filename == 'MAINTAINERS'): 95 continue 96 filepath = os.path.join(dirpath, filename) 97 if ctime < os.path.getctime(filepath): 98 return False 99 100 # Detect a board that has been removed since the current board database 101 # was generated 102 with open(output, encoding="utf-8") as inf: 103 for line in inf: 104 if 'Options,' in line: 105 return False 106 if line[0] == '#' or line == '\n': 107 continue 108 defconfig = line.split()[6] + '_defconfig' 109 if not os.path.exists(os.path.join(config_dir, defconfig)): 110 return False 111 112 return True 113 114 115class Expr: 116 """A single regular expression for matching boards to build""" 117 118 def __init__(self, expr): 119 """Set up a new Expr object. 120 121 Args: 122 expr (str): String containing regular expression to store 123 """ 124 self._expr = expr 125 self._re = re.compile(expr) 126 127 def matches(self, props): 128 """Check if any of the properties match the regular expression. 129 130 Args: 131 props (list of str): List of properties to check 132 Returns: 133 True if any of the properties match the regular expression 134 """ 135 for prop in props: 136 if self._re.match(prop): 137 return True 138 return False 139 140 def __str__(self): 141 return self._expr 142 143class Term: 144 """A list of expressions each of which must match with properties. 145 146 This provides a list of 'AND' expressions, meaning that each must 147 match the board properties for that board to be built. 148 """ 149 def __init__(self): 150 self._expr_list = [] 151 self._board_count = 0 152 153 def add_expr(self, expr): 154 """Add an Expr object to the list to check. 155 156 Args: 157 expr (Expr): New Expr object to add to the list of those that must 158 match for a board to be built. 159 """ 160 self._expr_list.append(Expr(expr)) 161 162 def __str__(self): 163 """Return some sort of useful string describing the term""" 164 return '&'.join([str(expr) for expr in self._expr_list]) 165 166 def matches(self, props): 167 """Check if any of the properties match this term 168 169 Each of the expressions in the term is checked. All must match. 170 171 Args: 172 props (list of str): List of properties to check 173 Returns: 174 True if all of the expressions in the Term match, else False 175 """ 176 for expr in self._expr_list: 177 if not expr.matches(props): 178 return False 179 return True 180 181 182class KconfigScanner: 183 184 """Kconfig scanner.""" 185 186 ### constant variable only used in this class ### 187 _SYMBOL_TABLE = { 188 'arch' : 'SYS_ARCH', 189 'cpu' : 'SYS_CPU', 190 'soc' : 'SYS_SOC', 191 'vendor' : 'SYS_VENDOR', 192 'board' : 'SYS_BOARD', 193 'config' : 'SYS_CONFIG_NAME', 194 # 'target' is added later 195 } 196 197 def __init__(self, srctree): 198 """Scan all the Kconfig files and create a Kconfig object.""" 199 # Define environment variables referenced from Kconfig 200 os.environ['srctree'] = srctree 201 os.environ['UBOOTVERSION'] = 'dummy' 202 os.environ['KCONFIG_OBJDIR'] = '' 203 self._tmpfile = None 204 self._conf = kconfiglib.Kconfig(warn=False) 205 206 def __del__(self): 207 """Delete a leftover temporary file before exit. 208 209 The scan() method of this class creates a temporay file and deletes 210 it on success. If scan() method throws an exception on the way, 211 the temporary file might be left over. In that case, it should be 212 deleted in this destructor. 213 """ 214 if self._tmpfile: 215 try_remove(self._tmpfile) 216 217 def scan(self, defconfig, warn_targets): 218 """Load a defconfig file to obtain board parameters. 219 220 Args: 221 defconfig (str): path to the defconfig file to be processed 222 warn_targets (bool): True to warn about missing or duplicate 223 CONFIG_TARGET options 224 225 Returns: 226 tuple: dictionary of board parameters. It has a form of: 227 { 228 'arch': <arch_name>, 229 'cpu': <cpu_name>, 230 'soc': <soc_name>, 231 'vendor': <vendor_name>, 232 'board': <board_name>, 233 'target': <target_name>, 234 'config': <config_header_name>, 235 } 236 warnings (list of str): list of warnings found 237 """ 238 leaf = os.path.basename(defconfig) 239 expect_target, match, rear = leaf.partition('_defconfig') 240 assert match and not rear, f'{leaf} : invalid defconfig' 241 242 self._conf.load_config(defconfig) 243 self._tmpfile = None 244 245 params = {} 246 warnings = [] 247 248 # Get the value of CONFIG_SYS_ARCH, CONFIG_SYS_CPU, ... etc. 249 # Set '-' if the value is empty. 250 for key, symbol in list(self._SYMBOL_TABLE.items()): 251 value = self._conf.syms.get(symbol).str_value 252 if value: 253 params[key] = value 254 else: 255 params[key] = '-' 256 257 # Check there is exactly one TARGET_xxx set 258 if warn_targets: 259 target = None 260 for name, sym in self._conf.syms.items(): 261 if name.startswith('TARGET_') and sym.str_value == 'y': 262 tname = name[7:].lower() 263 if target: 264 warnings.append( 265 f'WARNING: {leaf}: Duplicate TARGET_xxx: {target} and {tname}') 266 else: 267 target = tname 268 269 if not target: 270 cfg_name = expect_target.replace('-', '_').upper() 271 warnings.append(f'WARNING: {leaf}: No TARGET_{cfg_name} enabled') 272 273 params['target'] = expect_target 274 275 # fix-up for aarch64 276 if params['arch'] == 'arm' and params['cpu'] == 'armv8': 277 params['arch'] = 'aarch64' 278 279 # fix-up for riscv 280 if params['arch'] == 'riscv': 281 try: 282 value = self._conf.syms.get('ARCH_RV32I').str_value 283 except: 284 value = '' 285 if value == 'y': 286 params['arch'] = 'riscv32' 287 else: 288 params['arch'] = 'riscv64' 289 290 return params, warnings 291 292 293class MaintainersDatabase: 294 295 """The database of board status and maintainers. 296 297 Properties: 298 database: dict: 299 key: Board-target name (e.g. 'snow') 300 value: tuple: 301 str: Board status (e.g. 'Active') 302 str: List of maintainers, separated by : 303 warnings (list of str): List of warnings due to missing status, etc. 304 """ 305 306 def __init__(self): 307 """Create an empty database.""" 308 self.database = {} 309 self.warnings = [] 310 311 def get_status(self, target): 312 """Return the status of the given board. 313 314 The board status is generally either 'Active' or 'Orphan'. 315 Display a warning message and return '-' if status information 316 is not found. 317 318 Args: 319 target (str): Build-target name 320 321 Returns: 322 str: 'Active', 'Orphan' or '-'. 323 """ 324 if not target in self.database: 325 self.warnings.append(f"WARNING: no status info for '{target}'") 326 return '-' 327 328 tmp = self.database[target][0] 329 if tmp.startswith('Maintained'): 330 return 'Active' 331 if tmp.startswith('Supported'): 332 return 'Active' 333 if tmp.startswith('Orphan'): 334 return 'Orphan' 335 self.warnings.append(f"WARNING: {tmp}: unknown status for '{target}'") 336 return '-' 337 338 def get_maintainers(self, target): 339 """Return the maintainers of the given board. 340 341 Args: 342 target (str): Build-target name 343 344 Returns: 345 str: Maintainers of the board. If the board has two or more 346 maintainers, they are separated with colons. 347 """ 348 entry = self.database.get(target) 349 if entry: 350 status, maint_list = entry 351 if not status.startswith('Orphan'): 352 if len(maint_list) > 1 or (maint_list and maint_list[0] != '-'): 353 return ':'.join(maint_list) 354 355 self.warnings.append(f"WARNING: no maintainers for '{target}'") 356 return '' 357 358 def parse_file(self, srcdir, fname): 359 """Parse a MAINTAINERS file. 360 361 Parse a MAINTAINERS file and accumulate board status and maintainers 362 information in the self.database dict. 363 364 defconfig files are used to specify the target, e.g. xxx_defconfig is 365 used for target 'xxx'. If there is no defconfig file mentioned in the 366 MAINTAINERS file F: entries, then this function does nothing. 367 368 The N: name entries can be used to specify a defconfig file using 369 wildcards. 370 371 Args: 372 srcdir (str): Directory containing source code (Kconfig files) 373 fname (str): MAINTAINERS file to be parsed 374 """ 375 def add_targets(linenum): 376 """Add any new targets 377 378 Args: 379 linenum (int): Current line number 380 """ 381 if targets: 382 for target in targets: 383 self.database[target] = (status, maintainers) 384 385 targets = [] 386 maintainers = [] 387 status = '-' 388 with open(fname, encoding="utf-8") as inf: 389 for linenum, line in enumerate(inf): 390 # Check also commented maintainers 391 if line[:3] == '#M:': 392 line = line[1:] 393 tag, rest = line[:2], line[2:].strip() 394 if tag == 'M:': 395 maintainers.append(rest) 396 elif tag == 'F:': 397 # expand wildcard and filter by 'configs/*_defconfig' 398 glob_path = os.path.join(srcdir, rest) 399 for item in glob.glob(glob_path): 400 front, match, rear = item.partition('configs/') 401 if front.endswith('/'): 402 front = front[:-1] 403 if front == srcdir and match: 404 front, match, rear = rear.rpartition('_defconfig') 405 if match and not rear: 406 targets.append(front) 407 elif tag == 'S:': 408 status = rest 409 elif tag == 'N:': 410 # Just scan the configs directory since that's all we care 411 # about 412 walk_path = os.walk(os.path.join(srcdir, 'configs')) 413 for dirpath, _, fnames in walk_path: 414 for cfg in fnames: 415 path = os.path.join(dirpath, cfg)[len(srcdir) + 1:] 416 front, match, rear = path.partition('configs/') 417 if front or not match: 418 continue 419 front, match, rear = rear.rpartition('_defconfig') 420 421 # Use this entry if it matches the defconfig file 422 # without the _defconfig suffix. For example 423 # 'am335x.*' matches am335x_guardian_defconfig 424 if match and not rear and re.search(rest, front): 425 targets.append(front) 426 elif line == '\n': 427 add_targets(linenum) 428 targets = [] 429 maintainers = [] 430 status = '-' 431 add_targets(linenum) 432 433 434class Boards: 435 """Manage a list of boards.""" 436 def __init__(self): 437 self._boards = [] 438 439 def add_board(self, brd): 440 """Add a new board to the list. 441 442 The board's target member must not already exist in the board list. 443 444 Args: 445 brd (Board): board to add 446 """ 447 self._boards.append(brd) 448 449 def read_boards(self, fname): 450 """Read a list of boards from a board file. 451 452 Create a Board object for each and add it to our _boards list. 453 454 Args: 455 fname (str): Filename of boards.cfg file 456 """ 457 with open(fname, 'r', encoding='utf-8') as inf: 458 for line in inf: 459 if line[0] == '#': 460 continue 461 fields = line.split() 462 if not fields: 463 continue 464 for upto, field in enumerate(fields): 465 if field == '-': 466 fields[upto] = '' 467 while len(fields) < 8: 468 fields.append('') 469 if len(fields) > 8: 470 fields = fields[:8] 471 472 brd = board.Board(*fields) 473 self.add_board(brd) 474 475 476 def get_list(self): 477 """Return a list of available boards. 478 479 Returns: 480 List of Board objects 481 """ 482 return self._boards 483 484 def get_dict(self): 485 """Build a dictionary containing all the boards. 486 487 Returns: 488 Dictionary: 489 key is board.target 490 value is board 491 """ 492 board_dict = OrderedDict() 493 for brd in self._boards: 494 board_dict[brd.target] = brd 495 return board_dict 496 497 def get_selected_dict(self): 498 """Return a dictionary containing the selected boards 499 500 Returns: 501 List of Board objects that are marked selected 502 """ 503 board_dict = OrderedDict() 504 for brd in self._boards: 505 if brd.build_it: 506 board_dict[brd.target] = brd 507 return board_dict 508 509 def get_selected(self): 510 """Return a list of selected boards 511 512 Returns: 513 List of Board objects that are marked selected 514 """ 515 return [brd for brd in self._boards if brd.build_it] 516 517 def get_selected_names(self): 518 """Return a list of selected boards 519 520 Returns: 521 List of board names that are marked selected 522 """ 523 return [brd.target for brd in self._boards if brd.build_it] 524 525 @classmethod 526 def _build_terms(cls, args): 527 """Convert command line arguments to a list of terms. 528 529 This deals with parsing of the arguments. It handles the '&' 530 operator, which joins several expressions into a single Term. 531 532 For example: 533 ['arm & freescale sandbox', 'tegra'] 534 535 will produce 3 Terms containing expressions as follows: 536 arm, freescale 537 sandbox 538 tegra 539 540 The first Term has two expressions, both of which must match for 541 a board to be selected. 542 543 Args: 544 args (list of str): List of command line arguments 545 546 Returns: 547 list of Term: A list of Term objects 548 """ 549 syms = [] 550 for arg in args: 551 for word in arg.split(): 552 sym_build = [] 553 for term in word.split('&'): 554 if term: 555 sym_build.append(term) 556 sym_build.append('&') 557 syms += sym_build[:-1] 558 terms = [] 559 term = None 560 oper = None 561 for sym in syms: 562 if sym == '&': 563 oper = sym 564 elif oper: 565 term.add_expr(sym) 566 oper = None 567 else: 568 if term: 569 terms.append(term) 570 term = Term() 571 term.add_expr(sym) 572 if term: 573 terms.append(term) 574 return terms 575 576 def select_boards(self, args, exclude=None, brds=None): 577 """Mark boards selected based on args 578 579 Normally either boards (an explicit list of boards) or args (a list of 580 terms to match against) is used. It is possible to specify both, in 581 which case they are additive. 582 583 If brds and args are both empty, all boards are selected. 584 585 Args: 586 args (list of str): List of strings specifying boards to include, 587 either named, or by their target, architecture, cpu, vendor or 588 soc. If empty, all boards are selected. 589 exclude (list of str): List of boards to exclude, regardless of 590 'args', or None for none 591 brds (list of Board): List of boards to build, or None/[] for all 592 593 Returns: 594 Tuple 595 Dictionary which holds the list of boards which were selected 596 due to each argument, arranged by argument. 597 List of errors found 598 """ 599 def _check_board(brd): 600 """Check whether to include or exclude a board 601 602 Checks the various terms and decide whether to build it or not (the 603 'build_it' variable). 604 605 If it is built, add the board to the result[term] list so we know 606 which term caused it to be built. Add it to result['all'] also. 607 608 Keep a list of boards we found in 'found', so we can report boards 609 which appear in self._boards but not in brds. 610 611 Args: 612 brd (Board): Board to check 613 """ 614 matching_term = None 615 build_it = False 616 if terms: 617 for term in terms: 618 if term.matches(brd.props): 619 matching_term = str(term) 620 build_it = True 621 break 622 elif brds: 623 if brd.target in brds: 624 build_it = True 625 found.append(brd.target) 626 else: 627 build_it = True 628 629 # Check that it is not specifically excluded 630 for expr in exclude_list: 631 if expr.matches(brd.props): 632 build_it = False 633 break 634 635 if build_it: 636 brd.build_it = True 637 if matching_term: 638 result[matching_term].append(brd.target) 639 result['all'].append(brd.target) 640 641 result = OrderedDict() 642 warnings = [] 643 terms = self._build_terms(args) 644 645 result['all'] = [] 646 for term in terms: 647 result[str(term)] = [] 648 649 exclude_list = [] 650 if exclude: 651 for expr in exclude: 652 exclude_list.append(Expr(expr)) 653 654 found = [] 655 for brd in self._boards: 656 _check_board(brd) 657 658 if brds: 659 remaining = set(brds) - set(found) 660 if remaining: 661 warnings.append(f"Boards not found: {', '.join(remaining)}\n") 662 663 return result, warnings 664 665 @classmethod 666 def scan_defconfigs_for_multiprocess(cls, srcdir, queue, defconfigs, 667 warn_targets): 668 """Scan defconfig files and queue their board parameters 669 670 This function is intended to be passed to multiprocessing.Process() 671 constructor. 672 673 Args: 674 srcdir (str): Directory containing source code 675 queue (multiprocessing.Queue): The resulting board parameters are 676 written into this. 677 defconfigs (sequence of str): A sequence of defconfig files to be 678 scanned. 679 warn_targets (bool): True to warn about missing or duplicate 680 CONFIG_TARGET options 681 """ 682 kconf_scanner = KconfigScanner(srcdir) 683 for defconfig in defconfigs: 684 queue.put(kconf_scanner.scan(defconfig, warn_targets)) 685 686 @classmethod 687 def read_queues(cls, queues, params_list, warnings): 688 """Read the queues and append the data to the paramers list 689 690 Args: 691 queues (list of multiprocessing.Queue): Queues to read 692 params_list (list of dict): List to add params too 693 warnings (set of str): Set to add warnings to 694 """ 695 for que in queues: 696 while not que.empty(): 697 params, warn = que.get() 698 params_list.append(params) 699 warnings.update(warn) 700 701 def scan_defconfigs(self, config_dir, srcdir, jobs=1, warn_targets=False): 702 """Collect board parameters for all defconfig files. 703 704 This function invokes multiple processes for faster processing. 705 706 Args: 707 config_dir (str): Directory containing the defconfig files 708 srcdir (str): Directory containing source code (Kconfig files) 709 jobs (int): The number of jobs to run simultaneously 710 warn_targets (bool): True to warn about missing or duplicate 711 CONFIG_TARGET options 712 713 Returns: 714 tuple: 715 list of dict: List of board parameters, each a dict: 716 key: 'arch', 'cpu', 'soc', 'vendor', 'board', 'target', 717 'config' 718 value: string value of the key 719 list of str: List of warnings recorded 720 """ 721 all_defconfigs = [] 722 for (dirpath, _, filenames) in os.walk(config_dir): 723 for filename in fnmatch.filter(filenames, '*_defconfig'): 724 if fnmatch.fnmatch(filename, '.*'): 725 continue 726 all_defconfigs.append(os.path.join(dirpath, filename)) 727 728 total_boards = len(all_defconfigs) 729 processes = [] 730 queues = [] 731 for i in range(jobs): 732 defconfigs = all_defconfigs[total_boards * i // jobs : 733 total_boards * (i + 1) // jobs] 734 que = multiprocessing.Queue(maxsize=-1) 735 proc = multiprocessing.Process( 736 target=self.scan_defconfigs_for_multiprocess, 737 args=(srcdir, que, defconfigs, warn_targets)) 738 proc.start() 739 processes.append(proc) 740 queues.append(que) 741 742 # The resulting data should be accumulated to these lists 743 params_list = [] 744 warnings = set() 745 746 # Data in the queues should be retrieved preriodically. 747 # Otherwise, the queues would become full and subprocesses would get stuck. 748 while any(p.is_alive() for p in processes): 749 self.read_queues(queues, params_list, warnings) 750 # sleep for a while until the queues are filled 751 time.sleep(SLEEP_TIME) 752 753 # Joining subprocesses just in case 754 # (All subprocesses should already have been finished) 755 for proc in processes: 756 proc.join() 757 758 # retrieve leftover data 759 self.read_queues(queues, params_list, warnings) 760 761 return params_list, sorted(list(warnings)) 762 763 @classmethod 764 def insert_maintainers_info(cls, srcdir, params_list): 765 """Add Status and Maintainers information to the board parameters list. 766 767 Args: 768 params_list (list of dict): A list of the board parameters 769 770 Returns: 771 list of str: List of warnings collected due to missing status, etc. 772 """ 773 database = MaintainersDatabase() 774 for (dirpath, _, filenames) in os.walk(srcdir): 775 if 'MAINTAINERS' in filenames and 'tools/buildman' not in dirpath: 776 database.parse_file(srcdir, 777 os.path.join(dirpath, 'MAINTAINERS')) 778 779 for i, params in enumerate(params_list): 780 target = params['target'] 781 maintainers = database.get_maintainers(target) 782 params['maintainers'] = maintainers 783 if maintainers: 784 params['status'] = database.get_status(target) 785 else: 786 params['status'] = '-' 787 params_list[i] = params 788 return sorted(database.warnings) 789 790 @classmethod 791 def format_and_output(cls, params_list, output): 792 """Write board parameters into a file. 793 794 Columnate the board parameters, sort lines alphabetically, 795 and then write them to a file. 796 797 Args: 798 params_list (list of dict): The list of board parameters 799 output (str): The path to the output file 800 """ 801 fields = ('status', 'arch', 'cpu', 'soc', 'vendor', 'board', 'target', 802 'config', 'maintainers') 803 804 # First, decide the width of each column 805 max_length = {f: 0 for f in fields} 806 for params in params_list: 807 for field in fields: 808 max_length[field] = max(max_length[field], len(params[field])) 809 810 output_lines = [] 811 for params in params_list: 812 line = '' 813 for field in fields: 814 # insert two spaces between fields like column -t would 815 line += ' ' + params[field].ljust(max_length[field]) 816 output_lines.append(line.strip()) 817 818 # ignore case when sorting 819 output_lines.sort(key=str.lower) 820 821 with open(output, 'w', encoding="utf-8") as outf: 822 outf.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n') 823 824 def build_board_list(self, config_dir=CONFIG_DIR, srcdir='.', jobs=1, 825 warn_targets=False): 826 """Generate a board-database file 827 828 This works by reading the Kconfig, then loading each board's defconfig 829 in to get the setting for each option. In particular, CONFIG_TARGET_xxx 830 is typically set by the defconfig, where xxx is the target to build. 831 832 Args: 833 config_dir (str): Directory containing the defconfig files 834 srcdir (str): Directory containing source code (Kconfig files) 835 jobs (int): The number of jobs to run simultaneously 836 warn_targets (bool): True to warn about missing or duplicate 837 CONFIG_TARGET options 838 839 Returns: 840 tuple: 841 list of dict: List of board parameters, each a dict: 842 key: 'arch', 'cpu', 'soc', 'vendor', 'board', 'config', 843 'target' 844 value: string value of the key 845 list of str: Warnings that came up 846 """ 847 params_list, warnings = self.scan_defconfigs(config_dir, srcdir, jobs, 848 warn_targets) 849 m_warnings = self.insert_maintainers_info(srcdir, params_list) 850 return params_list, warnings + m_warnings 851 852 def ensure_board_list(self, output, jobs=1, force=False, quiet=False): 853 """Generate a board database file if needed. 854 855 This is intended to check if Kconfig has changed since the boards.cfg 856 files was generated. 857 858 Args: 859 output (str): The name of the output file 860 jobs (int): The number of jobs to run simultaneously 861 force (bool): Force to generate the output even if it is new 862 quiet (bool): True to avoid printing a message if nothing needs doing 863 864 Returns: 865 bool: True if all is well, False if there were warnings 866 """ 867 if not force: 868 if not quiet: 869 tprint('\rChecking for Kconfig changes...', newline=False) 870 is_new = output_is_new(output, CONFIG_DIR, '.') 871 print_clear() 872 if is_new: 873 if not quiet: 874 print(f'{output} is up to date. Nothing to do.') 875 return True 876 if not quiet: 877 tprint('\rGenerating board list...', newline=False) 878 params_list, warnings = self.build_board_list(CONFIG_DIR, '.', jobs) 879 print_clear() 880 for warn in warnings: 881 print(warn, file=sys.stderr) 882 self.format_and_output(params_list, output) 883 return not warnings 884