buildman: Move fetch-arch code into a separate function
[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, bldr, 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         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
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: {bldr.base_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(count, has_range, branch, git_dir):
152     """Determine the series which is to be built, if any
153
154     Args:
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'
159
160     Returns:
161         Series: Series to build, or None for none
162
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
167     merge)
168
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
173     """
174     if branch:
175         if count == -1:
176             if has_range:
177                 range_expr = branch
178             else:
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)
183
184             series = patchstream.get_metadata_for_list(range_expr,
185                     git_dir, None, series, allow_overwrite=True)
186         else:
187             # Honour the count
188             series = patchstream.get_metadata_for_list(branch,
189                     git_dir, count, series=None, allow_overwrite=True)
190     else:
191         series = None
192     return series
193
194
195 def do_fetch_arch(toolchains, col, fetch_arch):
196     """Handle the --fetch-arch option
197
198     Args:
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
202
203     Returns:
204         int: Return code for buildman
205     """
206     if fetch_arch == 'list':
207         sorted_list = toolchains.ListArchs()
208         print(col.build(
209             col.BLUE,
210             f"Available architectures: {' '.join(sorted_list)}\n"))
211         return 0
212
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(','):
218         print()
219         ret = toolchains.FetchAndInstall(arch)
220         if ret:
221             return ret
222     return 0
223
224
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
228
229     Args:
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
245     """
246     # Used so testing can obtain the builder: pylint: disable=W0603
247     global TEST_BUILDER
248
249     gitutil.setup()
250     col = terminal.Color()
251
252     git_dir = os.path.join(options.git, '.git')
253
254     no_toolchains = toolchains is None
255     if no_toolchains:
256         toolchains = toolchain.Toolchains(options.override_toolchain)
257
258     if options.fetch_arch:
259         return do_fetch_arch(toolchains, col, options.fetch_arch)
260
261     if no_toolchains:
262         toolchains.GetSettings()
263         toolchains.Scan(options.list_tool_chains and options.verbose)
264     if options.list_tool_chains:
265         toolchains.List()
266         print()
267         return 0
268
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 = '..'
273
274     nr_cups = options.threads or multiprocessing.cpu_count()
275
276     # Work out what subset of the boards we are building
277     if not brds:
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
283
284         brds = boards.Boards()
285
286         if options.maintainer_check:
287             warnings = brds.build_board_list(jobs=nr_cups)[1]
288             if warnings:
289                 for warn in warnings:
290                     print(warn, file=sys.stderr)
291                 return 2
292             return 0
293
294         okay = brds.ensure_board_list(
295             board_file,
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)
302
303     exclude = []
304     if options.exclude:
305         for arg in options.exclude:
306             exclude += arg.split(',')
307
308     if options.boards:
309         requested_boards = []
310         for brd in options.boards:
311             requested_boards += brd.split(',')
312     else:
313         requested_boards = None
314     why_selected, board_warnings = brds.select_boards(args, exclude,
315                                                       requested_boards)
316     selected = brds.get_selected()
317     if not selected:
318         sys.exit(col.build(col.RED, 'No matching boards found'))
319
320     if options.print_prefix:
321         err = show_toolchain_prefix(brds, toolchains)
322         if err:
323             sys.exit(col.build(col.RED, err))
324         return 0
325
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
331     if count == -1:
332         if not options.branch:
333             count = 1
334         else:
335             if has_range:
336                 count, msg = gitutil.count_commits_in_range(git_dir,
337                                                             options.branch)
338             else:
339                 count, msg = gitutil.count_commits_in_branch(git_dir,
340                                                              options.branch)
341             if count is None:
342                 sys.exit(col.build(col.RED, msg))
343             elif count == 0:
344                 sys.exit(col.build(col.RED,
345                                    f"Range '{options.branch}' has no commits"))
346             if msg:
347                 print(col.build(col.YELLOW, msg))
348             count += 1   # Build upstream commit also
349
350     if not count:
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'))
358         if count != 1:
359             sys.exit(col.build(col.RED,
360                                '-w can only be used with a single commit'))
361
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
367
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))
372     if not options.jobs:
373         options.jobs = max(1, (multiprocessing.cpu_count() +
374                 len(selected) - 1) // len(selected))
375
376     if not options.step:
377         options.step = len(series.commits) - 1
378
379     gnu_make = command.output(os.path.join(options.git,
380             'scripts/show-gnu-make'), raise_on_error=False).rstrip()
381     if not gnu_make:
382         sys.exit('GNU Make not found')
383
384     allow_missing = get_allow_missing(options.allow_missing,
385                                       options.no_allow_missing, len(selected),
386                                       options.branch)
387
388     # Create a new builder with the selected options.
389     output_dir = options.output_dir
390     if options.branch:
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)
399
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')
405         else:
406             adjust_cfg['LOCALVERSION_AUTO'] = '~'
407
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
425     if make_func:
426         builder.do_make = make_func
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, builder, options,
431                     board_warnings)
432     else:
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
437
438         # Work out which boards to build
439         board_selected = brds.get_selected_dict()
440
441         if series:
442             commits = series.commits
443             # Number the commits for test purposes
444             for i, commit in enumerate(commits):
445                 commit.sequence = i
446         else:
447             commits = None
448
449         if not options.ide:
450             tprint(get_action_summary(options.summary, commits, board_selected,
451                                     options))
452
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)
461         if options.summary:
462             builder.ShowSummary(commits, board_selected)
463         else:
464             fail, warned, excs = builder.BuildBoards(
465                 commits, board_selected, options.keep_outputs, options.verbose)
466             if excs:
467                 return 102
468             if fail:
469                 return 100
470             if warned and not options.ignore_warnings:
471                 return 101
472     return 0