blob: eef3f19f7ad6cc7dce0399f9538a6accae2d862f [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
Simon Glass5e728d42023-07-19 17:48:27 -0600216 def scan(self, defconfig, warn_targets):
Simon Glass0c477b72022-07-11 19:04:04 -0600217 """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 Glass5e728d42023-07-19 17:48:27 -0600221 warn_targets (bool): True to warn about missing or duplicate
222 CONFIG_TARGET options
Simon Glass0c477b72022-07-11 19:04:04 -0600223
224 Returns:
Simon Glass07a95d82023-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 Glass0c477b72022-07-11 19:04:04 -0600236 """
Simon Glass7188e4b2023-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 Glass1ee644d2023-07-19 17:48:13 -0600241 self._conf.load_config(defconfig)
Simon Glass0c477b72022-07-11 19:04:04 -0600242 self._tmpfile = None
243
244 params = {}
Simon Glass07a95d82023-07-19 17:48:21 -0600245 warnings = []
Simon Glass0c477b72022-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 Glass07a95d82023-07-19 17:48:21 -0600256 # Check there is exactly one TARGET_xxx set
Simon Glass5e728d42023-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 Glass07a95d82023-07-19 17:48:21 -0600267
Simon Glass5e728d42023-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 Glass061499b2023-07-19 17:48:22 -0600271
Simon Glass7188e4b2023-07-19 17:48:20 -0600272 params['target'] = expect_target
Simon Glass0c477b72022-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 Schuchardt8c7fd872022-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 Glass07a95d82023-07-19 17:48:21 -0600289 return params, warnings
Simon Glass0c477b72022-07-11 19:04:04 -0600290
291
292class MaintainersDatabase:
293
Simon Glass58d41be2022-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 Glassafbd2132022-07-11 19:04:08 -0600302 warnings (list of str): List of warnings due to missing status, etc.
Simon Glass58d41be2022-07-11 19:04:05 -0600303 """
Simon Glass0c477b72022-07-11 19:04:04 -0600304
305 def __init__(self):
306 """Create an empty database."""
307 self.database = {}
Simon Glassafbd2132022-07-11 19:04:08 -0600308 self.warnings = []
Simon Glass0c477b72022-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 Glass58d41be2022-07-11 19:04:05 -0600317 Args:
318 target (str): Build-target name
319
Simon Glass0c477b72022-07-11 19:04:04 -0600320 Returns:
Simon Glass58d41be2022-07-11 19:04:05 -0600321 str: 'Active', 'Orphan' or '-'.
Simon Glass0c477b72022-07-11 19:04:04 -0600322 """
323 if not target in self.database:
Simon Glassafbd2132022-07-11 19:04:08 -0600324 self.warnings.append(f"WARNING: no status info for '{target}'")
Simon Glass0c477b72022-07-11 19:04:04 -0600325 return '-'
326
327 tmp = self.database[target][0]
328 if tmp.startswith('Maintained'):
329 return 'Active'
Simon Glass58d41be2022-07-11 19:04:05 -0600330 if tmp.startswith('Supported'):
Simon Glass0c477b72022-07-11 19:04:04 -0600331 return 'Active'
Simon Glass58d41be2022-07-11 19:04:05 -0600332 if tmp.startswith('Orphan'):
Simon Glass0c477b72022-07-11 19:04:04 -0600333 return 'Orphan'
Simon Glassafbd2132022-07-11 19:04:08 -0600334 self.warnings.append(f"WARNING: {tmp}: unknown status for '{target}'")
Simon Glass58d41be2022-07-11 19:04:05 -0600335 return '-'
Simon Glass0c477b72022-07-11 19:04:04 -0600336
337 def get_maintainers(self, target):
338 """Return the maintainers of the given board.
339
Simon Glass58d41be2022-07-11 19:04:05 -0600340 Args:
341 target (str): Build-target name
342
Simon Glass0c477b72022-07-11 19:04:04 -0600343 Returns:
Simon Glass58d41be2022-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 Glass0c477b72022-07-11 19:04:04 -0600346 """
Simon Glasse6acab52023-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 Glass0c477b72022-07-11 19:04:04 -0600353
Simon Glasse6acab52023-07-19 17:48:26 -0600354 self.warnings.append(f"WARNING: no maintainers for '{target}'")
355 return ''
Simon Glass0c477b72022-07-11 19:04:04 -0600356
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600357 def parse_file(self, srcdir, fname):
Simon Glass0c477b72022-07-11 19:04:04 -0600358 """Parse a MAINTAINERS file.
359
Simon Glass58d41be2022-07-11 19:04:05 -0600360 Parse a MAINTAINERS file and accumulate board status and maintainers
361 information in the self.database dict.
Simon Glass0c477b72022-07-11 19:04:04 -0600362
Simon Glassc0b6fcc2023-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 Glass58d41be2022-07-11 19:04:05 -0600370 Args:
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600371 srcdir (str): Directory containing source code (Kconfig files)
Simon Glass58d41be2022-07-11 19:04:05 -0600372 fname (str): MAINTAINERS file to be parsed
Simon Glass0c477b72022-07-11 19:04:04 -0600373 """
Simon Glass9b828ec2023-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 """
Simon Glassb555e142023-07-19 17:48:18 -0600380 if targets:
381 for target in targets:
382 self.database[target] = (status, maintainers)
383
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 Glass5e728d42023-07-19 17:48:27 -0600665 def scan_defconfigs_for_multiprocess(cls, srcdir, queue, defconfigs,
666 warn_targets):
Simon Glass0c477b72022-07-11 19:04:04 -0600667 """Scan defconfig files and queue their board parameters
668
Simon Glass58d41be2022-07-11 19:04:05 -0600669 This function is intended to be passed to multiprocessing.Process()
670 constructor.
Simon Glass0c477b72022-07-11 19:04:04 -0600671
Simon Glass58d41be2022-07-11 19:04:05 -0600672 Args:
Simon Glasse1568362023-07-19 17:48:14 -0600673 srcdir (str): Directory containing source code
Simon Glass58d41be2022-07-11 19:04:05 -0600674 queue (multiprocessing.Queue): The resulting board parameters are
675 written into this.
676 defconfigs (sequence of str): A sequence of defconfig files to be
677 scanned.
Simon Glass5e728d42023-07-19 17:48:27 -0600678 warn_targets (bool): True to warn about missing or duplicate
679 CONFIG_TARGET options
Simon Glass0c477b72022-07-11 19:04:04 -0600680 """
Simon Glasse1568362023-07-19 17:48:14 -0600681 kconf_scanner = KconfigScanner(srcdir)
Simon Glass0c477b72022-07-11 19:04:04 -0600682 for defconfig in defconfigs:
Simon Glass5e728d42023-07-19 17:48:27 -0600683 queue.put(kconf_scanner.scan(defconfig, warn_targets))
Simon Glass0c477b72022-07-11 19:04:04 -0600684
Simon Glass58d41be2022-07-11 19:04:05 -0600685 @classmethod
Simon Glass07a95d82023-07-19 17:48:21 -0600686 def read_queues(cls, queues, params_list, warnings):
687 """Read the queues and append the data to the paramers list
688
689 Args:
690 queues (list of multiprocessing.Queue): Queues to read
691 params_list (list of dict): List to add params too
692 warnings (set of str): Set to add warnings to
693 """
Simon Glass58d41be2022-07-11 19:04:05 -0600694 for que in queues:
695 while not que.empty():
Simon Glass07a95d82023-07-19 17:48:21 -0600696 params, warn = que.get()
697 params_list.append(params)
698 warnings.update(warn)
Simon Glass0c477b72022-07-11 19:04:04 -0600699
Simon Glass5e728d42023-07-19 17:48:27 -0600700 def scan_defconfigs(self, config_dir, srcdir, jobs=1, warn_targets=False):
Simon Glass0c477b72022-07-11 19:04:04 -0600701 """Collect board parameters for all defconfig files.
702
703 This function invokes multiple processes for faster processing.
704
Simon Glass58d41be2022-07-11 19:04:05 -0600705 Args:
Simon Glasse1568362023-07-19 17:48:14 -0600706 config_dir (str): Directory containing the defconfig files
707 srcdir (str): Directory containing source code (Kconfig files)
Simon Glass58d41be2022-07-11 19:04:05 -0600708 jobs (int): The number of jobs to run simultaneously
Simon Glass5e728d42023-07-19 17:48:27 -0600709 warn_targets (bool): True to warn about missing or duplicate
710 CONFIG_TARGET options
Simon Glasse1568362023-07-19 17:48:14 -0600711
712 Returns:
Simon Glass07a95d82023-07-19 17:48:21 -0600713 tuple:
714 list of dict: List of board parameters, each a dict:
715 key: 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
716 'config'
717 value: string value of the key
718 list of str: List of warnings recorded
Simon Glass0c477b72022-07-11 19:04:04 -0600719 """
720 all_defconfigs = []
Simon Glasse1568362023-07-19 17:48:14 -0600721 for (dirpath, _, filenames) in os.walk(config_dir):
Simon Glass0c477b72022-07-11 19:04:04 -0600722 for filename in fnmatch.filter(filenames, '*_defconfig'):
723 if fnmatch.fnmatch(filename, '.*'):
724 continue
725 all_defconfigs.append(os.path.join(dirpath, filename))
726
727 total_boards = len(all_defconfigs)
728 processes = []
729 queues = []
730 for i in range(jobs):
731 defconfigs = all_defconfigs[total_boards * i // jobs :
732 total_boards * (i + 1) // jobs]
Simon Glass58d41be2022-07-11 19:04:05 -0600733 que = multiprocessing.Queue(maxsize=-1)
734 proc = multiprocessing.Process(
Simon Glass0c477b72022-07-11 19:04:04 -0600735 target=self.scan_defconfigs_for_multiprocess,
Simon Glass5e728d42023-07-19 17:48:27 -0600736 args=(srcdir, que, defconfigs, warn_targets))
Simon Glass58d41be2022-07-11 19:04:05 -0600737 proc.start()
738 processes.append(proc)
739 queues.append(que)
Simon Glass0c477b72022-07-11 19:04:04 -0600740
Simon Glass07a95d82023-07-19 17:48:21 -0600741 # The resulting data should be accumulated to these lists
Simon Glass0c477b72022-07-11 19:04:04 -0600742 params_list = []
Simon Glass07a95d82023-07-19 17:48:21 -0600743 warnings = set()
Simon Glass0c477b72022-07-11 19:04:04 -0600744
745 # Data in the queues should be retrieved preriodically.
746 # Otherwise, the queues would become full and subprocesses would get stuck.
Simon Glass58d41be2022-07-11 19:04:05 -0600747 while any(p.is_alive() for p in processes):
Simon Glass07a95d82023-07-19 17:48:21 -0600748 self.read_queues(queues, params_list, warnings)
Simon Glass0c477b72022-07-11 19:04:04 -0600749 # sleep for a while until the queues are filled
750 time.sleep(SLEEP_TIME)
751
752 # Joining subprocesses just in case
753 # (All subprocesses should already have been finished)
Simon Glass58d41be2022-07-11 19:04:05 -0600754 for proc in processes:
755 proc.join()
Simon Glass0c477b72022-07-11 19:04:04 -0600756
757 # retrieve leftover data
Simon Glass07a95d82023-07-19 17:48:21 -0600758 self.read_queues(queues, params_list, warnings)
Simon Glass0c477b72022-07-11 19:04:04 -0600759
Simon Glass07a95d82023-07-19 17:48:21 -0600760 return params_list, sorted(list(warnings))
Simon Glass0c477b72022-07-11 19:04:04 -0600761
Simon Glass58d41be2022-07-11 19:04:05 -0600762 @classmethod
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600763 def insert_maintainers_info(cls, srcdir, params_list):
Simon Glass0c477b72022-07-11 19:04:04 -0600764 """Add Status and Maintainers information to the board parameters list.
765
Simon Glass58d41be2022-07-11 19:04:05 -0600766 Args:
767 params_list (list of dict): A list of the board parameters
Simon Glassafbd2132022-07-11 19:04:08 -0600768
769 Returns:
770 list of str: List of warnings collected due to missing status, etc.
Simon Glass0c477b72022-07-11 19:04:04 -0600771 """
772 database = MaintainersDatabase()
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600773 for (dirpath, _, filenames) in os.walk(srcdir):
Simon Glass263b5982023-07-19 17:48:25 -0600774 if 'MAINTAINERS' in filenames and 'tools/buildman' not in dirpath:
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600775 database.parse_file(srcdir,
776 os.path.join(dirpath, 'MAINTAINERS'))
Simon Glass0c477b72022-07-11 19:04:04 -0600777
778 for i, params in enumerate(params_list):
779 target = params['target']
Simon Glassee088aa2023-07-19 17:48:24 -0600780 maintainers = database.get_maintainers(target)
781 params['maintainers'] = maintainers
782 if maintainers:
783 params['status'] = database.get_status(target)
784 else:
785 params['status'] = '-'
Simon Glass0c477b72022-07-11 19:04:04 -0600786 params_list[i] = params
Simon Glass263b5982023-07-19 17:48:25 -0600787 return sorted(database.warnings)
Simon Glass0c477b72022-07-11 19:04:04 -0600788
Simon Glass58d41be2022-07-11 19:04:05 -0600789 @classmethod
790 def format_and_output(cls, params_list, output):
Simon Glass0c477b72022-07-11 19:04:04 -0600791 """Write board parameters into a file.
792
793 Columnate the board parameters, sort lines alphabetically,
794 and then write them to a file.
795
Simon Glass58d41be2022-07-11 19:04:05 -0600796 Args:
797 params_list (list of dict): The list of board parameters
798 output (str): The path to the output file
Simon Glass0c477b72022-07-11 19:04:04 -0600799 """
Simon Glass58d41be2022-07-11 19:04:05 -0600800 fields = ('status', 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
Simon Glassd2d4c602022-07-11 19:04:06 -0600801 'config', 'maintainers')
Simon Glass0c477b72022-07-11 19:04:04 -0600802
803 # First, decide the width of each column
Simon Glass58d41be2022-07-11 19:04:05 -0600804 max_length = {f: 0 for f in fields}
Simon Glass0c477b72022-07-11 19:04:04 -0600805 for params in params_list:
Simon Glass58d41be2022-07-11 19:04:05 -0600806 for field in fields:
807 max_length[field] = max(max_length[field], len(params[field]))
Simon Glass0c477b72022-07-11 19:04:04 -0600808
809 output_lines = []
810 for params in params_list:
811 line = ''
Simon Glass58d41be2022-07-11 19:04:05 -0600812 for field in fields:
Simon Glass0c477b72022-07-11 19:04:04 -0600813 # insert two spaces between fields like column -t would
Simon Glass58d41be2022-07-11 19:04:05 -0600814 line += ' ' + params[field].ljust(max_length[field])
Simon Glass0c477b72022-07-11 19:04:04 -0600815 output_lines.append(line.strip())
816
817 # ignore case when sorting
818 output_lines.sort(key=str.lower)
819
Simon Glass58d41be2022-07-11 19:04:05 -0600820 with open(output, 'w', encoding="utf-8") as outf:
821 outf.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n')
Simon Glass0c477b72022-07-11 19:04:04 -0600822
Simon Glass5e728d42023-07-19 17:48:27 -0600823 def build_board_list(self, config_dir=CONFIG_DIR, srcdir='.', jobs=1,
824 warn_targets=False):
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600825 """Generate a board-database file
826
827 This works by reading the Kconfig, then loading each board's defconfig
828 in to get the setting for each option. In particular, CONFIG_TARGET_xxx
829 is typically set by the defconfig, where xxx is the target to build.
830
831 Args:
832 config_dir (str): Directory containing the defconfig files
833 srcdir (str): Directory containing source code (Kconfig files)
834 jobs (int): The number of jobs to run simultaneously
Simon Glass5e728d42023-07-19 17:48:27 -0600835 warn_targets (bool): True to warn about missing or duplicate
836 CONFIG_TARGET options
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600837
838 Returns:
839 tuple:
840 list of dict: List of board parameters, each a dict:
841 key: 'arch', 'cpu', 'soc', 'vendor', 'board', 'config',
842 'target'
843 value: string value of the key
844 list of str: Warnings that came up
845 """
Simon Glass5e728d42023-07-19 17:48:27 -0600846 params_list, warnings = self.scan_defconfigs(config_dir, srcdir, jobs,
847 warn_targets)
Simon Glass07a95d82023-07-19 17:48:21 -0600848 m_warnings = self.insert_maintainers_info(srcdir, params_list)
849 return params_list, warnings + m_warnings
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600850
Simon Glass0c477b72022-07-11 19:04:04 -0600851 def ensure_board_list(self, output, jobs=1, force=False, quiet=False):
852 """Generate a board database file if needed.
853
Simon Glasse1568362023-07-19 17:48:14 -0600854 This is intended to check if Kconfig has changed since the boards.cfg
855 files was generated.
856
Simon Glass58d41be2022-07-11 19:04:05 -0600857 Args:
858 output (str): The name of the output file
859 jobs (int): The number of jobs to run simultaneously
860 force (bool): Force to generate the output even if it is new
861 quiet (bool): True to avoid printing a message if nothing needs doing
Simon Glassafbd2132022-07-11 19:04:08 -0600862
863 Returns:
864 bool: True if all is well, False if there were warnings
Simon Glass0c477b72022-07-11 19:04:04 -0600865 """
Simon Glasse1568362023-07-19 17:48:14 -0600866 if not force and output_is_new(output, CONFIG_DIR, '.'):
Simon Glass0c477b72022-07-11 19:04:04 -0600867 if not quiet:
Simon Glass58d41be2022-07-11 19:04:05 -0600868 print(f'{output} is up to date. Nothing to do.')
Simon Glassafbd2132022-07-11 19:04:08 -0600869 return True
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600870 params_list, warnings = self.build_board_list(CONFIG_DIR, '.', jobs)
Simon Glassafbd2132022-07-11 19:04:08 -0600871 for warn in warnings:
872 print(warn, file=sys.stderr)
Simon Glass0c477b72022-07-11 19:04:04 -0600873 self.format_and_output(params_list, output)
Simon Glassafbd2132022-07-11 19:04:08 -0600874 return not warnings