blob: 341a5056dfd24574766afaf929b0ddc0468865c8 [file] [log] [blame]
Simon Glassc52bd222022-07-11 19:04:03 -06001# SPDX-License-Identifier: GPL-2.0+
2# Copyright (c) 2012 The Chromium OS Authors.
Simon Glassa8a01412022-07-11 19:04:04 -06003# Author: Simon Glass <sjg@chromium.org>
4# Author: Masahiro Yamada <yamada.m@jp.panasonic.com>
Simon Glassc52bd222022-07-11 19:04:03 -06005
6"""Maintains a list of boards and allows them to be selected"""
7
8from collections import OrderedDict
Simon Glassa8a01412022-07-11 19:04:04 -06009import errno
10import fnmatch
11import glob
12import multiprocessing
13import os
Simon Glassc52bd222022-07-11 19:04:03 -060014import re
Simon Glassa8a01412022-07-11 19:04:04 -060015import sys
16import tempfile
17import time
Simon Glassc52bd222022-07-11 19:04:03 -060018
19from buildman import board
Simon Glassa8a01412022-07-11 19:04:04 -060020from buildman import kconfiglib
21
Simon Glass283dcb62023-09-07 10:00:18 -060022from u_boot_pylib.terminal import print_clear, tprint
Simon Glassa8a01412022-07-11 19:04:04 -060023
24### constant variables ###
25OUTPUT_FILE = 'boards.cfg'
26CONFIG_DIR = 'configs'
27SLEEP_TIME = 0.03
Simon Glass969fd332022-07-11 19:04:05 -060028COMMENT_BLOCK = f'''#
Simon Glassa8a01412022-07-11 19:04:04 -060029# List of boards
Simon Glass969fd332022-07-11 19:04:05 -060030# Automatically generated by {__file__}: don't edit
Simon Glassa8a01412022-07-11 19:04:04 -060031#
Simon Glass256126c2022-07-11 19:04:06 -060032# Status, Arch, CPU, SoC, Vendor, Board, Target, Config, Maintainers
Simon Glassa8a01412022-07-11 19:04:04 -060033
Simon Glass969fd332022-07-11 19:04:05 -060034'''
Simon Glassa8a01412022-07-11 19:04:04 -060035
36
Simon Glass969fd332022-07-11 19:04:05 -060037def try_remove(fname):
38 """Remove a file ignoring 'No such file or directory' error.
39
40 Args:
41 fname (str): Filename to remove
42
43 Raises:
44 OSError: output file exists but could not be removed
45 """
Simon Glassa8a01412022-07-11 19:04:04 -060046 try:
Simon Glass969fd332022-07-11 19:04:05 -060047 os.remove(fname)
Simon Glassa8a01412022-07-11 19:04:04 -060048 except OSError as exception:
49 # Ignore 'No such file or directory' error
50 if exception.errno != errno.ENOENT:
51 raise
52
53
Simon Glassb27e9892023-07-19 17:48:14 -060054def output_is_new(output, config_dir, srcdir):
Simon Glassa8a01412022-07-11 19:04:04 -060055 """Check if the output file is up to date.
56
Simon Glass969fd332022-07-11 19:04:05 -060057 Looks at defconfig and Kconfig files to make sure none is newer than the
58 output file. Also ensures that the boards.cfg does not mention any removed
59 boards.
60
61 Args:
62 output (str): Filename to check
Simon Glassb27e9892023-07-19 17:48:14 -060063 config_dir (str): Directory containing defconfig files
64 srcdir (str): Directory containing Kconfig and MAINTAINERS files
Simon Glass969fd332022-07-11 19:04:05 -060065
Simon Glassa8a01412022-07-11 19:04:04 -060066 Returns:
Simon Glass969fd332022-07-11 19:04:05 -060067 True if the given output file exists and is newer than any of
68 *_defconfig, MAINTAINERS and Kconfig*. False otherwise.
69
70 Raises:
71 OSError: output file exists but could not be opened
Simon Glassa8a01412022-07-11 19:04:04 -060072 """
Simon Glass969fd332022-07-11 19:04:05 -060073 # pylint: disable=too-many-branches
Simon Glassa8a01412022-07-11 19:04:04 -060074 try:
75 ctime = os.path.getctime(output)
76 except OSError as exception:
77 if exception.errno == errno.ENOENT:
78 # return False on 'No such file or directory' error
79 return False
Simon Glass969fd332022-07-11 19:04:05 -060080 raise
Simon Glassa8a01412022-07-11 19:04:04 -060081
Simon Glassb27e9892023-07-19 17:48:14 -060082 for (dirpath, _, filenames) in os.walk(config_dir):
Simon Glassa8a01412022-07-11 19:04:04 -060083 for filename in fnmatch.filter(filenames, '*_defconfig'):
84 if fnmatch.fnmatch(filename, '.*'):
85 continue
86 filepath = os.path.join(dirpath, filename)
87 if ctime < os.path.getctime(filepath):
88 return False
89
Simon Glassb27e9892023-07-19 17:48:14 -060090 for (dirpath, _, filenames) in os.walk(srcdir):
Simon Glassa8a01412022-07-11 19:04:04 -060091 for filename in filenames:
92 if (fnmatch.fnmatch(filename, '*~') or
93 not fnmatch.fnmatch(filename, 'Kconfig*') and
94 not filename == 'MAINTAINERS'):
95 continue
96 filepath = os.path.join(dirpath, filename)
97 if ctime < os.path.getctime(filepath):
98 return False
99
100 # Detect a board that has been removed since the current board database
101 # was generated
Simon Glass969fd332022-07-11 19:04:05 -0600102 with open(output, encoding="utf-8") as inf:
103 for line in inf:
Simon Glass256126c2022-07-11 19:04:06 -0600104 if 'Options,' in line:
105 return False
Simon Glassa8a01412022-07-11 19:04:04 -0600106 if line[0] == '#' or line == '\n':
107 continue
108 defconfig = line.split()[6] + '_defconfig'
Simon Glassb27e9892023-07-19 17:48:14 -0600109 if not os.path.exists(os.path.join(config_dir, defconfig)):
Simon Glassa8a01412022-07-11 19:04:04 -0600110 return False
111
112 return True
Simon Glassc52bd222022-07-11 19:04:03 -0600113
114
115class Expr:
116 """A single regular expression for matching boards to build"""
117
118 def __init__(self, expr):
119 """Set up a new Expr object.
120
121 Args:
Simon Glass969fd332022-07-11 19:04:05 -0600122 expr (str): String cotaining regular expression to store
Simon Glassc52bd222022-07-11 19:04:03 -0600123 """
124 self._expr = expr
125 self._re = re.compile(expr)
126
127 def matches(self, props):
128 """Check if any of the properties match the regular expression.
129
130 Args:
Simon Glass969fd332022-07-11 19:04:05 -0600131 props (list of str): List of properties to check
Simon Glassc52bd222022-07-11 19:04:03 -0600132 Returns:
133 True if any of the properties match the regular expression
134 """
135 for prop in props:
136 if self._re.match(prop):
137 return True
138 return False
139
140 def __str__(self):
141 return self._expr
142
143class Term:
144 """A list of expressions each of which must match with properties.
145
146 This provides a list of 'AND' expressions, meaning that each must
147 match the board properties for that board to be built.
148 """
149 def __init__(self):
150 self._expr_list = []
151 self._board_count = 0
152
153 def add_expr(self, expr):
154 """Add an Expr object to the list to check.
155
156 Args:
Simon Glass969fd332022-07-11 19:04:05 -0600157 expr (Expr): New Expr object to add to the list of those that must
Simon Glassc52bd222022-07-11 19:04:03 -0600158 match for a board to be built.
159 """
160 self._expr_list.append(Expr(expr))
161
162 def __str__(self):
163 """Return some sort of useful string describing the term"""
164 return '&'.join([str(expr) for expr in self._expr_list])
165
166 def matches(self, props):
167 """Check if any of the properties match this term
168
169 Each of the expressions in the term is checked. All must match.
170
171 Args:
Simon Glass969fd332022-07-11 19:04:05 -0600172 props (list of str): List of properties to check
Simon Glassc52bd222022-07-11 19:04:03 -0600173 Returns:
174 True if all of the expressions in the Term match, else False
175 """
176 for expr in self._expr_list:
177 if not expr.matches(props):
178 return False
179 return True
180
181
Simon Glassa8a01412022-07-11 19:04:04 -0600182class KconfigScanner:
183
184 """Kconfig scanner."""
185
186 ### constant variable only used in this class ###
187 _SYMBOL_TABLE = {
188 'arch' : 'SYS_ARCH',
189 'cpu' : 'SYS_CPU',
190 'soc' : 'SYS_SOC',
191 'vendor' : 'SYS_VENDOR',
192 'board' : 'SYS_BOARD',
193 'config' : 'SYS_CONFIG_NAME',
Simon Glass256126c2022-07-11 19:04:06 -0600194 # 'target' is added later
Simon Glassa8a01412022-07-11 19:04:04 -0600195 }
196
Simon Glassb27e9892023-07-19 17:48:14 -0600197 def __init__(self, srctree):
Simon Glassa8a01412022-07-11 19:04:04 -0600198 """Scan all the Kconfig files and create a Kconfig object."""
199 # Define environment variables referenced from Kconfig
Simon Glassb27e9892023-07-19 17:48:14 -0600200 os.environ['srctree'] = srctree
Simon Glassa8a01412022-07-11 19:04:04 -0600201 os.environ['UBOOTVERSION'] = 'dummy'
202 os.environ['KCONFIG_OBJDIR'] = ''
Simon Glass969fd332022-07-11 19:04:05 -0600203 self._tmpfile = None
Simon Glassa8a01412022-07-11 19:04:04 -0600204 self._conf = kconfiglib.Kconfig(warn=False)
205
206 def __del__(self):
207 """Delete a leftover temporary file before exit.
208
209 The scan() method of this class creates a temporay file and deletes
210 it on success. If scan() method throws an exception on the way,
211 the temporary file might be left over. In that case, it should be
212 deleted in this destructor.
213 """
Simon Glass969fd332022-07-11 19:04:05 -0600214 if self._tmpfile:
Simon Glassa8a01412022-07-11 19:04:04 -0600215 try_remove(self._tmpfile)
216
Simon Glass1b218422023-07-19 17:48:27 -0600217 def scan(self, defconfig, warn_targets):
Simon Glassa8a01412022-07-11 19:04:04 -0600218 """Load a defconfig file to obtain board parameters.
219
Simon Glass969fd332022-07-11 19:04:05 -0600220 Args:
221 defconfig (str): path to the defconfig file to be processed
Simon Glass1b218422023-07-19 17:48:27 -0600222 warn_targets (bool): True to warn about missing or duplicate
223 CONFIG_TARGET options
Simon Glassa8a01412022-07-11 19:04:04 -0600224
225 Returns:
Simon Glassbec06ed2023-07-19 17:48:21 -0600226 tuple: dictionary of board parameters. It has a form of:
227 {
228 'arch': <arch_name>,
229 'cpu': <cpu_name>,
230 'soc': <soc_name>,
231 'vendor': <vendor_name>,
232 'board': <board_name>,
233 'target': <target_name>,
234 'config': <config_header_name>,
235 }
236 warnings (list of str): list of warnings found
Simon Glassa8a01412022-07-11 19:04:04 -0600237 """
Simon Glassf99f34b2023-07-19 17:48:20 -0600238 leaf = os.path.basename(defconfig)
239 expect_target, match, rear = leaf.partition('_defconfig')
240 assert match and not rear, f'{leaf} : invalid defconfig'
241
Simon Glass6a754c62023-07-19 17:48:13 -0600242 self._conf.load_config(defconfig)
Simon Glassa8a01412022-07-11 19:04:04 -0600243 self._tmpfile = None
244
245 params = {}
Simon Glassbec06ed2023-07-19 17:48:21 -0600246 warnings = []
Simon Glassa8a01412022-07-11 19:04:04 -0600247
248 # Get the value of CONFIG_SYS_ARCH, CONFIG_SYS_CPU, ... etc.
249 # Set '-' if the value is empty.
250 for key, symbol in list(self._SYMBOL_TABLE.items()):
251 value = self._conf.syms.get(symbol).str_value
252 if value:
253 params[key] = value
254 else:
255 params[key] = '-'
256
Simon Glassbec06ed2023-07-19 17:48:21 -0600257 # Check there is exactly one TARGET_xxx set
Simon Glass1b218422023-07-19 17:48:27 -0600258 if warn_targets:
259 target = None
260 for name, sym in self._conf.syms.items():
261 if name.startswith('TARGET_') and sym.str_value == 'y':
262 tname = name[7:].lower()
263 if target:
264 warnings.append(
265 f'WARNING: {leaf}: Duplicate TARGET_xxx: {target} and {tname}')
266 else:
267 target = tname
Simon Glassbec06ed2023-07-19 17:48:21 -0600268
Simon Glass1b218422023-07-19 17:48:27 -0600269 if not target:
270 cfg_name = expect_target.replace('-', '_').upper()
271 warnings.append(f'WARNING: {leaf}: No TARGET_{cfg_name} enabled')
Simon Glassad995992023-07-19 17:48:22 -0600272
Simon Glassf99f34b2023-07-19 17:48:20 -0600273 params['target'] = expect_target
Simon Glassa8a01412022-07-11 19:04:04 -0600274
275 # fix-up for aarch64
276 if params['arch'] == 'arm' and params['cpu'] == 'armv8':
277 params['arch'] = 'aarch64'
278
Heinrich Schuchardt3672ed72022-10-03 18:07:53 +0200279 # fix-up for riscv
280 if params['arch'] == 'riscv':
281 try:
282 value = self._conf.syms.get('ARCH_RV32I').str_value
283 except:
284 value = ''
285 if value == 'y':
286 params['arch'] = 'riscv32'
287 else:
288 params['arch'] = 'riscv64'
289
Simon Glassbec06ed2023-07-19 17:48:21 -0600290 return params, warnings
Simon Glassa8a01412022-07-11 19:04:04 -0600291
292
293class MaintainersDatabase:
294
Simon Glass969fd332022-07-11 19:04:05 -0600295 """The database of board status and maintainers.
296
297 Properties:
298 database: dict:
299 key: Board-target name (e.g. 'snow')
300 value: tuple:
301 str: Board status (e.g. 'Active')
302 str: List of maintainers, separated by :
Simon Glassadd76e72022-07-11 19:04:08 -0600303 warnings (list of str): List of warnings due to missing status, etc.
Simon Glass969fd332022-07-11 19:04:05 -0600304 """
Simon Glassa8a01412022-07-11 19:04:04 -0600305
306 def __init__(self):
307 """Create an empty database."""
308 self.database = {}
Simon Glassadd76e72022-07-11 19:04:08 -0600309 self.warnings = []
Simon Glassa8a01412022-07-11 19:04:04 -0600310
311 def get_status(self, target):
312 """Return the status of the given board.
313
314 The board status is generally either 'Active' or 'Orphan'.
315 Display a warning message and return '-' if status information
316 is not found.
317
Simon Glass969fd332022-07-11 19:04:05 -0600318 Args:
319 target (str): Build-target name
320
Simon Glassa8a01412022-07-11 19:04:04 -0600321 Returns:
Simon Glass969fd332022-07-11 19:04:05 -0600322 str: 'Active', 'Orphan' or '-'.
Simon Glassa8a01412022-07-11 19:04:04 -0600323 """
324 if not target in self.database:
Simon Glassadd76e72022-07-11 19:04:08 -0600325 self.warnings.append(f"WARNING: no status info for '{target}'")
Simon Glassa8a01412022-07-11 19:04:04 -0600326 return '-'
327
328 tmp = self.database[target][0]
329 if tmp.startswith('Maintained'):
330 return 'Active'
Simon Glass969fd332022-07-11 19:04:05 -0600331 if tmp.startswith('Supported'):
Simon Glassa8a01412022-07-11 19:04:04 -0600332 return 'Active'
Simon Glass969fd332022-07-11 19:04:05 -0600333 if tmp.startswith('Orphan'):
Simon Glassa8a01412022-07-11 19:04:04 -0600334 return 'Orphan'
Simon Glassadd76e72022-07-11 19:04:08 -0600335 self.warnings.append(f"WARNING: {tmp}: unknown status for '{target}'")
Simon Glass969fd332022-07-11 19:04:05 -0600336 return '-'
Simon Glassa8a01412022-07-11 19:04:04 -0600337
338 def get_maintainers(self, target):
339 """Return the maintainers of the given board.
340
Simon Glass969fd332022-07-11 19:04:05 -0600341 Args:
342 target (str): Build-target name
343
Simon Glassa8a01412022-07-11 19:04:04 -0600344 Returns:
Simon Glass969fd332022-07-11 19:04:05 -0600345 str: Maintainers of the board. If the board has two or more
346 maintainers, they are separated with colons.
Simon Glassa8a01412022-07-11 19:04:04 -0600347 """
Simon Glass9a7cc812023-07-19 17:48:26 -0600348 entry = self.database.get(target)
349 if entry:
350 status, maint_list = entry
351 if not status.startswith('Orphan'):
352 if len(maint_list) > 1 or (maint_list and maint_list[0] != '-'):
353 return ':'.join(maint_list)
Simon Glassa8a01412022-07-11 19:04:04 -0600354
Simon Glass9a7cc812023-07-19 17:48:26 -0600355 self.warnings.append(f"WARNING: no maintainers for '{target}'")
356 return ''
Simon Glassa8a01412022-07-11 19:04:04 -0600357
Simon Glass5df95cf2023-07-19 17:48:17 -0600358 def parse_file(self, srcdir, fname):
Simon Glassa8a01412022-07-11 19:04:04 -0600359 """Parse a MAINTAINERS file.
360
Simon Glass969fd332022-07-11 19:04:05 -0600361 Parse a MAINTAINERS file and accumulate board status and maintainers
362 information in the self.database dict.
Simon Glassa8a01412022-07-11 19:04:04 -0600363
Simon Glass5df95cf2023-07-19 17:48:17 -0600364 defconfig files are used to specify the target, e.g. xxx_defconfig is
365 used for target 'xxx'. If there is no defconfig file mentioned in the
366 MAINTAINERS file F: entries, then this function does nothing.
367
368 The N: name entries can be used to specify a defconfig file using
369 wildcards.
370
Simon Glass969fd332022-07-11 19:04:05 -0600371 Args:
Simon Glass5df95cf2023-07-19 17:48:17 -0600372 srcdir (str): Directory containing source code (Kconfig files)
Simon Glass969fd332022-07-11 19:04:05 -0600373 fname (str): MAINTAINERS file to be parsed
Simon Glassa8a01412022-07-11 19:04:04 -0600374 """
Simon Glassbc12d032023-07-19 17:48:19 -0600375 def add_targets(linenum):
376 """Add any new targets
377
378 Args:
379 linenum (int): Current line number
380 """
Simon Glassa114c612023-07-19 17:48:18 -0600381 if targets:
382 for target in targets:
383 self.database[target] = (status, maintainers)
384
Simon Glassa8a01412022-07-11 19:04:04 -0600385 targets = []
386 maintainers = []
387 status = '-'
Simon Glass969fd332022-07-11 19:04:05 -0600388 with open(fname, encoding="utf-8") as inf:
Simon Glassbc12d032023-07-19 17:48:19 -0600389 for linenum, line in enumerate(inf):
Simon Glass969fd332022-07-11 19:04:05 -0600390 # Check also commented maintainers
391 if line[:3] == '#M:':
392 line = line[1:]
393 tag, rest = line[:2], line[2:].strip()
394 if tag == 'M:':
395 maintainers.append(rest)
396 elif tag == 'F:':
397 # expand wildcard and filter by 'configs/*_defconfig'
Simon Glass5df95cf2023-07-19 17:48:17 -0600398 glob_path = os.path.join(srcdir, rest)
399 for item in glob.glob(glob_path):
Simon Glass969fd332022-07-11 19:04:05 -0600400 front, match, rear = item.partition('configs/')
Simon Glass5df95cf2023-07-19 17:48:17 -0600401 if front.endswith('/'):
402 front = front[:-1]
403 if front == srcdir and match:
Simon Glass969fd332022-07-11 19:04:05 -0600404 front, match, rear = rear.rpartition('_defconfig')
405 if match and not rear:
406 targets.append(front)
407 elif tag == 'S:':
408 status = rest
Simon Glasse8da1da2022-10-11 08:15:37 -0600409 elif tag == 'N:':
410 # Just scan the configs directory since that's all we care
411 # about
Simon Glass5df95cf2023-07-19 17:48:17 -0600412 walk_path = os.walk(os.path.join(srcdir, 'configs'))
413 for dirpath, _, fnames in walk_path:
414 for cfg in fnames:
Simon Glassc6491532023-07-19 17:48:23 -0600415 path = os.path.join(dirpath, cfg)[len(srcdir) + 1:]
Simon Glasse8da1da2022-10-11 08:15:37 -0600416 front, match, rear = path.partition('configs/')
Simon Glassc6491532023-07-19 17:48:23 -0600417 if front or not match:
418 continue
419 front, match, rear = rear.rpartition('_defconfig')
420
421 # Use this entry if it matches the defconfig file
422 # without the _defconfig suffix. For example
423 # 'am335x.*' matches am335x_guardian_defconfig
424 if match and not rear and re.search(rest, front):
425 targets.append(front)
Simon Glass969fd332022-07-11 19:04:05 -0600426 elif line == '\n':
Simon Glassbc12d032023-07-19 17:48:19 -0600427 add_targets(linenum)
Simon Glass969fd332022-07-11 19:04:05 -0600428 targets = []
429 maintainers = []
430 status = '-'
Simon Glassbc12d032023-07-19 17:48:19 -0600431 add_targets(linenum)
Simon Glassa8a01412022-07-11 19:04:04 -0600432
433
Simon Glassc52bd222022-07-11 19:04:03 -0600434class Boards:
435 """Manage a list of boards."""
436 def __init__(self):
Simon Glassc52bd222022-07-11 19:04:03 -0600437 self._boards = []
438
439 def add_board(self, brd):
440 """Add a new board to the list.
441
442 The board's target member must not already exist in the board list.
443
444 Args:
Simon Glass969fd332022-07-11 19:04:05 -0600445 brd (Board): board to add
Simon Glassc52bd222022-07-11 19:04:03 -0600446 """
447 self._boards.append(brd)
448
449 def read_boards(self, fname):
450 """Read a list of boards from a board file.
451
452 Create a Board object for each and add it to our _boards list.
453
454 Args:
Simon Glass969fd332022-07-11 19:04:05 -0600455 fname (str): Filename of boards.cfg file
Simon Glassc52bd222022-07-11 19:04:03 -0600456 """
457 with open(fname, 'r', encoding='utf-8') as inf:
458 for line in inf:
459 if line[0] == '#':
460 continue
461 fields = line.split()
462 if not fields:
463 continue
464 for upto, field in enumerate(fields):
465 if field == '-':
466 fields[upto] = ''
467 while len(fields) < 8:
468 fields.append('')
469 if len(fields) > 8:
470 fields = fields[:8]
471
472 brd = board.Board(*fields)
473 self.add_board(brd)
474
475
476 def get_list(self):
477 """Return a list of available boards.
478
479 Returns:
480 List of Board objects
481 """
482 return self._boards
483
484 def get_dict(self):
485 """Build a dictionary containing all the boards.
486
487 Returns:
488 Dictionary:
489 key is board.target
490 value is board
491 """
492 board_dict = OrderedDict()
493 for brd in self._boards:
494 board_dict[brd.target] = brd
495 return board_dict
496
497 def get_selected_dict(self):
498 """Return a dictionary containing the selected boards
499
500 Returns:
501 List of Board objects that are marked selected
502 """
503 board_dict = OrderedDict()
504 for brd in self._boards:
505 if brd.build_it:
506 board_dict[brd.target] = brd
507 return board_dict
508
509 def get_selected(self):
510 """Return a list of selected boards
511
512 Returns:
513 List of Board objects that are marked selected
514 """
515 return [brd for brd in self._boards if brd.build_it]
516
517 def get_selected_names(self):
518 """Return a list of selected boards
519
520 Returns:
521 List of board names that are marked selected
522 """
523 return [brd.target for brd in self._boards if brd.build_it]
524
525 @classmethod
526 def _build_terms(cls, args):
527 """Convert command line arguments to a list of terms.
528
529 This deals with parsing of the arguments. It handles the '&'
530 operator, which joins several expressions into a single Term.
531
532 For example:
533 ['arm & freescale sandbox', 'tegra']
534
535 will produce 3 Terms containing expressions as follows:
536 arm, freescale
537 sandbox
538 tegra
539
540 The first Term has two expressions, both of which must match for
541 a board to be selected.
542
543 Args:
Simon Glass969fd332022-07-11 19:04:05 -0600544 args (list of str): List of command line arguments
545
Simon Glassc52bd222022-07-11 19:04:03 -0600546 Returns:
Simon Glass969fd332022-07-11 19:04:05 -0600547 list of Term: A list of Term objects
Simon Glassc52bd222022-07-11 19:04:03 -0600548 """
549 syms = []
550 for arg in args:
551 for word in arg.split():
552 sym_build = []
553 for term in word.split('&'):
554 if term:
555 sym_build.append(term)
556 sym_build.append('&')
557 syms += sym_build[:-1]
558 terms = []
559 term = None
560 oper = None
561 for sym in syms:
562 if sym == '&':
563 oper = sym
564 elif oper:
565 term.add_expr(sym)
566 oper = None
567 else:
568 if term:
569 terms.append(term)
570 term = Term()
571 term.add_expr(sym)
572 if term:
573 terms.append(term)
574 return terms
575
576 def select_boards(self, args, exclude=None, brds=None):
577 """Mark boards selected based on args
578
579 Normally either boards (an explicit list of boards) or args (a list of
580 terms to match against) is used. It is possible to specify both, in
581 which case they are additive.
582
583 If brds and args are both empty, all boards are selected.
584
585 Args:
Simon Glass969fd332022-07-11 19:04:05 -0600586 args (list of str): List of strings specifying boards to include,
587 either named, or by their target, architecture, cpu, vendor or
588 soc. If empty, all boards are selected.
589 exclude (list of str): List of boards to exclude, regardless of
590 'args', or None for none
591 brds (list of Board): List of boards to build, or None/[] for all
Simon Glassc52bd222022-07-11 19:04:03 -0600592
593 Returns:
594 Tuple
595 Dictionary which holds the list of boards which were selected
596 due to each argument, arranged by argument.
597 List of errors found
598 """
Simon Glass969fd332022-07-11 19:04:05 -0600599 def _check_board(brd):
600 """Check whether to include or exclude a board
Simon Glassc52bd222022-07-11 19:04:03 -0600601
Simon Glass969fd332022-07-11 19:04:05 -0600602 Checks the various terms and decide whether to build it or not (the
603 'build_it' variable).
Simon Glassc52bd222022-07-11 19:04:03 -0600604
Simon Glass969fd332022-07-11 19:04:05 -0600605 If it is built, add the board to the result[term] list so we know
606 which term caused it to be built. Add it to result['all'] also.
Simon Glassc52bd222022-07-11 19:04:03 -0600607
Simon Glass969fd332022-07-11 19:04:05 -0600608 Keep a list of boards we found in 'found', so we can report boards
609 which appear in self._boards but not in brds.
610
611 Args:
612 brd (Board): Board to check
613 """
Simon Glassc52bd222022-07-11 19:04:03 -0600614 matching_term = None
615 build_it = False
616 if terms:
617 for term in terms:
618 if term.matches(brd.props):
619 matching_term = str(term)
620 build_it = True
621 break
622 elif brds:
623 if brd.target in brds:
624 build_it = True
625 found.append(brd.target)
626 else:
627 build_it = True
628
629 # Check that it is not specifically excluded
630 for expr in exclude_list:
631 if expr.matches(brd.props):
632 build_it = False
633 break
634
635 if build_it:
636 brd.build_it = True
637 if matching_term:
638 result[matching_term].append(brd.target)
639 result['all'].append(brd.target)
640
Simon Glass969fd332022-07-11 19:04:05 -0600641 result = OrderedDict()
642 warnings = []
643 terms = self._build_terms(args)
644
645 result['all'] = []
646 for term in terms:
647 result[str(term)] = []
648
649 exclude_list = []
650 if exclude:
651 for expr in exclude:
652 exclude_list.append(Expr(expr))
653
654 found = []
655 for brd in self._boards:
656 _check_board(brd)
657
Simon Glassc52bd222022-07-11 19:04:03 -0600658 if brds:
659 remaining = set(brds) - set(found)
660 if remaining:
661 warnings.append(f"Boards not found: {', '.join(remaining)}\n")
662
663 return result, warnings
Simon Glassa8a01412022-07-11 19:04:04 -0600664
Simon Glass969fd332022-07-11 19:04:05 -0600665 @classmethod
Simon Glass1b218422023-07-19 17:48:27 -0600666 def scan_defconfigs_for_multiprocess(cls, srcdir, queue, defconfigs,
667 warn_targets):
Simon Glassa8a01412022-07-11 19:04:04 -0600668 """Scan defconfig files and queue their board parameters
669
Simon Glass969fd332022-07-11 19:04:05 -0600670 This function is intended to be passed to multiprocessing.Process()
671 constructor.
Simon Glassa8a01412022-07-11 19:04:04 -0600672
Simon Glass969fd332022-07-11 19:04:05 -0600673 Args:
Simon Glassb27e9892023-07-19 17:48:14 -0600674 srcdir (str): Directory containing source code
Simon Glass969fd332022-07-11 19:04:05 -0600675 queue (multiprocessing.Queue): The resulting board parameters are
676 written into this.
677 defconfigs (sequence of str): A sequence of defconfig files to be
678 scanned.
Simon Glass1b218422023-07-19 17:48:27 -0600679 warn_targets (bool): True to warn about missing or duplicate
680 CONFIG_TARGET options
Simon Glassa8a01412022-07-11 19:04:04 -0600681 """
Simon Glassb27e9892023-07-19 17:48:14 -0600682 kconf_scanner = KconfigScanner(srcdir)
Simon Glassa8a01412022-07-11 19:04:04 -0600683 for defconfig in defconfigs:
Simon Glass1b218422023-07-19 17:48:27 -0600684 queue.put(kconf_scanner.scan(defconfig, warn_targets))
Simon Glassa8a01412022-07-11 19:04:04 -0600685
Simon Glass969fd332022-07-11 19:04:05 -0600686 @classmethod
Simon Glassbec06ed2023-07-19 17:48:21 -0600687 def read_queues(cls, queues, params_list, warnings):
688 """Read the queues and append the data to the paramers list
689
690 Args:
691 queues (list of multiprocessing.Queue): Queues to read
692 params_list (list of dict): List to add params too
693 warnings (set of str): Set to add warnings to
694 """
Simon Glass969fd332022-07-11 19:04:05 -0600695 for que in queues:
696 while not que.empty():
Simon Glassbec06ed2023-07-19 17:48:21 -0600697 params, warn = que.get()
698 params_list.append(params)
699 warnings.update(warn)
Simon Glassa8a01412022-07-11 19:04:04 -0600700
Simon Glass1b218422023-07-19 17:48:27 -0600701 def scan_defconfigs(self, config_dir, srcdir, jobs=1, warn_targets=False):
Simon Glassa8a01412022-07-11 19:04:04 -0600702 """Collect board parameters for all defconfig files.
703
704 This function invokes multiple processes for faster processing.
705
Simon Glass969fd332022-07-11 19:04:05 -0600706 Args:
Simon Glassb27e9892023-07-19 17:48:14 -0600707 config_dir (str): Directory containing the defconfig files
708 srcdir (str): Directory containing source code (Kconfig files)
Simon Glass969fd332022-07-11 19:04:05 -0600709 jobs (int): The number of jobs to run simultaneously
Simon Glass1b218422023-07-19 17:48:27 -0600710 warn_targets (bool): True to warn about missing or duplicate
711 CONFIG_TARGET options
Simon Glassb27e9892023-07-19 17:48:14 -0600712
713 Returns:
Simon Glassbec06ed2023-07-19 17:48:21 -0600714 tuple:
715 list of dict: List of board parameters, each a dict:
716 key: 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
717 'config'
718 value: string value of the key
719 list of str: List of warnings recorded
Simon Glassa8a01412022-07-11 19:04:04 -0600720 """
721 all_defconfigs = []
Simon Glassb27e9892023-07-19 17:48:14 -0600722 for (dirpath, _, filenames) in os.walk(config_dir):
Simon Glassa8a01412022-07-11 19:04:04 -0600723 for filename in fnmatch.filter(filenames, '*_defconfig'):
724 if fnmatch.fnmatch(filename, '.*'):
725 continue
726 all_defconfigs.append(os.path.join(dirpath, filename))
727
728 total_boards = len(all_defconfigs)
729 processes = []
730 queues = []
731 for i in range(jobs):
732 defconfigs = all_defconfigs[total_boards * i // jobs :
733 total_boards * (i + 1) // jobs]
Simon Glass969fd332022-07-11 19:04:05 -0600734 que = multiprocessing.Queue(maxsize=-1)
735 proc = multiprocessing.Process(
Simon Glassa8a01412022-07-11 19:04:04 -0600736 target=self.scan_defconfigs_for_multiprocess,
Simon Glass1b218422023-07-19 17:48:27 -0600737 args=(srcdir, que, defconfigs, warn_targets))
Simon Glass969fd332022-07-11 19:04:05 -0600738 proc.start()
739 processes.append(proc)
740 queues.append(que)
Simon Glassa8a01412022-07-11 19:04:04 -0600741
Simon Glassbec06ed2023-07-19 17:48:21 -0600742 # The resulting data should be accumulated to these lists
Simon Glassa8a01412022-07-11 19:04:04 -0600743 params_list = []
Simon Glassbec06ed2023-07-19 17:48:21 -0600744 warnings = set()
Simon Glassa8a01412022-07-11 19:04:04 -0600745
746 # Data in the queues should be retrieved preriodically.
747 # Otherwise, the queues would become full and subprocesses would get stuck.
Simon Glass969fd332022-07-11 19:04:05 -0600748 while any(p.is_alive() for p in processes):
Simon Glassbec06ed2023-07-19 17:48:21 -0600749 self.read_queues(queues, params_list, warnings)
Simon Glassa8a01412022-07-11 19:04:04 -0600750 # sleep for a while until the queues are filled
751 time.sleep(SLEEP_TIME)
752
753 # Joining subprocesses just in case
754 # (All subprocesses should already have been finished)
Simon Glass969fd332022-07-11 19:04:05 -0600755 for proc in processes:
756 proc.join()
Simon Glassa8a01412022-07-11 19:04:04 -0600757
758 # retrieve leftover data
Simon Glassbec06ed2023-07-19 17:48:21 -0600759 self.read_queues(queues, params_list, warnings)
Simon Glassa8a01412022-07-11 19:04:04 -0600760
Simon Glassbec06ed2023-07-19 17:48:21 -0600761 return params_list, sorted(list(warnings))
Simon Glassa8a01412022-07-11 19:04:04 -0600762
Simon Glass969fd332022-07-11 19:04:05 -0600763 @classmethod
Simon Glass5df95cf2023-07-19 17:48:17 -0600764 def insert_maintainers_info(cls, srcdir, params_list):
Simon Glassa8a01412022-07-11 19:04:04 -0600765 """Add Status and Maintainers information to the board parameters list.
766
Simon Glass969fd332022-07-11 19:04:05 -0600767 Args:
768 params_list (list of dict): A list of the board parameters
Simon Glassadd76e72022-07-11 19:04:08 -0600769
770 Returns:
771 list of str: List of warnings collected due to missing status, etc.
Simon Glassa8a01412022-07-11 19:04:04 -0600772 """
773 database = MaintainersDatabase()
Simon Glass5df95cf2023-07-19 17:48:17 -0600774 for (dirpath, _, filenames) in os.walk(srcdir):
Simon Glass1aaaafa2023-07-19 17:48:25 -0600775 if 'MAINTAINERS' in filenames and 'tools/buildman' not in dirpath:
Simon Glass5df95cf2023-07-19 17:48:17 -0600776 database.parse_file(srcdir,
777 os.path.join(dirpath, 'MAINTAINERS'))
Simon Glassa8a01412022-07-11 19:04:04 -0600778
779 for i, params in enumerate(params_list):
780 target = params['target']
Simon Glass4cab9aa2023-07-19 17:48:24 -0600781 maintainers = database.get_maintainers(target)
782 params['maintainers'] = maintainers
783 if maintainers:
784 params['status'] = database.get_status(target)
785 else:
786 params['status'] = '-'
Simon Glassa8a01412022-07-11 19:04:04 -0600787 params_list[i] = params
Simon Glass1aaaafa2023-07-19 17:48:25 -0600788 return sorted(database.warnings)
Simon Glassa8a01412022-07-11 19:04:04 -0600789
Simon Glass969fd332022-07-11 19:04:05 -0600790 @classmethod
791 def format_and_output(cls, params_list, output):
Simon Glassa8a01412022-07-11 19:04:04 -0600792 """Write board parameters into a file.
793
794 Columnate the board parameters, sort lines alphabetically,
795 and then write them to a file.
796
Simon Glass969fd332022-07-11 19:04:05 -0600797 Args:
798 params_list (list of dict): The list of board parameters
799 output (str): The path to the output file
Simon Glassa8a01412022-07-11 19:04:04 -0600800 """
Simon Glass969fd332022-07-11 19:04:05 -0600801 fields = ('status', 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
Simon Glass256126c2022-07-11 19:04:06 -0600802 'config', 'maintainers')
Simon Glassa8a01412022-07-11 19:04:04 -0600803
804 # First, decide the width of each column
Simon Glass969fd332022-07-11 19:04:05 -0600805 max_length = {f: 0 for f in fields}
Simon Glassa8a01412022-07-11 19:04:04 -0600806 for params in params_list:
Simon Glass969fd332022-07-11 19:04:05 -0600807 for field in fields:
808 max_length[field] = max(max_length[field], len(params[field]))
Simon Glassa8a01412022-07-11 19:04:04 -0600809
810 output_lines = []
811 for params in params_list:
812 line = ''
Simon Glass969fd332022-07-11 19:04:05 -0600813 for field in fields:
Simon Glassa8a01412022-07-11 19:04:04 -0600814 # insert two spaces between fields like column -t would
Simon Glass969fd332022-07-11 19:04:05 -0600815 line += ' ' + params[field].ljust(max_length[field])
Simon Glassa8a01412022-07-11 19:04:04 -0600816 output_lines.append(line.strip())
817
818 # ignore case when sorting
819 output_lines.sort(key=str.lower)
820
Simon Glass969fd332022-07-11 19:04:05 -0600821 with open(output, 'w', encoding="utf-8") as outf:
822 outf.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n')
Simon Glassa8a01412022-07-11 19:04:04 -0600823
Simon Glass1b218422023-07-19 17:48:27 -0600824 def build_board_list(self, config_dir=CONFIG_DIR, srcdir='.', jobs=1,
825 warn_targets=False):
Simon Glass5df95cf2023-07-19 17:48:17 -0600826 """Generate a board-database file
827
828 This works by reading the Kconfig, then loading each board's defconfig
829 in to get the setting for each option. In particular, CONFIG_TARGET_xxx
830 is typically set by the defconfig, where xxx is the target to build.
831
832 Args:
833 config_dir (str): Directory containing the defconfig files
834 srcdir (str): Directory containing source code (Kconfig files)
835 jobs (int): The number of jobs to run simultaneously
Simon Glass1b218422023-07-19 17:48:27 -0600836 warn_targets (bool): True to warn about missing or duplicate
837 CONFIG_TARGET options
Simon Glass5df95cf2023-07-19 17:48:17 -0600838
839 Returns:
840 tuple:
841 list of dict: List of board parameters, each a dict:
842 key: 'arch', 'cpu', 'soc', 'vendor', 'board', 'config',
843 'target'
844 value: string value of the key
845 list of str: Warnings that came up
846 """
Simon Glass1b218422023-07-19 17:48:27 -0600847 params_list, warnings = self.scan_defconfigs(config_dir, srcdir, jobs,
848 warn_targets)
Simon Glassbec06ed2023-07-19 17:48:21 -0600849 m_warnings = self.insert_maintainers_info(srcdir, params_list)
850 return params_list, warnings + m_warnings
Simon Glass5df95cf2023-07-19 17:48:17 -0600851
Simon Glassa8a01412022-07-11 19:04:04 -0600852 def ensure_board_list(self, output, jobs=1, force=False, quiet=False):
853 """Generate a board database file if needed.
854
Simon Glassb27e9892023-07-19 17:48:14 -0600855 This is intended to check if Kconfig has changed since the boards.cfg
856 files was generated.
857
Simon Glass969fd332022-07-11 19:04:05 -0600858 Args:
859 output (str): The name of the output file
860 jobs (int): The number of jobs to run simultaneously
861 force (bool): Force to generate the output even if it is new
862 quiet (bool): True to avoid printing a message if nothing needs doing
Simon Glassadd76e72022-07-11 19:04:08 -0600863
864 Returns:
865 bool: True if all is well, False if there were warnings
Simon Glassa8a01412022-07-11 19:04:04 -0600866 """
Simon Glass283dcb62023-09-07 10:00:18 -0600867 if not force:
Simon Glassa8a01412022-07-11 19:04:04 -0600868 if not quiet:
Simon Glass283dcb62023-09-07 10:00:18 -0600869 tprint('\rChecking for Kconfig changes...', newline=False)
870 is_new = output_is_new(output, CONFIG_DIR, '.')
871 print_clear()
872 if is_new:
873 if not quiet:
874 print(f'{output} is up to date. Nothing to do.')
875 return True
876 if not quiet:
877 tprint('\rGenerating board list...', newline=False)
Simon Glass5df95cf2023-07-19 17:48:17 -0600878 params_list, warnings = self.build_board_list(CONFIG_DIR, '.', jobs)
Simon Glass283dcb62023-09-07 10:00:18 -0600879 print_clear()
Simon Glassadd76e72022-07-11 19:04:08 -0600880 for warn in warnings:
881 print(warn, file=sys.stderr)
Simon Glassa8a01412022-07-11 19:04:04 -0600882 self.format_and_output(params_list, output)
Simon Glassadd76e72022-07-11 19:04:08 -0600883 return not warnings