blob: e7aa0d85a583220ee015f0338e7dabd49cc2ad94 [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 ###
Simon Glass0c477b72022-07-11 19:04:04 -060028CONFIG_DIR = 'configs'
29SLEEP_TIME = 0.03
Simon Glass58d41be2022-07-11 19:04:05 -060030COMMENT_BLOCK = f'''#
Simon Glass0c477b72022-07-11 19:04:04 -060031# List of boards
Simon Glass58d41be2022-07-11 19:04:05 -060032# Automatically generated by {__file__}: don't edit
Simon Glass0c477b72022-07-11 19:04:04 -060033#
Simon Glassd2d4c602022-07-11 19:04:06 -060034# Status, Arch, CPU, SoC, Vendor, Board, Target, Config, Maintainers
Simon Glass0c477b72022-07-11 19:04:04 -060035
Simon Glass58d41be2022-07-11 19:04:05 -060036'''
Simon Glass0c477b72022-07-11 19:04:04 -060037
38
Simon Glass58d41be2022-07-11 19:04:05 -060039def try_remove(fname):
40 """Remove a file ignoring 'No such file or directory' error.
41
42 Args:
43 fname (str): Filename to remove
44
45 Raises:
46 OSError: output file exists but could not be removed
47 """
Simon Glass0c477b72022-07-11 19:04:04 -060048 try:
Simon Glass58d41be2022-07-11 19:04:05 -060049 os.remove(fname)
Simon Glass0c477b72022-07-11 19:04:04 -060050 except OSError as exception:
51 # Ignore 'No such file or directory' error
52 if exception.errno != errno.ENOENT:
53 raise
54
55
Simon Glasse1568362023-07-19 17:48:14 -060056def output_is_new(output, config_dir, srcdir):
Simon Glass0c477b72022-07-11 19:04:04 -060057 """Check if the output file is up to date.
58
Simon Glass58d41be2022-07-11 19:04:05 -060059 Looks at defconfig and Kconfig files to make sure none is newer than the
60 output file. Also ensures that the boards.cfg does not mention any removed
61 boards.
62
63 Args:
64 output (str): Filename to check
Simon Glasse1568362023-07-19 17:48:14 -060065 config_dir (str): Directory containing defconfig files
66 srcdir (str): Directory containing Kconfig and MAINTAINERS files
Simon Glass58d41be2022-07-11 19:04:05 -060067
Simon Glass0c477b72022-07-11 19:04:04 -060068 Returns:
Simon Glass58d41be2022-07-11 19:04:05 -060069 True if the given output file exists and is newer than any of
70 *_defconfig, MAINTAINERS and Kconfig*. False otherwise.
71
72 Raises:
73 OSError: output file exists but could not be opened
Simon Glass0c477b72022-07-11 19:04:04 -060074 """
Simon Glass58d41be2022-07-11 19:04:05 -060075 # pylint: disable=too-many-branches
Simon Glass0c477b72022-07-11 19:04:04 -060076 try:
77 ctime = os.path.getctime(output)
78 except OSError as exception:
79 if exception.errno == errno.ENOENT:
80 # return False on 'No such file or directory' error
81 return False
Simon Glass58d41be2022-07-11 19:04:05 -060082 raise
Simon Glass0c477b72022-07-11 19:04:04 -060083
Simon Glasse1568362023-07-19 17:48:14 -060084 for (dirpath, _, filenames) in os.walk(config_dir):
Simon Glass0c477b72022-07-11 19:04:04 -060085 for filename in fnmatch.filter(filenames, '*_defconfig'):
86 if fnmatch.fnmatch(filename, '.*'):
87 continue
88 filepath = os.path.join(dirpath, filename)
89 if ctime < os.path.getctime(filepath):
90 return False
91
Simon Glasse1568362023-07-19 17:48:14 -060092 for (dirpath, _, filenames) in os.walk(srcdir):
Simon Glass0c477b72022-07-11 19:04:04 -060093 for filename in filenames:
94 if (fnmatch.fnmatch(filename, '*~') or
95 not fnmatch.fnmatch(filename, 'Kconfig*') and
96 not filename == 'MAINTAINERS'):
97 continue
98 filepath = os.path.join(dirpath, filename)
99 if ctime < os.path.getctime(filepath):
100 return False
101
102 # Detect a board that has been removed since the current board database
103 # was generated
Simon Glass58d41be2022-07-11 19:04:05 -0600104 with open(output, encoding="utf-8") as inf:
105 for line in inf:
Simon Glassd2d4c602022-07-11 19:04:06 -0600106 if 'Options,' in line:
107 return False
Simon Glass0c477b72022-07-11 19:04:04 -0600108 if line[0] == '#' or line == '\n':
109 continue
110 defconfig = line.split()[6] + '_defconfig'
Simon Glasse1568362023-07-19 17:48:14 -0600111 if not os.path.exists(os.path.join(config_dir, defconfig)):
Simon Glass0c477b72022-07-11 19:04:04 -0600112 return False
113
114 return True
115
116
Simon Glass20751d62022-07-11 19:04:03 -0600117class Expr:
118 """A single regular expression for matching boards to build"""
119
120 def __init__(self, expr):
121 """Set up a new Expr object.
122
123 Args:
Heinrich Schuchardt3399d332023-12-15 10:42:30 +0100124 expr (str): String containing regular expression to store
Simon Glass20751d62022-07-11 19:04:03 -0600125 """
126 self._expr = expr
127 self._re = re.compile(expr)
128
129 def matches(self, props):
130 """Check if any of the properties match the regular expression.
131
132 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600133 props (list of str): List of properties to check
Simon Glass20751d62022-07-11 19:04:03 -0600134 Returns:
135 True if any of the properties match the regular expression
136 """
137 for prop in props:
138 if self._re.match(prop):
139 return True
140 return False
141
142 def __str__(self):
143 return self._expr
144
145class Term:
146 """A list of expressions each of which must match with properties.
147
148 This provides a list of 'AND' expressions, meaning that each must
149 match the board properties for that board to be built.
150 """
151 def __init__(self):
152 self._expr_list = []
153 self._board_count = 0
154
155 def add_expr(self, expr):
156 """Add an Expr object to the list to check.
157
158 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600159 expr (Expr): New Expr object to add to the list of those that must
Simon Glass20751d62022-07-11 19:04:03 -0600160 match for a board to be built.
161 """
162 self._expr_list.append(Expr(expr))
163
164 def __str__(self):
165 """Return some sort of useful string describing the term"""
166 return '&'.join([str(expr) for expr in self._expr_list])
167
168 def matches(self, props):
169 """Check if any of the properties match this term
170
171 Each of the expressions in the term is checked. All must match.
172
173 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600174 props (list of str): List of properties to check
Simon Glass20751d62022-07-11 19:04:03 -0600175 Returns:
176 True if all of the expressions in the Term match, else False
177 """
178 for expr in self._expr_list:
179 if not expr.matches(props):
180 return False
181 return True
182
Simon Glass0c477b72022-07-11 19:04:04 -0600183
184class KconfigScanner:
185
186 """Kconfig scanner."""
187
188 ### constant variable only used in this class ###
189 _SYMBOL_TABLE = {
190 'arch' : 'SYS_ARCH',
191 'cpu' : 'SYS_CPU',
192 'soc' : 'SYS_SOC',
193 'vendor' : 'SYS_VENDOR',
194 'board' : 'SYS_BOARD',
195 'config' : 'SYS_CONFIG_NAME',
Simon Glassd2d4c602022-07-11 19:04:06 -0600196 # 'target' is added later
Simon Glass0c477b72022-07-11 19:04:04 -0600197 }
198
Simon Glasse1568362023-07-19 17:48:14 -0600199 def __init__(self, srctree):
Simon Glass0c477b72022-07-11 19:04:04 -0600200 """Scan all the Kconfig files and create a Kconfig object."""
201 # Define environment variables referenced from Kconfig
Simon Glasse1568362023-07-19 17:48:14 -0600202 os.environ['srctree'] = srctree
Simon Glass0c477b72022-07-11 19:04:04 -0600203 os.environ['UBOOTVERSION'] = 'dummy'
204 os.environ['KCONFIG_OBJDIR'] = ''
Simon Glass58d41be2022-07-11 19:04:05 -0600205 self._tmpfile = None
Simon Glass0c477b72022-07-11 19:04:04 -0600206 self._conf = kconfiglib.Kconfig(warn=False)
Simon Glass62dc0942024-11-08 08:23:44 -0700207 self._srctree = srctree
Simon Glass0c477b72022-07-11 19:04:04 -0600208
209 def __del__(self):
210 """Delete a leftover temporary file before exit.
211
212 The scan() method of this class creates a temporay file and deletes
213 it on success. If scan() method throws an exception on the way,
214 the temporary file might be left over. In that case, it should be
215 deleted in this destructor.
216 """
Simon Glass58d41be2022-07-11 19:04:05 -0600217 if self._tmpfile:
Simon Glass0c477b72022-07-11 19:04:04 -0600218 try_remove(self._tmpfile)
219
Simon Glass5e728d42023-07-19 17:48:27 -0600220 def scan(self, defconfig, warn_targets):
Simon Glass0c477b72022-07-11 19:04:04 -0600221 """Load a defconfig file to obtain board parameters.
222
Simon Glass58d41be2022-07-11 19:04:05 -0600223 Args:
224 defconfig (str): path to the defconfig file to be processed
Simon Glass5e728d42023-07-19 17:48:27 -0600225 warn_targets (bool): True to warn about missing or duplicate
226 CONFIG_TARGET options
Simon Glass0c477b72022-07-11 19:04:04 -0600227
228 Returns:
Simon Glass07a95d82023-07-19 17:48:21 -0600229 tuple: dictionary of board parameters. It has a form of:
230 {
231 'arch': <arch_name>,
232 'cpu': <cpu_name>,
233 'soc': <soc_name>,
234 'vendor': <vendor_name>,
235 'board': <board_name>,
236 'target': <target_name>,
237 'config': <config_header_name>,
238 }
239 warnings (list of str): list of warnings found
Simon Glass0c477b72022-07-11 19:04:04 -0600240 """
Simon Glass7188e4b2023-07-19 17:48:20 -0600241 leaf = os.path.basename(defconfig)
242 expect_target, match, rear = leaf.partition('_defconfig')
243 assert match and not rear, f'{leaf} : invalid defconfig'
244
Simon Glass62dc0942024-11-08 08:23:44 -0700245 temp = None
246 if b'#include' in tools.read_file(defconfig):
247 cmd = [
248 os.getenv('CPP', 'cpp'),
249 '-nostdinc', '-P',
250 '-I', self._srctree,
251 '-undef',
252 '-x', 'assembler-with-cpp',
253 defconfig]
254 result = command.run_pipe([cmd], capture=True, capture_stderr=True)
255 temp = tempfile.NamedTemporaryFile(prefix='buildman-')
256 tools.write_file(temp.name, result.stdout, False)
257 fname = temp.name
258 tout.info(f'Processing #include to produce {defconfig}')
259 else:
260 fname = defconfig
261
262 self._conf.load_config(fname)
263 if temp:
264 del temp
Simon Glass0c477b72022-07-11 19:04:04 -0600265 self._tmpfile = None
266
267 params = {}
Simon Glass07a95d82023-07-19 17:48:21 -0600268 warnings = []
Simon Glass0c477b72022-07-11 19:04:04 -0600269
270 # Get the value of CONFIG_SYS_ARCH, CONFIG_SYS_CPU, ... etc.
271 # Set '-' if the value is empty.
272 for key, symbol in list(self._SYMBOL_TABLE.items()):
273 value = self._conf.syms.get(symbol).str_value
274 if value:
275 params[key] = value
276 else:
277 params[key] = '-'
278
Simon Glass07a95d82023-07-19 17:48:21 -0600279 # Check there is exactly one TARGET_xxx set
Simon Glass5e728d42023-07-19 17:48:27 -0600280 if warn_targets:
281 target = None
282 for name, sym in self._conf.syms.items():
283 if name.startswith('TARGET_') and sym.str_value == 'y':
284 tname = name[7:].lower()
285 if target:
286 warnings.append(
287 f'WARNING: {leaf}: Duplicate TARGET_xxx: {target} and {tname}')
288 else:
289 target = tname
Simon Glass07a95d82023-07-19 17:48:21 -0600290
Simon Glass5e728d42023-07-19 17:48:27 -0600291 if not target:
292 cfg_name = expect_target.replace('-', '_').upper()
293 warnings.append(f'WARNING: {leaf}: No TARGET_{cfg_name} enabled')
Simon Glass061499b2023-07-19 17:48:22 -0600294
Simon Glass7188e4b2023-07-19 17:48:20 -0600295 params['target'] = expect_target
Simon Glass0c477b72022-07-11 19:04:04 -0600296
297 # fix-up for aarch64
298 if params['arch'] == 'arm' and params['cpu'] == 'armv8':
299 params['arch'] = 'aarch64'
300
Heinrich Schuchardt8c7fd872022-10-03 18:07:53 +0200301 # fix-up for riscv
302 if params['arch'] == 'riscv':
303 try:
304 value = self._conf.syms.get('ARCH_RV32I').str_value
305 except:
306 value = ''
307 if value == 'y':
308 params['arch'] = 'riscv32'
309 else:
310 params['arch'] = 'riscv64'
311
Simon Glass07a95d82023-07-19 17:48:21 -0600312 return params, warnings
Simon Glass0c477b72022-07-11 19:04:04 -0600313
314
315class MaintainersDatabase:
316
Simon Glass58d41be2022-07-11 19:04:05 -0600317 """The database of board status and maintainers.
318
319 Properties:
320 database: dict:
321 key: Board-target name (e.g. 'snow')
322 value: tuple:
323 str: Board status (e.g. 'Active')
324 str: List of maintainers, separated by :
Simon Glassafbd2132022-07-11 19:04:08 -0600325 warnings (list of str): List of warnings due to missing status, etc.
Simon Glass58d41be2022-07-11 19:04:05 -0600326 """
Simon Glass0c477b72022-07-11 19:04:04 -0600327
328 def __init__(self):
329 """Create an empty database."""
330 self.database = {}
Simon Glassafbd2132022-07-11 19:04:08 -0600331 self.warnings = []
Simon Glass0c477b72022-07-11 19:04:04 -0600332
333 def get_status(self, target):
334 """Return the status of the given board.
335
336 The board status is generally either 'Active' or 'Orphan'.
337 Display a warning message and return '-' if status information
338 is not found.
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: 'Active', 'Orphan' or '-'.
Simon Glass0c477b72022-07-11 19:04:04 -0600345 """
346 if not target in self.database:
Simon Glassafbd2132022-07-11 19:04:08 -0600347 self.warnings.append(f"WARNING: no status info for '{target}'")
Simon Glass0c477b72022-07-11 19:04:04 -0600348 return '-'
349
350 tmp = self.database[target][0]
351 if tmp.startswith('Maintained'):
352 return 'Active'
Simon Glass58d41be2022-07-11 19:04:05 -0600353 if tmp.startswith('Supported'):
Simon Glass0c477b72022-07-11 19:04:04 -0600354 return 'Active'
Simon Glass58d41be2022-07-11 19:04:05 -0600355 if tmp.startswith('Orphan'):
Simon Glass0c477b72022-07-11 19:04:04 -0600356 return 'Orphan'
Simon Glassafbd2132022-07-11 19:04:08 -0600357 self.warnings.append(f"WARNING: {tmp}: unknown status for '{target}'")
Simon Glass58d41be2022-07-11 19:04:05 -0600358 return '-'
Simon Glass0c477b72022-07-11 19:04:04 -0600359
360 def get_maintainers(self, target):
361 """Return the maintainers of the given board.
362
Simon Glass58d41be2022-07-11 19:04:05 -0600363 Args:
364 target (str): Build-target name
365
Simon Glass0c477b72022-07-11 19:04:04 -0600366 Returns:
Simon Glass58d41be2022-07-11 19:04:05 -0600367 str: Maintainers of the board. If the board has two or more
368 maintainers, they are separated with colons.
Simon Glass0c477b72022-07-11 19:04:04 -0600369 """
Simon Glasse6acab52023-07-19 17:48:26 -0600370 entry = self.database.get(target)
371 if entry:
372 status, maint_list = entry
373 if not status.startswith('Orphan'):
374 if len(maint_list) > 1 or (maint_list and maint_list[0] != '-'):
375 return ':'.join(maint_list)
Simon Glass0c477b72022-07-11 19:04:04 -0600376
Simon Glasse6acab52023-07-19 17:48:26 -0600377 self.warnings.append(f"WARNING: no maintainers for '{target}'")
378 return ''
Simon Glass0c477b72022-07-11 19:04:04 -0600379
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600380 def parse_file(self, srcdir, fname):
Simon Glass0c477b72022-07-11 19:04:04 -0600381 """Parse a MAINTAINERS file.
382
Simon Glass58d41be2022-07-11 19:04:05 -0600383 Parse a MAINTAINERS file and accumulate board status and maintainers
384 information in the self.database dict.
Simon Glass0c477b72022-07-11 19:04:04 -0600385
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600386 defconfig files are used to specify the target, e.g. xxx_defconfig is
387 used for target 'xxx'. If there is no defconfig file mentioned in the
388 MAINTAINERS file F: entries, then this function does nothing.
389
390 The N: name entries can be used to specify a defconfig file using
391 wildcards.
392
Simon Glass58d41be2022-07-11 19:04:05 -0600393 Args:
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600394 srcdir (str): Directory containing source code (Kconfig files)
Simon Glass58d41be2022-07-11 19:04:05 -0600395 fname (str): MAINTAINERS file to be parsed
Simon Glass0c477b72022-07-11 19:04:04 -0600396 """
Simon Glass9b828ec2023-07-19 17:48:19 -0600397 def add_targets(linenum):
398 """Add any new targets
399
400 Args:
401 linenum (int): Current line number
402 """
Simon Glassb555e142023-07-19 17:48:18 -0600403 if targets:
404 for target in targets:
405 self.database[target] = (status, maintainers)
406
Simon Glass0c477b72022-07-11 19:04:04 -0600407 targets = []
408 maintainers = []
409 status = '-'
Simon Glass58d41be2022-07-11 19:04:05 -0600410 with open(fname, encoding="utf-8") as inf:
Simon Glass9b828ec2023-07-19 17:48:19 -0600411 for linenum, line in enumerate(inf):
Simon Glass58d41be2022-07-11 19:04:05 -0600412 # Check also commented maintainers
413 if line[:3] == '#M:':
414 line = line[1:]
415 tag, rest = line[:2], line[2:].strip()
416 if tag == 'M:':
417 maintainers.append(rest)
418 elif tag == 'F:':
419 # expand wildcard and filter by 'configs/*_defconfig'
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600420 glob_path = os.path.join(srcdir, rest)
421 for item in glob.glob(glob_path):
Simon Glass58d41be2022-07-11 19:04:05 -0600422 front, match, rear = item.partition('configs/')
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600423 if front.endswith('/'):
424 front = front[:-1]
425 if front == srcdir and match:
Simon Glass58d41be2022-07-11 19:04:05 -0600426 front, match, rear = rear.rpartition('_defconfig')
427 if match and not rear:
428 targets.append(front)
429 elif tag == 'S:':
430 status = rest
Simon Glass3a90a692022-10-11 08:15:37 -0600431 elif tag == 'N:':
432 # Just scan the configs directory since that's all we care
433 # about
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600434 walk_path = os.walk(os.path.join(srcdir, 'configs'))
435 for dirpath, _, fnames in walk_path:
436 for cfg in fnames:
Simon Glass060ee972023-07-19 17:48:23 -0600437 path = os.path.join(dirpath, cfg)[len(srcdir) + 1:]
Simon Glass3a90a692022-10-11 08:15:37 -0600438 front, match, rear = path.partition('configs/')
Simon Glass060ee972023-07-19 17:48:23 -0600439 if front or not match:
440 continue
441 front, match, rear = rear.rpartition('_defconfig')
442
443 # Use this entry if it matches the defconfig file
444 # without the _defconfig suffix. For example
445 # 'am335x.*' matches am335x_guardian_defconfig
446 if match and not rear and re.search(rest, front):
447 targets.append(front)
Simon Glass58d41be2022-07-11 19:04:05 -0600448 elif line == '\n':
Simon Glass9b828ec2023-07-19 17:48:19 -0600449 add_targets(linenum)
Simon Glass58d41be2022-07-11 19:04:05 -0600450 targets = []
451 maintainers = []
452 status = '-'
Simon Glass9b828ec2023-07-19 17:48:19 -0600453 add_targets(linenum)
Simon Glass0c477b72022-07-11 19:04:04 -0600454
Simon Glass20751d62022-07-11 19:04:03 -0600455
456class Boards:
457 """Manage a list of boards."""
458 def __init__(self):
Simon Glass20751d62022-07-11 19:04:03 -0600459 self._boards = []
460
461 def add_board(self, brd):
462 """Add a new board to the list.
463
464 The board's target member must not already exist in the board list.
465
466 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600467 brd (Board): board to add
Simon Glass20751d62022-07-11 19:04:03 -0600468 """
469 self._boards.append(brd)
470
471 def read_boards(self, fname):
472 """Read a list of boards from a board file.
473
474 Create a Board object for each and add it to our _boards list.
475
476 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600477 fname (str): Filename of boards.cfg file
Simon Glass20751d62022-07-11 19:04:03 -0600478 """
479 with open(fname, 'r', encoding='utf-8') as inf:
480 for line in inf:
481 if line[0] == '#':
482 continue
483 fields = line.split()
484 if not fields:
485 continue
486 for upto, field in enumerate(fields):
487 if field == '-':
488 fields[upto] = ''
489 while len(fields) < 8:
490 fields.append('')
491 if len(fields) > 8:
492 fields = fields[:8]
493
494 brd = board.Board(*fields)
495 self.add_board(brd)
496
497
498 def get_list(self):
499 """Return a list of available boards.
500
501 Returns:
502 List of Board objects
503 """
504 return self._boards
505
506 def get_dict(self):
507 """Build a dictionary containing all the boards.
508
509 Returns:
510 Dictionary:
511 key is board.target
512 value is board
513 """
514 board_dict = OrderedDict()
515 for brd in self._boards:
516 board_dict[brd.target] = brd
517 return board_dict
518
519 def get_selected_dict(self):
520 """Return a dictionary containing the selected boards
521
522 Returns:
523 List of Board objects that are marked selected
524 """
525 board_dict = OrderedDict()
526 for brd in self._boards:
527 if brd.build_it:
528 board_dict[brd.target] = brd
529 return board_dict
530
531 def get_selected(self):
532 """Return a list of selected boards
533
534 Returns:
535 List of Board objects that are marked selected
536 """
537 return [brd for brd in self._boards if brd.build_it]
538
539 def get_selected_names(self):
540 """Return a list of selected boards
541
542 Returns:
543 List of board names that are marked selected
544 """
545 return [brd.target for brd in self._boards if brd.build_it]
546
547 @classmethod
548 def _build_terms(cls, args):
549 """Convert command line arguments to a list of terms.
550
551 This deals with parsing of the arguments. It handles the '&'
552 operator, which joins several expressions into a single Term.
553
554 For example:
555 ['arm & freescale sandbox', 'tegra']
556
557 will produce 3 Terms containing expressions as follows:
558 arm, freescale
559 sandbox
560 tegra
561
562 The first Term has two expressions, both of which must match for
563 a board to be selected.
564
565 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600566 args (list of str): List of command line arguments
567
Simon Glass20751d62022-07-11 19:04:03 -0600568 Returns:
Simon Glass58d41be2022-07-11 19:04:05 -0600569 list of Term: A list of Term objects
Simon Glass20751d62022-07-11 19:04:03 -0600570 """
571 syms = []
572 for arg in args:
573 for word in arg.split():
574 sym_build = []
575 for term in word.split('&'):
576 if term:
577 sym_build.append(term)
578 sym_build.append('&')
579 syms += sym_build[:-1]
580 terms = []
581 term = None
582 oper = None
583 for sym in syms:
584 if sym == '&':
585 oper = sym
586 elif oper:
587 term.add_expr(sym)
588 oper = None
589 else:
590 if term:
591 terms.append(term)
592 term = Term()
593 term.add_expr(sym)
594 if term:
595 terms.append(term)
596 return terms
597
598 def select_boards(self, args, exclude=None, brds=None):
599 """Mark boards selected based on args
600
601 Normally either boards (an explicit list of boards) or args (a list of
602 terms to match against) is used. It is possible to specify both, in
603 which case they are additive.
604
605 If brds and args are both empty, all boards are selected.
606
607 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600608 args (list of str): List of strings specifying boards to include,
609 either named, or by their target, architecture, cpu, vendor or
610 soc. If empty, all boards are selected.
611 exclude (list of str): List of boards to exclude, regardless of
612 'args', or None for none
613 brds (list of Board): List of boards to build, or None/[] for all
Simon Glass20751d62022-07-11 19:04:03 -0600614
615 Returns:
616 Tuple
617 Dictionary which holds the list of boards which were selected
618 due to each argument, arranged by argument.
619 List of errors found
620 """
Simon Glass58d41be2022-07-11 19:04:05 -0600621 def _check_board(brd):
622 """Check whether to include or exclude a board
Simon Glass20751d62022-07-11 19:04:03 -0600623
Simon Glass58d41be2022-07-11 19:04:05 -0600624 Checks the various terms and decide whether to build it or not (the
625 'build_it' variable).
Simon Glass20751d62022-07-11 19:04:03 -0600626
Simon Glass58d41be2022-07-11 19:04:05 -0600627 If it is built, add the board to the result[term] list so we know
628 which term caused it to be built. Add it to result['all'] also.
Simon Glass20751d62022-07-11 19:04:03 -0600629
Simon Glass58d41be2022-07-11 19:04:05 -0600630 Keep a list of boards we found in 'found', so we can report boards
631 which appear in self._boards but not in brds.
632
633 Args:
634 brd (Board): Board to check
635 """
Simon Glass20751d62022-07-11 19:04:03 -0600636 matching_term = None
637 build_it = False
638 if terms:
639 for term in terms:
640 if term.matches(brd.props):
641 matching_term = str(term)
642 build_it = True
643 break
644 elif brds:
645 if brd.target in brds:
646 build_it = True
647 found.append(brd.target)
648 else:
649 build_it = True
650
651 # Check that it is not specifically excluded
652 for expr in exclude_list:
653 if expr.matches(brd.props):
654 build_it = False
655 break
656
657 if build_it:
658 brd.build_it = True
659 if matching_term:
660 result[matching_term].append(brd.target)
661 result['all'].append(brd.target)
662
Simon Glass58d41be2022-07-11 19:04:05 -0600663 result = OrderedDict()
664 warnings = []
665 terms = self._build_terms(args)
666
667 result['all'] = []
668 for term in terms:
669 result[str(term)] = []
670
671 exclude_list = []
672 if exclude:
673 for expr in exclude:
674 exclude_list.append(Expr(expr))
675
676 found = []
677 for brd in self._boards:
678 _check_board(brd)
679
Simon Glass20751d62022-07-11 19:04:03 -0600680 if brds:
681 remaining = set(brds) - set(found)
682 if remaining:
683 warnings.append(f"Boards not found: {', '.join(remaining)}\n")
684
685 return result, warnings
Simon Glass0c477b72022-07-11 19:04:04 -0600686
Simon Glass58d41be2022-07-11 19:04:05 -0600687 @classmethod
Simon Glass5e728d42023-07-19 17:48:27 -0600688 def scan_defconfigs_for_multiprocess(cls, srcdir, queue, defconfigs,
689 warn_targets):
Simon Glass0c477b72022-07-11 19:04:04 -0600690 """Scan defconfig files and queue their board parameters
691
Simon Glass58d41be2022-07-11 19:04:05 -0600692 This function is intended to be passed to multiprocessing.Process()
693 constructor.
Simon Glass0c477b72022-07-11 19:04:04 -0600694
Simon Glass58d41be2022-07-11 19:04:05 -0600695 Args:
Simon Glasse1568362023-07-19 17:48:14 -0600696 srcdir (str): Directory containing source code
Simon Glass58d41be2022-07-11 19:04:05 -0600697 queue (multiprocessing.Queue): The resulting board parameters are
698 written into this.
699 defconfigs (sequence of str): A sequence of defconfig files to be
700 scanned.
Simon Glass5e728d42023-07-19 17:48:27 -0600701 warn_targets (bool): True to warn about missing or duplicate
702 CONFIG_TARGET options
Simon Glass0c477b72022-07-11 19:04:04 -0600703 """
Simon Glasse1568362023-07-19 17:48:14 -0600704 kconf_scanner = KconfigScanner(srcdir)
Simon Glass0c477b72022-07-11 19:04:04 -0600705 for defconfig in defconfigs:
Simon Glass5e728d42023-07-19 17:48:27 -0600706 queue.put(kconf_scanner.scan(defconfig, warn_targets))
Simon Glass0c477b72022-07-11 19:04:04 -0600707
Simon Glass58d41be2022-07-11 19:04:05 -0600708 @classmethod
Simon Glass07a95d82023-07-19 17:48:21 -0600709 def read_queues(cls, queues, params_list, warnings):
710 """Read the queues and append the data to the paramers list
711
712 Args:
713 queues (list of multiprocessing.Queue): Queues to read
714 params_list (list of dict): List to add params too
715 warnings (set of str): Set to add warnings to
716 """
Simon Glass58d41be2022-07-11 19:04:05 -0600717 for que in queues:
718 while not que.empty():
Simon Glass07a95d82023-07-19 17:48:21 -0600719 params, warn = que.get()
720 params_list.append(params)
721 warnings.update(warn)
Simon Glass0c477b72022-07-11 19:04:04 -0600722
Simon Glass5e728d42023-07-19 17:48:27 -0600723 def scan_defconfigs(self, config_dir, srcdir, jobs=1, warn_targets=False):
Simon Glass0c477b72022-07-11 19:04:04 -0600724 """Collect board parameters for all defconfig files.
725
726 This function invokes multiple processes for faster processing.
727
Simon Glass58d41be2022-07-11 19:04:05 -0600728 Args:
Simon Glasse1568362023-07-19 17:48:14 -0600729 config_dir (str): Directory containing the defconfig files
730 srcdir (str): Directory containing source code (Kconfig files)
Simon Glass58d41be2022-07-11 19:04:05 -0600731 jobs (int): The number of jobs to run simultaneously
Simon Glass5e728d42023-07-19 17:48:27 -0600732 warn_targets (bool): True to warn about missing or duplicate
733 CONFIG_TARGET options
Simon Glasse1568362023-07-19 17:48:14 -0600734
735 Returns:
Simon Glass07a95d82023-07-19 17:48:21 -0600736 tuple:
737 list of dict: List of board parameters, each a dict:
738 key: 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
739 'config'
740 value: string value of the key
741 list of str: List of warnings recorded
Simon Glass0c477b72022-07-11 19:04:04 -0600742 """
743 all_defconfigs = []
Simon Glasse1568362023-07-19 17:48:14 -0600744 for (dirpath, _, filenames) in os.walk(config_dir):
Simon Glass0c477b72022-07-11 19:04:04 -0600745 for filename in fnmatch.filter(filenames, '*_defconfig'):
746 if fnmatch.fnmatch(filename, '.*'):
747 continue
748 all_defconfigs.append(os.path.join(dirpath, filename))
749
750 total_boards = len(all_defconfigs)
751 processes = []
752 queues = []
753 for i in range(jobs):
754 defconfigs = all_defconfigs[total_boards * i // jobs :
755 total_boards * (i + 1) // jobs]
Simon Glass58d41be2022-07-11 19:04:05 -0600756 que = multiprocessing.Queue(maxsize=-1)
757 proc = multiprocessing.Process(
Simon Glass0c477b72022-07-11 19:04:04 -0600758 target=self.scan_defconfigs_for_multiprocess,
Simon Glass5e728d42023-07-19 17:48:27 -0600759 args=(srcdir, que, defconfigs, warn_targets))
Simon Glass58d41be2022-07-11 19:04:05 -0600760 proc.start()
761 processes.append(proc)
762 queues.append(que)
Simon Glass0c477b72022-07-11 19:04:04 -0600763
Simon Glass07a95d82023-07-19 17:48:21 -0600764 # The resulting data should be accumulated to these lists
Simon Glass0c477b72022-07-11 19:04:04 -0600765 params_list = []
Simon Glass07a95d82023-07-19 17:48:21 -0600766 warnings = set()
Simon Glass0c477b72022-07-11 19:04:04 -0600767
768 # Data in the queues should be retrieved preriodically.
769 # Otherwise, the queues would become full and subprocesses would get stuck.
Simon Glass58d41be2022-07-11 19:04:05 -0600770 while any(p.is_alive() for p in processes):
Simon Glass07a95d82023-07-19 17:48:21 -0600771 self.read_queues(queues, params_list, warnings)
Simon Glass0c477b72022-07-11 19:04:04 -0600772 # sleep for a while until the queues are filled
773 time.sleep(SLEEP_TIME)
774
775 # Joining subprocesses just in case
776 # (All subprocesses should already have been finished)
Simon Glass58d41be2022-07-11 19:04:05 -0600777 for proc in processes:
778 proc.join()
Simon Glass0c477b72022-07-11 19:04:04 -0600779
780 # retrieve leftover data
Simon Glass07a95d82023-07-19 17:48:21 -0600781 self.read_queues(queues, params_list, warnings)
Simon Glass0c477b72022-07-11 19:04:04 -0600782
Simon Glass07a95d82023-07-19 17:48:21 -0600783 return params_list, sorted(list(warnings))
Simon Glass0c477b72022-07-11 19:04:04 -0600784
Simon Glass58d41be2022-07-11 19:04:05 -0600785 @classmethod
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600786 def insert_maintainers_info(cls, srcdir, params_list):
Simon Glass0c477b72022-07-11 19:04:04 -0600787 """Add Status and Maintainers information to the board parameters list.
788
Simon Glass58d41be2022-07-11 19:04:05 -0600789 Args:
790 params_list (list of dict): A list of the board parameters
Simon Glassafbd2132022-07-11 19:04:08 -0600791
792 Returns:
793 list of str: List of warnings collected due to missing status, etc.
Simon Glass0c477b72022-07-11 19:04:04 -0600794 """
795 database = MaintainersDatabase()
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600796 for (dirpath, _, filenames) in os.walk(srcdir):
Simon Glass263b5982023-07-19 17:48:25 -0600797 if 'MAINTAINERS' in filenames and 'tools/buildman' not in dirpath:
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600798 database.parse_file(srcdir,
799 os.path.join(dirpath, 'MAINTAINERS'))
Simon Glass0c477b72022-07-11 19:04:04 -0600800
801 for i, params in enumerate(params_list):
802 target = params['target']
Simon Glassee088aa2023-07-19 17:48:24 -0600803 maintainers = database.get_maintainers(target)
804 params['maintainers'] = maintainers
805 if maintainers:
806 params['status'] = database.get_status(target)
807 else:
808 params['status'] = '-'
Simon Glass0c477b72022-07-11 19:04:04 -0600809 params_list[i] = params
Simon Glass263b5982023-07-19 17:48:25 -0600810 return sorted(database.warnings)
Simon Glass0c477b72022-07-11 19:04:04 -0600811
Simon Glass58d41be2022-07-11 19:04:05 -0600812 @classmethod
813 def format_and_output(cls, params_list, output):
Simon Glass0c477b72022-07-11 19:04:04 -0600814 """Write board parameters into a file.
815
816 Columnate the board parameters, sort lines alphabetically,
817 and then write them to a file.
818
Simon Glass58d41be2022-07-11 19:04:05 -0600819 Args:
820 params_list (list of dict): The list of board parameters
821 output (str): The path to the output file
Simon Glass0c477b72022-07-11 19:04:04 -0600822 """
Simon Glass58d41be2022-07-11 19:04:05 -0600823 fields = ('status', 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
Simon Glassd2d4c602022-07-11 19:04:06 -0600824 'config', 'maintainers')
Simon Glass0c477b72022-07-11 19:04:04 -0600825
826 # First, decide the width of each column
Simon Glass58d41be2022-07-11 19:04:05 -0600827 max_length = {f: 0 for f in fields}
Simon Glass0c477b72022-07-11 19:04:04 -0600828 for params in params_list:
Simon Glass58d41be2022-07-11 19:04:05 -0600829 for field in fields:
830 max_length[field] = max(max_length[field], len(params[field]))
Simon Glass0c477b72022-07-11 19:04:04 -0600831
832 output_lines = []
833 for params in params_list:
834 line = ''
Simon Glass58d41be2022-07-11 19:04:05 -0600835 for field in fields:
Simon Glass0c477b72022-07-11 19:04:04 -0600836 # insert two spaces between fields like column -t would
Simon Glass58d41be2022-07-11 19:04:05 -0600837 line += ' ' + params[field].ljust(max_length[field])
Simon Glass0c477b72022-07-11 19:04:04 -0600838 output_lines.append(line.strip())
839
840 # ignore case when sorting
841 output_lines.sort(key=str.lower)
842
Simon Glass58d41be2022-07-11 19:04:05 -0600843 with open(output, 'w', encoding="utf-8") as outf:
844 outf.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n')
Simon Glass0c477b72022-07-11 19:04:04 -0600845
Simon Glass5e728d42023-07-19 17:48:27 -0600846 def build_board_list(self, config_dir=CONFIG_DIR, srcdir='.', jobs=1,
847 warn_targets=False):
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600848 """Generate a board-database file
849
850 This works by reading the Kconfig, then loading each board's defconfig
851 in to get the setting for each option. In particular, CONFIG_TARGET_xxx
852 is typically set by the defconfig, where xxx is the target to build.
853
854 Args:
855 config_dir (str): Directory containing the defconfig files
856 srcdir (str): Directory containing source code (Kconfig files)
857 jobs (int): The number of jobs to run simultaneously
Simon Glass5e728d42023-07-19 17:48:27 -0600858 warn_targets (bool): True to warn about missing or duplicate
859 CONFIG_TARGET options
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600860
861 Returns:
862 tuple:
863 list of dict: List of board parameters, each a dict:
864 key: 'arch', 'cpu', 'soc', 'vendor', 'board', 'config',
865 'target'
866 value: string value of the key
867 list of str: Warnings that came up
868 """
Simon Glass5e728d42023-07-19 17:48:27 -0600869 params_list, warnings = self.scan_defconfigs(config_dir, srcdir, jobs,
870 warn_targets)
Simon Glass07a95d82023-07-19 17:48:21 -0600871 m_warnings = self.insert_maintainers_info(srcdir, params_list)
872 return params_list, warnings + m_warnings
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600873
Simon Glass0c477b72022-07-11 19:04:04 -0600874 def ensure_board_list(self, output, jobs=1, force=False, quiet=False):
875 """Generate a board database file if needed.
876
Simon Glasse1568362023-07-19 17:48:14 -0600877 This is intended to check if Kconfig has changed since the boards.cfg
878 files was generated.
879
Simon Glass58d41be2022-07-11 19:04:05 -0600880 Args:
881 output (str): The name of the output file
882 jobs (int): The number of jobs to run simultaneously
883 force (bool): Force to generate the output even if it is new
884 quiet (bool): True to avoid printing a message if nothing needs doing
Simon Glassafbd2132022-07-11 19:04:08 -0600885
886 Returns:
887 bool: True if all is well, False if there were warnings
Simon Glass0c477b72022-07-11 19:04:04 -0600888 """
Simon Glass89592fc2023-09-07 10:00:18 -0600889 if not force:
Simon Glass0c477b72022-07-11 19:04:04 -0600890 if not quiet:
Simon Glass89592fc2023-09-07 10:00:18 -0600891 tprint('\rChecking for Kconfig changes...', newline=False)
892 is_new = output_is_new(output, CONFIG_DIR, '.')
893 print_clear()
894 if is_new:
895 if not quiet:
896 print(f'{output} is up to date. Nothing to do.')
897 return True
898 if not quiet:
899 tprint('\rGenerating board list...', newline=False)
Simon Glassc0b6fcc2023-07-19 17:48:17 -0600900 params_list, warnings = self.build_board_list(CONFIG_DIR, '.', jobs)
Simon Glass89592fc2023-09-07 10:00:18 -0600901 print_clear()
Simon Glassafbd2132022-07-11 19:04:08 -0600902 for warn in warnings:
903 print(warn, file=sys.stderr)
Simon Glass0c477b72022-07-11 19:04:04 -0600904 self.format_and_output(params_list, output)
Simon Glassafbd2132022-07-11 19:04:08 -0600905 return not warnings