blob: 9e7b486656b565e0647b2a4d99d138f743a9d932 [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
Simon Glass62dc0942024-11-08 08:23:44 -070022from u_boot_pylib import command
Simon Glass89592fc2023-09-07 10:00:18 -060023from u_boot_pylib.terminal import print_clear, tprint
Simon Glass62dc0942024-11-08 08:23:44 -070024from u_boot_pylib import tools
25from u_boot_pylib import tout
Simon Glass20751d62022-07-11 19:04:03 -060026
Simon Glass0c477b72022-07-11 19:04:04 -060027### constant variables ###
28OUTPUT_FILE = 'boards.cfg'
29CONFIG_DIR = 'configs'
30SLEEP_TIME = 0.03
Simon Glass58d41be2022-07-11 19:04:05 -060031COMMENT_BLOCK = f'''#
Simon Glass0c477b72022-07-11 19:04:04 -060032# List of boards
Simon Glass58d41be2022-07-11 19:04:05 -060033# Automatically generated by {__file__}: don't edit
Simon Glass0c477b72022-07-11 19:04:04 -060034#
Simon Glassd2d4c602022-07-11 19:04:06 -060035# Status, Arch, CPU, SoC, Vendor, Board, Target, Config, Maintainers
Simon Glass0c477b72022-07-11 19:04:04 -060036
Simon Glass58d41be2022-07-11 19:04:05 -060037'''
Simon Glass0c477b72022-07-11 19:04:04 -060038
39
Simon Glass58d41be2022-07-11 19:04:05 -060040def try_remove(fname):
41 """Remove a file ignoring 'No such file or directory' error.
42
43 Args:
44 fname (str): Filename to remove
45
46 Raises:
47 OSError: output file exists but could not be removed
48 """
Simon Glass0c477b72022-07-11 19:04:04 -060049 try:
Simon Glass58d41be2022-07-11 19:04:05 -060050 os.remove(fname)
Simon Glass0c477b72022-07-11 19:04:04 -060051 except OSError as exception:
52 # Ignore 'No such file or directory' error
53 if exception.errno != errno.ENOENT:
54 raise
55
56
Simon Glasse1568362023-07-19 17:48:14 -060057def output_is_new(output, config_dir, srcdir):
Simon Glass0c477b72022-07-11 19:04:04 -060058 """Check if the output file is up to date.
59
Simon Glass58d41be2022-07-11 19:04:05 -060060 Looks at defconfig and Kconfig files to make sure none is newer than the
61 output file. Also ensures that the boards.cfg does not mention any removed
62 boards.
63
64 Args:
65 output (str): Filename to check
Simon Glasse1568362023-07-19 17:48:14 -060066 config_dir (str): Directory containing defconfig files
67 srcdir (str): Directory containing Kconfig and MAINTAINERS files
Simon Glass58d41be2022-07-11 19:04:05 -060068
Simon Glass0c477b72022-07-11 19:04:04 -060069 Returns:
Simon Glass58d41be2022-07-11 19:04:05 -060070 True if the given output file exists and is newer than any of
71 *_defconfig, MAINTAINERS and Kconfig*. False otherwise.
72
73 Raises:
74 OSError: output file exists but could not be opened
Simon Glass0c477b72022-07-11 19:04:04 -060075 """
Simon Glass58d41be2022-07-11 19:04:05 -060076 # pylint: disable=too-many-branches
Simon Glass0c477b72022-07-11 19:04:04 -060077 try:
78 ctime = os.path.getctime(output)
79 except OSError as exception:
80 if exception.errno == errno.ENOENT:
81 # return False on 'No such file or directory' error
82 return False
Simon Glass58d41be2022-07-11 19:04:05 -060083 raise
Simon Glass0c477b72022-07-11 19:04:04 -060084
Simon Glasse1568362023-07-19 17:48:14 -060085 for (dirpath, _, filenames) in os.walk(config_dir):
Simon Glass0c477b72022-07-11 19:04:04 -060086 for filename in fnmatch.filter(filenames, '*_defconfig'):
87 if fnmatch.fnmatch(filename, '.*'):
88 continue
89 filepath = os.path.join(dirpath, filename)
90 if ctime < os.path.getctime(filepath):
91 return False
92
Simon Glasse1568362023-07-19 17:48:14 -060093 for (dirpath, _, filenames) in os.walk(srcdir):
Simon Glass0c477b72022-07-11 19:04:04 -060094 for filename in filenames:
95 if (fnmatch.fnmatch(filename, '*~') or
96 not fnmatch.fnmatch(filename, 'Kconfig*') and
97 not filename == 'MAINTAINERS'):
98 continue
99 filepath = os.path.join(dirpath, filename)
100 if ctime < os.path.getctime(filepath):
101 return False
102
103 # Detect a board that has been removed since the current board database
104 # was generated
Simon Glass58d41be2022-07-11 19:04:05 -0600105 with open(output, encoding="utf-8") as inf:
106 for line in inf:
Simon Glassd2d4c602022-07-11 19:04:06 -0600107 if 'Options,' in line:
108 return False
Simon Glass0c477b72022-07-11 19:04:04 -0600109 if line[0] == '#' or line == '\n':
110 continue
111 defconfig = line.split()[6] + '_defconfig'
Simon Glasse1568362023-07-19 17:48:14 -0600112 if not os.path.exists(os.path.join(config_dir, defconfig)):
Simon Glass0c477b72022-07-11 19:04:04 -0600113 return False
114
115 return True
116
117
Simon Glass20751d62022-07-11 19:04:03 -0600118class Expr:
119 """A single regular expression for matching boards to build"""
120
121 def __init__(self, expr):
122 """Set up a new Expr object.
123
124 Args:
Heinrich Schuchardt3399d332023-12-15 10:42:30 +0100125 expr (str): String containing regular expression to store
Simon Glass20751d62022-07-11 19:04:03 -0600126 """
127 self._expr = expr
128 self._re = re.compile(expr)
129
130 def matches(self, props):
131 """Check if any of the properties match the regular expression.
132
133 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600134 props (list of str): List of properties to check
Simon Glass20751d62022-07-11 19:04:03 -0600135 Returns:
136 True if any of the properties match the regular expression
137 """
138 for prop in props:
139 if self._re.match(prop):
140 return True
141 return False
142
143 def __str__(self):
144 return self._expr
145
146class Term:
147 """A list of expressions each of which must match with properties.
148
149 This provides a list of 'AND' expressions, meaning that each must
150 match the board properties for that board to be built.
151 """
152 def __init__(self):
153 self._expr_list = []
154 self._board_count = 0
155
156 def add_expr(self, expr):
157 """Add an Expr object to the list to check.
158
159 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600160 expr (Expr): New Expr object to add to the list of those that must
Simon Glass20751d62022-07-11 19:04:03 -0600161 match for a board to be built.
162 """
163 self._expr_list.append(Expr(expr))
164
165 def __str__(self):
166 """Return some sort of useful string describing the term"""
167 return '&'.join([str(expr) for expr in self._expr_list])
168
169 def matches(self, props):
170 """Check if any of the properties match this term
171
172 Each of the expressions in the term is checked. All must match.
173
174 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600175 props (list of str): List of properties to check
Simon Glass20751d62022-07-11 19:04:03 -0600176 Returns:
177 True if all of the expressions in the Term match, else False
178 """
179 for expr in self._expr_list:
180 if not expr.matches(props):
181 return False
182 return True
183
Simon Glass0c477b72022-07-11 19:04:04 -0600184
185class KconfigScanner:
186
187 """Kconfig scanner."""
188
189 ### constant variable only used in this class ###
190 _SYMBOL_TABLE = {
191 'arch' : 'SYS_ARCH',
192 'cpu' : 'SYS_CPU',
193 'soc' : 'SYS_SOC',
194 'vendor' : 'SYS_VENDOR',
195 'board' : 'SYS_BOARD',
196 'config' : 'SYS_CONFIG_NAME',
Simon Glassd2d4c602022-07-11 19:04:06 -0600197 # 'target' is added later
Simon Glass0c477b72022-07-11 19:04:04 -0600198 }
199
Simon Glasse1568362023-07-19 17:48:14 -0600200 def __init__(self, srctree):
Simon Glass0c477b72022-07-11 19:04:04 -0600201 """Scan all the Kconfig files and create a Kconfig object."""
202 # Define environment variables referenced from Kconfig
Simon Glasse1568362023-07-19 17:48:14 -0600203 os.environ['srctree'] = srctree
Simon Glass0c477b72022-07-11 19:04:04 -0600204 os.environ['UBOOTVERSION'] = 'dummy'
205 os.environ['KCONFIG_OBJDIR'] = ''
Simon Glass58d41be2022-07-11 19:04:05 -0600206 self._tmpfile = None
Simon Glass0c477b72022-07-11 19:04:04 -0600207 self._conf = kconfiglib.Kconfig(warn=False)
Simon Glass62dc0942024-11-08 08:23:44 -0700208 self._srctree = srctree
Simon Glass0c477b72022-07-11 19:04:04 -0600209
210 def __del__(self):
211 """Delete a leftover temporary file before exit.
212
213 The scan() method of this class creates a temporay file and deletes
214 it on success. If scan() method throws an exception on the way,
215 the temporary file might be left over. In that case, it should be
216 deleted in this destructor.
217 """
Simon Glass58d41be2022-07-11 19:04:05 -0600218 if self._tmpfile:
Simon Glass0c477b72022-07-11 19:04:04 -0600219 try_remove(self._tmpfile)
220
Simon Glass5e728d42023-07-19 17:48:27 -0600221 def scan(self, defconfig, warn_targets):
Simon Glass0c477b72022-07-11 19:04:04 -0600222 """Load a defconfig file to obtain board parameters.
223
Simon Glass58d41be2022-07-11 19:04:05 -0600224 Args:
225 defconfig (str): path to the defconfig file to be processed
Simon Glass5e728d42023-07-19 17:48:27 -0600226 warn_targets (bool): True to warn about missing or duplicate
227 CONFIG_TARGET options
Simon Glass0c477b72022-07-11 19:04:04 -0600228
229 Returns:
Simon Glass07a95d82023-07-19 17:48:21 -0600230 tuple: dictionary of board parameters. It has a form of:
231 {
232 'arch': <arch_name>,
233 'cpu': <cpu_name>,
234 'soc': <soc_name>,
235 'vendor': <vendor_name>,
236 'board': <board_name>,
237 'target': <target_name>,
238 'config': <config_header_name>,
239 }
240 warnings (list of str): list of warnings found
Simon Glass0c477b72022-07-11 19:04:04 -0600241 """
Simon Glass7188e4b2023-07-19 17:48:20 -0600242 leaf = os.path.basename(defconfig)
243 expect_target, match, rear = leaf.partition('_defconfig')
244 assert match and not rear, f'{leaf} : invalid defconfig'
245
Simon Glass62dc0942024-11-08 08:23:44 -0700246 temp = None
247 if b'#include' in tools.read_file(defconfig):
248 cmd = [
249 os.getenv('CPP', 'cpp'),
250 '-nostdinc', '-P',
251 '-I', self._srctree,
252 '-undef',
253 '-x', 'assembler-with-cpp',
254 defconfig]
255 result = command.run_pipe([cmd], capture=True, capture_stderr=True)
256 temp = tempfile.NamedTemporaryFile(prefix='buildman-')
257 tools.write_file(temp.name, result.stdout, False)
258 fname = temp.name
259 tout.info(f'Processing #include to produce {defconfig}')
260 else:
261 fname = defconfig
262
263 self._conf.load_config(fname)
264 if temp:
265 del temp
Simon Glass0c477b72022-07-11 19:04:04 -0600266 self._tmpfile = None
267
268 params = {}
Simon Glass07a95d82023-07-19 17:48:21 -0600269 warnings = []
Simon Glass0c477b72022-07-11 19:04:04 -0600270
271 # Get the value of CONFIG_SYS_ARCH, CONFIG_SYS_CPU, ... etc.
272 # Set '-' if the value is empty.
273 for key, symbol in list(self._SYMBOL_TABLE.items()):
274 value = self._conf.syms.get(symbol).str_value
275 if value:
276 params[key] = value
277 else:
278 params[key] = '-'
279
Simon Glass07a95d82023-07-19 17:48:21 -0600280 # Check there is exactly one TARGET_xxx set
Simon Glass5e728d42023-07-19 17:48:27 -0600281 if warn_targets:
282 target = None
283 for name, sym in self._conf.syms.items():
284 if name.startswith('TARGET_') and sym.str_value == 'y':
285 tname = name[7:].lower()
286 if target:
287 warnings.append(
288 f'WARNING: {leaf}: Duplicate TARGET_xxx: {target} and {tname}')
289 else:
290 target = tname
Simon Glass07a95d82023-07-19 17:48:21 -0600291
Simon Glass5e728d42023-07-19 17:48:27 -0600292 if not target:
293 cfg_name = expect_target.replace('-', '_').upper()
294 warnings.append(f'WARNING: {leaf}: No TARGET_{cfg_name} enabled')
Simon Glass061499b2023-07-19 17:48:22 -0600295
Simon Glass7188e4b2023-07-19 17:48:20 -0600296 params['target'] = expect_target
Simon Glass0c477b72022-07-11 19:04:04 -0600297
298 # fix-up for aarch64
299 if params['arch'] == 'arm' and params['cpu'] == 'armv8':
300 params['arch'] = 'aarch64'
301
Heinrich Schuchardt8c7fd872022-10-03 18:07:53 +0200302 # fix-up for riscv
303 if params['arch'] == 'riscv':
304 try:
305 value = self._conf.syms.get('ARCH_RV32I').str_value
306 except:
307 value = ''
308 if value == 'y':
309 params['arch'] = 'riscv32'
310 else:
311 params['arch'] = 'riscv64'
312
Simon Glass07a95d82023-07-19 17:48:21 -0600313 return params, warnings
Simon Glass0c477b72022-07-11 19:04:04 -0600314
315
316class MaintainersDatabase:
317
Simon Glass58d41be2022-07-11 19:04:05 -0600318 """The database of board status and maintainers.
319
320 Properties:
321 database: dict:
322 key: Board-target name (e.g. 'snow')
323 value: tuple:
324 str: Board status (e.g. 'Active')
325 str: List of maintainers, separated by :
Simon Glassafbd2132022-07-11 19:04:08 -0600326 warnings (list of str): List of warnings due to missing status, etc.
Simon Glass58d41be2022-07-11 19:04:05 -0600327 """
Simon Glass0c477b72022-07-11 19:04:04 -0600328
329 def __init__(self):
330 """Create an empty database."""
331 self.database = {}
Simon Glassafbd2132022-07-11 19:04:08 -0600332 self.warnings = []
Simon Glass0c477b72022-07-11 19:04:04 -0600333
334 def get_status(self, target):
335 """Return the status of the given board.
336
337 The board status is generally either 'Active' or 'Orphan'.
338 Display a warning message and return '-' if status information
339 is not found.
340
Simon Glass58d41be2022-07-11 19:04:05 -0600341 Args:
342 target (str): Build-target name
343
Simon Glass0c477b72022-07-11 19:04:04 -0600344 Returns:
Simon Glass58d41be2022-07-11 19:04:05 -0600345 str: 'Active', 'Orphan' or '-'.
Simon Glass0c477b72022-07-11 19:04:04 -0600346 """
347 if not target in self.database:
Simon Glassafbd2132022-07-11 19:04:08 -0600348 self.warnings.append(f"WARNING: no status info for '{target}'")
Simon Glass0c477b72022-07-11 19:04:04 -0600349 return '-'
350
351 tmp = self.database[target][0]
352 if tmp.startswith('Maintained'):
353 return 'Active'
Simon Glass58d41be2022-07-11 19:04:05 -0600354 if tmp.startswith('Supported'):
Simon Glass0c477b72022-07-11 19:04:04 -0600355 return 'Active'
Simon Glass58d41be2022-07-11 19:04:05 -0600356 if tmp.startswith('Orphan'):
Simon Glass0c477b72022-07-11 19:04:04 -0600357 return 'Orphan'
Simon Glassafbd2132022-07-11 19:04:08 -0600358 self.warnings.append(f"WARNING: {tmp}: unknown status for '{target}'")
Simon Glass58d41be2022-07-11 19:04:05 -0600359 return '-'
Simon Glass0c477b72022-07-11 19:04:04 -0600360
361 def get_maintainers(self, target):
362 """Return the maintainers of the given board.
363
Simon Glass58d41be2022-07-11 19:04:05 -0600364 Args:
365 target (str): Build-target name
366
Simon Glass0c477b72022-07-11 19:04:04 -0600367 Returns:
Simon Glass58d41be2022-07-11 19:04:05 -0600368 str: Maintainers of the board. If the board has two or more
369 maintainers, they are separated with colons.
Simon Glass0c477b72022-07-11 19:04:04 -0600370 """
Simon Glasse6acab52023-07-19 17:48:26 -0600371 entry = self.database.get(target)
372 if entry:
373 status, maint_list = entry
374 if not status.startswith('Orphan'):
375 if len(maint_list) > 1 or (maint_list and maint_list[0] != '-'):
376 return ':'.join(maint_list)
Simon Glass0c477b72022-07-11 19:04:04 -0600377
Simon Glasse6acab52023-07-19 17:48:26 -0600378 self.warnings.append(f"WARNING: no maintainers for '{target}'")
379 return ''
Simon Glass0c477b72022-07-11 19:04:04 -0600380
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600381 def parse_file(self, srcdir, fname):
Simon Glass0c477b72022-07-11 19:04:04 -0600382 """Parse a MAINTAINERS file.
383
Simon Glass58d41be2022-07-11 19:04:05 -0600384 Parse a MAINTAINERS file and accumulate board status and maintainers
385 information in the self.database dict.
Simon Glass0c477b72022-07-11 19:04:04 -0600386
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600387 defconfig files are used to specify the target, e.g. xxx_defconfig is
388 used for target 'xxx'. If there is no defconfig file mentioned in the
389 MAINTAINERS file F: entries, then this function does nothing.
390
391 The N: name entries can be used to specify a defconfig file using
392 wildcards.
393
Simon Glass58d41be2022-07-11 19:04:05 -0600394 Args:
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600395 srcdir (str): Directory containing source code (Kconfig files)
Simon Glass58d41be2022-07-11 19:04:05 -0600396 fname (str): MAINTAINERS file to be parsed
Simon Glass0c477b72022-07-11 19:04:04 -0600397 """
Simon Glass9b828ec2023-07-19 17:48:19 -0600398 def add_targets(linenum):
399 """Add any new targets
400
401 Args:
402 linenum (int): Current line number
403 """
Simon Glassb555e142023-07-19 17:48:18 -0600404 if targets:
405 for target in targets:
406 self.database[target] = (status, maintainers)
407
Simon Glass0c477b72022-07-11 19:04:04 -0600408 targets = []
409 maintainers = []
410 status = '-'
Simon Glass58d41be2022-07-11 19:04:05 -0600411 with open(fname, encoding="utf-8") as inf:
Simon Glass9b828ec2023-07-19 17:48:19 -0600412 for linenum, line in enumerate(inf):
Simon Glass58d41be2022-07-11 19:04:05 -0600413 # Check also commented maintainers
414 if line[:3] == '#M:':
415 line = line[1:]
416 tag, rest = line[:2], line[2:].strip()
417 if tag == 'M:':
418 maintainers.append(rest)
419 elif tag == 'F:':
420 # expand wildcard and filter by 'configs/*_defconfig'
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600421 glob_path = os.path.join(srcdir, rest)
422 for item in glob.glob(glob_path):
Simon Glass58d41be2022-07-11 19:04:05 -0600423 front, match, rear = item.partition('configs/')
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600424 if front.endswith('/'):
425 front = front[:-1]
426 if front == srcdir and match:
Simon Glass58d41be2022-07-11 19:04:05 -0600427 front, match, rear = rear.rpartition('_defconfig')
428 if match and not rear:
429 targets.append(front)
430 elif tag == 'S:':
431 status = rest
Simon Glass3a90a692022-10-11 08:15:37 -0600432 elif tag == 'N:':
433 # Just scan the configs directory since that's all we care
434 # about
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600435 walk_path = os.walk(os.path.join(srcdir, 'configs'))
436 for dirpath, _, fnames in walk_path:
437 for cfg in fnames:
Simon Glass060ee972023-07-19 17:48:23 -0600438 path = os.path.join(dirpath, cfg)[len(srcdir) + 1:]
Simon Glass3a90a692022-10-11 08:15:37 -0600439 front, match, rear = path.partition('configs/')
Simon Glass060ee972023-07-19 17:48:23 -0600440 if front or not match:
441 continue
442 front, match, rear = rear.rpartition('_defconfig')
443
444 # Use this entry if it matches the defconfig file
445 # without the _defconfig suffix. For example
446 # 'am335x.*' matches am335x_guardian_defconfig
447 if match and not rear and re.search(rest, front):
448 targets.append(front)
Simon Glass58d41be2022-07-11 19:04:05 -0600449 elif line == '\n':
Simon Glass9b828ec2023-07-19 17:48:19 -0600450 add_targets(linenum)
Simon Glass58d41be2022-07-11 19:04:05 -0600451 targets = []
452 maintainers = []
453 status = '-'
Simon Glass9b828ec2023-07-19 17:48:19 -0600454 add_targets(linenum)
Simon Glass0c477b72022-07-11 19:04:04 -0600455
Simon Glass20751d62022-07-11 19:04:03 -0600456
457class Boards:
458 """Manage a list of boards."""
459 def __init__(self):
Simon Glass20751d62022-07-11 19:04:03 -0600460 self._boards = []
461
462 def add_board(self, brd):
463 """Add a new board to the list.
464
465 The board's target member must not already exist in the board list.
466
467 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600468 brd (Board): board to add
Simon Glass20751d62022-07-11 19:04:03 -0600469 """
470 self._boards.append(brd)
471
472 def read_boards(self, fname):
473 """Read a list of boards from a board file.
474
475 Create a Board object for each and add it to our _boards list.
476
477 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600478 fname (str): Filename of boards.cfg file
Simon Glass20751d62022-07-11 19:04:03 -0600479 """
480 with open(fname, 'r', encoding='utf-8') as inf:
481 for line in inf:
482 if line[0] == '#':
483 continue
484 fields = line.split()
485 if not fields:
486 continue
487 for upto, field in enumerate(fields):
488 if field == '-':
489 fields[upto] = ''
490 while len(fields) < 8:
491 fields.append('')
492 if len(fields) > 8:
493 fields = fields[:8]
494
495 brd = board.Board(*fields)
496 self.add_board(brd)
497
498
499 def get_list(self):
500 """Return a list of available boards.
501
502 Returns:
503 List of Board objects
504 """
505 return self._boards
506
507 def get_dict(self):
508 """Build a dictionary containing all the boards.
509
510 Returns:
511 Dictionary:
512 key is board.target
513 value is board
514 """
515 board_dict = OrderedDict()
516 for brd in self._boards:
517 board_dict[brd.target] = brd
518 return board_dict
519
520 def get_selected_dict(self):
521 """Return a dictionary containing the selected boards
522
523 Returns:
524 List of Board objects that are marked selected
525 """
526 board_dict = OrderedDict()
527 for brd in self._boards:
528 if brd.build_it:
529 board_dict[brd.target] = brd
530 return board_dict
531
532 def get_selected(self):
533 """Return a list of selected boards
534
535 Returns:
536 List of Board objects that are marked selected
537 """
538 return [brd for brd in self._boards if brd.build_it]
539
540 def get_selected_names(self):
541 """Return a list of selected boards
542
543 Returns:
544 List of board names that are marked selected
545 """
546 return [brd.target for brd in self._boards if brd.build_it]
547
548 @classmethod
549 def _build_terms(cls, args):
550 """Convert command line arguments to a list of terms.
551
552 This deals with parsing of the arguments. It handles the '&'
553 operator, which joins several expressions into a single Term.
554
555 For example:
556 ['arm & freescale sandbox', 'tegra']
557
558 will produce 3 Terms containing expressions as follows:
559 arm, freescale
560 sandbox
561 tegra
562
563 The first Term has two expressions, both of which must match for
564 a board to be selected.
565
566 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600567 args (list of str): List of command line arguments
568
Simon Glass20751d62022-07-11 19:04:03 -0600569 Returns:
Simon Glass58d41be2022-07-11 19:04:05 -0600570 list of Term: A list of Term objects
Simon Glass20751d62022-07-11 19:04:03 -0600571 """
572 syms = []
573 for arg in args:
574 for word in arg.split():
575 sym_build = []
576 for term in word.split('&'):
577 if term:
578 sym_build.append(term)
579 sym_build.append('&')
580 syms += sym_build[:-1]
581 terms = []
582 term = None
583 oper = None
584 for sym in syms:
585 if sym == '&':
586 oper = sym
587 elif oper:
588 term.add_expr(sym)
589 oper = None
590 else:
591 if term:
592 terms.append(term)
593 term = Term()
594 term.add_expr(sym)
595 if term:
596 terms.append(term)
597 return terms
598
599 def select_boards(self, args, exclude=None, brds=None):
600 """Mark boards selected based on args
601
602 Normally either boards (an explicit list of boards) or args (a list of
603 terms to match against) is used. It is possible to specify both, in
604 which case they are additive.
605
606 If brds and args are both empty, all boards are selected.
607
608 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600609 args (list of str): List of strings specifying boards to include,
610 either named, or by their target, architecture, cpu, vendor or
611 soc. If empty, all boards are selected.
612 exclude (list of str): List of boards to exclude, regardless of
613 'args', or None for none
614 brds (list of Board): List of boards to build, or None/[] for all
Simon Glass20751d62022-07-11 19:04:03 -0600615
616 Returns:
617 Tuple
618 Dictionary which holds the list of boards which were selected
619 due to each argument, arranged by argument.
620 List of errors found
621 """
Simon Glass58d41be2022-07-11 19:04:05 -0600622 def _check_board(brd):
623 """Check whether to include or exclude a board
Simon Glass20751d62022-07-11 19:04:03 -0600624
Simon Glass58d41be2022-07-11 19:04:05 -0600625 Checks the various terms and decide whether to build it or not (the
626 'build_it' variable).
Simon Glass20751d62022-07-11 19:04:03 -0600627
Simon Glass58d41be2022-07-11 19:04:05 -0600628 If it is built, add the board to the result[term] list so we know
629 which term caused it to be built. Add it to result['all'] also.
Simon Glass20751d62022-07-11 19:04:03 -0600630
Simon Glass58d41be2022-07-11 19:04:05 -0600631 Keep a list of boards we found in 'found', so we can report boards
632 which appear in self._boards but not in brds.
633
634 Args:
635 brd (Board): Board to check
636 """
Simon Glass20751d62022-07-11 19:04:03 -0600637 matching_term = None
638 build_it = False
639 if terms:
640 for term in terms:
641 if term.matches(brd.props):
642 matching_term = str(term)
643 build_it = True
644 break
645 elif brds:
646 if brd.target in brds:
647 build_it = True
648 found.append(brd.target)
649 else:
650 build_it = True
651
652 # Check that it is not specifically excluded
653 for expr in exclude_list:
654 if expr.matches(brd.props):
655 build_it = False
656 break
657
658 if build_it:
659 brd.build_it = True
660 if matching_term:
661 result[matching_term].append(brd.target)
662 result['all'].append(brd.target)
663
Simon Glass58d41be2022-07-11 19:04:05 -0600664 result = OrderedDict()
665 warnings = []
666 terms = self._build_terms(args)
667
668 result['all'] = []
669 for term in terms:
670 result[str(term)] = []
671
672 exclude_list = []
673 if exclude:
674 for expr in exclude:
675 exclude_list.append(Expr(expr))
676
677 found = []
678 for brd in self._boards:
679 _check_board(brd)
680
Simon Glass20751d62022-07-11 19:04:03 -0600681 if brds:
682 remaining = set(brds) - set(found)
683 if remaining:
684 warnings.append(f"Boards not found: {', '.join(remaining)}\n")
685
686 return result, warnings
Simon Glass0c477b72022-07-11 19:04:04 -0600687
Simon Glass58d41be2022-07-11 19:04:05 -0600688 @classmethod
Simon Glass5e728d42023-07-19 17:48:27 -0600689 def scan_defconfigs_for_multiprocess(cls, srcdir, queue, defconfigs,
690 warn_targets):
Simon Glass0c477b72022-07-11 19:04:04 -0600691 """Scan defconfig files and queue their board parameters
692
Simon Glass58d41be2022-07-11 19:04:05 -0600693 This function is intended to be passed to multiprocessing.Process()
694 constructor.
Simon Glass0c477b72022-07-11 19:04:04 -0600695
Simon Glass58d41be2022-07-11 19:04:05 -0600696 Args:
Simon Glasse1568362023-07-19 17:48:14 -0600697 srcdir (str): Directory containing source code
Simon Glass58d41be2022-07-11 19:04:05 -0600698 queue (multiprocessing.Queue): The resulting board parameters are
699 written into this.
700 defconfigs (sequence of str): A sequence of defconfig files to be
701 scanned.
Simon Glass5e728d42023-07-19 17:48:27 -0600702 warn_targets (bool): True to warn about missing or duplicate
703 CONFIG_TARGET options
Simon Glass0c477b72022-07-11 19:04:04 -0600704 """
Simon Glasse1568362023-07-19 17:48:14 -0600705 kconf_scanner = KconfigScanner(srcdir)
Simon Glass0c477b72022-07-11 19:04:04 -0600706 for defconfig in defconfigs:
Simon Glass5e728d42023-07-19 17:48:27 -0600707 queue.put(kconf_scanner.scan(defconfig, warn_targets))
Simon Glass0c477b72022-07-11 19:04:04 -0600708
Simon Glass58d41be2022-07-11 19:04:05 -0600709 @classmethod
Simon Glass07a95d82023-07-19 17:48:21 -0600710 def read_queues(cls, queues, params_list, warnings):
711 """Read the queues and append the data to the paramers list
712
713 Args:
714 queues (list of multiprocessing.Queue): Queues to read
715 params_list (list of dict): List to add params too
716 warnings (set of str): Set to add warnings to
717 """
Simon Glass58d41be2022-07-11 19:04:05 -0600718 for que in queues:
719 while not que.empty():
Simon Glass07a95d82023-07-19 17:48:21 -0600720 params, warn = que.get()
721 params_list.append(params)
722 warnings.update(warn)
Simon Glass0c477b72022-07-11 19:04:04 -0600723
Simon Glass5e728d42023-07-19 17:48:27 -0600724 def scan_defconfigs(self, config_dir, srcdir, jobs=1, warn_targets=False):
Simon Glass0c477b72022-07-11 19:04:04 -0600725 """Collect board parameters for all defconfig files.
726
727 This function invokes multiple processes for faster processing.
728
Simon Glass58d41be2022-07-11 19:04:05 -0600729 Args:
Simon Glasse1568362023-07-19 17:48:14 -0600730 config_dir (str): Directory containing the defconfig files
731 srcdir (str): Directory containing source code (Kconfig files)
Simon Glass58d41be2022-07-11 19:04:05 -0600732 jobs (int): The number of jobs to run simultaneously
Simon Glass5e728d42023-07-19 17:48:27 -0600733 warn_targets (bool): True to warn about missing or duplicate
734 CONFIG_TARGET options
Simon Glasse1568362023-07-19 17:48:14 -0600735
736 Returns:
Simon Glass07a95d82023-07-19 17:48:21 -0600737 tuple:
738 list of dict: List of board parameters, each a dict:
739 key: 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
740 'config'
741 value: string value of the key
742 list of str: List of warnings recorded
Simon Glass0c477b72022-07-11 19:04:04 -0600743 """
744 all_defconfigs = []
Simon Glasse1568362023-07-19 17:48:14 -0600745 for (dirpath, _, filenames) in os.walk(config_dir):
Simon Glass0c477b72022-07-11 19:04:04 -0600746 for filename in fnmatch.filter(filenames, '*_defconfig'):
747 if fnmatch.fnmatch(filename, '.*'):
748 continue
749 all_defconfigs.append(os.path.join(dirpath, filename))
750
751 total_boards = len(all_defconfigs)
752 processes = []
753 queues = []
754 for i in range(jobs):
755 defconfigs = all_defconfigs[total_boards * i // jobs :
756 total_boards * (i + 1) // jobs]
Simon Glass58d41be2022-07-11 19:04:05 -0600757 que = multiprocessing.Queue(maxsize=-1)
758 proc = multiprocessing.Process(
Simon Glass0c477b72022-07-11 19:04:04 -0600759 target=self.scan_defconfigs_for_multiprocess,
Simon Glass5e728d42023-07-19 17:48:27 -0600760 args=(srcdir, que, defconfigs, warn_targets))
Simon Glass58d41be2022-07-11 19:04:05 -0600761 proc.start()
762 processes.append(proc)
763 queues.append(que)
Simon Glass0c477b72022-07-11 19:04:04 -0600764
Simon Glass07a95d82023-07-19 17:48:21 -0600765 # The resulting data should be accumulated to these lists
Simon Glass0c477b72022-07-11 19:04:04 -0600766 params_list = []
Simon Glass07a95d82023-07-19 17:48:21 -0600767 warnings = set()
Simon Glass0c477b72022-07-11 19:04:04 -0600768
769 # Data in the queues should be retrieved preriodically.
770 # Otherwise, the queues would become full and subprocesses would get stuck.
Simon Glass58d41be2022-07-11 19:04:05 -0600771 while any(p.is_alive() for p in processes):
Simon Glass07a95d82023-07-19 17:48:21 -0600772 self.read_queues(queues, params_list, warnings)
Simon Glass0c477b72022-07-11 19:04:04 -0600773 # sleep for a while until the queues are filled
774 time.sleep(SLEEP_TIME)
775
776 # Joining subprocesses just in case
777 # (All subprocesses should already have been finished)
Simon Glass58d41be2022-07-11 19:04:05 -0600778 for proc in processes:
779 proc.join()
Simon Glass0c477b72022-07-11 19:04:04 -0600780
781 # retrieve leftover data
Simon Glass07a95d82023-07-19 17:48:21 -0600782 self.read_queues(queues, params_list, warnings)
Simon Glass0c477b72022-07-11 19:04:04 -0600783
Simon Glass07a95d82023-07-19 17:48:21 -0600784 return params_list, sorted(list(warnings))
Simon Glass0c477b72022-07-11 19:04:04 -0600785
Simon Glass58d41be2022-07-11 19:04:05 -0600786 @classmethod
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600787 def insert_maintainers_info(cls, srcdir, params_list):
Simon Glass0c477b72022-07-11 19:04:04 -0600788 """Add Status and Maintainers information to the board parameters list.
789
Simon Glass58d41be2022-07-11 19:04:05 -0600790 Args:
791 params_list (list of dict): A list of the board parameters
Simon Glassafbd2132022-07-11 19:04:08 -0600792
793 Returns:
794 list of str: List of warnings collected due to missing status, etc.
Simon Glass0c477b72022-07-11 19:04:04 -0600795 """
796 database = MaintainersDatabase()
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600797 for (dirpath, _, filenames) in os.walk(srcdir):
Simon Glass263b5982023-07-19 17:48:25 -0600798 if 'MAINTAINERS' in filenames and 'tools/buildman' not in dirpath:
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600799 database.parse_file(srcdir,
800 os.path.join(dirpath, 'MAINTAINERS'))
Simon Glass0c477b72022-07-11 19:04:04 -0600801
802 for i, params in enumerate(params_list):
803 target = params['target']
Simon Glassee088aa2023-07-19 17:48:24 -0600804 maintainers = database.get_maintainers(target)
805 params['maintainers'] = maintainers
806 if maintainers:
807 params['status'] = database.get_status(target)
808 else:
809 params['status'] = '-'
Simon Glass0c477b72022-07-11 19:04:04 -0600810 params_list[i] = params
Simon Glass263b5982023-07-19 17:48:25 -0600811 return sorted(database.warnings)
Simon Glass0c477b72022-07-11 19:04:04 -0600812
Simon Glass58d41be2022-07-11 19:04:05 -0600813 @classmethod
814 def format_and_output(cls, params_list, output):
Simon Glass0c477b72022-07-11 19:04:04 -0600815 """Write board parameters into a file.
816
817 Columnate the board parameters, sort lines alphabetically,
818 and then write them to a file.
819
Simon Glass58d41be2022-07-11 19:04:05 -0600820 Args:
821 params_list (list of dict): The list of board parameters
822 output (str): The path to the output file
Simon Glass0c477b72022-07-11 19:04:04 -0600823 """
Simon Glass58d41be2022-07-11 19:04:05 -0600824 fields = ('status', 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
Simon Glassd2d4c602022-07-11 19:04:06 -0600825 'config', 'maintainers')
Simon Glass0c477b72022-07-11 19:04:04 -0600826
827 # First, decide the width of each column
Simon Glass58d41be2022-07-11 19:04:05 -0600828 max_length = {f: 0 for f in fields}
Simon Glass0c477b72022-07-11 19:04:04 -0600829 for params in params_list:
Simon Glass58d41be2022-07-11 19:04:05 -0600830 for field in fields:
831 max_length[field] = max(max_length[field], len(params[field]))
Simon Glass0c477b72022-07-11 19:04:04 -0600832
833 output_lines = []
834 for params in params_list:
835 line = ''
Simon Glass58d41be2022-07-11 19:04:05 -0600836 for field in fields:
Simon Glass0c477b72022-07-11 19:04:04 -0600837 # insert two spaces between fields like column -t would
Simon Glass58d41be2022-07-11 19:04:05 -0600838 line += ' ' + params[field].ljust(max_length[field])
Simon Glass0c477b72022-07-11 19:04:04 -0600839 output_lines.append(line.strip())
840
841 # ignore case when sorting
842 output_lines.sort(key=str.lower)
843
Simon Glass58d41be2022-07-11 19:04:05 -0600844 with open(output, 'w', encoding="utf-8") as outf:
845 outf.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n')
Simon Glass0c477b72022-07-11 19:04:04 -0600846
Simon Glass5e728d42023-07-19 17:48:27 -0600847 def build_board_list(self, config_dir=CONFIG_DIR, srcdir='.', jobs=1,
848 warn_targets=False):
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600849 """Generate a board-database file
850
851 This works by reading the Kconfig, then loading each board's defconfig
852 in to get the setting for each option. In particular, CONFIG_TARGET_xxx
853 is typically set by the defconfig, where xxx is the target to build.
854
855 Args:
856 config_dir (str): Directory containing the defconfig files
857 srcdir (str): Directory containing source code (Kconfig files)
858 jobs (int): The number of jobs to run simultaneously
Simon Glass5e728d42023-07-19 17:48:27 -0600859 warn_targets (bool): True to warn about missing or duplicate
860 CONFIG_TARGET options
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600861
862 Returns:
863 tuple:
864 list of dict: List of board parameters, each a dict:
865 key: 'arch', 'cpu', 'soc', 'vendor', 'board', 'config',
866 'target'
867 value: string value of the key
868 list of str: Warnings that came up
869 """
Simon Glass5e728d42023-07-19 17:48:27 -0600870 params_list, warnings = self.scan_defconfigs(config_dir, srcdir, jobs,
871 warn_targets)
Simon Glass07a95d82023-07-19 17:48:21 -0600872 m_warnings = self.insert_maintainers_info(srcdir, params_list)
873 return params_list, warnings + m_warnings
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600874
Simon Glass0c477b72022-07-11 19:04:04 -0600875 def ensure_board_list(self, output, jobs=1, force=False, quiet=False):
876 """Generate a board database file if needed.
877
Simon Glasse1568362023-07-19 17:48:14 -0600878 This is intended to check if Kconfig has changed since the boards.cfg
879 files was generated.
880
Simon Glass58d41be2022-07-11 19:04:05 -0600881 Args:
882 output (str): The name of the output file
883 jobs (int): The number of jobs to run simultaneously
884 force (bool): Force to generate the output even if it is new
885 quiet (bool): True to avoid printing a message if nothing needs doing
Simon Glassafbd2132022-07-11 19:04:08 -0600886
887 Returns:
888 bool: True if all is well, False if there were warnings
Simon Glass0c477b72022-07-11 19:04:04 -0600889 """
Simon Glass89592fc2023-09-07 10:00:18 -0600890 if not force:
Simon Glass0c477b72022-07-11 19:04:04 -0600891 if not quiet:
Simon Glass89592fc2023-09-07 10:00:18 -0600892 tprint('\rChecking for Kconfig changes...', newline=False)
893 is_new = output_is_new(output, CONFIG_DIR, '.')
894 print_clear()
895 if is_new:
896 if not quiet:
897 print(f'{output} is up to date. Nothing to do.')
898 return True
899 if not quiet:
900 tprint('\rGenerating board list...', newline=False)
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600901 params_list, warnings = self.build_board_list(CONFIG_DIR, '.', jobs)
Simon Glass89592fc2023-09-07 10:00:18 -0600902 print_clear()
Simon Glassafbd2132022-07-11 19:04:08 -0600903 for warn in warnings:
904 print(warn, file=sys.stderr)
Simon Glass0c477b72022-07-11 19:04:04 -0600905 self.format_and_output(params_list, output)
Simon Glassafbd2132022-07-11 19:04:08 -0600906 return not warnings