blob: 2ccdee02a3cdc1f1c0e6b4a07fc77323ed13cf67 [file] [log] [blame]
Tom Rini83d290c2018-05-06 17:58:06 -04001# SPDX-License-Identifier: GPL-2.0+
Simon Glassfc3fe1c2013-04-03 11:07:16 +00002# Copyright (c) 2013 The Chromium OS Authors.
3#
4# Bloat-o-meter code used here Copyright 2004 Matt Mackall <mpm@selenic.com>
5#
Simon Glassfc3fe1c2013-04-03 11:07:16 +00006
7import collections
Simon Glassfc3fe1c2013-04-03 11:07:16 +00008from datetime import datetime, timedelta
9import glob
10import os
11import re
12import Queue
13import shutil
Simon Glass2f256642016-09-18 16:48:37 -060014import signal
Simon Glassfc3fe1c2013-04-03 11:07:16 +000015import string
16import sys
Simon Glassd436e382016-09-18 16:48:35 -060017import threading
Simon Glassfc3fe1c2013-04-03 11:07:16 +000018import time
19
Simon Glass190064b2014-08-09 15:33:00 -060020import builderthread
Simon Glassfc3fe1c2013-04-03 11:07:16 +000021import command
22import gitutil
23import terminal
Simon Glass4653a882014-09-05 19:00:07 -060024from terminal import Print
Simon Glassfc3fe1c2013-04-03 11:07:16 +000025import toolchain
26
27
28"""
29Theory of Operation
30
31Please see README for user documentation, and you should be familiar with
32that before trying to make sense of this.
33
34Buildman works by keeping the machine as busy as possible, building different
35commits for different boards on multiple CPUs at once.
36
37The source repo (self.git_dir) contains all the commits to be built. Each
38thread works on a single board at a time. It checks out the first commit,
39configures it for that board, then builds it. Then it checks out the next
40commit and builds it (typically without re-configuring). When it runs out
41of commits, it gets another job from the builder and starts again with that
42board.
43
44Clearly the builder threads could work either way - they could check out a
45commit and then built it for all boards. Using separate directories for each
46commit/board pair they could leave their build product around afterwards
47also.
48
49The intent behind building a single board for multiple commits, is to make
50use of incremental builds. Since each commit is built incrementally from
51the previous one, builds are faster. Reconfiguring for a different board
52removes all intermediate object files.
53
54Many threads can be working at once, but each has its own working directory.
55When a thread finishes a build, it puts the output files into a result
56directory.
57
58The base directory used by buildman is normally '../<branch>', i.e.
59a directory higher than the source repository and named after the branch
60being built.
61
62Within the base directory, we have one subdirectory for each commit. Within
63that is one subdirectory for each board. Within that is the build output for
64that commit/board combination.
65
66Buildman also create working directories for each thread, in a .bm-work/
67subdirectory in the base dir.
68
69As an example, say we are building branch 'us-net' for boards 'sandbox' and
70'seaboard', and say that us-net has two commits. We will have directories
71like this:
72
73us-net/ base directory
74 01_of_02_g4ed4ebc_net--Add-tftp-speed-/
75 sandbox/
76 u-boot.bin
77 seaboard/
78 u-boot.bin
79 02_of_02_g4ed4ebc_net--Check-tftp-comp/
80 sandbox/
81 u-boot.bin
82 seaboard/
83 u-boot.bin
84 .bm-work/
85 00/ working directory for thread 0 (contains source checkout)
86 build/ build output
87 01/ working directory for thread 1
88 build/ build output
89 ...
90u-boot/ source directory
91 .git/ repository
92"""
93
94# Possible build outcomes
95OUTCOME_OK, OUTCOME_WARNING, OUTCOME_ERROR, OUTCOME_UNKNOWN = range(4)
96
Simon Glass9a6d2e22017-04-12 18:23:26 -060097# Translate a commit subject into a valid filename (and handle unicode)
98trans_valid_chars = string.maketrans('/: ', '---')
99trans_valid_chars = trans_valid_chars.decode('latin-1')
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000100
Simon Glassb464f8e2016-11-13 14:25:53 -0700101BASE_CONFIG_FILENAMES = [
102 'u-boot.cfg', 'u-boot-spl.cfg', 'u-boot-tpl.cfg'
103]
104
105EXTRA_CONFIG_FILENAMES = [
Simon Glass843312d2015-02-05 22:06:15 -0700106 '.config', '.config-spl', '.config-tpl',
107 'autoconf.mk', 'autoconf-spl.mk', 'autoconf-tpl.mk',
108 'autoconf.h', 'autoconf-spl.h','autoconf-tpl.h',
Simon Glass843312d2015-02-05 22:06:15 -0700109]
110
Simon Glass8270e3c2015-08-25 21:52:14 -0600111class Config:
112 """Holds information about configuration settings for a board."""
Simon Glassb464f8e2016-11-13 14:25:53 -0700113 def __init__(self, config_filename, target):
Simon Glass8270e3c2015-08-25 21:52:14 -0600114 self.target = target
115 self.config = {}
Simon Glassb464f8e2016-11-13 14:25:53 -0700116 for fname in config_filename:
Simon Glass8270e3c2015-08-25 21:52:14 -0600117 self.config[fname] = {}
118
119 def Add(self, fname, key, value):
120 self.config[fname][key] = value
121
122 def __hash__(self):
123 val = 0
124 for fname in self.config:
125 for key, value in self.config[fname].iteritems():
126 print key, value
127 val = val ^ hash(key) & hash(value)
128 return val
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000129
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000130class Builder:
131 """Class for building U-Boot for a particular commit.
132
133 Public members: (many should ->private)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000134 already_done: Number of builds already completed
135 base_dir: Base directory to use for builder
136 checkout: True to check out source, False to skip that step.
137 This is used for testing.
138 col: terminal.Color() object
139 count: Number of commits to build
140 do_make: Method to call to invoke Make
141 fail: Number of builds that failed due to error
142 force_build: Force building even if a build already exists
143 force_config_on_failure: If a commit fails for a board, disable
144 incremental building for the next commit we build for that
145 board, so that we will see all warnings/errors again.
Simon Glass4266dc22014-07-13 12:22:31 -0600146 force_build_failures: If a previously-built build (i.e. built on
147 a previous run of buildman) is marked as failed, rebuild it.
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000148 git_dir: Git directory containing source repository
149 last_line_len: Length of the last line we printed (used for erasing
150 it with new progress information)
151 num_jobs: Number of jobs to run at once (passed to make as -j)
152 num_threads: Number of builder threads to run
153 out_queue: Queue of results to process
154 re_make_err: Compiled regular expression for ignore_lines
155 queue: Queue of jobs to run
156 threads: List of active threads
157 toolchains: Toolchains object to use for building
158 upto: Current commit number we are building (0.count-1)
159 warned: Number of builds that produced at least one warning
Simon Glass97e91522014-07-14 17:51:02 -0600160 force_reconfig: Reconfigure U-Boot on each comiit. This disables
161 incremental building, where buildman reconfigures on the first
162 commit for a baord, and then just does an incremental build for
163 the following commits. In fact buildman will reconfigure and
164 retry for any failing commits, so generally the only effect of
165 this option is to slow things down.
Simon Glass189a4962014-07-14 17:51:03 -0600166 in_tree: Build U-Boot in-tree instead of specifying an output
167 directory separate from the source code. This option is really
168 only useful for testing in-tree builds.
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000169
170 Private members:
171 _base_board_dict: Last-summarised Dict of boards
172 _base_err_lines: Last-summarised list of errors
Simon Glasse30965d2014-08-28 09:43:44 -0600173 _base_warn_lines: Last-summarised list of warnings
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000174 _build_period_us: Time taken for a single build (float object).
175 _complete_delay: Expected delay until completion (timedelta)
176 _next_delay_update: Next time we plan to display a progress update
177 (datatime)
178 _show_unknown: Show unknown boards (those not built) in summary
179 _timestamps: List of timestamps for the completion of the last
180 last _timestamp_count builds. Each is a datetime object.
181 _timestamp_count: Number of timestamps to keep in our list.
182 _working_dir: Base working directory containing all threads
183 """
184 class Outcome:
185 """Records a build outcome for a single make invocation
186
187 Public Members:
188 rc: Outcome value (OUTCOME_...)
189 err_lines: List of error lines or [] if none
190 sizes: Dictionary of image size information, keyed by filename
191 - Each value is itself a dictionary containing
192 values for 'text', 'data' and 'bss', being the integer
193 size in bytes of each section.
194 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
195 value is itself a dictionary:
196 key: function name
197 value: Size of function in bytes
Simon Glass843312d2015-02-05 22:06:15 -0700198 config: Dictionary keyed by filename - e.g. '.config'. Each
199 value is itself a dictionary:
200 key: config name
201 value: config value
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000202 """
Simon Glass843312d2015-02-05 22:06:15 -0700203 def __init__(self, rc, err_lines, sizes, func_sizes, config):
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000204 self.rc = rc
205 self.err_lines = err_lines
206 self.sizes = sizes
207 self.func_sizes = func_sizes
Simon Glass843312d2015-02-05 22:06:15 -0700208 self.config = config
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000209
210 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
Simon Glass5971ab52014-12-01 17:33:55 -0700211 gnu_make='make', checkout=True, show_unknown=True, step=1,
Stephen Warrenf79f1e02016-04-11 10:48:44 -0600212 no_subdirs=False, full_path=False, verbose_build=False,
Simon Glassb50113f2016-11-13 14:25:51 -0700213 incremental=False, per_board_out_dir=False,
Daniel Schwierzeck2371d1b2018-01-26 16:31:05 +0100214 config_only=False, squash_config_y=False,
215 warnings_as_errors=False):
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000216 """Create a new Builder object
217
218 Args:
219 toolchains: Toolchains object to use for building
220 base_dir: Base directory to use for builder
221 git_dir: Git directory containing source repository
222 num_threads: Number of builder threads to run
223 num_jobs: Number of jobs to run at once (passed to make as -j)
Masahiro Yamada99796922014-07-22 11:19:09 +0900224 gnu_make: the command name of GNU Make.
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000225 checkout: True to check out source, False to skip that step.
226 This is used for testing.
227 show_unknown: Show unknown boards (those not built) in summary
228 step: 1 to process every commit, n to process every nth commit
Simon Glassbb1501f2014-12-01 17:34:00 -0700229 no_subdirs: Don't create subdirectories when building current
230 source for a single board
231 full_path: Return the full path in CROSS_COMPILE and don't set
232 PATH
Simon Glassd2ce6582014-12-01 17:34:07 -0700233 verbose_build: Run build with V=1 and don't use 'make -s'
Stephen Warrenf79f1e02016-04-11 10:48:44 -0600234 incremental: Always perform incremental builds; don't run make
235 mrproper when configuring
236 per_board_out_dir: Build in a separate persistent directory per
237 board rather than a thread-specific directory
Simon Glassb50113f2016-11-13 14:25:51 -0700238 config_only: Only configure each build, don't build it
Simon Glassb464f8e2016-11-13 14:25:53 -0700239 squash_config_y: Convert CONFIG options with the value 'y' to '1'
Daniel Schwierzeck2371d1b2018-01-26 16:31:05 +0100240 warnings_as_errors: Treat all compiler warnings as errors
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000241 """
242 self.toolchains = toolchains
243 self.base_dir = base_dir
244 self._working_dir = os.path.join(base_dir, '.bm-work')
245 self.threads = []
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000246 self.do_make = self.Make
Masahiro Yamada99796922014-07-22 11:19:09 +0900247 self.gnu_make = gnu_make
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000248 self.checkout = checkout
249 self.num_threads = num_threads
250 self.num_jobs = num_jobs
251 self.already_done = 0
252 self.force_build = False
253 self.git_dir = git_dir
254 self._show_unknown = show_unknown
255 self._timestamp_count = 10
256 self._build_period_us = None
257 self._complete_delay = None
258 self._next_delay_update = datetime.now()
259 self.force_config_on_failure = True
Simon Glass4266dc22014-07-13 12:22:31 -0600260 self.force_build_failures = False
Simon Glass97e91522014-07-14 17:51:02 -0600261 self.force_reconfig = False
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000262 self._step = step
Simon Glass189a4962014-07-14 17:51:03 -0600263 self.in_tree = False
Simon Glass28370c12014-08-09 15:33:06 -0600264 self._error_lines = 0
Simon Glass5971ab52014-12-01 17:33:55 -0700265 self.no_subdirs = no_subdirs
Simon Glassbb1501f2014-12-01 17:34:00 -0700266 self.full_path = full_path
Simon Glassd2ce6582014-12-01 17:34:07 -0700267 self.verbose_build = verbose_build
Simon Glassb50113f2016-11-13 14:25:51 -0700268 self.config_only = config_only
Simon Glassb464f8e2016-11-13 14:25:53 -0700269 self.squash_config_y = squash_config_y
270 self.config_filenames = BASE_CONFIG_FILENAMES
271 if not self.squash_config_y:
272 self.config_filenames += EXTRA_CONFIG_FILENAMES
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000273
Daniel Schwierzeck2371d1b2018-01-26 16:31:05 +0100274 self.warnings_as_errors = warnings_as_errors
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000275 self.col = terminal.Color()
276
Simon Glasse30965d2014-08-28 09:43:44 -0600277 self._re_function = re.compile('(.*): In function.*')
278 self._re_files = re.compile('In file included from.*')
279 self._re_warning = re.compile('(.*):(\d*):(\d*): warning: .*')
280 self._re_note = re.compile('(.*):(\d*):(\d*): note: this is the location of the previous.*')
281
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000282 self.queue = Queue.Queue()
283 self.out_queue = Queue.Queue()
284 for i in range(self.num_threads):
Stephen Warrenf79f1e02016-04-11 10:48:44 -0600285 t = builderthread.BuilderThread(self, i, incremental,
286 per_board_out_dir)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000287 t.setDaemon(True)
288 t.start()
289 self.threads.append(t)
290
291 self.last_line_len = 0
Simon Glass190064b2014-08-09 15:33:00 -0600292 t = builderthread.ResultThread(self)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000293 t.setDaemon(True)
294 t.start()
295 self.threads.append(t)
296
297 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
298 self.re_make_err = re.compile('|'.join(ignore_lines))
299
Simon Glass2f256642016-09-18 16:48:37 -0600300 # Handle existing graceful with SIGINT / Ctrl-C
301 signal.signal(signal.SIGINT, self.signal_handler)
302
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000303 def __del__(self):
304 """Get rid of all threads created by the builder"""
305 for t in self.threads:
306 del t
307
Simon Glass2f256642016-09-18 16:48:37 -0600308 def signal_handler(self, signal, frame):
309 sys.exit(1)
310
Simon Glassb2ea7ab2014-08-09 15:33:02 -0600311 def SetDisplayOptions(self, show_errors=False, show_sizes=False,
Simon Glassed966652014-08-28 09:43:43 -0600312 show_detail=False, show_bloat=False,
Simon Glass843312d2015-02-05 22:06:15 -0700313 list_error_boards=False, show_config=False):
Simon Glassb2ea7ab2014-08-09 15:33:02 -0600314 """Setup display options for the builder.
315
316 show_errors: True to show summarised error/warning info
317 show_sizes: Show size deltas
318 show_detail: Show detail for each board
319 show_bloat: Show detail for each function
Simon Glassed966652014-08-28 09:43:43 -0600320 list_error_boards: Show the boards which caused each error/warning
Simon Glass843312d2015-02-05 22:06:15 -0700321 show_config: Show config deltas
Simon Glassb2ea7ab2014-08-09 15:33:02 -0600322 """
323 self._show_errors = show_errors
324 self._show_sizes = show_sizes
325 self._show_detail = show_detail
326 self._show_bloat = show_bloat
Simon Glassed966652014-08-28 09:43:43 -0600327 self._list_error_boards = list_error_boards
Simon Glass843312d2015-02-05 22:06:15 -0700328 self._show_config = show_config
Simon Glassb2ea7ab2014-08-09 15:33:02 -0600329
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000330 def _AddTimestamp(self):
331 """Add a new timestamp to the list and record the build period.
332
333 The build period is the length of time taken to perform a single
334 build (one board, one commit).
335 """
336 now = datetime.now()
337 self._timestamps.append(now)
338 count = len(self._timestamps)
339 delta = self._timestamps[-1] - self._timestamps[0]
340 seconds = delta.total_seconds()
341
342 # If we have enough data, estimate build period (time taken for a
343 # single build) and therefore completion time.
344 if count > 1 and self._next_delay_update < now:
345 self._next_delay_update = now + timedelta(seconds=2)
346 if seconds > 0:
347 self._build_period = float(seconds) / count
348 todo = self.count - self.upto
349 self._complete_delay = timedelta(microseconds=
350 self._build_period * todo * 1000000)
351 # Round it
352 self._complete_delay -= timedelta(
353 microseconds=self._complete_delay.microseconds)
354
355 if seconds > 60:
356 self._timestamps.popleft()
357 count -= 1
358
359 def ClearLine(self, length):
360 """Clear any characters on the current line
361
362 Make way for a new line of length 'length', by outputting enough
363 spaces to clear out the old line. Then remember the new length for
364 next time.
365
366 Args:
367 length: Length of new line, in characters
368 """
369 if length < self.last_line_len:
Simon Glass4653a882014-09-05 19:00:07 -0600370 Print(' ' * (self.last_line_len - length), newline=False)
371 Print('\r', newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000372 self.last_line_len = length
373 sys.stdout.flush()
374
375 def SelectCommit(self, commit, checkout=True):
376 """Checkout the selected commit for this build
377 """
378 self.commit = commit
379 if checkout and self.checkout:
380 gitutil.Checkout(commit.hash)
381
382 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
383 """Run make
384
385 Args:
386 commit: Commit object that is being built
387 brd: Board object that is being built
Roger Meierfd18a892014-08-20 22:10:29 +0200388 stage: Stage that we are at (mrproper, config, build)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000389 cwd: Directory where make should be run
390 args: Arguments to pass to make
391 kwargs: Arguments to pass to command.RunPipe()
392 """
Masahiro Yamada99796922014-07-22 11:19:09 +0900393 cmd = [self.gnu_make] + list(args)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000394 result = command.RunPipe([cmd], capture=True, capture_stderr=True,
395 cwd=cwd, raise_on_error=False, **kwargs)
Simon Glass40f11fc2015-02-05 22:06:12 -0700396 if self.verbose_build:
397 result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout
398 result.combined = '%s\n' % (' '.join(cmd)) + result.combined
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000399 return result
400
401 def ProcessResult(self, result):
402 """Process the result of a build, showing progress information
403
404 Args:
Simon Glasse5a0e5d2014-08-09 15:33:03 -0600405 result: A CommandResult object, which indicates the result for
406 a single build
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000407 """
408 col = terminal.Color()
409 if result:
410 target = result.brd.target
411
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000412 self.upto += 1
413 if result.return_code != 0:
414 self.fail += 1
415 elif result.stderr:
416 self.warned += 1
417 if result.already_done:
418 self.already_done += 1
Simon Glasse5a0e5d2014-08-09 15:33:03 -0600419 if self._verbose:
Simon Glass4653a882014-09-05 19:00:07 -0600420 Print('\r', newline=False)
Simon Glasse5a0e5d2014-08-09 15:33:03 -0600421 self.ClearLine(0)
422 boards_selected = {target : result.brd}
423 self.ResetResultSummary(boards_selected)
424 self.ProduceResultSummary(result.commit_upto, self.commits,
425 boards_selected)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000426 else:
427 target = '(starting)'
428
429 # Display separate counts for ok, warned and fail
430 ok = self.upto - self.warned - self.fail
431 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
432 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
433 line += self.col.Color(self.col.RED, '%5d' % self.fail)
434
435 name = ' /%-5d ' % self.count
436
437 # Add our current completion time estimate
438 self._AddTimestamp()
439 if self._complete_delay:
440 name += '%s : ' % self._complete_delay
441 # When building all boards for a commit, we can print a commit
442 # progress message.
443 if result and result.commit_upto is None:
444 name += 'commit %2d/%-3d' % (self.commit_upto + 1,
445 self.commit_count)
446
447 name += target
Simon Glass4653a882014-09-05 19:00:07 -0600448 Print(line + name, newline=False)
Simon Glass960421e2016-11-15 15:32:59 -0700449 length = 16 + len(name)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000450 self.ClearLine(length)
451
452 def _GetOutputDir(self, commit_upto):
453 """Get the name of the output directory for a commit number
454
455 The output directory is typically .../<branch>/<commit>.
456
457 Args:
458 commit_upto: Commit number to use (0..self.count-1)
459 """
Simon Glass5971ab52014-12-01 17:33:55 -0700460 commit_dir = None
Simon Glassfea58582014-08-09 15:32:59 -0600461 if self.commits:
462 commit = self.commits[commit_upto]
463 subject = commit.subject.translate(trans_valid_chars)
464 commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
465 self.commit_count, commit.hash, subject[:20]))
Simon Glass5971ab52014-12-01 17:33:55 -0700466 elif not self.no_subdirs:
Simon Glassfea58582014-08-09 15:32:59 -0600467 commit_dir = 'current'
Simon Glass5971ab52014-12-01 17:33:55 -0700468 if not commit_dir:
469 return self.base_dir
470 return os.path.join(self.base_dir, commit_dir)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000471
472 def GetBuildDir(self, commit_upto, target):
473 """Get the name of the build directory for a commit number
474
475 The build directory is typically .../<branch>/<commit>/<target>.
476
477 Args:
478 commit_upto: Commit number to use (0..self.count-1)
479 target: Target name
480 """
481 output_dir = self._GetOutputDir(commit_upto)
482 return os.path.join(output_dir, target)
483
484 def GetDoneFile(self, commit_upto, target):
485 """Get the name of the done file for a commit number
486
487 Args:
488 commit_upto: Commit number to use (0..self.count-1)
489 target: Target name
490 """
491 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
492
493 def GetSizesFile(self, commit_upto, target):
494 """Get the name of the sizes file for a commit number
495
496 Args:
497 commit_upto: Commit number to use (0..self.count-1)
498 target: Target name
499 """
500 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
501
502 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
503 """Get the name of the funcsizes file for a commit number and ELF file
504
505 Args:
506 commit_upto: Commit number to use (0..self.count-1)
507 target: Target name
508 elf_fname: Filename of elf image
509 """
510 return os.path.join(self.GetBuildDir(commit_upto, target),
511 '%s.sizes' % elf_fname.replace('/', '-'))
512
513 def GetObjdumpFile(self, commit_upto, target, elf_fname):
514 """Get the name of the objdump file for a commit number and ELF file
515
516 Args:
517 commit_upto: Commit number to use (0..self.count-1)
518 target: Target name
519 elf_fname: Filename of elf image
520 """
521 return os.path.join(self.GetBuildDir(commit_upto, target),
522 '%s.objdump' % elf_fname.replace('/', '-'))
523
524 def GetErrFile(self, commit_upto, target):
525 """Get the name of the err file for a commit number
526
527 Args:
528 commit_upto: Commit number to use (0..self.count-1)
529 target: Target name
530 """
531 output_dir = self.GetBuildDir(commit_upto, target)
532 return os.path.join(output_dir, 'err')
533
534 def FilterErrors(self, lines):
535 """Filter out errors in which we have no interest
536
537 We should probably use map().
538
539 Args:
540 lines: List of error lines, each a string
541 Returns:
542 New list with only interesting lines included
543 """
544 out_lines = []
545 for line in lines:
546 if not self.re_make_err.search(line):
547 out_lines.append(line)
548 return out_lines
549
550 def ReadFuncSizes(self, fname, fd):
551 """Read function sizes from the output of 'nm'
552
553 Args:
554 fd: File containing data to read
555 fname: Filename we are reading from (just for errors)
556
557 Returns:
558 Dictionary containing size of each function in bytes, indexed by
559 function name.
560 """
561 sym = {}
562 for line in fd.readlines():
563 try:
564 size, type, name = line[:-1].split()
565 except:
Simon Glass4653a882014-09-05 19:00:07 -0600566 Print("Invalid line in file '%s': '%s'" % (fname, line[:-1]))
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000567 continue
568 if type in 'tTdDbB':
569 # function names begin with '.' on 64-bit powerpc
570 if '.' in name[1:]:
571 name = 'static.' + name.split('.')[0]
572 sym[name] = sym.get(name, 0) + int(size, 16)
573 return sym
574
Simon Glass843312d2015-02-05 22:06:15 -0700575 def _ProcessConfig(self, fname):
576 """Read in a .config, autoconf.mk or autoconf.h file
577
578 This function handles all config file types. It ignores comments and
579 any #defines which don't start with CONFIG_.
580
581 Args:
582 fname: Filename to read
583
584 Returns:
585 Dictionary:
586 key: Config name (e.g. CONFIG_DM)
587 value: Config value (e.g. 1)
588 """
589 config = {}
590 if os.path.exists(fname):
591 with open(fname) as fd:
592 for line in fd:
593 line = line.strip()
594 if line.startswith('#define'):
595 values = line[8:].split(' ', 1)
596 if len(values) > 1:
597 key, value = values
598 else:
599 key = values[0]
Simon Glassb464f8e2016-11-13 14:25:53 -0700600 value = '1' if self.squash_config_y else ''
Simon Glass843312d2015-02-05 22:06:15 -0700601 if not key.startswith('CONFIG_'):
602 continue
603 elif not line or line[0] in ['#', '*', '/']:
604 continue
605 else:
606 key, value = line.split('=', 1)
Simon Glassb464f8e2016-11-13 14:25:53 -0700607 if self.squash_config_y and value == 'y':
608 value = '1'
Simon Glass843312d2015-02-05 22:06:15 -0700609 config[key] = value
610 return config
611
612 def GetBuildOutcome(self, commit_upto, target, read_func_sizes,
613 read_config):
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000614 """Work out the outcome of a build.
615
616 Args:
617 commit_upto: Commit number to check (0..n-1)
618 target: Target board to check
619 read_func_sizes: True to read function size information
Simon Glass843312d2015-02-05 22:06:15 -0700620 read_config: True to read .config and autoconf.h files
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000621
622 Returns:
623 Outcome object
624 """
625 done_file = self.GetDoneFile(commit_upto, target)
626 sizes_file = self.GetSizesFile(commit_upto, target)
627 sizes = {}
628 func_sizes = {}
Simon Glass843312d2015-02-05 22:06:15 -0700629 config = {}
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000630 if os.path.exists(done_file):
631 with open(done_file, 'r') as fd:
632 return_code = int(fd.readline())
633 err_lines = []
634 err_file = self.GetErrFile(commit_upto, target)
635 if os.path.exists(err_file):
636 with open(err_file, 'r') as fd:
637 err_lines = self.FilterErrors(fd.readlines())
638
639 # Decide whether the build was ok, failed or created warnings
640 if return_code:
641 rc = OUTCOME_ERROR
642 elif len(err_lines):
643 rc = OUTCOME_WARNING
644 else:
645 rc = OUTCOME_OK
646
647 # Convert size information to our simple format
648 if os.path.exists(sizes_file):
649 with open(sizes_file, 'r') as fd:
650 for line in fd.readlines():
651 values = line.split()
652 rodata = 0
653 if len(values) > 6:
654 rodata = int(values[6], 16)
655 size_dict = {
656 'all' : int(values[0]) + int(values[1]) +
657 int(values[2]),
658 'text' : int(values[0]) - rodata,
659 'data' : int(values[1]),
660 'bss' : int(values[2]),
661 'rodata' : rodata,
662 }
663 sizes[values[5]] = size_dict
664
665 if read_func_sizes:
666 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
667 for fname in glob.glob(pattern):
668 with open(fname, 'r') as fd:
669 dict_name = os.path.basename(fname).replace('.sizes',
670 '')
671 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
672
Simon Glass843312d2015-02-05 22:06:15 -0700673 if read_config:
674 output_dir = self.GetBuildDir(commit_upto, target)
Simon Glassb464f8e2016-11-13 14:25:53 -0700675 for name in self.config_filenames:
Simon Glass843312d2015-02-05 22:06:15 -0700676 fname = os.path.join(output_dir, name)
677 config[name] = self._ProcessConfig(fname)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000678
Simon Glass843312d2015-02-05 22:06:15 -0700679 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000680
Simon Glass843312d2015-02-05 22:06:15 -0700681 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {})
682
683 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
684 read_config):
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000685 """Calculate a summary of the results of building a commit.
686
687 Args:
688 board_selected: Dict containing boards to summarise
689 commit_upto: Commit number to summarize (0..self.count-1)
690 read_func_sizes: True to read function size information
Simon Glass843312d2015-02-05 22:06:15 -0700691 read_config: True to read .config and autoconf.h files
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000692
693 Returns:
694 Tuple:
695 Dict containing boards which passed building this commit.
696 keyed by board.target
Simon Glasse30965d2014-08-28 09:43:44 -0600697 List containing a summary of error lines
Simon Glassed966652014-08-28 09:43:43 -0600698 Dict keyed by error line, containing a list of the Board
699 objects with that error
Simon Glasse30965d2014-08-28 09:43:44 -0600700 List containing a summary of warning lines
701 Dict keyed by error line, containing a list of the Board
702 objects with that warning
Simon Glass8270e3c2015-08-25 21:52:14 -0600703 Dictionary keyed by board.target. Each value is a dictionary:
704 key: filename - e.g. '.config'
Simon Glass843312d2015-02-05 22:06:15 -0700705 value is itself a dictionary:
706 key: config name
707 value: config value
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000708 """
Simon Glasse30965d2014-08-28 09:43:44 -0600709 def AddLine(lines_summary, lines_boards, line, board):
710 line = line.rstrip()
711 if line in lines_boards:
712 lines_boards[line].append(board)
713 else:
714 lines_boards[line] = [board]
715 lines_summary.append(line)
716
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000717 board_dict = {}
718 err_lines_summary = []
Simon Glassed966652014-08-28 09:43:43 -0600719 err_lines_boards = {}
Simon Glasse30965d2014-08-28 09:43:44 -0600720 warn_lines_summary = []
721 warn_lines_boards = {}
Simon Glass843312d2015-02-05 22:06:15 -0700722 config = {}
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000723
724 for board in boards_selected.itervalues():
725 outcome = self.GetBuildOutcome(commit_upto, board.target,
Simon Glass843312d2015-02-05 22:06:15 -0700726 read_func_sizes, read_config)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000727 board_dict[board.target] = outcome
Simon Glasse30965d2014-08-28 09:43:44 -0600728 last_func = None
729 last_was_warning = False
730 for line in outcome.err_lines:
731 if line:
732 if (self._re_function.match(line) or
733 self._re_files.match(line)):
734 last_func = line
Simon Glassed966652014-08-28 09:43:43 -0600735 else:
Simon Glasse30965d2014-08-28 09:43:44 -0600736 is_warning = self._re_warning.match(line)
737 is_note = self._re_note.match(line)
738 if is_warning or (last_was_warning and is_note):
739 if last_func:
740 AddLine(warn_lines_summary, warn_lines_boards,
741 last_func, board)
742 AddLine(warn_lines_summary, warn_lines_boards,
743 line, board)
744 else:
745 if last_func:
746 AddLine(err_lines_summary, err_lines_boards,
747 last_func, board)
748 AddLine(err_lines_summary, err_lines_boards,
749 line, board)
750 last_was_warning = is_warning
751 last_func = None
Simon Glassb464f8e2016-11-13 14:25:53 -0700752 tconfig = Config(self.config_filenames, board.target)
753 for fname in self.config_filenames:
Simon Glass843312d2015-02-05 22:06:15 -0700754 if outcome.config:
755 for key, value in outcome.config[fname].iteritems():
Simon Glass8270e3c2015-08-25 21:52:14 -0600756 tconfig.Add(fname, key, value)
757 config[board.target] = tconfig
Simon Glass843312d2015-02-05 22:06:15 -0700758
Simon Glasse30965d2014-08-28 09:43:44 -0600759 return (board_dict, err_lines_summary, err_lines_boards,
Simon Glass843312d2015-02-05 22:06:15 -0700760 warn_lines_summary, warn_lines_boards, config)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000761
762 def AddOutcome(self, board_dict, arch_list, changes, char, color):
763 """Add an output to our list of outcomes for each architecture
764
765 This simple function adds failing boards (changes) to the
766 relevant architecture string, so we can print the results out
767 sorted by architecture.
768
769 Args:
770 board_dict: Dict containing all boards
771 arch_list: Dict keyed by arch name. Value is a string containing
772 a list of board names which failed for that arch.
773 changes: List of boards to add to arch_list
774 color: terminal.Colour object
775 """
776 done_arch = {}
777 for target in changes:
778 if target in board_dict:
779 arch = board_dict[target].arch
780 else:
781 arch = 'unknown'
782 str = self.col.Color(color, ' ' + target)
783 if not arch in done_arch:
Simon Glass63c619e2015-02-05 22:06:11 -0700784 str = ' %s %s' % (self.col.Color(color, char), str)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000785 done_arch[arch] = True
786 if not arch in arch_list:
787 arch_list[arch] = str
788 else:
789 arch_list[arch] += str
790
791
792 def ColourNum(self, num):
793 color = self.col.RED if num > 0 else self.col.GREEN
794 if num == 0:
795 return '0'
796 return self.col.Color(color, str(num))
797
798 def ResetResultSummary(self, board_selected):
799 """Reset the results summary ready for use.
800
801 Set up the base board list to be all those selected, and set the
802 error lines to empty.
803
804 Following this, calls to PrintResultSummary() will use this
805 information to work out what has changed.
806
807 Args:
808 board_selected: Dict containing boards to summarise, keyed by
809 board.target
810 """
811 self._base_board_dict = {}
812 for board in board_selected:
Simon Glass843312d2015-02-05 22:06:15 -0700813 self._base_board_dict[board] = Builder.Outcome(0, [], [], {}, {})
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000814 self._base_err_lines = []
Simon Glasse30965d2014-08-28 09:43:44 -0600815 self._base_warn_lines = []
816 self._base_err_line_boards = {}
817 self._base_warn_line_boards = {}
Simon Glass8270e3c2015-08-25 21:52:14 -0600818 self._base_config = None
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000819
820 def PrintFuncSizeDetail(self, fname, old, new):
821 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
822 delta, common = [], {}
823
824 for a in old:
825 if a in new:
826 common[a] = 1
827
828 for name in old:
829 if name not in common:
830 remove += 1
831 down += old[name]
832 delta.append([-old[name], name])
833
834 for name in new:
835 if name not in common:
836 add += 1
837 up += new[name]
838 delta.append([new[name], name])
839
840 for name in common:
841 diff = new.get(name, 0) - old.get(name, 0)
842 if diff > 0:
843 grow, up = grow + 1, up + diff
844 elif diff < 0:
845 shrink, down = shrink + 1, down - diff
846 delta.append([diff, name])
847
848 delta.sort()
849 delta.reverse()
850
851 args = [add, -remove, grow, -shrink, up, -down, up - down]
Tom Rinid5686a62017-05-22 13:48:52 -0400852 if max(args) == 0 and min(args) == 0:
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000853 return
854 args = [self.ColourNum(x) for x in args]
855 indent = ' ' * 15
Simon Glass4653a882014-09-05 19:00:07 -0600856 Print('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
857 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
858 Print('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
859 'delta'))
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000860 for diff, name in delta:
861 if diff:
862 color = self.col.RED if diff > 0 else self.col.GREEN
863 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
864 old.get(name, '-'), new.get(name,'-'), diff)
Simon Glass4653a882014-09-05 19:00:07 -0600865 Print(msg, colour=color)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000866
867
868 def PrintSizeDetail(self, target_list, show_bloat):
869 """Show details size information for each board
870
871 Args:
872 target_list: List of targets, each a dict containing:
873 'target': Target name
874 'total_diff': Total difference in bytes across all areas
875 <part_name>: Difference for that part
876 show_bloat: Show detail for each function
877 """
878 targets_by_diff = sorted(target_list, reverse=True,
879 key=lambda x: x['_total_diff'])
880 for result in targets_by_diff:
881 printed_target = False
882 for name in sorted(result):
883 diff = result[name]
884 if name.startswith('_'):
885 continue
886 if diff != 0:
887 color = self.col.RED if diff > 0 else self.col.GREEN
888 msg = ' %s %+d' % (name, diff)
889 if not printed_target:
Simon Glass4653a882014-09-05 19:00:07 -0600890 Print('%10s %-15s:' % ('', result['_target']),
891 newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000892 printed_target = True
Simon Glass4653a882014-09-05 19:00:07 -0600893 Print(msg, colour=color, newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000894 if printed_target:
Simon Glass4653a882014-09-05 19:00:07 -0600895 Print()
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000896 if show_bloat:
897 target = result['_target']
898 outcome = result['_outcome']
899 base_outcome = self._base_board_dict[target]
900 for fname in outcome.func_sizes:
901 self.PrintFuncSizeDetail(fname,
902 base_outcome.func_sizes[fname],
903 outcome.func_sizes[fname])
904
905
906 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
907 show_bloat):
908 """Print a summary of image sizes broken down by section.
909
910 The summary takes the form of one line per architecture. The
911 line contains deltas for each of the sections (+ means the section
912 got bigger, - means smaller). The nunmbers are the average number
913 of bytes that a board in this section increased by.
914
915 For example:
916 powerpc: (622 boards) text -0.0
917 arm: (285 boards) text -0.0
918 nds32: (3 boards) text -8.0
919
920 Args:
921 board_selected: Dict containing boards to summarise, keyed by
922 board.target
923 board_dict: Dict containing boards for which we built this
924 commit, keyed by board.target. The value is an Outcome object.
925 show_detail: Show detail for each board
926 show_bloat: Show detail for each function
927 """
928 arch_list = {}
929 arch_count = {}
930
931 # Calculate changes in size for different image parts
932 # The previous sizes are in Board.sizes, for each board
933 for target in board_dict:
934 if target not in board_selected:
935 continue
936 base_sizes = self._base_board_dict[target].sizes
937 outcome = board_dict[target]
938 sizes = outcome.sizes
939
940 # Loop through the list of images, creating a dict of size
941 # changes for each image/part. We end up with something like
942 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
943 # which means that U-Boot data increased by 5 bytes and SPL
944 # text decreased by 4.
945 err = {'_target' : target}
946 for image in sizes:
947 if image in base_sizes:
948 base_image = base_sizes[image]
949 # Loop through the text, data, bss parts
950 for part in sorted(sizes[image]):
951 diff = sizes[image][part] - base_image[part]
952 col = None
953 if diff:
954 if image == 'u-boot':
955 name = part
956 else:
957 name = image + ':' + part
958 err[name] = diff
959 arch = board_selected[target].arch
960 if not arch in arch_count:
961 arch_count[arch] = 1
962 else:
963 arch_count[arch] += 1
964 if not sizes:
965 pass # Only add to our list when we have some stats
966 elif not arch in arch_list:
967 arch_list[arch] = [err]
968 else:
969 arch_list[arch].append(err)
970
971 # We now have a list of image size changes sorted by arch
972 # Print out a summary of these
973 for arch, target_list in arch_list.iteritems():
974 # Get total difference for each type
975 totals = {}
976 for result in target_list:
977 total = 0
978 for name, diff in result.iteritems():
979 if name.startswith('_'):
980 continue
981 total += diff
982 if name in totals:
983 totals[name] += diff
984 else:
985 totals[name] = diff
986 result['_total_diff'] = total
987 result['_outcome'] = board_dict[result['_target']]
988
989 count = len(target_list)
990 printed_arch = False
991 for name in sorted(totals):
992 diff = totals[name]
993 if diff:
994 # Display the average difference in this name for this
995 # architecture
996 avg_diff = float(diff) / count
997 color = self.col.RED if avg_diff > 0 else self.col.GREEN
998 msg = ' %s %+1.1f' % (name, avg_diff)
999 if not printed_arch:
Simon Glass4653a882014-09-05 19:00:07 -06001000 Print('%10s: (for %d/%d boards)' % (arch, count,
1001 arch_count[arch]), newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001002 printed_arch = True
Simon Glass4653a882014-09-05 19:00:07 -06001003 Print(msg, colour=color, newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001004
1005 if printed_arch:
Simon Glass4653a882014-09-05 19:00:07 -06001006 Print()
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001007 if show_detail:
1008 self.PrintSizeDetail(target_list, show_bloat)
1009
1010
1011 def PrintResultSummary(self, board_selected, board_dict, err_lines,
Simon Glasse30965d2014-08-28 09:43:44 -06001012 err_line_boards, warn_lines, warn_line_boards,
Simon Glass843312d2015-02-05 22:06:15 -07001013 config, show_sizes, show_detail, show_bloat,
1014 show_config):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001015 """Compare results with the base results and display delta.
1016
1017 Only boards mentioned in board_selected will be considered. This
1018 function is intended to be called repeatedly with the results of
1019 each commit. It therefore shows a 'diff' between what it saw in
1020 the last call and what it sees now.
1021
1022 Args:
1023 board_selected: Dict containing boards to summarise, keyed by
1024 board.target
1025 board_dict: Dict containing boards for which we built this
1026 commit, keyed by board.target. The value is an Outcome object.
1027 err_lines: A list of errors for this commit, or [] if there is
1028 none, or we don't want to print errors
Simon Glassed966652014-08-28 09:43:43 -06001029 err_line_boards: Dict keyed by error line, containing a list of
1030 the Board objects with that error
Simon Glasse30965d2014-08-28 09:43:44 -06001031 warn_lines: A list of warnings for this commit, or [] if there is
1032 none, or we don't want to print errors
1033 warn_line_boards: Dict keyed by warning line, containing a list of
1034 the Board objects with that warning
Simon Glass843312d2015-02-05 22:06:15 -07001035 config: Dictionary keyed by filename - e.g. '.config'. Each
1036 value is itself a dictionary:
1037 key: config name
1038 value: config value
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001039 show_sizes: Show image size deltas
1040 show_detail: Show detail for each board
1041 show_bloat: Show detail for each function
Simon Glass843312d2015-02-05 22:06:15 -07001042 show_config: Show config changes
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001043 """
Simon Glasse30965d2014-08-28 09:43:44 -06001044 def _BoardList(line, line_boards):
Simon Glassed966652014-08-28 09:43:43 -06001045 """Helper function to get a line of boards containing a line
1046
1047 Args:
1048 line: Error line to search for
1049 Return:
1050 String containing a list of boards with that error line, or
1051 '' if the user has not requested such a list
1052 """
1053 if self._list_error_boards:
1054 names = []
Simon Glasse30965d2014-08-28 09:43:44 -06001055 for board in line_boards[line]:
Simon Glassf66153b2014-10-16 01:05:55 -06001056 if not board.target in names:
1057 names.append(board.target)
Simon Glassed966652014-08-28 09:43:43 -06001058 names_str = '(%s) ' % ','.join(names)
1059 else:
1060 names_str = ''
1061 return names_str
1062
Simon Glasse30965d2014-08-28 09:43:44 -06001063 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1064 char):
1065 better_lines = []
1066 worse_lines = []
1067 for line in lines:
1068 if line not in base_lines:
1069 worse_lines.append(char + '+' +
1070 _BoardList(line, line_boards) + line)
1071 for line in base_lines:
1072 if line not in lines:
1073 better_lines.append(char + '-' +
1074 _BoardList(line, base_line_boards) + line)
1075 return better_lines, worse_lines
1076
Simon Glass843312d2015-02-05 22:06:15 -07001077 def _CalcConfig(delta, name, config):
1078 """Calculate configuration changes
1079
1080 Args:
1081 delta: Type of the delta, e.g. '+'
1082 name: name of the file which changed (e.g. .config)
1083 config: configuration change dictionary
1084 key: config name
1085 value: config value
1086 Returns:
1087 String containing the configuration changes which can be
1088 printed
1089 """
1090 out = ''
1091 for key in sorted(config.keys()):
1092 out += '%s=%s ' % (key, config[key])
Simon Glass8270e3c2015-08-25 21:52:14 -06001093 return '%s %s: %s' % (delta, name, out)
Simon Glass843312d2015-02-05 22:06:15 -07001094
Simon Glass8270e3c2015-08-25 21:52:14 -06001095 def _AddConfig(lines, name, config_plus, config_minus, config_change):
1096 """Add changes in configuration to a list
Simon Glass843312d2015-02-05 22:06:15 -07001097
1098 Args:
Simon Glass8270e3c2015-08-25 21:52:14 -06001099 lines: list to add to
1100 name: config file name
Simon Glass843312d2015-02-05 22:06:15 -07001101 config_plus: configurations added, dictionary
1102 key: config name
1103 value: config value
1104 config_minus: configurations removed, dictionary
1105 key: config name
1106 value: config value
1107 config_change: configurations changed, dictionary
1108 key: config name
1109 value: config value
1110 """
1111 if config_plus:
Simon Glass8270e3c2015-08-25 21:52:14 -06001112 lines.append(_CalcConfig('+', name, config_plus))
Simon Glass843312d2015-02-05 22:06:15 -07001113 if config_minus:
Simon Glass8270e3c2015-08-25 21:52:14 -06001114 lines.append(_CalcConfig('-', name, config_minus))
Simon Glass843312d2015-02-05 22:06:15 -07001115 if config_change:
Simon Glass8270e3c2015-08-25 21:52:14 -06001116 lines.append(_CalcConfig('c', name, config_change))
1117
1118 def _OutputConfigInfo(lines):
1119 for line in lines:
1120 if not line:
1121 continue
1122 if line[0] == '+':
1123 col = self.col.GREEN
1124 elif line[0] == '-':
1125 col = self.col.RED
1126 elif line[0] == 'c':
1127 col = self.col.YELLOW
1128 Print(' ' + line, newline=True, colour=col)
1129
Simon Glass843312d2015-02-05 22:06:15 -07001130
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001131 better = [] # List of boards fixed since last commit
1132 worse = [] # List of new broken boards since last commit
1133 new = [] # List of boards that didn't exist last time
1134 unknown = [] # List of boards that were not built
1135
1136 for target in board_dict:
1137 if target not in board_selected:
1138 continue
1139
1140 # If the board was built last time, add its outcome to a list
1141 if target in self._base_board_dict:
1142 base_outcome = self._base_board_dict[target].rc
1143 outcome = board_dict[target]
1144 if outcome.rc == OUTCOME_UNKNOWN:
1145 unknown.append(target)
1146 elif outcome.rc < base_outcome:
1147 better.append(target)
1148 elif outcome.rc > base_outcome:
1149 worse.append(target)
1150 else:
1151 new.append(target)
1152
1153 # Get a list of errors that have appeared, and disappeared
Simon Glasse30965d2014-08-28 09:43:44 -06001154 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1155 self._base_err_line_boards, err_lines, err_line_boards, '')
1156 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1157 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001158
1159 # Display results by arch
Simon Glasse30965d2014-08-28 09:43:44 -06001160 if (better or worse or unknown or new or worse_err or better_err
1161 or worse_warn or better_warn):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001162 arch_list = {}
1163 self.AddOutcome(board_selected, arch_list, better, '',
1164 self.col.GREEN)
1165 self.AddOutcome(board_selected, arch_list, worse, '+',
1166 self.col.RED)
1167 self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE)
1168 if self._show_unknown:
1169 self.AddOutcome(board_selected, arch_list, unknown, '?',
1170 self.col.MAGENTA)
1171 for arch, target_list in arch_list.iteritems():
Simon Glass4653a882014-09-05 19:00:07 -06001172 Print('%10s: %s' % (arch, target_list))
Simon Glass28370c12014-08-09 15:33:06 -06001173 self._error_lines += 1
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001174 if better_err:
Simon Glass4653a882014-09-05 19:00:07 -06001175 Print('\n'.join(better_err), colour=self.col.GREEN)
Simon Glass28370c12014-08-09 15:33:06 -06001176 self._error_lines += 1
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001177 if worse_err:
Simon Glass4653a882014-09-05 19:00:07 -06001178 Print('\n'.join(worse_err), colour=self.col.RED)
Simon Glass28370c12014-08-09 15:33:06 -06001179 self._error_lines += 1
Simon Glasse30965d2014-08-28 09:43:44 -06001180 if better_warn:
Simon Glass4653a882014-09-05 19:00:07 -06001181 Print('\n'.join(better_warn), colour=self.col.CYAN)
Simon Glasse30965d2014-08-28 09:43:44 -06001182 self._error_lines += 1
1183 if worse_warn:
Simon Glass4653a882014-09-05 19:00:07 -06001184 Print('\n'.join(worse_warn), colour=self.col.MAGENTA)
Simon Glasse30965d2014-08-28 09:43:44 -06001185 self._error_lines += 1
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001186
1187 if show_sizes:
1188 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1189 show_bloat)
1190
Simon Glass8270e3c2015-08-25 21:52:14 -06001191 if show_config and self._base_config:
1192 summary = {}
1193 arch_config_plus = {}
1194 arch_config_minus = {}
1195 arch_config_change = {}
1196 arch_list = []
1197
1198 for target in board_dict:
1199 if target not in board_selected:
Simon Glass843312d2015-02-05 22:06:15 -07001200 continue
Simon Glass8270e3c2015-08-25 21:52:14 -06001201 arch = board_selected[target].arch
1202 if arch not in arch_list:
1203 arch_list.append(arch)
1204
1205 for arch in arch_list:
1206 arch_config_plus[arch] = {}
1207 arch_config_minus[arch] = {}
1208 arch_config_change[arch] = {}
Simon Glassb464f8e2016-11-13 14:25:53 -07001209 for name in self.config_filenames:
Simon Glass8270e3c2015-08-25 21:52:14 -06001210 arch_config_plus[arch][name] = {}
1211 arch_config_minus[arch][name] = {}
1212 arch_config_change[arch][name] = {}
1213
1214 for target in board_dict:
1215 if target not in board_selected:
1216 continue
1217
1218 arch = board_selected[target].arch
1219
1220 all_config_plus = {}
1221 all_config_minus = {}
1222 all_config_change = {}
1223 tbase = self._base_config[target]
1224 tconfig = config[target]
1225 lines = []
Simon Glassb464f8e2016-11-13 14:25:53 -07001226 for name in self.config_filenames:
Simon Glass8270e3c2015-08-25 21:52:14 -06001227 if not tconfig.config[name]:
1228 continue
1229 config_plus = {}
1230 config_minus = {}
1231 config_change = {}
1232 base = tbase.config[name]
1233 for key, value in tconfig.config[name].iteritems():
1234 if key not in base:
1235 config_plus[key] = value
1236 all_config_plus[key] = value
1237 for key, value in base.iteritems():
1238 if key not in tconfig.config[name]:
1239 config_minus[key] = value
1240 all_config_minus[key] = value
1241 for key, value in base.iteritems():
1242 new_value = tconfig.config.get(key)
1243 if new_value and value != new_value:
1244 desc = '%s -> %s' % (value, new_value)
1245 config_change[key] = desc
1246 all_config_change[key] = desc
1247
1248 arch_config_plus[arch][name].update(config_plus)
1249 arch_config_minus[arch][name].update(config_minus)
1250 arch_config_change[arch][name].update(config_change)
1251
1252 _AddConfig(lines, name, config_plus, config_minus,
1253 config_change)
1254 _AddConfig(lines, 'all', all_config_plus, all_config_minus,
1255 all_config_change)
1256 summary[target] = '\n'.join(lines)
1257
1258 lines_by_target = {}
1259 for target, lines in summary.iteritems():
1260 if lines in lines_by_target:
1261 lines_by_target[lines].append(target)
1262 else:
1263 lines_by_target[lines] = [target]
1264
1265 for arch in arch_list:
1266 lines = []
1267 all_plus = {}
1268 all_minus = {}
1269 all_change = {}
Simon Glassb464f8e2016-11-13 14:25:53 -07001270 for name in self.config_filenames:
Simon Glass8270e3c2015-08-25 21:52:14 -06001271 all_plus.update(arch_config_plus[arch][name])
1272 all_minus.update(arch_config_minus[arch][name])
1273 all_change.update(arch_config_change[arch][name])
1274 _AddConfig(lines, name, arch_config_plus[arch][name],
1275 arch_config_minus[arch][name],
1276 arch_config_change[arch][name])
1277 _AddConfig(lines, 'all', all_plus, all_minus, all_change)
1278 #arch_summary[target] = '\n'.join(lines)
1279 if lines:
1280 Print('%s:' % arch)
1281 _OutputConfigInfo(lines)
1282
1283 for lines, targets in lines_by_target.iteritems():
1284 if not lines:
1285 continue
1286 Print('%s :' % ' '.join(sorted(targets)))
1287 _OutputConfigInfo(lines.split('\n'))
1288
Simon Glass843312d2015-02-05 22:06:15 -07001289
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001290 # Save our updated information for the next call to this function
1291 self._base_board_dict = board_dict
1292 self._base_err_lines = err_lines
Simon Glasse30965d2014-08-28 09:43:44 -06001293 self._base_warn_lines = warn_lines
1294 self._base_err_line_boards = err_line_boards
1295 self._base_warn_line_boards = warn_line_boards
Simon Glass843312d2015-02-05 22:06:15 -07001296 self._base_config = config
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001297
1298 # Get a list of boards that did not get built, if needed
1299 not_built = []
1300 for board in board_selected:
1301 if not board in board_dict:
1302 not_built.append(board)
1303 if not_built:
Simon Glass4653a882014-09-05 19:00:07 -06001304 Print("Boards not built (%d): %s" % (len(not_built),
1305 ', '.join(not_built)))
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001306
Simon Glassb2ea7ab2014-08-09 15:33:02 -06001307 def ProduceResultSummary(self, commit_upto, commits, board_selected):
Simon Glasse30965d2014-08-28 09:43:44 -06001308 (board_dict, err_lines, err_line_boards, warn_lines,
Simon Glass843312d2015-02-05 22:06:15 -07001309 warn_line_boards, config) = self.GetResultSummary(
Simon Glassed966652014-08-28 09:43:43 -06001310 board_selected, commit_upto,
Simon Glass843312d2015-02-05 22:06:15 -07001311 read_func_sizes=self._show_bloat,
1312 read_config=self._show_config)
Simon Glassb2ea7ab2014-08-09 15:33:02 -06001313 if commits:
1314 msg = '%02d: %s' % (commit_upto + 1,
1315 commits[commit_upto].subject)
Simon Glass4653a882014-09-05 19:00:07 -06001316 Print(msg, colour=self.col.BLUE)
Simon Glassb2ea7ab2014-08-09 15:33:02 -06001317 self.PrintResultSummary(board_selected, board_dict,
Simon Glassed966652014-08-28 09:43:43 -06001318 err_lines if self._show_errors else [], err_line_boards,
Simon Glasse30965d2014-08-28 09:43:44 -06001319 warn_lines if self._show_errors else [], warn_line_boards,
Simon Glass843312d2015-02-05 22:06:15 -07001320 config, self._show_sizes, self._show_detail,
1321 self._show_bloat, self._show_config)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001322
Simon Glassb2ea7ab2014-08-09 15:33:02 -06001323 def ShowSummary(self, commits, board_selected):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001324 """Show a build summary for U-Boot for a given board list.
1325
1326 Reset the result summary, then repeatedly call GetResultSummary on
1327 each commit's results, then display the differences we see.
1328
1329 Args:
1330 commit: Commit objects to summarise
1331 board_selected: Dict containing boards to summarise
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001332 """
Simon Glassfea58582014-08-09 15:32:59 -06001333 self.commit_count = len(commits) if commits else 1
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001334 self.commits = commits
1335 self.ResetResultSummary(board_selected)
Simon Glass28370c12014-08-09 15:33:06 -06001336 self._error_lines = 0
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001337
1338 for commit_upto in range(0, self.commit_count, self._step):
Simon Glassb2ea7ab2014-08-09 15:33:02 -06001339 self.ProduceResultSummary(commit_upto, commits, board_selected)
Simon Glass28370c12014-08-09 15:33:06 -06001340 if not self._error_lines:
Simon Glass4653a882014-09-05 19:00:07 -06001341 Print('(no errors to report)', colour=self.col.GREEN)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001342
1343
1344 def SetupBuild(self, board_selected, commits):
1345 """Set up ready to start a build.
1346
1347 Args:
1348 board_selected: Selected boards to build
1349 commits: Selected commits to build
1350 """
1351 # First work out how many commits we will build
Simon Glassfea58582014-08-09 15:32:59 -06001352 count = (self.commit_count + self._step - 1) / self._step
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001353 self.count = len(board_selected) * count
1354 self.upto = self.warned = self.fail = 0
1355 self._timestamps = collections.deque()
1356
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001357 def GetThreadDir(self, thread_num):
1358 """Get the directory path to the working dir for a thread.
1359
1360 Args:
1361 thread_num: Number of thread to check.
1362 """
1363 return os.path.join(self._working_dir, '%02d' % thread_num)
1364
Simon Glassfea58582014-08-09 15:32:59 -06001365 def _PrepareThread(self, thread_num, setup_git):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001366 """Prepare the working directory for a thread.
1367
1368 This clones or fetches the repo into the thread's work directory.
1369
1370 Args:
1371 thread_num: Thread number (0, 1, ...)
Simon Glassfea58582014-08-09 15:32:59 -06001372 setup_git: True to set up a git repo clone
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001373 """
1374 thread_dir = self.GetThreadDir(thread_num)
Simon Glass190064b2014-08-09 15:33:00 -06001375 builderthread.Mkdir(thread_dir)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001376 git_dir = os.path.join(thread_dir, '.git')
1377
1378 # Clone the repo if it doesn't already exist
1379 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1380 # we have a private index but uses the origin repo's contents?
Simon Glassfea58582014-08-09 15:32:59 -06001381 if setup_git and self.git_dir:
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001382 src_dir = os.path.abspath(self.git_dir)
1383 if os.path.exists(git_dir):
1384 gitutil.Fetch(git_dir, thread_dir)
1385 else:
Simon Glass21f0eb32016-09-18 16:48:31 -06001386 Print('\rCloning repo for thread %d' % thread_num,
1387 newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001388 gitutil.Clone(src_dir, thread_dir)
Simon Glass21f0eb32016-09-18 16:48:31 -06001389 Print('\r%s\r' % (' ' * 30), newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001390
Simon Glassfea58582014-08-09 15:32:59 -06001391 def _PrepareWorkingSpace(self, max_threads, setup_git):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001392 """Prepare the working directory for use.
1393
1394 Set up the git repo for each thread.
1395
1396 Args:
1397 max_threads: Maximum number of threads we expect to need.
Simon Glassfea58582014-08-09 15:32:59 -06001398 setup_git: True to set up a git repo clone
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001399 """
Simon Glass190064b2014-08-09 15:33:00 -06001400 builderthread.Mkdir(self._working_dir)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001401 for thread in range(max_threads):
Simon Glassfea58582014-08-09 15:32:59 -06001402 self._PrepareThread(thread, setup_git)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001403
1404 def _PrepareOutputSpace(self):
1405 """Get the output directories ready to receive files.
1406
1407 We delete any output directories which look like ones we need to
1408 create. Having left over directories is confusing when the user wants
1409 to check the output manually.
1410 """
Simon Glass1a915672014-12-01 17:33:53 -07001411 if not self.commits:
1412 return
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001413 dir_list = []
1414 for commit_upto in range(self.commit_count):
1415 dir_list.append(self._GetOutputDir(commit_upto))
1416
Simon Glassb222abe2016-09-18 16:48:32 -06001417 to_remove = []
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001418 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1419 if dirname not in dir_list:
Simon Glassb222abe2016-09-18 16:48:32 -06001420 to_remove.append(dirname)
1421 if to_remove:
1422 Print('Removing %d old build directories' % len(to_remove),
1423 newline=False)
1424 for dirname in to_remove:
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001425 shutil.rmtree(dirname)
1426
Simon Glasse5a0e5d2014-08-09 15:33:03 -06001427 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001428 """Build all commits for a list of boards
1429
1430 Args:
1431 commits: List of commits to be build, each a Commit object
1432 boards_selected: Dict of selected boards, key is target name,
1433 value is Board object
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001434 keep_outputs: True to save build output files
Simon Glasse5a0e5d2014-08-09 15:33:03 -06001435 verbose: Display build results as they are completed
Simon Glass2c3deb92014-08-28 09:43:39 -06001436 Returns:
1437 Tuple containing:
1438 - number of boards that failed to build
1439 - number of boards that issued warnings
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001440 """
Simon Glassfea58582014-08-09 15:32:59 -06001441 self.commit_count = len(commits) if commits else 1
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001442 self.commits = commits
Simon Glasse5a0e5d2014-08-09 15:33:03 -06001443 self._verbose = verbose
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001444
1445 self.ResetResultSummary(board_selected)
Thierry Redingf3d015c2014-08-19 10:22:39 +02001446 builderthread.Mkdir(self.base_dir, parents = True)
Simon Glassfea58582014-08-09 15:32:59 -06001447 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1448 commits is not None)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001449 self._PrepareOutputSpace()
Simon Glass745b3952016-09-18 16:48:33 -06001450 Print('\rStarting build...', newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001451 self.SetupBuild(board_selected, commits)
1452 self.ProcessResult(None)
1453
1454 # Create jobs to build all commits for each board
1455 for brd in board_selected.itervalues():
Simon Glass190064b2014-08-09 15:33:00 -06001456 job = builderthread.BuilderJob()
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001457 job.board = brd
1458 job.commits = commits
1459 job.keep_outputs = keep_outputs
1460 job.step = self._step
1461 self.queue.put(job)
1462
Simon Glassd436e382016-09-18 16:48:35 -06001463 term = threading.Thread(target=self.queue.join)
1464 term.setDaemon(True)
1465 term.start()
1466 while term.isAlive():
1467 term.join(100)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001468
1469 # Wait until we have processed all output
1470 self.out_queue.join()
Simon Glass4653a882014-09-05 19:00:07 -06001471 Print()
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001472 self.ClearLine(0)
Simon Glass2c3deb92014-08-28 09:43:39 -06001473 return (self.fail, self.warned)