blob: 422f3476b01173adb42a2294c3bf281e9299f503 [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:
Simon Glass060ee972023-07-19 17:48:23 -0600414 path = os.path.join(dirpath, cfg)[len(srcdir) + 1:]
Simon Glass3a90a692022-10-11 08:15:37 -0600415 front, match, rear = path.partition('configs/')
Simon Glass060ee972023-07-19 17:48:23 -0600416 if front or not match:
417 continue
418 front, match, rear = rear.rpartition('_defconfig')
419
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)
Simon Glass58d41be2022-07-11 19:04:05 -0600425 elif line == '\n':
Simon Glass9b828ec2023-07-19 17:48:19 -0600426 add_targets(linenum)
Simon Glass58d41be2022-07-11 19:04:05 -0600427 targets = []
428 maintainers = []
429 status = '-'
Simon Glass9b828ec2023-07-19 17:48:19 -0600430 add_targets(linenum)
Simon Glass0c477b72022-07-11 19:04:04 -0600431
Simon Glass20751d62022-07-11 19:04:03 -0600432
433class Boards:
434 """Manage a list of boards."""
435 def __init__(self):
Simon Glass20751d62022-07-11 19:04:03 -0600436 self._boards = []
437
438 def add_board(self, brd):
439 """Add a new board to the list.
440
441 The board's target member must not already exist in the board list.
442
443 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600444 brd (Board): board to add
Simon Glass20751d62022-07-11 19:04:03 -0600445 """
446 self._boards.append(brd)
447
448 def read_boards(self, fname):
449 """Read a list of boards from a board file.
450
451 Create a Board object for each and add it to our _boards list.
452
453 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600454 fname (str): Filename of boards.cfg file
Simon Glass20751d62022-07-11 19:04:03 -0600455 """
456 with open(fname, 'r', encoding='utf-8') as inf:
457 for line in inf:
458 if line[0] == '#':
459 continue
460 fields = line.split()
461 if not fields:
462 continue
463 for upto, field in enumerate(fields):
464 if field == '-':
465 fields[upto] = ''
466 while len(fields) < 8:
467 fields.append('')
468 if len(fields) > 8:
469 fields = fields[:8]
470
471 brd = board.Board(*fields)
472 self.add_board(brd)
473
474
475 def get_list(self):
476 """Return a list of available boards.
477
478 Returns:
479 List of Board objects
480 """
481 return self._boards
482
483 def get_dict(self):
484 """Build a dictionary containing all the boards.
485
486 Returns:
487 Dictionary:
488 key is board.target
489 value is board
490 """
491 board_dict = OrderedDict()
492 for brd in self._boards:
493 board_dict[brd.target] = brd
494 return board_dict
495
496 def get_selected_dict(self):
497 """Return a dictionary containing the selected boards
498
499 Returns:
500 List of Board objects that are marked selected
501 """
502 board_dict = OrderedDict()
503 for brd in self._boards:
504 if brd.build_it:
505 board_dict[brd.target] = brd
506 return board_dict
507
508 def get_selected(self):
509 """Return a list of selected boards
510
511 Returns:
512 List of Board objects that are marked selected
513 """
514 return [brd for brd in self._boards if brd.build_it]
515
516 def get_selected_names(self):
517 """Return a list of selected boards
518
519 Returns:
520 List of board names that are marked selected
521 """
522 return [brd.target for brd in self._boards if brd.build_it]
523
524 @classmethod
525 def _build_terms(cls, args):
526 """Convert command line arguments to a list of terms.
527
528 This deals with parsing of the arguments. It handles the '&'
529 operator, which joins several expressions into a single Term.
530
531 For example:
532 ['arm & freescale sandbox', 'tegra']
533
534 will produce 3 Terms containing expressions as follows:
535 arm, freescale
536 sandbox
537 tegra
538
539 The first Term has two expressions, both of which must match for
540 a board to be selected.
541
542 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600543 args (list of str): List of command line arguments
544
Simon Glass20751d62022-07-11 19:04:03 -0600545 Returns:
Simon Glass58d41be2022-07-11 19:04:05 -0600546 list of Term: A list of Term objects
Simon Glass20751d62022-07-11 19:04:03 -0600547 """
548 syms = []
549 for arg in args:
550 for word in arg.split():
551 sym_build = []
552 for term in word.split('&'):
553 if term:
554 sym_build.append(term)
555 sym_build.append('&')
556 syms += sym_build[:-1]
557 terms = []
558 term = None
559 oper = None
560 for sym in syms:
561 if sym == '&':
562 oper = sym
563 elif oper:
564 term.add_expr(sym)
565 oper = None
566 else:
567 if term:
568 terms.append(term)
569 term = Term()
570 term.add_expr(sym)
571 if term:
572 terms.append(term)
573 return terms
574
575 def select_boards(self, args, exclude=None, brds=None):
576 """Mark boards selected based on args
577
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.
581
582 If brds and args are both empty, all boards are selected.
583
584 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600585 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
Simon Glass20751d62022-07-11 19:04:03 -0600591
592 Returns:
593 Tuple
594 Dictionary which holds the list of boards which were selected
595 due to each argument, arranged by argument.
596 List of errors found
597 """
Simon Glass58d41be2022-07-11 19:04:05 -0600598 def _check_board(brd):
599 """Check whether to include or exclude a board
Simon Glass20751d62022-07-11 19:04:03 -0600600
Simon Glass58d41be2022-07-11 19:04:05 -0600601 Checks the various terms and decide whether to build it or not (the
602 'build_it' variable).
Simon Glass20751d62022-07-11 19:04:03 -0600603
Simon Glass58d41be2022-07-11 19:04:05 -0600604 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.
Simon Glass20751d62022-07-11 19:04:03 -0600606
Simon Glass58d41be2022-07-11 19:04:05 -0600607 Keep a list of boards we found in 'found', so we can report boards
608 which appear in self._boards but not in brds.
609
610 Args:
611 brd (Board): Board to check
612 """
Simon Glass20751d62022-07-11 19:04:03 -0600613 matching_term = None
614 build_it = False
615 if terms:
616 for term in terms:
617 if term.matches(brd.props):
618 matching_term = str(term)
619 build_it = True
620 break
621 elif brds:
622 if brd.target in brds:
623 build_it = True
624 found.append(brd.target)
625 else:
626 build_it = True
627
628 # Check that it is not specifically excluded
629 for expr in exclude_list:
630 if expr.matches(brd.props):
631 build_it = False
632 break
633
634 if build_it:
635 brd.build_it = True
636 if matching_term:
637 result[matching_term].append(brd.target)
638 result['all'].append(brd.target)
639
Simon Glass58d41be2022-07-11 19:04:05 -0600640 result = OrderedDict()
641 warnings = []
642 terms = self._build_terms(args)
643
644 result['all'] = []
645 for term in terms:
646 result[str(term)] = []
647
648 exclude_list = []
649 if exclude:
650 for expr in exclude:
651 exclude_list.append(Expr(expr))
652
653 found = []
654 for brd in self._boards:
655 _check_board(brd)
656
Simon Glass20751d62022-07-11 19:04:03 -0600657 if brds:
658 remaining = set(brds) - set(found)
659 if remaining:
660 warnings.append(f"Boards not found: {', '.join(remaining)}\n")
661
662 return result, warnings
Simon Glass0c477b72022-07-11 19:04:04 -0600663
Simon Glass58d41be2022-07-11 19:04:05 -0600664 @classmethod
Simon Glasse1568362023-07-19 17:48:14 -0600665 def scan_defconfigs_for_multiprocess(cls, srcdir, queue, defconfigs):
Simon Glass0c477b72022-07-11 19:04:04 -0600666 """Scan defconfig files and queue their board parameters
667
Simon Glass58d41be2022-07-11 19:04:05 -0600668 This function is intended to be passed to multiprocessing.Process()
669 constructor.
Simon Glass0c477b72022-07-11 19:04:04 -0600670
Simon Glass58d41be2022-07-11 19:04:05 -0600671 Args:
Simon Glasse1568362023-07-19 17:48:14 -0600672 srcdir (str): Directory containing source code
Simon Glass58d41be2022-07-11 19:04:05 -0600673 queue (multiprocessing.Queue): The resulting board parameters are
674 written into this.
675 defconfigs (sequence of str): A sequence of defconfig files to be
676 scanned.
Simon Glass0c477b72022-07-11 19:04:04 -0600677 """
Simon Glasse1568362023-07-19 17:48:14 -0600678 kconf_scanner = KconfigScanner(srcdir)
Simon Glass0c477b72022-07-11 19:04:04 -0600679 for defconfig in defconfigs:
680 queue.put(kconf_scanner.scan(defconfig))
681
Simon Glass58d41be2022-07-11 19:04:05 -0600682 @classmethod
Simon Glass07a95d82023-07-19 17:48:21 -0600683 def read_queues(cls, queues, params_list, warnings):
684 """Read the queues and append the data to the paramers list
685
686 Args:
687 queues (list of multiprocessing.Queue): Queues to read
688 params_list (list of dict): List to add params too
689 warnings (set of str): Set to add warnings to
690 """
Simon Glass58d41be2022-07-11 19:04:05 -0600691 for que in queues:
692 while not que.empty():
Simon Glass07a95d82023-07-19 17:48:21 -0600693 params, warn = que.get()
694 params_list.append(params)
695 warnings.update(warn)
Simon Glass0c477b72022-07-11 19:04:04 -0600696
Simon Glasse1568362023-07-19 17:48:14 -0600697 def scan_defconfigs(self, config_dir, srcdir, jobs=1):
Simon Glass0c477b72022-07-11 19:04:04 -0600698 """Collect board parameters for all defconfig files.
699
700 This function invokes multiple processes for faster processing.
701
Simon Glass58d41be2022-07-11 19:04:05 -0600702 Args:
Simon Glasse1568362023-07-19 17:48:14 -0600703 config_dir (str): Directory containing the defconfig files
704 srcdir (str): Directory containing source code (Kconfig files)
Simon Glass58d41be2022-07-11 19:04:05 -0600705 jobs (int): The number of jobs to run simultaneously
Simon Glasse1568362023-07-19 17:48:14 -0600706
707 Returns:
Simon Glass07a95d82023-07-19 17:48:21 -0600708 tuple:
709 list of dict: List of board parameters, each a dict:
710 key: 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
711 'config'
712 value: string value of the key
713 list of str: List of warnings recorded
Simon Glass0c477b72022-07-11 19:04:04 -0600714 """
715 all_defconfigs = []
Simon Glasse1568362023-07-19 17:48:14 -0600716 for (dirpath, _, filenames) in os.walk(config_dir):
Simon Glass0c477b72022-07-11 19:04:04 -0600717 for filename in fnmatch.filter(filenames, '*_defconfig'):
718 if fnmatch.fnmatch(filename, '.*'):
719 continue
720 all_defconfigs.append(os.path.join(dirpath, filename))
721
722 total_boards = len(all_defconfigs)
723 processes = []
724 queues = []
725 for i in range(jobs):
726 defconfigs = all_defconfigs[total_boards * i // jobs :
727 total_boards * (i + 1) // jobs]
Simon Glass58d41be2022-07-11 19:04:05 -0600728 que = multiprocessing.Queue(maxsize=-1)
729 proc = multiprocessing.Process(
Simon Glass0c477b72022-07-11 19:04:04 -0600730 target=self.scan_defconfigs_for_multiprocess,
Simon Glasse1568362023-07-19 17:48:14 -0600731 args=(srcdir, que, defconfigs))
Simon Glass58d41be2022-07-11 19:04:05 -0600732 proc.start()
733 processes.append(proc)
734 queues.append(que)
Simon Glass0c477b72022-07-11 19:04:04 -0600735
Simon Glass07a95d82023-07-19 17:48:21 -0600736 # The resulting data should be accumulated to these lists
Simon Glass0c477b72022-07-11 19:04:04 -0600737 params_list = []
Simon Glass07a95d82023-07-19 17:48:21 -0600738 warnings = set()
Simon Glass0c477b72022-07-11 19:04:04 -0600739
740 # Data in the queues should be retrieved preriodically.
741 # Otherwise, the queues would become full and subprocesses would get stuck.
Simon Glass58d41be2022-07-11 19:04:05 -0600742 while any(p.is_alive() for p in processes):
Simon Glass07a95d82023-07-19 17:48:21 -0600743 self.read_queues(queues, params_list, warnings)
Simon Glass0c477b72022-07-11 19:04:04 -0600744 # sleep for a while until the queues are filled
745 time.sleep(SLEEP_TIME)
746
747 # Joining subprocesses just in case
748 # (All subprocesses should already have been finished)
Simon Glass58d41be2022-07-11 19:04:05 -0600749 for proc in processes:
750 proc.join()
Simon Glass0c477b72022-07-11 19:04:04 -0600751
752 # retrieve leftover data
Simon Glass07a95d82023-07-19 17:48:21 -0600753 self.read_queues(queues, params_list, warnings)
Simon Glass0c477b72022-07-11 19:04:04 -0600754
Simon Glass07a95d82023-07-19 17:48:21 -0600755 return params_list, sorted(list(warnings))
Simon Glass0c477b72022-07-11 19:04:04 -0600756
Simon Glass58d41be2022-07-11 19:04:05 -0600757 @classmethod
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600758 def insert_maintainers_info(cls, srcdir, params_list):
Simon Glass0c477b72022-07-11 19:04:04 -0600759 """Add Status and Maintainers information to the board parameters list.
760
Simon Glass58d41be2022-07-11 19:04:05 -0600761 Args:
762 params_list (list of dict): A list of the board parameters
Simon Glassafbd2132022-07-11 19:04:08 -0600763
764 Returns:
765 list of str: List of warnings collected due to missing status, etc.
Simon Glass0c477b72022-07-11 19:04:04 -0600766 """
767 database = MaintainersDatabase()
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600768 for (dirpath, _, filenames) in os.walk(srcdir):
Simon Glass0c477b72022-07-11 19:04:04 -0600769 if 'MAINTAINERS' in filenames:
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600770 database.parse_file(srcdir,
771 os.path.join(dirpath, 'MAINTAINERS'))
Simon Glass0c477b72022-07-11 19:04:04 -0600772
773 for i, params in enumerate(params_list):
774 target = params['target']
Simon Glassee088aa2023-07-19 17:48:24 -0600775 maintainers = database.get_maintainers(target)
776 params['maintainers'] = maintainers
777 if maintainers:
778 params['status'] = database.get_status(target)
779 else:
780 params['status'] = '-'
Simon Glass0c477b72022-07-11 19:04:04 -0600781 params_list[i] = params
Simon Glassafbd2132022-07-11 19:04:08 -0600782 return database.warnings
Simon Glass0c477b72022-07-11 19:04:04 -0600783
Simon Glass58d41be2022-07-11 19:04:05 -0600784 @classmethod
785 def format_and_output(cls, params_list, output):
Simon Glass0c477b72022-07-11 19:04:04 -0600786 """Write board parameters into a file.
787
788 Columnate the board parameters, sort lines alphabetically,
789 and then write them to a file.
790
Simon Glass58d41be2022-07-11 19:04:05 -0600791 Args:
792 params_list (list of dict): The list of board parameters
793 output (str): The path to the output file
Simon Glass0c477b72022-07-11 19:04:04 -0600794 """
Simon Glass58d41be2022-07-11 19:04:05 -0600795 fields = ('status', 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
Simon Glassd2d4c602022-07-11 19:04:06 -0600796 'config', 'maintainers')
Simon Glass0c477b72022-07-11 19:04:04 -0600797
798 # First, decide the width of each column
Simon Glass58d41be2022-07-11 19:04:05 -0600799 max_length = {f: 0 for f in fields}
Simon Glass0c477b72022-07-11 19:04:04 -0600800 for params in params_list:
Simon Glass58d41be2022-07-11 19:04:05 -0600801 for field in fields:
802 max_length[field] = max(max_length[field], len(params[field]))
Simon Glass0c477b72022-07-11 19:04:04 -0600803
804 output_lines = []
805 for params in params_list:
806 line = ''
Simon Glass58d41be2022-07-11 19:04:05 -0600807 for field in fields:
Simon Glass0c477b72022-07-11 19:04:04 -0600808 # insert two spaces between fields like column -t would
Simon Glass58d41be2022-07-11 19:04:05 -0600809 line += ' ' + params[field].ljust(max_length[field])
Simon Glass0c477b72022-07-11 19:04:04 -0600810 output_lines.append(line.strip())
811
812 # ignore case when sorting
813 output_lines.sort(key=str.lower)
814
Simon Glass58d41be2022-07-11 19:04:05 -0600815 with open(output, 'w', encoding="utf-8") as outf:
816 outf.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n')
Simon Glass0c477b72022-07-11 19:04:04 -0600817
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600818 def build_board_list(self, config_dir, srcdir, jobs=1):
819 """Generate a board-database file
820
821 This works by reading the Kconfig, then loading each board's defconfig
822 in to get the setting for each option. In particular, CONFIG_TARGET_xxx
823 is typically set by the defconfig, where xxx is the target to build.
824
825 Args:
826 config_dir (str): Directory containing the defconfig files
827 srcdir (str): Directory containing source code (Kconfig files)
828 jobs (int): The number of jobs to run simultaneously
829
830 Returns:
831 tuple:
832 list of dict: List of board parameters, each a dict:
833 key: 'arch', 'cpu', 'soc', 'vendor', 'board', 'config',
834 'target'
835 value: string value of the key
836 list of str: Warnings that came up
837 """
Simon Glass07a95d82023-07-19 17:48:21 -0600838 params_list, warnings = self.scan_defconfigs(config_dir, srcdir, jobs)
839 m_warnings = self.insert_maintainers_info(srcdir, params_list)
840 return params_list, warnings + m_warnings
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600841
Simon Glass0c477b72022-07-11 19:04:04 -0600842 def ensure_board_list(self, output, jobs=1, force=False, quiet=False):
843 """Generate a board database file if needed.
844
Simon Glasse1568362023-07-19 17:48:14 -0600845 This is intended to check if Kconfig has changed since the boards.cfg
846 files was generated.
847
Simon Glass58d41be2022-07-11 19:04:05 -0600848 Args:
849 output (str): The name of the output file
850 jobs (int): The number of jobs to run simultaneously
851 force (bool): Force to generate the output even if it is new
852 quiet (bool): True to avoid printing a message if nothing needs doing
Simon Glassafbd2132022-07-11 19:04:08 -0600853
854 Returns:
855 bool: True if all is well, False if there were warnings
Simon Glass0c477b72022-07-11 19:04:04 -0600856 """
Simon Glasse1568362023-07-19 17:48:14 -0600857 if not force and output_is_new(output, CONFIG_DIR, '.'):
Simon Glass0c477b72022-07-11 19:04:04 -0600858 if not quiet:
Simon Glass58d41be2022-07-11 19:04:05 -0600859 print(f'{output} is up to date. Nothing to do.')
Simon Glassafbd2132022-07-11 19:04:08 -0600860 return True
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600861 params_list, warnings = self.build_board_list(CONFIG_DIR, '.', jobs)
Simon Glassafbd2132022-07-11 19:04:08 -0600862 for warn in warnings:
863 print(warn, file=sys.stderr)
Simon Glass0c477b72022-07-11 19:04:04 -0600864 self.format_and_output(params_list, output)
Simon Glassafbd2132022-07-11 19:04:08 -0600865 return not warnings