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 ''
32 def get_action_summary(is_summary, commits, selected, step, threads, jobs):
33 """Return a string summarising the intended action.
36 is_summary (bool): True if this is a summary (otherwise it is building)
37 commits (list): List of commits being built
38 selected (list of Board): List of Board objects that are marked
39 step (int): Step increment through commits
40 threads (int): Number of processor threads being used
41 jobs (int): Number of jobs to build at once
48 count = (count + step - 1) // step
49 commit_str = f'{count} commit{get_plural(count)}'
51 commit_str = 'current source'
52 msg = (f"{'Summary of' if is_summary else 'Building'} "
53 f'{commit_str} for {len(selected)} boards')
54 msg += (f' ({threads} thread{get_plural(threads)}, '
55 f'{jobs} job{get_plural(jobs)} per thread)')
58 # pylint: disable=R0913
59 def show_actions(series, why_selected, boards_selected, output_dir,
60 board_warnings, step, threads, jobs, verbose):
61 """Display a list of actions that we would take, if not a dry run.
65 why_selected: Dictionary where each key is a buildman argument
66 provided by the user, and the value is the list of boards
67 brought in by that argument. For example, 'arm' might bring
68 in 400 boards, so in this case the key would be 'arm' and
69 the value would be a list of board names.
70 boards_selected: Dict of selected boards, key is target name,
72 output_dir (str): Output directory for builder
73 board_warnings: List of warnings obtained from board selected
74 step (int): Step increment through commits
75 threads (int): Number of processor threads being used
76 jobs (int): Number of jobs to build at once
77 verbose (bool): True to indicate why each board was selected
79 col = terminal.Color()
80 print('Dry run, so not doing much. But I would do this:')
83 commits = series.commits
86 print(get_action_summary(False, commits, boards_selected, step, threads,
88 print(f'Build directory: {output_dir}')
90 for upto in range(0, len(series.commits), step):
91 commit = series.commits[upto]
92 print(' ', col.build(col.YELLOW, commit.hash[:8], bright=False), end=' ')
95 for arg in why_selected:
97 print(arg, f': {len(why_selected[arg])} boards')
99 print(f" {' '.join(why_selected[arg])}")
100 print('Total boards to build for each '
101 f"commit: {len(why_selected['all'])}\n")
103 for warning in board_warnings:
104 print(col.build(col.YELLOW, warning))
106 def show_toolchain_prefix(brds, toolchains):
107 """Show information about a the tool chain used by one or more boards
109 The function checks that all boards use the same toolchain, then prints
110 the correct value for CROSS_COMPILE.
113 boards: Boards object containing selected boards
114 toolchains: Toolchains object containing available toolchains
117 None on success, string error message otherwise
119 board_selected = brds.get_selected_dict()
121 for brd in board_selected.values():
122 tc_set.add(toolchains.Select(brd.arch))
124 return 'Supplied boards must share one toolchain'
125 tchain = tc_set.pop()
126 print(tchain.GetEnvArgs(toolchain.VAR_CROSS_COMPILE))
129 def get_allow_missing(opt_allow, opt_no_allow, num_selected, has_branch):
130 """Figure out whether to allow external blobs
132 Uses the allow-missing setting and the provided arguments to decide whether
133 missing external blobs should be allowed
136 opt_allow (bool): True if --allow-missing flag is set
137 opt_no_allow (bool): True if --no-allow-missing flag is set
138 num_selected (int): Number of selected board
139 has_branch (bool): True if a git branch (to build) has been provided
142 bool: True to allow missing external blobs, False to produce an error if
143 external blobs are used
145 allow_missing = False
146 am_setting = bsettings.GetGlobalItemValue('allow-missing')
148 if am_setting == 'always':
150 if 'multiple' in am_setting and num_selected > 1:
152 if 'branch' in am_setting and has_branch:
158 allow_missing = False
162 def count_commits(branch, count, col, git_dir):
163 """Could the number of commits in the branch/ranch being built
166 branch (str): Name of branch to build, or None if none
167 count (int): Number of commits to build, or -1 for all
168 col (Terminal.Color): Color object to use
169 git_dir (str): Git directory to use, e.g. './.git'
173 Number of commits being built
174 True if the 'branch' string contains a range rather than a simple
177 has_range = branch and '..' in branch
183 count, msg = gitutil.count_commits_in_range(git_dir, branch)
185 count, msg = gitutil.count_commits_in_branch(git_dir, branch)
187 sys.exit(col.build(col.RED, msg))
189 sys.exit(col.build(col.RED,
190 f"Range '{branch}' has no commits"))
192 print(col.build(col.YELLOW, msg))
193 count += 1 # Build upstream commit also
196 msg = (f"No commits found to process in branch '{branch}': "
197 "set branch's upstream or use -c flag")
198 sys.exit(col.build(col.RED, msg))
199 return count, has_range
202 def determine_series(selected, col, git_dir, count, branch, work_in_output):
203 """Determine the series which is to be built, if any
206 selected (list of Board): List of Board objects that are marked
208 col (Terminal.Color): Color object to use
209 git_dir (str): Git directory to use, e.g. './.git'
210 count (int): Number of commits in branch
211 branch (str): Name of branch to build, or None if none
212 work_in_output (bool): True to work in the output directory
215 Series: Series to build, or None for none
217 Read the metadata from the commits. First look at the upstream commit,
218 then the ones in the branch. We would like to do something like
219 upstream/master~..branch but that isn't possible if upstream/master is
220 a merge commit (it will list all the commits that form part of the
223 Conflicting tags are not a problem for buildman, since it does not use
224 them. For example, Series-version is not useful for buildman. On the
225 other hand conflicting tags will cause an error. So allow later tags
226 to overwrite earlier ones by setting allow_overwrite=True
229 # Work out how many commits to build. We want to build everything on the
230 # branch. We also build the upstream commit as a control so we can see
231 # problems introduced by the first commit on the branch.
232 count, has_range = count_commits(branch, count, col, git_dir)
234 if len(selected) != 1:
235 sys.exit(col.build(col.RED,
236 '-w can only be used with a single board'))
238 sys.exit(col.build(col.RED,
239 '-w can only be used with a single commit'))
246 range_expr = gitutil.get_range_in_branch(git_dir, branch)
247 upstream_commit = gitutil.get_upstream(git_dir, branch)
248 series = patchstream.get_metadata_for_list(upstream_commit,
249 git_dir, 1, series=None, allow_overwrite=True)
251 series = patchstream.get_metadata_for_list(range_expr,
252 git_dir, None, series, allow_overwrite=True)
255 series = patchstream.get_metadata_for_list(branch,
256 git_dir, count, series=None, allow_overwrite=True)
262 def do_fetch_arch(toolchains, col, fetch_arch):
263 """Handle the --fetch-arch option
266 toolchains (Toolchains): Tool chains to use
267 col (terminal.Color): Color object to build
268 fetch_arch (str): Argument passed to the --fetch-arch option
271 int: Return code for buildman
273 if fetch_arch == 'list':
274 sorted_list = toolchains.ListArchs()
277 f"Available architectures: {' '.join(sorted_list)}\n"))
280 if fetch_arch == 'all':
281 fetch_arch = ','.join(toolchains.ListArchs())
282 print(col.build(col.CYAN,
283 f'\nDownloading toolchains: {fetch_arch}'))
284 for arch in fetch_arch.split(','):
286 ret = toolchains.FetchAndInstall(arch)
292 def get_toolchains(toolchains, col, override_toolchain, fetch_arch,
293 list_tool_chains, verbose):
294 """Get toolchains object to use
297 toolchains (Toolchains or None): Toolchains to use. If None, then a
298 Toolchains object will be created and scanned
299 col (Terminal.Color): Color object
300 override_toolchain (str or None): Override value for toolchain, or None
301 fetch_arch (bool): True to fetch the toolchain for the architectures
302 list_tool_chains (bool): True to list all tool chains
303 verbose (bool): True for verbose output when listing toolchains
307 int: Operation completed and buildman should exit with exit code
308 Toolchains: Toolchains object to use
310 no_toolchains = toolchains is None
312 toolchains = toolchain.Toolchains(override_toolchain)
315 return do_fetch_arch(toolchains, col, fetch_arch)
318 toolchains.GetSettings()
319 toolchains.Scan(list_tool_chains and verbose)
327 def get_boards_obj(output_dir, regen_board_list, maintainer_check, threads,
329 """Object the Boards object to use
331 Creates the output directory and ensures there is a boards.cfg file, then
335 output_dir (str): Output directory to use
336 regen_board_list (bool): True to just regenerate the board list
337 maintainer_check (bool): True to just run a maintainer check
338 threads (int or None): Number of threads to use to create boards file
339 verbose (bool): False to suppress output from boards-file generation
343 int: Operation completed and buildman should exit with exit code
344 Boards: Boards object to use
346 brds = boards.Boards()
347 nr_cpus = threads or multiprocessing.cpu_count()
349 warnings = brds.build_board_list(jobs=nr_cpus)[1]
351 for warn in warnings:
352 print(warn, file=sys.stderr)
356 if not os.path.exists(output_dir):
357 os.makedirs(output_dir)
358 board_file = os.path.join(output_dir, 'boards.cfg')
359 if regen_board_list and regen_board_list != '-':
360 board_file = regen_board_list
362 okay = brds.ensure_board_list(board_file, nr_cpus, force=regen_board_list,
365 return 0 if okay else 2
366 brds.read_boards(board_file)
370 def determine_boards(brds, args, col, opt_boards, exclude_list):
371 """Determine which boards to build
373 Each element of args and exclude can refer to a board name, arch or SoC
376 brds (Boards): Boards object
377 args (list of str): Arguments describing boards to build
378 col (Terminal.Color): Color object
379 opt_boards (list of str): Specific boards to build, or None for all
380 exclude_list (list of str): Arguments describing boards to exclude
384 list of Board: List of Board objects that are marked selected
385 why_selected: Dictionary where each key is a buildman argument
386 provided by the user, and the value is the list of boards
387 brought in by that argument. For example, 'arm' might bring
388 in 400 boards, so in this case the key would be 'arm' and
389 the value would be a list of board names.
390 board_warnings: List of warnings obtained from board selected
394 for arg in exclude_list:
395 exclude += arg.split(',')
398 requested_boards = []
399 for brd in opt_boards:
400 requested_boards += brd.split(',')
402 requested_boards = None
403 why_selected, board_warnings = brds.select_boards(args, exclude,
405 selected = brds.get_selected()
407 sys.exit(col.build(col.RED, 'No matching boards found'))
408 return selected, why_selected, board_warnings
411 def adjust_options(options, series, selected):
412 """Adjust options according to various constraints
414 Updates verbose, show_errors, threads, jobs and step
417 options (Options): Options object to adjust
418 series (Series): Series being built / summarised
419 selected (list of Board): List of Board objects that are marked
421 if not series and not options.dry_run:
422 options.verbose = True
423 if not options.summary:
424 options.show_errors = True
426 # By default we have one thread per CPU. But if there are not enough jobs
427 # we can have fewer threads and use a high '-j' value for make.
428 if options.threads is None:
429 options.threads = min(multiprocessing.cpu_count(), len(selected))
431 options.jobs = max(1, (multiprocessing.cpu_count() +
432 len(selected) - 1) // len(selected))
435 options.step = len(series.commits) - 1
438 def setup_output_dir(output_dir, work_in_output, branch, no_subdirs, col,
440 """Set up the output directory
443 output_dir (str): Output directory provided by the user, or None if none
444 work_in_output (bool): True to work in the output directory
445 branch (str): Name of branch to build, or None if none
446 no_subdirs (bool): True to put the output in the top-level output dir
447 clean_dir: Used for tests only, indicates that the existing output_dir
448 should be removed before starting the build
451 str: Updated output directory pathname
455 sys.exit(col.build(col.RED, '-w requires that you specify -o'))
457 if branch and not no_subdirs:
458 # As a special case allow the board directory to be placed in the
459 # output directory itself rather than any subdirectory.
460 dirname = branch.replace('/', '_')
461 output_dir = os.path.join(output_dir, dirname)
462 if clean_dir and os.path.exists(output_dir):
463 shutil.rmtree(output_dir)
467 def do_buildman(options, args, toolchains=None, make_func=None, brds=None,
468 clean_dir=False, test_thread_exceptions=False):
469 """The main control code for buildman
472 options: Command line options object
473 args: Command line arguments (list of strings)
474 toolchains: Toolchains to use - this should be a Toolchains()
475 object. If None, then it will be created and scanned
476 make_func: Make function to use for the builder. This is called
477 to execute 'make'. If this is None, the normal function
478 will be used, which calls the 'make' tool with suitable
479 arguments. This setting is useful for tests.
480 brds: Boards() object to use, containing a list of available
481 boards. If this is None it will be created and scanned.
482 clean_dir: Used for tests only, indicates that the existing output_dir
483 should be removed before starting the build
484 test_thread_exceptions: Uses for tests only, True to make the threads
485 raise an exception instead of reporting their result. This simulates
486 a failure in the code somewhere
488 # Used so testing can obtain the builder: pylint: disable=W0603
492 col = terminal.Color()
494 git_dir = os.path.join(options.git, '.git')
496 toolchains = get_toolchains(toolchains, col, options.override_toolchain,
497 options.fetch_arch, options.list_tool_chains,
499 output_dir = setup_output_dir(
500 options.output_dir, options.work_in_output, options.branch,
501 options.no_subdirs, col, clean_dir)
503 # Work out what subset of the boards we are building
505 brds = get_boards_obj(output_dir, options.regen_board_list,
506 options.maintainer_check, options.threads,
508 if isinstance(brds, int):
511 selected, why_selected, board_warnings = determine_boards(
512 brds, args, col, options.boards, options.exclude)
514 if options.print_prefix:
515 err = show_toolchain_prefix(brds, toolchains)
517 sys.exit(col.build(col.RED, err))
520 series = determine_series(selected, col, git_dir, options.count,
521 options.branch, options.work_in_output)
523 adjust_options(options, series, selected)
525 # For a dry run, just show our actions as a sanity check
527 show_actions(series, why_selected, selected, output_dir, board_warnings,
528 options.step, options.threads, options.jobs,
532 gnu_make = command.output(os.path.join(options.git,
533 'scripts/show-gnu-make'), raise_on_error=False).rstrip()
535 sys.exit('GNU Make not found')
537 allow_missing = get_allow_missing(options.allow_missing,
538 options.no_allow_missing, len(selected),
541 # Create a new builder with the selected options.
543 adjust_cfg = cfgutil.convert_list_to_dict(options.adjust_cfg)
545 # Drop LOCALVERSION_AUTO since it changes the version string on every commit
546 if options.reproducible_builds:
547 # If these are mentioned, leave the local version alone
548 if 'LOCALVERSION' in adjust_cfg or 'LOCALVERSION_AUTO' in adjust_cfg:
549 print('Not dropping LOCALVERSION_AUTO for reproducible build')
551 adjust_cfg['LOCALVERSION_AUTO'] = '~'
553 builder = Builder(toolchains, output_dir, git_dir,
554 options.threads, options.jobs, gnu_make=gnu_make, checkout=True,
555 show_unknown=options.show_unknown, step=options.step,
556 no_subdirs=options.no_subdirs, full_path=options.full_path,
557 verbose_build=options.verbose_build,
558 mrproper=options.mrproper,
559 per_board_out_dir=options.per_board_out_dir,
560 config_only=options.config_only,
561 squash_config_y=not options.preserve_config_y,
562 warnings_as_errors=options.warnings_as_errors,
563 work_in_output=options.work_in_output,
564 test_thread_exceptions=test_thread_exceptions,
565 adjust_cfg=adjust_cfg,
566 allow_missing=allow_missing, no_lto=options.no_lto,
567 reproducible_builds=options.reproducible_builds)
568 TEST_BUILDER = builder
569 builder.force_config_on_failure = not options.quick
571 builder.do_make = make_func
573 builder.force_build = options.force_build
574 builder.force_build_failures = options.force_build_failures
575 builder.force_reconfig = options.force_reconfig
576 builder.in_tree = options.in_tree
578 # Work out which boards to build
579 board_selected = brds.get_selected_dict()
582 commits = series.commits
583 # Number the commits for test purposes
584 for i, commit in enumerate(commits):
590 tprint(get_action_summary(options.summary, commits, board_selected,
591 options.step, options.threads, options.jobs))
593 # We can't show function sizes without board details at present
594 if options.show_bloat:
595 options.show_detail = True
596 builder.SetDisplayOptions(
597 options.show_errors, options.show_sizes, options.show_detail,
598 options.show_bloat, options.list_error_boards, options.show_config,
599 options.show_environment, options.filter_dtb_warnings,
600 options.filter_migration_warnings, options.ide)
602 builder.ShowSummary(commits, board_selected)
604 fail, warned, excs = builder.BuildBoards(
605 commits, board_selected, options.keep_outputs, options.verbose)
610 if warned and not options.ignore_warnings: