blob: 83adbf167c7d731140b0370f89c3420d8f749c7f [file] [log] [blame]
Simon Glassc52bd222022-07-11 19:04:03 -06001# SPDX-License-Identifier: GPL-2.0+
2# Copyright (c) 2012 The Chromium OS Authors.
Simon Glassa8a01412022-07-11 19:04:04 -06003# Author: Simon Glass <sjg@chromium.org>
4# Author: Masahiro Yamada <yamada.m@jp.panasonic.com>
Simon Glassc52bd222022-07-11 19:04:03 -06005
6"""Maintains a list of boards and allows them to be selected"""
7
8from collections import OrderedDict
Simon Glassa8a01412022-07-11 19:04:04 -06009import errno
10import fnmatch
11import glob
12import multiprocessing
13import os
Simon Glassc52bd222022-07-11 19:04:03 -060014import re
Simon Glassa8a01412022-07-11 19:04:04 -060015import sys
16import tempfile
17import time
Simon Glassc52bd222022-07-11 19:04:03 -060018
19from buildman import board
Simon Glassa8a01412022-07-11 19:04:04 -060020from buildman import kconfiglib
21
22
23### constant variables ###
24OUTPUT_FILE = 'boards.cfg'
25CONFIG_DIR = 'configs'
26SLEEP_TIME = 0.03
Simon Glass969fd332022-07-11 19:04:05 -060027COMMENT_BLOCK = f'''#
Simon Glassa8a01412022-07-11 19:04:04 -060028# List of boards
Simon Glass969fd332022-07-11 19:04:05 -060029# Automatically generated by {__file__}: don't edit
Simon Glassa8a01412022-07-11 19:04:04 -060030#
Simon Glass256126c2022-07-11 19:04:06 -060031# Status, Arch, CPU, SoC, Vendor, Board, Target, Config, Maintainers
Simon Glassa8a01412022-07-11 19:04:04 -060032
Simon Glass969fd332022-07-11 19:04:05 -060033'''
Simon Glassa8a01412022-07-11 19:04:04 -060034
35
Simon Glass969fd332022-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 Glassa8a01412022-07-11 19:04:04 -060045 try:
Simon Glass969fd332022-07-11 19:04:05 -060046 os.remove(fname)
Simon Glassa8a01412022-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 Glassb27e9892023-07-19 17:48:14 -060053def output_is_new(output, config_dir, srcdir):
Simon Glassa8a01412022-07-11 19:04:04 -060054 """Check if the output file is up to date.
55
Simon Glass969fd332022-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 Glassb27e9892023-07-19 17:48:14 -060062 config_dir (str): Directory containing defconfig files
63 srcdir (str): Directory containing Kconfig and MAINTAINERS files
Simon Glass969fd332022-07-11 19:04:05 -060064
Simon Glassa8a01412022-07-11 19:04:04 -060065 Returns:
Simon Glass969fd332022-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 Glassa8a01412022-07-11 19:04:04 -060071 """
Simon Glass969fd332022-07-11 19:04:05 -060072 # pylint: disable=too-many-branches
Simon Glassa8a01412022-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 Glass969fd332022-07-11 19:04:05 -060079 raise
Simon Glassa8a01412022-07-11 19:04:04 -060080
Simon Glassb27e9892023-07-19 17:48:14 -060081 for (dirpath, _, filenames) in os.walk(config_dir):
Simon Glassa8a01412022-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 Glassb27e9892023-07-19 17:48:14 -060089 for (dirpath, _, filenames) in os.walk(srcdir):
Simon Glassa8a01412022-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 Glass969fd332022-07-11 19:04:05 -0600101 with open(output, encoding="utf-8") as inf:
102 for line in inf:
Simon Glass256126c2022-07-11 19:04:06 -0600103 if 'Options,' in line:
104 return False
Simon Glassa8a01412022-07-11 19:04:04 -0600105 if line[0] == '#' or line == '\n':
106 continue
107 defconfig = line.split()[6] + '_defconfig'
Simon Glassb27e9892023-07-19 17:48:14 -0600108 if not os.path.exists(os.path.join(config_dir, defconfig)):
Simon Glassa8a01412022-07-11 19:04:04 -0600109 return False
110
111 return True
Simon Glassc52bd222022-07-11 19:04:03 -0600112
113
114class 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 Glass969fd332022-07-11 19:04:05 -0600121 expr (str): String cotaining regular expression to store
Simon Glassc52bd222022-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 Glass969fd332022-07-11 19:04:05 -0600130 props (list of str): List of properties to check
Simon Glassc52bd222022-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 Glass969fd332022-07-11 19:04:05 -0600156 expr (Expr): New Expr object to add to the list of those that must
Simon Glassc52bd222022-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 Glass969fd332022-07-11 19:04:05 -0600171 props (list of str): List of properties to check
Simon Glassc52bd222022-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
180
Simon Glassa8a01412022-07-11 19:04:04 -0600181class 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 Glass256126c2022-07-11 19:04:06 -0600193 # 'target' is added later
Simon Glassa8a01412022-07-11 19:04:04 -0600194 }
195
Simon Glassb27e9892023-07-19 17:48:14 -0600196 def __init__(self, srctree):
Simon Glassa8a01412022-07-11 19:04:04 -0600197 """Scan all the Kconfig files and create a Kconfig object."""
198 # Define environment variables referenced from Kconfig
Simon Glassb27e9892023-07-19 17:48:14 -0600199 os.environ['srctree'] = srctree
Simon Glassa8a01412022-07-11 19:04:04 -0600200 os.environ['UBOOTVERSION'] = 'dummy'
201 os.environ['KCONFIG_OBJDIR'] = ''
Simon Glass969fd332022-07-11 19:04:05 -0600202 self._tmpfile = None
Simon Glassa8a01412022-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 Glass969fd332022-07-11 19:04:05 -0600213 if self._tmpfile:
Simon Glassa8a01412022-07-11 19:04:04 -0600214 try_remove(self._tmpfile)
215
Simon Glass1b218422023-07-19 17:48:27 -0600216 def scan(self, defconfig, warn_targets):
Simon Glassa8a01412022-07-11 19:04:04 -0600217 """Load a defconfig file to obtain board parameters.
218
Simon Glass969fd332022-07-11 19:04:05 -0600219 Args:
220 defconfig (str): path to the defconfig file to be processed
Simon Glass1b218422023-07-19 17:48:27 -0600221 warn_targets (bool): True to warn about missing or duplicate
222 CONFIG_TARGET options
Simon Glassa8a01412022-07-11 19:04:04 -0600223
224 Returns:
Simon Glassbec06ed2023-07-19 17:48:21 -0600225 tuple: dictionary of board parameters. It has a form of:
226 {
227 'arch': <arch_name>,
228 'cpu': <cpu_name>,
229 'soc': <soc_name>,
230 'vendor': <vendor_name>,
231 'board': <board_name>,
232 'target': <target_name>,
233 'config': <config_header_name>,
234 }
235 warnings (list of str): list of warnings found
Simon Glassa8a01412022-07-11 19:04:04 -0600236 """
Simon Glassf99f34b2023-07-19 17:48:20 -0600237 leaf = os.path.basename(defconfig)
238 expect_target, match, rear = leaf.partition('_defconfig')
239 assert match and not rear, f'{leaf} : invalid defconfig'
240
Simon Glass6a754c62023-07-19 17:48:13 -0600241 self._conf.load_config(defconfig)
Simon Glassa8a01412022-07-11 19:04:04 -0600242 self._tmpfile = None
243
244 params = {}
Simon Glassbec06ed2023-07-19 17:48:21 -0600245 warnings = []
Simon Glassa8a01412022-07-11 19:04:04 -0600246
247 # Get the value of CONFIG_SYS_ARCH, CONFIG_SYS_CPU, ... etc.
248 # Set '-' if the value is empty.
249 for key, symbol in list(self._SYMBOL_TABLE.items()):
250 value = self._conf.syms.get(symbol).str_value
251 if value:
252 params[key] = value
253 else:
254 params[key] = '-'
255
Simon Glassbec06ed2023-07-19 17:48:21 -0600256 # Check there is exactly one TARGET_xxx set
Simon Glass1b218422023-07-19 17:48:27 -0600257 if warn_targets:
258 target = None
259 for name, sym in self._conf.syms.items():
260 if name.startswith('TARGET_') and sym.str_value == 'y':
261 tname = name[7:].lower()
262 if target:
263 warnings.append(
264 f'WARNING: {leaf}: Duplicate TARGET_xxx: {target} and {tname}')
265 else:
266 target = tname
Simon Glassbec06ed2023-07-19 17:48:21 -0600267
Simon Glass1b218422023-07-19 17:48:27 -0600268 if not target:
269 cfg_name = expect_target.replace('-', '_').upper()
270 warnings.append(f'WARNING: {leaf}: No TARGET_{cfg_name} enabled')
Simon Glassad995992023-07-19 17:48:22 -0600271
Simon Glassf99f34b2023-07-19 17:48:20 -0600272 params['target'] = expect_target
Simon Glassa8a01412022-07-11 19:04:04 -0600273
274 # fix-up for aarch64
275 if params['arch'] == 'arm' and params['cpu'] == 'armv8':
276 params['arch'] = 'aarch64'
277
Heinrich Schuchardt3672ed72022-10-03 18:07:53 +0200278 # fix-up for riscv
279 if params['arch'] == 'riscv':
280 try:
281 value = self._conf.syms.get('ARCH_RV32I').str_value
282 except:
283 value = ''
284 if value == 'y':
285 params['arch'] = 'riscv32'
286 else:
287 params['arch'] = 'riscv64'
288
Simon Glassbec06ed2023-07-19 17:48:21 -0600289 return params, warnings
Simon Glassa8a01412022-07-11 19:04:04 -0600290
291
292class MaintainersDatabase:
293
Simon Glass969fd332022-07-11 19:04:05 -0600294 """The database of board status and maintainers.
295
296 Properties:
297 database: dict:
298 key: Board-target name (e.g. 'snow')
299 value: tuple:
300 str: Board status (e.g. 'Active')
301 str: List of maintainers, separated by :
Simon Glassadd76e72022-07-11 19:04:08 -0600302 warnings (list of str): List of warnings due to missing status, etc.
Simon Glass969fd332022-07-11 19:04:05 -0600303 """
Simon Glassa8a01412022-07-11 19:04:04 -0600304
305 def __init__(self):
306 """Create an empty database."""
307 self.database = {}
Simon Glassadd76e72022-07-11 19:04:08 -0600308 self.warnings = []
Simon Glassa8a01412022-07-11 19:04:04 -0600309
310 def get_status(self, target):
311 """Return the status of the given board.
312
313 The board status is generally either 'Active' or 'Orphan'.
314 Display a warning message and return '-' if status information
315 is not found.
316
Simon Glass969fd332022-07-11 19:04:05 -0600317 Args:
318 target (str): Build-target name
319
Simon Glassa8a01412022-07-11 19:04:04 -0600320 Returns:
Simon Glass969fd332022-07-11 19:04:05 -0600321 str: 'Active', 'Orphan' or '-'.
Simon Glassa8a01412022-07-11 19:04:04 -0600322 """
323 if not target in self.database:
Simon Glassadd76e72022-07-11 19:04:08 -0600324 self.warnings.append(f"WARNING: no status info for '{target}'")
Simon Glassa8a01412022-07-11 19:04:04 -0600325 return '-'
326
327 tmp = self.database[target][0]
328 if tmp.startswith('Maintained'):
329 return 'Active'
Simon Glass969fd332022-07-11 19:04:05 -0600330 if tmp.startswith('Supported'):
Simon Glassa8a01412022-07-11 19:04:04 -0600331 return 'Active'
Simon Glass969fd332022-07-11 19:04:05 -0600332 if tmp.startswith('Orphan'):
Simon Glassa8a01412022-07-11 19:04:04 -0600333 return 'Orphan'
Simon Glassadd76e72022-07-11 19:04:08 -0600334 self.warnings.append(f"WARNING: {tmp}: unknown status for '{target}'")
Simon Glass969fd332022-07-11 19:04:05 -0600335 return '-'
Simon Glassa8a01412022-07-11 19:04:04 -0600336
337 def get_maintainers(self, target):
338 """Return the maintainers of the given board.
339
Simon Glass969fd332022-07-11 19:04:05 -0600340 Args:
341 target (str): Build-target name
342
Simon Glassa8a01412022-07-11 19:04:04 -0600343 Returns:
Simon Glass969fd332022-07-11 19:04:05 -0600344 str: Maintainers of the board. If the board has two or more
345 maintainers, they are separated with colons.
Simon Glassa8a01412022-07-11 19:04:04 -0600346 """
Simon Glass9a7cc812023-07-19 17:48:26 -0600347 entry = self.database.get(target)
348 if entry:
349 status, maint_list = entry
350 if not status.startswith('Orphan'):
351 if len(maint_list) > 1 or (maint_list and maint_list[0] != '-'):
352 return ':'.join(maint_list)
Simon Glassa8a01412022-07-11 19:04:04 -0600353
Simon Glass9a7cc812023-07-19 17:48:26 -0600354 self.warnings.append(f"WARNING: no maintainers for '{target}'")
355 return ''
Simon Glassa8a01412022-07-11 19:04:04 -0600356
Simon Glass5df95cf2023-07-19 17:48:17 -0600357 def parse_file(self, srcdir, fname):
Simon Glassa8a01412022-07-11 19:04:04 -0600358 """Parse a MAINTAINERS file.
359
Simon Glass969fd332022-07-11 19:04:05 -0600360 Parse a MAINTAINERS file and accumulate board status and maintainers
361 information in the self.database dict.
Simon Glassa8a01412022-07-11 19:04:04 -0600362
Simon Glass5df95cf2023-07-19 17:48:17 -0600363 defconfig files are used to specify the target, e.g. xxx_defconfig is
364 used for target 'xxx'. If there is no defconfig file mentioned in the
365 MAINTAINERS file F: entries, then this function does nothing.
366
367 The N: name entries can be used to specify a defconfig file using
368 wildcards.
369
Simon Glass969fd332022-07-11 19:04:05 -0600370 Args:
Simon Glass5df95cf2023-07-19 17:48:17 -0600371 srcdir (str): Directory containing source code (Kconfig files)
Simon Glass969fd332022-07-11 19:04:05 -0600372 fname (str): MAINTAINERS file to be parsed
Simon Glassa8a01412022-07-11 19:04:04 -0600373 """
Simon Glassbc12d032023-07-19 17:48:19 -0600374 def add_targets(linenum):
375 """Add any new targets
376
377 Args:
378 linenum (int): Current line number
379 """
380 added = False
Simon Glassa114c612023-07-19 17:48:18 -0600381 if targets:
382 for target in targets:
383 self.database[target] = (status, maintainers)
Simon Glassbc12d032023-07-19 17:48:19 -0600384 added = True
385 if not added and (status != '-' and maintainers):
386 leaf = fname[len(srcdir) + 1:]
387 if leaf != 'MAINTAINERS':
388 self.warnings.append(
389 f'WARNING: orphaned defconfig in {leaf} ending at line {linenum + 1}')
Simon Glassa114c612023-07-19 17:48:18 -0600390
Simon Glassa8a01412022-07-11 19:04:04 -0600391 targets = []
392 maintainers = []
393 status = '-'
Simon Glass969fd332022-07-11 19:04:05 -0600394 with open(fname, encoding="utf-8") as inf:
Simon Glassbc12d032023-07-19 17:48:19 -0600395 for linenum, line in enumerate(inf):
Simon Glass969fd332022-07-11 19:04:05 -0600396 # Check also commented maintainers
397 if line[:3] == '#M:':
398 line = line[1:]
399 tag, rest = line[:2], line[2:].strip()
400 if tag == 'M:':
401 maintainers.append(rest)
402 elif tag == 'F:':
403 # expand wildcard and filter by 'configs/*_defconfig'
Simon Glass5df95cf2023-07-19 17:48:17 -0600404 glob_path = os.path.join(srcdir, rest)
405 for item in glob.glob(glob_path):
Simon Glass969fd332022-07-11 19:04:05 -0600406 front, match, rear = item.partition('configs/')
Simon Glass5df95cf2023-07-19 17:48:17 -0600407 if front.endswith('/'):
408 front = front[:-1]
409 if front == srcdir and match:
Simon Glass969fd332022-07-11 19:04:05 -0600410 front, match, rear = rear.rpartition('_defconfig')
411 if match and not rear:
412 targets.append(front)
413 elif tag == 'S:':
414 status = rest
Simon Glasse8da1da2022-10-11 08:15:37 -0600415 elif tag == 'N:':
416 # Just scan the configs directory since that's all we care
417 # about
Simon Glass5df95cf2023-07-19 17:48:17 -0600418 walk_path = os.walk(os.path.join(srcdir, 'configs'))
419 for dirpath, _, fnames in walk_path:
420 for cfg in fnames:
Simon Glassc6491532023-07-19 17:48:23 -0600421 path = os.path.join(dirpath, cfg)[len(srcdir) + 1:]
Simon Glasse8da1da2022-10-11 08:15:37 -0600422 front, match, rear = path.partition('configs/')
Simon Glassc6491532023-07-19 17:48:23 -0600423 if front or not match:
424 continue
425 front, match, rear = rear.rpartition('_defconfig')
426
427 # Use this entry if it matches the defconfig file
428 # without the _defconfig suffix. For example
429 # 'am335x.*' matches am335x_guardian_defconfig
430 if match and not rear and re.search(rest, front):
431 targets.append(front)
Simon Glass969fd332022-07-11 19:04:05 -0600432 elif line == '\n':
Simon Glassbc12d032023-07-19 17:48:19 -0600433 add_targets(linenum)
Simon Glass969fd332022-07-11 19:04:05 -0600434 targets = []
435 maintainers = []
436 status = '-'
Simon Glassbc12d032023-07-19 17:48:19 -0600437 add_targets(linenum)
Simon Glassa8a01412022-07-11 19:04:04 -0600438
439
Simon Glassc52bd222022-07-11 19:04:03 -0600440class Boards:
441 """Manage a list of boards."""
442 def __init__(self):
Simon Glassc52bd222022-07-11 19:04:03 -0600443 self._boards = []
444
445 def add_board(self, brd):
446 """Add a new board to the list.
447
448 The board's target member must not already exist in the board list.
449
450 Args:
Simon Glass969fd332022-07-11 19:04:05 -0600451 brd (Board): board to add
Simon Glassc52bd222022-07-11 19:04:03 -0600452 """
453 self._boards.append(brd)
454
455 def read_boards(self, fname):
456 """Read a list of boards from a board file.
457
458 Create a Board object for each and add it to our _boards list.
459
460 Args:
Simon Glass969fd332022-07-11 19:04:05 -0600461 fname (str): Filename of boards.cfg file
Simon Glassc52bd222022-07-11 19:04:03 -0600462 """
463 with open(fname, 'r', encoding='utf-8') as inf:
464 for line in inf:
465 if line[0] == '#':
466 continue
467 fields = line.split()
468 if not fields:
469 continue
470 for upto, field in enumerate(fields):
471 if field == '-':
472 fields[upto] = ''
473 while len(fields) < 8:
474 fields.append('')
475 if len(fields) > 8:
476 fields = fields[:8]
477
478 brd = board.Board(*fields)
479 self.add_board(brd)
480
481
482 def get_list(self):
483 """Return a list of available boards.
484
485 Returns:
486 List of Board objects
487 """
488 return self._boards
489
490 def get_dict(self):
491 """Build a dictionary containing all the boards.
492
493 Returns:
494 Dictionary:
495 key is board.target
496 value is board
497 """
498 board_dict = OrderedDict()
499 for brd in self._boards:
500 board_dict[brd.target] = brd
501 return board_dict
502
503 def get_selected_dict(self):
504 """Return a dictionary containing the selected boards
505
506 Returns:
507 List of Board objects that are marked selected
508 """
509 board_dict = OrderedDict()
510 for brd in self._boards:
511 if brd.build_it:
512 board_dict[brd.target] = brd
513 return board_dict
514
515 def get_selected(self):
516 """Return a list of selected boards
517
518 Returns:
519 List of Board objects that are marked selected
520 """
521 return [brd for brd in self._boards if brd.build_it]
522
523 def get_selected_names(self):
524 """Return a list of selected boards
525
526 Returns:
527 List of board names that are marked selected
528 """
529 return [brd.target for brd in self._boards if brd.build_it]
530
531 @classmethod
532 def _build_terms(cls, args):
533 """Convert command line arguments to a list of terms.
534
535 This deals with parsing of the arguments. It handles the '&'
536 operator, which joins several expressions into a single Term.
537
538 For example:
539 ['arm & freescale sandbox', 'tegra']
540
541 will produce 3 Terms containing expressions as follows:
542 arm, freescale
543 sandbox
544 tegra
545
546 The first Term has two expressions, both of which must match for
547 a board to be selected.
548
549 Args:
Simon Glass969fd332022-07-11 19:04:05 -0600550 args (list of str): List of command line arguments
551
Simon Glassc52bd222022-07-11 19:04:03 -0600552 Returns:
Simon Glass969fd332022-07-11 19:04:05 -0600553 list of Term: A list of Term objects
Simon Glassc52bd222022-07-11 19:04:03 -0600554 """
555 syms = []
556 for arg in args:
557 for word in arg.split():
558 sym_build = []
559 for term in word.split('&'):
560 if term:
561 sym_build.append(term)
562 sym_build.append('&')
563 syms += sym_build[:-1]
564 terms = []
565 term = None
566 oper = None
567 for sym in syms:
568 if sym == '&':
569 oper = sym
570 elif oper:
571 term.add_expr(sym)
572 oper = None
573 else:
574 if term:
575 terms.append(term)
576 term = Term()
577 term.add_expr(sym)
578 if term:
579 terms.append(term)
580 return terms
581
582 def select_boards(self, args, exclude=None, brds=None):
583 """Mark boards selected based on args
584
585 Normally either boards (an explicit list of boards) or args (a list of
586 terms to match against) is used. It is possible to specify both, in
587 which case they are additive.
588
589 If brds and args are both empty, all boards are selected.
590
591 Args:
Simon Glass969fd332022-07-11 19:04:05 -0600592 args (list of str): List of strings specifying boards to include,
593 either named, or by their target, architecture, cpu, vendor or
594 soc. If empty, all boards are selected.
595 exclude (list of str): List of boards to exclude, regardless of
596 'args', or None for none
597 brds (list of Board): List of boards to build, or None/[] for all
Simon Glassc52bd222022-07-11 19:04:03 -0600598
599 Returns:
600 Tuple
601 Dictionary which holds the list of boards which were selected
602 due to each argument, arranged by argument.
603 List of errors found
604 """
Simon Glass969fd332022-07-11 19:04:05 -0600605 def _check_board(brd):
606 """Check whether to include or exclude a board
Simon Glassc52bd222022-07-11 19:04:03 -0600607
Simon Glass969fd332022-07-11 19:04:05 -0600608 Checks the various terms and decide whether to build it or not (the
609 'build_it' variable).
Simon Glassc52bd222022-07-11 19:04:03 -0600610
Simon Glass969fd332022-07-11 19:04:05 -0600611 If it is built, add the board to the result[term] list so we know
612 which term caused it to be built. Add it to result['all'] also.
Simon Glassc52bd222022-07-11 19:04:03 -0600613
Simon Glass969fd332022-07-11 19:04:05 -0600614 Keep a list of boards we found in 'found', so we can report boards
615 which appear in self._boards but not in brds.
616
617 Args:
618 brd (Board): Board to check
619 """
Simon Glassc52bd222022-07-11 19:04:03 -0600620 matching_term = None
621 build_it = False
622 if terms:
623 for term in terms:
624 if term.matches(brd.props):
625 matching_term = str(term)
626 build_it = True
627 break
628 elif brds:
629 if brd.target in brds:
630 build_it = True
631 found.append(brd.target)
632 else:
633 build_it = True
634
635 # Check that it is not specifically excluded
636 for expr in exclude_list:
637 if expr.matches(brd.props):
638 build_it = False
639 break
640
641 if build_it:
642 brd.build_it = True
643 if matching_term:
644 result[matching_term].append(brd.target)
645 result['all'].append(brd.target)
646
Simon Glass969fd332022-07-11 19:04:05 -0600647 result = OrderedDict()
648 warnings = []
649 terms = self._build_terms(args)
650
651 result['all'] = []
652 for term in terms:
653 result[str(term)] = []
654
655 exclude_list = []
656 if exclude:
657 for expr in exclude:
658 exclude_list.append(Expr(expr))
659
660 found = []
661 for brd in self._boards:
662 _check_board(brd)
663
Simon Glassc52bd222022-07-11 19:04:03 -0600664 if brds:
665 remaining = set(brds) - set(found)
666 if remaining:
667 warnings.append(f"Boards not found: {', '.join(remaining)}\n")
668
669 return result, warnings
Simon Glassa8a01412022-07-11 19:04:04 -0600670
Simon Glass969fd332022-07-11 19:04:05 -0600671 @classmethod
Simon Glass1b218422023-07-19 17:48:27 -0600672 def scan_defconfigs_for_multiprocess(cls, srcdir, queue, defconfigs,
673 warn_targets):
Simon Glassa8a01412022-07-11 19:04:04 -0600674 """Scan defconfig files and queue their board parameters
675
Simon Glass969fd332022-07-11 19:04:05 -0600676 This function is intended to be passed to multiprocessing.Process()
677 constructor.
Simon Glassa8a01412022-07-11 19:04:04 -0600678
Simon Glass969fd332022-07-11 19:04:05 -0600679 Args:
Simon Glassb27e9892023-07-19 17:48:14 -0600680 srcdir (str): Directory containing source code
Simon Glass969fd332022-07-11 19:04:05 -0600681 queue (multiprocessing.Queue): The resulting board parameters are
682 written into this.
683 defconfigs (sequence of str): A sequence of defconfig files to be
684 scanned.
Simon Glass1b218422023-07-19 17:48:27 -0600685 warn_targets (bool): True to warn about missing or duplicate
686 CONFIG_TARGET options
Simon Glassa8a01412022-07-11 19:04:04 -0600687 """
Simon Glassb27e9892023-07-19 17:48:14 -0600688 kconf_scanner = KconfigScanner(srcdir)
Simon Glassa8a01412022-07-11 19:04:04 -0600689 for defconfig in defconfigs:
Simon Glass1b218422023-07-19 17:48:27 -0600690 queue.put(kconf_scanner.scan(defconfig, warn_targets))
Simon Glassa8a01412022-07-11 19:04:04 -0600691
Simon Glass969fd332022-07-11 19:04:05 -0600692 @classmethod
Simon Glassbec06ed2023-07-19 17:48:21 -0600693 def read_queues(cls, queues, params_list, warnings):
694 """Read the queues and append the data to the paramers list
695
696 Args:
697 queues (list of multiprocessing.Queue): Queues to read
698 params_list (list of dict): List to add params too
699 warnings (set of str): Set to add warnings to
700 """
Simon Glass969fd332022-07-11 19:04:05 -0600701 for que in queues:
702 while not que.empty():
Simon Glassbec06ed2023-07-19 17:48:21 -0600703 params, warn = que.get()
704 params_list.append(params)
705 warnings.update(warn)
Simon Glassa8a01412022-07-11 19:04:04 -0600706
Simon Glass1b218422023-07-19 17:48:27 -0600707 def scan_defconfigs(self, config_dir, srcdir, jobs=1, warn_targets=False):
Simon Glassa8a01412022-07-11 19:04:04 -0600708 """Collect board parameters for all defconfig files.
709
710 This function invokes multiple processes for faster processing.
711
Simon Glass969fd332022-07-11 19:04:05 -0600712 Args:
Simon Glassb27e9892023-07-19 17:48:14 -0600713 config_dir (str): Directory containing the defconfig files
714 srcdir (str): Directory containing source code (Kconfig files)
Simon Glass969fd332022-07-11 19:04:05 -0600715 jobs (int): The number of jobs to run simultaneously
Simon Glass1b218422023-07-19 17:48:27 -0600716 warn_targets (bool): True to warn about missing or duplicate
717 CONFIG_TARGET options
Simon Glassb27e9892023-07-19 17:48:14 -0600718
719 Returns:
Simon Glassbec06ed2023-07-19 17:48:21 -0600720 tuple:
721 list of dict: List of board parameters, each a dict:
722 key: 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
723 'config'
724 value: string value of the key
725 list of str: List of warnings recorded
Simon Glassa8a01412022-07-11 19:04:04 -0600726 """
727 all_defconfigs = []
Simon Glassb27e9892023-07-19 17:48:14 -0600728 for (dirpath, _, filenames) in os.walk(config_dir):
Simon Glassa8a01412022-07-11 19:04:04 -0600729 for filename in fnmatch.filter(filenames, '*_defconfig'):
730 if fnmatch.fnmatch(filename, '.*'):
731 continue
732 all_defconfigs.append(os.path.join(dirpath, filename))
733
734 total_boards = len(all_defconfigs)
735 processes = []
736 queues = []
737 for i in range(jobs):
738 defconfigs = all_defconfigs[total_boards * i // jobs :
739 total_boards * (i + 1) // jobs]
Simon Glass969fd332022-07-11 19:04:05 -0600740 que = multiprocessing.Queue(maxsize=-1)
741 proc = multiprocessing.Process(
Simon Glassa8a01412022-07-11 19:04:04 -0600742 target=self.scan_defconfigs_for_multiprocess,
Simon Glass1b218422023-07-19 17:48:27 -0600743 args=(srcdir, que, defconfigs, warn_targets))
Simon Glass969fd332022-07-11 19:04:05 -0600744 proc.start()
745 processes.append(proc)
746 queues.append(que)
Simon Glassa8a01412022-07-11 19:04:04 -0600747
Simon Glassbec06ed2023-07-19 17:48:21 -0600748 # The resulting data should be accumulated to these lists
Simon Glassa8a01412022-07-11 19:04:04 -0600749 params_list = []
Simon Glassbec06ed2023-07-19 17:48:21 -0600750 warnings = set()
Simon Glassa8a01412022-07-11 19:04:04 -0600751
752 # Data in the queues should be retrieved preriodically.
753 # Otherwise, the queues would become full and subprocesses would get stuck.
Simon Glass969fd332022-07-11 19:04:05 -0600754 while any(p.is_alive() for p in processes):
Simon Glassbec06ed2023-07-19 17:48:21 -0600755 self.read_queues(queues, params_list, warnings)
Simon Glassa8a01412022-07-11 19:04:04 -0600756 # sleep for a while until the queues are filled
757 time.sleep(SLEEP_TIME)
758
759 # Joining subprocesses just in case
760 # (All subprocesses should already have been finished)
Simon Glass969fd332022-07-11 19:04:05 -0600761 for proc in processes:
762 proc.join()
Simon Glassa8a01412022-07-11 19:04:04 -0600763
764 # retrieve leftover data
Simon Glassbec06ed2023-07-19 17:48:21 -0600765 self.read_queues(queues, params_list, warnings)
Simon Glassa8a01412022-07-11 19:04:04 -0600766
Simon Glassbec06ed2023-07-19 17:48:21 -0600767 return params_list, sorted(list(warnings))
Simon Glassa8a01412022-07-11 19:04:04 -0600768
Simon Glass969fd332022-07-11 19:04:05 -0600769 @classmethod
Simon Glass5df95cf2023-07-19 17:48:17 -0600770 def insert_maintainers_info(cls, srcdir, params_list):
Simon Glassa8a01412022-07-11 19:04:04 -0600771 """Add Status and Maintainers information to the board parameters list.
772
Simon Glass969fd332022-07-11 19:04:05 -0600773 Args:
774 params_list (list of dict): A list of the board parameters
Simon Glassadd76e72022-07-11 19:04:08 -0600775
776 Returns:
777 list of str: List of warnings collected due to missing status, etc.
Simon Glassa8a01412022-07-11 19:04:04 -0600778 """
779 database = MaintainersDatabase()
Simon Glass5df95cf2023-07-19 17:48:17 -0600780 for (dirpath, _, filenames) in os.walk(srcdir):
Simon Glass1aaaafa2023-07-19 17:48:25 -0600781 if 'MAINTAINERS' in filenames and 'tools/buildman' not in dirpath:
Simon Glass5df95cf2023-07-19 17:48:17 -0600782 database.parse_file(srcdir,
783 os.path.join(dirpath, 'MAINTAINERS'))
Simon Glassa8a01412022-07-11 19:04:04 -0600784
785 for i, params in enumerate(params_list):
786 target = params['target']
Simon Glass4cab9aa2023-07-19 17:48:24 -0600787 maintainers = database.get_maintainers(target)
788 params['maintainers'] = maintainers
789 if maintainers:
790 params['status'] = database.get_status(target)
791 else:
792 params['status'] = '-'
Simon Glassa8a01412022-07-11 19:04:04 -0600793 params_list[i] = params
Simon Glass1aaaafa2023-07-19 17:48:25 -0600794 return sorted(database.warnings)
Simon Glassa8a01412022-07-11 19:04:04 -0600795
Simon Glass969fd332022-07-11 19:04:05 -0600796 @classmethod
797 def format_and_output(cls, params_list, output):
Simon Glassa8a01412022-07-11 19:04:04 -0600798 """Write board parameters into a file.
799
800 Columnate the board parameters, sort lines alphabetically,
801 and then write them to a file.
802
Simon Glass969fd332022-07-11 19:04:05 -0600803 Args:
804 params_list (list of dict): The list of board parameters
805 output (str): The path to the output file
Simon Glassa8a01412022-07-11 19:04:04 -0600806 """
Simon Glass969fd332022-07-11 19:04:05 -0600807 fields = ('status', 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
Simon Glass256126c2022-07-11 19:04:06 -0600808 'config', 'maintainers')
Simon Glassa8a01412022-07-11 19:04:04 -0600809
810 # First, decide the width of each column
Simon Glass969fd332022-07-11 19:04:05 -0600811 max_length = {f: 0 for f in fields}
Simon Glassa8a01412022-07-11 19:04:04 -0600812 for params in params_list:
Simon Glass969fd332022-07-11 19:04:05 -0600813 for field in fields:
814 max_length[field] = max(max_length[field], len(params[field]))
Simon Glassa8a01412022-07-11 19:04:04 -0600815
816 output_lines = []
817 for params in params_list:
818 line = ''
Simon Glass969fd332022-07-11 19:04:05 -0600819 for field in fields:
Simon Glassa8a01412022-07-11 19:04:04 -0600820 # insert two spaces between fields like column -t would
Simon Glass969fd332022-07-11 19:04:05 -0600821 line += ' ' + params[field].ljust(max_length[field])
Simon Glassa8a01412022-07-11 19:04:04 -0600822 output_lines.append(line.strip())
823
824 # ignore case when sorting
825 output_lines.sort(key=str.lower)
826
Simon Glass969fd332022-07-11 19:04:05 -0600827 with open(output, 'w', encoding="utf-8") as outf:
828 outf.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n')
Simon Glassa8a01412022-07-11 19:04:04 -0600829
Simon Glass1b218422023-07-19 17:48:27 -0600830 def build_board_list(self, config_dir=CONFIG_DIR, srcdir='.', jobs=1,
831 warn_targets=False):
Simon Glass5df95cf2023-07-19 17:48:17 -0600832 """Generate a board-database file
833
834 This works by reading the Kconfig, then loading each board's defconfig
835 in to get the setting for each option. In particular, CONFIG_TARGET_xxx
836 is typically set by the defconfig, where xxx is the target to build.
837
838 Args:
839 config_dir (str): Directory containing the defconfig files
840 srcdir (str): Directory containing source code (Kconfig files)
841 jobs (int): The number of jobs to run simultaneously
Simon Glass1b218422023-07-19 17:48:27 -0600842 warn_targets (bool): True to warn about missing or duplicate
843 CONFIG_TARGET options
Simon Glass5df95cf2023-07-19 17:48:17 -0600844
845 Returns:
846 tuple:
847 list of dict: List of board parameters, each a dict:
848 key: 'arch', 'cpu', 'soc', 'vendor', 'board', 'config',
849 'target'
850 value: string value of the key
851 list of str: Warnings that came up
852 """
Simon Glass1b218422023-07-19 17:48:27 -0600853 params_list, warnings = self.scan_defconfigs(config_dir, srcdir, jobs,
854 warn_targets)
Simon Glassbec06ed2023-07-19 17:48:21 -0600855 m_warnings = self.insert_maintainers_info(srcdir, params_list)
856 return params_list, warnings + m_warnings
Simon Glass5df95cf2023-07-19 17:48:17 -0600857
Simon Glassa8a01412022-07-11 19:04:04 -0600858 def ensure_board_list(self, output, jobs=1, force=False, quiet=False):
859 """Generate a board database file if needed.
860
Simon Glassb27e9892023-07-19 17:48:14 -0600861 This is intended to check if Kconfig has changed since the boards.cfg
862 files was generated.
863
Simon Glass969fd332022-07-11 19:04:05 -0600864 Args:
865 output (str): The name of the output file
866 jobs (int): The number of jobs to run simultaneously
867 force (bool): Force to generate the output even if it is new
868 quiet (bool): True to avoid printing a message if nothing needs doing
Simon Glassadd76e72022-07-11 19:04:08 -0600869
870 Returns:
871 bool: True if all is well, False if there were warnings
Simon Glassa8a01412022-07-11 19:04:04 -0600872 """
Simon Glassb27e9892023-07-19 17:48:14 -0600873 if not force and output_is_new(output, CONFIG_DIR, '.'):
Simon Glassa8a01412022-07-11 19:04:04 -0600874 if not quiet:
Simon Glass969fd332022-07-11 19:04:05 -0600875 print(f'{output} is up to date. Nothing to do.')
Simon Glassadd76e72022-07-11 19:04:08 -0600876 return True
Simon Glass5df95cf2023-07-19 17:48:17 -0600877 params_list, warnings = self.build_board_list(CONFIG_DIR, '.', jobs)
Simon Glassadd76e72022-07-11 19:04:08 -0600878 for warn in warnings:
879 print(warn, file=sys.stderr)
Simon Glassa8a01412022-07-11 19:04:04 -0600880 self.format_and_output(params_list, output)
Simon Glassadd76e72022-07-11 19:04:08 -0600881 return not warnings