1 # SPDX-License-Identifier: GPL-2.0+
2 # Copyright (c) 2013 The Chromium OS Authors.
11 from buildman import board
12 from buildman import bsettings
13 from buildman import toolchain
14 from buildman.builder import Builder
15 from patman import command
16 from patman import gitutil
17 from patman import patchstream
18 from patman import terminal
19 from patman.terminal import Print
22 """Returns a plural 's' if count is not 1"""
23 return 's' if count != 1 else ''
25 def GetActionSummary(is_summary, commits, selected, options):
26 """Return a string summarising the intended action.
33 count = (count + options.step - 1) // options.step
34 commit_str = '%d commit%s' % (count, GetPlural(count))
36 commit_str = 'current source'
37 str = '%s %s for %d boards' % (
38 'Summary of' if is_summary else 'Building', commit_str,
40 str += ' (%d thread%s, %d job%s per thread)' % (options.threads,
41 GetPlural(options.threads), options.jobs, GetPlural(options.jobs))
44 def ShowActions(series, why_selected, boards_selected, builder, options,
46 """Display a list of actions that we would take, if not a dry run.
50 why_selected: Dictionary where each key is a buildman argument
51 provided by the user, and the value is the list of boards
52 brought in by that argument. For example, 'arm' might bring
53 in 400 boards, so in this case the key would be 'arm' and
54 the value would be a list of board names.
55 boards_selected: Dict of selected boards, key is target name,
57 builder: The builder that will be used to build the commits
58 options: Command line options object
59 board_warnings: List of warnings obtained from board selected
61 col = terminal.Color()
62 print('Dry run, so not doing much. But I would do this:')
65 commits = series.commits
68 print(GetActionSummary(False, commits, boards_selected,
70 print('Build directory: %s' % builder.base_dir)
72 for upto in range(0, len(series.commits), options.step):
73 commit = series.commits[upto]
74 print(' ', col.Color(col.YELLOW, commit.hash[:8], bright=False), end=' ')
77 for arg in why_selected:
79 print(arg, ': %d boards' % len(why_selected[arg]))
81 print(' %s' % ' '.join(why_selected[arg]))
82 print(('Total boards to build for each commit: %d\n' %
83 len(why_selected['all'])))
85 for warning in board_warnings:
86 print(col.Color(col.YELLOW, warning))
88 def ShowToolchainPrefix(boards, toolchains):
89 """Show information about a the tool chain used by one or more boards
91 The function checks that all boards use the same toolchain, then prints
92 the correct value for CROSS_COMPILE.
95 boards: Boards object containing selected boards
96 toolchains: Toolchains object containing available toolchains
99 None on success, string error message otherwise
101 boards = boards.GetSelectedDict()
103 for brd in boards.values():
104 tc_set.add(toolchains.Select(brd.arch))
106 return 'Supplied boards must share one toolchain'
109 print(tc.GetEnvArgs(toolchain.VAR_CROSS_COMPILE))
112 def DoBuildman(options, args, toolchains=None, make_func=None, boards=None,
113 clean_dir=False, test_thread_exceptions=False):
114 """The main control code for buildman
117 options: Command line options object
118 args: Command line arguments (list of strings)
119 toolchains: Toolchains to use - this should be a Toolchains()
120 object. If None, then it will be created and scanned
121 make_func: Make function to use for the builder. This is called
122 to execute 'make'. If this is None, the normal function
123 will be used, which calls the 'make' tool with suitable
124 arguments. This setting is useful for tests.
125 board: Boards() object to use, containing a list of available
126 boards. If this is None it will be created and scanned.
127 clean_dir: Used for tests only, indicates that the existing output_dir
128 should be removed before starting the build
129 test_thread_exceptions: Uses for tests only, True to make the threads
130 raise an exception instead of reporting their result. This simulates
131 a failure in the code somewhere
135 if options.full_help:
136 pager = os.getenv('PAGER')
139 fname = os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])),
141 command.Run(pager, fname)
145 col = terminal.Color()
147 options.git_dir = os.path.join(options.git, '.git')
149 no_toolchains = toolchains is None
151 toolchains = toolchain.Toolchains(options.override_toolchain)
153 if options.fetch_arch:
154 if options.fetch_arch == 'list':
155 sorted_list = toolchains.ListArchs()
156 print(col.Color(col.BLUE, 'Available architectures: %s\n' %
157 ' '.join(sorted_list)))
160 fetch_arch = options.fetch_arch
161 if fetch_arch == 'all':
162 fetch_arch = ','.join(toolchains.ListArchs())
163 print(col.Color(col.CYAN, '\nDownloading toolchains: %s' %
165 for arch in fetch_arch.split(','):
167 ret = toolchains.FetchAndInstall(arch)
173 toolchains.GetSettings()
174 toolchains.Scan(options.list_tool_chains and options.verbose)
175 if options.list_tool_chains:
180 if options.incremental:
181 print(col.Color(col.RED,
182 'Warning: -I has been removed. See documentation'))
183 if not options.output_dir:
184 if options.work_in_output:
185 sys.exit(col.Color(col.RED, '-w requires that you specify -o'))
186 options.output_dir = '..'
188 # Work out what subset of the boards we are building
190 if not os.path.exists(options.output_dir):
191 os.makedirs(options.output_dir)
192 board_file = os.path.join(options.output_dir, 'boards.cfg')
193 our_path = os.path.dirname(os.path.realpath(__file__))
194 genboardscfg = os.path.join(our_path, '../genboardscfg.py')
195 if not os.path.exists(genboardscfg):
196 genboardscfg = os.path.join(options.git, 'tools/genboardscfg.py')
197 status = subprocess.call([genboardscfg, '-q', '-o', board_file])
199 # Older versions don't support -q
200 status = subprocess.call([genboardscfg, '-o', board_file])
202 sys.exit("Failed to generate boards.cfg")
204 boards = board.Boards()
205 boards.ReadBoards(board_file)
209 for arg in options.exclude:
210 exclude += arg.split(',')
213 requested_boards = []
214 for b in options.boards:
215 requested_boards += b.split(',')
217 requested_boards = None
218 why_selected, board_warnings = boards.SelectBoards(args, exclude,
220 selected = boards.GetSelected()
221 if not len(selected):
222 sys.exit(col.Color(col.RED, 'No matching boards found'))
224 if options.print_prefix:
225 err = ShowToolchainPrefix(boards, toolchains)
227 sys.exit(col.Color(col.RED, err))
230 # Work out how many commits to build. We want to build everything on the
231 # branch. We also build the upstream commit as a control so we can see
232 # problems introduced by the first commit on the branch.
233 count = options.count
234 has_range = options.branch and '..' in options.branch
236 if not options.branch:
240 count, msg = gitutil.CountCommitsInRange(options.git_dir,
243 count, msg = gitutil.CountCommitsInBranch(options.git_dir,
246 sys.exit(col.Color(col.RED, msg))
248 sys.exit(col.Color(col.RED, "Range '%s' has no commits" %
251 print(col.Color(col.YELLOW, msg))
252 count += 1 # Build upstream commit also
255 str = ("No commits found to process in branch '%s': "
256 "set branch's upstream or use -c flag" % options.branch)
257 sys.exit(col.Color(col.RED, str))
258 if options.work_in_output:
259 if len(selected) != 1:
260 sys.exit(col.Color(col.RED,
261 '-w can only be used with a single board'))
263 sys.exit(col.Color(col.RED,
264 '-w can only be used with a single commit'))
266 # Read the metadata from the commits. First look at the upstream commit,
267 # then the ones in the branch. We would like to do something like
268 # upstream/master~..branch but that isn't possible if upstream/master is
269 # a merge commit (it will list all the commits that form part of the
271 # Conflicting tags are not a problem for buildman, since it does not use
272 # them. For example, Series-version is not useful for buildman. On the
273 # other hand conflicting tags will cause an error. So allow later tags
274 # to overwrite earlier ones by setting allow_overwrite=True
278 range_expr = options.branch
280 range_expr = gitutil.GetRangeInBranch(options.git_dir,
282 upstream_commit = gitutil.GetUpstream(options.git_dir,
284 series = patchstream.get_metadata_for_list(upstream_commit,
285 options.git_dir, 1, series=None, allow_overwrite=True)
287 series = patchstream.get_metadata_for_list(range_expr,
288 options.git_dir, None, series, allow_overwrite=True)
291 series = patchstream.get_metadata_for_list(options.branch,
292 options.git_dir, count, series=None, allow_overwrite=True)
295 if not options.dry_run:
296 options.verbose = True
297 if not options.summary:
298 options.show_errors = True
300 # By default we have one thread per CPU. But if there are not enough jobs
301 # we can have fewer threads and use a high '-j' value for make.
302 if options.threads is None:
303 options.threads = min(multiprocessing.cpu_count(), len(selected))
305 options.jobs = max(1, (multiprocessing.cpu_count() +
306 len(selected) - 1) // len(selected))
309 options.step = len(series.commits) - 1
311 gnu_make = command.Output(os.path.join(options.git,
312 'scripts/show-gnu-make'), raise_on_error=False).rstrip()
314 sys.exit('GNU Make not found')
316 # Create a new builder with the selected options.
317 output_dir = options.output_dir
319 dirname = options.branch.replace('/', '_')
320 # As a special case allow the board directory to be placed in the
321 # output directory itself rather than any subdirectory.
322 if not options.no_subdirs:
323 output_dir = os.path.join(options.output_dir, dirname)
324 if clean_dir and os.path.exists(output_dir):
325 shutil.rmtree(output_dir)
326 builder = Builder(toolchains, output_dir, options.git_dir,
327 options.threads, options.jobs, gnu_make=gnu_make, checkout=True,
328 show_unknown=options.show_unknown, step=options.step,
329 no_subdirs=options.no_subdirs, full_path=options.full_path,
330 verbose_build=options.verbose_build,
331 mrproper=options.mrproper,
332 per_board_out_dir=options.per_board_out_dir,
333 config_only=options.config_only,
334 squash_config_y=not options.preserve_config_y,
335 warnings_as_errors=options.warnings_as_errors,
336 work_in_output=options.work_in_output,
337 test_thread_exceptions=test_thread_exceptions)
338 builder.force_config_on_failure = not options.quick
340 builder.do_make = make_func
342 # For a dry run, just show our actions as a sanity check
344 ShowActions(series, why_selected, selected, builder, options,
347 builder.force_build = options.force_build
348 builder.force_build_failures = options.force_build_failures
349 builder.force_reconfig = options.force_reconfig
350 builder.in_tree = options.in_tree
352 # Work out which boards to build
353 board_selected = boards.GetSelectedDict()
356 commits = series.commits
357 # Number the commits for test purposes
358 for commit in range(len(commits)):
359 commits[commit].sequence = commit
363 Print(GetActionSummary(options.summary, commits, board_selected,
366 # We can't show function sizes without board details at present
367 if options.show_bloat:
368 options.show_detail = True
369 builder.SetDisplayOptions(
370 options.show_errors, options.show_sizes, options.show_detail,
371 options.show_bloat, options.list_error_boards, options.show_config,
372 options.show_environment, options.filter_dtb_warnings,
373 options.filter_migration_warnings)
375 builder.ShowSummary(commits, board_selected)
377 fail, warned, excs = builder.BuildBoards(
378 commits, board_selected, options.keep_outputs, options.verbose)
383 elif warned and not options.ignore_warnings: