blob: dabf694e0d0cdb7db71953a956dae95319366ddd [file] [log] [blame]
Simon Glass20751d62022-07-11 19:04:03 -06001# SPDX-License-Identifier: GPL-2.0+
2# Copyright (c) 2012 The Chromium OS Authors.
Simon Glass0c477b72022-07-11 19:04:04 -06003# Author: Simon Glass <sjg@chromium.org>
4# Author: Masahiro Yamada <yamada.m@jp.panasonic.com>
Simon Glass20751d62022-07-11 19:04:03 -06005
6"""Maintains a list of boards and allows them to be selected"""
7
8from collections import OrderedDict
Simon Glass0c477b72022-07-11 19:04:04 -06009import errno
10import fnmatch
11import glob
12import multiprocessing
13import os
Simon Glass20751d62022-07-11 19:04:03 -060014import re
Simon Glass0c477b72022-07-11 19:04:04 -060015import sys
16import tempfile
17import time
Simon Glass20751d62022-07-11 19:04:03 -060018
19from buildman import board
Simon Glass0c477b72022-07-11 19:04:04 -060020from buildman import kconfiglib
Simon Glass20751d62022-07-11 19:04:03 -060021
22
Simon Glass0c477b72022-07-11 19:04:04 -060023### constant variables ###
24OUTPUT_FILE = 'boards.cfg'
25CONFIG_DIR = 'configs'
26SLEEP_TIME = 0.03
Simon Glass58d41be2022-07-11 19:04:05 -060027COMMENT_BLOCK = f'''#
Simon Glass0c477b72022-07-11 19:04:04 -060028# List of boards
Simon Glass58d41be2022-07-11 19:04:05 -060029# Automatically generated by {__file__}: don't edit
Simon Glass0c477b72022-07-11 19:04:04 -060030#
Simon Glassd2d4c602022-07-11 19:04:06 -060031# Status, Arch, CPU, SoC, Vendor, Board, Target, Config, Maintainers
Simon Glass0c477b72022-07-11 19:04:04 -060032
Simon Glass58d41be2022-07-11 19:04:05 -060033'''
Simon Glass0c477b72022-07-11 19:04:04 -060034
35
Simon Glass58d41be2022-07-11 19:04:05 -060036def try_remove(fname):
37 """Remove a file ignoring 'No such file or directory' error.
38
39 Args:
40 fname (str): Filename to remove
41
42 Raises:
43 OSError: output file exists but could not be removed
44 """
Simon Glass0c477b72022-07-11 19:04:04 -060045 try:
Simon Glass58d41be2022-07-11 19:04:05 -060046 os.remove(fname)
Simon Glass0c477b72022-07-11 19:04:04 -060047 except OSError as exception:
48 # Ignore 'No such file or directory' error
49 if exception.errno != errno.ENOENT:
50 raise
51
52
Simon Glasse1568362023-07-19 17:48:14 -060053def output_is_new(output, config_dir, srcdir):
Simon Glass0c477b72022-07-11 19:04:04 -060054 """Check if the output file is up to date.
55
Simon Glass58d41be2022-07-11 19:04:05 -060056 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
58 boards.
59
60 Args:
61 output (str): Filename to check
Simon Glasse1568362023-07-19 17:48:14 -060062 config_dir (str): Directory containing defconfig files
63 srcdir (str): Directory containing Kconfig and MAINTAINERS files
Simon Glass58d41be2022-07-11 19:04:05 -060064
Simon Glass0c477b72022-07-11 19:04:04 -060065 Returns:
Simon Glass58d41be2022-07-11 19:04:05 -060066 True if the given output file exists and is newer than any of
67 *_defconfig, MAINTAINERS and Kconfig*. False otherwise.
68
69 Raises:
70 OSError: output file exists but could not be opened
Simon Glass0c477b72022-07-11 19:04:04 -060071 """
Simon Glass58d41be2022-07-11 19:04:05 -060072 # pylint: disable=too-many-branches
Simon Glass0c477b72022-07-11 19:04:04 -060073 try:
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
78 return False
Simon Glass58d41be2022-07-11 19:04:05 -060079 raise
Simon Glass0c477b72022-07-11 19:04:04 -060080
Simon Glasse1568362023-07-19 17:48:14 -060081 for (dirpath, _, filenames) in os.walk(config_dir):
Simon Glass0c477b72022-07-11 19:04:04 -060082 for filename in fnmatch.filter(filenames, '*_defconfig'):
83 if fnmatch.fnmatch(filename, '.*'):
84 continue
85 filepath = os.path.join(dirpath, filename)
86 if ctime < os.path.getctime(filepath):
87 return False
88
Simon Glasse1568362023-07-19 17:48:14 -060089 for (dirpath, _, filenames) in os.walk(srcdir):
Simon Glass0c477b72022-07-11 19:04:04 -060090 for filename in filenames:
91 if (fnmatch.fnmatch(filename, '*~') or
92 not fnmatch.fnmatch(filename, 'Kconfig*') and
93 not filename == 'MAINTAINERS'):
94 continue
95 filepath = os.path.join(dirpath, filename)
96 if ctime < os.path.getctime(filepath):
97 return False
98
99 # Detect a board that has been removed since the current board database
100 # was generated
Simon Glass58d41be2022-07-11 19:04:05 -0600101 with open(output, encoding="utf-8") as inf:
102 for line in inf:
Simon Glassd2d4c602022-07-11 19:04:06 -0600103 if 'Options,' in line:
104 return False
Simon Glass0c477b72022-07-11 19:04:04 -0600105 if line[0] == '#' or line == '\n':
106 continue
107 defconfig = line.split()[6] + '_defconfig'
Simon Glasse1568362023-07-19 17:48:14 -0600108 if not os.path.exists(os.path.join(config_dir, defconfig)):
Simon Glass0c477b72022-07-11 19:04:04 -0600109 return False
110
111 return True
112
113
Simon Glass20751d62022-07-11 19:04:03 -0600114class Expr:
115 """A single regular expression for matching boards to build"""
116
117 def __init__(self, expr):
118 """Set up a new Expr object.
119
120 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600121 expr (str): String cotaining regular expression to store
Simon Glass20751d62022-07-11 19:04:03 -0600122 """
123 self._expr = expr
124 self._re = re.compile(expr)
125
126 def matches(self, props):
127 """Check if any of the properties match the regular expression.
128
129 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600130 props (list of str): List of properties to check
Simon Glass20751d62022-07-11 19:04:03 -0600131 Returns:
132 True if any of the properties match the regular expression
133 """
134 for prop in props:
135 if self._re.match(prop):
136 return True
137 return False
138
139 def __str__(self):
140 return self._expr
141
142class Term:
143 """A list of expressions each of which must match with properties.
144
145 This provides a list of 'AND' expressions, meaning that each must
146 match the board properties for that board to be built.
147 """
148 def __init__(self):
149 self._expr_list = []
150 self._board_count = 0
151
152 def add_expr(self, expr):
153 """Add an Expr object to the list to check.
154
155 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600156 expr (Expr): New Expr object to add to the list of those that must
Simon Glass20751d62022-07-11 19:04:03 -0600157 match for a board to be built.
158 """
159 self._expr_list.append(Expr(expr))
160
161 def __str__(self):
162 """Return some sort of useful string describing the term"""
163 return '&'.join([str(expr) for expr in self._expr_list])
164
165 def matches(self, props):
166 """Check if any of the properties match this term
167
168 Each of the expressions in the term is checked. All must match.
169
170 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600171 props (list of str): List of properties to check
Simon Glass20751d62022-07-11 19:04:03 -0600172 Returns:
173 True if all of the expressions in the Term match, else False
174 """
175 for expr in self._expr_list:
176 if not expr.matches(props):
177 return False
178 return True
179
Simon Glass0c477b72022-07-11 19:04:04 -0600180
181class KconfigScanner:
182
183 """Kconfig scanner."""
184
185 ### constant variable only used in this class ###
186 _SYMBOL_TABLE = {
187 'arch' : 'SYS_ARCH',
188 'cpu' : 'SYS_CPU',
189 'soc' : 'SYS_SOC',
190 'vendor' : 'SYS_VENDOR',
191 'board' : 'SYS_BOARD',
192 'config' : 'SYS_CONFIG_NAME',
Simon Glassd2d4c602022-07-11 19:04:06 -0600193 # 'target' is added later
Simon Glass0c477b72022-07-11 19:04:04 -0600194 }
195
Simon Glasse1568362023-07-19 17:48:14 -0600196 def __init__(self, srctree):
Simon Glass0c477b72022-07-11 19:04:04 -0600197 """Scan all the Kconfig files and create a Kconfig object."""
198 # Define environment variables referenced from Kconfig
Simon Glasse1568362023-07-19 17:48:14 -0600199 os.environ['srctree'] = srctree
Simon Glass0c477b72022-07-11 19:04:04 -0600200 os.environ['UBOOTVERSION'] = 'dummy'
201 os.environ['KCONFIG_OBJDIR'] = ''
Simon Glass58d41be2022-07-11 19:04:05 -0600202 self._tmpfile = None
Simon Glass0c477b72022-07-11 19:04:04 -0600203 self._conf = kconfiglib.Kconfig(warn=False)
204
205 def __del__(self):
206 """Delete a leftover temporary file before exit.
207
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.
212 """
Simon Glass58d41be2022-07-11 19:04:05 -0600213 if self._tmpfile:
Simon Glass0c477b72022-07-11 19:04:04 -0600214 try_remove(self._tmpfile)
215
216 def scan(self, defconfig):
217 """Load a defconfig file to obtain board parameters.
218
Simon Glass58d41be2022-07-11 19:04:05 -0600219 Args:
220 defconfig (str): path to the defconfig file to be processed
Simon Glass0c477b72022-07-11 19:04:04 -0600221
222 Returns:
Simon Glass07a95d82023-07-19 17:48:21 -0600223 tuple: dictionary of board parameters. It has a form of:
224 {
225 'arch': <arch_name>,
226 'cpu': <cpu_name>,
227 'soc': <soc_name>,
228 'vendor': <vendor_name>,
229 'board': <board_name>,
230 'target': <target_name>,
231 'config': <config_header_name>,
232 }
233 warnings (list of str): list of warnings found
Simon Glass0c477b72022-07-11 19:04:04 -0600234 """
Simon Glass7188e4b2023-07-19 17:48:20 -0600235 leaf = os.path.basename(defconfig)
236 expect_target, match, rear = leaf.partition('_defconfig')
237 assert match and not rear, f'{leaf} : invalid defconfig'
238
Simon Glass1ee644d2023-07-19 17:48:13 -0600239 self._conf.load_config(defconfig)
Simon Glass0c477b72022-07-11 19:04:04 -0600240 self._tmpfile = None
241
242 params = {}
Simon Glass07a95d82023-07-19 17:48:21 -0600243 warnings = []
Simon Glass0c477b72022-07-11 19:04:04 -0600244
245 # Get the value of CONFIG_SYS_ARCH, CONFIG_SYS_CPU, ... etc.
246 # Set '-' if the value is empty.
247 for key, symbol in list(self._SYMBOL_TABLE.items()):
248 value = self._conf.syms.get(symbol).str_value
249 if value:
250 params[key] = value
251 else:
252 params[key] = '-'
253
Simon Glass07a95d82023-07-19 17:48:21 -0600254 # Check there is exactly one TARGET_xxx set
255 target = None
256 for name, sym in self._conf.syms.items():
257 if name.startswith('TARGET_') and sym.str_value == 'y':
258 tname = name[7:].lower()
259 if target:
260 warnings.append(
261 f'WARNING: {leaf}: Duplicate TARGET_xxx: {target} and {tname}')
262 else:
263 target = tname
264
Simon Glass061499b2023-07-19 17:48:22 -0600265 if not target:
266 cfg_name = expect_target.replace('-', '_').upper()
267 warnings.append(f'WARNING: {leaf}: No TARGET_{cfg_name} enabled')
268
Simon Glass7188e4b2023-07-19 17:48:20 -0600269 params['target'] = expect_target
Simon Glass0c477b72022-07-11 19:04:04 -0600270
271 # fix-up for aarch64
272 if params['arch'] == 'arm' and params['cpu'] == 'armv8':
273 params['arch'] = 'aarch64'
274
Heinrich Schuchardt8c7fd872022-10-03 18:07:53 +0200275 # fix-up for riscv
276 if params['arch'] == 'riscv':
277 try:
278 value = self._conf.syms.get('ARCH_RV32I').str_value
279 except:
280 value = ''
281 if value == 'y':
282 params['arch'] = 'riscv32'
283 else:
284 params['arch'] = 'riscv64'
285
Simon Glass07a95d82023-07-19 17:48:21 -0600286 return params, warnings
Simon Glass0c477b72022-07-11 19:04:04 -0600287
288
289class MaintainersDatabase:
290
Simon Glass58d41be2022-07-11 19:04:05 -0600291 """The database of board status and maintainers.
292
293 Properties:
294 database: dict:
295 key: Board-target name (e.g. 'snow')
296 value: tuple:
297 str: Board status (e.g. 'Active')
298 str: List of maintainers, separated by :
Simon Glassafbd2132022-07-11 19:04:08 -0600299 warnings (list of str): List of warnings due to missing status, etc.
Simon Glass58d41be2022-07-11 19:04:05 -0600300 """
Simon Glass0c477b72022-07-11 19:04:04 -0600301
302 def __init__(self):
303 """Create an empty database."""
304 self.database = {}
Simon Glassafbd2132022-07-11 19:04:08 -0600305 self.warnings = []
Simon Glass0c477b72022-07-11 19:04:04 -0600306
307 def get_status(self, target):
308 """Return the status of the given board.
309
310 The board status is generally either 'Active' or 'Orphan'.
311 Display a warning message and return '-' if status information
312 is not found.
313
Simon Glass58d41be2022-07-11 19:04:05 -0600314 Args:
315 target (str): Build-target name
316
Simon Glass0c477b72022-07-11 19:04:04 -0600317 Returns:
Simon Glass58d41be2022-07-11 19:04:05 -0600318 str: 'Active', 'Orphan' or '-'.
Simon Glass0c477b72022-07-11 19:04:04 -0600319 """
320 if not target in self.database:
Simon Glassafbd2132022-07-11 19:04:08 -0600321 self.warnings.append(f"WARNING: no status info for '{target}'")
Simon Glass0c477b72022-07-11 19:04:04 -0600322 return '-'
323
324 tmp = self.database[target][0]
325 if tmp.startswith('Maintained'):
326 return 'Active'
Simon Glass58d41be2022-07-11 19:04:05 -0600327 if tmp.startswith('Supported'):
Simon Glass0c477b72022-07-11 19:04:04 -0600328 return 'Active'
Simon Glass58d41be2022-07-11 19:04:05 -0600329 if tmp.startswith('Orphan'):
Simon Glass0c477b72022-07-11 19:04:04 -0600330 return 'Orphan'
Simon Glassafbd2132022-07-11 19:04:08 -0600331 self.warnings.append(f"WARNING: {tmp}: unknown status for '{target}'")
Simon Glass58d41be2022-07-11 19:04:05 -0600332 return '-'
Simon Glass0c477b72022-07-11 19:04:04 -0600333
334 def get_maintainers(self, target):
335 """Return the maintainers of the given board.
336
Simon Glass58d41be2022-07-11 19:04:05 -0600337 Args:
338 target (str): Build-target name
339
Simon Glass0c477b72022-07-11 19:04:04 -0600340 Returns:
Simon Glass58d41be2022-07-11 19:04:05 -0600341 str: Maintainers of the board. If the board has two or more
342 maintainers, they are separated with colons.
Simon Glass0c477b72022-07-11 19:04:04 -0600343 """
344 if not target in self.database:
Simon Glassafbd2132022-07-11 19:04:08 -0600345 self.warnings.append(f"WARNING: no maintainers for '{target}'")
Simon Glass0c477b72022-07-11 19:04:04 -0600346 return ''
347
348 return ':'.join(self.database[target][1])
349
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600350 def parse_file(self, srcdir, fname):
Simon Glass0c477b72022-07-11 19:04:04 -0600351 """Parse a MAINTAINERS file.
352
Simon Glass58d41be2022-07-11 19:04:05 -0600353 Parse a MAINTAINERS file and accumulate board status and maintainers
354 information in the self.database dict.
Simon Glass0c477b72022-07-11 19:04:04 -0600355
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600356 defconfig files are used to specify the target, e.g. xxx_defconfig is
357 used for target 'xxx'. If there is no defconfig file mentioned in the
358 MAINTAINERS file F: entries, then this function does nothing.
359
360 The N: name entries can be used to specify a defconfig file using
361 wildcards.
362
Simon Glass58d41be2022-07-11 19:04:05 -0600363 Args:
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600364 srcdir (str): Directory containing source code (Kconfig files)
Simon Glass58d41be2022-07-11 19:04:05 -0600365 fname (str): MAINTAINERS file to be parsed
Simon Glass0c477b72022-07-11 19:04:04 -0600366 """
Simon Glass9b828ec2023-07-19 17:48:19 -0600367 def add_targets(linenum):
368 """Add any new targets
369
370 Args:
371 linenum (int): Current line number
372 """
373 added = False
Simon Glassb555e142023-07-19 17:48:18 -0600374 if targets:
375 for target in targets:
376 self.database[target] = (status, maintainers)
Simon Glass9b828ec2023-07-19 17:48:19 -0600377 added = True
378 if not added and (status != '-' and maintainers):
379 leaf = fname[len(srcdir) + 1:]
380 if leaf != 'MAINTAINERS':
381 self.warnings.append(
382 f'WARNING: orphaned defconfig in {leaf} ending at line {linenum + 1}')
Simon Glassb555e142023-07-19 17:48:18 -0600383
Simon Glass0c477b72022-07-11 19:04:04 -0600384 targets = []
385 maintainers = []
386 status = '-'
Simon Glass58d41be2022-07-11 19:04:05 -0600387 with open(fname, encoding="utf-8") as inf:
Simon Glass9b828ec2023-07-19 17:48:19 -0600388 for linenum, line in enumerate(inf):
Simon Glass58d41be2022-07-11 19:04:05 -0600389 # Check also commented maintainers
390 if line[:3] == '#M:':
391 line = line[1:]
392 tag, rest = line[:2], line[2:].strip()
393 if tag == 'M:':
394 maintainers.append(rest)
395 elif tag == 'F:':
396 # expand wildcard and filter by 'configs/*_defconfig'
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600397 glob_path = os.path.join(srcdir, rest)
398 for item in glob.glob(glob_path):
Simon Glass58d41be2022-07-11 19:04:05 -0600399 front, match, rear = item.partition('configs/')
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600400 if front.endswith('/'):
401 front = front[:-1]
402 if front == srcdir and match:
Simon Glass58d41be2022-07-11 19:04:05 -0600403 front, match, rear = rear.rpartition('_defconfig')
404 if match and not rear:
405 targets.append(front)
406 elif tag == 'S:':
407 status = rest
Simon Glass3a90a692022-10-11 08:15:37 -0600408 elif tag == 'N:':
409 # Just scan the configs directory since that's all we care
410 # about
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600411 walk_path = os.walk(os.path.join(srcdir, 'configs'))
412 for dirpath, _, fnames in walk_path:
413 for cfg in fnames:
414 path = os.path.join(dirpath, cfg)
Simon Glass3a90a692022-10-11 08:15:37 -0600415 front, match, rear = path.partition('configs/')
416 if not front and match:
417 front, match, rear = rear.rpartition('_defconfig')
418 if match and not rear:
419 targets.append(front)
Simon Glass58d41be2022-07-11 19:04:05 -0600420 elif line == '\n':
Simon Glass9b828ec2023-07-19 17:48:19 -0600421 add_targets(linenum)
Simon Glass58d41be2022-07-11 19:04:05 -0600422 targets = []
423 maintainers = []
424 status = '-'
Simon Glass9b828ec2023-07-19 17:48:19 -0600425 add_targets(linenum)
Simon Glass0c477b72022-07-11 19:04:04 -0600426
Simon Glass20751d62022-07-11 19:04:03 -0600427
428class Boards:
429 """Manage a list of boards."""
430 def __init__(self):
Simon Glass20751d62022-07-11 19:04:03 -0600431 self._boards = []
432
433 def add_board(self, brd):
434 """Add a new board to the list.
435
436 The board's target member must not already exist in the board list.
437
438 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600439 brd (Board): board to add
Simon Glass20751d62022-07-11 19:04:03 -0600440 """
441 self._boards.append(brd)
442
443 def read_boards(self, fname):
444 """Read a list of boards from a board file.
445
446 Create a Board object for each and add it to our _boards list.
447
448 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600449 fname (str): Filename of boards.cfg file
Simon Glass20751d62022-07-11 19:04:03 -0600450 """
451 with open(fname, 'r', encoding='utf-8') as inf:
452 for line in inf:
453 if line[0] == '#':
454 continue
455 fields = line.split()
456 if not fields:
457 continue
458 for upto, field in enumerate(fields):
459 if field == '-':
460 fields[upto] = ''
461 while len(fields) < 8:
462 fields.append('')
463 if len(fields) > 8:
464 fields = fields[:8]
465
466 brd = board.Board(*fields)
467 self.add_board(brd)
468
469
470 def get_list(self):
471 """Return a list of available boards.
472
473 Returns:
474 List of Board objects
475 """
476 return self._boards
477
478 def get_dict(self):
479 """Build a dictionary containing all the boards.
480
481 Returns:
482 Dictionary:
483 key is board.target
484 value is board
485 """
486 board_dict = OrderedDict()
487 for brd in self._boards:
488 board_dict[brd.target] = brd
489 return board_dict
490
491 def get_selected_dict(self):
492 """Return a dictionary containing the selected boards
493
494 Returns:
495 List of Board objects that are marked selected
496 """
497 board_dict = OrderedDict()
498 for brd in self._boards:
499 if brd.build_it:
500 board_dict[brd.target] = brd
501 return board_dict
502
503 def get_selected(self):
504 """Return a list of selected boards
505
506 Returns:
507 List of Board objects that are marked selected
508 """
509 return [brd for brd in self._boards if brd.build_it]
510
511 def get_selected_names(self):
512 """Return a list of selected boards
513
514 Returns:
515 List of board names that are marked selected
516 """
517 return [brd.target for brd in self._boards if brd.build_it]
518
519 @classmethod
520 def _build_terms(cls, args):
521 """Convert command line arguments to a list of terms.
522
523 This deals with parsing of the arguments. It handles the '&'
524 operator, which joins several expressions into a single Term.
525
526 For example:
527 ['arm & freescale sandbox', 'tegra']
528
529 will produce 3 Terms containing expressions as follows:
530 arm, freescale
531 sandbox
532 tegra
533
534 The first Term has two expressions, both of which must match for
535 a board to be selected.
536
537 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600538 args (list of str): List of command line arguments
539
Simon Glass20751d62022-07-11 19:04:03 -0600540 Returns:
Simon Glass58d41be2022-07-11 19:04:05 -0600541 list of Term: A list of Term objects
Simon Glass20751d62022-07-11 19:04:03 -0600542 """
543 syms = []
544 for arg in args:
545 for word in arg.split():
546 sym_build = []
547 for term in word.split('&'):
548 if term:
549 sym_build.append(term)
550 sym_build.append('&')
551 syms += sym_build[:-1]
552 terms = []
553 term = None
554 oper = None
555 for sym in syms:
556 if sym == '&':
557 oper = sym
558 elif oper:
559 term.add_expr(sym)
560 oper = None
561 else:
562 if term:
563 terms.append(term)
564 term = Term()
565 term.add_expr(sym)
566 if term:
567 terms.append(term)
568 return terms
569
570 def select_boards(self, args, exclude=None, brds=None):
571 """Mark boards selected based on args
572
573 Normally either boards (an explicit list of boards) or args (a list of
574 terms to match against) is used. It is possible to specify both, in
575 which case they are additive.
576
577 If brds and args are both empty, all boards are selected.
578
579 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600580 args (list of str): List of strings specifying boards to include,
581 either named, or by their target, architecture, cpu, vendor or
582 soc. If empty, all boards are selected.
583 exclude (list of str): List of boards to exclude, regardless of
584 'args', or None for none
585 brds (list of Board): List of boards to build, or None/[] for all
Simon Glass20751d62022-07-11 19:04:03 -0600586
587 Returns:
588 Tuple
589 Dictionary which holds the list of boards which were selected
590 due to each argument, arranged by argument.
591 List of errors found
592 """
Simon Glass58d41be2022-07-11 19:04:05 -0600593 def _check_board(brd):
594 """Check whether to include or exclude a board
Simon Glass20751d62022-07-11 19:04:03 -0600595
Simon Glass58d41be2022-07-11 19:04:05 -0600596 Checks the various terms and decide whether to build it or not (the
597 'build_it' variable).
Simon Glass20751d62022-07-11 19:04:03 -0600598
Simon Glass58d41be2022-07-11 19:04:05 -0600599 If it is built, add the board to the result[term] list so we know
600 which term caused it to be built. Add it to result['all'] also.
Simon Glass20751d62022-07-11 19:04:03 -0600601
Simon Glass58d41be2022-07-11 19:04:05 -0600602 Keep a list of boards we found in 'found', so we can report boards
603 which appear in self._boards but not in brds.
604
605 Args:
606 brd (Board): Board to check
607 """
Simon Glass20751d62022-07-11 19:04:03 -0600608 matching_term = None
609 build_it = False
610 if terms:
611 for term in terms:
612 if term.matches(brd.props):
613 matching_term = str(term)
614 build_it = True
615 break
616 elif brds:
617 if brd.target in brds:
618 build_it = True
619 found.append(brd.target)
620 else:
621 build_it = True
622
623 # Check that it is not specifically excluded
624 for expr in exclude_list:
625 if expr.matches(brd.props):
626 build_it = False
627 break
628
629 if build_it:
630 brd.build_it = True
631 if matching_term:
632 result[matching_term].append(brd.target)
633 result['all'].append(brd.target)
634
Simon Glass58d41be2022-07-11 19:04:05 -0600635 result = OrderedDict()
636 warnings = []
637 terms = self._build_terms(args)
638
639 result['all'] = []
640 for term in terms:
641 result[str(term)] = []
642
643 exclude_list = []
644 if exclude:
645 for expr in exclude:
646 exclude_list.append(Expr(expr))
647
648 found = []
649 for brd in self._boards:
650 _check_board(brd)
651
Simon Glass20751d62022-07-11 19:04:03 -0600652 if brds:
653 remaining = set(brds) - set(found)
654 if remaining:
655 warnings.append(f"Boards not found: {', '.join(remaining)}\n")
656
657 return result, warnings
Simon Glass0c477b72022-07-11 19:04:04 -0600658
Simon Glass58d41be2022-07-11 19:04:05 -0600659 @classmethod
Simon Glasse1568362023-07-19 17:48:14 -0600660 def scan_defconfigs_for_multiprocess(cls, srcdir, queue, defconfigs):
Simon Glass0c477b72022-07-11 19:04:04 -0600661 """Scan defconfig files and queue their board parameters
662
Simon Glass58d41be2022-07-11 19:04:05 -0600663 This function is intended to be passed to multiprocessing.Process()
664 constructor.
Simon Glass0c477b72022-07-11 19:04:04 -0600665
Simon Glass58d41be2022-07-11 19:04:05 -0600666 Args:
Simon Glasse1568362023-07-19 17:48:14 -0600667 srcdir (str): Directory containing source code
Simon Glass58d41be2022-07-11 19:04:05 -0600668 queue (multiprocessing.Queue): The resulting board parameters are
669 written into this.
670 defconfigs (sequence of str): A sequence of defconfig files to be
671 scanned.
Simon Glass0c477b72022-07-11 19:04:04 -0600672 """
Simon Glasse1568362023-07-19 17:48:14 -0600673 kconf_scanner = KconfigScanner(srcdir)
Simon Glass0c477b72022-07-11 19:04:04 -0600674 for defconfig in defconfigs:
675 queue.put(kconf_scanner.scan(defconfig))
676
Simon Glass58d41be2022-07-11 19:04:05 -0600677 @classmethod
Simon Glass07a95d82023-07-19 17:48:21 -0600678 def read_queues(cls, queues, params_list, warnings):
679 """Read the queues and append the data to the paramers list
680
681 Args:
682 queues (list of multiprocessing.Queue): Queues to read
683 params_list (list of dict): List to add params too
684 warnings (set of str): Set to add warnings to
685 """
Simon Glass58d41be2022-07-11 19:04:05 -0600686 for que in queues:
687 while not que.empty():
Simon Glass07a95d82023-07-19 17:48:21 -0600688 params, warn = que.get()
689 params_list.append(params)
690 warnings.update(warn)
Simon Glass0c477b72022-07-11 19:04:04 -0600691
Simon Glasse1568362023-07-19 17:48:14 -0600692 def scan_defconfigs(self, config_dir, srcdir, jobs=1):
Simon Glass0c477b72022-07-11 19:04:04 -0600693 """Collect board parameters for all defconfig files.
694
695 This function invokes multiple processes for faster processing.
696
Simon Glass58d41be2022-07-11 19:04:05 -0600697 Args:
Simon Glasse1568362023-07-19 17:48:14 -0600698 config_dir (str): Directory containing the defconfig files
699 srcdir (str): Directory containing source code (Kconfig files)
Simon Glass58d41be2022-07-11 19:04:05 -0600700 jobs (int): The number of jobs to run simultaneously
Simon Glasse1568362023-07-19 17:48:14 -0600701
702 Returns:
Simon Glass07a95d82023-07-19 17:48:21 -0600703 tuple:
704 list of dict: List of board parameters, each a dict:
705 key: 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
706 'config'
707 value: string value of the key
708 list of str: List of warnings recorded
Simon Glass0c477b72022-07-11 19:04:04 -0600709 """
710 all_defconfigs = []
Simon Glasse1568362023-07-19 17:48:14 -0600711 for (dirpath, _, filenames) in os.walk(config_dir):
Simon Glass0c477b72022-07-11 19:04:04 -0600712 for filename in fnmatch.filter(filenames, '*_defconfig'):
713 if fnmatch.fnmatch(filename, '.*'):
714 continue
715 all_defconfigs.append(os.path.join(dirpath, filename))
716
717 total_boards = len(all_defconfigs)
718 processes = []
719 queues = []
720 for i in range(jobs):
721 defconfigs = all_defconfigs[total_boards * i // jobs :
722 total_boards * (i + 1) // jobs]
Simon Glass58d41be2022-07-11 19:04:05 -0600723 que = multiprocessing.Queue(maxsize=-1)
724 proc = multiprocessing.Process(
Simon Glass0c477b72022-07-11 19:04:04 -0600725 target=self.scan_defconfigs_for_multiprocess,
Simon Glasse1568362023-07-19 17:48:14 -0600726 args=(srcdir, que, defconfigs))
Simon Glass58d41be2022-07-11 19:04:05 -0600727 proc.start()
728 processes.append(proc)
729 queues.append(que)
Simon Glass0c477b72022-07-11 19:04:04 -0600730
Simon Glass07a95d82023-07-19 17:48:21 -0600731 # The resulting data should be accumulated to these lists
Simon Glass0c477b72022-07-11 19:04:04 -0600732 params_list = []
Simon Glass07a95d82023-07-19 17:48:21 -0600733 warnings = set()
Simon Glass0c477b72022-07-11 19:04:04 -0600734
735 # Data in the queues should be retrieved preriodically.
736 # Otherwise, the queues would become full and subprocesses would get stuck.
Simon Glass58d41be2022-07-11 19:04:05 -0600737 while any(p.is_alive() for p in processes):
Simon Glass07a95d82023-07-19 17:48:21 -0600738 self.read_queues(queues, params_list, warnings)
Simon Glass0c477b72022-07-11 19:04:04 -0600739 # sleep for a while until the queues are filled
740 time.sleep(SLEEP_TIME)
741
742 # Joining subprocesses just in case
743 # (All subprocesses should already have been finished)
Simon Glass58d41be2022-07-11 19:04:05 -0600744 for proc in processes:
745 proc.join()
Simon Glass0c477b72022-07-11 19:04:04 -0600746
747 # retrieve leftover data
Simon Glass07a95d82023-07-19 17:48:21 -0600748 self.read_queues(queues, params_list, warnings)
Simon Glass0c477b72022-07-11 19:04:04 -0600749
Simon Glass07a95d82023-07-19 17:48:21 -0600750 return params_list, sorted(list(warnings))
Simon Glass0c477b72022-07-11 19:04:04 -0600751
Simon Glass58d41be2022-07-11 19:04:05 -0600752 @classmethod
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600753 def insert_maintainers_info(cls, srcdir, params_list):
Simon Glass0c477b72022-07-11 19:04:04 -0600754 """Add Status and Maintainers information to the board parameters list.
755
Simon Glass58d41be2022-07-11 19:04:05 -0600756 Args:
757 params_list (list of dict): A list of the board parameters
Simon Glassafbd2132022-07-11 19:04:08 -0600758
759 Returns:
760 list of str: List of warnings collected due to missing status, etc.
Simon Glass0c477b72022-07-11 19:04:04 -0600761 """
762 database = MaintainersDatabase()
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600763 for (dirpath, _, filenames) in os.walk(srcdir):
Simon Glass0c477b72022-07-11 19:04:04 -0600764 if 'MAINTAINERS' in filenames:
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600765 database.parse_file(srcdir,
766 os.path.join(dirpath, 'MAINTAINERS'))
Simon Glass0c477b72022-07-11 19:04:04 -0600767
768 for i, params in enumerate(params_list):
769 target = params['target']
770 params['status'] = database.get_status(target)
771 params['maintainers'] = database.get_maintainers(target)
772 params_list[i] = params
Simon Glassafbd2132022-07-11 19:04:08 -0600773 return database.warnings
Simon Glass0c477b72022-07-11 19:04:04 -0600774
Simon Glass58d41be2022-07-11 19:04:05 -0600775 @classmethod
776 def format_and_output(cls, params_list, output):
Simon Glass0c477b72022-07-11 19:04:04 -0600777 """Write board parameters into a file.
778
779 Columnate the board parameters, sort lines alphabetically,
780 and then write them to a file.
781
Simon Glass58d41be2022-07-11 19:04:05 -0600782 Args:
783 params_list (list of dict): The list of board parameters
784 output (str): The path to the output file
Simon Glass0c477b72022-07-11 19:04:04 -0600785 """
Simon Glass58d41be2022-07-11 19:04:05 -0600786 fields = ('status', 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
Simon Glassd2d4c602022-07-11 19:04:06 -0600787 'config', 'maintainers')
Simon Glass0c477b72022-07-11 19:04:04 -0600788
789 # First, decide the width of each column
Simon Glass58d41be2022-07-11 19:04:05 -0600790 max_length = {f: 0 for f in fields}
Simon Glass0c477b72022-07-11 19:04:04 -0600791 for params in params_list:
Simon Glass58d41be2022-07-11 19:04:05 -0600792 for field in fields:
793 max_length[field] = max(max_length[field], len(params[field]))
Simon Glass0c477b72022-07-11 19:04:04 -0600794
795 output_lines = []
796 for params in params_list:
797 line = ''
Simon Glass58d41be2022-07-11 19:04:05 -0600798 for field in fields:
Simon Glass0c477b72022-07-11 19:04:04 -0600799 # insert two spaces between fields like column -t would
Simon Glass58d41be2022-07-11 19:04:05 -0600800 line += ' ' + params[field].ljust(max_length[field])
Simon Glass0c477b72022-07-11 19:04:04 -0600801 output_lines.append(line.strip())
802
803 # ignore case when sorting
804 output_lines.sort(key=str.lower)
805
Simon Glass58d41be2022-07-11 19:04:05 -0600806 with open(output, 'w', encoding="utf-8") as outf:
807 outf.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n')
Simon Glass0c477b72022-07-11 19:04:04 -0600808
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600809 def build_board_list(self, config_dir, srcdir, jobs=1):
810 """Generate a board-database file
811
812 This works by reading the Kconfig, then loading each board's defconfig
813 in to get the setting for each option. In particular, CONFIG_TARGET_xxx
814 is typically set by the defconfig, where xxx is the target to build.
815
816 Args:
817 config_dir (str): Directory containing the defconfig files
818 srcdir (str): Directory containing source code (Kconfig files)
819 jobs (int): The number of jobs to run simultaneously
820
821 Returns:
822 tuple:
823 list of dict: List of board parameters, each a dict:
824 key: 'arch', 'cpu', 'soc', 'vendor', 'board', 'config',
825 'target'
826 value: string value of the key
827 list of str: Warnings that came up
828 """
Simon Glass07a95d82023-07-19 17:48:21 -0600829 params_list, warnings = self.scan_defconfigs(config_dir, srcdir, jobs)
830 m_warnings = self.insert_maintainers_info(srcdir, params_list)
831 return params_list, warnings + m_warnings
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600832
Simon Glass0c477b72022-07-11 19:04:04 -0600833 def ensure_board_list(self, output, jobs=1, force=False, quiet=False):
834 """Generate a board database file if needed.
835
Simon Glasse1568362023-07-19 17:48:14 -0600836 This is intended to check if Kconfig has changed since the boards.cfg
837 files was generated.
838
Simon Glass58d41be2022-07-11 19:04:05 -0600839 Args:
840 output (str): The name of the output file
841 jobs (int): The number of jobs to run simultaneously
842 force (bool): Force to generate the output even if it is new
843 quiet (bool): True to avoid printing a message if nothing needs doing
Simon Glassafbd2132022-07-11 19:04:08 -0600844
845 Returns:
846 bool: True if all is well, False if there were warnings
Simon Glass0c477b72022-07-11 19:04:04 -0600847 """
Simon Glasse1568362023-07-19 17:48:14 -0600848 if not force and output_is_new(output, CONFIG_DIR, '.'):
Simon Glass0c477b72022-07-11 19:04:04 -0600849 if not quiet:
Simon Glass58d41be2022-07-11 19:04:05 -0600850 print(f'{output} is up to date. Nothing to do.')
Simon Glassafbd2132022-07-11 19:04:08 -0600851 return True
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600852 params_list, warnings = self.build_board_list(CONFIG_DIR, '.', jobs)
Simon Glassafbd2132022-07-11 19:04:08 -0600853 for warn in warnings:
854 print(warn, file=sys.stderr)
Simon Glass0c477b72022-07-11 19:04:04 -0600855 self.format_and_output(params_list, output)
Simon Glassafbd2132022-07-11 19:04:08 -0600856 return not warnings