3e6933cc05e74149e7d82fea937aa1cd0ccd4fc7
[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, step, threads, jobs):
33     """Return a string summarising the intended action.
34
35     Args:
36         is_summary (bool): True if this is a summary (otherwise it is building)
37         commits (list): List of commits being built
38         selected (list of Board): List of Board objects that are marked
39         step (int): Step increment through commits
40         threads (int): Number of processor threads being used
41         jobs (int): Number of jobs to build at once
42
43     Returns:
44         Summary string.
45     """
46     if commits:
47         count = len(commits)
48         count = (count + step - 1) // step
49         commit_str = f'{count} commit{get_plural(count)}'
50     else:
51         commit_str = 'current source'
52     msg = (f"{'Summary of' if is_summary else 'Building'} "
53            f'{commit_str} for {len(selected)} boards')
54     msg += (f' ({threads} thread{get_plural(threads)}, '
55             f'{jobs} job{get_plural(jobs)} per thread)')
56     return msg
57
58 # pylint: disable=R0913
59 def show_actions(series, why_selected, boards_selected, output_dir,
60                  board_warnings, step, threads, jobs, verbose):
61     """Display a list of actions that we would take, if not a dry run.
62
63     Args:
64         series: Series object
65         why_selected: Dictionary where each key is a buildman argument
66                 provided by the user, and the value is the list of boards
67                 brought in by that argument. For example, 'arm' might bring
68                 in 400 boards, so in this case the key would be 'arm' and
69                 the value would be a list of board names.
70         boards_selected: Dict of selected boards, key is target name,
71                 value is Board object
72         output_dir (str): Output directory for builder
73         board_warnings: List of warnings obtained from board selected
74         step (int): Step increment through commits
75         threads (int): Number of processor threads being used
76         jobs (int): Number of jobs to build at once
77         verbose (bool): True to indicate why each board was selected
78     """
79     col = terminal.Color()
80     print('Dry run, so not doing much. But I would do this:')
81     print()
82     if series:
83         commits = series.commits
84     else:
85         commits = None
86     print(get_action_summary(False, commits, boards_selected, step, threads,
87                              jobs))
88     print(f'Build directory: {output_dir}')
89     if commits:
90         for upto in range(0, len(series.commits), step):
91             commit = series.commits[upto]
92             print('   ', col.build(col.YELLOW, commit.hash[:8], bright=False), end=' ')
93             print(commit.subject)
94     print()
95     for arg in why_selected:
96         if arg != 'all':
97             print(arg, f': {len(why_selected[arg])} boards')
98             if verbose:
99                 print(f"   {' '.join(why_selected[arg])}")
100     print('Total boards to build for each '
101           f"commit: {len(why_selected['all'])}\n")
102     if board_warnings:
103         for warning in board_warnings:
104             print(col.build(col.YELLOW, warning))
105
106 def show_toolchain_prefix(brds, toolchains):
107     """Show information about a the tool chain used by one or more boards
108
109     The function checks that all boards use the same toolchain, then prints
110     the correct value for CROSS_COMPILE.
111
112     Args:
113         boards: Boards object containing selected boards
114         toolchains: Toolchains object containing available toolchains
115
116     Return:
117         None on success, string error message otherwise
118     """
119     board_selected = brds.get_selected_dict()
120     tc_set = set()
121     for brd in board_selected.values():
122         tc_set.add(toolchains.Select(brd.arch))
123     if len(tc_set) != 1:
124         return 'Supplied boards must share one toolchain'
125     tchain = tc_set.pop()
126     print(tchain.GetEnvArgs(toolchain.VAR_CROSS_COMPILE))
127     return None
128
129 def get_allow_missing(opt_allow, opt_no_allow, num_selected, has_branch):
130     """Figure out whether to allow external blobs
131
132     Uses the allow-missing setting and the provided arguments to decide whether
133     missing external blobs should be allowed
134
135     Args:
136         opt_allow (bool): True if --allow-missing flag is set
137         opt_no_allow (bool): True if --no-allow-missing flag is set
138         num_selected (int): Number of selected board
139         has_branch (bool): True if a git branch (to build) has been provided
140
141     Returns:
142         bool: True to allow missing external blobs, False to produce an error if
143             external blobs are used
144     """
145     allow_missing = False
146     am_setting = bsettings.GetGlobalItemValue('allow-missing')
147     if am_setting:
148         if am_setting == 'always':
149             allow_missing = True
150         if 'multiple' in am_setting and num_selected > 1:
151             allow_missing = True
152         if 'branch' in am_setting and has_branch:
153             allow_missing = True
154
155     if opt_allow:
156         allow_missing = True
157     if opt_no_allow:
158         allow_missing = False
159     return allow_missing
160
161
162 def count_commits(branch, count, col, git_dir):
163     """Could the number of commits in the branch/ranch being built
164
165     Args:
166         branch (str): Name of branch to build, or None if none
167         count (int): Number of commits to build, or -1 for all
168         col (Terminal.Color): Color object to use
169         git_dir (str): Git directory to use, e.g. './.git'
170
171     Returns:
172         tuple:
173             Number of commits being built
174             True if the 'branch' string contains a range rather than a simple
175                 name
176     """
177     has_range = branch and '..' in branch
178     if count == -1:
179         if not branch:
180             count = 1
181         else:
182             if has_range:
183                 count, msg = gitutil.count_commits_in_range(git_dir, branch)
184             else:
185                 count, msg = gitutil.count_commits_in_branch(git_dir, branch)
186             if count is None:
187                 sys.exit(col.build(col.RED, msg))
188             elif count == 0:
189                 sys.exit(col.build(col.RED,
190                                    f"Range '{branch}' has no commits"))
191             if msg:
192                 print(col.build(col.YELLOW, msg))
193             count += 1   # Build upstream commit also
194
195     if not count:
196         msg = (f"No commits found to process in branch '{branch}': "
197                "set branch's upstream or use -c flag")
198         sys.exit(col.build(col.RED, msg))
199     return count, has_range
200
201
202 def determine_series(selected, col, git_dir, count, branch, work_in_output):
203     """Determine the series which is to be built, if any
204
205     Args:
206         selected (list of Board): List of Board objects that are marked
207             selected
208         col (Terminal.Color): Color object to use
209         git_dir (str): Git directory to use, e.g. './.git'
210         count (int): Number of commits in branch
211         branch (str): Name of branch to build, or None if none
212         work_in_output (bool): True to work in the output directory
213
214     Returns:
215         Series: Series to build, or None for none
216
217     Read the metadata from the commits. First look at the upstream commit,
218     then the ones in the branch. We would like to do something like
219     upstream/master~..branch but that isn't possible if upstream/master is
220     a merge commit (it will list all the commits that form part of the
221     merge)
222
223     Conflicting tags are not a problem for buildman, since it does not use
224     them. For example, Series-version is not useful for buildman. On the
225     other hand conflicting tags will cause an error. So allow later tags
226     to overwrite earlier ones by setting allow_overwrite=True
227     """
228
229     # Work out how many commits to build. We want to build everything on the
230     # branch. We also build the upstream commit as a control so we can see
231     # problems introduced by the first commit on the branch.
232     count, has_range = count_commits(branch, count, col, git_dir)
233     if work_in_output:
234         if len(selected) != 1:
235             sys.exit(col.build(col.RED,
236                                '-w can only be used with a single board'))
237         if count != 1:
238             sys.exit(col.build(col.RED,
239                                '-w can only be used with a single commit'))
240
241     if branch:
242         if count == -1:
243             if has_range:
244                 range_expr = branch
245             else:
246                 range_expr = gitutil.get_range_in_branch(git_dir, branch)
247             upstream_commit = gitutil.get_upstream(git_dir, branch)
248             series = patchstream.get_metadata_for_list(upstream_commit,
249                 git_dir, 1, series=None, allow_overwrite=True)
250
251             series = patchstream.get_metadata_for_list(range_expr,
252                     git_dir, None, series, allow_overwrite=True)
253         else:
254             # Honour the count
255             series = patchstream.get_metadata_for_list(branch,
256                     git_dir, count, series=None, allow_overwrite=True)
257     else:
258         series = None
259     return series
260
261
262 def do_fetch_arch(toolchains, col, fetch_arch):
263     """Handle the --fetch-arch option
264
265     Args:
266         toolchains (Toolchains): Tool chains to use
267         col (terminal.Color): Color object to build
268         fetch_arch (str): Argument passed to the --fetch-arch option
269
270     Returns:
271         int: Return code for buildman
272     """
273     if fetch_arch == 'list':
274         sorted_list = toolchains.ListArchs()
275         print(col.build(
276             col.BLUE,
277             f"Available architectures: {' '.join(sorted_list)}\n"))
278         return 0
279
280     if fetch_arch == 'all':
281         fetch_arch = ','.join(toolchains.ListArchs())
282         print(col.build(col.CYAN,
283                         f'\nDownloading toolchains: {fetch_arch}'))
284     for arch in fetch_arch.split(','):
285         print()
286         ret = toolchains.FetchAndInstall(arch)
287         if ret:
288             return ret
289     return 0
290
291
292 def get_toolchains(toolchains, col, override_toolchain, fetch_arch,
293                    list_tool_chains, verbose):
294     """Get toolchains object to use
295
296     Args:
297         toolchains (Toolchains or None): Toolchains to use. If None, then a
298             Toolchains object will be created and scanned
299         col (Terminal.Color): Color object
300         override_toolchain (str or None): Override value for toolchain, or None
301         fetch_arch (bool): True to fetch the toolchain for the architectures
302         list_tool_chains (bool): True to list all tool chains
303         verbose (bool): True for verbose output when listing toolchains
304
305     Returns:
306         Either:
307             int: Operation completed and buildman should exit with exit code
308             Toolchains: Toolchains object to use
309     """
310     no_toolchains = toolchains is None
311     if no_toolchains:
312         toolchains = toolchain.Toolchains(override_toolchain)
313
314     if fetch_arch:
315         return do_fetch_arch(toolchains, col, fetch_arch)
316
317     if no_toolchains:
318         toolchains.GetSettings()
319         toolchains.Scan(list_tool_chains and verbose)
320     if list_tool_chains:
321         toolchains.List()
322         print()
323         return 0
324     return toolchains
325
326
327 def get_boards_obj(output_dir, regen_board_list, maintainer_check, threads,
328                    verbose):
329     """Object the Boards object to use
330
331     Creates the output directory and ensures there is a boards.cfg file, then
332     read it in.
333
334     Args:
335         output_dir (str): Output directory to use
336         regen_board_list (bool): True to just regenerate the board list
337         maintainer_check (bool): True to just run a maintainer check
338         threads (int or None): Number of threads to use to create boards file
339         verbose (bool): False to suppress output from boards-file generation
340
341     Returns:
342         Either:
343             int: Operation completed and buildman should exit with exit code
344             Boards: Boards object to use
345     """
346     brds = boards.Boards()
347     nr_cpus = threads or multiprocessing.cpu_count()
348     if maintainer_check:
349         warnings = brds.build_board_list(jobs=nr_cpus)[1]
350         if warnings:
351             for warn in warnings:
352                 print(warn, file=sys.stderr)
353             return 2
354         return 0
355
356     if not os.path.exists(output_dir):
357         os.makedirs(output_dir)
358     board_file = os.path.join(output_dir, 'boards.cfg')
359     if regen_board_list and regen_board_list != '-':
360         board_file = regen_board_list
361
362     okay = brds.ensure_board_list(board_file, nr_cpus, force=regen_board_list,
363                                   quiet=not verbose)
364     if regen_board_list:
365         return 0 if okay else 2
366     brds.read_boards(board_file)
367     return brds
368
369
370 def determine_boards(brds, args, col, opt_boards, exclude_list):
371     """Determine which boards to build
372
373     Each element of args and exclude can refer to a board name, arch or SoC
374
375     Args:
376         brds (Boards): Boards object
377         args (list of str): Arguments describing boards to build
378         col (Terminal.Color): Color object
379         opt_boards (list of str): Specific boards to build, or None for all
380         exclude_list (list of str): Arguments describing boards to exclude
381
382     Returns:
383         tuple:
384             list of Board: List of Board objects that are marked selected
385             why_selected: Dictionary where each key is a buildman argument
386                     provided by the user, and the value is the list of boards
387                     brought in by that argument. For example, 'arm' might bring
388                     in 400 boards, so in this case the key would be 'arm' and
389                     the value would be a list of board names.
390             board_warnings: List of warnings obtained from board selected
391     """
392     exclude = []
393     if exclude_list:
394         for arg in exclude_list:
395             exclude += arg.split(',')
396
397     if opt_boards:
398         requested_boards = []
399         for brd in opt_boards:
400             requested_boards += brd.split(',')
401     else:
402         requested_boards = None
403     why_selected, board_warnings = brds.select_boards(args, exclude,
404                                                       requested_boards)
405     selected = brds.get_selected()
406     if not selected:
407         sys.exit(col.build(col.RED, 'No matching boards found'))
408     return selected, why_selected, board_warnings
409
410
411 def adjust_options(options, series, selected):
412     """Adjust options according to various constraints
413
414     Updates verbose, show_errors, threads, jobs and step
415
416     Args:
417         options (Options): Options object to adjust
418         series (Series): Series being built / summarised
419         selected (list of Board): List of Board objects that are marked
420     """
421     if not series and not options.dry_run:
422         options.verbose = True
423         if not options.summary:
424             options.show_errors = True
425
426     # By default we have one thread per CPU. But if there are not enough jobs
427     # we can have fewer threads and use a high '-j' value for make.
428     if options.threads is None:
429         options.threads = min(multiprocessing.cpu_count(), len(selected))
430     if not options.jobs:
431         options.jobs = max(1, (multiprocessing.cpu_count() +
432                 len(selected) - 1) // len(selected))
433
434     if not options.step:
435         options.step = len(series.commits) - 1
436
437 def do_buildman(options, args, toolchains=None, make_func=None, brds=None,
438                 clean_dir=False, test_thread_exceptions=False):
439     """The main control code for buildman
440
441     Args:
442         options: Command line options object
443         args: Command line arguments (list of strings)
444         toolchains: Toolchains to use - this should be a Toolchains()
445                 object. If None, then it will be created and scanned
446         make_func: Make function to use for the builder. This is called
447                 to execute 'make'. If this is None, the normal function
448                 will be used, which calls the 'make' tool with suitable
449                 arguments. This setting is useful for tests.
450         brds: Boards() object to use, containing a list of available
451                 boards. If this is None it will be created and scanned.
452         clean_dir: Used for tests only, indicates that the existing output_dir
453             should be removed before starting the build
454         test_thread_exceptions: Uses for tests only, True to make the threads
455             raise an exception instead of reporting their result. This simulates
456             a failure in the code somewhere
457     """
458     # Used so testing can obtain the builder: pylint: disable=W0603
459     global TEST_BUILDER
460
461     gitutil.setup()
462     col = terminal.Color()
463
464     git_dir = os.path.join(options.git, '.git')
465
466     toolchains = get_toolchains(toolchains, col, options.override_toolchain,
467                                 options.fetch_arch, options.list_tool_chains,
468                                 options.verbose)
469     output_dir = options.output_dir
470     if not output_dir:
471         if options.work_in_output:
472             sys.exit(col.build(col.RED, '-w requires that you specify -o'))
473         output_dir = '..'
474     if options.branch and not options.no_subdirs:
475         # As a special case allow the board directory to be placed in the
476         # output directory itself rather than any subdirectory.
477         dirname = options.branch.replace('/', '_')
478         output_dir = os.path.join(output_dir, dirname)
479         if clean_dir and os.path.exists(output_dir):
480             shutil.rmtree(output_dir)
481
482     # Work out what subset of the boards we are building
483     if not brds:
484         brds = get_boards_obj(output_dir, options.regen_board_list,
485                               options.maintainer_check, options.threads,
486                               options.verbose)
487         if isinstance(brds, int):
488             return brds
489
490     selected, why_selected, board_warnings = determine_boards(
491         brds, args, col, options.boards, options.exclude)
492
493     if options.print_prefix:
494         err = show_toolchain_prefix(brds, toolchains)
495         if err:
496             sys.exit(col.build(col.RED, err))
497         return 0
498
499     series = determine_series(selected, col, git_dir, options.count,
500                               options.branch, options.work_in_output)
501
502     adjust_options(options, series, selected)
503
504     # For a dry run, just show our actions as a sanity check
505     if options.dry_run:
506         show_actions(series, why_selected, selected, output_dir, board_warnings,
507                      options.step, options.threads, options.jobs,
508                      options.verbose)
509         return 0
510
511     gnu_make = command.output(os.path.join(options.git,
512             'scripts/show-gnu-make'), raise_on_error=False).rstrip()
513     if not gnu_make:
514         sys.exit('GNU Make not found')
515
516     allow_missing = get_allow_missing(options.allow_missing,
517                                       options.no_allow_missing, len(selected),
518                                       options.branch)
519
520     # Create a new builder with the selected options.
521
522     adjust_cfg = cfgutil.convert_list_to_dict(options.adjust_cfg)
523
524     # Drop LOCALVERSION_AUTO since it changes the version string on every commit
525     if options.reproducible_builds:
526         # If these are mentioned, leave the local version alone
527         if 'LOCALVERSION' in adjust_cfg or 'LOCALVERSION_AUTO' in adjust_cfg:
528             print('Not dropping LOCALVERSION_AUTO for reproducible build')
529         else:
530             adjust_cfg['LOCALVERSION_AUTO'] = '~'
531
532     builder = Builder(toolchains, output_dir, git_dir,
533             options.threads, options.jobs, gnu_make=gnu_make, checkout=True,
534             show_unknown=options.show_unknown, step=options.step,
535             no_subdirs=options.no_subdirs, full_path=options.full_path,
536             verbose_build=options.verbose_build,
537             mrproper=options.mrproper,
538             per_board_out_dir=options.per_board_out_dir,
539             config_only=options.config_only,
540             squash_config_y=not options.preserve_config_y,
541             warnings_as_errors=options.warnings_as_errors,
542             work_in_output=options.work_in_output,
543             test_thread_exceptions=test_thread_exceptions,
544             adjust_cfg=adjust_cfg,
545             allow_missing=allow_missing, no_lto=options.no_lto,
546             reproducible_builds=options.reproducible_builds)
547     TEST_BUILDER = builder
548     builder.force_config_on_failure = not options.quick
549     if make_func:
550         builder.do_make = make_func
551
552     builder.force_build = options.force_build
553     builder.force_build_failures = options.force_build_failures
554     builder.force_reconfig = options.force_reconfig
555     builder.in_tree = options.in_tree
556
557     # Work out which boards to build
558     board_selected = brds.get_selected_dict()
559
560     if series:
561         commits = series.commits
562         # Number the commits for test purposes
563         for i, commit in enumerate(commits):
564             commit.sequence = i
565     else:
566         commits = None
567
568     if not options.ide:
569         tprint(get_action_summary(options.summary, commits, board_selected,
570                                   options.step, options.threads, options.jobs))
571
572     # We can't show function sizes without board details at present
573     if options.show_bloat:
574         options.show_detail = True
575     builder.SetDisplayOptions(
576         options.show_errors, options.show_sizes, options.show_detail,
577         options.show_bloat, options.list_error_boards, options.show_config,
578         options.show_environment, options.filter_dtb_warnings,
579         options.filter_migration_warnings, options.ide)
580     if options.summary:
581         builder.ShowSummary(commits, board_selected)
582     else:
583         fail, warned, excs = builder.BuildBoards(
584             commits, board_selected, options.keep_outputs, options.verbose)
585         if excs:
586             return 102
587         if fail:
588             return 100
589         if warned and not options.ignore_warnings:
590             return 101
591     return 0