buildman: Add a flag for reproducible builds
[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 import multiprocessing
6 import os
7 import shutil
8 import subprocess
9 import sys
10
11 from buildman import boards
12 from buildman import bsettings
13 from buildman import cfgutil
14 from buildman import toolchain
15 from buildman.builder import Builder
16 from patman import command
17 from patman import gitutil
18 from patman import patchstream
19 from patman import terminal
20 from patman import tools
21 from patman.terminal import tprint
22
23 def GetPlural(count):
24     """Returns a plural 's' if count is not 1"""
25     return 's' if count != 1 else ''
26
27 def GetActionSummary(is_summary, commits, selected, options):
28     """Return a string summarising the intended action.
29
30     Returns:
31         Summary string.
32     """
33     if commits:
34         count = len(commits)
35         count = (count + options.step - 1) // options.step
36         commit_str = '%d commit%s' % (count, GetPlural(count))
37     else:
38         commit_str = 'current source'
39     str = '%s %s for %d boards' % (
40         'Summary of' if is_summary else 'Building', commit_str,
41         len(selected))
42     str += ' (%d thread%s, %d job%s per thread)' % (options.threads,
43             GetPlural(options.threads), options.jobs, GetPlural(options.jobs))
44     return str
45
46 def ShowActions(series, why_selected, boards_selected, builder, options,
47                 board_warnings):
48     """Display a list of actions that we would take, if not a dry run.
49
50     Args:
51         series: Series object
52         why_selected: Dictionary where each key is a buildman argument
53                 provided by the user, and the value is the list of boards
54                 brought in by that argument. For example, 'arm' might bring
55                 in 400 boards, so in this case the key would be 'arm' and
56                 the value would be a list of board names.
57         boards_selected: Dict of selected boards, key is target name,
58                 value is Board object
59         builder: The builder that will be used to build the commits
60         options: Command line options object
61         board_warnings: List of warnings obtained from board selected
62     """
63     col = terminal.Color()
64     print('Dry run, so not doing much. But I would do this:')
65     print()
66     if series:
67         commits = series.commits
68     else:
69         commits = None
70     print(GetActionSummary(False, commits, boards_selected,
71             options))
72     print('Build directory: %s' % builder.base_dir)
73     if commits:
74         for upto in range(0, len(series.commits), options.step):
75             commit = series.commits[upto]
76             print('   ', col.build(col.YELLOW, commit.hash[:8], bright=False), end=' ')
77             print(commit.subject)
78     print()
79     for arg in why_selected:
80         if arg != 'all':
81             print(arg, ': %d boards' % len(why_selected[arg]))
82             if options.verbose:
83                 print('   %s' % ' '.join(why_selected[arg]))
84     print(('Total boards to build for each commit: %d\n' %
85             len(why_selected['all'])))
86     if board_warnings:
87         for warning in board_warnings:
88             print(col.build(col.YELLOW, warning))
89
90 def ShowToolchainPrefix(brds, toolchains):
91     """Show information about a the tool chain used by one or more boards
92
93     The function checks that all boards use the same toolchain, then prints
94     the correct value for CROSS_COMPILE.
95
96     Args:
97         boards: Boards object containing selected boards
98         toolchains: Toolchains object containing available toolchains
99
100     Return:
101         None on success, string error message otherwise
102     """
103     board_selected = brds.get_selected_dict()
104     tc_set = set()
105     for brd in board_selected.values():
106         tc_set.add(toolchains.Select(brd.arch))
107     if len(tc_set) != 1:
108         return 'Supplied boards must share one toolchain'
109         return False
110     tc = tc_set.pop()
111     print(tc.GetEnvArgs(toolchain.VAR_CROSS_COMPILE))
112     return None
113
114 def get_allow_missing(opt_allow, opt_no_allow, num_selected, has_branch):
115     allow_missing = False
116     am_setting = bsettings.GetGlobalItemValue('allow-missing')
117     if am_setting:
118         if am_setting == 'always':
119             allow_missing = True
120         if 'multiple' in am_setting and num_selected > 1:
121             allow_missing = True
122         if 'branch' in am_setting and has_branch:
123             allow_missing = True
124
125     if opt_allow:
126         allow_missing = True
127     if opt_no_allow:
128         allow_missing = False
129     return allow_missing
130
131 def DoBuildman(options, args, toolchains=None, make_func=None, brds=None,
132                clean_dir=False, test_thread_exceptions=False):
133     """The main control code for buildman
134
135     Args:
136         options: Command line options object
137         args: Command line arguments (list of strings)
138         toolchains: Toolchains to use - this should be a Toolchains()
139                 object. If None, then it will be created and scanned
140         make_func: Make function to use for the builder. This is called
141                 to execute 'make'. If this is None, the normal function
142                 will be used, which calls the 'make' tool with suitable
143                 arguments. This setting is useful for tests.
144         brds: Boards() object to use, containing a list of available
145                 boards. If this is None it will be created and scanned.
146         clean_dir: Used for tests only, indicates that the existing output_dir
147             should be removed before starting the build
148         test_thread_exceptions: Uses for tests only, True to make the threads
149             raise an exception instead of reporting their result. This simulates
150             a failure in the code somewhere
151     """
152     global builder
153
154     if options.full_help:
155         tools.print_full_help(
156             os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])),
157                          'README.rst'))
158         return 0
159
160     gitutil.setup()
161     col = terminal.Color()
162
163     options.git_dir = os.path.join(options.git, '.git')
164
165     no_toolchains = toolchains is None
166     if no_toolchains:
167         toolchains = toolchain.Toolchains(options.override_toolchain)
168
169     if options.fetch_arch:
170         if options.fetch_arch == 'list':
171             sorted_list = toolchains.ListArchs()
172             print(col.build(col.BLUE, 'Available architectures: %s\n' %
173                             ' '.join(sorted_list)))
174             return 0
175         else:
176             fetch_arch = options.fetch_arch
177             if fetch_arch == 'all':
178                 fetch_arch = ','.join(toolchains.ListArchs())
179                 print(col.build(col.CYAN, '\nDownloading toolchains: %s' %
180                                 fetch_arch))
181             for arch in fetch_arch.split(','):
182                 print()
183                 ret = toolchains.FetchAndInstall(arch)
184                 if ret:
185                     return ret
186             return 0
187
188     if no_toolchains:
189         toolchains.GetSettings()
190         toolchains.Scan(options.list_tool_chains and options.verbose)
191     if options.list_tool_chains:
192         toolchains.List()
193         print()
194         return 0
195
196     if not options.output_dir:
197         if options.work_in_output:
198             sys.exit(col.build(col.RED, '-w requires that you specify -o'))
199         options.output_dir = '..'
200
201     # Work out what subset of the boards we are building
202     if not brds:
203         if not os.path.exists(options.output_dir):
204             os.makedirs(options.output_dir)
205         board_file = os.path.join(options.output_dir, 'boards.cfg')
206
207         brds = boards.Boards()
208         ok = brds.ensure_board_list(board_file,
209                                     options.threads or multiprocessing.cpu_count(),
210                                     force=options.regen_board_list,
211                                     quiet=not options.verbose)
212         if options.regen_board_list:
213             return 0 if ok else 2
214         brds.read_boards(board_file)
215
216     exclude = []
217     if options.exclude:
218         for arg in options.exclude:
219             exclude += arg.split(',')
220
221     if options.boards:
222         requested_boards = []
223         for b in options.boards:
224             requested_boards += b.split(',')
225     else:
226         requested_boards = None
227     why_selected, board_warnings = brds.select_boards(args, exclude,
228                                                       requested_boards)
229     selected = brds.get_selected()
230     if not len(selected):
231         sys.exit(col.build(col.RED, 'No matching boards found'))
232
233     if options.print_prefix:
234         err = ShowToolchainPrefix(brds, toolchains)
235         if err:
236             sys.exit(col.build(col.RED, err))
237         return 0
238
239     # Work out how many commits to build. We want to build everything on the
240     # branch. We also build the upstream commit as a control so we can see
241     # problems introduced by the first commit on the branch.
242     count = options.count
243     has_range = options.branch and '..' in options.branch
244     if count == -1:
245         if not options.branch:
246             count = 1
247         else:
248             if has_range:
249                 count, msg = gitutil.count_commits_in_range(options.git_dir,
250                                                          options.branch)
251             else:
252                 count, msg = gitutil.count_commits_in_branch(options.git_dir,
253                                                           options.branch)
254             if count is None:
255                 sys.exit(col.build(col.RED, msg))
256             elif count == 0:
257                 sys.exit(col.build(col.RED, "Range '%s' has no commits" %
258                                    options.branch))
259             if msg:
260                 print(col.build(col.YELLOW, msg))
261             count += 1   # Build upstream commit also
262
263     if not count:
264         str = ("No commits found to process in branch '%s': "
265                "set branch's upstream or use -c flag" % options.branch)
266         sys.exit(col.build(col.RED, str))
267     if options.work_in_output:
268         if len(selected) != 1:
269             sys.exit(col.build(col.RED,
270                                '-w can only be used with a single board'))
271         if count != 1:
272             sys.exit(col.build(col.RED,
273                                '-w can only be used with a single commit'))
274
275     # Read the metadata from the commits. First look at the upstream commit,
276     # then the ones in the branch. We would like to do something like
277     # upstream/master~..branch but that isn't possible if upstream/master is
278     # a merge commit (it will list all the commits that form part of the
279     # merge)
280     # Conflicting tags are not a problem for buildman, since it does not use
281     # them. For example, Series-version is not useful for buildman. On the
282     # other hand conflicting tags will cause an error. So allow later tags
283     # to overwrite earlier ones by setting allow_overwrite=True
284     if options.branch:
285         if count == -1:
286             if has_range:
287                 range_expr = options.branch
288             else:
289                 range_expr = gitutil.get_range_in_branch(options.git_dir,
290                                                       options.branch)
291             upstream_commit = gitutil.get_upstream(options.git_dir,
292                                                   options.branch)
293             series = patchstream.get_metadata_for_list(upstream_commit,
294                 options.git_dir, 1, series=None, allow_overwrite=True)
295
296             series = patchstream.get_metadata_for_list(range_expr,
297                     options.git_dir, None, series, allow_overwrite=True)
298         else:
299             # Honour the count
300             series = patchstream.get_metadata_for_list(options.branch,
301                     options.git_dir, count, series=None, allow_overwrite=True)
302     else:
303         series = None
304         if not options.dry_run:
305             options.verbose = True
306             if not options.summary:
307                 options.show_errors = True
308
309     # By default we have one thread per CPU. But if there are not enough jobs
310     # we can have fewer threads and use a high '-j' value for make.
311     if options.threads is None:
312         options.threads = min(multiprocessing.cpu_count(), len(selected))
313     if not options.jobs:
314         options.jobs = max(1, (multiprocessing.cpu_count() +
315                 len(selected) - 1) // len(selected))
316
317     if not options.step:
318         options.step = len(series.commits) - 1
319
320     gnu_make = command.output(os.path.join(options.git,
321             'scripts/show-gnu-make'), raise_on_error=False).rstrip()
322     if not gnu_make:
323         sys.exit('GNU Make not found')
324
325     allow_missing = get_allow_missing(options.allow_missing,
326                                       options.no_allow_missing, len(selected),
327                                       options.branch)
328
329     # Create a new builder with the selected options.
330     output_dir = options.output_dir
331     if options.branch:
332         dirname = options.branch.replace('/', '_')
333         # As a special case allow the board directory to be placed in the
334         # output directory itself rather than any subdirectory.
335         if not options.no_subdirs:
336             output_dir = os.path.join(options.output_dir, dirname)
337         if clean_dir and os.path.exists(output_dir):
338             shutil.rmtree(output_dir)
339     adjust_cfg = cfgutil.convert_list_to_dict(options.adjust_cfg)
340
341     # Drop LOCALVERSION_AUTO since it changes the version string on every commit
342     if options.reproducible_builds:
343         # If these are mentioned, leave the local version alone
344         if 'LOCALVERSION' in adjust_cfg or 'LOCALVERSION_AUTO' in adjust_cfg:
345             print('Not dropping LOCALVERSION_AUTO for reproducible build')
346         else:
347             adjust_cfg['LOCALVERSION_AUTO'] = '~'
348
349     builder = Builder(toolchains, output_dir, options.git_dir,
350             options.threads, options.jobs, gnu_make=gnu_make, checkout=True,
351             show_unknown=options.show_unknown, step=options.step,
352             no_subdirs=options.no_subdirs, full_path=options.full_path,
353             verbose_build=options.verbose_build,
354             mrproper=options.mrproper,
355             per_board_out_dir=options.per_board_out_dir,
356             config_only=options.config_only,
357             squash_config_y=not options.preserve_config_y,
358             warnings_as_errors=options.warnings_as_errors,
359             work_in_output=options.work_in_output,
360             test_thread_exceptions=test_thread_exceptions,
361             adjust_cfg=adjust_cfg,
362             allow_missing=allow_missing, no_lto=options.no_lto,
363             reproducible_builds=options.reproducible_builds)
364     builder.force_config_on_failure = not options.quick
365     if make_func:
366         builder.do_make = make_func
367
368     # For a dry run, just show our actions as a sanity check
369     if options.dry_run:
370         ShowActions(series, why_selected, selected, builder, options,
371                     board_warnings)
372     else:
373         builder.force_build = options.force_build
374         builder.force_build_failures = options.force_build_failures
375         builder.force_reconfig = options.force_reconfig
376         builder.in_tree = options.in_tree
377
378         # Work out which boards to build
379         board_selected = brds.get_selected_dict()
380
381         if series:
382             commits = series.commits
383             # Number the commits for test purposes
384             for commit in range(len(commits)):
385                 commits[commit].sequence = commit
386         else:
387             commits = None
388
389         if not options.ide:
390             tprint(GetActionSummary(options.summary, commits, board_selected,
391                                     options))
392
393         # We can't show function sizes without board details at present
394         if options.show_bloat:
395             options.show_detail = True
396         builder.SetDisplayOptions(
397             options.show_errors, options.show_sizes, options.show_detail,
398             options.show_bloat, options.list_error_boards, options.show_config,
399             options.show_environment, options.filter_dtb_warnings,
400             options.filter_migration_warnings, options.ide)
401         if options.summary:
402             builder.ShowSummary(commits, board_selected)
403         else:
404             fail, warned, excs = builder.BuildBoards(
405                 commits, board_selected, options.keep_outputs, options.verbose)
406             if excs:
407                 return 102
408             elif fail:
409                 return 100
410             elif warned and not options.ignore_warnings:
411                 return 101
412     return 0