Prepare v2024.10
[platform/kernel/u-boot.git] / tools / buildman / control.py
1 # SPDX-License-Identifier: GPL-2.0+
2 # Copyright (c) 2013 The Chromium OS Authors.
3 #
4
5 """Control module for buildman
6
7 This holds the main control logic for buildman, when not running tests.
8 """
9
10 import getpass
11 import multiprocessing
12 import os
13 import shutil
14 import sys
15 import tempfile
16 import time
17
18 from buildman import boards
19 from buildman import bsettings
20 from buildman import cfgutil
21 from buildman import toolchain
22 from buildman.builder import Builder
23 from patman import gitutil
24 from patman import patchstream
25 from u_boot_pylib import command
26 from u_boot_pylib import terminal
27 from u_boot_pylib import tools
28 from u_boot_pylib.terminal import print_clear, tprint
29
30 TEST_BUILDER = None
31
32 # Space-separated list of buildman process IDs currently running jobs
33 RUNNING_FNAME = f'buildmanq.{getpass.getuser()}'
34
35 # Lock file for access to RUNNING_FILE
36 LOCK_FNAME = f'{RUNNING_FNAME}.lock'
37
38 # Wait time for access to lock (seconds)
39 LOCK_WAIT_S = 10
40
41 # Wait time to start running
42 RUN_WAIT_S = 300
43
44 def get_plural(count):
45     """Returns a plural 's' if count is not 1"""
46     return 's' if count != 1 else ''
47
48
49 def 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
65 def 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
90 def 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
138 def 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
160 def 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
180 def 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
213 def 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
253 def 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
320 def 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
350 def 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
385 def 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
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 not os.path.exists(output_dir):
418         os.makedirs(output_dir)
419     board_file = os.path.join(output_dir, '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
431 def 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
472 def 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
503 def setup_output_dir(output_dir, work_in_output, branch, no_subdirs, col,
504                      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         clean_dir: Used for tests only, indicates that the existing output_dir
513             should be removed before starting the build
514
515     Returns:
516         str: Updated output directory pathname
517     """
518     if not output_dir:
519         if work_in_output:
520             sys.exit(col.build(col.RED, '-w requires that you specify -o'))
521         output_dir = '..'
522     if branch and not no_subdirs:
523         # As a special case allow the board directory to be placed in the
524         # output directory itself rather than any subdirectory.
525         dirname = branch.replace('/', '_')
526         output_dir = os.path.join(output_dir, dirname)
527         if clean_dir and os.path.exists(output_dir):
528             shutil.rmtree(output_dir)
529     return output_dir
530
531
532 def run_builder(builder, commits, board_selected, args):
533     """Run the builder or show the summary
534
535     Args:
536         commits (list of Commit): List of commits being built, None if no branch
537         boards_selected (dict): Dict of selected boards:
538             key: target name
539             value: Board object
540         args (Namespace): Namespace to use
541
542     Returns:
543         int: Return code for buildman
544     """
545     gnu_make = command.output(os.path.join(args.git,
546             'scripts/show-gnu-make'), raise_on_error=False).rstrip()
547     if not gnu_make:
548         sys.exit('GNU Make not found')
549     builder.gnu_make = gnu_make
550
551     if not args.ide:
552         commit_count = count_build_commits(commits, args.step)
553         tprint(get_action_summary(args.summary, commit_count, board_selected,
554                                   args.threads, args.jobs))
555
556     builder.set_display_options(
557         args.show_errors, args.show_sizes, args.show_detail, args.show_bloat,
558         args.list_error_boards, args.show_config, args.show_environment,
559         args.filter_dtb_warnings, args.filter_migration_warnings, args.ide)
560     if args.summary:
561         builder.show_summary(commits, board_selected)
562     else:
563         fail, warned, excs = builder.build_boards(
564             commits, board_selected, args.keep_outputs, args.verbose)
565         if excs:
566             return 102
567         if fail:
568             return 100
569         if warned and not args.ignore_warnings:
570             return 101
571     return 0
572
573
574 def calc_adjust_cfg(adjust_cfg, reproducible_builds):
575     """Calculate the value to use for adjust_cfg
576
577     Args:
578         adjust_cfg (list of str): List of configuration changes. See cfgutil for
579             details
580         reproducible_builds (bool): True to adjust the configuration to get
581             reproduceable builds
582
583     Returns:
584         adjust_cfg (list of str): List of configuration changes
585     """
586     adjust_cfg = cfgutil.convert_list_to_dict(adjust_cfg)
587
588     # Drop LOCALVERSION_AUTO since it changes the version string on every commit
589     if reproducible_builds:
590         # If these are mentioned, leave the local version alone
591         if 'LOCALVERSION' in adjust_cfg or 'LOCALVERSION_AUTO' in adjust_cfg:
592             print('Not dropping LOCALVERSION_AUTO for reproducible build')
593         else:
594             adjust_cfg['LOCALVERSION_AUTO'] = '~'
595     return adjust_cfg
596
597
598 def read_procs(tmpdir=tempfile.gettempdir()):
599     """Read the list of running buildman processes
600
601     If the list is corrupted, returns an empty list
602
603     Args:
604         tmpdir (str): Temporary directory to use (for testing only)
605     """
606     running_fname = os.path.join(tmpdir, RUNNING_FNAME)
607     procs = []
608     if os.path.exists(running_fname):
609         items = tools.read_file(running_fname, binary=False).split()
610         try:
611             procs = [int(x) for x in items]
612         except ValueError: # Handle invalid format
613             pass
614     return procs
615
616
617 def check_pid(pid):
618     """Check for existence of a unix PID
619
620     https://stackoverflow.com/questions/568271/how-to-check-if-there-exists-a-process-with-a-given-pid-in-python
621
622     Args:
623         pid (int): PID to check
624
625     Returns:
626         True if it exists, else False
627     """
628     try:
629         os.kill(pid, 0)
630     except OSError:
631         return False
632     else:
633         return True
634
635
636 def write_procs(procs, tmpdir=tempfile.gettempdir()):
637     """Write the list of running buildman processes
638
639     Args:
640         tmpdir (str): Temporary directory to use (for testing only)
641     """
642     running_fname = os.path.join(tmpdir, RUNNING_FNAME)
643     tools.write_file(running_fname, ' '.join([str(p) for p in procs]),
644                      binary=False)
645
646     # Allow another user to access the file
647     os.chmod(running_fname, 0o666)
648
649 def wait_for_process_limit(limit, tmpdir=tempfile.gettempdir(),
650                            pid=os.getpid()):
651     """Wait until the number of buildman processes drops to the limit
652
653     This uses FileLock to protect a 'running' file, which contains a list of
654     PIDs of running buildman processes. The number of PIDs in the file indicates
655     the number of running processes.
656
657     When buildman starts up, it calls this function to wait until it is OK to
658     start the build.
659
660     On exit, no attempt is made to remove the PID from the file, since other
661     buildman processes will notice that the PID is no-longer valid, and ignore
662     it.
663
664     Two timeouts are provided:
665         LOCK_WAIT_S: length of time to wait for the lock; if this occurs, the
666             lock is busted / removed before trying again
667         RUN_WAIT_S: length of time to wait to be allowed to run; if this occurs,
668             the build starts, with the PID being added to the file.
669
670     Args:
671         limit (int): Maximum number of buildman processes, including this one;
672             must be > 0
673         tmpdir (str): Temporary directory to use (for testing only)
674         pid (int): Current process ID (for testing only)
675     """
676     from filelock import Timeout, FileLock
677
678     running_fname = os.path.join(tmpdir, RUNNING_FNAME)
679     lock_fname = os.path.join(tmpdir, LOCK_FNAME)
680     lock = FileLock(lock_fname)
681
682     # Allow another user to access the file
683     col = terminal.Color()
684     tprint('Waiting for other buildman processes...', newline=False,
685            colour=col.RED)
686
687     claimed = False
688     deadline = time.time() + RUN_WAIT_S
689     while True:
690         try:
691             with lock.acquire(timeout=LOCK_WAIT_S):
692                 os.chmod(lock_fname, 0o666)
693                 procs = read_procs(tmpdir)
694
695                 # Drop PIDs which are not running
696                 procs = list(filter(check_pid, procs))
697
698                 # If we haven't hit the limit, add ourself
699                 if len(procs) < limit:
700                     tprint('done...', newline=False)
701                     claimed = True
702                 if time.time() >= deadline:
703                     tprint('timeout...', newline=False)
704                     claimed = True
705                 if claimed:
706                     write_procs(procs + [pid], tmpdir)
707                     break
708
709         except Timeout:
710             tprint('failed to get lock: busting...', newline=False)
711             os.remove(lock_fname)
712
713         time.sleep(1)
714     tprint('starting build', newline=False)
715     print_clear()
716
717 def do_buildman(args, toolchains=None, make_func=None, brds=None,
718                 clean_dir=False, test_thread_exceptions=False):
719     """The main control code for buildman
720
721     Args:
722         args: ArgumentParser object
723         args: Command line arguments (list of strings)
724         toolchains: Toolchains to use - this should be a Toolchains()
725                 object. If None, then it will be created and scanned
726         make_func: Make function to use for the builder. This is called
727                 to execute 'make'. If this is None, the normal function
728                 will be used, which calls the 'make' tool with suitable
729                 arguments. This setting is useful for tests.
730         brds: Boards() object to use, containing a list of available
731                 boards. If this is None it will be created and scanned.
732         clean_dir: Used for tests only, indicates that the existing output_dir
733             should be removed before starting the build
734         test_thread_exceptions: Uses for tests only, True to make the threads
735             raise an exception instead of reporting their result. This simulates
736             a failure in the code somewhere
737     """
738     # Used so testing can obtain the builder: pylint: disable=W0603
739     global TEST_BUILDER
740
741     gitutil.setup()
742     col = terminal.Color()
743
744     git_dir = os.path.join(args.git, '.git')
745
746     toolchains = get_toolchains(toolchains, col, args.override_toolchain,
747                                 args.fetch_arch, args.list_tool_chains,
748                                 args.verbose)
749     if isinstance(toolchains, int):
750         return toolchains
751
752     output_dir = setup_output_dir(
753         args.output_dir, args.work_in_output, args.branch,
754         args.no_subdirs, col, clean_dir)
755
756     # Work out what subset of the boards we are building
757     if not brds:
758         brds = get_boards_obj(output_dir, args.regen_board_list,
759                               args.maintainer_check, args.full_check,
760                               args.threads, args.verbose and
761                               not args.print_arch and not args.print_prefix)
762         if isinstance(brds, int):
763             return brds
764
765     selected, why_selected, board_warnings = determine_boards(
766         brds, args.terms, col, args.boards, args.exclude)
767
768     if args.print_prefix:
769         show_toolchain_prefix(brds, toolchains)
770         return 0
771
772     if args.print_arch:
773         show_arch(brds)
774         return 0
775
776     series = determine_series(selected, col, git_dir, args.count,
777                               args.branch, args.work_in_output)
778
779     adjust_args(args, series, selected)
780
781     # For a dry run, just show our actions as a sanity check
782     if args.dry_run:
783         show_actions(series, why_selected, selected, output_dir, board_warnings,
784                      args.step, args.threads, args.jobs,
785                      args.verbose)
786         return 0
787
788     # Create a new builder with the selected args
789     builder = Builder(toolchains, output_dir, git_dir,
790             args.threads, args.jobs, checkout=True,
791             show_unknown=args.show_unknown, step=args.step,
792             no_subdirs=args.no_subdirs, full_path=args.full_path,
793             verbose_build=args.verbose_build,
794             mrproper=args.mrproper,
795             fallback_mrproper=args.fallback_mrproper,
796             per_board_out_dir=args.per_board_out_dir,
797             config_only=args.config_only,
798             squash_config_y=not args.preserve_config_y,
799             warnings_as_errors=args.warnings_as_errors,
800             work_in_output=args.work_in_output,
801             test_thread_exceptions=test_thread_exceptions,
802             adjust_cfg=calc_adjust_cfg(args.adjust_cfg,
803                                        args.reproducible_builds),
804             allow_missing=get_allow_missing(args.allow_missing,
805                                             args.no_allow_missing,
806                                             len(selected), args.branch),
807             no_lto=args.no_lto,
808             reproducible_builds=args.reproducible_builds,
809             force_build = args.force_build,
810             force_build_failures = args.force_build_failures,
811             force_reconfig = args.force_reconfig, in_tree = args.in_tree,
812             force_config_on_failure=not args.quick, make_func=make_func)
813
814     TEST_BUILDER = builder
815
816     if args.process_limit:
817         wait_for_process_limit(args.process_limit)
818
819     return run_builder(builder, series.commits if series else None,
820                        brds.get_selected_dict(), args)