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
437 def do_buildman(options, args, toolchains=None, make_func=None, brds=None,
438 clean_dir=False, test_thread_exceptions=False):
439 """The main control code for buildman
442 options: Command line options object
443 args: Command line arguments (list of strings)
444 toolchains: Toolchains to use - this should be a Toolchains()
445 object. If None, then it will be created and scanned
446 make_func: Make function to use for the builder. This is called
447 to execute 'make'. If this is None, the normal function
448 will be used, which calls the 'make' tool with suitable
449 arguments. This setting is useful for tests.
450 brds: Boards() object to use, containing a list of available
451 boards. If this is None it will be created and scanned.
452 clean_dir: Used for tests only, indicates that the existing output_dir
453 should be removed before starting the build
454 test_thread_exceptions: Uses for tests only, True to make the threads
455 raise an exception instead of reporting their result. This simulates
456 a failure in the code somewhere
458 # Used so testing can obtain the builder: pylint: disable=W0603
462 col = terminal.Color()
464 git_dir = os.path.join(options.git, '.git')
466 toolchains = get_toolchains(toolchains, col, options.override_toolchain,
467 options.fetch_arch, options.list_tool_chains,
469 output_dir = options.output_dir
471 if options.work_in_output:
472 sys.exit(col.build(col.RED, '-w requires that you specify -o'))
474 if options.branch and not options.no_subdirs:
475 # As a special case allow the board directory to be placed in the
476 # output directory itself rather than any subdirectory.
477 dirname = options.branch.replace('/', '_')
478 output_dir = os.path.join(output_dir, dirname)
479 if clean_dir and os.path.exists(output_dir):
480 shutil.rmtree(output_dir)
482 # Work out what subset of the boards we are building
484 brds = get_boards_obj(output_dir, options.regen_board_list,
485 options.maintainer_check, options.threads,
487 if isinstance(brds, int):
490 selected, why_selected, board_warnings = determine_boards(
491 brds, args, col, options.boards, options.exclude)
493 if options.print_prefix:
494 err = show_toolchain_prefix(brds, toolchains)
496 sys.exit(col.build(col.RED, err))
499 series = determine_series(selected, col, git_dir, options.count,
500 options.branch, options.work_in_output)
502 adjust_options(options, series, selected)
504 # For a dry run, just show our actions as a sanity check
506 show_actions(series, why_selected, selected, output_dir, board_warnings,
507 options.step, options.threads, options.jobs,
511 gnu_make = command.output(os.path.join(options.git,
512 'scripts/show-gnu-make'), raise_on_error=False).rstrip()
514 sys.exit('GNU Make not found')
516 allow_missing = get_allow_missing(options.allow_missing,
517 options.no_allow_missing, len(selected),
520 # Create a new builder with the selected options.
522 adjust_cfg = cfgutil.convert_list_to_dict(options.adjust_cfg)
524 # Drop LOCALVERSION_AUTO since it changes the version string on every commit
525 if options.reproducible_builds:
526 # If these are mentioned, leave the local version alone
527 if 'LOCALVERSION' in adjust_cfg or 'LOCALVERSION_AUTO' in adjust_cfg:
528 print('Not dropping LOCALVERSION_AUTO for reproducible build')
530 adjust_cfg['LOCALVERSION_AUTO'] = '~'
532 builder = Builder(toolchains, output_dir, git_dir,
533 options.threads, options.jobs, gnu_make=gnu_make, checkout=True,
534 show_unknown=options.show_unknown, step=options.step,
535 no_subdirs=options.no_subdirs, full_path=options.full_path,
536 verbose_build=options.verbose_build,
537 mrproper=options.mrproper,
538 per_board_out_dir=options.per_board_out_dir,
539 config_only=options.config_only,
540 squash_config_y=not options.preserve_config_y,
541 warnings_as_errors=options.warnings_as_errors,
542 work_in_output=options.work_in_output,
543 test_thread_exceptions=test_thread_exceptions,
544 adjust_cfg=adjust_cfg,
545 allow_missing=allow_missing, no_lto=options.no_lto,
546 reproducible_builds=options.reproducible_builds)
547 TEST_BUILDER = builder
548 builder.force_config_on_failure = not options.quick
550 builder.do_make = make_func
552 builder.force_build = options.force_build
553 builder.force_build_failures = options.force_build_failures
554 builder.force_reconfig = options.force_reconfig
555 builder.in_tree = options.in_tree
557 # Work out which boards to build
558 board_selected = brds.get_selected_dict()
561 commits = series.commits
562 # Number the commits for test purposes
563 for i, commit in enumerate(commits):
569 tprint(get_action_summary(options.summary, commits, board_selected,
570 options.step, options.threads, options.jobs))
572 # We can't show function sizes without board details at present
573 if options.show_bloat:
574 options.show_detail = True
575 builder.SetDisplayOptions(
576 options.show_errors, options.show_sizes, options.show_detail,
577 options.show_bloat, options.list_error_boards, options.show_config,
578 options.show_environment, options.filter_dtb_warnings,
579 options.filter_migration_warnings, options.ide)
581 builder.ShowSummary(commits, board_selected)
583 fail, warned, excs = builder.BuildBoards(
584 commits, board_selected, options.keep_outputs, options.verbose)
589 if warned and not options.ignore_warnings: