blob: b30b344bc8ed03f0b756b72ea089141a32a07d96 [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
Simon Glassa8a01412022-07-11 19:04:04 -0600266 return params
267
268
269class MaintainersDatabase:
270
Simon Glass969fd332022-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 Glassadd76e72022-07-11 19:04:08 -0600279 warnings (list of str): List of warnings due to missing status, etc.
Simon Glass969fd332022-07-11 19:04:05 -0600280 """
Simon Glassa8a01412022-07-11 19:04:04 -0600281
282 def __init__(self):
283 """Create an empty database."""
284 self.database = {}
Simon Glassadd76e72022-07-11 19:04:08 -0600285 self.warnings = []
Simon Glassa8a01412022-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 Glass969fd332022-07-11 19:04:05 -0600294 Args:
295 target (str): Build-target name
296
Simon Glassa8a01412022-07-11 19:04:04 -0600297 Returns:
Simon Glass969fd332022-07-11 19:04:05 -0600298 str: 'Active', 'Orphan' or '-'.
Simon Glassa8a01412022-07-11 19:04:04 -0600299 """
300 if not target in self.database:
Simon Glassadd76e72022-07-11 19:04:08 -0600301 self.warnings.append(f"WARNING: no status info for '{target}'")
Simon Glassa8a01412022-07-11 19:04:04 -0600302 return '-'
303
304 tmp = self.database[target][0]
305 if tmp.startswith('Maintained'):
306 return 'Active'
Simon Glass969fd332022-07-11 19:04:05 -0600307 if tmp.startswith('Supported'):
Simon Glassa8a01412022-07-11 19:04:04 -0600308 return 'Active'
Simon Glass969fd332022-07-11 19:04:05 -0600309 if tmp.startswith('Orphan'):
Simon Glassa8a01412022-07-11 19:04:04 -0600310 return 'Orphan'
Simon Glassadd76e72022-07-11 19:04:08 -0600311 self.warnings.append(f"WARNING: {tmp}: unknown status for '{target}'")
Simon Glass969fd332022-07-11 19:04:05 -0600312 return '-'
Simon Glassa8a01412022-07-11 19:04:04 -0600313
314 def get_maintainers(self, target):
315 """Return the maintainers of the given board.
316
Simon Glass969fd332022-07-11 19:04:05 -0600317 Args:
318 target (str): Build-target name
319
Simon Glassa8a01412022-07-11 19:04:04 -0600320 Returns:
Simon Glass969fd332022-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 Glassa8a01412022-07-11 19:04:04 -0600323 """
324 if not target in self.database:
Simon Glassadd76e72022-07-11 19:04:08 -0600325 self.warnings.append(f"WARNING: no maintainers for '{target}'")
Simon Glassa8a01412022-07-11 19:04:04 -0600326 return ''
327
328 return ':'.join(self.database[target][1])
329
Simon Glass969fd332022-07-11 19:04:05 -0600330 def parse_file(self, fname):
Simon Glassa8a01412022-07-11 19:04:04 -0600331 """Parse a MAINTAINERS file.
332
Simon Glass969fd332022-07-11 19:04:05 -0600333 Parse a MAINTAINERS file and accumulate board status and maintainers
334 information in the self.database dict.
Simon Glassa8a01412022-07-11 19:04:04 -0600335
Simon Glass969fd332022-07-11 19:04:05 -0600336 Args:
337 fname (str): MAINTAINERS file to be parsed
Simon Glassa8a01412022-07-11 19:04:04 -0600338 """
339 targets = []
340 maintainers = []
341 status = '-'
Simon Glass969fd332022-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 Glassa8a01412022-07-11 19:04:04 -0600366 if targets:
367 for target in targets:
368 self.database[target] = (status, maintainers)
369
370
Simon Glassc52bd222022-07-11 19:04:03 -0600371class Boards:
372 """Manage a list of boards."""
373 def __init__(self):
374 # Use a simple list here, sinc OrderedDict requires Python 2.7
375 self._boards = []
376
377 def add_board(self, brd):
378 """Add a new board to the list.
379
380 The board's target member must not already exist in the board list.
381
382 Args:
Simon Glass969fd332022-07-11 19:04:05 -0600383 brd (Board): board to add
Simon Glassc52bd222022-07-11 19:04:03 -0600384 """
385 self._boards.append(brd)
386
387 def read_boards(self, fname):
388 """Read a list of boards from a board file.
389
390 Create a Board object for each and add it to our _boards list.
391
392 Args:
Simon Glass969fd332022-07-11 19:04:05 -0600393 fname (str): Filename of boards.cfg file
Simon Glassc52bd222022-07-11 19:04:03 -0600394 """
395 with open(fname, 'r', encoding='utf-8') as inf:
396 for line in inf:
397 if line[0] == '#':
398 continue
399 fields = line.split()
400 if not fields:
401 continue
402 for upto, field in enumerate(fields):
403 if field == '-':
404 fields[upto] = ''
405 while len(fields) < 8:
406 fields.append('')
407 if len(fields) > 8:
408 fields = fields[:8]
409
410 brd = board.Board(*fields)
411 self.add_board(brd)
412
413
414 def get_list(self):
415 """Return a list of available boards.
416
417 Returns:
418 List of Board objects
419 """
420 return self._boards
421
422 def get_dict(self):
423 """Build a dictionary containing all the boards.
424
425 Returns:
426 Dictionary:
427 key is board.target
428 value is board
429 """
430 board_dict = OrderedDict()
431 for brd in self._boards:
432 board_dict[brd.target] = brd
433 return board_dict
434
435 def get_selected_dict(self):
436 """Return a dictionary containing the selected boards
437
438 Returns:
439 List of Board objects that are marked selected
440 """
441 board_dict = OrderedDict()
442 for brd in self._boards:
443 if brd.build_it:
444 board_dict[brd.target] = brd
445 return board_dict
446
447 def get_selected(self):
448 """Return a list of selected boards
449
450 Returns:
451 List of Board objects that are marked selected
452 """
453 return [brd for brd in self._boards if brd.build_it]
454
455 def get_selected_names(self):
456 """Return a list of selected boards
457
458 Returns:
459 List of board names that are marked selected
460 """
461 return [brd.target for brd in self._boards if brd.build_it]
462
463 @classmethod
464 def _build_terms(cls, args):
465 """Convert command line arguments to a list of terms.
466
467 This deals with parsing of the arguments. It handles the '&'
468 operator, which joins several expressions into a single Term.
469
470 For example:
471 ['arm & freescale sandbox', 'tegra']
472
473 will produce 3 Terms containing expressions as follows:
474 arm, freescale
475 sandbox
476 tegra
477
478 The first Term has two expressions, both of which must match for
479 a board to be selected.
480
481 Args:
Simon Glass969fd332022-07-11 19:04:05 -0600482 args (list of str): List of command line arguments
483
Simon Glassc52bd222022-07-11 19:04:03 -0600484 Returns:
Simon Glass969fd332022-07-11 19:04:05 -0600485 list of Term: A list of Term objects
Simon Glassc52bd222022-07-11 19:04:03 -0600486 """
487 syms = []
488 for arg in args:
489 for word in arg.split():
490 sym_build = []
491 for term in word.split('&'):
492 if term:
493 sym_build.append(term)
494 sym_build.append('&')
495 syms += sym_build[:-1]
496 terms = []
497 term = None
498 oper = None
499 for sym in syms:
500 if sym == '&':
501 oper = sym
502 elif oper:
503 term.add_expr(sym)
504 oper = None
505 else:
506 if term:
507 terms.append(term)
508 term = Term()
509 term.add_expr(sym)
510 if term:
511 terms.append(term)
512 return terms
513
514 def select_boards(self, args, exclude=None, brds=None):
515 """Mark boards selected based on args
516
517 Normally either boards (an explicit list of boards) or args (a list of
518 terms to match against) is used. It is possible to specify both, in
519 which case they are additive.
520
521 If brds and args are both empty, all boards are selected.
522
523 Args:
Simon Glass969fd332022-07-11 19:04:05 -0600524 args (list of str): List of strings specifying boards to include,
525 either named, or by their target, architecture, cpu, vendor or
526 soc. If empty, all boards are selected.
527 exclude (list of str): List of boards to exclude, regardless of
528 'args', or None for none
529 brds (list of Board): List of boards to build, or None/[] for all
Simon Glassc52bd222022-07-11 19:04:03 -0600530
531 Returns:
532 Tuple
533 Dictionary which holds the list of boards which were selected
534 due to each argument, arranged by argument.
535 List of errors found
536 """
Simon Glass969fd332022-07-11 19:04:05 -0600537 def _check_board(brd):
538 """Check whether to include or exclude a board
Simon Glassc52bd222022-07-11 19:04:03 -0600539
Simon Glass969fd332022-07-11 19:04:05 -0600540 Checks the various terms and decide whether to build it or not (the
541 'build_it' variable).
Simon Glassc52bd222022-07-11 19:04:03 -0600542
Simon Glass969fd332022-07-11 19:04:05 -0600543 If it is built, add the board to the result[term] list so we know
544 which term caused it to be built. Add it to result['all'] also.
Simon Glassc52bd222022-07-11 19:04:03 -0600545
Simon Glass969fd332022-07-11 19:04:05 -0600546 Keep a list of boards we found in 'found', so we can report boards
547 which appear in self._boards but not in brds.
548
549 Args:
550 brd (Board): Board to check
551 """
Simon Glassc52bd222022-07-11 19:04:03 -0600552 matching_term = None
553 build_it = False
554 if terms:
555 for term in terms:
556 if term.matches(brd.props):
557 matching_term = str(term)
558 build_it = True
559 break
560 elif brds:
561 if brd.target in brds:
562 build_it = True
563 found.append(brd.target)
564 else:
565 build_it = True
566
567 # Check that it is not specifically excluded
568 for expr in exclude_list:
569 if expr.matches(brd.props):
570 build_it = False
571 break
572
573 if build_it:
574 brd.build_it = True
575 if matching_term:
576 result[matching_term].append(brd.target)
577 result['all'].append(brd.target)
578
Simon Glass969fd332022-07-11 19:04:05 -0600579 result = OrderedDict()
580 warnings = []
581 terms = self._build_terms(args)
582
583 result['all'] = []
584 for term in terms:
585 result[str(term)] = []
586
587 exclude_list = []
588 if exclude:
589 for expr in exclude:
590 exclude_list.append(Expr(expr))
591
592 found = []
593 for brd in self._boards:
594 _check_board(brd)
595
Simon Glassc52bd222022-07-11 19:04:03 -0600596 if brds:
597 remaining = set(brds) - set(found)
598 if remaining:
599 warnings.append(f"Boards not found: {', '.join(remaining)}\n")
600
601 return result, warnings
Simon Glassa8a01412022-07-11 19:04:04 -0600602
Simon Glass969fd332022-07-11 19:04:05 -0600603 @classmethod
604 def scan_defconfigs_for_multiprocess(cls, queue, defconfigs):
Simon Glassa8a01412022-07-11 19:04:04 -0600605 """Scan defconfig files and queue their board parameters
606
Simon Glass969fd332022-07-11 19:04:05 -0600607 This function is intended to be passed to multiprocessing.Process()
608 constructor.
Simon Glassa8a01412022-07-11 19:04:04 -0600609
Simon Glass969fd332022-07-11 19:04:05 -0600610 Args:
611 queue (multiprocessing.Queue): The resulting board parameters are
612 written into this.
613 defconfigs (sequence of str): A sequence of defconfig files to be
614 scanned.
Simon Glassa8a01412022-07-11 19:04:04 -0600615 """
616 kconf_scanner = KconfigScanner()
617 for defconfig in defconfigs:
618 queue.put(kconf_scanner.scan(defconfig))
619
Simon Glass969fd332022-07-11 19:04:05 -0600620 @classmethod
621 def read_queues(cls, queues, params_list):
Simon Glassa8a01412022-07-11 19:04:04 -0600622 """Read the queues and append the data to the paramers list"""
Simon Glass969fd332022-07-11 19:04:05 -0600623 for que in queues:
624 while not que.empty():
625 params_list.append(que.get())
Simon Glassa8a01412022-07-11 19:04:04 -0600626
627 def scan_defconfigs(self, jobs=1):
628 """Collect board parameters for all defconfig files.
629
630 This function invokes multiple processes for faster processing.
631
Simon Glass969fd332022-07-11 19:04:05 -0600632 Args:
633 jobs (int): The number of jobs to run simultaneously
Simon Glassa8a01412022-07-11 19:04:04 -0600634 """
635 all_defconfigs = []
Simon Glass969fd332022-07-11 19:04:05 -0600636 for (dirpath, _, filenames) in os.walk(CONFIG_DIR):
Simon Glassa8a01412022-07-11 19:04:04 -0600637 for filename in fnmatch.filter(filenames, '*_defconfig'):
638 if fnmatch.fnmatch(filename, '.*'):
639 continue
640 all_defconfigs.append(os.path.join(dirpath, filename))
641
642 total_boards = len(all_defconfigs)
643 processes = []
644 queues = []
645 for i in range(jobs):
646 defconfigs = all_defconfigs[total_boards * i // jobs :
647 total_boards * (i + 1) // jobs]
Simon Glass969fd332022-07-11 19:04:05 -0600648 que = multiprocessing.Queue(maxsize=-1)
649 proc = multiprocessing.Process(
Simon Glassa8a01412022-07-11 19:04:04 -0600650 target=self.scan_defconfigs_for_multiprocess,
Simon Glass969fd332022-07-11 19:04:05 -0600651 args=(que, defconfigs))
652 proc.start()
653 processes.append(proc)
654 queues.append(que)
Simon Glassa8a01412022-07-11 19:04:04 -0600655
656 # The resulting data should be accumulated to this list
657 params_list = []
658
659 # Data in the queues should be retrieved preriodically.
660 # Otherwise, the queues would become full and subprocesses would get stuck.
Simon Glass969fd332022-07-11 19:04:05 -0600661 while any(p.is_alive() for p in processes):
Simon Glassa8a01412022-07-11 19:04:04 -0600662 self.read_queues(queues, params_list)
663 # sleep for a while until the queues are filled
664 time.sleep(SLEEP_TIME)
665
666 # Joining subprocesses just in case
667 # (All subprocesses should already have been finished)
Simon Glass969fd332022-07-11 19:04:05 -0600668 for proc in processes:
669 proc.join()
Simon Glassa8a01412022-07-11 19:04:04 -0600670
671 # retrieve leftover data
672 self.read_queues(queues, params_list)
673
674 return params_list
675
Simon Glass969fd332022-07-11 19:04:05 -0600676 @classmethod
677 def insert_maintainers_info(cls, params_list):
Simon Glassa8a01412022-07-11 19:04:04 -0600678 """Add Status and Maintainers information to the board parameters list.
679
Simon Glass969fd332022-07-11 19:04:05 -0600680 Args:
681 params_list (list of dict): A list of the board parameters
Simon Glassadd76e72022-07-11 19:04:08 -0600682
683 Returns:
684 list of str: List of warnings collected due to missing status, etc.
Simon Glassa8a01412022-07-11 19:04:04 -0600685 """
686 database = MaintainersDatabase()
Simon Glass969fd332022-07-11 19:04:05 -0600687 for (dirpath, _, filenames) in os.walk('.'):
Simon Glassa8a01412022-07-11 19:04:04 -0600688 if 'MAINTAINERS' in filenames:
689 database.parse_file(os.path.join(dirpath, 'MAINTAINERS'))
690
691 for i, params in enumerate(params_list):
692 target = params['target']
693 params['status'] = database.get_status(target)
694 params['maintainers'] = database.get_maintainers(target)
695 params_list[i] = params
Simon Glassadd76e72022-07-11 19:04:08 -0600696 return database.warnings
Simon Glassa8a01412022-07-11 19:04:04 -0600697
Simon Glass969fd332022-07-11 19:04:05 -0600698 @classmethod
699 def format_and_output(cls, params_list, output):
Simon Glassa8a01412022-07-11 19:04:04 -0600700 """Write board parameters into a file.
701
702 Columnate the board parameters, sort lines alphabetically,
703 and then write them to a file.
704
Simon Glass969fd332022-07-11 19:04:05 -0600705 Args:
706 params_list (list of dict): The list of board parameters
707 output (str): The path to the output file
Simon Glassa8a01412022-07-11 19:04:04 -0600708 """
Simon Glass969fd332022-07-11 19:04:05 -0600709 fields = ('status', 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
Simon Glass256126c2022-07-11 19:04:06 -0600710 'config', 'maintainers')
Simon Glassa8a01412022-07-11 19:04:04 -0600711
712 # First, decide the width of each column
Simon Glass969fd332022-07-11 19:04:05 -0600713 max_length = {f: 0 for f in fields}
Simon Glassa8a01412022-07-11 19:04:04 -0600714 for params in params_list:
Simon Glass969fd332022-07-11 19:04:05 -0600715 for field in fields:
716 max_length[field] = max(max_length[field], len(params[field]))
Simon Glassa8a01412022-07-11 19:04:04 -0600717
718 output_lines = []
719 for params in params_list:
720 line = ''
Simon Glass969fd332022-07-11 19:04:05 -0600721 for field in fields:
Simon Glassa8a01412022-07-11 19:04:04 -0600722 # insert two spaces between fields like column -t would
Simon Glass969fd332022-07-11 19:04:05 -0600723 line += ' ' + params[field].ljust(max_length[field])
Simon Glassa8a01412022-07-11 19:04:04 -0600724 output_lines.append(line.strip())
725
726 # ignore case when sorting
727 output_lines.sort(key=str.lower)
728
Simon Glass969fd332022-07-11 19:04:05 -0600729 with open(output, 'w', encoding="utf-8") as outf:
730 outf.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n')
Simon Glassa8a01412022-07-11 19:04:04 -0600731
732 def ensure_board_list(self, output, jobs=1, force=False, quiet=False):
733 """Generate a board database file if needed.
734
Simon Glass969fd332022-07-11 19:04:05 -0600735 Args:
736 output (str): The name of the output file
737 jobs (int): The number of jobs to run simultaneously
738 force (bool): Force to generate the output even if it is new
739 quiet (bool): True to avoid printing a message if nothing needs doing
Simon Glassadd76e72022-07-11 19:04:08 -0600740
741 Returns:
742 bool: True if all is well, False if there were warnings
Simon Glassa8a01412022-07-11 19:04:04 -0600743 """
744 if not force and output_is_new(output):
745 if not quiet:
Simon Glass969fd332022-07-11 19:04:05 -0600746 print(f'{output} is up to date. Nothing to do.')
Simon Glassadd76e72022-07-11 19:04:08 -0600747 return True
Simon Glassa8a01412022-07-11 19:04:04 -0600748 params_list = self.scan_defconfigs(jobs)
Simon Glassadd76e72022-07-11 19:04:08 -0600749 warnings = self.insert_maintainers_info(params_list)
750 for warn in warnings:
751 print(warn, file=sys.stderr)
Simon Glassa8a01412022-07-11 19:04:04 -0600752 self.format_and_output(params_list, output)
Simon Glassadd76e72022-07-11 19:04:08 -0600753 return not warnings