1 # SPDX-License-Identifier: GPL-2.0+
2 # Copyright (c) 2013 The Chromium OS Authors.
5 """Control module for buildman
7 This holds the main control logic for buildman, when not running tests.
10 import multiprocessing
15 from buildman import boards
16 from buildman import bsettings
17 from buildman import cfgutil
18 from buildman import toolchain
19 from buildman.builder import Builder
20 from patman import gitutil
21 from patman import patchstream
22 from u_boot_pylib import command
23 from u_boot_pylib import terminal
24 from u_boot_pylib.terminal import tprint
28 def get_plural(count):
29 """Returns a plural 's' if count is not 1"""
30 return 's' if count != 1 else ''
33 def count_build_commits(commits, step):
34 """Calculate the number of commits to be built
37 commits (list of Commit): Commits to build or None
38 step (int): Step value for commits, typically 1
41 Number of commits that will be built
45 return (count + step - 1) // step
49 def get_action_summary(is_summary, commit_count, selected, threads, jobs):
50 """Return a string summarising the intended action.
53 is_summary (bool): True if this is a summary (otherwise it is building)
54 commits (list): List of commits being built
55 selected (list of Board): List of Board objects that are marked
56 step (int): Step increment through commits
57 threads (int): Number of processor threads being used
58 jobs (int): Number of jobs to build at once
64 commit_str = f'{commit_count} commit{get_plural(commit_count)}'
66 commit_str = 'current source'
67 msg = (f"{'Summary of' if is_summary else 'Building'} "
68 f'{commit_str} for {len(selected)} boards')
69 msg += (f' ({threads} thread{get_plural(threads)}, '
70 f'{jobs} job{get_plural(jobs)} per thread)')
73 # pylint: disable=R0913
74 def show_actions(series, why_selected, boards_selected, output_dir,
75 board_warnings, step, threads, jobs, verbose):
76 """Display a list of actions that we would take, if not a dry run.
80 why_selected: Dictionary where each key is a buildman argument
81 provided by the user, and the value is the list of boards
82 brought in by that argument. For example, 'arm' might bring
83 in 400 boards, so in this case the key would be 'arm' and
84 the value would be a list of board names.
85 boards_selected: Dict of selected boards, key is target name,
87 output_dir (str): Output directory for builder
88 board_warnings: List of warnings obtained from board selected
89 step (int): Step increment through commits
90 threads (int): Number of processor threads being used
91 jobs (int): Number of jobs to build at once
92 verbose (bool): True to indicate why each board was selected
94 col = terminal.Color()
95 print('Dry run, so not doing much. But I would do this:')
98 commits = series.commits
101 print(get_action_summary(False, count_build_commits(commits, step),
102 boards_selected, threads, jobs))
103 print(f'Build directory: {output_dir}')
105 for upto in range(0, len(series.commits), step):
106 commit = series.commits[upto]
107 print(' ', col.build(col.YELLOW, commit.hash[:8], bright=False), end=' ')
108 print(commit.subject)
110 for arg in why_selected:
112 print(arg, f': {len(why_selected[arg])} boards')
114 print(f" {' '.join(why_selected[arg])}")
115 print('Total boards to build for each '
116 f"commit: {len(why_selected['all'])}\n")
118 for warning in board_warnings:
119 print(col.build(col.YELLOW, warning))
121 def show_toolchain_prefix(brds, toolchains):
122 """Show information about a the tool chain used by one or more boards
124 The function checks that all boards use the same toolchain, then prints
125 the correct value for CROSS_COMPILE.
128 boards: Boards object containing selected boards
129 toolchains: Toolchains object containing available toolchains
132 None on success, string error message otherwise
134 board_selected = brds.get_selected_dict()
136 for brd in board_selected.values():
137 tc_set.add(toolchains.Select(brd.arch))
139 sys.exit('Supplied boards must share one toolchain')
140 tchain = tc_set.pop()
141 print(tchain.GetEnvArgs(toolchain.VAR_CROSS_COMPILE))
144 """Show information about a the architecture used by one or more boards
146 The function checks that all boards use the same architecture, then prints
147 the correct value for ARCH.
150 boards: Boards object containing selected boards
153 None on success, string error message otherwise
155 board_selected = brds.get_selected_dict()
157 for brd in board_selected.values():
158 arch_set.add(brd.arch)
159 if len(arch_set) != 1:
160 sys.exit('Supplied boards must share one arch')
161 print(arch_set.pop())
163 def get_allow_missing(opt_allow, opt_no_allow, num_selected, has_branch):
164 """Figure out whether to allow external blobs
166 Uses the allow-missing setting and the provided arguments to decide whether
167 missing external blobs should be allowed
170 opt_allow (bool): True if --allow-missing flag is set
171 opt_no_allow (bool): True if --no-allow-missing flag is set
172 num_selected (int): Number of selected board
173 has_branch (bool): True if a git branch (to build) has been provided
176 bool: True to allow missing external blobs, False to produce an error if
177 external blobs are used
179 allow_missing = False
180 am_setting = bsettings.get_global_item_value('allow-missing')
182 if am_setting == 'always':
184 if 'multiple' in am_setting and num_selected > 1:
186 if 'branch' in am_setting and has_branch:
192 allow_missing = False
196 def count_commits(branch, count, col, git_dir):
197 """Could the number of commits in the branch/ranch being built
200 branch (str): Name of branch to build, or None if none
201 count (int): Number of commits to build, or -1 for all
202 col (Terminal.Color): Color object to use
203 git_dir (str): Git directory to use, e.g. './.git'
207 Number of commits being built
208 True if the 'branch' string contains a range rather than a simple
211 has_range = branch and '..' in branch
217 count, msg = gitutil.count_commits_in_range(git_dir, branch)
219 count, msg = gitutil.count_commits_in_branch(git_dir, branch)
221 sys.exit(col.build(col.RED, msg))
223 sys.exit(col.build(col.RED,
224 f"Range '{branch}' has no commits"))
226 print(col.build(col.YELLOW, msg))
227 count += 1 # Build upstream commit also
230 msg = (f"No commits found to process in branch '{branch}': "
231 "set branch's upstream or use -c flag")
232 sys.exit(col.build(col.RED, msg))
233 return count, has_range
236 def determine_series(selected, col, git_dir, count, branch, work_in_output):
237 """Determine the series which is to be built, if any
239 If there is a series, the commits in that series are numbered by setting
240 their sequence value (starting from 0). This is used by tests.
243 selected (list of Board): List of Board objects that are marked
245 col (Terminal.Color): Color object to use
246 git_dir (str): Git directory to use, e.g. './.git'
247 count (int): Number of commits in branch
248 branch (str): Name of branch to build, or None if none
249 work_in_output (bool): True to work in the output directory
252 Series: Series to build, or None for none
254 Read the metadata from the commits. First look at the upstream commit,
255 then the ones in the branch. We would like to do something like
256 upstream/master~..branch but that isn't possible if upstream/master is
257 a merge commit (it will list all the commits that form part of the
260 Conflicting tags are not a problem for buildman, since it does not use
261 them. For example, Series-version is not useful for buildman. On the
262 other hand conflicting tags will cause an error. So allow later tags
263 to overwrite earlier ones by setting allow_overwrite=True
266 # Work out how many commits to build. We want to build everything on the
267 # branch. We also build the upstream commit as a control so we can see
268 # problems introduced by the first commit on the branch.
269 count, has_range = count_commits(branch, count, col, git_dir)
271 if len(selected) != 1:
272 sys.exit(col.build(col.RED,
273 '-w can only be used with a single board'))
275 sys.exit(col.build(col.RED,
276 '-w can only be used with a single commit'))
283 range_expr = gitutil.get_range_in_branch(git_dir, branch)
284 upstream_commit = gitutil.get_upstream(git_dir, branch)
285 series = patchstream.get_metadata_for_list(upstream_commit,
286 git_dir, 1, series=None, allow_overwrite=True)
288 series = patchstream.get_metadata_for_list(range_expr,
289 git_dir, None, series, allow_overwrite=True)
292 series = patchstream.get_metadata_for_list(branch,
293 git_dir, count, series=None, allow_overwrite=True)
295 # Number the commits for test purposes
296 for i, commit in enumerate(series.commits):
303 def do_fetch_arch(toolchains, col, fetch_arch):
304 """Handle the --fetch-arch option
307 toolchains (Toolchains): Tool chains to use
308 col (terminal.Color): Color object to build
309 fetch_arch (str): Argument passed to the --fetch-arch option
312 int: Return code for buildman
314 if fetch_arch == 'list':
315 sorted_list = toolchains.ListArchs()
318 f"Available architectures: {' '.join(sorted_list)}\n"))
321 if fetch_arch == 'all':
322 fetch_arch = ','.join(toolchains.ListArchs())
323 print(col.build(col.CYAN,
324 f'\nDownloading toolchains: {fetch_arch}'))
325 for arch in fetch_arch.split(','):
327 ret = toolchains.FetchAndInstall(arch)
333 def get_toolchains(toolchains, col, override_toolchain, fetch_arch,
334 list_tool_chains, verbose):
335 """Get toolchains object to use
338 toolchains (Toolchains or None): Toolchains to use. If None, then a
339 Toolchains object will be created and scanned
340 col (Terminal.Color): Color object
341 override_toolchain (str or None): Override value for toolchain, or None
342 fetch_arch (bool): True to fetch the toolchain for the architectures
343 list_tool_chains (bool): True to list all tool chains
344 verbose (bool): True for verbose output when listing toolchains
348 int: Operation completed and buildman should exit with exit code
349 Toolchains: Toolchains object to use
351 no_toolchains = toolchains is None
353 toolchains = toolchain.Toolchains(override_toolchain)
356 return do_fetch_arch(toolchains, col, fetch_arch)
359 toolchains.GetSettings()
360 toolchains.Scan(list_tool_chains and verbose)
368 def get_boards_obj(output_dir, regen_board_list, maintainer_check, full_check,
370 """Object the Boards object to use
372 Creates the output directory and ensures there is a boards.cfg file, then
376 output_dir (str): Output directory to use
377 regen_board_list (bool): True to just regenerate the board list
378 maintainer_check (bool): True to just run a maintainer check
379 full_check (bool): True to just run a full check of Kconfig and
381 threads (int or None): Number of threads to use to create boards file
382 verbose (bool): False to suppress output from boards-file generation
386 int: Operation completed and buildman should exit with exit code
387 Boards: Boards object to use
389 brds = boards.Boards()
390 nr_cpus = threads or multiprocessing.cpu_count()
391 if maintainer_check or full_check:
392 warnings = brds.build_board_list(jobs=nr_cpus,
393 warn_targets=full_check)[1]
395 for warn in warnings:
396 print(warn, file=sys.stderr)
400 if not os.path.exists(output_dir):
401 os.makedirs(output_dir)
402 board_file = os.path.join(output_dir, 'boards.cfg')
403 if regen_board_list and regen_board_list != '-':
404 board_file = regen_board_list
406 okay = brds.ensure_board_list(board_file, nr_cpus, force=regen_board_list,
409 return 0 if okay else 2
410 brds.read_boards(board_file)
414 def determine_boards(brds, args, col, opt_boards, exclude_list):
415 """Determine which boards to build
417 Each element of args and exclude can refer to a board name, arch or SoC
420 brds (Boards): Boards object
421 args (list of str): Arguments describing boards to build
422 col (Terminal.Color): Color object
423 opt_boards (list of str): Specific boards to build, or None for all
424 exclude_list (list of str): Arguments describing boards to exclude
428 list of Board: List of Board objects that are marked selected
429 why_selected: Dictionary where each key is a buildman argument
430 provided by the user, and the value is the list of boards
431 brought in by that argument. For example, 'arm' might bring
432 in 400 boards, so in this case the key would be 'arm' and
433 the value would be a list of board names.
434 board_warnings: List of warnings obtained from board selected
438 for arg in exclude_list:
439 exclude += arg.split(',')
442 requested_boards = []
443 for brd in opt_boards:
444 requested_boards += brd.split(',')
446 requested_boards = None
447 why_selected, board_warnings = brds.select_boards(args, exclude,
449 selected = brds.get_selected()
451 sys.exit(col.build(col.RED, 'No matching boards found'))
452 return selected, why_selected, board_warnings
455 def adjust_args(args, series, selected):
456 """Adjust arguments according to various constraints
458 Updates verbose, show_errors, threads, jobs and step
461 args (Namespace): Namespace object to adjust
462 series (Series): Series being built / summarised
463 selected (list of Board): List of Board objects that are marked
465 if not series and not args.dry_run:
468 args.show_errors = True
470 # By default we have one thread per CPU. But if there are not enough jobs
471 # we can have fewer threads and use a high '-j' value for make.
472 if args.threads is None:
473 args.threads = min(multiprocessing.cpu_count(), len(selected))
475 args.jobs = max(1, (multiprocessing.cpu_count() +
476 len(selected) - 1) // len(selected))
479 args.step = len(series.commits) - 1
481 # We can't show function sizes without board details at present
483 args.show_detail = True
486 def setup_output_dir(output_dir, work_in_output, branch, no_subdirs, col,
488 """Set up the output directory
491 output_dir (str): Output directory provided by the user, or None if none
492 work_in_output (bool): True to work in the output directory
493 branch (str): Name of branch to build, or None if none
494 no_subdirs (bool): True to put the output in the top-level output dir
495 clean_dir: Used for tests only, indicates that the existing output_dir
496 should be removed before starting the build
499 str: Updated output directory pathname
503 sys.exit(col.build(col.RED, '-w requires that you specify -o'))
505 if branch and not no_subdirs:
506 # As a special case allow the board directory to be placed in the
507 # output directory itself rather than any subdirectory.
508 dirname = branch.replace('/', '_')
509 output_dir = os.path.join(output_dir, dirname)
510 if clean_dir and os.path.exists(output_dir):
511 shutil.rmtree(output_dir)
515 def run_builder(builder, commits, board_selected, args):
516 """Run the builder or show the summary
519 commits (list of Commit): List of commits being built, None if no branch
520 boards_selected (dict): Dict of selected boards:
523 args (Namespace): Namespace to use
526 int: Return code for buildman
528 gnu_make = command.output(os.path.join(args.git,
529 'scripts/show-gnu-make'), raise_on_error=False).rstrip()
531 sys.exit('GNU Make not found')
532 builder.gnu_make = gnu_make
535 commit_count = count_build_commits(commits, args.step)
536 tprint(get_action_summary(args.summary, commit_count, board_selected,
537 args.threads, args.jobs))
539 builder.set_display_options(
540 args.show_errors, args.show_sizes, args.show_detail, args.show_bloat,
541 args.list_error_boards, args.show_config, args.show_environment,
542 args.filter_dtb_warnings, args.filter_migration_warnings, args.ide)
544 builder.show_summary(commits, board_selected)
546 fail, warned, excs = builder.build_boards(
547 commits, board_selected, args.keep_outputs, args.verbose)
552 if warned and not args.ignore_warnings:
557 def calc_adjust_cfg(adjust_cfg, reproducible_builds):
558 """Calculate the value to use for adjust_cfg
561 adjust_cfg (list of str): List of configuration changes. See cfgutil for
563 reproducible_builds (bool): True to adjust the configuration to get
567 adjust_cfg (list of str): List of configuration changes
569 adjust_cfg = cfgutil.convert_list_to_dict(adjust_cfg)
571 # Drop LOCALVERSION_AUTO since it changes the version string on every commit
572 if reproducible_builds:
573 # If these are mentioned, leave the local version alone
574 if 'LOCALVERSION' in adjust_cfg or 'LOCALVERSION_AUTO' in adjust_cfg:
575 print('Not dropping LOCALVERSION_AUTO for reproducible build')
577 adjust_cfg['LOCALVERSION_AUTO'] = '~'
581 def do_buildman(args, toolchains=None, make_func=None, brds=None,
582 clean_dir=False, test_thread_exceptions=False):
583 """The main control code for buildman
586 args: ArgumentParser object
587 args: Command line arguments (list of strings)
588 toolchains: Toolchains to use - this should be a Toolchains()
589 object. If None, then it will be created and scanned
590 make_func: Make function to use for the builder. This is called
591 to execute 'make'. If this is None, the normal function
592 will be used, which calls the 'make' tool with suitable
593 arguments. This setting is useful for tests.
594 brds: Boards() object to use, containing a list of available
595 boards. If this is None it will be created and scanned.
596 clean_dir: Used for tests only, indicates that the existing output_dir
597 should be removed before starting the build
598 test_thread_exceptions: Uses for tests only, True to make the threads
599 raise an exception instead of reporting their result. This simulates
600 a failure in the code somewhere
602 # Used so testing can obtain the builder: pylint: disable=W0603
606 col = terminal.Color()
608 git_dir = os.path.join(args.git, '.git')
610 toolchains = get_toolchains(toolchains, col, args.override_toolchain,
611 args.fetch_arch, args.list_tool_chains,
613 if isinstance(toolchains, int):
616 output_dir = setup_output_dir(
617 args.output_dir, args.work_in_output, args.branch,
618 args.no_subdirs, col, clean_dir)
620 # Work out what subset of the boards we are building
622 brds = get_boards_obj(output_dir, args.regen_board_list,
623 args.maintainer_check, args.full_check,
624 args.threads, args.verbose)
625 if isinstance(brds, int):
628 selected, why_selected, board_warnings = determine_boards(
629 brds, args.terms, col, args.boards, args.exclude)
631 if args.print_prefix:
632 show_toolchain_prefix(brds, toolchains)
639 series = determine_series(selected, col, git_dir, args.count,
640 args.branch, args.work_in_output)
642 adjust_args(args, series, selected)
644 # For a dry run, just show our actions as a sanity check
646 show_actions(series, why_selected, selected, output_dir, board_warnings,
647 args.step, args.threads, args.jobs,
651 # Create a new builder with the selected args
652 builder = Builder(toolchains, output_dir, git_dir,
653 args.threads, args.jobs, checkout=True,
654 show_unknown=args.show_unknown, step=args.step,
655 no_subdirs=args.no_subdirs, full_path=args.full_path,
656 verbose_build=args.verbose_build,
657 mrproper=args.mrproper,
658 per_board_out_dir=args.per_board_out_dir,
659 config_only=args.config_only,
660 squash_config_y=not args.preserve_config_y,
661 warnings_as_errors=args.warnings_as_errors,
662 work_in_output=args.work_in_output,
663 test_thread_exceptions=test_thread_exceptions,
664 adjust_cfg=calc_adjust_cfg(args.adjust_cfg,
665 args.reproducible_builds),
666 allow_missing=get_allow_missing(args.allow_missing,
667 args.no_allow_missing,
668 len(selected), args.branch),
670 reproducible_builds=args.reproducible_builds,
671 force_build = args.force_build,
672 force_build_failures = args.force_build_failures,
673 force_reconfig = args.force_reconfig, in_tree = args.in_tree,
674 force_config_on_failure=not args.quick, make_func=make_func)
676 TEST_BUILDER = builder
678 return run_builder(builder, series.commits if series else None,
679 brds.get_selected_dict(), args)