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 :
282 """Create an empty database."""
285 def get_status(self, target):
286 """Return the status of the given board.
288 The board status is generally either 'Active' or 'Orphan'.
289 Display a warning message and return '-' if status information
293 target (str): Build-target name
296 str: 'Active', 'Orphan' or '-'.
298 if not target in self.database:
299 print(f"WARNING: no status info for '{target}'", file=sys.stderr)
302 tmp = self.database[target][0]
303 if tmp.startswith('Maintained'):
305 if tmp.startswith('Supported'):
307 if tmp.startswith('Orphan'):
309 print(f"WARNING: {tmp}: unknown status for '{target}'", file=sys.stderr)
312 def get_maintainers(self, target):
313 """Return the maintainers of the given board.
316 target (str): Build-target name
319 str: Maintainers of the board. If the board has two or more
320 maintainers, they are separated with colons.
322 if not target in self.database:
323 print(f"WARNING: no maintainers for '{target}'", file=sys.stderr)
326 return ':'.join(self.database[target][1])
328 def parse_file(self, fname):
329 """Parse a MAINTAINERS file.
331 Parse a MAINTAINERS file and accumulate board status and maintainers
332 information in the self.database dict.
335 fname (str): MAINTAINERS file to be parsed
340 with open(fname, encoding="utf-8") as inf:
342 # Check also commented maintainers
343 if line[:3] == '#M:':
345 tag, rest = line[:2], line[2:].strip()
347 maintainers.append(rest)
349 # expand wildcard and filter by 'configs/*_defconfig'
350 for item in glob.glob(rest):
351 front, match, rear = item.partition('configs/')
352 if not front and match:
353 front, match, rear = rear.rpartition('_defconfig')
354 if match and not rear:
355 targets.append(front)
359 for target in targets:
360 self.database[target] = (status, maintainers)
365 for target in targets:
366 self.database[target] = (status, maintainers)
370 """Manage a list of boards."""
372 # Use a simple list here, sinc OrderedDict requires Python 2.7
375 def add_board(self, brd):
376 """Add a new board to the list.
378 The board's target member must not already exist in the board list.
381 brd (Board): board to add
383 self._boards.append(brd)
385 def read_boards(self, fname):
386 """Read a list of boards from a board file.
388 Create a Board object for each and add it to our _boards list.
391 fname (str): Filename of boards.cfg file
393 with open(fname, 'r', encoding='utf-8') as inf:
397 fields = line.split()
400 for upto, field in enumerate(fields):
403 while len(fields) < 8:
408 brd = board.Board(*fields)
413 """Return a list of available boards.
416 List of Board objects
421 """Build a dictionary containing all the boards.
428 board_dict = OrderedDict()
429 for brd in self._boards:
430 board_dict[brd.target] = brd
433 def get_selected_dict(self):
434 """Return a dictionary containing the selected boards
437 List of Board objects that are marked selected
439 board_dict = OrderedDict()
440 for brd in self._boards:
442 board_dict[brd.target] = brd
445 def get_selected(self):
446 """Return a list of selected boards
449 List of Board objects that are marked selected
451 return [brd for brd in self._boards if brd.build_it]
453 def get_selected_names(self):
454 """Return a list of selected boards
457 List of board names that are marked selected
459 return [brd.target for brd in self._boards if brd.build_it]
462 def _build_terms(cls, args):
463 """Convert command line arguments to a list of terms.
465 This deals with parsing of the arguments. It handles the '&'
466 operator, which joins several expressions into a single Term.
469 ['arm & freescale sandbox', 'tegra']
471 will produce 3 Terms containing expressions as follows:
476 The first Term has two expressions, both of which must match for
477 a board to be selected.
480 args (list of str): List of command line arguments
483 list of Term: A list of Term objects
487 for word in arg.split():
489 for term in word.split('&'):
491 sym_build.append(term)
492 sym_build.append('&')
493 syms += sym_build[:-1]
512 def select_boards(self, args, exclude=None, brds=None):
513 """Mark boards selected based on args
515 Normally either boards (an explicit list of boards) or args (a list of
516 terms to match against) is used. It is possible to specify both, in
517 which case they are additive.
519 If brds and args are both empty, all boards are selected.
522 args (list of str): List of strings specifying boards to include,
523 either named, or by their target, architecture, cpu, vendor or
524 soc. If empty, all boards are selected.
525 exclude (list of str): List of boards to exclude, regardless of
526 'args', or None for none
527 brds (list of Board): List of boards to build, or None/[] for all
531 Dictionary which holds the list of boards which were selected
532 due to each argument, arranged by argument.
535 def _check_board(brd):
536 """Check whether to include or exclude a board
538 Checks the various terms and decide whether to build it or not (the
539 'build_it' variable).
541 If it is built, add the board to the result[term] list so we know
542 which term caused it to be built. Add it to result['all'] also.
544 Keep a list of boards we found in 'found', so we can report boards
545 which appear in self._boards but not in brds.
548 brd (Board): Board to check
554 if term.matches(brd.props):
555 matching_term = str(term)
559 if brd.target in brds:
561 found.append(brd.target)
565 # Check that it is not specifically excluded
566 for expr in exclude_list:
567 if expr.matches(brd.props):
574 result[matching_term].append(brd.target)
575 result['all'].append(brd.target)
577 result = OrderedDict()
579 terms = self._build_terms(args)
583 result[str(term)] = []
588 exclude_list.append(Expr(expr))
591 for brd in self._boards:
595 remaining = set(brds) - set(found)
597 warnings.append(f"Boards not found: {', '.join(remaining)}\n")
599 return result, warnings
602 def scan_defconfigs_for_multiprocess(cls, queue, defconfigs):
603 """Scan defconfig files and queue their board parameters
605 This function is intended to be passed to multiprocessing.Process()
609 queue (multiprocessing.Queue): The resulting board parameters are
611 defconfigs (sequence of str): A sequence of defconfig files to be
614 kconf_scanner = KconfigScanner()
615 for defconfig in defconfigs:
616 queue.put(kconf_scanner.scan(defconfig))
619 def read_queues(cls, queues, params_list):
620 """Read the queues and append the data to the paramers list"""
622 while not que.empty():
623 params_list.append(que.get())
625 def scan_defconfigs(self, jobs=1):
626 """Collect board parameters for all defconfig files.
628 This function invokes multiple processes for faster processing.
631 jobs (int): The number of jobs to run simultaneously
634 for (dirpath, _, filenames) in os.walk(CONFIG_DIR):
635 for filename in fnmatch.filter(filenames, '*_defconfig'):
636 if fnmatch.fnmatch(filename, '.*'):
638 all_defconfigs.append(os.path.join(dirpath, filename))
640 total_boards = len(all_defconfigs)
643 for i in range(jobs):
644 defconfigs = all_defconfigs[total_boards * i // jobs :
645 total_boards * (i + 1) // jobs]
646 que = multiprocessing.Queue(maxsize=-1)
647 proc = multiprocessing.Process(
648 target=self.scan_defconfigs_for_multiprocess,
649 args=(que, defconfigs))
651 processes.append(proc)
654 # The resulting data should be accumulated to this list
657 # Data in the queues should be retrieved preriodically.
658 # Otherwise, the queues would become full and subprocesses would get stuck.
659 while any(p.is_alive() for p in processes):
660 self.read_queues(queues, params_list)
661 # sleep for a while until the queues are filled
662 time.sleep(SLEEP_TIME)
664 # Joining subprocesses just in case
665 # (All subprocesses should already have been finished)
666 for proc in processes:
669 # retrieve leftover data
670 self.read_queues(queues, params_list)
675 def insert_maintainers_info(cls, params_list):
676 """Add Status and Maintainers information to the board parameters list.
679 params_list (list of dict): A list of the board parameters
681 database = MaintainersDatabase()
682 for (dirpath, _, filenames) in os.walk('.'):
683 if 'MAINTAINERS' in filenames:
684 database.parse_file(os.path.join(dirpath, 'MAINTAINERS'))
686 for i, params in enumerate(params_list):
687 target = params['target']
688 params['status'] = database.get_status(target)
689 params['maintainers'] = database.get_maintainers(target)
690 params_list[i] = params
693 def format_and_output(cls, params_list, output):
694 """Write board parameters into a file.
696 Columnate the board parameters, sort lines alphabetically,
697 and then write them to a file.
700 params_list (list of dict): The list of board parameters
701 output (str): The path to the output file
703 fields = ('status', 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
704 'config', 'maintainers')
706 # First, decide the width of each column
707 max_length = {f: 0 for f in fields}
708 for params in params_list:
710 max_length[field] = max(max_length[field], len(params[field]))
713 for params in params_list:
716 # insert two spaces between fields like column -t would
717 line += ' ' + params[field].ljust(max_length[field])
718 output_lines.append(line.strip())
720 # ignore case when sorting
721 output_lines.sort(key=str.lower)
723 with open(output, 'w', encoding="utf-8") as outf:
724 outf.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n')
726 def ensure_board_list(self, output, jobs=1, force=False, quiet=False):
727 """Generate a board database file if needed.
730 output (str): The name of the output file
731 jobs (int): The number of jobs to run simultaneously
732 force (bool): Force to generate the output even if it is new
733 quiet (bool): True to avoid printing a message if nothing needs doing
735 if not force and output_is_new(output):
737 print(f'{output} is up to date. Nothing to do.')
739 params_list = self.scan_defconfigs(jobs)
740 self.insert_maintainers_info(params_list)
741 self.format_and_output(params_list, output)