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