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