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
8from collections import OrderedDict
9import errno
10import fnmatch
11import glob
12import multiprocessing
13import os
14import re
15import sys
16import tempfile
17import time
18
19from buildman import board
20from buildman import kconfiglib
21
22from u_boot_pylib.terminal import print_clear, tprint
23
24### constant variables ###
25OUTPUT_FILE = 'boards.cfg'
26CONFIG_DIR = 'configs'
27SLEEP_TIME = 0.03
28COMMENT_BLOCK = f'''#
29# List of boards
30#   Automatically generated by {__file__}: don't edit
31#
32# Status, Arch, CPU, SoC, Vendor, Board, Target, Config, Maintainers
33
34'''
35
36
37def try_remove(fname):
38    """Remove a file ignoring 'No such file or directory' error.
39
40    Args:
41        fname (str): Filename to remove
42
43    Raises:
44        OSError: output file exists but could not be removed
45    """
46    try:
47        os.remove(fname)
48    except OSError as exception:
49        # Ignore 'No such file or directory' error
50        if exception.errno != errno.ENOENT:
51            raise
52
53
54def output_is_new(output, config_dir, srcdir):
55    """Check if the output file is up to date.
56
57    Looks at defconfig and Kconfig files to make sure none is newer than the
58    output file. Also ensures that the boards.cfg does not mention any removed
59    boards.
60
61    Args:
62        output (str): Filename to check
63        config_dir (str): Directory containing defconfig files
64        srcdir (str): Directory containing Kconfig and MAINTAINERS files
65
66    Returns:
67        True if the given output file exists and is newer than any of
68        *_defconfig, MAINTAINERS and Kconfig*.  False otherwise.
69
70    Raises:
71        OSError: output file exists but could not be opened
72    """
73    # pylint: disable=too-many-branches
74    try:
75        ctime = os.path.getctime(output)
76    except OSError as exception:
77        if exception.errno == errno.ENOENT:
78            # return False on 'No such file or directory' error
79            return False
80        raise
81
82    for (dirpath, _, filenames) in os.walk(config_dir):
83        for filename in fnmatch.filter(filenames, '*_defconfig'):
84            if fnmatch.fnmatch(filename, '.*'):
85                continue
86            filepath = os.path.join(dirpath, filename)
87            if ctime < os.path.getctime(filepath):
88                return False
89
90    for (dirpath, _, filenames) in os.walk(srcdir):
91        for filename in filenames:
92            if (fnmatch.fnmatch(filename, '*~') or
93                not fnmatch.fnmatch(filename, 'Kconfig*') and
94                not filename == 'MAINTAINERS'):
95                continue
96            filepath = os.path.join(dirpath, filename)
97            if ctime < os.path.getctime(filepath):
98                return False
99
100    # Detect a board that has been removed since the current board database
101    # was generated
102    with open(output, encoding="utf-8") as inf:
103        for line in inf:
104            if 'Options,' in line:
105                return False
106            if line[0] == '#' or line == '\n':
107                continue
108            defconfig = line.split()[6] + '_defconfig'
109            if not os.path.exists(os.path.join(config_dir, defconfig)):
110                return False
111
112    return True
113
114
115class Expr:
116    """A single regular expression for matching boards to build"""
117
118    def __init__(self, expr):
119        """Set up a new Expr object.
120
121        Args:
122            expr (str): String containing regular expression to store
123        """
124        self._expr = expr
125        self._re = re.compile(expr)
126
127    def matches(self, props):
128        """Check if any of the properties match the regular expression.
129
130        Args:
131           props (list of str): List of properties to check
132        Returns:
133           True if any of the properties match the regular expression
134        """
135        for prop in props:
136            if self._re.match(prop):
137                return True
138        return False
139
140    def __str__(self):
141        return self._expr
142
143class Term:
144    """A list of expressions each of which must match with properties.
145
146    This provides a list of 'AND' expressions, meaning that each must
147    match the board properties for that board to be built.
148    """
149    def __init__(self):
150        self._expr_list = []
151        self._board_count = 0
152
153    def add_expr(self, expr):
154        """Add an Expr object to the list to check.
155
156        Args:
157            expr (Expr): New Expr object to add to the list of those that must
158                  match for a board to be built.
159        """
160        self._expr_list.append(Expr(expr))
161
162    def __str__(self):
163        """Return some sort of useful string describing the term"""
164        return '&'.join([str(expr) for expr in self._expr_list])
165
166    def matches(self, props):
167        """Check if any of the properties match this term
168
169        Each of the expressions in the term is checked. All must match.
170
171        Args:
172           props (list of str): List of properties to check
173        Returns:
174           True if all of the expressions in the Term match, else False
175        """
176        for expr in self._expr_list:
177            if not expr.matches(props):
178                return False
179        return True
180
181
182class KconfigScanner:
183
184    """Kconfig scanner."""
185
186    ### constant variable only used in this class ###
187    _SYMBOL_TABLE = {
188        'arch' : 'SYS_ARCH',
189        'cpu' : 'SYS_CPU',
190        'soc' : 'SYS_SOC',
191        'vendor' : 'SYS_VENDOR',
192        'board' : 'SYS_BOARD',
193        'config' : 'SYS_CONFIG_NAME',
194        # 'target' is added later
195    }
196
197    def __init__(self, srctree):
198        """Scan all the Kconfig files and create a Kconfig object."""
199        # Define environment variables referenced from Kconfig
200        os.environ['srctree'] = srctree
201        os.environ['UBOOTVERSION'] = 'dummy'
202        os.environ['KCONFIG_OBJDIR'] = ''
203        self._tmpfile = None
204        self._conf = kconfiglib.Kconfig(warn=False)
205
206    def __del__(self):
207        """Delete a leftover temporary file before exit.
208
209        The scan() method of this class creates a temporay file and deletes
210        it on success.  If scan() method throws an exception on the way,
211        the temporary file might be left over.  In that case, it should be
212        deleted in this destructor.
213        """
214        if self._tmpfile:
215            try_remove(self._tmpfile)
216
217    def scan(self, defconfig, warn_targets):
218        """Load a defconfig file to obtain board parameters.
219
220        Args:
221            defconfig (str): path to the defconfig file to be processed
222            warn_targets (bool): True to warn about missing or duplicate
223                CONFIG_TARGET options
224
225        Returns:
226            tuple: dictionary of board parameters.  It has a form of:
227                {
228                    'arch': <arch_name>,
229                    'cpu': <cpu_name>,
230                    'soc': <soc_name>,
231                    'vendor': <vendor_name>,
232                    'board': <board_name>,
233                    'target': <target_name>,
234                    'config': <config_header_name>,
235                }
236            warnings (list of str): list of warnings found
237        """
238        leaf = os.path.basename(defconfig)
239        expect_target, match, rear = leaf.partition('_defconfig')
240        assert match and not rear, f'{leaf} : invalid defconfig'
241
242        self._conf.load_config(defconfig)
243        self._tmpfile = None
244
245        params = {}
246        warnings = []
247
248        # Get the value of CONFIG_SYS_ARCH, CONFIG_SYS_CPU, ... etc.
249        # Set '-' if the value is empty.
250        for key, symbol in list(self._SYMBOL_TABLE.items()):
251            value = self._conf.syms.get(symbol).str_value
252            if value:
253                params[key] = value
254            else:
255                params[key] = '-'
256
257        # Check there is exactly one TARGET_xxx set
258        if warn_targets:
259            target = None
260            for name, sym in self._conf.syms.items():
261                if name.startswith('TARGET_') and sym.str_value == 'y':
262                    tname = name[7:].lower()
263                    if target:
264                        warnings.append(
265                            f'WARNING: {leaf}: Duplicate TARGET_xxx: {target} and {tname}')
266                    else:
267                        target = tname
268
269            if not target:
270                cfg_name = expect_target.replace('-', '_').upper()
271                warnings.append(f'WARNING: {leaf}: No TARGET_{cfg_name} enabled')
272
273        params['target'] = expect_target
274
275        # fix-up for aarch64
276        if params['arch'] == 'arm' and params['cpu'] == 'armv8':
277            params['arch'] = 'aarch64'
278
279        # fix-up for riscv
280        if params['arch'] == 'riscv':
281            try:
282                value = self._conf.syms.get('ARCH_RV32I').str_value
283            except:
284                value = ''
285            if value == 'y':
286                params['arch'] = 'riscv32'
287            else:
288                params['arch'] = 'riscv64'
289
290        return params, warnings
291
292
293class MaintainersDatabase:
294
295    """The database of board status and maintainers.
296
297    Properties:
298        database: dict:
299            key: Board-target name (e.g. 'snow')
300            value: tuple:
301                str: Board status (e.g. 'Active')
302                str: List of maintainers, separated by :
303        warnings (list of str): List of warnings due to missing status, etc.
304    """
305
306    def __init__(self):
307        """Create an empty database."""
308        self.database = {}
309        self.warnings = []
310
311    def get_status(self, target):
312        """Return the status of the given board.
313
314        The board status is generally either 'Active' or 'Orphan'.
315        Display a warning message and return '-' if status information
316        is not found.
317
318        Args:
319            target (str): Build-target name
320
321        Returns:
322            str: 'Active', 'Orphan' or '-'.
323        """
324        if not target in self.database:
325            self.warnings.append(f"WARNING: no status info for '{target}'")
326            return '-'
327
328        tmp = self.database[target][0]
329        if tmp.startswith('Maintained'):
330            return 'Active'
331        if tmp.startswith('Supported'):
332            return 'Active'
333        if tmp.startswith('Orphan'):
334            return 'Orphan'
335        self.warnings.append(f"WARNING: {tmp}: unknown status for '{target}'")
336        return '-'
337
338    def get_maintainers(self, target):
339        """Return the maintainers of the given board.
340
341        Args:
342            target (str): Build-target name
343
344        Returns:
345            str: Maintainers of the board.  If the board has two or more
346            maintainers, they are separated with colons.
347        """
348        entry = self.database.get(target)
349        if entry:
350            status, maint_list = entry
351            if not status.startswith('Orphan'):
352                if len(maint_list) > 1 or (maint_list and maint_list[0] != '-'):
353                    return ':'.join(maint_list)
354
355        self.warnings.append(f"WARNING: no maintainers for '{target}'")
356        return ''
357
358    def parse_file(self, srcdir, fname):
359        """Parse a MAINTAINERS file.
360
361        Parse a MAINTAINERS file and accumulate board status and maintainers
362        information in the self.database dict.
363
364        defconfig files are used to specify the target, e.g. xxx_defconfig is
365        used for target 'xxx'. If there is no defconfig file mentioned in the
366        MAINTAINERS file F: entries, then this function does nothing.
367
368        The N: name entries can be used to specify a defconfig file using
369        wildcards.
370
371        Args:
372            srcdir (str): Directory containing source code (Kconfig files)
373            fname (str): MAINTAINERS file to be parsed
374        """
375        def add_targets(linenum):
376            """Add any new targets
377
378            Args:
379                linenum (int): Current line number
380            """
381            if targets:
382                for target in targets:
383                    self.database[target] = (status, maintainers)
384
385        targets = []
386        maintainers = []
387        status = '-'
388        with open(fname, encoding="utf-8") as inf:
389            for linenum, line in enumerate(inf):
390                # Check also commented maintainers
391                if line[:3] == '#M:':
392                    line = line[1:]
393                tag, rest = line[:2], line[2:].strip()
394                if tag == 'M:':
395                    maintainers.append(rest)
396                elif tag == 'F:':
397                    # expand wildcard and filter by 'configs/*_defconfig'
398                    glob_path = os.path.join(srcdir, rest)
399                    for item in glob.glob(glob_path):
400                        front, match, rear = item.partition('configs/')
401                        if front.endswith('/'):
402                            front = front[:-1]
403                        if front == srcdir and match:
404                            front, match, rear = rear.rpartition('_defconfig')
405                            if match and not rear:
406                                targets.append(front)
407                elif tag == 'S:':
408                    status = rest
409                elif tag == 'N:':
410                    # Just scan the configs directory since that's all we care
411                    # about
412                    walk_path = os.walk(os.path.join(srcdir, 'configs'))
413                    for dirpath, _, fnames in walk_path:
414                        for cfg in fnames:
415                            path = os.path.join(dirpath, cfg)[len(srcdir) + 1:]
416                            front, match, rear = path.partition('configs/')
417                            if front or not match:
418                                continue
419                            front, match, rear = rear.rpartition('_defconfig')
420
421                            # Use this entry if it matches the defconfig file
422                            # without the _defconfig suffix. For example
423                            # 'am335x.*' matches am335x_guardian_defconfig
424                            if match and not rear and re.search(rest, front):
425                                targets.append(front)
426                elif line == '\n':
427                    add_targets(linenum)
428                    targets = []
429                    maintainers = []
430                    status = '-'
431        add_targets(linenum)
432
433
434class Boards:
435    """Manage a list of boards."""
436    def __init__(self):
437        self._boards = []
438
439    def add_board(self, brd):
440        """Add a new board to the list.
441
442        The board's target member must not already exist in the board list.
443
444        Args:
445            brd (Board): board to add
446        """
447        self._boards.append(brd)
448
449    def read_boards(self, fname):
450        """Read a list of boards from a board file.
451
452        Create a Board object for each and add it to our _boards list.
453
454        Args:
455            fname (str): Filename of boards.cfg file
456        """
457        with open(fname, 'r', encoding='utf-8') as inf:
458            for line in inf:
459                if line[0] == '#':
460                    continue
461                fields = line.split()
462                if not fields:
463                    continue
464                for upto, field in enumerate(fields):
465                    if field == '-':
466                        fields[upto] = ''
467                while len(fields) < 8:
468                    fields.append('')
469                if len(fields) > 8:
470                    fields = fields[:8]
471
472                brd = board.Board(*fields)
473                self.add_board(brd)
474
475
476    def get_list(self):
477        """Return a list of available boards.
478
479        Returns:
480            List of Board objects
481        """
482        return self._boards
483
484    def get_dict(self):
485        """Build a dictionary containing all the boards.
486
487        Returns:
488            Dictionary:
489                key is board.target
490                value is board
491        """
492        board_dict = OrderedDict()
493        for brd in self._boards:
494            board_dict[brd.target] = brd
495        return board_dict
496
497    def get_selected_dict(self):
498        """Return a dictionary containing the selected boards
499
500        Returns:
501            List of Board objects that are marked selected
502        """
503        board_dict = OrderedDict()
504        for brd in self._boards:
505            if brd.build_it:
506                board_dict[brd.target] = brd
507        return board_dict
508
509    def get_selected(self):
510        """Return a list of selected boards
511
512        Returns:
513            List of Board objects that are marked selected
514        """
515        return [brd for brd in self._boards if brd.build_it]
516
517    def get_selected_names(self):
518        """Return a list of selected boards
519
520        Returns:
521            List of board names that are marked selected
522        """
523        return [brd.target for brd in self._boards if brd.build_it]
524
525    @classmethod
526    def _build_terms(cls, args):
527        """Convert command line arguments to a list of terms.
528
529        This deals with parsing of the arguments. It handles the '&'
530        operator, which joins several expressions into a single Term.
531
532        For example:
533            ['arm & freescale sandbox', 'tegra']
534
535        will produce 3 Terms containing expressions as follows:
536            arm, freescale
537            sandbox
538            tegra
539
540        The first Term has two expressions, both of which must match for
541        a board to be selected.
542
543        Args:
544            args (list of str): List of command line arguments
545
546        Returns:
547            list of Term: A list of Term objects
548        """
549        syms = []
550        for arg in args:
551            for word in arg.split():
552                sym_build = []
553                for term in word.split('&'):
554                    if term:
555                        sym_build.append(term)
556                    sym_build.append('&')
557                syms += sym_build[:-1]
558        terms = []
559        term = None
560        oper = None
561        for sym in syms:
562            if sym == '&':
563                oper = sym
564            elif oper:
565                term.add_expr(sym)
566                oper = None
567            else:
568                if term:
569                    terms.append(term)
570                term = Term()
571                term.add_expr(sym)
572        if term:
573            terms.append(term)
574        return terms
575
576    def select_boards(self, args, exclude=None, brds=None):
577        """Mark boards selected based on args
578
579        Normally either boards (an explicit list of boards) or args (a list of
580        terms to match against) is used. It is possible to specify both, in
581        which case they are additive.
582
583        If brds and args are both empty, all boards are selected.
584
585        Args:
586            args (list of str): List of strings specifying boards to include,
587                either named, or by their target, architecture, cpu, vendor or
588                soc. If empty, all boards are selected.
589            exclude (list of str): List of boards to exclude, regardless of
590                'args', or None for none
591            brds (list of Board): List of boards to build, or None/[] for all
592
593        Returns:
594            Tuple
595                Dictionary which holds the list of boards which were selected
596                    due to each argument, arranged by argument.
597                List of errors found
598        """
599        def _check_board(brd):
600            """Check whether to include or exclude a board
601
602            Checks the various terms and decide whether to build it or not (the
603            'build_it' variable).
604
605            If it is built, add the board to the result[term] list so we know
606            which term caused it to be built. Add it to result['all'] also.
607
608            Keep a list of boards we found in 'found', so we can report boards
609            which appear in self._boards but not in brds.
610
611            Args:
612                brd (Board): Board to check
613            """
614            matching_term = None
615            build_it = False
616            if terms:
617                for term in terms:
618                    if term.matches(brd.props):
619                        matching_term = str(term)
620                        build_it = True
621                        break
622            elif brds:
623                if brd.target in brds:
624                    build_it = True
625                    found.append(brd.target)
626            else:
627                build_it = True
628
629            # Check that it is not specifically excluded
630            for expr in exclude_list:
631                if expr.matches(brd.props):
632                    build_it = False
633                    break
634
635            if build_it:
636                brd.build_it = True
637                if matching_term:
638                    result[matching_term].append(brd.target)
639                result['all'].append(brd.target)
640
641        result = OrderedDict()
642        warnings = []
643        terms = self._build_terms(args)
644
645        result['all'] = []
646        for term in terms:
647            result[str(term)] = []
648
649        exclude_list = []
650        if exclude:
651            for expr in exclude:
652                exclude_list.append(Expr(expr))
653
654        found = []
655        for brd in self._boards:
656            _check_board(brd)
657
658        if brds:
659            remaining = set(brds) - set(found)
660            if remaining:
661                warnings.append(f"Boards not found: {', '.join(remaining)}\n")
662
663        return result, warnings
664
665    @classmethod
666    def scan_defconfigs_for_multiprocess(cls, srcdir, queue, defconfigs,
667                                         warn_targets):
668        """Scan defconfig files and queue their board parameters
669
670        This function is intended to be passed to multiprocessing.Process()
671        constructor.
672
673        Args:
674            srcdir (str): Directory containing source code
675            queue (multiprocessing.Queue): The resulting board parameters are
676                written into this.
677            defconfigs (sequence of str): A sequence of defconfig files to be
678                scanned.
679            warn_targets (bool): True to warn about missing or duplicate
680                CONFIG_TARGET options
681        """
682        kconf_scanner = KconfigScanner(srcdir)
683        for defconfig in defconfigs:
684            queue.put(kconf_scanner.scan(defconfig, warn_targets))
685
686    @classmethod
687    def read_queues(cls, queues, params_list, warnings):
688        """Read the queues and append the data to the paramers list
689
690        Args:
691            queues (list of multiprocessing.Queue): Queues to read
692            params_list (list of dict): List to add params too
693            warnings (set of str): Set to add warnings to
694        """
695        for que in queues:
696            while not que.empty():
697                params, warn = que.get()
698                params_list.append(params)
699                warnings.update(warn)
700
701    def scan_defconfigs(self, config_dir, srcdir, jobs=1, warn_targets=False):
702        """Collect board parameters for all defconfig files.
703
704        This function invokes multiple processes for faster processing.
705
706        Args:
707            config_dir (str): Directory containing the defconfig files
708            srcdir (str): Directory containing source code (Kconfig files)
709            jobs (int): The number of jobs to run simultaneously
710            warn_targets (bool): True to warn about missing or duplicate
711                CONFIG_TARGET options
712
713        Returns:
714            tuple:
715                list of dict: List of board parameters, each a dict:
716                    key: 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
717                        'config'
718                    value: string value of the key
719                list of str: List of warnings recorded
720        """
721        all_defconfigs = []
722        for (dirpath, _, filenames) in os.walk(config_dir):
723            for filename in fnmatch.filter(filenames, '*_defconfig'):
724                if fnmatch.fnmatch(filename, '.*'):
725                    continue
726                all_defconfigs.append(os.path.join(dirpath, filename))
727
728        total_boards = len(all_defconfigs)
729        processes = []
730        queues = []
731        for i in range(jobs):
732            defconfigs = all_defconfigs[total_boards * i // jobs :
733                                        total_boards * (i + 1) // jobs]
734            que = multiprocessing.Queue(maxsize=-1)
735            proc = multiprocessing.Process(
736                target=self.scan_defconfigs_for_multiprocess,
737                args=(srcdir, que, defconfigs, warn_targets))
738            proc.start()
739            processes.append(proc)
740            queues.append(que)
741
742        # The resulting data should be accumulated to these lists
743        params_list = []
744        warnings = set()
745
746        # Data in the queues should be retrieved preriodically.
747        # Otherwise, the queues would become full and subprocesses would get stuck.
748        while any(p.is_alive() for p in processes):
749            self.read_queues(queues, params_list, warnings)
750            # sleep for a while until the queues are filled
751            time.sleep(SLEEP_TIME)
752
753        # Joining subprocesses just in case
754        # (All subprocesses should already have been finished)
755        for proc in processes:
756            proc.join()
757
758        # retrieve leftover data
759        self.read_queues(queues, params_list, warnings)
760
761        return params_list, sorted(list(warnings))
762
763    @classmethod
764    def insert_maintainers_info(cls, srcdir, params_list):
765        """Add Status and Maintainers information to the board parameters list.
766
767        Args:
768            params_list (list of dict): A list of the board parameters
769
770        Returns:
771            list of str: List of warnings collected due to missing status, etc.
772        """
773        database = MaintainersDatabase()
774        for (dirpath, _, filenames) in os.walk(srcdir):
775            if 'MAINTAINERS' in filenames and 'tools/buildman' not in dirpath:
776                database.parse_file(srcdir,
777                                    os.path.join(dirpath, 'MAINTAINERS'))
778
779        for i, params in enumerate(params_list):
780            target = params['target']
781            maintainers = database.get_maintainers(target)
782            params['maintainers'] = maintainers
783            if maintainers:
784                params['status'] = database.get_status(target)
785            else:
786                params['status'] = '-'
787            params_list[i] = params
788        return sorted(database.warnings)
789
790    @classmethod
791    def format_and_output(cls, params_list, output):
792        """Write board parameters into a file.
793
794        Columnate the board parameters, sort lines alphabetically,
795        and then write them to a file.
796
797        Args:
798            params_list (list of dict): The list of board parameters
799            output (str): The path to the output file
800        """
801        fields = ('status', 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
802                  'config', 'maintainers')
803
804        # First, decide the width of each column
805        max_length = {f: 0 for f in fields}
806        for params in params_list:
807            for field in fields:
808                max_length[field] = max(max_length[field], len(params[field]))
809
810        output_lines = []
811        for params in params_list:
812            line = ''
813            for field in fields:
814                # insert two spaces between fields like column -t would
815                line += '  ' + params[field].ljust(max_length[field])
816            output_lines.append(line.strip())
817
818        # ignore case when sorting
819        output_lines.sort(key=str.lower)
820
821        with open(output, 'w', encoding="utf-8") as outf:
822            outf.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n')
823
824    def build_board_list(self, config_dir=CONFIG_DIR, srcdir='.', jobs=1,
825                         warn_targets=False):
826        """Generate a board-database file
827
828        This works by reading the Kconfig, then loading each board's defconfig
829        in to get the setting for each option. In particular, CONFIG_TARGET_xxx
830        is typically set by the defconfig, where xxx is the target to build.
831
832        Args:
833            config_dir (str): Directory containing the defconfig files
834            srcdir (str): Directory containing source code (Kconfig files)
835            jobs (int): The number of jobs to run simultaneously
836            warn_targets (bool): True to warn about missing or duplicate
837                CONFIG_TARGET options
838
839        Returns:
840            tuple:
841                list of dict: List of board parameters, each a dict:
842                    key: 'arch', 'cpu', 'soc', 'vendor', 'board', 'config',
843                         'target'
844                    value: string value of the key
845                list of str: Warnings that came up
846        """
847        params_list, warnings = self.scan_defconfigs(config_dir, srcdir, jobs,
848                                                     warn_targets)
849        m_warnings = self.insert_maintainers_info(srcdir, params_list)
850        return params_list, warnings + m_warnings
851
852    def ensure_board_list(self, output, jobs=1, force=False, quiet=False):
853        """Generate a board database file if needed.
854
855        This is intended to check if Kconfig has changed since the boards.cfg
856        files was generated.
857
858        Args:
859            output (str): The name of the output file
860            jobs (int): The number of jobs to run simultaneously
861            force (bool): Force to generate the output even if it is new
862            quiet (bool): True to avoid printing a message if nothing needs doing
863
864        Returns:
865            bool: True if all is well, False if there were warnings
866        """
867        if not force:
868            if not quiet:
869                tprint('\rChecking for Kconfig changes...', newline=False)
870            is_new = output_is_new(output, CONFIG_DIR, '.')
871            print_clear()
872            if is_new:
873                if not quiet:
874                    print(f'{output} is up to date. Nothing to do.')
875                return True
876        if not quiet:
877            tprint('\rGenerating board list...', newline=False)
878        params_list, warnings = self.build_board_list(CONFIG_DIR, '.', jobs)
879        print_clear()
880        for warn in warnings:
881            print(warn, file=sys.stderr)
882        self.format_and_output(params_list, output)
883        return not warnings
884