blob: cdc4d9ffd271492ad0860ce35e3c17008b85ff3f [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
22
23### constant variables ###
24OUTPUT_FILE = 'boards.cfg'
25CONFIG_DIR = 'configs'
26SLEEP_TIME = 0.03
Simon Glass969fd332022-07-11 19:04:05 -060027COMMENT_BLOCK = f'''#
Simon Glassa8a01412022-07-11 19:04:04 -060028# List of boards
Simon Glass969fd332022-07-11 19:04:05 -060029# Automatically generated by {__file__}: don't edit
Simon Glassa8a01412022-07-11 19:04:04 -060030#
Simon Glass256126c2022-07-11 19:04:06 -060031# Status, Arch, CPU, SoC, Vendor, Board, Target, Config, Maintainers
Simon Glassa8a01412022-07-11 19:04:04 -060032
Simon Glass969fd332022-07-11 19:04:05 -060033'''
Simon Glassa8a01412022-07-11 19:04:04 -060034
35
Simon Glass969fd332022-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 Glassa8a01412022-07-11 19:04:04 -060045 try:
Simon Glass969fd332022-07-11 19:04:05 -060046 os.remove(fname)
Simon Glassa8a01412022-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 Glass969fd332022-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 Glassa8a01412022-07-11 19:04:04 -060063 Returns:
Simon Glass969fd332022-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 Glassa8a01412022-07-11 19:04:04 -060069 """
Simon Glass969fd332022-07-11 19:04:05 -060070 # pylint: disable=too-many-branches
Simon Glassa8a01412022-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 Glass969fd332022-07-11 19:04:05 -060077 raise
Simon Glassa8a01412022-07-11 19:04:04 -060078
Simon Glass969fd332022-07-11 19:04:05 -060079 for (dirpath, _, filenames) in os.walk(CONFIG_DIR):
Simon Glassa8a01412022-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 Glass969fd332022-07-11 19:04:05 -060087 for (dirpath, _, filenames) in os.walk('.'):
Simon Glassa8a01412022-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 Glass969fd332022-07-11 19:04:05 -060099 with open(output, encoding="utf-8") as inf:
100 for line in inf:
Simon Glass256126c2022-07-11 19:04:06 -0600101 if 'Options,' in line:
102 return False
Simon Glassa8a01412022-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
Simon Glassc52bd222022-07-11 19:04:03 -0600110
111
112class 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 Glass969fd332022-07-11 19:04:05 -0600119 expr (str): String cotaining regular expression to store
Simon Glassc52bd222022-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 Glass969fd332022-07-11 19:04:05 -0600128 props (list of str): List of properties to check
Simon Glassc52bd222022-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 Glass969fd332022-07-11 19:04:05 -0600154 expr (Expr): New Expr object to add to the list of those that must
Simon Glassc52bd222022-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 Glass969fd332022-07-11 19:04:05 -0600169 props (list of str): List of properties to check
Simon Glassc52bd222022-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
178
Simon Glassa8a01412022-07-11 19:04:04 -0600179class 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 Glass256126c2022-07-11 19:04:06 -0600191 # 'target' is added later
Simon Glassa8a01412022-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 Glass969fd332022-07-11 19:04:05 -0600200 self._tmpfile = None
Simon Glassa8a01412022-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 Glass969fd332022-07-11 19:04:05 -0600211 if self._tmpfile:
Simon Glassa8a01412022-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 Glass969fd332022-07-11 19:04:05 -0600217 Args:
218 defconfig (str): path to the defconfig file to be processed
Simon Glassa8a01412022-07-11 19:04:04 -0600219
220 Returns:
Simon Glass256126c2022-07-11 19:04:06 -0600221 A dictionary of board parameters. It has a form of:
Simon Glass969fd332022-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 Glass969fd332022-07-11 19:04:05 -0600230 }
Simon Glassa8a01412022-07-11 19:04:04 -0600231 """
232 # strip special prefixes and save it in a temporary file
Simon Glass969fd332022-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 Glassa8a01412022-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 Glass969fd332022-07-11 19:04:05 -0600260 assert match and not rear, f'{defconfig} : invalid defconfig'
Simon Glassa8a01412022-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
Heinrich Schuchardt3672ed72022-10-03 18:07:53 +0200266 # fix-up for riscv
267 if params['arch'] == 'riscv':
268 try:
269 value = self._conf.syms.get('ARCH_RV32I').str_value
270 except:
271 value = ''
272 if value == 'y':
273 params['arch'] = 'riscv32'
274 else:
275 params['arch'] = 'riscv64'
276
Simon Glassa8a01412022-07-11 19:04:04 -0600277 return params
278
279
280class MaintainersDatabase:
281
Simon Glass969fd332022-07-11 19:04:05 -0600282 """The database of board status and maintainers.
283
284 Properties:
285 database: dict:
286 key: Board-target name (e.g. 'snow')
287 value: tuple:
288 str: Board status (e.g. 'Active')
289 str: List of maintainers, separated by :
Simon Glassadd76e72022-07-11 19:04:08 -0600290 warnings (list of str): List of warnings due to missing status, etc.
Simon Glass969fd332022-07-11 19:04:05 -0600291 """
Simon Glassa8a01412022-07-11 19:04:04 -0600292
293 def __init__(self):
294 """Create an empty database."""
295 self.database = {}
Simon Glassadd76e72022-07-11 19:04:08 -0600296 self.warnings = []
Simon Glassa8a01412022-07-11 19:04:04 -0600297
298 def get_status(self, target):
299 """Return the status of the given board.
300
301 The board status is generally either 'Active' or 'Orphan'.
302 Display a warning message and return '-' if status information
303 is not found.
304
Simon Glass969fd332022-07-11 19:04:05 -0600305 Args:
306 target (str): Build-target name
307
Simon Glassa8a01412022-07-11 19:04:04 -0600308 Returns:
Simon Glass969fd332022-07-11 19:04:05 -0600309 str: 'Active', 'Orphan' or '-'.
Simon Glassa8a01412022-07-11 19:04:04 -0600310 """
311 if not target in self.database:
Simon Glassadd76e72022-07-11 19:04:08 -0600312 self.warnings.append(f"WARNING: no status info for '{target}'")
Simon Glassa8a01412022-07-11 19:04:04 -0600313 return '-'
314
315 tmp = self.database[target][0]
316 if tmp.startswith('Maintained'):
317 return 'Active'
Simon Glass969fd332022-07-11 19:04:05 -0600318 if tmp.startswith('Supported'):
Simon Glassa8a01412022-07-11 19:04:04 -0600319 return 'Active'
Simon Glass969fd332022-07-11 19:04:05 -0600320 if tmp.startswith('Orphan'):
Simon Glassa8a01412022-07-11 19:04:04 -0600321 return 'Orphan'
Simon Glassadd76e72022-07-11 19:04:08 -0600322 self.warnings.append(f"WARNING: {tmp}: unknown status for '{target}'")
Simon Glass969fd332022-07-11 19:04:05 -0600323 return '-'
Simon Glassa8a01412022-07-11 19:04:04 -0600324
325 def get_maintainers(self, target):
326 """Return the maintainers of the given board.
327
Simon Glass969fd332022-07-11 19:04:05 -0600328 Args:
329 target (str): Build-target name
330
Simon Glassa8a01412022-07-11 19:04:04 -0600331 Returns:
Simon Glass969fd332022-07-11 19:04:05 -0600332 str: Maintainers of the board. If the board has two or more
333 maintainers, they are separated with colons.
Simon Glassa8a01412022-07-11 19:04:04 -0600334 """
335 if not target in self.database:
Simon Glassadd76e72022-07-11 19:04:08 -0600336 self.warnings.append(f"WARNING: no maintainers for '{target}'")
Simon Glassa8a01412022-07-11 19:04:04 -0600337 return ''
338
339 return ':'.join(self.database[target][1])
340
Simon Glass969fd332022-07-11 19:04:05 -0600341 def parse_file(self, fname):
Simon Glassa8a01412022-07-11 19:04:04 -0600342 """Parse a MAINTAINERS file.
343
Simon Glass969fd332022-07-11 19:04:05 -0600344 Parse a MAINTAINERS file and accumulate board status and maintainers
345 information in the self.database dict.
Simon Glassa8a01412022-07-11 19:04:04 -0600346
Simon Glass969fd332022-07-11 19:04:05 -0600347 Args:
348 fname (str): MAINTAINERS file to be parsed
Simon Glassa8a01412022-07-11 19:04:04 -0600349 """
350 targets = []
351 maintainers = []
352 status = '-'
Simon Glass969fd332022-07-11 19:04:05 -0600353 with open(fname, encoding="utf-8") as inf:
354 for line in inf:
355 # Check also commented maintainers
356 if line[:3] == '#M:':
357 line = line[1:]
358 tag, rest = line[:2], line[2:].strip()
359 if tag == 'M:':
360 maintainers.append(rest)
361 elif tag == 'F:':
362 # expand wildcard and filter by 'configs/*_defconfig'
363 for item in glob.glob(rest):
364 front, match, rear = item.partition('configs/')
365 if not front and match:
366 front, match, rear = rear.rpartition('_defconfig')
367 if match and not rear:
368 targets.append(front)
369 elif tag == 'S:':
370 status = rest
371 elif line == '\n':
372 for target in targets:
373 self.database[target] = (status, maintainers)
374 targets = []
375 maintainers = []
376 status = '-'
Simon Glassa8a01412022-07-11 19:04:04 -0600377 if targets:
378 for target in targets:
379 self.database[target] = (status, maintainers)
380
381
Simon Glassc52bd222022-07-11 19:04:03 -0600382class Boards:
383 """Manage a list of boards."""
384 def __init__(self):
Simon Glassc52bd222022-07-11 19:04:03 -0600385 self._boards = []
386
387 def add_board(self, brd):
388 """Add a new board to the list.
389
390 The board's target member must not already exist in the board list.
391
392 Args:
Simon Glass969fd332022-07-11 19:04:05 -0600393 brd (Board): board to add
Simon Glassc52bd222022-07-11 19:04:03 -0600394 """
395 self._boards.append(brd)
396
397 def read_boards(self, fname):
398 """Read a list of boards from a board file.
399
400 Create a Board object for each and add it to our _boards list.
401
402 Args:
Simon Glass969fd332022-07-11 19:04:05 -0600403 fname (str): Filename of boards.cfg file
Simon Glassc52bd222022-07-11 19:04:03 -0600404 """
405 with open(fname, 'r', encoding='utf-8') as inf:
406 for line in inf:
407 if line[0] == '#':
408 continue
409 fields = line.split()
410 if not fields:
411 continue
412 for upto, field in enumerate(fields):
413 if field == '-':
414 fields[upto] = ''
415 while len(fields) < 8:
416 fields.append('')
417 if len(fields) > 8:
418 fields = fields[:8]
419
420 brd = board.Board(*fields)
421 self.add_board(brd)
422
423
424 def get_list(self):
425 """Return a list of available boards.
426
427 Returns:
428 List of Board objects
429 """
430 return self._boards
431
432 def get_dict(self):
433 """Build a dictionary containing all the boards.
434
435 Returns:
436 Dictionary:
437 key is board.target
438 value is board
439 """
440 board_dict = OrderedDict()
441 for brd in self._boards:
442 board_dict[brd.target] = brd
443 return board_dict
444
445 def get_selected_dict(self):
446 """Return a dictionary containing the selected boards
447
448 Returns:
449 List of Board objects that are marked selected
450 """
451 board_dict = OrderedDict()
452 for brd in self._boards:
453 if brd.build_it:
454 board_dict[brd.target] = brd
455 return board_dict
456
457 def get_selected(self):
458 """Return a list of selected boards
459
460 Returns:
461 List of Board objects that are marked selected
462 """
463 return [brd for brd in self._boards if brd.build_it]
464
465 def get_selected_names(self):
466 """Return a list of selected boards
467
468 Returns:
469 List of board names that are marked selected
470 """
471 return [brd.target for brd in self._boards if brd.build_it]
472
473 @classmethod
474 def _build_terms(cls, args):
475 """Convert command line arguments to a list of terms.
476
477 This deals with parsing of the arguments. It handles the '&'
478 operator, which joins several expressions into a single Term.
479
480 For example:
481 ['arm & freescale sandbox', 'tegra']
482
483 will produce 3 Terms containing expressions as follows:
484 arm, freescale
485 sandbox
486 tegra
487
488 The first Term has two expressions, both of which must match for
489 a board to be selected.
490
491 Args:
Simon Glass969fd332022-07-11 19:04:05 -0600492 args (list of str): List of command line arguments
493
Simon Glassc52bd222022-07-11 19:04:03 -0600494 Returns:
Simon Glass969fd332022-07-11 19:04:05 -0600495 list of Term: A list of Term objects
Simon Glassc52bd222022-07-11 19:04:03 -0600496 """
497 syms = []
498 for arg in args:
499 for word in arg.split():
500 sym_build = []
501 for term in word.split('&'):
502 if term:
503 sym_build.append(term)
504 sym_build.append('&')
505 syms += sym_build[:-1]
506 terms = []
507 term = None
508 oper = None
509 for sym in syms:
510 if sym == '&':
511 oper = sym
512 elif oper:
513 term.add_expr(sym)
514 oper = None
515 else:
516 if term:
517 terms.append(term)
518 term = Term()
519 term.add_expr(sym)
520 if term:
521 terms.append(term)
522 return terms
523
524 def select_boards(self, args, exclude=None, brds=None):
525 """Mark boards selected based on args
526
527 Normally either boards (an explicit list of boards) or args (a list of
528 terms to match against) is used. It is possible to specify both, in
529 which case they are additive.
530
531 If brds and args are both empty, all boards are selected.
532
533 Args:
Simon Glass969fd332022-07-11 19:04:05 -0600534 args (list of str): List of strings specifying boards to include,
535 either named, or by their target, architecture, cpu, vendor or
536 soc. If empty, all boards are selected.
537 exclude (list of str): List of boards to exclude, regardless of
538 'args', or None for none
539 brds (list of Board): List of boards to build, or None/[] for all
Simon Glassc52bd222022-07-11 19:04:03 -0600540
541 Returns:
542 Tuple
543 Dictionary which holds the list of boards which were selected
544 due to each argument, arranged by argument.
545 List of errors found
546 """
Simon Glass969fd332022-07-11 19:04:05 -0600547 def _check_board(brd):
548 """Check whether to include or exclude a board
Simon Glassc52bd222022-07-11 19:04:03 -0600549
Simon Glass969fd332022-07-11 19:04:05 -0600550 Checks the various terms and decide whether to build it or not (the
551 'build_it' variable).
Simon Glassc52bd222022-07-11 19:04:03 -0600552
Simon Glass969fd332022-07-11 19:04:05 -0600553 If it is built, add the board to the result[term] list so we know
554 which term caused it to be built. Add it to result['all'] also.
Simon Glassc52bd222022-07-11 19:04:03 -0600555
Simon Glass969fd332022-07-11 19:04:05 -0600556 Keep a list of boards we found in 'found', so we can report boards
557 which appear in self._boards but not in brds.
558
559 Args:
560 brd (Board): Board to check
561 """
Simon Glassc52bd222022-07-11 19:04:03 -0600562 matching_term = None
563 build_it = False
564 if terms:
565 for term in terms:
566 if term.matches(brd.props):
567 matching_term = str(term)
568 build_it = True
569 break
570 elif brds:
571 if brd.target in brds:
572 build_it = True
573 found.append(brd.target)
574 else:
575 build_it = True
576
577 # Check that it is not specifically excluded
578 for expr in exclude_list:
579 if expr.matches(brd.props):
580 build_it = False
581 break
582
583 if build_it:
584 brd.build_it = True
585 if matching_term:
586 result[matching_term].append(brd.target)
587 result['all'].append(brd.target)
588
Simon Glass969fd332022-07-11 19:04:05 -0600589 result = OrderedDict()
590 warnings = []
591 terms = self._build_terms(args)
592
593 result['all'] = []
594 for term in terms:
595 result[str(term)] = []
596
597 exclude_list = []
598 if exclude:
599 for expr in exclude:
600 exclude_list.append(Expr(expr))
601
602 found = []
603 for brd in self._boards:
604 _check_board(brd)
605
Simon Glassc52bd222022-07-11 19:04:03 -0600606 if brds:
607 remaining = set(brds) - set(found)
608 if remaining:
609 warnings.append(f"Boards not found: {', '.join(remaining)}\n")
610
611 return result, warnings
Simon Glassa8a01412022-07-11 19:04:04 -0600612
Simon Glass969fd332022-07-11 19:04:05 -0600613 @classmethod
614 def scan_defconfigs_for_multiprocess(cls, queue, defconfigs):
Simon Glassa8a01412022-07-11 19:04:04 -0600615 """Scan defconfig files and queue their board parameters
616
Simon Glass969fd332022-07-11 19:04:05 -0600617 This function is intended to be passed to multiprocessing.Process()
618 constructor.
Simon Glassa8a01412022-07-11 19:04:04 -0600619
Simon Glass969fd332022-07-11 19:04:05 -0600620 Args:
621 queue (multiprocessing.Queue): The resulting board parameters are
622 written into this.
623 defconfigs (sequence of str): A sequence of defconfig files to be
624 scanned.
Simon Glassa8a01412022-07-11 19:04:04 -0600625 """
626 kconf_scanner = KconfigScanner()
627 for defconfig in defconfigs:
628 queue.put(kconf_scanner.scan(defconfig))
629
Simon Glass969fd332022-07-11 19:04:05 -0600630 @classmethod
631 def read_queues(cls, queues, params_list):
Simon Glassa8a01412022-07-11 19:04:04 -0600632 """Read the queues and append the data to the paramers list"""
Simon Glass969fd332022-07-11 19:04:05 -0600633 for que in queues:
634 while not que.empty():
635 params_list.append(que.get())
Simon Glassa8a01412022-07-11 19:04:04 -0600636
637 def scan_defconfigs(self, jobs=1):
638 """Collect board parameters for all defconfig files.
639
640 This function invokes multiple processes for faster processing.
641
Simon Glass969fd332022-07-11 19:04:05 -0600642 Args:
643 jobs (int): The number of jobs to run simultaneously
Simon Glassa8a01412022-07-11 19:04:04 -0600644 """
645 all_defconfigs = []
Simon Glass969fd332022-07-11 19:04:05 -0600646 for (dirpath, _, filenames) in os.walk(CONFIG_DIR):
Simon Glassa8a01412022-07-11 19:04:04 -0600647 for filename in fnmatch.filter(filenames, '*_defconfig'):
648 if fnmatch.fnmatch(filename, '.*'):
649 continue
650 all_defconfigs.append(os.path.join(dirpath, filename))
651
652 total_boards = len(all_defconfigs)
653 processes = []
654 queues = []
655 for i in range(jobs):
656 defconfigs = all_defconfigs[total_boards * i // jobs :
657 total_boards * (i + 1) // jobs]
Simon Glass969fd332022-07-11 19:04:05 -0600658 que = multiprocessing.Queue(maxsize=-1)
659 proc = multiprocessing.Process(
Simon Glassa8a01412022-07-11 19:04:04 -0600660 target=self.scan_defconfigs_for_multiprocess,
Simon Glass969fd332022-07-11 19:04:05 -0600661 args=(que, defconfigs))
662 proc.start()
663 processes.append(proc)
664 queues.append(que)
Simon Glassa8a01412022-07-11 19:04:04 -0600665
666 # The resulting data should be accumulated to this list
667 params_list = []
668
669 # Data in the queues should be retrieved preriodically.
670 # Otherwise, the queues would become full and subprocesses would get stuck.
Simon Glass969fd332022-07-11 19:04:05 -0600671 while any(p.is_alive() for p in processes):
Simon Glassa8a01412022-07-11 19:04:04 -0600672 self.read_queues(queues, params_list)
673 # sleep for a while until the queues are filled
674 time.sleep(SLEEP_TIME)
675
676 # Joining subprocesses just in case
677 # (All subprocesses should already have been finished)
Simon Glass969fd332022-07-11 19:04:05 -0600678 for proc in processes:
679 proc.join()
Simon Glassa8a01412022-07-11 19:04:04 -0600680
681 # retrieve leftover data
682 self.read_queues(queues, params_list)
683
684 return params_list
685
Simon Glass969fd332022-07-11 19:04:05 -0600686 @classmethod
687 def insert_maintainers_info(cls, params_list):
Simon Glassa8a01412022-07-11 19:04:04 -0600688 """Add Status and Maintainers information to the board parameters list.
689
Simon Glass969fd332022-07-11 19:04:05 -0600690 Args:
691 params_list (list of dict): A list of the board parameters
Simon Glassadd76e72022-07-11 19:04:08 -0600692
693 Returns:
694 list of str: List of warnings collected due to missing status, etc.
Simon Glassa8a01412022-07-11 19:04:04 -0600695 """
696 database = MaintainersDatabase()
Simon Glass969fd332022-07-11 19:04:05 -0600697 for (dirpath, _, filenames) in os.walk('.'):
Simon Glassa8a01412022-07-11 19:04:04 -0600698 if 'MAINTAINERS' in filenames:
699 database.parse_file(os.path.join(dirpath, 'MAINTAINERS'))
700
701 for i, params in enumerate(params_list):
702 target = params['target']
703 params['status'] = database.get_status(target)
704 params['maintainers'] = database.get_maintainers(target)
705 params_list[i] = params
Simon Glassadd76e72022-07-11 19:04:08 -0600706 return database.warnings
Simon Glassa8a01412022-07-11 19:04:04 -0600707
Simon Glass969fd332022-07-11 19:04:05 -0600708 @classmethod
709 def format_and_output(cls, params_list, output):
Simon Glassa8a01412022-07-11 19:04:04 -0600710 """Write board parameters into a file.
711
712 Columnate the board parameters, sort lines alphabetically,
713 and then write them to a file.
714
Simon Glass969fd332022-07-11 19:04:05 -0600715 Args:
716 params_list (list of dict): The list of board parameters
717 output (str): The path to the output file
Simon Glassa8a01412022-07-11 19:04:04 -0600718 """
Simon Glass969fd332022-07-11 19:04:05 -0600719 fields = ('status', 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
Simon Glass256126c2022-07-11 19:04:06 -0600720 'config', 'maintainers')
Simon Glassa8a01412022-07-11 19:04:04 -0600721
722 # First, decide the width of each column
Simon Glass969fd332022-07-11 19:04:05 -0600723 max_length = {f: 0 for f in fields}
Simon Glassa8a01412022-07-11 19:04:04 -0600724 for params in params_list:
Simon Glass969fd332022-07-11 19:04:05 -0600725 for field in fields:
726 max_length[field] = max(max_length[field], len(params[field]))
Simon Glassa8a01412022-07-11 19:04:04 -0600727
728 output_lines = []
729 for params in params_list:
730 line = ''
Simon Glass969fd332022-07-11 19:04:05 -0600731 for field in fields:
Simon Glassa8a01412022-07-11 19:04:04 -0600732 # insert two spaces between fields like column -t would
Simon Glass969fd332022-07-11 19:04:05 -0600733 line += ' ' + params[field].ljust(max_length[field])
Simon Glassa8a01412022-07-11 19:04:04 -0600734 output_lines.append(line.strip())
735
736 # ignore case when sorting
737 output_lines.sort(key=str.lower)
738
Simon Glass969fd332022-07-11 19:04:05 -0600739 with open(output, 'w', encoding="utf-8") as outf:
740 outf.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n')
Simon Glassa8a01412022-07-11 19:04:04 -0600741
742 def ensure_board_list(self, output, jobs=1, force=False, quiet=False):
743 """Generate a board database file if needed.
744
Simon Glass969fd332022-07-11 19:04:05 -0600745 Args:
746 output (str): The name of the output file
747 jobs (int): The number of jobs to run simultaneously
748 force (bool): Force to generate the output even if it is new
749 quiet (bool): True to avoid printing a message if nothing needs doing
Simon Glassadd76e72022-07-11 19:04:08 -0600750
751 Returns:
752 bool: True if all is well, False if there were warnings
Simon Glassa8a01412022-07-11 19:04:04 -0600753 """
754 if not force and output_is_new(output):
755 if not quiet:
Simon Glass969fd332022-07-11 19:04:05 -0600756 print(f'{output} is up to date. Nothing to do.')
Simon Glassadd76e72022-07-11 19:04:08 -0600757 return True
Simon Glassa8a01412022-07-11 19:04:04 -0600758 params_list = self.scan_defconfigs(jobs)
Simon Glassadd76e72022-07-11 19:04:08 -0600759 warnings = self.insert_maintainers_info(params_list)
760 for warn in warnings:
761 print(warn, file=sys.stderr)
Simon Glassa8a01412022-07-11 19:04:04 -0600762 self.format_and_output(params_list, output)
Simon Glassadd76e72022-07-11 19:04:08 -0600763 return not warnings