blob: d3d027f02ab539ba650bc4034331ae08afc9015c [file] [log] [blame]
Tom Rini10e47792018-05-06 17:58:06 -04001# SPDX-License-Identifier: GPL-2.0+
Simon Glassc05694f2013-04-03 11:07:16 +00002# Copyright (c) 2013 The Chromium OS Authors.
3#
Simon Glassc05694f2013-04-03 11:07:16 +00004
Simon Glassc1e1e1d2023-07-19 17:48:30 -06005"""Control module for buildman
6
7This holds the main control logic for buildman, when not running tests.
8"""
9
Simon Glassc229d322024-06-23 11:55:15 -060010import getpass
Simon Glassc05694f2013-04-03 11:07:16 +000011import multiprocessing
12import os
Simon Glassa10ebe12014-09-05 19:00:18 -060013import shutil
Simon Glassc05694f2013-04-03 11:07:16 +000014import sys
Simon Glassc229d322024-06-23 11:55:15 -060015import tempfile
16import time
Simon Glassc05694f2013-04-03 11:07:16 +000017
Simon Glass20751d62022-07-11 19:04:03 -060018from buildman import boards
Simon Glassf0d9c102020-04-17 18:09:02 -060019from buildman import bsettings
Simon Glasse5650a82022-01-22 05:07:33 -070020from buildman import cfgutil
Simon Glassf0d9c102020-04-17 18:09:02 -060021from buildman import toolchain
22from buildman.builder import Builder
Simon Glassa997ea52020-04-17 18:09:04 -060023from patman import gitutil
24from patman import patchstream
Simon Glass131444f2023-02-23 18:18:04 -070025from u_boot_pylib import command
26from u_boot_pylib import terminal
Simon Glassc229d322024-06-23 11:55:15 -060027from u_boot_pylib import tools
28from u_boot_pylib.terminal import print_clear, tprint
Simon Glassc05694f2013-04-03 11:07:16 +000029
Simon Glassaf0e29f2023-07-19 17:48:31 -060030TEST_BUILDER = None
31
Simon Glassc229d322024-06-23 11:55:15 -060032# Space-separated list of buildman process IDs currently running jobs
33RUNNING_FNAME = f'buildmanq.{getpass.getuser()}'
34
35# Lock file for access to RUNNING_FILE
36LOCK_FNAME = f'{RUNNING_FNAME}.lock'
37
38# Wait time for access to lock (seconds)
39LOCK_WAIT_S = 10
40
41# Wait time to start running
42RUN_WAIT_S = 300
43
Simon Glassc1e1e1d2023-07-19 17:48:30 -060044def get_plural(count):
Simon Glassc05694f2013-04-03 11:07:16 +000045 """Returns a plural 's' if count is not 1"""
46 return 's' if count != 1 else ''
47
Simon Glass14905d32023-07-19 17:49:00 -060048
49def count_build_commits(commits, step):
50 """Calculate the number of commits to be built
51
52 Args:
53 commits (list of Commit): Commits to build or None
54 step (int): Step value for commits, typically 1
55
56 Returns:
57 Number of commits that will be built
58 """
59 if commits:
60 count = len(commits)
61 return (count + step - 1) // step
62 return 0
63
64
65def get_action_summary(is_summary, commit_count, selected, threads, jobs):
Simon Glassc05694f2013-04-03 11:07:16 +000066 """Return a string summarising the intended action.
67
Simon Glassf50a7282023-07-19 17:48:45 -060068 Args:
69 is_summary (bool): True if this is a summary (otherwise it is building)
70 commits (list): List of commits being built
71 selected (list of Board): List of Board objects that are marked
72 step (int): Step increment through commits
73 threads (int): Number of processor threads being used
74 jobs (int): Number of jobs to build at once
75
Simon Glassc05694f2013-04-03 11:07:16 +000076 Returns:
77 Summary string.
78 """
Simon Glass14905d32023-07-19 17:49:00 -060079 if commit_count:
80 commit_str = f'{commit_count} commit{get_plural(commit_count)}'
Simon Glassd326ad72014-08-09 15:32:59 -060081 else:
82 commit_str = 'current source'
Simon Glassaf0e29f2023-07-19 17:48:31 -060083 msg = (f"{'Summary of' if is_summary else 'Building'} "
84 f'{commit_str} for {len(selected)} boards')
Simon Glassf50a7282023-07-19 17:48:45 -060085 msg += (f' ({threads} thread{get_plural(threads)}, '
86 f'{jobs} job{get_plural(jobs)} per thread)')
Simon Glassaf0e29f2023-07-19 17:48:31 -060087 return msg
Simon Glassc05694f2013-04-03 11:07:16 +000088
Simon Glassaf0e29f2023-07-19 17:48:31 -060089# pylint: disable=R0913
Simon Glass01c671d2023-07-19 17:48:46 -060090def show_actions(series, why_selected, boards_selected, output_dir,
91 board_warnings, step, threads, jobs, verbose):
Simon Glassc05694f2013-04-03 11:07:16 +000092 """Display a list of actions that we would take, if not a dry run.
93
94 Args:
95 series: Series object
96 why_selected: Dictionary where each key is a buildman argument
Simon Glass6af145f2017-01-23 05:38:56 -070097 provided by the user, and the value is the list of boards
98 brought in by that argument. For example, 'arm' might bring
99 in 400 boards, so in this case the key would be 'arm' and
Simon Glassc05694f2013-04-03 11:07:16 +0000100 the value would be a list of board names.
101 boards_selected: Dict of selected boards, key is target name,
102 value is Board object
Simon Glassf83559c2023-07-19 17:48:36 -0600103 output_dir (str): Output directory for builder
Simon Glassd9eb9f02018-06-11 23:26:46 -0600104 board_warnings: List of warnings obtained from board selected
Simon Glass01c671d2023-07-19 17:48:46 -0600105 step (int): Step increment through commits
106 threads (int): Number of processor threads being used
107 jobs (int): Number of jobs to build at once
108 verbose (bool): True to indicate why each board was selected
Simon Glassc05694f2013-04-03 11:07:16 +0000109 """
110 col = terminal.Color()
Simon Glassc78ed662019-10-31 07:42:53 -0600111 print('Dry run, so not doing much. But I would do this:')
112 print()
Simon Glassd326ad72014-08-09 15:32:59 -0600113 if series:
114 commits = series.commits
115 else:
116 commits = None
Simon Glass14905d32023-07-19 17:49:00 -0600117 print(get_action_summary(False, count_build_commits(commits, step),
118 boards_selected, threads, jobs))
Simon Glassf83559c2023-07-19 17:48:36 -0600119 print(f'Build directory: {output_dir}')
Simon Glassd326ad72014-08-09 15:32:59 -0600120 if commits:
Simon Glass01c671d2023-07-19 17:48:46 -0600121 for upto in range(0, len(series.commits), step):
Simon Glassd326ad72014-08-09 15:32:59 -0600122 commit = series.commits[upto]
Simon Glassf45d3742022-01-29 14:14:17 -0700123 print(' ', col.build(col.YELLOW, commit.hash[:8], bright=False), end=' ')
Simon Glassc78ed662019-10-31 07:42:53 -0600124 print(commit.subject)
125 print()
Simon Glassc05694f2013-04-03 11:07:16 +0000126 for arg in why_selected:
Simon Glassf869a902024-07-11 09:10:04 +0100127 # When -x is used, only the 'all' member exists
128 if arg != 'all' or len(why_selected) == 1:
Simon Glassaf0e29f2023-07-19 17:48:31 -0600129 print(arg, f': {len(why_selected[arg])} boards')
Simon Glass01c671d2023-07-19 17:48:46 -0600130 if verbose:
Simon Glassaf0e29f2023-07-19 17:48:31 -0600131 print(f" {' '.join(why_selected[arg])}")
132 print('Total boards to build for each '
133 f"commit: {len(why_selected['all'])}\n")
Simon Glassd9eb9f02018-06-11 23:26:46 -0600134 if board_warnings:
135 for warning in board_warnings:
Simon Glassf45d3742022-01-29 14:14:17 -0700136 print(col.build(col.YELLOW, warning))
Simon Glassc05694f2013-04-03 11:07:16 +0000137
Simon Glassc1e1e1d2023-07-19 17:48:30 -0600138def show_toolchain_prefix(brds, toolchains):
Simon Glass48ac42e2019-12-05 15:59:14 -0700139 """Show information about a the tool chain used by one or more boards
140
Simon Glass2df44be2020-03-18 09:42:47 -0600141 The function checks that all boards use the same toolchain, then prints
142 the correct value for CROSS_COMPILE.
Simon Glass48ac42e2019-12-05 15:59:14 -0700143
144 Args:
145 boards: Boards object containing selected boards
146 toolchains: Toolchains object containing available toolchains
Simon Glass48ac42e2019-12-05 15:59:14 -0700147
148 Return:
149 None on success, string error message otherwise
150 """
Simon Glass127a2392022-07-11 19:04:02 -0600151 board_selected = brds.get_selected_dict()
Simon Glass48ac42e2019-12-05 15:59:14 -0700152 tc_set = set()
Simon Glass5df45222022-07-11 19:04:00 -0600153 for brd in board_selected.values():
Simon Glass48ac42e2019-12-05 15:59:14 -0700154 tc_set.add(toolchains.Select(brd.arch))
155 if len(tc_set) != 1:
Simon Glasseaf1be22023-07-19 17:48:56 -0600156 sys.exit('Supplied boards must share one toolchain')
Simon Glassaf0e29f2023-07-19 17:48:31 -0600157 tchain = tc_set.pop()
158 print(tchain.GetEnvArgs(toolchain.VAR_CROSS_COMPILE))
Simon Glass48ac42e2019-12-05 15:59:14 -0700159
Simon Glassa8a0ce72023-07-19 17:49:28 -0600160def show_arch(brds):
161 """Show information about a the architecture used by one or more boards
162
163 The function checks that all boards use the same architecture, then prints
164 the correct value for ARCH.
165
166 Args:
167 boards: Boards object containing selected boards
168
169 Return:
170 None on success, string error message otherwise
171 """
172 board_selected = brds.get_selected_dict()
173 arch_set = set()
174 for brd in board_selected.values():
175 arch_set.add(brd.arch)
176 if len(arch_set) != 1:
177 sys.exit('Supplied boards must share one arch')
178 print(arch_set.pop())
179
Tom Rini93ebd462022-11-09 19:14:53 -0700180def get_allow_missing(opt_allow, opt_no_allow, num_selected, has_branch):
Simon Glassaf0e29f2023-07-19 17:48:31 -0600181 """Figure out whether to allow external blobs
182
183 Uses the allow-missing setting and the provided arguments to decide whether
184 missing external blobs should be allowed
185
186 Args:
187 opt_allow (bool): True if --allow-missing flag is set
188 opt_no_allow (bool): True if --no-allow-missing flag is set
189 num_selected (int): Number of selected board
190 has_branch (bool): True if a git branch (to build) has been provided
191
192 Returns:
193 bool: True to allow missing external blobs, False to produce an error if
194 external blobs are used
195 """
Tom Rini93ebd462022-11-09 19:14:53 -0700196 allow_missing = False
Simon Glass06b83a52023-07-19 17:49:05 -0600197 am_setting = bsettings.get_global_item_value('allow-missing')
Tom Rini93ebd462022-11-09 19:14:53 -0700198 if am_setting:
199 if am_setting == 'always':
200 allow_missing = True
201 if 'multiple' in am_setting and num_selected > 1:
202 allow_missing = True
203 if 'branch' in am_setting and has_branch:
204 allow_missing = True
205
206 if opt_allow:
207 allow_missing = True
208 if opt_no_allow:
209 allow_missing = False
210 return allow_missing
211
Simon Glass2183b842023-07-19 17:48:33 -0600212
Simon Glassfdbff802023-07-19 17:48:48 -0600213def count_commits(branch, count, col, git_dir):
214 """Could the number of commits in the branch/ranch being built
215
216 Args:
217 branch (str): Name of branch to build, or None if none
218 count (int): Number of commits to build, or -1 for all
219 col (Terminal.Color): Color object to use
220 git_dir (str): Git directory to use, e.g. './.git'
221
222 Returns:
223 tuple:
224 Number of commits being built
225 True if the 'branch' string contains a range rather than a simple
226 name
227 """
228 has_range = branch and '..' in branch
229 if count == -1:
230 if not branch:
231 count = 1
232 else:
233 if has_range:
234 count, msg = gitutil.count_commits_in_range(git_dir, branch)
235 else:
236 count, msg = gitutil.count_commits_in_branch(git_dir, branch)
237 if count is None:
238 sys.exit(col.build(col.RED, msg))
239 elif count == 0:
240 sys.exit(col.build(col.RED,
241 f"Range '{branch}' has no commits"))
242 if msg:
243 print(col.build(col.YELLOW, msg))
244 count += 1 # Build upstream commit also
245
246 if not count:
247 msg = (f"No commits found to process in branch '{branch}': "
248 "set branch's upstream or use -c flag")
249 sys.exit(col.build(col.RED, msg))
250 return count, has_range
251
252
Simon Glass100196e2023-07-19 17:48:40 -0600253def determine_series(selected, col, git_dir, count, branch, work_in_output):
Simon Glass2183b842023-07-19 17:48:33 -0600254 """Determine the series which is to be built, if any
255
Simon Glass1b140492023-07-19 17:48:50 -0600256 If there is a series, the commits in that series are numbered by setting
257 their sequence value (starting from 0). This is used by tests.
258
Simon Glass2183b842023-07-19 17:48:33 -0600259 Args:
Simon Glassf50a7282023-07-19 17:48:45 -0600260 selected (list of Board): List of Board objects that are marked
Simon Glass100196e2023-07-19 17:48:40 -0600261 selected
262 col (Terminal.Color): Color object to use
263 git_dir (str): Git directory to use, e.g. './.git'
Simon Glass2183b842023-07-19 17:48:33 -0600264 count (int): Number of commits in branch
Simon Glass2183b842023-07-19 17:48:33 -0600265 branch (str): Name of branch to build, or None if none
Simon Glass100196e2023-07-19 17:48:40 -0600266 work_in_output (bool): True to work in the output directory
Simon Glass2183b842023-07-19 17:48:33 -0600267
268 Returns:
269 Series: Series to build, or None for none
270
271 Read the metadata from the commits. First look at the upstream commit,
272 then the ones in the branch. We would like to do something like
273 upstream/master~..branch but that isn't possible if upstream/master is
274 a merge commit (it will list all the commits that form part of the
275 merge)
276
277 Conflicting tags are not a problem for buildman, since it does not use
278 them. For example, Series-version is not useful for buildman. On the
279 other hand conflicting tags will cause an error. So allow later tags
280 to overwrite earlier ones by setting allow_overwrite=True
281 """
Simon Glass100196e2023-07-19 17:48:40 -0600282
283 # Work out how many commits to build. We want to build everything on the
284 # branch. We also build the upstream commit as a control so we can see
285 # problems introduced by the first commit on the branch.
Simon Glassfdbff802023-07-19 17:48:48 -0600286 count, has_range = count_commits(branch, count, col, git_dir)
Simon Glass100196e2023-07-19 17:48:40 -0600287 if work_in_output:
288 if len(selected) != 1:
289 sys.exit(col.build(col.RED,
290 '-w can only be used with a single board'))
291 if count != 1:
292 sys.exit(col.build(col.RED,
293 '-w can only be used with a single commit'))
294
Simon Glass2183b842023-07-19 17:48:33 -0600295 if branch:
296 if count == -1:
297 if has_range:
298 range_expr = branch
299 else:
300 range_expr = gitutil.get_range_in_branch(git_dir, branch)
301 upstream_commit = gitutil.get_upstream(git_dir, branch)
302 series = patchstream.get_metadata_for_list(upstream_commit,
303 git_dir, 1, series=None, allow_overwrite=True)
304
305 series = patchstream.get_metadata_for_list(range_expr,
306 git_dir, None, series, allow_overwrite=True)
307 else:
308 # Honour the count
309 series = patchstream.get_metadata_for_list(branch,
310 git_dir, count, series=None, allow_overwrite=True)
Simon Glass1b140492023-07-19 17:48:50 -0600311
312 # Number the commits for test purposes
313 for i, commit in enumerate(series.commits):
314 commit.sequence = i
Simon Glass2183b842023-07-19 17:48:33 -0600315 else:
316 series = None
317 return series
318
319
Simon Glass6c4beca2023-07-19 17:48:34 -0600320def do_fetch_arch(toolchains, col, fetch_arch):
321 """Handle the --fetch-arch option
322
323 Args:
324 toolchains (Toolchains): Tool chains to use
325 col (terminal.Color): Color object to build
326 fetch_arch (str): Argument passed to the --fetch-arch option
327
328 Returns:
329 int: Return code for buildman
330 """
331 if fetch_arch == 'list':
332 sorted_list = toolchains.ListArchs()
333 print(col.build(
334 col.BLUE,
335 f"Available architectures: {' '.join(sorted_list)}\n"))
336 return 0
337
338 if fetch_arch == 'all':
339 fetch_arch = ','.join(toolchains.ListArchs())
340 print(col.build(col.CYAN,
341 f'\nDownloading toolchains: {fetch_arch}'))
342 for arch in fetch_arch.split(','):
343 print()
344 ret = toolchains.FetchAndInstall(arch)
345 if ret:
346 return ret
347 return 0
348
349
Simon Glass3bd8e302023-07-19 17:48:42 -0600350def get_toolchains(toolchains, col, override_toolchain, fetch_arch,
351 list_tool_chains, verbose):
352 """Get toolchains object to use
353
354 Args:
355 toolchains (Toolchains or None): Toolchains to use. If None, then a
356 Toolchains object will be created and scanned
357 col (Terminal.Color): Color object
358 override_toolchain (str or None): Override value for toolchain, or None
359 fetch_arch (bool): True to fetch the toolchain for the architectures
360 list_tool_chains (bool): True to list all tool chains
361 verbose (bool): True for verbose output when listing toolchains
362
363 Returns:
364 Either:
365 int: Operation completed and buildman should exit with exit code
366 Toolchains: Toolchains object to use
367 """
368 no_toolchains = toolchains is None
369 if no_toolchains:
370 toolchains = toolchain.Toolchains(override_toolchain)
371
372 if fetch_arch:
373 return do_fetch_arch(toolchains, col, fetch_arch)
374
375 if no_toolchains:
376 toolchains.GetSettings()
377 toolchains.Scan(list_tool_chains and verbose)
378 if list_tool_chains:
379 toolchains.List()
380 print()
381 return 0
382 return toolchains
383
384
Simon Glass66d4c882023-07-19 17:49:30 -0600385def get_boards_obj(output_dir, regen_board_list, maintainer_check, full_check,
386 threads, verbose):
Simon Glassabcc11d2023-07-19 17:48:41 -0600387 """Object the Boards object to use
388
389 Creates the output directory and ensures there is a boards.cfg file, then
390 read it in.
391
392 Args:
393 output_dir (str): Output directory to use
394 regen_board_list (bool): True to just regenerate the board list
395 maintainer_check (bool): True to just run a maintainer check
Simon Glass66d4c882023-07-19 17:49:30 -0600396 full_check (bool): True to just run a full check of Kconfig and
397 maintainers
Simon Glassabcc11d2023-07-19 17:48:41 -0600398 threads (int or None): Number of threads to use to create boards file
399 verbose (bool): False to suppress output from boards-file generation
400
401 Returns:
402 Either:
403 int: Operation completed and buildman should exit with exit code
404 Boards: Boards object to use
405 """
406 brds = boards.Boards()
407 nr_cpus = threads or multiprocessing.cpu_count()
Simon Glass66d4c882023-07-19 17:49:30 -0600408 if maintainer_check or full_check:
409 warnings = brds.build_board_list(jobs=nr_cpus,
410 warn_targets=full_check)[1]
Simon Glassabcc11d2023-07-19 17:48:41 -0600411 if warnings:
412 for warn in warnings:
413 print(warn, file=sys.stderr)
414 return 2
415 return 0
416
417 if not os.path.exists(output_dir):
418 os.makedirs(output_dir)
419 board_file = os.path.join(output_dir, 'boards.cfg')
420 if regen_board_list and regen_board_list != '-':
421 board_file = regen_board_list
422
423 okay = brds.ensure_board_list(board_file, nr_cpus, force=regen_board_list,
424 quiet=not verbose)
425 if regen_board_list:
426 return 0 if okay else 2
427 brds.read_boards(board_file)
428 return brds
429
430
Simon Glass72f8bff2023-07-19 17:48:39 -0600431def determine_boards(brds, args, col, opt_boards, exclude_list):
432 """Determine which boards to build
433
434 Each element of args and exclude can refer to a board name, arch or SoC
435
436 Args:
437 brds (Boards): Boards object
438 args (list of str): Arguments describing boards to build
439 col (Terminal.Color): Color object
440 opt_boards (list of str): Specific boards to build, or None for all
441 exclude_list (list of str): Arguments describing boards to exclude
442
443 Returns:
444 tuple:
445 list of Board: List of Board objects that are marked selected
446 why_selected: Dictionary where each key is a buildman argument
447 provided by the user, and the value is the list of boards
448 brought in by that argument. For example, 'arm' might bring
449 in 400 boards, so in this case the key would be 'arm' and
450 the value would be a list of board names.
451 board_warnings: List of warnings obtained from board selected
452 """
453 exclude = []
454 if exclude_list:
455 for arg in exclude_list:
456 exclude += arg.split(',')
457
458 if opt_boards:
459 requested_boards = []
460 for brd in opt_boards:
461 requested_boards += brd.split(',')
462 else:
463 requested_boards = None
464 why_selected, board_warnings = brds.select_boards(args, exclude,
465 requested_boards)
466 selected = brds.get_selected()
467 if not selected:
468 sys.exit(col.build(col.RED, 'No matching boards found'))
469 return selected, why_selected, board_warnings
470
471
Simon Glassa56ac992023-07-19 17:49:04 -0600472def adjust_args(args, series, selected):
473 """Adjust arguments according to various constraints
Simon Glass70274872023-07-19 17:48:47 -0600474
475 Updates verbose, show_errors, threads, jobs and step
476
477 Args:
Simon Glassa56ac992023-07-19 17:49:04 -0600478 args (Namespace): Namespace object to adjust
Simon Glass70274872023-07-19 17:48:47 -0600479 series (Series): Series being built / summarised
480 selected (list of Board): List of Board objects that are marked
481 """
Simon Glassa56ac992023-07-19 17:49:04 -0600482 if not series and not args.dry_run:
483 args.verbose = True
484 if not args.summary:
485 args.show_errors = True
Simon Glass70274872023-07-19 17:48:47 -0600486
487 # By default we have one thread per CPU. But if there are not enough jobs
488 # we can have fewer threads and use a high '-j' value for make.
Simon Glassa56ac992023-07-19 17:49:04 -0600489 if args.threads is None:
490 args.threads = min(multiprocessing.cpu_count(), len(selected))
491 if not args.jobs:
492 args.jobs = max(1, (multiprocessing.cpu_count() +
Simon Glass70274872023-07-19 17:48:47 -0600493 len(selected) - 1) // len(selected))
494
Simon Glassa56ac992023-07-19 17:49:04 -0600495 if not args.step:
496 args.step = len(series.commits) - 1
Simon Glass70274872023-07-19 17:48:47 -0600497
Simon Glass31353f22023-07-19 17:48:53 -0600498 # We can't show function sizes without board details at present
Simon Glassa56ac992023-07-19 17:49:04 -0600499 if args.show_bloat:
500 args.show_detail = True
Simon Glass31353f22023-07-19 17:48:53 -0600501
Simon Glass3b300c52023-07-19 17:48:49 -0600502
503def setup_output_dir(output_dir, work_in_output, branch, no_subdirs, col,
504 clean_dir):
505 """Set up the output directory
506
507 Args:
508 output_dir (str): Output directory provided by the user, or None if none
509 work_in_output (bool): True to work in the output directory
510 branch (str): Name of branch to build, or None if none
511 no_subdirs (bool): True to put the output in the top-level output dir
512 clean_dir: Used for tests only, indicates that the existing output_dir
513 should be removed before starting the build
514
515 Returns:
516 str: Updated output directory pathname
517 """
518 if not output_dir:
519 if work_in_output:
520 sys.exit(col.build(col.RED, '-w requires that you specify -o'))
521 output_dir = '..'
522 if branch and not no_subdirs:
523 # As a special case allow the board directory to be placed in the
524 # output directory itself rather than any subdirectory.
525 dirname = branch.replace('/', '_')
526 output_dir = os.path.join(output_dir, dirname)
527 if clean_dir and os.path.exists(output_dir):
528 shutil.rmtree(output_dir)
529 return output_dir
530
Simon Glassfdd750b2023-07-19 17:48:57 -0600531
Simon Glassa56ac992023-07-19 17:49:04 -0600532def run_builder(builder, commits, board_selected, args):
Simon Glass927c8272023-07-19 17:48:54 -0600533 """Run the builder or show the summary
534
535 Args:
536 commits (list of Commit): List of commits being built, None if no branch
537 boards_selected (dict): Dict of selected boards:
538 key: target name
539 value: Board object
Simon Glassa56ac992023-07-19 17:49:04 -0600540 args (Namespace): Namespace to use
Simon Glass927c8272023-07-19 17:48:54 -0600541
542 Returns:
543 int: Return code for buildman
544 """
Simon Glassa56ac992023-07-19 17:49:04 -0600545 gnu_make = command.output(os.path.join(args.git,
Simon Glassfdd750b2023-07-19 17:48:57 -0600546 'scripts/show-gnu-make'), raise_on_error=False).rstrip()
547 if not gnu_make:
548 sys.exit('GNU Make not found')
549 builder.gnu_make = gnu_make
550
Simon Glassa56ac992023-07-19 17:49:04 -0600551 if not args.ide:
552 commit_count = count_build_commits(commits, args.step)
553 tprint(get_action_summary(args.summary, commit_count, board_selected,
554 args.threads, args.jobs))
Simon Glass927c8272023-07-19 17:48:54 -0600555
Simon Glassbc74d942023-07-19 17:49:06 -0600556 builder.set_display_options(
557 args.show_errors, args.show_sizes, args.show_detail, args.show_bloat,
558 args.list_error_boards, args.show_config, args.show_environment,
559 args.filter_dtb_warnings, args.filter_migration_warnings, args.ide)
Simon Glassa56ac992023-07-19 17:49:04 -0600560 if args.summary:
Simon Glassbc74d942023-07-19 17:49:06 -0600561 builder.show_summary(commits, board_selected)
Simon Glass927c8272023-07-19 17:48:54 -0600562 else:
Simon Glassbc74d942023-07-19 17:49:06 -0600563 fail, warned, excs = builder.build_boards(
Simon Glassa56ac992023-07-19 17:49:04 -0600564 commits, board_selected, args.keep_outputs, args.verbose)
Simon Glass927c8272023-07-19 17:48:54 -0600565 if excs:
566 return 102
567 if fail:
568 return 100
Simon Glassa56ac992023-07-19 17:49:04 -0600569 if warned and not args.ignore_warnings:
Simon Glass927c8272023-07-19 17:48:54 -0600570 return 101
571 return 0
Simon Glass3b300c52023-07-19 17:48:49 -0600572
Simon Glassf7524f32023-07-19 17:48:58 -0600573
574def calc_adjust_cfg(adjust_cfg, reproducible_builds):
575 """Calculate the value to use for adjust_cfg
576
577 Args:
578 adjust_cfg (list of str): List of configuration changes. See cfgutil for
579 details
580 reproducible_builds (bool): True to adjust the configuration to get
581 reproduceable builds
582
583 Returns:
584 adjust_cfg (list of str): List of configuration changes
585 """
586 adjust_cfg = cfgutil.convert_list_to_dict(adjust_cfg)
587
588 # Drop LOCALVERSION_AUTO since it changes the version string on every commit
589 if reproducible_builds:
590 # If these are mentioned, leave the local version alone
591 if 'LOCALVERSION' in adjust_cfg or 'LOCALVERSION_AUTO' in adjust_cfg:
592 print('Not dropping LOCALVERSION_AUTO for reproducible build')
593 else:
594 adjust_cfg['LOCALVERSION_AUTO'] = '~'
595 return adjust_cfg
596
Simon Glassc229d322024-06-23 11:55:15 -0600597
598def read_procs(tmpdir=tempfile.gettempdir()):
599 """Read the list of running buildman processes
600
601 If the list is corrupted, returns an empty list
602
603 Args:
604 tmpdir (str): Temporary directory to use (for testing only)
605 """
606 running_fname = os.path.join(tmpdir, RUNNING_FNAME)
607 procs = []
608 if os.path.exists(running_fname):
609 items = tools.read_file(running_fname, binary=False).split()
610 try:
611 procs = [int(x) for x in items]
612 except ValueError: # Handle invalid format
613 pass
614 return procs
615
616
617def check_pid(pid):
618 """Check for existence of a unix PID
619
620 https://stackoverflow.com/questions/568271/how-to-check-if-there-exists-a-process-with-a-given-pid-in-python
621
622 Args:
623 pid (int): PID to check
624
625 Returns:
626 True if it exists, else False
627 """
628 try:
629 os.kill(pid, 0)
630 except OSError:
631 return False
632 else:
633 return True
634
635
636def write_procs(procs, tmpdir=tempfile.gettempdir()):
637 """Write the list of running buildman processes
638
639 Args:
640 tmpdir (str): Temporary directory to use (for testing only)
641 """
642 running_fname = os.path.join(tmpdir, RUNNING_FNAME)
643 tools.write_file(running_fname, ' '.join([str(p) for p in procs]),
644 binary=False)
645
646 # Allow another user to access the file
647 os.chmod(running_fname, 0o666)
648
649def wait_for_process_limit(limit, tmpdir=tempfile.gettempdir(),
650 pid=os.getpid()):
651 """Wait until the number of buildman processes drops to the limit
652
653 This uses FileLock to protect a 'running' file, which contains a list of
654 PIDs of running buildman processes. The number of PIDs in the file indicates
655 the number of running processes.
656
657 When buildman starts up, it calls this function to wait until it is OK to
658 start the build.
659
660 On exit, no attempt is made to remove the PID from the file, since other
661 buildman processes will notice that the PID is no-longer valid, and ignore
662 it.
663
664 Two timeouts are provided:
665 LOCK_WAIT_S: length of time to wait for the lock; if this occurs, the
666 lock is busted / removed before trying again
667 RUN_WAIT_S: length of time to wait to be allowed to run; if this occurs,
668 the build starts, with the PID being added to the file.
669
670 Args:
671 limit (int): Maximum number of buildman processes, including this one;
672 must be > 0
673 tmpdir (str): Temporary directory to use (for testing only)
674 pid (int): Current process ID (for testing only)
675 """
676 from filelock import Timeout, FileLock
677
678 running_fname = os.path.join(tmpdir, RUNNING_FNAME)
679 lock_fname = os.path.join(tmpdir, LOCK_FNAME)
680 lock = FileLock(lock_fname)
681
682 # Allow another user to access the file
683 col = terminal.Color()
684 tprint('Waiting for other buildman processes...', newline=False,
685 colour=col.RED)
686
687 claimed = False
688 deadline = time.time() + RUN_WAIT_S
689 while True:
690 try:
691 with lock.acquire(timeout=LOCK_WAIT_S):
692 os.chmod(lock_fname, 0o666)
693 procs = read_procs(tmpdir)
694
695 # Drop PIDs which are not running
696 procs = list(filter(check_pid, procs))
697
698 # If we haven't hit the limit, add ourself
699 if len(procs) < limit:
700 tprint('done...', newline=False)
701 claimed = True
702 if time.time() >= deadline:
703 tprint('timeout...', newline=False)
704 claimed = True
705 if claimed:
706 write_procs(procs + [pid], tmpdir)
707 break
708
709 except Timeout:
710 tprint('failed to get lock: busting...', newline=False)
711 os.remove(lock_fname)
712
713 time.sleep(1)
714 tprint('starting build', newline=False)
715 print_clear()
Simon Glassf7524f32023-07-19 17:48:58 -0600716
Simon Glassa56ac992023-07-19 17:49:04 -0600717def do_buildman(args, toolchains=None, make_func=None, brds=None,
Simon Glassc1e1e1d2023-07-19 17:48:30 -0600718 clean_dir=False, test_thread_exceptions=False):
Simon Glassc05694f2013-04-03 11:07:16 +0000719 """The main control code for buildman
720
721 Args:
Simon Glassa56ac992023-07-19 17:49:04 -0600722 args: ArgumentParser object
Simon Glassc05694f2013-04-03 11:07:16 +0000723 args: Command line arguments (list of strings)
Simon Glassed098bb2014-09-05 19:00:13 -0600724 toolchains: Toolchains to use - this should be a Toolchains()
725 object. If None, then it will be created and scanned
726 make_func: Make function to use for the builder. This is called
727 to execute 'make'. If this is None, the normal function
728 will be used, which calls the 'make' tool with suitable
729 arguments. This setting is useful for tests.
Simon Glass5df45222022-07-11 19:04:00 -0600730 brds: Boards() object to use, containing a list of available
Simon Glasscbd36582014-09-05 19:00:16 -0600731 boards. If this is None it will be created and scanned.
Simon Glassa29b3ea2021-04-11 16:27:25 +1200732 clean_dir: Used for tests only, indicates that the existing output_dir
733 should be removed before starting the build
Simon Glass9bf9a722021-04-11 16:27:27 +1200734 test_thread_exceptions: Uses for tests only, True to make the threads
735 raise an exception instead of reporting their result. This simulates
736 a failure in the code somewhere
Simon Glassc05694f2013-04-03 11:07:16 +0000737 """
Simon Glassaf0e29f2023-07-19 17:48:31 -0600738 # Used so testing can obtain the builder: pylint: disable=W0603
739 global TEST_BUILDER
Simon Glassa10ebe12014-09-05 19:00:18 -0600740
Simon Glass761648b2022-01-29 14:14:11 -0700741 gitutil.setup()
Simon Glass9f1ba0f2016-07-27 20:33:02 -0600742 col = terminal.Color()
Simon Glassc05694f2013-04-03 11:07:16 +0000743
Simon Glassa56ac992023-07-19 17:49:04 -0600744 git_dir = os.path.join(args.git, '.git')
Simon Glassc05694f2013-04-03 11:07:16 +0000745
Simon Glassa56ac992023-07-19 17:49:04 -0600746 toolchains = get_toolchains(toolchains, col, args.override_toolchain,
747 args.fetch_arch, args.list_tool_chains,
748 args.verbose)
Simon Glass18a07252023-08-03 12:51:36 -0600749 if isinstance(toolchains, int):
750 return toolchains
751
Simon Glass3b300c52023-07-19 17:48:49 -0600752 output_dir = setup_output_dir(
Simon Glassa56ac992023-07-19 17:49:04 -0600753 args.output_dir, args.work_in_output, args.branch,
754 args.no_subdirs, col, clean_dir)
Simon Glass6029af12020-04-09 15:08:51 -0600755
Simon Glassc05694f2013-04-03 11:07:16 +0000756 # Work out what subset of the boards we are building
Simon Glass5df45222022-07-11 19:04:00 -0600757 if not brds:
Simon Glassa56ac992023-07-19 17:49:04 -0600758 brds = get_boards_obj(output_dir, args.regen_board_list,
Simon Glass66d4c882023-07-19 17:49:30 -0600759 args.maintainer_check, args.full_check,
Simon Glass89592fc2023-09-07 10:00:18 -0600760 args.threads, args.verbose and
761 not args.print_arch and not args.print_prefix)
Simon Glassabcc11d2023-07-19 17:48:41 -0600762 if isinstance(brds, int):
763 return brds
Simon Glass924c73a2014-08-28 09:43:41 -0600764
Simon Glass72f8bff2023-07-19 17:48:39 -0600765 selected, why_selected, board_warnings = determine_boards(
Simon Glassa56ac992023-07-19 17:49:04 -0600766 brds, args.terms, col, args.boards, args.exclude)
Simon Glassc05694f2013-04-03 11:07:16 +0000767
Simon Glassa56ac992023-07-19 17:49:04 -0600768 if args.print_prefix:
Simon Glasseaf1be22023-07-19 17:48:56 -0600769 show_toolchain_prefix(brds, toolchains)
Simon Glass48ac42e2019-12-05 15:59:14 -0700770 return 0
771
Simon Glassa8a0ce72023-07-19 17:49:28 -0600772 if args.print_arch:
773 show_arch(brds)
774 return 0
775
Simon Glassa56ac992023-07-19 17:49:04 -0600776 series = determine_series(selected, col, git_dir, args.count,
777 args.branch, args.work_in_output)
Simon Glassc05694f2013-04-03 11:07:16 +0000778
Simon Glassa56ac992023-07-19 17:49:04 -0600779 adjust_args(args, series, selected)
Simon Glassc05694f2013-04-03 11:07:16 +0000780
Simon Glass70274872023-07-19 17:48:47 -0600781 # For a dry run, just show our actions as a sanity check
Simon Glassa56ac992023-07-19 17:49:04 -0600782 if args.dry_run:
Simon Glass70274872023-07-19 17:48:47 -0600783 show_actions(series, why_selected, selected, output_dir, board_warnings,
Simon Glassa56ac992023-07-19 17:49:04 -0600784 args.step, args.threads, args.jobs,
785 args.verbose)
Simon Glass70274872023-07-19 17:48:47 -0600786 return 0
Simon Glassc05694f2013-04-03 11:07:16 +0000787
Simon Glassa56ac992023-07-19 17:49:04 -0600788 # Create a new builder with the selected args
Simon Glass2183b842023-07-19 17:48:33 -0600789 builder = Builder(toolchains, output_dir, git_dir,
Simon Glassa56ac992023-07-19 17:49:04 -0600790 args.threads, args.jobs, checkout=True,
791 show_unknown=args.show_unknown, step=args.step,
Tom Rini3f6f9b22024-07-05 14:34:07 -0600792 no_subdirs=args.no_subdirs, full_path=args.full_path,
793 verbose_build=args.verbose_build,
794 mrproper=args.mrproper,
795 fallback_mrproper=args.fallback_mrproper,
Simon Glassa56ac992023-07-19 17:49:04 -0600796 per_board_out_dir=args.per_board_out_dir,
797 config_only=args.config_only,
798 squash_config_y=not args.preserve_config_y,
799 warnings_as_errors=args.warnings_as_errors,
800 work_in_output=args.work_in_output,
Simon Glasse5650a82022-01-22 05:07:33 -0700801 test_thread_exceptions=test_thread_exceptions,
Simon Glassa56ac992023-07-19 17:49:04 -0600802 adjust_cfg=calc_adjust_cfg(args.adjust_cfg,
803 args.reproducible_builds),
804 allow_missing=get_allow_missing(args.allow_missing,
805 args.no_allow_missing,
806 len(selected), args.branch),
807 no_lto=args.no_lto,
808 reproducible_builds=args.reproducible_builds,
809 force_build = args.force_build,
810 force_build_failures = args.force_build_failures,
811 force_reconfig = args.force_reconfig, in_tree = args.in_tree,
812 force_config_on_failure=not args.quick, make_func=make_func)
Simon Glassc05694f2013-04-03 11:07:16 +0000813
Simon Glasscf91d312023-07-19 17:48:52 -0600814 TEST_BUILDER = builder
Simon Glassc05694f2013-04-03 11:07:16 +0000815
Simon Glassc229d322024-06-23 11:55:15 -0600816 if args.process_limit:
817 wait_for_process_limit(args.process_limit)
818
Simon Glass00401d92023-07-19 17:48:55 -0600819 return run_builder(builder, series.commits if series else None,
Simon Glassa56ac992023-07-19 17:49:04 -0600820 brds.get_selected_dict(), args)