1# SPDX-License-Identifier: GPL-2.0+
2# Copyright (c) 2013 The Chromium OS Authors.
3#
4
5"""Control module for buildman
6
7This holds the main control logic for buildman, when not running tests.
8"""
9
10import getpass
11import multiprocessing
12import os
13import shutil
14import sys
15import tempfile
16import time
17
18from buildman import boards
19from buildman import bsettings
20from buildman import cfgutil
21from buildman import toolchain
22from buildman.builder import Builder
23from patman import patchstream
24from u_boot_pylib import command
25from u_boot_pylib import gitutil
26from u_boot_pylib import terminal
27from u_boot_pylib import tools
28from u_boot_pylib.terminal import print_clear, tprint
29
30TEST_BUILDER = None
31
32# Space-separated list of buildman process IDs currently running jobs
33RUNNING_FNAME = f'buildmanq.{getpass.getuser()}'
34
35# Lock file for access to RUNNING_FILE
36LOCK_FNAME = f'{RUNNING_FNAME}.lock'
37
38# Wait time for access to lock (seconds)
39LOCK_WAIT_S = 10
40
41# Wait time to start running
42RUN_WAIT_S = 300
43
44def get_plural(count):
45    """Returns a plural 's' if count is not 1"""
46    return 's' if count != 1 else ''
47
48
49def count_build_commits(commits, step):
50    """Calculate the number of commits to be built
51
52    Args:
53        commits (list of Commit): Commits to build or None
54        step (int): Step value for commits, typically 1
55
56    Returns:
57        Number of commits that will be built
58    """
59    if commits:
60        count = len(commits)
61        return (count + step - 1) // step
62    return 0
63
64
65def get_action_summary(is_summary, commit_count, selected, threads, jobs):
66    """Return a string summarising the intended action.
67
68    Args:
69        is_summary (bool): True if this is a summary (otherwise it is building)
70        commits (list): List of commits being built
71        selected (list of Board): List of Board objects that are marked
72        step (int): Step increment through commits
73        threads (int): Number of processor threads being used
74        jobs (int): Number of jobs to build at once
75
76    Returns:
77        Summary string.
78    """
79    if commit_count:
80        commit_str = f'{commit_count} commit{get_plural(commit_count)}'
81    else:
82        commit_str = 'current source'
83    msg = (f"{'Summary of' if is_summary else 'Building'} "
84           f'{commit_str} for {len(selected)} boards')
85    msg += (f' ({threads} thread{get_plural(threads)}, '
86            f'{jobs} job{get_plural(jobs)} per thread)')
87    return msg
88
89# pylint: disable=R0913
90def show_actions(series, why_selected, boards_selected, output_dir,
91                 board_warnings, step, threads, jobs, verbose):
92    """Display a list of actions that we would take, if not a dry run.
93
94    Args:
95        series: Series object
96        why_selected: Dictionary where each key is a buildman argument
97                provided by the user, and the value is the list of boards
98                brought in by that argument. For example, 'arm' might bring
99                in 400 boards, so in this case the key would be 'arm' and
100                the value would be a list of board names.
101        boards_selected: Dict of selected boards, key is target name,
102                value is Board object
103        output_dir (str): Output directory for builder
104        board_warnings: List of warnings obtained from board selected
105        step (int): Step increment through commits
106        threads (int): Number of processor threads being used
107        jobs (int): Number of jobs to build at once
108        verbose (bool): True to indicate why each board was selected
109    """
110    col = terminal.Color()
111    print('Dry run, so not doing much. But I would do this:')
112    print()
113    if series:
114        commits = series.commits
115    else:
116        commits = None
117    print(get_action_summary(False, count_build_commits(commits, step),
118                             boards_selected, threads, jobs))
119    print(f'Build directory: {output_dir}')
120    if commits:
121        for upto in range(0, len(series.commits), step):
122            commit = series.commits[upto]
123            print('   ', col.build(col.YELLOW, commit.hash[:8], bright=False), end=' ')
124            print(commit.subject)
125    print()
126    for arg in why_selected:
127        # When -x is used, only the 'all' member exists
128        if arg != 'all' or len(why_selected) == 1:
129            print(arg, f': {len(why_selected[arg])} boards')
130            if verbose:
131                print(f"   {' '.join(why_selected[arg])}")
132    print('Total boards to build for each '
133          f"commit: {len(why_selected['all'])}\n")
134    if board_warnings:
135        for warning in board_warnings:
136            print(col.build(col.YELLOW, warning))
137
138def show_toolchain_prefix(brds, toolchains):
139    """Show information about a the tool chain used by one or more boards
140
141    The function checks that all boards use the same toolchain, then prints
142    the correct value for CROSS_COMPILE.
143
144    Args:
145        boards: Boards object containing selected boards
146        toolchains: Toolchains object containing available toolchains
147
148    Return:
149        None on success, string error message otherwise
150    """
151    board_selected = brds.get_selected_dict()
152    tc_set = set()
153    for brd in board_selected.values():
154        tc_set.add(toolchains.Select(brd.arch))
155    if len(tc_set) != 1:
156        sys.exit('Supplied boards must share one toolchain')
157    tchain = tc_set.pop()
158    print(tchain.GetEnvArgs(toolchain.VAR_CROSS_COMPILE))
159
160def show_arch(brds):
161    """Show information about a the architecture used by one or more boards
162
163    The function checks that all boards use the same architecture, then prints
164    the correct value for ARCH.
165
166    Args:
167        boards: Boards object containing selected boards
168
169    Return:
170        None on success, string error message otherwise
171    """
172    board_selected = brds.get_selected_dict()
173    arch_set = set()
174    for brd in board_selected.values():
175        arch_set.add(brd.arch)
176    if len(arch_set) != 1:
177        sys.exit('Supplied boards must share one arch')
178    print(arch_set.pop())
179
180def get_allow_missing(opt_allow, opt_no_allow, num_selected, has_branch):
181    """Figure out whether to allow external blobs
182
183    Uses the allow-missing setting and the provided arguments to decide whether
184    missing external blobs should be allowed
185
186    Args:
187        opt_allow (bool): True if --allow-missing flag is set
188        opt_no_allow (bool): True if --no-allow-missing flag is set
189        num_selected (int): Number of selected board
190        has_branch (bool): True if a git branch (to build) has been provided
191
192    Returns:
193        bool: True to allow missing external blobs, False to produce an error if
194            external blobs are used
195    """
196    allow_missing = False
197    am_setting = bsettings.get_global_item_value('allow-missing')
198    if am_setting:
199        if am_setting == 'always':
200            allow_missing = True
201        if 'multiple' in am_setting and num_selected > 1:
202            allow_missing = True
203        if 'branch' in am_setting and has_branch:
204            allow_missing = True
205
206    if opt_allow:
207        allow_missing = True
208    if opt_no_allow:
209        allow_missing = False
210    return allow_missing
211
212
213def count_commits(branch, count, col, git_dir):
214    """Could the number of commits in the branch/ranch being built
215
216    Args:
217        branch (str): Name of branch to build, or None if none
218        count (int): Number of commits to build, or -1 for all
219        col (Terminal.Color): Color object to use
220        git_dir (str): Git directory to use, e.g. './.git'
221
222    Returns:
223        tuple:
224            Number of commits being built
225            True if the 'branch' string contains a range rather than a simple
226                name
227    """
228    has_range = branch and '..' in branch
229    if count == -1:
230        if not branch:
231            count = 1
232        else:
233            if has_range:
234                count, msg = gitutil.count_commits_in_range(git_dir, branch)
235            else:
236                count, msg = gitutil.count_commits_in_branch(git_dir, branch)
237            if count is None:
238                sys.exit(col.build(col.RED, msg))
239            elif count == 0:
240                sys.exit(col.build(col.RED,
241                                   f"Range '{branch}' has no commits"))
242            if msg:
243                print(col.build(col.YELLOW, msg))
244            count += 1   # Build upstream commit also
245
246    if not count:
247        msg = (f"No commits found to process in branch '{branch}': "
248               "set branch's upstream or use -c flag")
249        sys.exit(col.build(col.RED, msg))
250    return count, has_range
251
252
253def determine_series(selected, col, git_dir, count, branch, work_in_output):
254    """Determine the series which is to be built, if any
255
256    If there is a series, the commits in that series are numbered by setting
257    their sequence value (starting from 0). This is used by tests.
258
259    Args:
260        selected (list of Board): List of Board objects that are marked
261            selected
262        col (Terminal.Color): Color object to use
263        git_dir (str): Git directory to use, e.g. './.git'
264        count (int): Number of commits in branch
265        branch (str): Name of branch to build, or None if none
266        work_in_output (bool): True to work in the output directory
267
268    Returns:
269        Series: Series to build, or None for none
270
271    Read the metadata from the commits. First look at the upstream commit,
272    then the ones in the branch. We would like to do something like
273    upstream/master~..branch but that isn't possible if upstream/master is
274    a merge commit (it will list all the commits that form part of the
275    merge)
276
277    Conflicting tags are not a problem for buildman, since it does not use
278    them. For example, Series-version is not useful for buildman. On the
279    other hand conflicting tags will cause an error. So allow later tags
280    to overwrite earlier ones by setting allow_overwrite=True
281    """
282
283    # Work out how many commits to build. We want to build everything on the
284    # branch. We also build the upstream commit as a control so we can see
285    # problems introduced by the first commit on the branch.
286    count, has_range = count_commits(branch, count, col, git_dir)
287    if work_in_output:
288        if len(selected) != 1:
289            sys.exit(col.build(col.RED,
290                               '-w can only be used with a single board'))
291        if count != 1:
292            sys.exit(col.build(col.RED,
293                               '-w can only be used with a single commit'))
294
295    if branch:
296        if count == -1:
297            if has_range:
298                range_expr = branch
299            else:
300                range_expr = gitutil.get_range_in_branch(git_dir, branch)
301            upstream_commit = gitutil.get_upstream(git_dir, branch)
302            series = patchstream.get_metadata_for_list(upstream_commit,
303                git_dir, 1, series=None, allow_overwrite=True)
304
305            series = patchstream.get_metadata_for_list(range_expr,
306                    git_dir, None, series, allow_overwrite=True)
307        else:
308            # Honour the count
309            series = patchstream.get_metadata_for_list(branch,
310                    git_dir, count, series=None, allow_overwrite=True)
311
312        # Number the commits for test purposes
313        for i, commit in enumerate(series.commits):
314            commit.sequence = i
315    else:
316        series = None
317    return series
318
319
320def do_fetch_arch(toolchains, col, fetch_arch):
321    """Handle the --fetch-arch option
322
323    Args:
324        toolchains (Toolchains): Tool chains to use
325        col (terminal.Color): Color object to build
326        fetch_arch (str): Argument passed to the --fetch-arch option
327
328    Returns:
329        int: Return code for buildman
330    """
331    if fetch_arch == 'list':
332        sorted_list = toolchains.ListArchs()
333        print(col.build(
334            col.BLUE,
335            f"Available architectures: {' '.join(sorted_list)}\n"))
336        return 0
337
338    if fetch_arch == 'all':
339        fetch_arch = ','.join(toolchains.ListArchs())
340        print(col.build(col.CYAN,
341                        f'\nDownloading toolchains: {fetch_arch}'))
342    for arch in fetch_arch.split(','):
343        print()
344        ret = toolchains.FetchAndInstall(arch)
345        if ret:
346            return ret
347    return 0
348
349
350def get_toolchains(toolchains, col, override_toolchain, fetch_arch,
351                   list_tool_chains, verbose):
352    """Get toolchains object to use
353
354    Args:
355        toolchains (Toolchains or None): Toolchains to use. If None, then a
356            Toolchains object will be created and scanned
357        col (Terminal.Color): Color object
358        override_toolchain (str or None): Override value for toolchain, or None
359        fetch_arch (bool): True to fetch the toolchain for the architectures
360        list_tool_chains (bool): True to list all tool chains
361        verbose (bool): True for verbose output when listing toolchains
362
363    Returns:
364        Either:
365            int: Operation completed and buildman should exit with exit code
366            Toolchains: Toolchains object to use
367    """
368    no_toolchains = toolchains is None
369    if no_toolchains:
370        toolchains = toolchain.Toolchains(override_toolchain)
371
372    if fetch_arch:
373        return do_fetch_arch(toolchains, col, fetch_arch)
374
375    if no_toolchains:
376        toolchains.GetSettings()
377        toolchains.Scan(list_tool_chains and verbose)
378    if list_tool_chains:
379        toolchains.List()
380        print()
381        return 0
382    return toolchains
383
384
385def get_boards_obj(output_dir, regen_board_list, maintainer_check, full_check,
386                   threads, verbose):
387    """Object the Boards object to use
388
389    Creates the output directory and ensures there is a boards.cfg file, then
390    read it in.
391
392    Args:
393        output_dir (str): Output directory to use, or None to use current dir
394        regen_board_list (bool): True to just regenerate the board list
395        maintainer_check (bool): True to just run a maintainer check
396        full_check (bool): True to just run a full check of Kconfig and
397            maintainers
398        threads (int or None): Number of threads to use to create boards file
399        verbose (bool): False to suppress output from boards-file generation
400
401    Returns:
402        Either:
403            int: Operation completed and buildman should exit with exit code
404            Boards: Boards object to use
405    """
406    brds = boards.Boards()
407    nr_cpus = threads or multiprocessing.cpu_count()
408    if maintainer_check or full_check:
409        warnings = brds.build_board_list(jobs=nr_cpus,
410                                         warn_targets=full_check)[1]
411        if warnings:
412            for warn in warnings:
413                print(warn, file=sys.stderr)
414            return 2
415        return 0
416
417    if output_dir and not os.path.exists(output_dir):
418        os.makedirs(output_dir)
419    board_file = os.path.join(output_dir or '', 'boards.cfg')
420    if regen_board_list and regen_board_list != '-':
421        board_file = regen_board_list
422
423    okay = brds.ensure_board_list(board_file, nr_cpus, force=regen_board_list,
424                                  quiet=not verbose)
425    if regen_board_list:
426        return 0 if okay else 2
427    brds.read_boards(board_file)
428    return brds
429
430
431def determine_boards(brds, args, col, opt_boards, exclude_list):
432    """Determine which boards to build
433
434    Each element of args and exclude can refer to a board name, arch or SoC
435
436    Args:
437        brds (Boards): Boards object
438        args (list of str): Arguments describing boards to build
439        col (Terminal.Color): Color object
440        opt_boards (list of str): Specific boards to build, or None for all
441        exclude_list (list of str): Arguments describing boards to exclude
442
443    Returns:
444        tuple:
445            list of Board: List of Board objects that are marked selected
446            why_selected: Dictionary where each key is a buildman argument
447                    provided by the user, and the value is the list of boards
448                    brought in by that argument. For example, 'arm' might bring
449                    in 400 boards, so in this case the key would be 'arm' and
450                    the value would be a list of board names.
451            board_warnings: List of warnings obtained from board selected
452    """
453    exclude = []
454    if exclude_list:
455        for arg in exclude_list:
456            exclude += arg.split(',')
457
458    if opt_boards:
459        requested_boards = []
460        for brd in opt_boards:
461            requested_boards += brd.split(',')
462    else:
463        requested_boards = None
464    why_selected, board_warnings = brds.select_boards(args, exclude,
465                                                      requested_boards)
466    selected = brds.get_selected()
467    if not selected:
468        sys.exit(col.build(col.RED, 'No matching boards found'))
469    return selected, why_selected, board_warnings
470
471
472def adjust_args(args, series, selected):
473    """Adjust arguments according to various constraints
474
475    Updates verbose, show_errors, threads, jobs and step
476
477    Args:
478        args (Namespace): Namespace object to adjust
479        series (Series): Series being built / summarised
480        selected (list of Board): List of Board objects that are marked
481    """
482    if not series and not args.dry_run:
483        args.verbose = True
484        if not args.summary:
485            args.show_errors = True
486
487    # By default we have one thread per CPU. But if there are not enough jobs
488    # we can have fewer threads and use a high '-j' value for make.
489    if args.threads is None:
490        args.threads = min(multiprocessing.cpu_count(), len(selected))
491    if not args.jobs:
492        args.jobs = max(1, (multiprocessing.cpu_count() +
493                len(selected) - 1) // len(selected))
494
495    if not args.step:
496        args.step = len(series.commits) - 1
497
498    # We can't show function sizes without board details at present
499    if args.show_bloat:
500        args.show_detail = True
501
502
503def setup_output_dir(output_dir, work_in_output, branch, no_subdirs, col,
504                     in_tree, clean_dir):
505    """Set up the output directory
506
507    Args:
508        output_dir (str): Output directory provided by the user, or None if none
509        work_in_output (bool): True to work in the output directory
510        branch (str): Name of branch to build, or None if none
511        no_subdirs (bool): True to put the output in the top-level output dir
512        in_tree (bool): True if doing an in-tree build
513        clean_dir: Used for tests only, indicates that the existing output_dir
514            should be removed before starting the build
515
516    Returns:
517        str: Updated output directory pathname
518    """
519    if not output_dir:
520        output_dir = '..'
521        if work_in_output:
522            if not in_tree:
523                sys.exit(col.build(col.RED, '-w requires that you specify -o'))
524            output_dir = None
525    if branch and not no_subdirs:
526        # As a special case allow the board directory to be placed in the
527        # output directory itself rather than any subdirectory.
528        dirname = branch.replace('/', '_')
529        output_dir = os.path.join(output_dir, dirname)
530        if clean_dir and os.path.exists(output_dir):
531            shutil.rmtree(output_dir)
532    return output_dir
533
534
535def run_builder(builder, commits, board_selected, args):
536    """Run the builder or show the summary
537
538    Args:
539        commits (list of Commit): List of commits being built, None if no branch
540        boards_selected (dict): Dict of selected boards:
541            key: target name
542            value: Board object
543        args (Namespace): Namespace to use
544
545    Returns:
546        int: Return code for buildman
547    """
548    gnu_make = command.output(os.path.join(args.git,
549            'scripts/show-gnu-make'), raise_on_error=False).rstrip()
550    if not gnu_make:
551        sys.exit('GNU Make not found')
552    builder.gnu_make = gnu_make
553
554    if not args.ide:
555        commit_count = count_build_commits(commits, args.step)
556        tprint(get_action_summary(args.summary, commit_count, board_selected,
557                                  args.threads, args.jobs))
558
559    builder.set_display_options(
560        args.show_errors, args.show_sizes, args.show_detail, args.show_bloat,
561        args.list_error_boards, args.show_config, args.show_environment,
562        args.filter_dtb_warnings, args.filter_migration_warnings, args.ide)
563    if args.summary:
564        builder.show_summary(commits, board_selected)
565    else:
566        fail, warned, excs = builder.build_boards(
567            commits, board_selected, args.keep_outputs, args.verbose)
568        if excs:
569            return 102
570        if fail:
571            return 100
572        if warned and not args.ignore_warnings:
573            return 101
574    return 0
575
576
577def calc_adjust_cfg(adjust_cfg, reproducible_builds):
578    """Calculate the value to use for adjust_cfg
579
580    Args:
581        adjust_cfg (list of str): List of configuration changes. See cfgutil for
582            details
583        reproducible_builds (bool): True to adjust the configuration to get
584            reproduceable builds
585
586    Returns:
587        adjust_cfg (list of str): List of configuration changes
588    """
589    adjust_cfg = cfgutil.convert_list_to_dict(adjust_cfg)
590
591    # Drop LOCALVERSION_AUTO since it changes the version string on every commit
592    if reproducible_builds:
593        # If these are mentioned, leave the local version alone
594        if 'LOCALVERSION' in adjust_cfg or 'LOCALVERSION_AUTO' in adjust_cfg:
595            print('Not dropping LOCALVERSION_AUTO for reproducible build')
596        else:
597            adjust_cfg['LOCALVERSION_AUTO'] = '~'
598    return adjust_cfg
599
600
601def read_procs(tmpdir=tempfile.gettempdir()):
602    """Read the list of running buildman processes
603
604    If the list is corrupted, returns an empty list
605
606    Args:
607        tmpdir (str): Temporary directory to use (for testing only)
608    """
609    running_fname = os.path.join(tmpdir, RUNNING_FNAME)
610    procs = []
611    if os.path.exists(running_fname):
612        items = tools.read_file(running_fname, binary=False).split()
613        try:
614            procs = [int(x) for x in items]
615        except ValueError: # Handle invalid format
616            pass
617    return procs
618
619
620def check_pid(pid):
621    """Check for existence of a unix PID
622
623    https://stackoverflow.com/questions/568271/how-to-check-if-there-exists-a-process-with-a-given-pid-in-python
624
625    Args:
626        pid (int): PID to check
627
628    Returns:
629        True if it exists, else False
630    """
631    try:
632        os.kill(pid, 0)
633    except OSError:
634        return False
635    else:
636        return True
637
638
639def write_procs(procs, tmpdir=tempfile.gettempdir()):
640    """Write the list of running buildman processes
641
642    Args:
643        tmpdir (str): Temporary directory to use (for testing only)
644    """
645    running_fname = os.path.join(tmpdir, RUNNING_FNAME)
646    tools.write_file(running_fname, ' '.join([str(p) for p in procs]),
647                     binary=False)
648
649    # Allow another user to access the file
650    os.chmod(running_fname, 0o666)
651
652def wait_for_process_limit(limit, tmpdir=tempfile.gettempdir(),
653                           pid=os.getpid()):
654    """Wait until the number of buildman processes drops to the limit
655
656    This uses FileLock to protect a 'running' file, which contains a list of
657    PIDs of running buildman processes. The number of PIDs in the file indicates
658    the number of running processes.
659
660    When buildman starts up, it calls this function to wait until it is OK to
661    start the build.
662
663    On exit, no attempt is made to remove the PID from the file, since other
664    buildman processes will notice that the PID is no-longer valid, and ignore
665    it.
666
667    Two timeouts are provided:
668        LOCK_WAIT_S: length of time to wait for the lock; if this occurs, the
669            lock is busted / removed before trying again
670        RUN_WAIT_S: length of time to wait to be allowed to run; if this occurs,
671            the build starts, with the PID being added to the file.
672
673    Args:
674        limit (int): Maximum number of buildman processes, including this one;
675            must be > 0
676        tmpdir (str): Temporary directory to use (for testing only)
677        pid (int): Current process ID (for testing only)
678    """
679    from filelock import Timeout, FileLock
680
681    running_fname = os.path.join(tmpdir, RUNNING_FNAME)
682    lock_fname = os.path.join(tmpdir, LOCK_FNAME)
683    lock = FileLock(lock_fname)
684
685    # Allow another user to access the file
686    col = terminal.Color()
687    tprint('Waiting for other buildman processes...', newline=False,
688           colour=col.RED)
689
690    claimed = False
691    deadline = time.time() + RUN_WAIT_S
692    while True:
693        try:
694            with lock.acquire(timeout=LOCK_WAIT_S):
695                os.chmod(lock_fname, 0o666)
696                procs = read_procs(tmpdir)
697
698                # Drop PIDs which are not running
699                procs = list(filter(check_pid, procs))
700
701                # If we haven't hit the limit, add ourself
702                if len(procs) < limit:
703                    tprint('done...', newline=False)
704                    claimed = True
705                if time.time() >= deadline:
706                    tprint('timeout...', newline=False)
707                    claimed = True
708                if claimed:
709                    write_procs(procs + [pid], tmpdir)
710                    break
711
712        except Timeout:
713            tprint('failed to get lock: busting...', newline=False)
714            os.remove(lock_fname)
715
716        time.sleep(1)
717    tprint('starting build', newline=False)
718    print_clear()
719
720def do_buildman(args, toolchains=None, make_func=None, brds=None,
721                clean_dir=False, test_thread_exceptions=False):
722    """The main control code for buildman
723
724    Args:
725        args: ArgumentParser object
726        args: Command line arguments (list of strings)
727        toolchains: Toolchains to use - this should be a Toolchains()
728                object. If None, then it will be created and scanned
729        make_func: Make function to use for the builder. This is called
730                to execute 'make'. If this is None, the normal function
731                will be used, which calls the 'make' tool with suitable
732                arguments. This setting is useful for tests.
733        brds: Boards() object to use, containing a list of available
734                boards. If this is None it will be created and scanned.
735        clean_dir: Used for tests only, indicates that the existing output_dir
736            should be removed before starting the build
737        test_thread_exceptions: Uses for tests only, True to make the threads
738            raise an exception instead of reporting their result. This simulates
739            a failure in the code somewhere
740    """
741    # Used so testing can obtain the builder: pylint: disable=W0603
742    global TEST_BUILDER
743
744    gitutil.setup()
745    col = terminal.Color()
746
747    git_dir = os.path.join(args.git, '.git')
748
749    toolchains = get_toolchains(toolchains, col, args.override_toolchain,
750                                args.fetch_arch, args.list_tool_chains,
751                                args.verbose)
752    if isinstance(toolchains, int):
753        return toolchains
754
755    output_dir = setup_output_dir(
756        args.output_dir, args.work_in_output, args.branch,
757        args.no_subdirs, col, args.in_tree, clean_dir)
758
759    # Work out what subset of the boards we are building
760    if not brds:
761        brds = get_boards_obj(output_dir, args.regen_board_list,
762                              args.maintainer_check, args.full_check,
763                              args.threads, args.verbose and
764                              not args.print_arch and not args.print_prefix)
765        if isinstance(brds, int):
766            return brds
767
768    selected, why_selected, board_warnings = determine_boards(
769        brds, args.terms, col, args.boards, args.exclude)
770
771    if args.print_prefix:
772        show_toolchain_prefix(brds, toolchains)
773        return 0
774
775    if args.print_arch:
776        show_arch(brds)
777        return 0
778
779    series = determine_series(selected, col, git_dir, args.count,
780                              args.branch, args.work_in_output)
781
782    adjust_args(args, series, selected)
783
784    # For a dry run, just show our actions as a sanity check
785    if args.dry_run:
786        show_actions(series, why_selected, selected, output_dir, board_warnings,
787                     args.step, args.threads, args.jobs,
788                     args.verbose)
789        return 0
790
791    if args.config_only and args.target:
792        raise ValueError('Cannot use --config-only with --target')
793
794    # Create a new builder with the selected args
795    builder = Builder(toolchains, output_dir, git_dir,
796            args.threads, args.jobs, checkout=True,
797            show_unknown=args.show_unknown, step=args.step,
798            no_subdirs=args.no_subdirs, full_path=args.full_path,
799            verbose_build=args.verbose_build,
800            mrproper=args.mrproper,
801            fallback_mrproper=args.fallback_mrproper,
802            per_board_out_dir=args.per_board_out_dir,
803            config_only=args.config_only,
804            squash_config_y=not args.preserve_config_y,
805            warnings_as_errors=args.warnings_as_errors,
806            work_in_output=args.work_in_output,
807            test_thread_exceptions=test_thread_exceptions,
808            adjust_cfg=calc_adjust_cfg(args.adjust_cfg,
809                                       args.reproducible_builds),
810            allow_missing=get_allow_missing(args.allow_missing,
811                                            args.no_allow_missing,
812                                            len(selected), args.branch),
813            no_lto=args.no_lto,
814            reproducible_builds=args.reproducible_builds,
815            force_build = args.force_build,
816            force_build_failures = args.force_build_failures,
817            force_reconfig = args.force_reconfig, in_tree = args.in_tree,
818            force_config_on_failure=not args.quick, make_func=make_func,
819            dtc_skip=args.dtc_skip, build_target=args.target)
820
821    TEST_BUILDER = builder
822
823    if args.process_limit:
824        wait_for_process_limit(args.process_limit)
825
826    return run_builder(builder, series.commits if series else None,
827                       brds.get_selected_dict(), args)
828