Prepare v2023.10
[platform/kernel/u-boot.git] / tools / buildman / boards.py
1 # SPDX-License-Identifier: GPL-2.0+
2 # Copyright (c) 2012 The Chromium OS Authors.
3 # Author: Simon Glass <sjg@chromium.org>
4 # Author: Masahiro Yamada <yamada.m@jp.panasonic.com>
5
6 """Maintains a list of boards and allows them to be selected"""
7
8 from collections import OrderedDict
9 import errno
10 import fnmatch
11 import glob
12 import multiprocessing
13 import os
14 import re
15 import sys
16 import tempfile
17 import time
18
19 from buildman import board
20 from buildman import kconfiglib
21
22
23 ### constant variables ###
24 OUTPUT_FILE = 'boards.cfg'
25 CONFIG_DIR = 'configs'
26 SLEEP_TIME = 0.03
27 COMMENT_BLOCK = f'''#
28 # List of boards
29 #   Automatically generated by {__file__}: don't edit
30 #
31 # Status, Arch, CPU, SoC, Vendor, Board, Target, Config, Maintainers
32
33 '''
34
35
36 def 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     """
45     try:
46         os.remove(fname)
47     except OSError as exception:
48         # Ignore 'No such file or directory' error
49         if exception.errno != errno.ENOENT:
50             raise
51
52
53 def output_is_new(output, config_dir, srcdir):
54     """Check if the output file is up to date.
55
56     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         config_dir (str): Directory containing defconfig files
63         srcdir (str): Directory containing Kconfig and MAINTAINERS files
64
65     Returns:
66         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
71     """
72     # pylint: disable=too-many-branches
73     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
79         raise
80
81     for (dirpath, _, filenames) in os.walk(config_dir):
82         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
89     for (dirpath, _, filenames) in os.walk(srcdir):
90         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
101     with open(output, encoding="utf-8") as inf:
102         for line in inf:
103             if 'Options,' in line:
104                 return False
105             if line[0] == '#' or line == '\n':
106                 continue
107             defconfig = line.split()[6] + '_defconfig'
108             if not os.path.exists(os.path.join(config_dir, defconfig)):
109                 return False
110
111     return True
112
113
114 class 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:
121             expr (str): String cotaining regular expression to store
122         """
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:
130            props (list of str): List of properties to check
131         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
142 class 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:
156             expr (Expr): New Expr object to add to the list of those that must
157                   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:
171            props (list of str): List of properties to check
172         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
180
181 class 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',
193         # 'target' is added later
194     }
195
196     def __init__(self, srctree):
197         """Scan all the Kconfig files and create a Kconfig object."""
198         # Define environment variables referenced from Kconfig
199         os.environ['srctree'] = srctree
200         os.environ['UBOOTVERSION'] = 'dummy'
201         os.environ['KCONFIG_OBJDIR'] = ''
202         self._tmpfile = None
203         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         """
213         if self._tmpfile:
214             try_remove(self._tmpfile)
215
216     def scan(self, defconfig, warn_targets):
217         """Load a defconfig file to obtain board parameters.
218
219         Args:
220             defconfig (str): path to the defconfig file to be processed
221             warn_targets (bool): True to warn about missing or duplicate
222                 CONFIG_TARGET options
223
224         Returns:
225             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
236         """
237         leaf = os.path.basename(defconfig)
238         expect_target, match, rear = leaf.partition('_defconfig')
239         assert match and not rear, f'{leaf} : invalid defconfig'
240
241         self._conf.load_config(defconfig)
242         self._tmpfile = None
243
244         params = {}
245         warnings = []
246
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
256         # Check there is exactly one TARGET_xxx set
257         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
267
268             if not target:
269                 cfg_name = expect_target.replace('-', '_').upper()
270                 warnings.append(f'WARNING: {leaf}: No TARGET_{cfg_name} enabled')
271
272         params['target'] = expect_target
273
274         # fix-up for aarch64
275         if params['arch'] == 'arm' and params['cpu'] == 'armv8':
276             params['arch'] = 'aarch64'
277
278         # 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
289         return params, warnings
290
291
292 class MaintainersDatabase:
293
294     """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 :
302         warnings (list of str): List of warnings due to missing status, etc.
303     """
304
305     def __init__(self):
306         """Create an empty database."""
307         self.database = {}
308         self.warnings = []
309
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
317         Args:
318             target (str): Build-target name
319
320         Returns:
321             str: 'Active', 'Orphan' or '-'.
322         """
323         if not target in self.database:
324             self.warnings.append(f"WARNING: no status info for '{target}'")
325             return '-'
326
327         tmp = self.database[target][0]
328         if tmp.startswith('Maintained'):
329             return 'Active'
330         if tmp.startswith('Supported'):
331             return 'Active'
332         if tmp.startswith('Orphan'):
333             return 'Orphan'
334         self.warnings.append(f"WARNING: {tmp}: unknown status for '{target}'")
335         return '-'
336
337     def get_maintainers(self, target):
338         """Return the maintainers of the given board.
339
340         Args:
341             target (str): Build-target name
342
343         Returns:
344             str: Maintainers of the board.  If the board has two or more
345             maintainers, they are separated with colons.
346         """
347         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)
353
354         self.warnings.append(f"WARNING: no maintainers for '{target}'")
355         return ''
356
357     def parse_file(self, srcdir, fname):
358         """Parse a MAINTAINERS file.
359
360         Parse a MAINTAINERS file and accumulate board status and maintainers
361         information in the self.database dict.
362
363         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
370         Args:
371             srcdir (str): Directory containing source code (Kconfig files)
372             fname (str): MAINTAINERS file to be parsed
373         """
374         def add_targets(linenum):
375             """Add any new targets
376
377             Args:
378                 linenum (int): Current line number
379             """
380             if targets:
381                 for target in targets:
382                     self.database[target] = (status, maintainers)
383
384         targets = []
385         maintainers = []
386         status = '-'
387         with open(fname, encoding="utf-8") as inf:
388             for linenum, line in enumerate(inf):
389                 # Check also commented maintainers
390                 if line[:3] == '#M:':
391                     line = line[1:]
392                 tag, rest = line[:2], line[2:].strip()
393                 if tag == 'M:':
394                     maintainers.append(rest)
395                 elif tag == 'F:':
396                     # expand wildcard and filter by 'configs/*_defconfig'
397                     glob_path = os.path.join(srcdir, rest)
398                     for item in glob.glob(glob_path):
399                         front, match, rear = item.partition('configs/')
400                         if front.endswith('/'):
401                             front = front[:-1]
402                         if front == srcdir and match:
403                             front, match, rear = rear.rpartition('_defconfig')
404                             if match and not rear:
405                                 targets.append(front)
406                 elif tag == 'S:':
407                     status = rest
408                 elif tag == 'N:':
409                     # Just scan the configs directory since that's all we care
410                     # about
411                     walk_path = os.walk(os.path.join(srcdir, 'configs'))
412                     for dirpath, _, fnames in walk_path:
413                         for cfg in fnames:
414                             path = os.path.join(dirpath, cfg)[len(srcdir) + 1:]
415                             front, match, rear = path.partition('configs/')
416                             if front or not match:
417                                 continue
418                             front, match, rear = rear.rpartition('_defconfig')
419
420                             # Use this entry if it matches the defconfig file
421                             # without the _defconfig suffix. For example
422                             # 'am335x.*' matches am335x_guardian_defconfig
423                             if match and not rear and re.search(rest, front):
424                                 targets.append(front)
425                 elif line == '\n':
426                     add_targets(linenum)
427                     targets = []
428                     maintainers = []
429                     status = '-'
430         add_targets(linenum)
431
432
433 class Boards:
434     """Manage a list of boards."""
435     def __init__(self):
436         self._boards = []
437
438     def add_board(self, brd):
439         """Add a new board to the list.
440
441         The board's target member must not already exist in the board list.
442
443         Args:
444             brd (Board): board to add
445         """
446         self._boards.append(brd)
447
448     def read_boards(self, fname):
449         """Read a list of boards from a board file.
450
451         Create a Board object for each and add it to our _boards list.
452
453         Args:
454             fname (str): Filename of boards.cfg file
455         """
456         with open(fname, 'r', encoding='utf-8') as inf:
457             for line in inf:
458                 if line[0] == '#':
459                     continue
460                 fields = line.split()
461                 if not fields:
462                     continue
463                 for upto, field in enumerate(fields):
464                     if field == '-':
465                         fields[upto] = ''
466                 while len(fields) < 8:
467                     fields.append('')
468                 if len(fields) > 8:
469                     fields = fields[:8]
470
471                 brd = board.Board(*fields)
472                 self.add_board(brd)
473
474
475     def get_list(self):
476         """Return a list of available boards.
477
478         Returns:
479             List of Board objects
480         """
481         return self._boards
482
483     def get_dict(self):
484         """Build a dictionary containing all the boards.
485
486         Returns:
487             Dictionary:
488                 key is board.target
489                 value is board
490         """
491         board_dict = OrderedDict()
492         for brd in self._boards:
493             board_dict[brd.target] = brd
494         return board_dict
495
496     def get_selected_dict(self):
497         """Return a dictionary containing the selected boards
498
499         Returns:
500             List of Board objects that are marked selected
501         """
502         board_dict = OrderedDict()
503         for brd in self._boards:
504             if brd.build_it:
505                 board_dict[brd.target] = brd
506         return board_dict
507
508     def get_selected(self):
509         """Return a list of selected boards
510
511         Returns:
512             List of Board objects that are marked selected
513         """
514         return [brd for brd in self._boards if brd.build_it]
515
516     def get_selected_names(self):
517         """Return a list of selected boards
518
519         Returns:
520             List of board names that are marked selected
521         """
522         return [brd.target for brd in self._boards if brd.build_it]
523
524     @classmethod
525     def _build_terms(cls, args):
526         """Convert command line arguments to a list of terms.
527
528         This deals with parsing of the arguments. It handles the '&'
529         operator, which joins several expressions into a single Term.
530
531         For example:
532             ['arm & freescale sandbox', 'tegra']
533
534         will produce 3 Terms containing expressions as follows:
535             arm, freescale
536             sandbox
537             tegra
538
539         The first Term has two expressions, both of which must match for
540         a board to be selected.
541
542         Args:
543             args (list of str): List of command line arguments
544
545         Returns:
546             list of Term: A list of Term objects
547         """
548         syms = []
549         for arg in args:
550             for word in arg.split():
551                 sym_build = []
552                 for term in word.split('&'):
553                     if term:
554                         sym_build.append(term)
555                     sym_build.append('&')
556                 syms += sym_build[:-1]
557         terms = []
558         term = None
559         oper = None
560         for sym in syms:
561             if sym == '&':
562                 oper = sym
563             elif oper:
564                 term.add_expr(sym)
565                 oper = None
566             else:
567                 if term:
568                     terms.append(term)
569                 term = Term()
570                 term.add_expr(sym)
571         if term:
572             terms.append(term)
573         return terms
574
575     def select_boards(self, args, exclude=None, brds=None):
576         """Mark boards selected based on args
577
578         Normally either boards (an explicit list of boards) or args (a list of
579         terms to match against) is used. It is possible to specify both, in
580         which case they are additive.
581
582         If brds and args are both empty, all boards are selected.
583
584         Args:
585             args (list of str): List of strings specifying boards to include,
586                 either named, or by their target, architecture, cpu, vendor or
587                 soc. If empty, all boards are selected.
588             exclude (list of str): List of boards to exclude, regardless of
589                 'args', or None for none
590             brds (list of Board): List of boards to build, or None/[] for all
591
592         Returns:
593             Tuple
594                 Dictionary which holds the list of boards which were selected
595                     due to each argument, arranged by argument.
596                 List of errors found
597         """
598         def _check_board(brd):
599             """Check whether to include or exclude a board
600
601             Checks the various terms and decide whether to build it or not (the
602             'build_it' variable).
603
604             If it is built, add the board to the result[term] list so we know
605             which term caused it to be built. Add it to result['all'] also.
606
607             Keep a list of boards we found in 'found', so we can report boards
608             which appear in self._boards but not in brds.
609
610             Args:
611                 brd (Board): Board to check
612             """
613             matching_term = None
614             build_it = False
615             if terms:
616                 for term in terms:
617                     if term.matches(brd.props):
618                         matching_term = str(term)
619                         build_it = True
620                         break
621             elif brds:
622                 if brd.target in brds:
623                     build_it = True
624                     found.append(brd.target)
625             else:
626                 build_it = True
627
628             # Check that it is not specifically excluded
629             for expr in exclude_list:
630                 if expr.matches(brd.props):
631                     build_it = False
632                     break
633
634             if build_it:
635                 brd.build_it = True
636                 if matching_term:
637                     result[matching_term].append(brd.target)
638                 result['all'].append(brd.target)
639
640         result = OrderedDict()
641         warnings = []
642         terms = self._build_terms(args)
643
644         result['all'] = []
645         for term in terms:
646             result[str(term)] = []
647
648         exclude_list = []
649         if exclude:
650             for expr in exclude:
651                 exclude_list.append(Expr(expr))
652
653         found = []
654         for brd in self._boards:
655             _check_board(brd)
656
657         if brds:
658             remaining = set(brds) - set(found)
659             if remaining:
660                 warnings.append(f"Boards not found: {', '.join(remaining)}\n")
661
662         return result, warnings
663
664     @classmethod
665     def scan_defconfigs_for_multiprocess(cls, srcdir, queue, defconfigs,
666                                          warn_targets):
667         """Scan defconfig files and queue their board parameters
668
669         This function is intended to be passed to multiprocessing.Process()
670         constructor.
671
672         Args:
673             srcdir (str): Directory containing source code
674             queue (multiprocessing.Queue): The resulting board parameters are
675                 written into this.
676             defconfigs (sequence of str): A sequence of defconfig files to be
677                 scanned.
678             warn_targets (bool): True to warn about missing or duplicate
679                 CONFIG_TARGET options
680         """
681         kconf_scanner = KconfigScanner(srcdir)
682         for defconfig in defconfigs:
683             queue.put(kconf_scanner.scan(defconfig, warn_targets))
684
685     @classmethod
686     def read_queues(cls, queues, params_list, warnings):
687         """Read the queues and append the data to the paramers list
688
689         Args:
690             queues (list of multiprocessing.Queue): Queues to read
691             params_list (list of dict): List to add params too
692             warnings (set of str): Set to add warnings to
693         """
694         for que in queues:
695             while not que.empty():
696                 params, warn = que.get()
697                 params_list.append(params)
698                 warnings.update(warn)
699
700     def scan_defconfigs(self, config_dir, srcdir, jobs=1, warn_targets=False):
701         """Collect board parameters for all defconfig files.
702
703         This function invokes multiple processes for faster processing.
704
705         Args:
706             config_dir (str): Directory containing the defconfig files
707             srcdir (str): Directory containing source code (Kconfig files)
708             jobs (int): The number of jobs to run simultaneously
709             warn_targets (bool): True to warn about missing or duplicate
710                 CONFIG_TARGET options
711
712         Returns:
713             tuple:
714                 list of dict: List of board parameters, each a dict:
715                     key: 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
716                         'config'
717                     value: string value of the key
718                 list of str: List of warnings recorded
719         """
720         all_defconfigs = []
721         for (dirpath, _, filenames) in os.walk(config_dir):
722             for filename in fnmatch.filter(filenames, '*_defconfig'):
723                 if fnmatch.fnmatch(filename, '.*'):
724                     continue
725                 all_defconfigs.append(os.path.join(dirpath, filename))
726
727         total_boards = len(all_defconfigs)
728         processes = []
729         queues = []
730         for i in range(jobs):
731             defconfigs = all_defconfigs[total_boards * i // jobs :
732                                         total_boards * (i + 1) // jobs]
733             que = multiprocessing.Queue(maxsize=-1)
734             proc = multiprocessing.Process(
735                 target=self.scan_defconfigs_for_multiprocess,
736                 args=(srcdir, que, defconfigs, warn_targets))
737             proc.start()
738             processes.append(proc)
739             queues.append(que)
740
741         # The resulting data should be accumulated to these lists
742         params_list = []
743         warnings = set()
744
745         # Data in the queues should be retrieved preriodically.
746         # Otherwise, the queues would become full and subprocesses would get stuck.
747         while any(p.is_alive() for p in processes):
748             self.read_queues(queues, params_list, warnings)
749             # sleep for a while until the queues are filled
750             time.sleep(SLEEP_TIME)
751
752         # Joining subprocesses just in case
753         # (All subprocesses should already have been finished)
754         for proc in processes:
755             proc.join()
756
757         # retrieve leftover data
758         self.read_queues(queues, params_list, warnings)
759
760         return params_list, sorted(list(warnings))
761
762     @classmethod
763     def insert_maintainers_info(cls, srcdir, params_list):
764         """Add Status and Maintainers information to the board parameters list.
765
766         Args:
767             params_list (list of dict): A list of the board parameters
768
769         Returns:
770             list of str: List of warnings collected due to missing status, etc.
771         """
772         database = MaintainersDatabase()
773         for (dirpath, _, filenames) in os.walk(srcdir):
774             if 'MAINTAINERS' in filenames and 'tools/buildman' not in dirpath:
775                 database.parse_file(srcdir,
776                                     os.path.join(dirpath, 'MAINTAINERS'))
777
778         for i, params in enumerate(params_list):
779             target = params['target']
780             maintainers = database.get_maintainers(target)
781             params['maintainers'] = maintainers
782             if maintainers:
783                 params['status'] = database.get_status(target)
784             else:
785                 params['status'] = '-'
786             params_list[i] = params
787         return sorted(database.warnings)
788
789     @classmethod
790     def format_and_output(cls, params_list, output):
791         """Write board parameters into a file.
792
793         Columnate the board parameters, sort lines alphabetically,
794         and then write them to a file.
795
796         Args:
797             params_list (list of dict): The list of board parameters
798             output (str): The path to the output file
799         """
800         fields = ('status', 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
801                   'config', 'maintainers')
802
803         # First, decide the width of each column
804         max_length = {f: 0 for f in fields}
805         for params in params_list:
806             for field in fields:
807                 max_length[field] = max(max_length[field], len(params[field]))
808
809         output_lines = []
810         for params in params_list:
811             line = ''
812             for field in fields:
813                 # insert two spaces between fields like column -t would
814                 line += '  ' + params[field].ljust(max_length[field])
815             output_lines.append(line.strip())
816
817         # ignore case when sorting
818         output_lines.sort(key=str.lower)
819
820         with open(output, 'w', encoding="utf-8") as outf:
821             outf.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n')
822
823     def build_board_list(self, config_dir=CONFIG_DIR, srcdir='.', jobs=1,
824                          warn_targets=False):
825         """Generate a board-database file
826
827         This works by reading the Kconfig, then loading each board's defconfig
828         in to get the setting for each option. In particular, CONFIG_TARGET_xxx
829         is typically set by the defconfig, where xxx is the target to build.
830
831         Args:
832             config_dir (str): Directory containing the defconfig files
833             srcdir (str): Directory containing source code (Kconfig files)
834             jobs (int): The number of jobs to run simultaneously
835             warn_targets (bool): True to warn about missing or duplicate
836                 CONFIG_TARGET options
837
838         Returns:
839             tuple:
840                 list of dict: List of board parameters, each a dict:
841                     key: 'arch', 'cpu', 'soc', 'vendor', 'board', 'config',
842                          'target'
843                     value: string value of the key
844                 list of str: Warnings that came up
845         """
846         params_list, warnings = self.scan_defconfigs(config_dir, srcdir, jobs,
847                                                      warn_targets)
848         m_warnings = self.insert_maintainers_info(srcdir, params_list)
849         return params_list, warnings + m_warnings
850
851     def ensure_board_list(self, output, jobs=1, force=False, quiet=False):
852         """Generate a board database file if needed.
853
854         This is intended to check if Kconfig has changed since the boards.cfg
855         files was generated.
856
857         Args:
858             output (str): The name of the output file
859             jobs (int): The number of jobs to run simultaneously
860             force (bool): Force to generate the output even if it is new
861             quiet (bool): True to avoid printing a message if nothing needs doing
862
863         Returns:
864             bool: True if all is well, False if there were warnings
865         """
866         if not force and output_is_new(output, CONFIG_DIR, '.'):
867             if not quiet:
868                 print(f'{output} is up to date. Nothing to do.')
869             return True
870         params_list, warnings = self.build_board_list(CONFIG_DIR, '.', jobs)
871         for warn in warnings:
872             print(warn, file=sys.stderr)
873         self.format_and_output(params_list, output)
874         return not warnings