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