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, options):
33 """Return a string summarising the intended action.
40 count = (count + options.step - 1) // options.step
41 commit_str = f'{count} commit{get_plural(count)}'
43 commit_str = 'current source'
44 msg = (f"{'Summary of' if is_summary else 'Building'} "
45 f'{commit_str} for {len(selected)} boards')
46 msg += (f' ({options.threads} thread{get_plural(options.threads)}, '
47 f'{options.jobs} job{get_plural(options.jobs)} per thread)')
50 # pylint: disable=R0913
51 def show_actions(series, why_selected, boards_selected, output_dir, options,
53 """Display a list of actions that we would take, if not a dry run.
57 why_selected: Dictionary where each key is a buildman argument
58 provided by the user, and the value is the list of boards
59 brought in by that argument. For example, 'arm' might bring
60 in 400 boards, so in this case the key would be 'arm' and
61 the value would be a list of board names.
62 boards_selected: Dict of selected boards, key is target name,
64 output_dir (str): Output directory for builder
65 options: Command line options object
66 board_warnings: List of warnings obtained from board selected
68 col = terminal.Color()
69 print('Dry run, so not doing much. But I would do this:')
72 commits = series.commits
75 print(get_action_summary(False, commits, boards_selected,
77 print(f'Build directory: {output_dir}')
79 for upto in range(0, len(series.commits), options.step):
80 commit = series.commits[upto]
81 print(' ', col.build(col.YELLOW, commit.hash[:8], bright=False), end=' ')
84 for arg in why_selected:
86 print(arg, f': {len(why_selected[arg])} boards')
88 print(f" {' '.join(why_selected[arg])}")
89 print('Total boards to build for each '
90 f"commit: {len(why_selected['all'])}\n")
92 for warning in board_warnings:
93 print(col.build(col.YELLOW, warning))
95 def show_toolchain_prefix(brds, toolchains):
96 """Show information about a the tool chain used by one or more boards
98 The function checks that all boards use the same toolchain, then prints
99 the correct value for CROSS_COMPILE.
102 boards: Boards object containing selected boards
103 toolchains: Toolchains object containing available toolchains
106 None on success, string error message otherwise
108 board_selected = brds.get_selected_dict()
110 for brd in board_selected.values():
111 tc_set.add(toolchains.Select(brd.arch))
113 return 'Supplied boards must share one toolchain'
114 tchain = tc_set.pop()
115 print(tchain.GetEnvArgs(toolchain.VAR_CROSS_COMPILE))
118 def get_allow_missing(opt_allow, opt_no_allow, num_selected, has_branch):
119 """Figure out whether to allow external blobs
121 Uses the allow-missing setting and the provided arguments to decide whether
122 missing external blobs should be allowed
125 opt_allow (bool): True if --allow-missing flag is set
126 opt_no_allow (bool): True if --no-allow-missing flag is set
127 num_selected (int): Number of selected board
128 has_branch (bool): True if a git branch (to build) has been provided
131 bool: True to allow missing external blobs, False to produce an error if
132 external blobs are used
134 allow_missing = False
135 am_setting = bsettings.GetGlobalItemValue('allow-missing')
137 if am_setting == 'always':
139 if 'multiple' in am_setting and num_selected > 1:
141 if 'branch' in am_setting and has_branch:
147 allow_missing = False
151 def determine_series(selected, col, git_dir, count, branch, work_in_output):
152 """Determine the series which is to be built, if any
155 selected (list of Board(: List of Board objects that are marked
157 col (Terminal.Color): Color object to use
158 git_dir (str): Git directory to use, e.g. './.git'
159 count (int): Number of commits in branch
160 branch (str): Name of branch to build, or None if none
161 work_in_output (bool): True to work in the output directory
164 Series: Series to build, or None for none
166 Read the metadata from the commits. First look at the upstream commit,
167 then the ones in the branch. We would like to do something like
168 upstream/master~..branch but that isn't possible if upstream/master is
169 a merge commit (it will list all the commits that form part of the
172 Conflicting tags are not a problem for buildman, since it does not use
173 them. For example, Series-version is not useful for buildman. On the
174 other hand conflicting tags will cause an error. So allow later tags
175 to overwrite earlier ones by setting allow_overwrite=True
178 # Work out how many commits to build. We want to build everything on the
179 # branch. We also build the upstream commit as a control so we can see
180 # problems introduced by the first commit on the branch.
181 has_range = branch and '..' in branch
187 count, msg = gitutil.count_commits_in_range(git_dir, branch)
189 count, msg = gitutil.count_commits_in_branch(git_dir, branch)
191 sys.exit(col.build(col.RED, msg))
193 sys.exit(col.build(col.RED,
194 f"Range '{branch}' has no commits"))
196 print(col.build(col.YELLOW, msg))
197 count += 1 # Build upstream commit also
200 msg = (f"No commits found to process in branch '{branch}': "
201 "set branch's upstream or use -c flag")
202 sys.exit(col.build(col.RED, msg))
204 if len(selected) != 1:
205 sys.exit(col.build(col.RED,
206 '-w can only be used with a single board'))
208 sys.exit(col.build(col.RED,
209 '-w can only be used with a single commit'))
216 range_expr = gitutil.get_range_in_branch(git_dir, branch)
217 upstream_commit = gitutil.get_upstream(git_dir, branch)
218 series = patchstream.get_metadata_for_list(upstream_commit,
219 git_dir, 1, series=None, allow_overwrite=True)
221 series = patchstream.get_metadata_for_list(range_expr,
222 git_dir, None, series, allow_overwrite=True)
225 series = patchstream.get_metadata_for_list(branch,
226 git_dir, count, series=None, allow_overwrite=True)
232 def do_fetch_arch(toolchains, col, fetch_arch):
233 """Handle the --fetch-arch option
236 toolchains (Toolchains): Tool chains to use
237 col (terminal.Color): Color object to build
238 fetch_arch (str): Argument passed to the --fetch-arch option
241 int: Return code for buildman
243 if fetch_arch == 'list':
244 sorted_list = toolchains.ListArchs()
247 f"Available architectures: {' '.join(sorted_list)}\n"))
250 if fetch_arch == 'all':
251 fetch_arch = ','.join(toolchains.ListArchs())
252 print(col.build(col.CYAN,
253 f'\nDownloading toolchains: {fetch_arch}'))
254 for arch in fetch_arch.split(','):
256 ret = toolchains.FetchAndInstall(arch)
262 def determine_boards(brds, args, col, opt_boards, exclude_list):
263 """Determine which boards to build
265 Each element of args and exclude can refer to a board name, arch or SoC
268 brds (Boards): Boards object
269 args (list of str): Arguments describing boards to build
270 col (Terminal.Color): Color object
271 opt_boards (list of str): Specific boards to build, or None for all
272 exclude_list (list of str): Arguments describing boards to exclude
276 list of Board: List of Board objects that are marked selected
277 why_selected: Dictionary where each key is a buildman argument
278 provided by the user, and the value is the list of boards
279 brought in by that argument. For example, 'arm' might bring
280 in 400 boards, so in this case the key would be 'arm' and
281 the value would be a list of board names.
282 board_warnings: List of warnings obtained from board selected
286 for arg in exclude_list:
287 exclude += arg.split(',')
290 requested_boards = []
291 for brd in opt_boards:
292 requested_boards += brd.split(',')
294 requested_boards = None
295 why_selected, board_warnings = brds.select_boards(args, exclude,
297 selected = brds.get_selected()
299 sys.exit(col.build(col.RED, 'No matching boards found'))
300 return selected, why_selected, board_warnings
303 def do_buildman(options, args, toolchains=None, make_func=None, brds=None,
304 clean_dir=False, test_thread_exceptions=False):
305 """The main control code for buildman
308 options: Command line options object
309 args: Command line arguments (list of strings)
310 toolchains: Toolchains to use - this should be a Toolchains()
311 object. If None, then it will be created and scanned
312 make_func: Make function to use for the builder. This is called
313 to execute 'make'. If this is None, the normal function
314 will be used, which calls the 'make' tool with suitable
315 arguments. This setting is useful for tests.
316 brds: Boards() object to use, containing a list of available
317 boards. If this is None it will be created and scanned.
318 clean_dir: Used for tests only, indicates that the existing output_dir
319 should be removed before starting the build
320 test_thread_exceptions: Uses for tests only, True to make the threads
321 raise an exception instead of reporting their result. This simulates
322 a failure in the code somewhere
324 # Used so testing can obtain the builder: pylint: disable=W0603
328 col = terminal.Color()
330 git_dir = os.path.join(options.git, '.git')
332 no_toolchains = toolchains is None
334 toolchains = toolchain.Toolchains(options.override_toolchain)
336 if options.fetch_arch:
337 return do_fetch_arch(toolchains, col, options.fetch_arch)
340 toolchains.GetSettings()
341 toolchains.Scan(options.list_tool_chains and options.verbose)
342 if options.list_tool_chains:
347 if not options.output_dir:
348 if options.work_in_output:
349 sys.exit(col.build(col.RED, '-w requires that you specify -o'))
350 options.output_dir = '..'
352 nr_cups = options.threads or multiprocessing.cpu_count()
354 # Work out what subset of the boards we are building
356 if not os.path.exists(options.output_dir):
357 os.makedirs(options.output_dir)
358 board_file = os.path.join(options.output_dir, 'boards.cfg')
359 if options.regen_board_list and options.regen_board_list != '-':
360 board_file = options.regen_board_list
362 brds = boards.Boards()
364 if options.maintainer_check:
365 warnings = brds.build_board_list(jobs=nr_cups)[1]
367 for warn in warnings:
368 print(warn, file=sys.stderr)
372 okay = brds.ensure_board_list(
374 options.threads or multiprocessing.cpu_count(),
375 force=options.regen_board_list,
376 quiet=not options.verbose)
377 if options.regen_board_list:
378 return 0 if okay else 2
379 brds.read_boards(board_file)
381 selected, why_selected, board_warnings = determine_boards(
382 brds, args, col, options.boards, options.exclude)
384 if options.print_prefix:
385 err = show_toolchain_prefix(brds, toolchains)
387 sys.exit(col.build(col.RED, err))
390 series = determine_series(selected, col, git_dir, options.count,
391 options.branch, options.work_in_output)
392 if not series and not options.dry_run:
393 options.verbose = True
394 if not options.summary:
395 options.show_errors = True
397 # By default we have one thread per CPU. But if there are not enough jobs
398 # we can have fewer threads and use a high '-j' value for make.
399 if options.threads is None:
400 options.threads = min(multiprocessing.cpu_count(), len(selected))
402 options.jobs = max(1, (multiprocessing.cpu_count() +
403 len(selected) - 1) // len(selected))
406 options.step = len(series.commits) - 1
408 gnu_make = command.output(os.path.join(options.git,
409 'scripts/show-gnu-make'), raise_on_error=False).rstrip()
411 sys.exit('GNU Make not found')
413 allow_missing = get_allow_missing(options.allow_missing,
414 options.no_allow_missing, len(selected),
417 # Create a new builder with the selected options.
418 output_dir = options.output_dir
420 dirname = options.branch.replace('/', '_')
421 # As a special case allow the board directory to be placed in the
422 # output directory itself rather than any subdirectory.
423 if not options.no_subdirs:
424 output_dir = os.path.join(options.output_dir, dirname)
425 if clean_dir and os.path.exists(output_dir):
426 shutil.rmtree(output_dir)
428 # For a dry run, just show our actions as a sanity check
430 show_actions(series, why_selected, selected, output_dir, options,
434 adjust_cfg = cfgutil.convert_list_to_dict(options.adjust_cfg)
436 # Drop LOCALVERSION_AUTO since it changes the version string on every commit
437 if options.reproducible_builds:
438 # If these are mentioned, leave the local version alone
439 if 'LOCALVERSION' in adjust_cfg or 'LOCALVERSION_AUTO' in adjust_cfg:
440 print('Not dropping LOCALVERSION_AUTO for reproducible build')
442 adjust_cfg['LOCALVERSION_AUTO'] = '~'
444 builder = Builder(toolchains, output_dir, git_dir,
445 options.threads, options.jobs, gnu_make=gnu_make, checkout=True,
446 show_unknown=options.show_unknown, step=options.step,
447 no_subdirs=options.no_subdirs, full_path=options.full_path,
448 verbose_build=options.verbose_build,
449 mrproper=options.mrproper,
450 per_board_out_dir=options.per_board_out_dir,
451 config_only=options.config_only,
452 squash_config_y=not options.preserve_config_y,
453 warnings_as_errors=options.warnings_as_errors,
454 work_in_output=options.work_in_output,
455 test_thread_exceptions=test_thread_exceptions,
456 adjust_cfg=adjust_cfg,
457 allow_missing=allow_missing, no_lto=options.no_lto,
458 reproducible_builds=options.reproducible_builds)
459 TEST_BUILDER = builder
460 builder.force_config_on_failure = not options.quick
462 builder.do_make = make_func
464 builder.force_build = options.force_build
465 builder.force_build_failures = options.force_build_failures
466 builder.force_reconfig = options.force_reconfig
467 builder.in_tree = options.in_tree
469 # Work out which boards to build
470 board_selected = brds.get_selected_dict()
473 commits = series.commits
474 # Number the commits for test purposes
475 for i, commit in enumerate(commits):
481 tprint(get_action_summary(options.summary, commits, board_selected,
484 # We can't show function sizes without board details at present
485 if options.show_bloat:
486 options.show_detail = True
487 builder.SetDisplayOptions(
488 options.show_errors, options.show_sizes, options.show_detail,
489 options.show_bloat, options.list_error_boards, options.show_config,
490 options.show_environment, options.filter_dtb_warnings,
491 options.filter_migration_warnings, options.ide)
493 builder.ShowSummary(commits, board_selected)
495 fail, warned, excs = builder.BuildBoards(
496 commits, board_selected, options.keep_outputs, options.verbose)
501 if warned and not options.ignore_warnings: