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):
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
64 True if the given output file exists and is newer than any of
65 *_defconfig, MAINTAINERS and Kconfig*. False otherwise.
68 OSError: output file exists but could not be opened
70 # pylint: disable=too-many-branches
72 ctime = os.path.getctime(output)
73 except OSError as exception:
74 if exception.errno == errno.ENOENT:
75 # return False on 'No such file or directory' error
79 for (dirpath, _, filenames) in os.walk(CONFIG_DIR):
80 for filename in fnmatch.filter(filenames, '*_defconfig'):
81 if fnmatch.fnmatch(filename, '.*'):
83 filepath = os.path.join(dirpath, filename)
84 if ctime < os.path.getctime(filepath):
87 for (dirpath, _, filenames) in os.walk('.'):
88 for filename in filenames:
89 if (fnmatch.fnmatch(filename, '*~') or
90 not fnmatch.fnmatch(filename, 'Kconfig*') and
91 not filename == 'MAINTAINERS'):
93 filepath = os.path.join(dirpath, filename)
94 if ctime < os.path.getctime(filepath):
97 # Detect a board that has been removed since the current board database
99 with open(output, encoding="utf-8") as inf:
101 if 'Options,' in line:
103 if line[0] == '#' or line == '\n':
105 defconfig = line.split()[6] + '_defconfig'
106 if not os.path.exists(os.path.join(CONFIG_DIR, defconfig)):
113 """A single regular expression for matching boards to build"""
115 def __init__(self, expr):
116 """Set up a new Expr object.
119 expr (str): String cotaining regular expression to store
122 self._re = re.compile(expr)
124 def matches(self, props):
125 """Check if any of the properties match the regular expression.
128 props (list of str): List of properties to check
130 True if any of the properties match the regular expression
133 if self._re.match(prop):
141 """A list of expressions each of which must match with properties.
143 This provides a list of 'AND' expressions, meaning that each must
144 match the board properties for that board to be built.
148 self._board_count = 0
150 def add_expr(self, expr):
151 """Add an Expr object to the list to check.
154 expr (Expr): New Expr object to add to the list of those that must
155 match for a board to be built.
157 self._expr_list.append(Expr(expr))
160 """Return some sort of useful string describing the term"""
161 return '&'.join([str(expr) for expr in self._expr_list])
163 def matches(self, props):
164 """Check if any of the properties match this term
166 Each of the expressions in the term is checked. All must match.
169 props (list of str): List of properties to check
171 True if all of the expressions in the Term match, else False
173 for expr in self._expr_list:
174 if not expr.matches(props):
179 class KconfigScanner:
181 """Kconfig scanner."""
183 ### constant variable only used in this class ###
188 'vendor' : 'SYS_VENDOR',
189 'board' : 'SYS_BOARD',
190 'config' : 'SYS_CONFIG_NAME',
191 # 'target' is added later
195 """Scan all the Kconfig files and create a Kconfig object."""
196 # Define environment variables referenced from Kconfig
197 os.environ['srctree'] = os.getcwd()
198 os.environ['UBOOTVERSION'] = 'dummy'
199 os.environ['KCONFIG_OBJDIR'] = ''
201 self._conf = kconfiglib.Kconfig(warn=False)
204 """Delete a leftover temporary file before exit.
206 The scan() method of this class creates a temporay file and deletes
207 it on success. If scan() method throws an exception on the way,
208 the temporary file might be left over. In that case, it should be
209 deleted in this destructor.
212 try_remove(self._tmpfile)
214 def scan(self, defconfig):
215 """Load a defconfig file to obtain board parameters.
218 defconfig (str): path to the defconfig file to be processed
221 A dictionary of board parameters. It has a form of:
226 'vendor': <vendor_name>,
227 'board': <board_name>,
228 'target': <target_name>,
229 'config': <config_header_name>,
232 # strip special prefixes and save it in a temporary file
233 outfd, self._tmpfile = tempfile.mkstemp()
234 with os.fdopen(outfd, 'w') as outf:
235 with open(defconfig, encoding='utf-8') as inf:
237 colon = line.find(':CONFIG_')
241 outf.write(line[colon + 1:])
243 self._conf.load_config(self._tmpfile)
244 try_remove(self._tmpfile)
249 # Get the value of CONFIG_SYS_ARCH, CONFIG_SYS_CPU, ... etc.
250 # Set '-' if the value is empty.
251 for key, symbol in list(self._SYMBOL_TABLE.items()):
252 value = self._conf.syms.get(symbol).str_value
258 defconfig = os.path.basename(defconfig)
259 params['target'], match, rear = defconfig.partition('_defconfig')
260 assert match and not rear, f'{defconfig} : invalid defconfig'
263 if params['arch'] == 'arm' and params['cpu'] == 'armv8':
264 params['arch'] = 'aarch64'
269 class MaintainersDatabase:
271 """The database of board status and maintainers.
275 key: Board-target name (e.g. 'snow')
277 str: Board status (e.g. 'Active')
278 str: List of maintainers, separated by :
279 warnings (list of str): List of warnings due to missing status, etc.
283 """Create an empty database."""
287 def get_status(self, target):
288 """Return the status of the given board.
290 The board status is generally either 'Active' or 'Orphan'.
291 Display a warning message and return '-' if status information
295 target (str): Build-target name
298 str: 'Active', 'Orphan' or '-'.
300 if not target in self.database:
301 self.warnings.append(f"WARNING: no status info for '{target}'")
304 tmp = self.database[target][0]
305 if tmp.startswith('Maintained'):
307 if tmp.startswith('Supported'):
309 if tmp.startswith('Orphan'):
311 self.warnings.append(f"WARNING: {tmp}: unknown status for '{target}'")
314 def get_maintainers(self, target):
315 """Return the maintainers of the given board.
318 target (str): Build-target name
321 str: Maintainers of the board. If the board has two or more
322 maintainers, they are separated with colons.
324 if not target in self.database:
325 self.warnings.append(f"WARNING: no maintainers for '{target}'")
328 return ':'.join(self.database[target][1])
330 def parse_file(self, fname):
331 """Parse a MAINTAINERS file.
333 Parse a MAINTAINERS file and accumulate board status and maintainers
334 information in the self.database dict.
337 fname (str): MAINTAINERS file to be parsed
342 with open(fname, encoding="utf-8") as inf:
344 # Check also commented maintainers
345 if line[:3] == '#M:':
347 tag, rest = line[:2], line[2:].strip()
349 maintainers.append(rest)
351 # expand wildcard and filter by 'configs/*_defconfig'
352 for item in glob.glob(rest):
353 front, match, rear = item.partition('configs/')
354 if not front and match:
355 front, match, rear = rear.rpartition('_defconfig')
356 if match and not rear:
357 targets.append(front)
361 for target in targets:
362 self.database[target] = (status, maintainers)
367 for target in targets:
368 self.database[target] = (status, maintainers)
372 """Manage a list of boards."""
374 # Use a simple list here, sinc OrderedDict requires Python 2.7
377 def add_board(self, brd):
378 """Add a new board to the list.
380 The board's target member must not already exist in the board list.
383 brd (Board): board to add
385 self._boards.append(brd)
387 def read_boards(self, fname):
388 """Read a list of boards from a board file.
390 Create a Board object for each and add it to our _boards list.
393 fname (str): Filename of boards.cfg file
395 with open(fname, 'r', encoding='utf-8') as inf:
399 fields = line.split()
402 for upto, field in enumerate(fields):
405 while len(fields) < 8:
410 brd = board.Board(*fields)
415 """Return a list of available boards.
418 List of Board objects
423 """Build a dictionary containing all the boards.
430 board_dict = OrderedDict()
431 for brd in self._boards:
432 board_dict[brd.target] = brd
435 def get_selected_dict(self):
436 """Return a dictionary containing the selected boards
439 List of Board objects that are marked selected
441 board_dict = OrderedDict()
442 for brd in self._boards:
444 board_dict[brd.target] = brd
447 def get_selected(self):
448 """Return a list of selected boards
451 List of Board objects that are marked selected
453 return [brd for brd in self._boards if brd.build_it]
455 def get_selected_names(self):
456 """Return a list of selected boards
459 List of board names that are marked selected
461 return [brd.target for brd in self._boards if brd.build_it]
464 def _build_terms(cls, args):
465 """Convert command line arguments to a list of terms.
467 This deals with parsing of the arguments. It handles the '&'
468 operator, which joins several expressions into a single Term.
471 ['arm & freescale sandbox', 'tegra']
473 will produce 3 Terms containing expressions as follows:
478 The first Term has two expressions, both of which must match for
479 a board to be selected.
482 args (list of str): List of command line arguments
485 list of Term: A list of Term objects
489 for word in arg.split():
491 for term in word.split('&'):
493 sym_build.append(term)
494 sym_build.append('&')
495 syms += sym_build[:-1]
514 def select_boards(self, args, exclude=None, brds=None):
515 """Mark boards selected based on args
517 Normally either boards (an explicit list of boards) or args (a list of
518 terms to match against) is used. It is possible to specify both, in
519 which case they are additive.
521 If brds and args are both empty, all boards are selected.
524 args (list of str): List of strings specifying boards to include,
525 either named, or by their target, architecture, cpu, vendor or
526 soc. If empty, all boards are selected.
527 exclude (list of str): List of boards to exclude, regardless of
528 'args', or None for none
529 brds (list of Board): List of boards to build, or None/[] for all
533 Dictionary which holds the list of boards which were selected
534 due to each argument, arranged by argument.
537 def _check_board(brd):
538 """Check whether to include or exclude a board
540 Checks the various terms and decide whether to build it or not (the
541 'build_it' variable).
543 If it is built, add the board to the result[term] list so we know
544 which term caused it to be built. Add it to result['all'] also.
546 Keep a list of boards we found in 'found', so we can report boards
547 which appear in self._boards but not in brds.
550 brd (Board): Board to check
556 if term.matches(brd.props):
557 matching_term = str(term)
561 if brd.target in brds:
563 found.append(brd.target)
567 # Check that it is not specifically excluded
568 for expr in exclude_list:
569 if expr.matches(brd.props):
576 result[matching_term].append(brd.target)
577 result['all'].append(brd.target)
579 result = OrderedDict()
581 terms = self._build_terms(args)
585 result[str(term)] = []
590 exclude_list.append(Expr(expr))
593 for brd in self._boards:
597 remaining = set(brds) - set(found)
599 warnings.append(f"Boards not found: {', '.join(remaining)}\n")
601 return result, warnings
604 def scan_defconfigs_for_multiprocess(cls, queue, defconfigs):
605 """Scan defconfig files and queue their board parameters
607 This function is intended to be passed to multiprocessing.Process()
611 queue (multiprocessing.Queue): The resulting board parameters are
613 defconfigs (sequence of str): A sequence of defconfig files to be
616 kconf_scanner = KconfigScanner()
617 for defconfig in defconfigs:
618 queue.put(kconf_scanner.scan(defconfig))
621 def read_queues(cls, queues, params_list):
622 """Read the queues and append the data to the paramers list"""
624 while not que.empty():
625 params_list.append(que.get())
627 def scan_defconfigs(self, jobs=1):
628 """Collect board parameters for all defconfig files.
630 This function invokes multiple processes for faster processing.
633 jobs (int): The number of jobs to run simultaneously
636 for (dirpath, _, filenames) in os.walk(CONFIG_DIR):
637 for filename in fnmatch.filter(filenames, '*_defconfig'):
638 if fnmatch.fnmatch(filename, '.*'):
640 all_defconfigs.append(os.path.join(dirpath, filename))
642 total_boards = len(all_defconfigs)
645 for i in range(jobs):
646 defconfigs = all_defconfigs[total_boards * i // jobs :
647 total_boards * (i + 1) // jobs]
648 que = multiprocessing.Queue(maxsize=-1)
649 proc = multiprocessing.Process(
650 target=self.scan_defconfigs_for_multiprocess,
651 args=(que, defconfigs))
653 processes.append(proc)
656 # The resulting data should be accumulated to this list
659 # Data in the queues should be retrieved preriodically.
660 # Otherwise, the queues would become full and subprocesses would get stuck.
661 while any(p.is_alive() for p in processes):
662 self.read_queues(queues, params_list)
663 # sleep for a while until the queues are filled
664 time.sleep(SLEEP_TIME)
666 # Joining subprocesses just in case
667 # (All subprocesses should already have been finished)
668 for proc in processes:
671 # retrieve leftover data
672 self.read_queues(queues, params_list)
677 def insert_maintainers_info(cls, params_list):
678 """Add Status and Maintainers information to the board parameters list.
681 params_list (list of dict): A list of the board parameters
684 list of str: List of warnings collected due to missing status, etc.
686 database = MaintainersDatabase()
687 for (dirpath, _, filenames) in os.walk('.'):
688 if 'MAINTAINERS' in filenames:
689 database.parse_file(os.path.join(dirpath, 'MAINTAINERS'))
691 for i, params in enumerate(params_list):
692 target = params['target']
693 params['status'] = database.get_status(target)
694 params['maintainers'] = database.get_maintainers(target)
695 params_list[i] = params
696 return database.warnings
699 def format_and_output(cls, params_list, output):
700 """Write board parameters into a file.
702 Columnate the board parameters, sort lines alphabetically,
703 and then write them to a file.
706 params_list (list of dict): The list of board parameters
707 output (str): The path to the output file
709 fields = ('status', 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
710 'config', 'maintainers')
712 # First, decide the width of each column
713 max_length = {f: 0 for f in fields}
714 for params in params_list:
716 max_length[field] = max(max_length[field], len(params[field]))
719 for params in params_list:
722 # insert two spaces between fields like column -t would
723 line += ' ' + params[field].ljust(max_length[field])
724 output_lines.append(line.strip())
726 # ignore case when sorting
727 output_lines.sort(key=str.lower)
729 with open(output, 'w', encoding="utf-8") as outf:
730 outf.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n')
732 def ensure_board_list(self, output, jobs=1, force=False, quiet=False):
733 """Generate a board database file if needed.
736 output (str): The name of the output file
737 jobs (int): The number of jobs to run simultaneously
738 force (bool): Force to generate the output even if it is new
739 quiet (bool): True to avoid printing a message if nothing needs doing
742 bool: True if all is well, False if there were warnings
744 if not force and output_is_new(output):
746 print(f'{output} is up to date. Nothing to do.')
748 params_list = self.scan_defconfigs(jobs)
749 warnings = self.insert_maintainers_info(params_list)
750 for warn in warnings:
751 print(warn, file=sys.stderr)
752 self.format_and_output(params_list, output)