buildman: Move setting up the output dir into a 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, 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
438 def setup_output_dir(output_dir, work_in_output, branch, no_subdirs, col,
439                      clean_dir):
440     """Set up the output directory
441
442     Args:
443         output_dir (str): Output directory provided by the user, or None if none
444         work_in_output (bool): True to work in the output directory
445         branch (str): Name of branch to build, or None if none
446         no_subdirs (bool): True to put the output in the top-level output dir
447         clean_dir: Used for tests only, indicates that the existing output_dir
448             should be removed before starting the build
449
450     Returns:
451         str: Updated output directory pathname
452     """
453     if not output_dir:
454         if work_in_output:
455             sys.exit(col.build(col.RED, '-w requires that you specify -o'))
456         output_dir = '..'
457     if branch and not no_subdirs:
458         # As a special case allow the board directory to be placed in the
459         # output directory itself rather than any subdirectory.
460         dirname = branch.replace('/', '_')
461         output_dir = os.path.join(output_dir, dirname)
462         if clean_dir and os.path.exists(output_dir):
463             shutil.rmtree(output_dir)
464     return output_dir
465
466
467 def do_buildman(options, args, toolchains=None, make_func=None, brds=None,
468                 clean_dir=False, test_thread_exceptions=False):
469     """The main control code for buildman
470
471     Args:
472         options: Command line options object
473         args: Command line arguments (list of strings)
474         toolchains: Toolchains to use - this should be a Toolchains()
475                 object. If None, then it will be created and scanned
476         make_func: Make function to use for the builder. This is called
477                 to execute 'make'. If this is None, the normal function
478                 will be used, which calls the 'make' tool with suitable
479                 arguments. This setting is useful for tests.
480         brds: Boards() object to use, containing a list of available
481                 boards. If this is None it will be created and scanned.
482         clean_dir: Used for tests only, indicates that the existing output_dir
483             should be removed before starting the build
484         test_thread_exceptions: Uses for tests only, True to make the threads
485             raise an exception instead of reporting their result. This simulates
486             a failure in the code somewhere
487     """
488     # Used so testing can obtain the builder: pylint: disable=W0603
489     global TEST_BUILDER
490
491     gitutil.setup()
492     col = terminal.Color()
493
494     git_dir = os.path.join(options.git, '.git')
495
496     toolchains = get_toolchains(toolchains, col, options.override_toolchain,
497                                 options.fetch_arch, options.list_tool_chains,
498                                 options.verbose)
499     output_dir = setup_output_dir(
500         options.output_dir, options.work_in_output, options.branch,
501         options.no_subdirs, col, clean_dir)
502
503     # Work out what subset of the boards we are building
504     if not brds:
505         brds = get_boards_obj(output_dir, options.regen_board_list,
506                               options.maintainer_check, options.threads,
507                               options.verbose)
508         if isinstance(brds, int):
509             return brds
510
511     selected, why_selected, board_warnings = determine_boards(
512         brds, args, col, options.boards, options.exclude)
513
514     if options.print_prefix:
515         err = show_toolchain_prefix(brds, toolchains)
516         if err:
517             sys.exit(col.build(col.RED, err))
518         return 0
519
520     series = determine_series(selected, col, git_dir, options.count,
521                               options.branch, options.work_in_output)
522
523     adjust_options(options, series, selected)
524
525     # For a dry run, just show our actions as a sanity check
526     if options.dry_run:
527         show_actions(series, why_selected, selected, output_dir, board_warnings,
528                      options.step, options.threads, options.jobs,
529                      options.verbose)
530         return 0
531
532     gnu_make = command.output(os.path.join(options.git,
533             'scripts/show-gnu-make'), raise_on_error=False).rstrip()
534     if not gnu_make:
535         sys.exit('GNU Make not found')
536
537     allow_missing = get_allow_missing(options.allow_missing,
538                                       options.no_allow_missing, len(selected),
539                                       options.branch)
540
541     # Create a new builder with the selected options.
542
543     adjust_cfg = cfgutil.convert_list_to_dict(options.adjust_cfg)
544
545     # Drop LOCALVERSION_AUTO since it changes the version string on every commit
546     if options.reproducible_builds:
547         # If these are mentioned, leave the local version alone
548         if 'LOCALVERSION' in adjust_cfg or 'LOCALVERSION_AUTO' in adjust_cfg:
549             print('Not dropping LOCALVERSION_AUTO for reproducible build')
550         else:
551             adjust_cfg['LOCALVERSION_AUTO'] = '~'
552
553     builder = Builder(toolchains, output_dir, git_dir,
554             options.threads, options.jobs, gnu_make=gnu_make, checkout=True,
555             show_unknown=options.show_unknown, step=options.step,
556             no_subdirs=options.no_subdirs, full_path=options.full_path,
557             verbose_build=options.verbose_build,
558             mrproper=options.mrproper,
559             per_board_out_dir=options.per_board_out_dir,
560             config_only=options.config_only,
561             squash_config_y=not options.preserve_config_y,
562             warnings_as_errors=options.warnings_as_errors,
563             work_in_output=options.work_in_output,
564             test_thread_exceptions=test_thread_exceptions,
565             adjust_cfg=adjust_cfg,
566             allow_missing=allow_missing, no_lto=options.no_lto,
567             reproducible_builds=options.reproducible_builds)
568     TEST_BUILDER = builder
569     builder.force_config_on_failure = not options.quick
570     if make_func:
571         builder.do_make = make_func
572
573     builder.force_build = options.force_build
574     builder.force_build_failures = options.force_build_failures
575     builder.force_reconfig = options.force_reconfig
576     builder.in_tree = options.in_tree
577
578     # Work out which boards to build
579     board_selected = brds.get_selected_dict()
580
581     if series:
582         commits = series.commits
583         # Number the commits for test purposes
584         for i, commit in enumerate(commits):
585             commit.sequence = i
586     else:
587         commits = None
588
589     if not options.ide:
590         tprint(get_action_summary(options.summary, commits, board_selected,
591                                   options.step, options.threads, options.jobs))
592
593     # We can't show function sizes without board details at present
594     if options.show_bloat:
595         options.show_detail = True
596     builder.SetDisplayOptions(
597         options.show_errors, options.show_sizes, options.show_detail,
598         options.show_bloat, options.list_error_boards, options.show_config,
599         options.show_environment, options.filter_dtb_warnings,
600         options.filter_migration_warnings, options.ide)
601     if options.summary:
602         builder.ShowSummary(commits, board_selected)
603     else:
604         fail, warned, excs = builder.BuildBoards(
605             commits, board_selected, options.keep_outputs, options.verbose)
606         if excs:
607             return 102
608         if fail:
609             return 100
610         if warned and not options.ignore_warnings:
611             return 101
612     return 0