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, bldr, 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 bldr: The builder that will be used to build the commits
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: {bldr.base_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(count, has_range, branch, git_dir):
152 """Determine the series which is to be built, if any
155 count (int): Number of commits in branch
156 has_range (bool): True if a range of commits ('xx..yy') is being built
157 branch (str): Name of branch to build, or None if none
158 git_dir (str): Git directory to use, e.g. './.git'
161 Series: Series to build, or None for none
163 Read the metadata from the commits. First look at the upstream commit,
164 then the ones in the branch. We would like to do something like
165 upstream/master~..branch but that isn't possible if upstream/master is
166 a merge commit (it will list all the commits that form part of the
169 Conflicting tags are not a problem for buildman, since it does not use
170 them. For example, Series-version is not useful for buildman. On the
171 other hand conflicting tags will cause an error. So allow later tags
172 to overwrite earlier ones by setting allow_overwrite=True
179 range_expr = gitutil.get_range_in_branch(git_dir, branch)
180 upstream_commit = gitutil.get_upstream(git_dir, branch)
181 series = patchstream.get_metadata_for_list(upstream_commit,
182 git_dir, 1, series=None, allow_overwrite=True)
184 series = patchstream.get_metadata_for_list(range_expr,
185 git_dir, None, series, allow_overwrite=True)
188 series = patchstream.get_metadata_for_list(branch,
189 git_dir, count, series=None, allow_overwrite=True)
195 def do_fetch_arch(toolchains, col, fetch_arch):
196 """Handle the --fetch-arch option
199 toolchains (Toolchains): Tool chains to use
200 col (terminal.Color): Color object to build
201 fetch_arch (str): Argument passed to the --fetch-arch option
204 int: Return code for buildman
206 if fetch_arch == 'list':
207 sorted_list = toolchains.ListArchs()
210 f"Available architectures: {' '.join(sorted_list)}\n"))
213 if fetch_arch == 'all':
214 fetch_arch = ','.join(toolchains.ListArchs())
215 print(col.build(col.CYAN,
216 f'\nDownloading toolchains: {fetch_arch}'))
217 for arch in fetch_arch.split(','):
219 ret = toolchains.FetchAndInstall(arch)
225 def do_buildman(options, args, toolchains=None, make_func=None, brds=None,
226 clean_dir=False, test_thread_exceptions=False):
227 """The main control code for buildman
230 options: Command line options object
231 args: Command line arguments (list of strings)
232 toolchains: Toolchains to use - this should be a Toolchains()
233 object. If None, then it will be created and scanned
234 make_func: Make function to use for the builder. This is called
235 to execute 'make'. If this is None, the normal function
236 will be used, which calls the 'make' tool with suitable
237 arguments. This setting is useful for tests.
238 brds: Boards() object to use, containing a list of available
239 boards. If this is None it will be created and scanned.
240 clean_dir: Used for tests only, indicates that the existing output_dir
241 should be removed before starting the build
242 test_thread_exceptions: Uses for tests only, True to make the threads
243 raise an exception instead of reporting their result. This simulates
244 a failure in the code somewhere
246 # Used so testing can obtain the builder: pylint: disable=W0603
250 col = terminal.Color()
252 git_dir = os.path.join(options.git, '.git')
254 no_toolchains = toolchains is None
256 toolchains = toolchain.Toolchains(options.override_toolchain)
258 if options.fetch_arch:
259 return do_fetch_arch(toolchains, col, options.fetch_arch)
262 toolchains.GetSettings()
263 toolchains.Scan(options.list_tool_chains and options.verbose)
264 if options.list_tool_chains:
269 if not options.output_dir:
270 if options.work_in_output:
271 sys.exit(col.build(col.RED, '-w requires that you specify -o'))
272 options.output_dir = '..'
274 nr_cups = options.threads or multiprocessing.cpu_count()
276 # Work out what subset of the boards we are building
278 if not os.path.exists(options.output_dir):
279 os.makedirs(options.output_dir)
280 board_file = os.path.join(options.output_dir, 'boards.cfg')
281 if options.regen_board_list and options.regen_board_list != '-':
282 board_file = options.regen_board_list
284 brds = boards.Boards()
286 if options.maintainer_check:
287 warnings = brds.build_board_list(jobs=nr_cups)[1]
289 for warn in warnings:
290 print(warn, file=sys.stderr)
294 okay = brds.ensure_board_list(
296 options.threads or multiprocessing.cpu_count(),
297 force=options.regen_board_list,
298 quiet=not options.verbose)
299 if options.regen_board_list:
300 return 0 if okay else 2
301 brds.read_boards(board_file)
305 for arg in options.exclude:
306 exclude += arg.split(',')
309 requested_boards = []
310 for brd in options.boards:
311 requested_boards += brd.split(',')
313 requested_boards = None
314 why_selected, board_warnings = brds.select_boards(args, exclude,
316 selected = brds.get_selected()
318 sys.exit(col.build(col.RED, 'No matching boards found'))
320 if options.print_prefix:
321 err = show_toolchain_prefix(brds, toolchains)
323 sys.exit(col.build(col.RED, err))
326 # Work out how many commits to build. We want to build everything on the
327 # branch. We also build the upstream commit as a control so we can see
328 # problems introduced by the first commit on the branch.
329 count = options.count
330 has_range = options.branch and '..' in options.branch
332 if not options.branch:
336 count, msg = gitutil.count_commits_in_range(git_dir,
339 count, msg = gitutil.count_commits_in_branch(git_dir,
342 sys.exit(col.build(col.RED, msg))
344 sys.exit(col.build(col.RED,
345 f"Range '{options.branch}' has no commits"))
347 print(col.build(col.YELLOW, msg))
348 count += 1 # Build upstream commit also
351 msg = (f"No commits found to process in branch '{options.branch}': "
352 "set branch's upstream or use -c flag")
353 sys.exit(col.build(col.RED, msg))
354 if options.work_in_output:
355 if len(selected) != 1:
356 sys.exit(col.build(col.RED,
357 '-w can only be used with a single board'))
359 sys.exit(col.build(col.RED,
360 '-w can only be used with a single commit'))
362 series = determine_series(count, has_range, options.branch, git_dir)
363 if not series and not options.dry_run:
364 options.verbose = True
365 if not options.summary:
366 options.show_errors = True
368 # By default we have one thread per CPU. But if there are not enough jobs
369 # we can have fewer threads and use a high '-j' value for make.
370 if options.threads is None:
371 options.threads = min(multiprocessing.cpu_count(), len(selected))
373 options.jobs = max(1, (multiprocessing.cpu_count() +
374 len(selected) - 1) // len(selected))
377 options.step = len(series.commits) - 1
379 gnu_make = command.output(os.path.join(options.git,
380 'scripts/show-gnu-make'), raise_on_error=False).rstrip()
382 sys.exit('GNU Make not found')
384 allow_missing = get_allow_missing(options.allow_missing,
385 options.no_allow_missing, len(selected),
388 # Create a new builder with the selected options.
389 output_dir = options.output_dir
391 dirname = options.branch.replace('/', '_')
392 # As a special case allow the board directory to be placed in the
393 # output directory itself rather than any subdirectory.
394 if not options.no_subdirs:
395 output_dir = os.path.join(options.output_dir, dirname)
396 if clean_dir and os.path.exists(output_dir):
397 shutil.rmtree(output_dir)
398 adjust_cfg = cfgutil.convert_list_to_dict(options.adjust_cfg)
400 # Drop LOCALVERSION_AUTO since it changes the version string on every commit
401 if options.reproducible_builds:
402 # If these are mentioned, leave the local version alone
403 if 'LOCALVERSION' in adjust_cfg or 'LOCALVERSION_AUTO' in adjust_cfg:
404 print('Not dropping LOCALVERSION_AUTO for reproducible build')
406 adjust_cfg['LOCALVERSION_AUTO'] = '~'
408 builder = Builder(toolchains, output_dir, git_dir,
409 options.threads, options.jobs, gnu_make=gnu_make, checkout=True,
410 show_unknown=options.show_unknown, step=options.step,
411 no_subdirs=options.no_subdirs, full_path=options.full_path,
412 verbose_build=options.verbose_build,
413 mrproper=options.mrproper,
414 per_board_out_dir=options.per_board_out_dir,
415 config_only=options.config_only,
416 squash_config_y=not options.preserve_config_y,
417 warnings_as_errors=options.warnings_as_errors,
418 work_in_output=options.work_in_output,
419 test_thread_exceptions=test_thread_exceptions,
420 adjust_cfg=adjust_cfg,
421 allow_missing=allow_missing, no_lto=options.no_lto,
422 reproducible_builds=options.reproducible_builds)
423 TEST_BUILDER = builder
424 builder.force_config_on_failure = not options.quick
426 builder.do_make = make_func
428 # For a dry run, just show our actions as a sanity check
430 show_actions(series, why_selected, selected, builder, options,
433 builder.force_build = options.force_build
434 builder.force_build_failures = options.force_build_failures
435 builder.force_reconfig = options.force_reconfig
436 builder.in_tree = options.in_tree
438 # Work out which boards to build
439 board_selected = brds.get_selected_dict()
442 commits = series.commits
443 # Number the commits for test purposes
444 for i, commit in enumerate(commits):
450 tprint(get_action_summary(options.summary, commits, board_selected,
453 # We can't show function sizes without board details at present
454 if options.show_bloat:
455 options.show_detail = True
456 builder.SetDisplayOptions(
457 options.show_errors, options.show_sizes, options.show_detail,
458 options.show_bloat, options.list_error_boards, options.show_config,
459 options.show_environment, options.filter_dtb_warnings,
460 options.filter_migration_warnings, options.ide)
462 builder.ShowSummary(commits, board_selected)
464 fail, warned, excs = builder.BuildBoards(
465 commits, board_selected, options.keep_outputs, options.verbose)
470 if warned and not options.ignore_warnings: