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>
6 """Maintains a list of boards and allows them to be selected"""
8 from collections import OrderedDict
12 import multiprocessing
19 from buildman import board
20 from buildman import kconfiglib
23 ### constant variables ###
24 OUTPUT_FILE = 'boards.cfg'
25 CONFIG_DIR = 'configs'
29 # Automatically generated by {__file__}: don't edit
31 # Status, Arch, CPU, SoC, Vendor, Board, Target, Config, Maintainers
36 def try_remove(fname):
37 """Remove a file ignoring 'No such file or directory' error.
40 fname (str): Filename to remove
43 OSError: output file exists but could not be removed
47 except OSError as exception:
48 # Ignore 'No such file or directory' error
49 if exception.errno != errno.ENOENT:
53 def output_is_new(output, config_dir, srcdir):
54 """Check if the output file is up to date.
56 Looks at defconfig and Kconfig files to make sure none is newer than the
57 output file. Also ensures that the boards.cfg does not mention any removed
61 output (str): Filename to check
62 config_dir (str): Directory containing defconfig files
63 srcdir (str): Directory containing Kconfig and MAINTAINERS files
66 True if the given output file exists and is newer than any of
67 *_defconfig, MAINTAINERS and Kconfig*. False otherwise.
70 OSError: output file exists but could not be opened
72 # pylint: disable=too-many-branches
74 ctime = os.path.getctime(output)
75 except OSError as exception:
76 if exception.errno == errno.ENOENT:
77 # return False on 'No such file or directory' error
81 for (dirpath, _, filenames) in os.walk(config_dir):
82 for filename in fnmatch.filter(filenames, '*_defconfig'):
83 if fnmatch.fnmatch(filename, '.*'):
85 filepath = os.path.join(dirpath, filename)
86 if ctime < os.path.getctime(filepath):
89 for (dirpath, _, filenames) in os.walk(srcdir):
90 for filename in filenames:
91 if (fnmatch.fnmatch(filename, '*~') or
92 not fnmatch.fnmatch(filename, 'Kconfig*') and
93 not filename == 'MAINTAINERS'):
95 filepath = os.path.join(dirpath, filename)
96 if ctime < os.path.getctime(filepath):
99 # Detect a board that has been removed since the current board database
101 with open(output, encoding="utf-8") as inf:
103 if 'Options,' in line:
105 if line[0] == '#' or line == '\n':
107 defconfig = line.split()[6] + '_defconfig'
108 if not os.path.exists(os.path.join(config_dir, defconfig)):
115 """A single regular expression for matching boards to build"""
117 def __init__(self, expr):
118 """Set up a new Expr object.
121 expr (str): String cotaining regular expression to store
124 self._re = re.compile(expr)
126 def matches(self, props):
127 """Check if any of the properties match the regular expression.
130 props (list of str): List of properties to check
132 True if any of the properties match the regular expression
135 if self._re.match(prop):
143 """A list of expressions each of which must match with properties.
145 This provides a list of 'AND' expressions, meaning that each must
146 match the board properties for that board to be built.
150 self._board_count = 0
152 def add_expr(self, expr):
153 """Add an Expr object to the list to check.
156 expr (Expr): New Expr object to add to the list of those that must
157 match for a board to be built.
159 self._expr_list.append(Expr(expr))
162 """Return some sort of useful string describing the term"""
163 return '&'.join([str(expr) for expr in self._expr_list])
165 def matches(self, props):
166 """Check if any of the properties match this term
168 Each of the expressions in the term is checked. All must match.
171 props (list of str): List of properties to check
173 True if all of the expressions in the Term match, else False
175 for expr in self._expr_list:
176 if not expr.matches(props):
181 class KconfigScanner:
183 """Kconfig scanner."""
185 ### constant variable only used in this class ###
190 'vendor' : 'SYS_VENDOR',
191 'board' : 'SYS_BOARD',
192 'config' : 'SYS_CONFIG_NAME',
193 # 'target' is added later
196 def __init__(self, srctree):
197 """Scan all the Kconfig files and create a Kconfig object."""
198 # Define environment variables referenced from Kconfig
199 os.environ['srctree'] = srctree
200 os.environ['UBOOTVERSION'] = 'dummy'
201 os.environ['KCONFIG_OBJDIR'] = ''
203 self._conf = kconfiglib.Kconfig(warn=False)
206 """Delete a leftover temporary file before exit.
208 The scan() method of this class creates a temporay file and deletes
209 it on success. If scan() method throws an exception on the way,
210 the temporary file might be left over. In that case, it should be
211 deleted in this destructor.
214 try_remove(self._tmpfile)
216 def scan(self, defconfig, warn_targets):
217 """Load a defconfig file to obtain board parameters.
220 defconfig (str): path to the defconfig file to be processed
221 warn_targets (bool): True to warn about missing or duplicate
222 CONFIG_TARGET options
225 tuple: dictionary of board parameters. It has a form of:
230 'vendor': <vendor_name>,
231 'board': <board_name>,
232 'target': <target_name>,
233 'config': <config_header_name>,
235 warnings (list of str): list of warnings found
237 leaf = os.path.basename(defconfig)
238 expect_target, match, rear = leaf.partition('_defconfig')
239 assert match and not rear, f'{leaf} : invalid defconfig'
241 self._conf.load_config(defconfig)
247 # Get the value of CONFIG_SYS_ARCH, CONFIG_SYS_CPU, ... etc.
248 # Set '-' if the value is empty.
249 for key, symbol in list(self._SYMBOL_TABLE.items()):
250 value = self._conf.syms.get(symbol).str_value
256 # Check there is exactly one TARGET_xxx set
259 for name, sym in self._conf.syms.items():
260 if name.startswith('TARGET_') and sym.str_value == 'y':
261 tname = name[7:].lower()
264 f'WARNING: {leaf}: Duplicate TARGET_xxx: {target} and {tname}')
269 cfg_name = expect_target.replace('-', '_').upper()
270 warnings.append(f'WARNING: {leaf}: No TARGET_{cfg_name} enabled')
272 params['target'] = expect_target
275 if params['arch'] == 'arm' and params['cpu'] == 'armv8':
276 params['arch'] = 'aarch64'
279 if params['arch'] == 'riscv':
281 value = self._conf.syms.get('ARCH_RV32I').str_value
285 params['arch'] = 'riscv32'
287 params['arch'] = 'riscv64'
289 return params, warnings
292 class MaintainersDatabase:
294 """The database of board status and maintainers.
298 key: Board-target name (e.g. 'snow')
300 str: Board status (e.g. 'Active')
301 str: List of maintainers, separated by :
302 warnings (list of str): List of warnings due to missing status, etc.
306 """Create an empty database."""
310 def get_status(self, target):
311 """Return the status of the given board.
313 The board status is generally either 'Active' or 'Orphan'.
314 Display a warning message and return '-' if status information
318 target (str): Build-target name
321 str: 'Active', 'Orphan' or '-'.
323 if not target in self.database:
324 self.warnings.append(f"WARNING: no status info for '{target}'")
327 tmp = self.database[target][0]
328 if tmp.startswith('Maintained'):
330 if tmp.startswith('Supported'):
332 if tmp.startswith('Orphan'):
334 self.warnings.append(f"WARNING: {tmp}: unknown status for '{target}'")
337 def get_maintainers(self, target):
338 """Return the maintainers of the given board.
341 target (str): Build-target name
344 str: Maintainers of the board. If the board has two or more
345 maintainers, they are separated with colons.
347 entry = self.database.get(target)
349 status, maint_list = entry
350 if not status.startswith('Orphan'):
351 if len(maint_list) > 1 or (maint_list and maint_list[0] != '-'):
352 return ':'.join(maint_list)
354 self.warnings.append(f"WARNING: no maintainers for '{target}'")
357 def parse_file(self, srcdir, fname):
358 """Parse a MAINTAINERS file.
360 Parse a MAINTAINERS file and accumulate board status and maintainers
361 information in the self.database dict.
363 defconfig files are used to specify the target, e.g. xxx_defconfig is
364 used for target 'xxx'. If there is no defconfig file mentioned in the
365 MAINTAINERS file F: entries, then this function does nothing.
367 The N: name entries can be used to specify a defconfig file using
371 srcdir (str): Directory containing source code (Kconfig files)
372 fname (str): MAINTAINERS file to be parsed
374 def add_targets(linenum):
375 """Add any new targets
378 linenum (int): Current line number
381 for target in targets:
382 self.database[target] = (status, maintainers)
387 with open(fname, encoding="utf-8") as inf:
388 for linenum, line in enumerate(inf):
389 # Check also commented maintainers
390 if line[:3] == '#M:':
392 tag, rest = line[:2], line[2:].strip()
394 maintainers.append(rest)
396 # expand wildcard and filter by 'configs/*_defconfig'
397 glob_path = os.path.join(srcdir, rest)
398 for item in glob.glob(glob_path):
399 front, match, rear = item.partition('configs/')
400 if front.endswith('/'):
402 if front == srcdir and match:
403 front, match, rear = rear.rpartition('_defconfig')
404 if match and not rear:
405 targets.append(front)
409 # Just scan the configs directory since that's all we care
411 walk_path = os.walk(os.path.join(srcdir, 'configs'))
412 for dirpath, _, fnames in walk_path:
414 path = os.path.join(dirpath, cfg)[len(srcdir) + 1:]
415 front, match, rear = path.partition('configs/')
416 if front or not match:
418 front, match, rear = rear.rpartition('_defconfig')
420 # Use this entry if it matches the defconfig file
421 # without the _defconfig suffix. For example
422 # 'am335x.*' matches am335x_guardian_defconfig
423 if match and not rear and re.search(rest, front):
424 targets.append(front)
434 """Manage a list of boards."""
438 def add_board(self, brd):
439 """Add a new board to the list.
441 The board's target member must not already exist in the board list.
444 brd (Board): board to add
446 self._boards.append(brd)
448 def read_boards(self, fname):
449 """Read a list of boards from a board file.
451 Create a Board object for each and add it to our _boards list.
454 fname (str): Filename of boards.cfg file
456 with open(fname, 'r', encoding='utf-8') as inf:
460 fields = line.split()
463 for upto, field in enumerate(fields):
466 while len(fields) < 8:
471 brd = board.Board(*fields)
476 """Return a list of available boards.
479 List of Board objects
484 """Build a dictionary containing all the boards.
491 board_dict = OrderedDict()
492 for brd in self._boards:
493 board_dict[brd.target] = brd
496 def get_selected_dict(self):
497 """Return a dictionary containing the selected boards
500 List of Board objects that are marked selected
502 board_dict = OrderedDict()
503 for brd in self._boards:
505 board_dict[brd.target] = brd
508 def get_selected(self):
509 """Return a list of selected boards
512 List of Board objects that are marked selected
514 return [brd for brd in self._boards if brd.build_it]
516 def get_selected_names(self):
517 """Return a list of selected boards
520 List of board names that are marked selected
522 return [brd.target for brd in self._boards if brd.build_it]
525 def _build_terms(cls, args):
526 """Convert command line arguments to a list of terms.
528 This deals with parsing of the arguments. It handles the '&'
529 operator, which joins several expressions into a single Term.
532 ['arm & freescale sandbox', 'tegra']
534 will produce 3 Terms containing expressions as follows:
539 The first Term has two expressions, both of which must match for
540 a board to be selected.
543 args (list of str): List of command line arguments
546 list of Term: A list of Term objects
550 for word in arg.split():
552 for term in word.split('&'):
554 sym_build.append(term)
555 sym_build.append('&')
556 syms += sym_build[:-1]
575 def select_boards(self, args, exclude=None, brds=None):
576 """Mark boards selected based on args
578 Normally either boards (an explicit list of boards) or args (a list of
579 terms to match against) is used. It is possible to specify both, in
580 which case they are additive.
582 If brds and args are both empty, all boards are selected.
585 args (list of str): List of strings specifying boards to include,
586 either named, or by their target, architecture, cpu, vendor or
587 soc. If empty, all boards are selected.
588 exclude (list of str): List of boards to exclude, regardless of
589 'args', or None for none
590 brds (list of Board): List of boards to build, or None/[] for all
594 Dictionary which holds the list of boards which were selected
595 due to each argument, arranged by argument.
598 def _check_board(brd):
599 """Check whether to include or exclude a board
601 Checks the various terms and decide whether to build it or not (the
602 'build_it' variable).
604 If it is built, add the board to the result[term] list so we know
605 which term caused it to be built. Add it to result['all'] also.
607 Keep a list of boards we found in 'found', so we can report boards
608 which appear in self._boards but not in brds.
611 brd (Board): Board to check
617 if term.matches(brd.props):
618 matching_term = str(term)
622 if brd.target in brds:
624 found.append(brd.target)
628 # Check that it is not specifically excluded
629 for expr in exclude_list:
630 if expr.matches(brd.props):
637 result[matching_term].append(brd.target)
638 result['all'].append(brd.target)
640 result = OrderedDict()
642 terms = self._build_terms(args)
646 result[str(term)] = []
651 exclude_list.append(Expr(expr))
654 for brd in self._boards:
658 remaining = set(brds) - set(found)
660 warnings.append(f"Boards not found: {', '.join(remaining)}\n")
662 return result, warnings
665 def scan_defconfigs_for_multiprocess(cls, srcdir, queue, defconfigs,
667 """Scan defconfig files and queue their board parameters
669 This function is intended to be passed to multiprocessing.Process()
673 srcdir (str): Directory containing source code
674 queue (multiprocessing.Queue): The resulting board parameters are
676 defconfigs (sequence of str): A sequence of defconfig files to be
678 warn_targets (bool): True to warn about missing or duplicate
679 CONFIG_TARGET options
681 kconf_scanner = KconfigScanner(srcdir)
682 for defconfig in defconfigs:
683 queue.put(kconf_scanner.scan(defconfig, warn_targets))
686 def read_queues(cls, queues, params_list, warnings):
687 """Read the queues and append the data to the paramers list
690 queues (list of multiprocessing.Queue): Queues to read
691 params_list (list of dict): List to add params too
692 warnings (set of str): Set to add warnings to
695 while not que.empty():
696 params, warn = que.get()
697 params_list.append(params)
698 warnings.update(warn)
700 def scan_defconfigs(self, config_dir, srcdir, jobs=1, warn_targets=False):
701 """Collect board parameters for all defconfig files.
703 This function invokes multiple processes for faster processing.
706 config_dir (str): Directory containing the defconfig files
707 srcdir (str): Directory containing source code (Kconfig files)
708 jobs (int): The number of jobs to run simultaneously
709 warn_targets (bool): True to warn about missing or duplicate
710 CONFIG_TARGET options
714 list of dict: List of board parameters, each a dict:
715 key: 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
717 value: string value of the key
718 list of str: List of warnings recorded
721 for (dirpath, _, filenames) in os.walk(config_dir):
722 for filename in fnmatch.filter(filenames, '*_defconfig'):
723 if fnmatch.fnmatch(filename, '.*'):
725 all_defconfigs.append(os.path.join(dirpath, filename))
727 total_boards = len(all_defconfigs)
730 for i in range(jobs):
731 defconfigs = all_defconfigs[total_boards * i // jobs :
732 total_boards * (i + 1) // jobs]
733 que = multiprocessing.Queue(maxsize=-1)
734 proc = multiprocessing.Process(
735 target=self.scan_defconfigs_for_multiprocess,
736 args=(srcdir, que, defconfigs, warn_targets))
738 processes.append(proc)
741 # The resulting data should be accumulated to these lists
745 # Data in the queues should be retrieved preriodically.
746 # Otherwise, the queues would become full and subprocesses would get stuck.
747 while any(p.is_alive() for p in processes):
748 self.read_queues(queues, params_list, warnings)
749 # sleep for a while until the queues are filled
750 time.sleep(SLEEP_TIME)
752 # Joining subprocesses just in case
753 # (All subprocesses should already have been finished)
754 for proc in processes:
757 # retrieve leftover data
758 self.read_queues(queues, params_list, warnings)
760 return params_list, sorted(list(warnings))
763 def insert_maintainers_info(cls, srcdir, params_list):
764 """Add Status and Maintainers information to the board parameters list.
767 params_list (list of dict): A list of the board parameters
770 list of str: List of warnings collected due to missing status, etc.
772 database = MaintainersDatabase()
773 for (dirpath, _, filenames) in os.walk(srcdir):
774 if 'MAINTAINERS' in filenames and 'tools/buildman' not in dirpath:
775 database.parse_file(srcdir,
776 os.path.join(dirpath, 'MAINTAINERS'))
778 for i, params in enumerate(params_list):
779 target = params['target']
780 maintainers = database.get_maintainers(target)
781 params['maintainers'] = maintainers
783 params['status'] = database.get_status(target)
785 params['status'] = '-'
786 params_list[i] = params
787 return sorted(database.warnings)
790 def format_and_output(cls, params_list, output):
791 """Write board parameters into a file.
793 Columnate the board parameters, sort lines alphabetically,
794 and then write them to a file.
797 params_list (list of dict): The list of board parameters
798 output (str): The path to the output file
800 fields = ('status', 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
801 'config', 'maintainers')
803 # First, decide the width of each column
804 max_length = {f: 0 for f in fields}
805 for params in params_list:
807 max_length[field] = max(max_length[field], len(params[field]))
810 for params in params_list:
813 # insert two spaces between fields like column -t would
814 line += ' ' + params[field].ljust(max_length[field])
815 output_lines.append(line.strip())
817 # ignore case when sorting
818 output_lines.sort(key=str.lower)
820 with open(output, 'w', encoding="utf-8") as outf:
821 outf.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n')
823 def build_board_list(self, config_dir=CONFIG_DIR, srcdir='.', jobs=1,
825 """Generate a board-database file
827 This works by reading the Kconfig, then loading each board's defconfig
828 in to get the setting for each option. In particular, CONFIG_TARGET_xxx
829 is typically set by the defconfig, where xxx is the target to build.
832 config_dir (str): Directory containing the defconfig files
833 srcdir (str): Directory containing source code (Kconfig files)
834 jobs (int): The number of jobs to run simultaneously
835 warn_targets (bool): True to warn about missing or duplicate
836 CONFIG_TARGET options
840 list of dict: List of board parameters, each a dict:
841 key: 'arch', 'cpu', 'soc', 'vendor', 'board', 'config',
843 value: string value of the key
844 list of str: Warnings that came up
846 params_list, warnings = self.scan_defconfigs(config_dir, srcdir, jobs,
848 m_warnings = self.insert_maintainers_info(srcdir, params_list)
849 return params_list, warnings + m_warnings
851 def ensure_board_list(self, output, jobs=1, force=False, quiet=False):
852 """Generate a board database file if needed.
854 This is intended to check if Kconfig has changed since the boards.cfg
858 output (str): The name of the output file
859 jobs (int): The number of jobs to run simultaneously
860 force (bool): Force to generate the output even if it is new
861 quiet (bool): True to avoid printing a message if nothing needs doing
864 bool: True if all is well, False if there were warnings
866 if not force and output_is_new(output, CONFIG_DIR, '.'):
868 print(f'{output} is up to date. Nothing to do.')
870 params_list, warnings = self.build_board_list(CONFIG_DIR, '.', jobs)
871 for warn in warnings:
872 print(warn, file=sys.stderr)
873 self.format_and_output(params_list, output)