bulidman: Move more code to determine_series()
[platform/kernel/u-boot.git] / tools / buildman / control.py
1 # SPDX-License-Identifier: GPL-2.0+
2 # Copyright (c) 2013 The Chromium OS Authors.
3 #
4
5 """Control module for buildman
6
7 This holds the main control logic for buildman, when not running tests.
8 """
9
10 import multiprocessing
11 import os
12 import shutil
13 import sys
14
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
25
26 TEST_BUILDER = None
27
28 def get_plural(count):
29     """Returns a plural 's' if count is not 1"""
30     return 's' if count != 1 else ''
31
32 def get_action_summary(is_summary, commits, selected, options):
33     """Return a string summarising the intended action.
34
35     Returns:
36         Summary string.
37     """
38     if commits:
39         count = len(commits)
40         count = (count + options.step - 1) // options.step
41         commit_str = f'{count} commit{get_plural(count)}'
42     else:
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)')
48     return msg
49
50 # pylint: disable=R0913
51 def show_actions(series, why_selected, boards_selected, output_dir, options,
52                  board_warnings):
53     """Display a list of actions that we would take, if not a dry run.
54
55     Args:
56         series: Series object
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,
63                 value is Board object
64         output_dir (str): Output directory for builder
65         options: Command line options object
66         board_warnings: List of warnings obtained from board selected
67     """
68     col = terminal.Color()
69     print('Dry run, so not doing much. But I would do this:')
70     print()
71     if series:
72         commits = series.commits
73     else:
74         commits = None
75     print(get_action_summary(False, commits, boards_selected,
76             options))
77     print(f'Build directory: {output_dir}')
78     if commits:
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=' ')
82             print(commit.subject)
83     print()
84     for arg in why_selected:
85         if arg != 'all':
86             print(arg, f': {len(why_selected[arg])} boards')
87             if options.verbose:
88                 print(f"   {' '.join(why_selected[arg])}")
89     print('Total boards to build for each '
90           f"commit: {len(why_selected['all'])}\n")
91     if board_warnings:
92         for warning in board_warnings:
93             print(col.build(col.YELLOW, warning))
94
95 def show_toolchain_prefix(brds, toolchains):
96     """Show information about a the tool chain used by one or more boards
97
98     The function checks that all boards use the same toolchain, then prints
99     the correct value for CROSS_COMPILE.
100
101     Args:
102         boards: Boards object containing selected boards
103         toolchains: Toolchains object containing available toolchains
104
105     Return:
106         None on success, string error message otherwise
107     """
108     board_selected = brds.get_selected_dict()
109     tc_set = set()
110     for brd in board_selected.values():
111         tc_set.add(toolchains.Select(brd.arch))
112     if len(tc_set) != 1:
113         return 'Supplied boards must share one toolchain'
114     tchain = tc_set.pop()
115     print(tchain.GetEnvArgs(toolchain.VAR_CROSS_COMPILE))
116     return None
117
118 def get_allow_missing(opt_allow, opt_no_allow, num_selected, has_branch):
119     """Figure out whether to allow external blobs
120
121     Uses the allow-missing setting and the provided arguments to decide whether
122     missing external blobs should be allowed
123
124     Args:
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
129
130     Returns:
131         bool: True to allow missing external blobs, False to produce an error if
132             external blobs are used
133     """
134     allow_missing = False
135     am_setting = bsettings.GetGlobalItemValue('allow-missing')
136     if am_setting:
137         if am_setting == 'always':
138             allow_missing = True
139         if 'multiple' in am_setting and num_selected > 1:
140             allow_missing = True
141         if 'branch' in am_setting and has_branch:
142             allow_missing = True
143
144     if opt_allow:
145         allow_missing = True
146     if opt_no_allow:
147         allow_missing = False
148     return allow_missing
149
150
151 def determine_series(selected, col, git_dir, count, branch, work_in_output):
152     """Determine the series which is to be built, if any
153
154     Args:
155         selected (list of Board(: List of Board objects that are marked
156             selected
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
162
163     Returns:
164         Series: Series to build, or None for none
165
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
170     merge)
171
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
176     """
177
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
182     if count == -1:
183         if not branch:
184             count = 1
185         else:
186             if has_range:
187                 count, msg = gitutil.count_commits_in_range(git_dir, branch)
188             else:
189                 count, msg = gitutil.count_commits_in_branch(git_dir, branch)
190             if count is None:
191                 sys.exit(col.build(col.RED, msg))
192             elif count == 0:
193                 sys.exit(col.build(col.RED,
194                                    f"Range '{branch}' has no commits"))
195             if msg:
196                 print(col.build(col.YELLOW, msg))
197             count += 1   # Build upstream commit also
198
199     if not count:
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))
203     if work_in_output:
204         if len(selected) != 1:
205             sys.exit(col.build(col.RED,
206                                '-w can only be used with a single board'))
207         if count != 1:
208             sys.exit(col.build(col.RED,
209                                '-w can only be used with a single commit'))
210
211     if branch:
212         if count == -1:
213             if has_range:
214                 range_expr = branch
215             else:
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)
220
221             series = patchstream.get_metadata_for_list(range_expr,
222                     git_dir, None, series, allow_overwrite=True)
223         else:
224             # Honour the count
225             series = patchstream.get_metadata_for_list(branch,
226                     git_dir, count, series=None, allow_overwrite=True)
227     else:
228         series = None
229     return series
230
231
232 def do_fetch_arch(toolchains, col, fetch_arch):
233     """Handle the --fetch-arch option
234
235     Args:
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
239
240     Returns:
241         int: Return code for buildman
242     """
243     if fetch_arch == 'list':
244         sorted_list = toolchains.ListArchs()
245         print(col.build(
246             col.BLUE,
247             f"Available architectures: {' '.join(sorted_list)}\n"))
248         return 0
249
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(','):
255         print()
256         ret = toolchains.FetchAndInstall(arch)
257         if ret:
258             return ret
259     return 0
260
261
262 def determine_boards(brds, args, col, opt_boards, exclude_list):
263     """Determine which boards to build
264
265     Each element of args and exclude can refer to a board name, arch or SoC
266
267     Args:
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
273
274     Returns:
275         tuple:
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
283     """
284     exclude = []
285     if exclude_list:
286         for arg in exclude_list:
287             exclude += arg.split(',')
288
289     if opt_boards:
290         requested_boards = []
291         for brd in opt_boards:
292             requested_boards += brd.split(',')
293     else:
294         requested_boards = None
295     why_selected, board_warnings = brds.select_boards(args, exclude,
296                                                       requested_boards)
297     selected = brds.get_selected()
298     if not selected:
299         sys.exit(col.build(col.RED, 'No matching boards found'))
300     return selected, why_selected, board_warnings
301
302
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
306
307     Args:
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
323     """
324     # Used so testing can obtain the builder: pylint: disable=W0603
325     global TEST_BUILDER
326
327     gitutil.setup()
328     col = terminal.Color()
329
330     git_dir = os.path.join(options.git, '.git')
331
332     no_toolchains = toolchains is None
333     if no_toolchains:
334         toolchains = toolchain.Toolchains(options.override_toolchain)
335
336     if options.fetch_arch:
337         return do_fetch_arch(toolchains, col, options.fetch_arch)
338
339     if no_toolchains:
340         toolchains.GetSettings()
341         toolchains.Scan(options.list_tool_chains and options.verbose)
342     if options.list_tool_chains:
343         toolchains.List()
344         print()
345         return 0
346
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 = '..'
351
352     nr_cups = options.threads or multiprocessing.cpu_count()
353
354     # Work out what subset of the boards we are building
355     if not brds:
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
361
362         brds = boards.Boards()
363
364         if options.maintainer_check:
365             warnings = brds.build_board_list(jobs=nr_cups)[1]
366             if warnings:
367                 for warn in warnings:
368                     print(warn, file=sys.stderr)
369                 return 2
370             return 0
371
372         okay = brds.ensure_board_list(
373             board_file,
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)
380
381     selected, why_selected, board_warnings = determine_boards(
382         brds, args, col, options.boards, options.exclude)
383
384     if options.print_prefix:
385         err = show_toolchain_prefix(brds, toolchains)
386         if err:
387             sys.exit(col.build(col.RED, err))
388         return 0
389
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
396
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))
401     if not options.jobs:
402         options.jobs = max(1, (multiprocessing.cpu_count() +
403                 len(selected) - 1) // len(selected))
404
405     if not options.step:
406         options.step = len(series.commits) - 1
407
408     gnu_make = command.output(os.path.join(options.git,
409             'scripts/show-gnu-make'), raise_on_error=False).rstrip()
410     if not gnu_make:
411         sys.exit('GNU Make not found')
412
413     allow_missing = get_allow_missing(options.allow_missing,
414                                       options.no_allow_missing, len(selected),
415                                       options.branch)
416
417     # Create a new builder with the selected options.
418     output_dir = options.output_dir
419     if options.branch:
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)
427
428     # For a dry run, just show our actions as a sanity check
429     if options.dry_run:
430         show_actions(series, why_selected, selected, output_dir, options,
431                     board_warnings)
432         return 0
433
434     adjust_cfg = cfgutil.convert_list_to_dict(options.adjust_cfg)
435
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')
441         else:
442             adjust_cfg['LOCALVERSION_AUTO'] = '~'
443
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
461     if make_func:
462         builder.do_make = make_func
463
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
468
469     # Work out which boards to build
470     board_selected = brds.get_selected_dict()
471
472     if series:
473         commits = series.commits
474         # Number the commits for test purposes
475         for i, commit in enumerate(commits):
476             commit.sequence = i
477     else:
478         commits = None
479
480     if not options.ide:
481         tprint(get_action_summary(options.summary, commits, board_selected,
482                                 options))
483
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)
492     if options.summary:
493         builder.ShowSummary(commits, board_selected)
494     else:
495         fail, warned, excs = builder.BuildBoards(
496             commits, board_selected, options.keep_outputs, options.verbose)
497         if excs:
498             return 102
499         if fail:
500             return 100
501         if warned and not options.ignore_warnings:
502             return 101
503     return 0