blob: 8a0971aa4070b31f21c1c93e857cc5be0588606a [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
53def output_is_new(output):
54 """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
62
Simon Glass0c477b72022-07-11 19:04:04 -060063 Returns:
Simon Glass58d41be2022-07-11 19:04:05 -060064 True if the given output file exists and is newer than any of
65 *_defconfig, MAINTAINERS and Kconfig*. False otherwise.
66
67 Raises:
68 OSError: output file exists but could not be opened
Simon Glass0c477b72022-07-11 19:04:04 -060069 """
Simon Glass58d41be2022-07-11 19:04:05 -060070 # pylint: disable=too-many-branches
Simon Glass0c477b72022-07-11 19:04:04 -060071 try:
72 ctime = os.path.getctime(output)
73 except OSError as exception:
74 if exception.errno == errno.ENOENT:
75 # return False on 'No such file or directory' error
76 return False
Simon Glass58d41be2022-07-11 19:04:05 -060077 raise
Simon Glass0c477b72022-07-11 19:04:04 -060078
Simon Glass58d41be2022-07-11 19:04:05 -060079 for (dirpath, _, filenames) in os.walk(CONFIG_DIR):
Simon Glass0c477b72022-07-11 19:04:04 -060080 for filename in fnmatch.filter(filenames, '*_defconfig'):
81 if fnmatch.fnmatch(filename, '.*'):
82 continue
83 filepath = os.path.join(dirpath, filename)
84 if ctime < os.path.getctime(filepath):
85 return False
86
Simon Glass58d41be2022-07-11 19:04:05 -060087 for (dirpath, _, filenames) in os.walk('.'):
Simon Glass0c477b72022-07-11 19:04:04 -060088 for filename in filenames:
89 if (fnmatch.fnmatch(filename, '*~') or
90 not fnmatch.fnmatch(filename, 'Kconfig*') and
91 not filename == 'MAINTAINERS'):
92 continue
93 filepath = os.path.join(dirpath, filename)
94 if ctime < os.path.getctime(filepath):
95 return False
96
97 # Detect a board that has been removed since the current board database
98 # was generated
Simon Glass58d41be2022-07-11 19:04:05 -060099 with open(output, encoding="utf-8") as inf:
100 for line in inf:
Simon Glassd2d4c602022-07-11 19:04:06 -0600101 if 'Options,' in line:
102 return False
Simon Glass0c477b72022-07-11 19:04:04 -0600103 if line[0] == '#' or line == '\n':
104 continue
105 defconfig = line.split()[6] + '_defconfig'
106 if not os.path.exists(os.path.join(CONFIG_DIR, defconfig)):
107 return False
108
109 return True
110
111
Simon Glass20751d62022-07-11 19:04:03 -0600112class Expr:
113 """A single regular expression for matching boards to build"""
114
115 def __init__(self, expr):
116 """Set up a new Expr object.
117
118 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600119 expr (str): String cotaining regular expression to store
Simon Glass20751d62022-07-11 19:04:03 -0600120 """
121 self._expr = expr
122 self._re = re.compile(expr)
123
124 def matches(self, props):
125 """Check if any of the properties match the regular expression.
126
127 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600128 props (list of str): List of properties to check
Simon Glass20751d62022-07-11 19:04:03 -0600129 Returns:
130 True if any of the properties match the regular expression
131 """
132 for prop in props:
133 if self._re.match(prop):
134 return True
135 return False
136
137 def __str__(self):
138 return self._expr
139
140class Term:
141 """A list of expressions each of which must match with properties.
142
143 This provides a list of 'AND' expressions, meaning that each must
144 match the board properties for that board to be built.
145 """
146 def __init__(self):
147 self._expr_list = []
148 self._board_count = 0
149
150 def add_expr(self, expr):
151 """Add an Expr object to the list to check.
152
153 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600154 expr (Expr): New Expr object to add to the list of those that must
Simon Glass20751d62022-07-11 19:04:03 -0600155 match for a board to be built.
156 """
157 self._expr_list.append(Expr(expr))
158
159 def __str__(self):
160 """Return some sort of useful string describing the term"""
161 return '&'.join([str(expr) for expr in self._expr_list])
162
163 def matches(self, props):
164 """Check if any of the properties match this term
165
166 Each of the expressions in the term is checked. All must match.
167
168 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600169 props (list of str): List of properties to check
Simon Glass20751d62022-07-11 19:04:03 -0600170 Returns:
171 True if all of the expressions in the Term match, else False
172 """
173 for expr in self._expr_list:
174 if not expr.matches(props):
175 return False
176 return True
177
Simon Glass0c477b72022-07-11 19:04:04 -0600178
179class KconfigScanner:
180
181 """Kconfig scanner."""
182
183 ### constant variable only used in this class ###
184 _SYMBOL_TABLE = {
185 'arch' : 'SYS_ARCH',
186 'cpu' : 'SYS_CPU',
187 'soc' : 'SYS_SOC',
188 'vendor' : 'SYS_VENDOR',
189 'board' : 'SYS_BOARD',
190 'config' : 'SYS_CONFIG_NAME',
Simon Glassd2d4c602022-07-11 19:04:06 -0600191 # 'target' is added later
Simon Glass0c477b72022-07-11 19:04:04 -0600192 }
193
194 def __init__(self):
195 """Scan all the Kconfig files and create a Kconfig object."""
196 # Define environment variables referenced from Kconfig
197 os.environ['srctree'] = os.getcwd()
198 os.environ['UBOOTVERSION'] = 'dummy'
199 os.environ['KCONFIG_OBJDIR'] = ''
Simon Glass58d41be2022-07-11 19:04:05 -0600200 self._tmpfile = None
Simon Glass0c477b72022-07-11 19:04:04 -0600201 self._conf = kconfiglib.Kconfig(warn=False)
202
203 def __del__(self):
204 """Delete a leftover temporary file before exit.
205
206 The scan() method of this class creates a temporay file and deletes
207 it on success. If scan() method throws an exception on the way,
208 the temporary file might be left over. In that case, it should be
209 deleted in this destructor.
210 """
Simon Glass58d41be2022-07-11 19:04:05 -0600211 if self._tmpfile:
Simon Glass0c477b72022-07-11 19:04:04 -0600212 try_remove(self._tmpfile)
213
214 def scan(self, defconfig):
215 """Load a defconfig file to obtain board parameters.
216
Simon Glass58d41be2022-07-11 19:04:05 -0600217 Args:
218 defconfig (str): path to the defconfig file to be processed
Simon Glass0c477b72022-07-11 19:04:04 -0600219
220 Returns:
Simon Glassd2d4c602022-07-11 19:04:06 -0600221 A dictionary of board parameters. It has a form of:
Simon Glass58d41be2022-07-11 19:04:05 -0600222 {
223 'arch': <arch_name>,
224 'cpu': <cpu_name>,
225 'soc': <soc_name>,
226 'vendor': <vendor_name>,
227 'board': <board_name>,
228 'target': <target_name>,
229 'config': <config_header_name>,
Simon Glass58d41be2022-07-11 19:04:05 -0600230 }
Simon Glass0c477b72022-07-11 19:04:04 -0600231 """
232 # strip special prefixes and save it in a temporary file
Simon Glass58d41be2022-07-11 19:04:05 -0600233 outfd, self._tmpfile = tempfile.mkstemp()
234 with os.fdopen(outfd, 'w') as outf:
235 with open(defconfig, encoding='utf-8') as inf:
236 for line in inf:
237 colon = line.find(':CONFIG_')
238 if colon == -1:
239 outf.write(line)
240 else:
241 outf.write(line[colon + 1:])
Simon Glass0c477b72022-07-11 19:04:04 -0600242
243 self._conf.load_config(self._tmpfile)
244 try_remove(self._tmpfile)
245 self._tmpfile = None
246
247 params = {}
248
249 # Get the value of CONFIG_SYS_ARCH, CONFIG_SYS_CPU, ... etc.
250 # Set '-' if the value is empty.
251 for key, symbol in list(self._SYMBOL_TABLE.items()):
252 value = self._conf.syms.get(symbol).str_value
253 if value:
254 params[key] = value
255 else:
256 params[key] = '-'
257
258 defconfig = os.path.basename(defconfig)
259 params['target'], match, rear = defconfig.partition('_defconfig')
Simon Glass58d41be2022-07-11 19:04:05 -0600260 assert match and not rear, f'{defconfig} : invalid defconfig'
Simon Glass0c477b72022-07-11 19:04:04 -0600261
262 # fix-up for aarch64
263 if params['arch'] == 'arm' and params['cpu'] == 'armv8':
264 params['arch'] = 'aarch64'
265
Simon Glass0c477b72022-07-11 19:04:04 -0600266 return params
267
268
269class MaintainersDatabase:
270
Simon Glass58d41be2022-07-11 19:04:05 -0600271 """The database of board status and maintainers.
272
273 Properties:
274 database: dict:
275 key: Board-target name (e.g. 'snow')
276 value: tuple:
277 str: Board status (e.g. 'Active')
278 str: List of maintainers, separated by :
Simon Glassafbd2132022-07-11 19:04:08 -0600279 warnings (list of str): List of warnings due to missing status, etc.
Simon Glass58d41be2022-07-11 19:04:05 -0600280 """
Simon Glass0c477b72022-07-11 19:04:04 -0600281
282 def __init__(self):
283 """Create an empty database."""
284 self.database = {}
Simon Glassafbd2132022-07-11 19:04:08 -0600285 self.warnings = []
Simon Glass0c477b72022-07-11 19:04:04 -0600286
287 def get_status(self, target):
288 """Return the status of the given board.
289
290 The board status is generally either 'Active' or 'Orphan'.
291 Display a warning message and return '-' if status information
292 is not found.
293
Simon Glass58d41be2022-07-11 19:04:05 -0600294 Args:
295 target (str): Build-target name
296
Simon Glass0c477b72022-07-11 19:04:04 -0600297 Returns:
Simon Glass58d41be2022-07-11 19:04:05 -0600298 str: 'Active', 'Orphan' or '-'.
Simon Glass0c477b72022-07-11 19:04:04 -0600299 """
300 if not target in self.database:
Simon Glassafbd2132022-07-11 19:04:08 -0600301 self.warnings.append(f"WARNING: no status info for '{target}'")
Simon Glass0c477b72022-07-11 19:04:04 -0600302 return '-'
303
304 tmp = self.database[target][0]
305 if tmp.startswith('Maintained'):
306 return 'Active'
Simon Glass58d41be2022-07-11 19:04:05 -0600307 if tmp.startswith('Supported'):
Simon Glass0c477b72022-07-11 19:04:04 -0600308 return 'Active'
Simon Glass58d41be2022-07-11 19:04:05 -0600309 if tmp.startswith('Orphan'):
Simon Glass0c477b72022-07-11 19:04:04 -0600310 return 'Orphan'
Simon Glassafbd2132022-07-11 19:04:08 -0600311 self.warnings.append(f"WARNING: {tmp}: unknown status for '{target}'")
Simon Glass58d41be2022-07-11 19:04:05 -0600312 return '-'
Simon Glass0c477b72022-07-11 19:04:04 -0600313
314 def get_maintainers(self, target):
315 """Return the maintainers of the given board.
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: Maintainers of the board. If the board has two or more
322 maintainers, they are separated with colons.
Simon Glass0c477b72022-07-11 19:04:04 -0600323 """
324 if not target in self.database:
Simon Glassafbd2132022-07-11 19:04:08 -0600325 self.warnings.append(f"WARNING: no maintainers for '{target}'")
Simon Glass0c477b72022-07-11 19:04:04 -0600326 return ''
327
328 return ':'.join(self.database[target][1])
329
Simon Glass58d41be2022-07-11 19:04:05 -0600330 def parse_file(self, fname):
Simon Glass0c477b72022-07-11 19:04:04 -0600331 """Parse a MAINTAINERS file.
332
Simon Glass58d41be2022-07-11 19:04:05 -0600333 Parse a MAINTAINERS file and accumulate board status and maintainers
334 information in the self.database dict.
Simon Glass0c477b72022-07-11 19:04:04 -0600335
Simon Glass58d41be2022-07-11 19:04:05 -0600336 Args:
337 fname (str): MAINTAINERS file to be parsed
Simon Glass0c477b72022-07-11 19:04:04 -0600338 """
339 targets = []
340 maintainers = []
341 status = '-'
Simon Glass58d41be2022-07-11 19:04:05 -0600342 with open(fname, encoding="utf-8") as inf:
343 for line in inf:
344 # Check also commented maintainers
345 if line[:3] == '#M:':
346 line = line[1:]
347 tag, rest = line[:2], line[2:].strip()
348 if tag == 'M:':
349 maintainers.append(rest)
350 elif tag == 'F:':
351 # expand wildcard and filter by 'configs/*_defconfig'
352 for item in glob.glob(rest):
353 front, match, rear = item.partition('configs/')
354 if not front and match:
355 front, match, rear = rear.rpartition('_defconfig')
356 if match and not rear:
357 targets.append(front)
358 elif tag == 'S:':
359 status = rest
360 elif line == '\n':
361 for target in targets:
362 self.database[target] = (status, maintainers)
363 targets = []
364 maintainers = []
365 status = '-'
Simon Glass0c477b72022-07-11 19:04:04 -0600366 if targets:
367 for target in targets:
368 self.database[target] = (status, maintainers)
369
Simon Glass20751d62022-07-11 19:04:03 -0600370
371class Boards:
372 """Manage a list of boards."""
373 def __init__(self):
Simon Glass20751d62022-07-11 19:04:03 -0600374 self._boards = []
375
376 def add_board(self, brd):
377 """Add a new board to the list.
378
379 The board's target member must not already exist in the board list.
380
381 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600382 brd (Board): board to add
Simon Glass20751d62022-07-11 19:04:03 -0600383 """
384 self._boards.append(brd)
385
386 def read_boards(self, fname):
387 """Read a list of boards from a board file.
388
389 Create a Board object for each and add it to our _boards list.
390
391 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600392 fname (str): Filename of boards.cfg file
Simon Glass20751d62022-07-11 19:04:03 -0600393 """
394 with open(fname, 'r', encoding='utf-8') as inf:
395 for line in inf:
396 if line[0] == '#':
397 continue
398 fields = line.split()
399 if not fields:
400 continue
401 for upto, field in enumerate(fields):
402 if field == '-':
403 fields[upto] = ''
404 while len(fields) < 8:
405 fields.append('')
406 if len(fields) > 8:
407 fields = fields[:8]
408
409 brd = board.Board(*fields)
410 self.add_board(brd)
411
412
413 def get_list(self):
414 """Return a list of available boards.
415
416 Returns:
417 List of Board objects
418 """
419 return self._boards
420
421 def get_dict(self):
422 """Build a dictionary containing all the boards.
423
424 Returns:
425 Dictionary:
426 key is board.target
427 value is board
428 """
429 board_dict = OrderedDict()
430 for brd in self._boards:
431 board_dict[brd.target] = brd
432 return board_dict
433
434 def get_selected_dict(self):
435 """Return a dictionary containing the selected boards
436
437 Returns:
438 List of Board objects that are marked selected
439 """
440 board_dict = OrderedDict()
441 for brd in self._boards:
442 if brd.build_it:
443 board_dict[brd.target] = brd
444 return board_dict
445
446 def get_selected(self):
447 """Return a list of selected boards
448
449 Returns:
450 List of Board objects that are marked selected
451 """
452 return [brd for brd in self._boards if brd.build_it]
453
454 def get_selected_names(self):
455 """Return a list of selected boards
456
457 Returns:
458 List of board names that are marked selected
459 """
460 return [brd.target for brd in self._boards if brd.build_it]
461
462 @classmethod
463 def _build_terms(cls, args):
464 """Convert command line arguments to a list of terms.
465
466 This deals with parsing of the arguments. It handles the '&'
467 operator, which joins several expressions into a single Term.
468
469 For example:
470 ['arm & freescale sandbox', 'tegra']
471
472 will produce 3 Terms containing expressions as follows:
473 arm, freescale
474 sandbox
475 tegra
476
477 The first Term has two expressions, both of which must match for
478 a board to be selected.
479
480 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600481 args (list of str): List of command line arguments
482
Simon Glass20751d62022-07-11 19:04:03 -0600483 Returns:
Simon Glass58d41be2022-07-11 19:04:05 -0600484 list of Term: A list of Term objects
Simon Glass20751d62022-07-11 19:04:03 -0600485 """
486 syms = []
487 for arg in args:
488 for word in arg.split():
489 sym_build = []
490 for term in word.split('&'):
491 if term:
492 sym_build.append(term)
493 sym_build.append('&')
494 syms += sym_build[:-1]
495 terms = []
496 term = None
497 oper = None
498 for sym in syms:
499 if sym == '&':
500 oper = sym
501 elif oper:
502 term.add_expr(sym)
503 oper = None
504 else:
505 if term:
506 terms.append(term)
507 term = Term()
508 term.add_expr(sym)
509 if term:
510 terms.append(term)
511 return terms
512
513 def select_boards(self, args, exclude=None, brds=None):
514 """Mark boards selected based on args
515
516 Normally either boards (an explicit list of boards) or args (a list of
517 terms to match against) is used. It is possible to specify both, in
518 which case they are additive.
519
520 If brds and args are both empty, all boards are selected.
521
522 Args:
Simon Glass58d41be2022-07-11 19:04:05 -0600523 args (list of str): List of strings specifying boards to include,
524 either named, or by their target, architecture, cpu, vendor or
525 soc. If empty, all boards are selected.
526 exclude (list of str): List of boards to exclude, regardless of
527 'args', or None for none
528 brds (list of Board): List of boards to build, or None/[] for all
Simon Glass20751d62022-07-11 19:04:03 -0600529
530 Returns:
531 Tuple
532 Dictionary which holds the list of boards which were selected
533 due to each argument, arranged by argument.
534 List of errors found
535 """
Simon Glass58d41be2022-07-11 19:04:05 -0600536 def _check_board(brd):
537 """Check whether to include or exclude a board
Simon Glass20751d62022-07-11 19:04:03 -0600538
Simon Glass58d41be2022-07-11 19:04:05 -0600539 Checks the various terms and decide whether to build it or not (the
540 'build_it' variable).
Simon Glass20751d62022-07-11 19:04:03 -0600541
Simon Glass58d41be2022-07-11 19:04:05 -0600542 If it is built, add the board to the result[term] list so we know
543 which term caused it to be built. Add it to result['all'] also.
Simon Glass20751d62022-07-11 19:04:03 -0600544
Simon Glass58d41be2022-07-11 19:04:05 -0600545 Keep a list of boards we found in 'found', so we can report boards
546 which appear in self._boards but not in brds.
547
548 Args:
549 brd (Board): Board to check
550 """
Simon Glass20751d62022-07-11 19:04:03 -0600551 matching_term = None
552 build_it = False
553 if terms:
554 for term in terms:
555 if term.matches(brd.props):
556 matching_term = str(term)
557 build_it = True
558 break
559 elif brds:
560 if brd.target in brds:
561 build_it = True
562 found.append(brd.target)
563 else:
564 build_it = True
565
566 # Check that it is not specifically excluded
567 for expr in exclude_list:
568 if expr.matches(brd.props):
569 build_it = False
570 break
571
572 if build_it:
573 brd.build_it = True
574 if matching_term:
575 result[matching_term].append(brd.target)
576 result['all'].append(brd.target)
577
Simon Glass58d41be2022-07-11 19:04:05 -0600578 result = OrderedDict()
579 warnings = []
580 terms = self._build_terms(args)
581
582 result['all'] = []
583 for term in terms:
584 result[str(term)] = []
585
586 exclude_list = []
587 if exclude:
588 for expr in exclude:
589 exclude_list.append(Expr(expr))
590
591 found = []
592 for brd in self._boards:
593 _check_board(brd)
594
Simon Glass20751d62022-07-11 19:04:03 -0600595 if brds:
596 remaining = set(brds) - set(found)
597 if remaining:
598 warnings.append(f"Boards not found: {', '.join(remaining)}\n")
599
600 return result, warnings
Simon Glass0c477b72022-07-11 19:04:04 -0600601
Simon Glass58d41be2022-07-11 19:04:05 -0600602 @classmethod
603 def scan_defconfigs_for_multiprocess(cls, queue, defconfigs):
Simon Glass0c477b72022-07-11 19:04:04 -0600604 """Scan defconfig files and queue their board parameters
605
Simon Glass58d41be2022-07-11 19:04:05 -0600606 This function is intended to be passed to multiprocessing.Process()
607 constructor.
Simon Glass0c477b72022-07-11 19:04:04 -0600608
Simon Glass58d41be2022-07-11 19:04:05 -0600609 Args:
610 queue (multiprocessing.Queue): The resulting board parameters are
611 written into this.
612 defconfigs (sequence of str): A sequence of defconfig files to be
613 scanned.
Simon Glass0c477b72022-07-11 19:04:04 -0600614 """
615 kconf_scanner = KconfigScanner()
616 for defconfig in defconfigs:
617 queue.put(kconf_scanner.scan(defconfig))
618
Simon Glass58d41be2022-07-11 19:04:05 -0600619 @classmethod
620 def read_queues(cls, queues, params_list):
Simon Glass0c477b72022-07-11 19:04:04 -0600621 """Read the queues and append the data to the paramers list"""
Simon Glass58d41be2022-07-11 19:04:05 -0600622 for que in queues:
623 while not que.empty():
624 params_list.append(que.get())
Simon Glass0c477b72022-07-11 19:04:04 -0600625
626 def scan_defconfigs(self, jobs=1):
627 """Collect board parameters for all defconfig files.
628
629 This function invokes multiple processes for faster processing.
630
Simon Glass58d41be2022-07-11 19:04:05 -0600631 Args:
632 jobs (int): The number of jobs to run simultaneously
Simon Glass0c477b72022-07-11 19:04:04 -0600633 """
634 all_defconfigs = []
Simon Glass58d41be2022-07-11 19:04:05 -0600635 for (dirpath, _, filenames) in os.walk(CONFIG_DIR):
Simon Glass0c477b72022-07-11 19:04:04 -0600636 for filename in fnmatch.filter(filenames, '*_defconfig'):
637 if fnmatch.fnmatch(filename, '.*'):
638 continue
639 all_defconfigs.append(os.path.join(dirpath, filename))
640
641 total_boards = len(all_defconfigs)
642 processes = []
643 queues = []
644 for i in range(jobs):
645 defconfigs = all_defconfigs[total_boards * i // jobs :
646 total_boards * (i + 1) // jobs]
Simon Glass58d41be2022-07-11 19:04:05 -0600647 que = multiprocessing.Queue(maxsize=-1)
648 proc = multiprocessing.Process(
Simon Glass0c477b72022-07-11 19:04:04 -0600649 target=self.scan_defconfigs_for_multiprocess,
Simon Glass58d41be2022-07-11 19:04:05 -0600650 args=(que, defconfigs))
651 proc.start()
652 processes.append(proc)
653 queues.append(que)
Simon Glass0c477b72022-07-11 19:04:04 -0600654
655 # The resulting data should be accumulated to this list
656 params_list = []
657
658 # Data in the queues should be retrieved preriodically.
659 # Otherwise, the queues would become full and subprocesses would get stuck.
Simon Glass58d41be2022-07-11 19:04:05 -0600660 while any(p.is_alive() for p in processes):
Simon Glass0c477b72022-07-11 19:04:04 -0600661 self.read_queues(queues, params_list)
662 # sleep for a while until the queues are filled
663 time.sleep(SLEEP_TIME)
664
665 # Joining subprocesses just in case
666 # (All subprocesses should already have been finished)
Simon Glass58d41be2022-07-11 19:04:05 -0600667 for proc in processes:
668 proc.join()
Simon Glass0c477b72022-07-11 19:04:04 -0600669
670 # retrieve leftover data
671 self.read_queues(queues, params_list)
672
673 return params_list
674
Simon Glass58d41be2022-07-11 19:04:05 -0600675 @classmethod
676 def insert_maintainers_info(cls, params_list):
Simon Glass0c477b72022-07-11 19:04:04 -0600677 """Add Status and Maintainers information to the board parameters list.
678
Simon Glass58d41be2022-07-11 19:04:05 -0600679 Args:
680 params_list (list of dict): A list of the board parameters
Simon Glassafbd2132022-07-11 19:04:08 -0600681
682 Returns:
683 list of str: List of warnings collected due to missing status, etc.
Simon Glass0c477b72022-07-11 19:04:04 -0600684 """
685 database = MaintainersDatabase()
Simon Glass58d41be2022-07-11 19:04:05 -0600686 for (dirpath, _, filenames) in os.walk('.'):
Simon Glass0c477b72022-07-11 19:04:04 -0600687 if 'MAINTAINERS' in filenames:
688 database.parse_file(os.path.join(dirpath, 'MAINTAINERS'))
689
690 for i, params in enumerate(params_list):
691 target = params['target']
692 params['status'] = database.get_status(target)
693 params['maintainers'] = database.get_maintainers(target)
694 params_list[i] = params
Simon Glassafbd2132022-07-11 19:04:08 -0600695 return database.warnings
Simon Glass0c477b72022-07-11 19:04:04 -0600696
Simon Glass58d41be2022-07-11 19:04:05 -0600697 @classmethod
698 def format_and_output(cls, params_list, output):
Simon Glass0c477b72022-07-11 19:04:04 -0600699 """Write board parameters into a file.
700
701 Columnate the board parameters, sort lines alphabetically,
702 and then write them to a file.
703
Simon Glass58d41be2022-07-11 19:04:05 -0600704 Args:
705 params_list (list of dict): The list of board parameters
706 output (str): The path to the output file
Simon Glass0c477b72022-07-11 19:04:04 -0600707 """
Simon Glass58d41be2022-07-11 19:04:05 -0600708 fields = ('status', 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
Simon Glassd2d4c602022-07-11 19:04:06 -0600709 'config', 'maintainers')
Simon Glass0c477b72022-07-11 19:04:04 -0600710
711 # First, decide the width of each column
Simon Glass58d41be2022-07-11 19:04:05 -0600712 max_length = {f: 0 for f in fields}
Simon Glass0c477b72022-07-11 19:04:04 -0600713 for params in params_list:
Simon Glass58d41be2022-07-11 19:04:05 -0600714 for field in fields:
715 max_length[field] = max(max_length[field], len(params[field]))
Simon Glass0c477b72022-07-11 19:04:04 -0600716
717 output_lines = []
718 for params in params_list:
719 line = ''
Simon Glass58d41be2022-07-11 19:04:05 -0600720 for field in fields:
Simon Glass0c477b72022-07-11 19:04:04 -0600721 # insert two spaces between fields like column -t would
Simon Glass58d41be2022-07-11 19:04:05 -0600722 line += ' ' + params[field].ljust(max_length[field])
Simon Glass0c477b72022-07-11 19:04:04 -0600723 output_lines.append(line.strip())
724
725 # ignore case when sorting
726 output_lines.sort(key=str.lower)
727
Simon Glass58d41be2022-07-11 19:04:05 -0600728 with open(output, 'w', encoding="utf-8") as outf:
729 outf.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n')
Simon Glass0c477b72022-07-11 19:04:04 -0600730
731 def ensure_board_list(self, output, jobs=1, force=False, quiet=False):
732 """Generate a board database file if needed.
733
Simon Glass58d41be2022-07-11 19:04:05 -0600734 Args:
735 output (str): The name of the output file
736 jobs (int): The number of jobs to run simultaneously
737 force (bool): Force to generate the output even if it is new
738 quiet (bool): True to avoid printing a message if nothing needs doing
Simon Glassafbd2132022-07-11 19:04:08 -0600739
740 Returns:
741 bool: True if all is well, False if there were warnings
Simon Glass0c477b72022-07-11 19:04:04 -0600742 """
743 if not force and output_is_new(output):
744 if not quiet:
Simon Glass58d41be2022-07-11 19:04:05 -0600745 print(f'{output} is up to date. Nothing to do.')
Simon Glassafbd2132022-07-11 19:04:08 -0600746 return True
Simon Glass0c477b72022-07-11 19:04:04 -0600747 params_list = self.scan_defconfigs(jobs)
Simon Glassafbd2132022-07-11 19:04:08 -0600748 warnings = self.insert_maintainers_info(params_list)
749 for warn in warnings:
750 print(warn, file=sys.stderr)
Simon Glass0c477b72022-07-11 19:04:04 -0600751 self.format_and_output(params_list, output)
Simon Glassafbd2132022-07-11 19:04:08 -0600752 return not warnings