blob: 236e0617ac42ac6d9d1c7e872fcbe1b606b2e68b [file] [log] [blame]
Simon Glassc05694f2013-04-03 11:07:16 +00001# Copyright (c) 2013 The Chromium OS Authors.
2#
3# Bloat-o-meter code used here Copyright 2004 Matt Mackall <mpm@selenic.com>
4#
Wolfgang Denkd79de1d2013-07-08 09:37:19 +02005# SPDX-License-Identifier: GPL-2.0+
Simon Glassc05694f2013-04-03 11:07:16 +00006#
7
8import collections
Simon Glassc05694f2013-04-03 11:07:16 +00009from datetime import datetime, timedelta
10import glob
11import os
12import re
13import Queue
14import shutil
Simon Glass205ac042016-09-18 16:48:37 -060015import signal
Simon Glassc05694f2013-04-03 11:07:16 +000016import string
17import sys
Simon Glassd26e1442016-09-18 16:48:35 -060018import threading
Simon Glassc05694f2013-04-03 11:07:16 +000019import time
20
Simon Glass4a1e88b2014-08-09 15:33:00 -060021import builderthread
Simon Glassc05694f2013-04-03 11:07:16 +000022import command
23import gitutil
24import terminal
Simon Glass4433aa92014-09-05 19:00:07 -060025from terminal import Print
Simon Glassc05694f2013-04-03 11:07:16 +000026import toolchain
27
28
29"""
30Theory of Operation
31
32Please see README for user documentation, and you should be familiar with
33that before trying to make sense of this.
34
35Buildman works by keeping the machine as busy as possible, building different
36commits for different boards on multiple CPUs at once.
37
38The source repo (self.git_dir) contains all the commits to be built. Each
39thread works on a single board at a time. It checks out the first commit,
40configures it for that board, then builds it. Then it checks out the next
41commit and builds it (typically without re-configuring). When it runs out
42of commits, it gets another job from the builder and starts again with that
43board.
44
45Clearly the builder threads could work either way - they could check out a
46commit and then built it for all boards. Using separate directories for each
47commit/board pair they could leave their build product around afterwards
48also.
49
50The intent behind building a single board for multiple commits, is to make
51use of incremental builds. Since each commit is built incrementally from
52the previous one, builds are faster. Reconfiguring for a different board
53removes all intermediate object files.
54
55Many threads can be working at once, but each has its own working directory.
56When a thread finishes a build, it puts the output files into a result
57directory.
58
59The base directory used by buildman is normally '../<branch>', i.e.
60a directory higher than the source repository and named after the branch
61being built.
62
63Within the base directory, we have one subdirectory for each commit. Within
64that is one subdirectory for each board. Within that is the build output for
65that commit/board combination.
66
67Buildman also create working directories for each thread, in a .bm-work/
68subdirectory in the base dir.
69
70As an example, say we are building branch 'us-net' for boards 'sandbox' and
71'seaboard', and say that us-net has two commits. We will have directories
72like this:
73
74us-net/ base directory
75 01_of_02_g4ed4ebc_net--Add-tftp-speed-/
76 sandbox/
77 u-boot.bin
78 seaboard/
79 u-boot.bin
80 02_of_02_g4ed4ebc_net--Check-tftp-comp/
81 sandbox/
82 u-boot.bin
83 seaboard/
84 u-boot.bin
85 .bm-work/
86 00/ working directory for thread 0 (contains source checkout)
87 build/ build output
88 01/ working directory for thread 1
89 build/ build output
90 ...
91u-boot/ source directory
92 .git/ repository
93"""
94
95# Possible build outcomes
96OUTCOME_OK, OUTCOME_WARNING, OUTCOME_ERROR, OUTCOME_UNKNOWN = range(4)
97
98# Translate a commit subject into a valid filename
99trans_valid_chars = string.maketrans("/: ", "---")
100
Simon Glasscde5c302016-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 Glassdb17fb82015-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 Glassdb17fb82015-02-05 22:06:15 -0700109]
110
Simon Glasscad8abf2015-08-25 21:52:14 -0600111class Config:
112 """Holds information about configuration settings for a board."""
Simon Glasscde5c302016-11-13 14:25:53 -0700113 def __init__(self, config_filename, target):
Simon Glasscad8abf2015-08-25 21:52:14 -0600114 self.target = target
115 self.config = {}
Simon Glasscde5c302016-11-13 14:25:53 -0700116 for fname in config_filename:
Simon Glasscad8abf2015-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 Glassc05694f2013-04-03 11:07:16 +0000129
Simon Glassc05694f2013-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 Glassc05694f2013-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 Glass7041c392014-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 Glassc05694f2013-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 Glassf3018b7a2014-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 Glass38df2e22014-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 Glassc05694f2013-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 Glass03749d42014-08-28 09:43:44 -0600173 _base_warn_lines: Last-summarised list of warnings
Simon Glassc05694f2013-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 Glassdb17fb82015-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 Glassc05694f2013-04-03 11:07:16 +0000202 """
Simon Glassdb17fb82015-02-05 22:06:15 -0700203 def __init__(self, rc, err_lines, sizes, func_sizes, config):
Simon Glassc05694f2013-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 Glassdb17fb82015-02-05 22:06:15 -0700208 self.config = config
Simon Glassc05694f2013-04-03 11:07:16 +0000209
210 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
Simon Glasse87bde12014-12-01 17:33:55 -0700211 gnu_make='make', checkout=True, show_unknown=True, step=1,
Stephen Warren97c96902016-04-11 10:48:44 -0600212 no_subdirs=False, full_path=False, verbose_build=False,
Simon Glass739e8512016-11-13 14:25:51 -0700213 incremental=False, per_board_out_dir=False,
Simon Glasscde5c302016-11-13 14:25:53 -0700214 config_only=False, squash_config_y=False):
Simon Glassc05694f2013-04-03 11:07:16 +0000215 """Create a new Builder object
216
217 Args:
218 toolchains: Toolchains object to use for building
219 base_dir: Base directory to use for builder
220 git_dir: Git directory containing source repository
221 num_threads: Number of builder threads to run
222 num_jobs: Number of jobs to run at once (passed to make as -j)
Masahiro Yamada1fe610d2014-07-22 11:19:09 +0900223 gnu_make: the command name of GNU Make.
Simon Glassc05694f2013-04-03 11:07:16 +0000224 checkout: True to check out source, False to skip that step.
225 This is used for testing.
226 show_unknown: Show unknown boards (those not built) in summary
227 step: 1 to process every commit, n to process every nth commit
Simon Glassd48a46c2014-12-01 17:34:00 -0700228 no_subdirs: Don't create subdirectories when building current
229 source for a single board
230 full_path: Return the full path in CROSS_COMPILE and don't set
231 PATH
Simon Glass655b6102014-12-01 17:34:07 -0700232 verbose_build: Run build with V=1 and don't use 'make -s'
Stephen Warren97c96902016-04-11 10:48:44 -0600233 incremental: Always perform incremental builds; don't run make
234 mrproper when configuring
235 per_board_out_dir: Build in a separate persistent directory per
236 board rather than a thread-specific directory
Simon Glass739e8512016-11-13 14:25:51 -0700237 config_only: Only configure each build, don't build it
Simon Glasscde5c302016-11-13 14:25:53 -0700238 squash_config_y: Convert CONFIG options with the value 'y' to '1'
Simon Glassc05694f2013-04-03 11:07:16 +0000239 """
240 self.toolchains = toolchains
241 self.base_dir = base_dir
242 self._working_dir = os.path.join(base_dir, '.bm-work')
243 self.threads = []
Simon Glassc05694f2013-04-03 11:07:16 +0000244 self.do_make = self.Make
Masahiro Yamada1fe610d2014-07-22 11:19:09 +0900245 self.gnu_make = gnu_make
Simon Glassc05694f2013-04-03 11:07:16 +0000246 self.checkout = checkout
247 self.num_threads = num_threads
248 self.num_jobs = num_jobs
249 self.already_done = 0
250 self.force_build = False
251 self.git_dir = git_dir
252 self._show_unknown = show_unknown
253 self._timestamp_count = 10
254 self._build_period_us = None
255 self._complete_delay = None
256 self._next_delay_update = datetime.now()
257 self.force_config_on_failure = True
Simon Glass7041c392014-07-13 12:22:31 -0600258 self.force_build_failures = False
Simon Glassf3018b7a2014-07-14 17:51:02 -0600259 self.force_reconfig = False
Simon Glassc05694f2013-04-03 11:07:16 +0000260 self._step = step
Simon Glass38df2e22014-07-14 17:51:03 -0600261 self.in_tree = False
Simon Glassbb4dffb2014-08-09 15:33:06 -0600262 self._error_lines = 0
Simon Glasse87bde12014-12-01 17:33:55 -0700263 self.no_subdirs = no_subdirs
Simon Glassd48a46c2014-12-01 17:34:00 -0700264 self.full_path = full_path
Simon Glass655b6102014-12-01 17:34:07 -0700265 self.verbose_build = verbose_build
Simon Glass739e8512016-11-13 14:25:51 -0700266 self.config_only = config_only
Simon Glasscde5c302016-11-13 14:25:53 -0700267 self.squash_config_y = squash_config_y
268 self.config_filenames = BASE_CONFIG_FILENAMES
269 if not self.squash_config_y:
270 self.config_filenames += EXTRA_CONFIG_FILENAMES
Simon Glassc05694f2013-04-03 11:07:16 +0000271
272 self.col = terminal.Color()
273
Simon Glass03749d42014-08-28 09:43:44 -0600274 self._re_function = re.compile('(.*): In function.*')
275 self._re_files = re.compile('In file included from.*')
276 self._re_warning = re.compile('(.*):(\d*):(\d*): warning: .*')
277 self._re_note = re.compile('(.*):(\d*):(\d*): note: this is the location of the previous.*')
278
Simon Glassc05694f2013-04-03 11:07:16 +0000279 self.queue = Queue.Queue()
280 self.out_queue = Queue.Queue()
281 for i in range(self.num_threads):
Stephen Warren97c96902016-04-11 10:48:44 -0600282 t = builderthread.BuilderThread(self, i, incremental,
283 per_board_out_dir)
Simon Glassc05694f2013-04-03 11:07:16 +0000284 t.setDaemon(True)
285 t.start()
286 self.threads.append(t)
287
288 self.last_line_len = 0
Simon Glass4a1e88b2014-08-09 15:33:00 -0600289 t = builderthread.ResultThread(self)
Simon Glassc05694f2013-04-03 11:07:16 +0000290 t.setDaemon(True)
291 t.start()
292 self.threads.append(t)
293
294 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
295 self.re_make_err = re.compile('|'.join(ignore_lines))
296
Simon Glass205ac042016-09-18 16:48:37 -0600297 # Handle existing graceful with SIGINT / Ctrl-C
298 signal.signal(signal.SIGINT, self.signal_handler)
299
Simon Glassc05694f2013-04-03 11:07:16 +0000300 def __del__(self):
301 """Get rid of all threads created by the builder"""
302 for t in self.threads:
303 del t
304
Simon Glass205ac042016-09-18 16:48:37 -0600305 def signal_handler(self, signal, frame):
306 sys.exit(1)
307
Simon Glasseb48bbc2014-08-09 15:33:02 -0600308 def SetDisplayOptions(self, show_errors=False, show_sizes=False,
Simon Glass3394c9f2014-08-28 09:43:43 -0600309 show_detail=False, show_bloat=False,
Simon Glassdb17fb82015-02-05 22:06:15 -0700310 list_error_boards=False, show_config=False):
Simon Glasseb48bbc2014-08-09 15:33:02 -0600311 """Setup display options for the builder.
312
313 show_errors: True to show summarised error/warning info
314 show_sizes: Show size deltas
315 show_detail: Show detail for each board
316 show_bloat: Show detail for each function
Simon Glass3394c9f2014-08-28 09:43:43 -0600317 list_error_boards: Show the boards which caused each error/warning
Simon Glassdb17fb82015-02-05 22:06:15 -0700318 show_config: Show config deltas
Simon Glasseb48bbc2014-08-09 15:33:02 -0600319 """
320 self._show_errors = show_errors
321 self._show_sizes = show_sizes
322 self._show_detail = show_detail
323 self._show_bloat = show_bloat
Simon Glass3394c9f2014-08-28 09:43:43 -0600324 self._list_error_boards = list_error_boards
Simon Glassdb17fb82015-02-05 22:06:15 -0700325 self._show_config = show_config
Simon Glasseb48bbc2014-08-09 15:33:02 -0600326
Simon Glassc05694f2013-04-03 11:07:16 +0000327 def _AddTimestamp(self):
328 """Add a new timestamp to the list and record the build period.
329
330 The build period is the length of time taken to perform a single
331 build (one board, one commit).
332 """
333 now = datetime.now()
334 self._timestamps.append(now)
335 count = len(self._timestamps)
336 delta = self._timestamps[-1] - self._timestamps[0]
337 seconds = delta.total_seconds()
338
339 # If we have enough data, estimate build period (time taken for a
340 # single build) and therefore completion time.
341 if count > 1 and self._next_delay_update < now:
342 self._next_delay_update = now + timedelta(seconds=2)
343 if seconds > 0:
344 self._build_period = float(seconds) / count
345 todo = self.count - self.upto
346 self._complete_delay = timedelta(microseconds=
347 self._build_period * todo * 1000000)
348 # Round it
349 self._complete_delay -= timedelta(
350 microseconds=self._complete_delay.microseconds)
351
352 if seconds > 60:
353 self._timestamps.popleft()
354 count -= 1
355
356 def ClearLine(self, length):
357 """Clear any characters on the current line
358
359 Make way for a new line of length 'length', by outputting enough
360 spaces to clear out the old line. Then remember the new length for
361 next time.
362
363 Args:
364 length: Length of new line, in characters
365 """
366 if length < self.last_line_len:
Simon Glass4433aa92014-09-05 19:00:07 -0600367 Print(' ' * (self.last_line_len - length), newline=False)
368 Print('\r', newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +0000369 self.last_line_len = length
370 sys.stdout.flush()
371
372 def SelectCommit(self, commit, checkout=True):
373 """Checkout the selected commit for this build
374 """
375 self.commit = commit
376 if checkout and self.checkout:
377 gitutil.Checkout(commit.hash)
378
379 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
380 """Run make
381
382 Args:
383 commit: Commit object that is being built
384 brd: Board object that is being built
Roger Meiere0a0e552014-08-20 22:10:29 +0200385 stage: Stage that we are at (mrproper, config, build)
Simon Glassc05694f2013-04-03 11:07:16 +0000386 cwd: Directory where make should be run
387 args: Arguments to pass to make
388 kwargs: Arguments to pass to command.RunPipe()
389 """
Masahiro Yamada1fe610d2014-07-22 11:19:09 +0900390 cmd = [self.gnu_make] + list(args)
Simon Glassc05694f2013-04-03 11:07:16 +0000391 result = command.RunPipe([cmd], capture=True, capture_stderr=True,
392 cwd=cwd, raise_on_error=False, **kwargs)
Simon Glass413f91a2015-02-05 22:06:12 -0700393 if self.verbose_build:
394 result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout
395 result.combined = '%s\n' % (' '.join(cmd)) + result.combined
Simon Glassc05694f2013-04-03 11:07:16 +0000396 return result
397
398 def ProcessResult(self, result):
399 """Process the result of a build, showing progress information
400
401 Args:
Simon Glass78e418e2014-08-09 15:33:03 -0600402 result: A CommandResult object, which indicates the result for
403 a single build
Simon Glassc05694f2013-04-03 11:07:16 +0000404 """
405 col = terminal.Color()
406 if result:
407 target = result.brd.target
408
Simon Glassc05694f2013-04-03 11:07:16 +0000409 self.upto += 1
410 if result.return_code != 0:
411 self.fail += 1
412 elif result.stderr:
413 self.warned += 1
414 if result.already_done:
415 self.already_done += 1
Simon Glass78e418e2014-08-09 15:33:03 -0600416 if self._verbose:
Simon Glass4433aa92014-09-05 19:00:07 -0600417 Print('\r', newline=False)
Simon Glass78e418e2014-08-09 15:33:03 -0600418 self.ClearLine(0)
419 boards_selected = {target : result.brd}
420 self.ResetResultSummary(boards_selected)
421 self.ProduceResultSummary(result.commit_upto, self.commits,
422 boards_selected)
Simon Glassc05694f2013-04-03 11:07:16 +0000423 else:
424 target = '(starting)'
425
426 # Display separate counts for ok, warned and fail
427 ok = self.upto - self.warned - self.fail
428 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
429 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
430 line += self.col.Color(self.col.RED, '%5d' % self.fail)
431
432 name = ' /%-5d ' % self.count
433
434 # Add our current completion time estimate
435 self._AddTimestamp()
436 if self._complete_delay:
437 name += '%s : ' % self._complete_delay
438 # When building all boards for a commit, we can print a commit
439 # progress message.
440 if result and result.commit_upto is None:
441 name += 'commit %2d/%-3d' % (self.commit_upto + 1,
442 self.commit_count)
443
444 name += target
Simon Glass4433aa92014-09-05 19:00:07 -0600445 Print(line + name, newline=False)
Simon Glassb45bd6b2016-11-15 15:32:59 -0700446 length = 16 + len(name)
Simon Glassc05694f2013-04-03 11:07:16 +0000447 self.ClearLine(length)
448
449 def _GetOutputDir(self, commit_upto):
450 """Get the name of the output directory for a commit number
451
452 The output directory is typically .../<branch>/<commit>.
453
454 Args:
455 commit_upto: Commit number to use (0..self.count-1)
456 """
Simon Glasse87bde12014-12-01 17:33:55 -0700457 commit_dir = None
Simon Glassd326ad72014-08-09 15:32:59 -0600458 if self.commits:
459 commit = self.commits[commit_upto]
460 subject = commit.subject.translate(trans_valid_chars)
461 commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
462 self.commit_count, commit.hash, subject[:20]))
Simon Glasse87bde12014-12-01 17:33:55 -0700463 elif not self.no_subdirs:
Simon Glassd326ad72014-08-09 15:32:59 -0600464 commit_dir = 'current'
Simon Glasse87bde12014-12-01 17:33:55 -0700465 if not commit_dir:
466 return self.base_dir
467 return os.path.join(self.base_dir, commit_dir)
Simon Glassc05694f2013-04-03 11:07:16 +0000468
469 def GetBuildDir(self, commit_upto, target):
470 """Get the name of the build directory for a commit number
471
472 The build directory is typically .../<branch>/<commit>/<target>.
473
474 Args:
475 commit_upto: Commit number to use (0..self.count-1)
476 target: Target name
477 """
478 output_dir = self._GetOutputDir(commit_upto)
479 return os.path.join(output_dir, target)
480
481 def GetDoneFile(self, commit_upto, target):
482 """Get the name of the done file for a commit number
483
484 Args:
485 commit_upto: Commit number to use (0..self.count-1)
486 target: Target name
487 """
488 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
489
490 def GetSizesFile(self, commit_upto, target):
491 """Get the name of the sizes file for a commit number
492
493 Args:
494 commit_upto: Commit number to use (0..self.count-1)
495 target: Target name
496 """
497 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
498
499 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
500 """Get the name of the funcsizes file for a commit number and ELF file
501
502 Args:
503 commit_upto: Commit number to use (0..self.count-1)
504 target: Target name
505 elf_fname: Filename of elf image
506 """
507 return os.path.join(self.GetBuildDir(commit_upto, target),
508 '%s.sizes' % elf_fname.replace('/', '-'))
509
510 def GetObjdumpFile(self, commit_upto, target, elf_fname):
511 """Get the name of the objdump file for a commit number and ELF file
512
513 Args:
514 commit_upto: Commit number to use (0..self.count-1)
515 target: Target name
516 elf_fname: Filename of elf image
517 """
518 return os.path.join(self.GetBuildDir(commit_upto, target),
519 '%s.objdump' % elf_fname.replace('/', '-'))
520
521 def GetErrFile(self, commit_upto, target):
522 """Get the name of the err file for a commit number
523
524 Args:
525 commit_upto: Commit number to use (0..self.count-1)
526 target: Target name
527 """
528 output_dir = self.GetBuildDir(commit_upto, target)
529 return os.path.join(output_dir, 'err')
530
531 def FilterErrors(self, lines):
532 """Filter out errors in which we have no interest
533
534 We should probably use map().
535
536 Args:
537 lines: List of error lines, each a string
538 Returns:
539 New list with only interesting lines included
540 """
541 out_lines = []
542 for line in lines:
543 if not self.re_make_err.search(line):
544 out_lines.append(line)
545 return out_lines
546
547 def ReadFuncSizes(self, fname, fd):
548 """Read function sizes from the output of 'nm'
549
550 Args:
551 fd: File containing data to read
552 fname: Filename we are reading from (just for errors)
553
554 Returns:
555 Dictionary containing size of each function in bytes, indexed by
556 function name.
557 """
558 sym = {}
559 for line in fd.readlines():
560 try:
561 size, type, name = line[:-1].split()
562 except:
Simon Glass4433aa92014-09-05 19:00:07 -0600563 Print("Invalid line in file '%s': '%s'" % (fname, line[:-1]))
Simon Glassc05694f2013-04-03 11:07:16 +0000564 continue
565 if type in 'tTdDbB':
566 # function names begin with '.' on 64-bit powerpc
567 if '.' in name[1:]:
568 name = 'static.' + name.split('.')[0]
569 sym[name] = sym.get(name, 0) + int(size, 16)
570 return sym
571
Simon Glassdb17fb82015-02-05 22:06:15 -0700572 def _ProcessConfig(self, fname):
573 """Read in a .config, autoconf.mk or autoconf.h file
574
575 This function handles all config file types. It ignores comments and
576 any #defines which don't start with CONFIG_.
577
578 Args:
579 fname: Filename to read
580
581 Returns:
582 Dictionary:
583 key: Config name (e.g. CONFIG_DM)
584 value: Config value (e.g. 1)
585 """
586 config = {}
587 if os.path.exists(fname):
588 with open(fname) as fd:
589 for line in fd:
590 line = line.strip()
591 if line.startswith('#define'):
592 values = line[8:].split(' ', 1)
593 if len(values) > 1:
594 key, value = values
595 else:
596 key = values[0]
Simon Glasscde5c302016-11-13 14:25:53 -0700597 value = '1' if self.squash_config_y else ''
Simon Glassdb17fb82015-02-05 22:06:15 -0700598 if not key.startswith('CONFIG_'):
599 continue
600 elif not line or line[0] in ['#', '*', '/']:
601 continue
602 else:
603 key, value = line.split('=', 1)
Simon Glasscde5c302016-11-13 14:25:53 -0700604 if self.squash_config_y and value == 'y':
605 value = '1'
Simon Glassdb17fb82015-02-05 22:06:15 -0700606 config[key] = value
607 return config
608
609 def GetBuildOutcome(self, commit_upto, target, read_func_sizes,
610 read_config):
Simon Glassc05694f2013-04-03 11:07:16 +0000611 """Work out the outcome of a build.
612
613 Args:
614 commit_upto: Commit number to check (0..n-1)
615 target: Target board to check
616 read_func_sizes: True to read function size information
Simon Glassdb17fb82015-02-05 22:06:15 -0700617 read_config: True to read .config and autoconf.h files
Simon Glassc05694f2013-04-03 11:07:16 +0000618
619 Returns:
620 Outcome object
621 """
622 done_file = self.GetDoneFile(commit_upto, target)
623 sizes_file = self.GetSizesFile(commit_upto, target)
624 sizes = {}
625 func_sizes = {}
Simon Glassdb17fb82015-02-05 22:06:15 -0700626 config = {}
Simon Glassc05694f2013-04-03 11:07:16 +0000627 if os.path.exists(done_file):
628 with open(done_file, 'r') as fd:
629 return_code = int(fd.readline())
630 err_lines = []
631 err_file = self.GetErrFile(commit_upto, target)
632 if os.path.exists(err_file):
633 with open(err_file, 'r') as fd:
634 err_lines = self.FilterErrors(fd.readlines())
635
636 # Decide whether the build was ok, failed or created warnings
637 if return_code:
638 rc = OUTCOME_ERROR
639 elif len(err_lines):
640 rc = OUTCOME_WARNING
641 else:
642 rc = OUTCOME_OK
643
644 # Convert size information to our simple format
645 if os.path.exists(sizes_file):
646 with open(sizes_file, 'r') as fd:
647 for line in fd.readlines():
648 values = line.split()
649 rodata = 0
650 if len(values) > 6:
651 rodata = int(values[6], 16)
652 size_dict = {
653 'all' : int(values[0]) + int(values[1]) +
654 int(values[2]),
655 'text' : int(values[0]) - rodata,
656 'data' : int(values[1]),
657 'bss' : int(values[2]),
658 'rodata' : rodata,
659 }
660 sizes[values[5]] = size_dict
661
662 if read_func_sizes:
663 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
664 for fname in glob.glob(pattern):
665 with open(fname, 'r') as fd:
666 dict_name = os.path.basename(fname).replace('.sizes',
667 '')
668 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
669
Simon Glassdb17fb82015-02-05 22:06:15 -0700670 if read_config:
671 output_dir = self.GetBuildDir(commit_upto, target)
Simon Glasscde5c302016-11-13 14:25:53 -0700672 for name in self.config_filenames:
Simon Glassdb17fb82015-02-05 22:06:15 -0700673 fname = os.path.join(output_dir, name)
674 config[name] = self._ProcessConfig(fname)
675
676 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config)
Simon Glassc05694f2013-04-03 11:07:16 +0000677
Simon Glassdb17fb82015-02-05 22:06:15 -0700678 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {})
Simon Glassc05694f2013-04-03 11:07:16 +0000679
Simon Glassdb17fb82015-02-05 22:06:15 -0700680 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
681 read_config):
Simon Glassc05694f2013-04-03 11:07:16 +0000682 """Calculate a summary of the results of building a commit.
683
684 Args:
685 board_selected: Dict containing boards to summarise
686 commit_upto: Commit number to summarize (0..self.count-1)
687 read_func_sizes: True to read function size information
Simon Glassdb17fb82015-02-05 22:06:15 -0700688 read_config: True to read .config and autoconf.h files
Simon Glassc05694f2013-04-03 11:07:16 +0000689
690 Returns:
691 Tuple:
692 Dict containing boards which passed building this commit.
693 keyed by board.target
Simon Glass03749d42014-08-28 09:43:44 -0600694 List containing a summary of error lines
Simon Glass3394c9f2014-08-28 09:43:43 -0600695 Dict keyed by error line, containing a list of the Board
696 objects with that error
Simon Glass03749d42014-08-28 09:43:44 -0600697 List containing a summary of warning lines
698 Dict keyed by error line, containing a list of the Board
699 objects with that warning
Simon Glasscad8abf2015-08-25 21:52:14 -0600700 Dictionary keyed by board.target. Each value is a dictionary:
701 key: filename - e.g. '.config'
Simon Glassdb17fb82015-02-05 22:06:15 -0700702 value is itself a dictionary:
703 key: config name
704 value: config value
Simon Glassc05694f2013-04-03 11:07:16 +0000705 """
Simon Glass03749d42014-08-28 09:43:44 -0600706 def AddLine(lines_summary, lines_boards, line, board):
707 line = line.rstrip()
708 if line in lines_boards:
709 lines_boards[line].append(board)
710 else:
711 lines_boards[line] = [board]
712 lines_summary.append(line)
713
Simon Glassc05694f2013-04-03 11:07:16 +0000714 board_dict = {}
715 err_lines_summary = []
Simon Glass3394c9f2014-08-28 09:43:43 -0600716 err_lines_boards = {}
Simon Glass03749d42014-08-28 09:43:44 -0600717 warn_lines_summary = []
718 warn_lines_boards = {}
Simon Glassdb17fb82015-02-05 22:06:15 -0700719 config = {}
Simon Glassc05694f2013-04-03 11:07:16 +0000720
721 for board in boards_selected.itervalues():
722 outcome = self.GetBuildOutcome(commit_upto, board.target,
Simon Glassdb17fb82015-02-05 22:06:15 -0700723 read_func_sizes, read_config)
Simon Glassc05694f2013-04-03 11:07:16 +0000724 board_dict[board.target] = outcome
Simon Glass03749d42014-08-28 09:43:44 -0600725 last_func = None
726 last_was_warning = False
727 for line in outcome.err_lines:
728 if line:
729 if (self._re_function.match(line) or
730 self._re_files.match(line)):
731 last_func = line
Simon Glass3394c9f2014-08-28 09:43:43 -0600732 else:
Simon Glass03749d42014-08-28 09:43:44 -0600733 is_warning = self._re_warning.match(line)
734 is_note = self._re_note.match(line)
735 if is_warning or (last_was_warning and is_note):
736 if last_func:
737 AddLine(warn_lines_summary, warn_lines_boards,
738 last_func, board)
739 AddLine(warn_lines_summary, warn_lines_boards,
740 line, board)
741 else:
742 if last_func:
743 AddLine(err_lines_summary, err_lines_boards,
744 last_func, board)
745 AddLine(err_lines_summary, err_lines_boards,
746 line, board)
747 last_was_warning = is_warning
748 last_func = None
Simon Glasscde5c302016-11-13 14:25:53 -0700749 tconfig = Config(self.config_filenames, board.target)
750 for fname in self.config_filenames:
Simon Glassdb17fb82015-02-05 22:06:15 -0700751 if outcome.config:
752 for key, value in outcome.config[fname].iteritems():
Simon Glasscad8abf2015-08-25 21:52:14 -0600753 tconfig.Add(fname, key, value)
754 config[board.target] = tconfig
Simon Glassdb17fb82015-02-05 22:06:15 -0700755
Simon Glass03749d42014-08-28 09:43:44 -0600756 return (board_dict, err_lines_summary, err_lines_boards,
Simon Glassdb17fb82015-02-05 22:06:15 -0700757 warn_lines_summary, warn_lines_boards, config)
Simon Glassc05694f2013-04-03 11:07:16 +0000758
759 def AddOutcome(self, board_dict, arch_list, changes, char, color):
760 """Add an output to our list of outcomes for each architecture
761
762 This simple function adds failing boards (changes) to the
763 relevant architecture string, so we can print the results out
764 sorted by architecture.
765
766 Args:
767 board_dict: Dict containing all boards
768 arch_list: Dict keyed by arch name. Value is a string containing
769 a list of board names which failed for that arch.
770 changes: List of boards to add to arch_list
771 color: terminal.Colour object
772 """
773 done_arch = {}
774 for target in changes:
775 if target in board_dict:
776 arch = board_dict[target].arch
777 else:
778 arch = 'unknown'
779 str = self.col.Color(color, ' ' + target)
780 if not arch in done_arch:
Simon Glass26f72372015-02-05 22:06:11 -0700781 str = ' %s %s' % (self.col.Color(color, char), str)
Simon Glassc05694f2013-04-03 11:07:16 +0000782 done_arch[arch] = True
783 if not arch in arch_list:
784 arch_list[arch] = str
785 else:
786 arch_list[arch] += str
787
788
789 def ColourNum(self, num):
790 color = self.col.RED if num > 0 else self.col.GREEN
791 if num == 0:
792 return '0'
793 return self.col.Color(color, str(num))
794
795 def ResetResultSummary(self, board_selected):
796 """Reset the results summary ready for use.
797
798 Set up the base board list to be all those selected, and set the
799 error lines to empty.
800
801 Following this, calls to PrintResultSummary() will use this
802 information to work out what has changed.
803
804 Args:
805 board_selected: Dict containing boards to summarise, keyed by
806 board.target
807 """
808 self._base_board_dict = {}
809 for board in board_selected:
Simon Glassdb17fb82015-02-05 22:06:15 -0700810 self._base_board_dict[board] = Builder.Outcome(0, [], [], {}, {})
Simon Glassc05694f2013-04-03 11:07:16 +0000811 self._base_err_lines = []
Simon Glass03749d42014-08-28 09:43:44 -0600812 self._base_warn_lines = []
813 self._base_err_line_boards = {}
814 self._base_warn_line_boards = {}
Simon Glasscad8abf2015-08-25 21:52:14 -0600815 self._base_config = None
Simon Glassc05694f2013-04-03 11:07:16 +0000816
817 def PrintFuncSizeDetail(self, fname, old, new):
818 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
819 delta, common = [], {}
820
821 for a in old:
822 if a in new:
823 common[a] = 1
824
825 for name in old:
826 if name not in common:
827 remove += 1
828 down += old[name]
829 delta.append([-old[name], name])
830
831 for name in new:
832 if name not in common:
833 add += 1
834 up += new[name]
835 delta.append([new[name], name])
836
837 for name in common:
838 diff = new.get(name, 0) - old.get(name, 0)
839 if diff > 0:
840 grow, up = grow + 1, up + diff
841 elif diff < 0:
842 shrink, down = shrink + 1, down - diff
843 delta.append([diff, name])
844
845 delta.sort()
846 delta.reverse()
847
848 args = [add, -remove, grow, -shrink, up, -down, up - down]
849 if max(args) == 0:
850 return
851 args = [self.ColourNum(x) for x in args]
852 indent = ' ' * 15
Simon Glass4433aa92014-09-05 19:00:07 -0600853 Print('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
854 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
855 Print('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
856 'delta'))
Simon Glassc05694f2013-04-03 11:07:16 +0000857 for diff, name in delta:
858 if diff:
859 color = self.col.RED if diff > 0 else self.col.GREEN
860 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
861 old.get(name, '-'), new.get(name,'-'), diff)
Simon Glass4433aa92014-09-05 19:00:07 -0600862 Print(msg, colour=color)
Simon Glassc05694f2013-04-03 11:07:16 +0000863
864
865 def PrintSizeDetail(self, target_list, show_bloat):
866 """Show details size information for each board
867
868 Args:
869 target_list: List of targets, each a dict containing:
870 'target': Target name
871 'total_diff': Total difference in bytes across all areas
872 <part_name>: Difference for that part
873 show_bloat: Show detail for each function
874 """
875 targets_by_diff = sorted(target_list, reverse=True,
876 key=lambda x: x['_total_diff'])
877 for result in targets_by_diff:
878 printed_target = False
879 for name in sorted(result):
880 diff = result[name]
881 if name.startswith('_'):
882 continue
883 if diff != 0:
884 color = self.col.RED if diff > 0 else self.col.GREEN
885 msg = ' %s %+d' % (name, diff)
886 if not printed_target:
Simon Glass4433aa92014-09-05 19:00:07 -0600887 Print('%10s %-15s:' % ('', result['_target']),
888 newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +0000889 printed_target = True
Simon Glass4433aa92014-09-05 19:00:07 -0600890 Print(msg, colour=color, newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +0000891 if printed_target:
Simon Glass4433aa92014-09-05 19:00:07 -0600892 Print()
Simon Glassc05694f2013-04-03 11:07:16 +0000893 if show_bloat:
894 target = result['_target']
895 outcome = result['_outcome']
896 base_outcome = self._base_board_dict[target]
897 for fname in outcome.func_sizes:
898 self.PrintFuncSizeDetail(fname,
899 base_outcome.func_sizes[fname],
900 outcome.func_sizes[fname])
901
902
903 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
904 show_bloat):
905 """Print a summary of image sizes broken down by section.
906
907 The summary takes the form of one line per architecture. The
908 line contains deltas for each of the sections (+ means the section
909 got bigger, - means smaller). The nunmbers are the average number
910 of bytes that a board in this section increased by.
911
912 For example:
913 powerpc: (622 boards) text -0.0
914 arm: (285 boards) text -0.0
915 nds32: (3 boards) text -8.0
916
917 Args:
918 board_selected: Dict containing boards to summarise, keyed by
919 board.target
920 board_dict: Dict containing boards for which we built this
921 commit, keyed by board.target. The value is an Outcome object.
922 show_detail: Show detail for each board
923 show_bloat: Show detail for each function
924 """
925 arch_list = {}
926 arch_count = {}
927
928 # Calculate changes in size for different image parts
929 # The previous sizes are in Board.sizes, for each board
930 for target in board_dict:
931 if target not in board_selected:
932 continue
933 base_sizes = self._base_board_dict[target].sizes
934 outcome = board_dict[target]
935 sizes = outcome.sizes
936
937 # Loop through the list of images, creating a dict of size
938 # changes for each image/part. We end up with something like
939 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
940 # which means that U-Boot data increased by 5 bytes and SPL
941 # text decreased by 4.
942 err = {'_target' : target}
943 for image in sizes:
944 if image in base_sizes:
945 base_image = base_sizes[image]
946 # Loop through the text, data, bss parts
947 for part in sorted(sizes[image]):
948 diff = sizes[image][part] - base_image[part]
949 col = None
950 if diff:
951 if image == 'u-boot':
952 name = part
953 else:
954 name = image + ':' + part
955 err[name] = diff
956 arch = board_selected[target].arch
957 if not arch in arch_count:
958 arch_count[arch] = 1
959 else:
960 arch_count[arch] += 1
961 if not sizes:
962 pass # Only add to our list when we have some stats
963 elif not arch in arch_list:
964 arch_list[arch] = [err]
965 else:
966 arch_list[arch].append(err)
967
968 # We now have a list of image size changes sorted by arch
969 # Print out a summary of these
970 for arch, target_list in arch_list.iteritems():
971 # Get total difference for each type
972 totals = {}
973 for result in target_list:
974 total = 0
975 for name, diff in result.iteritems():
976 if name.startswith('_'):
977 continue
978 total += diff
979 if name in totals:
980 totals[name] += diff
981 else:
982 totals[name] = diff
983 result['_total_diff'] = total
984 result['_outcome'] = board_dict[result['_target']]
985
986 count = len(target_list)
987 printed_arch = False
988 for name in sorted(totals):
989 diff = totals[name]
990 if diff:
991 # Display the average difference in this name for this
992 # architecture
993 avg_diff = float(diff) / count
994 color = self.col.RED if avg_diff > 0 else self.col.GREEN
995 msg = ' %s %+1.1f' % (name, avg_diff)
996 if not printed_arch:
Simon Glass4433aa92014-09-05 19:00:07 -0600997 Print('%10s: (for %d/%d boards)' % (arch, count,
998 arch_count[arch]), newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +0000999 printed_arch = True
Simon Glass4433aa92014-09-05 19:00:07 -06001000 Print(msg, colour=color, newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +00001001
1002 if printed_arch:
Simon Glass4433aa92014-09-05 19:00:07 -06001003 Print()
Simon Glassc05694f2013-04-03 11:07:16 +00001004 if show_detail:
1005 self.PrintSizeDetail(target_list, show_bloat)
1006
1007
1008 def PrintResultSummary(self, board_selected, board_dict, err_lines,
Simon Glass03749d42014-08-28 09:43:44 -06001009 err_line_boards, warn_lines, warn_line_boards,
Simon Glassdb17fb82015-02-05 22:06:15 -07001010 config, show_sizes, show_detail, show_bloat,
1011 show_config):
Simon Glassc05694f2013-04-03 11:07:16 +00001012 """Compare results with the base results and display delta.
1013
1014 Only boards mentioned in board_selected will be considered. This
1015 function is intended to be called repeatedly with the results of
1016 each commit. It therefore shows a 'diff' between what it saw in
1017 the last call and what it sees now.
1018
1019 Args:
1020 board_selected: Dict containing boards to summarise, keyed by
1021 board.target
1022 board_dict: Dict containing boards for which we built this
1023 commit, keyed by board.target. The value is an Outcome object.
1024 err_lines: A list of errors for this commit, or [] if there is
1025 none, or we don't want to print errors
Simon Glass3394c9f2014-08-28 09:43:43 -06001026 err_line_boards: Dict keyed by error line, containing a list of
1027 the Board objects with that error
Simon Glass03749d42014-08-28 09:43:44 -06001028 warn_lines: A list of warnings for this commit, or [] if there is
1029 none, or we don't want to print errors
1030 warn_line_boards: Dict keyed by warning line, containing a list of
1031 the Board objects with that warning
Simon Glassdb17fb82015-02-05 22:06:15 -07001032 config: Dictionary keyed by filename - e.g. '.config'. Each
1033 value is itself a dictionary:
1034 key: config name
1035 value: config value
Simon Glassc05694f2013-04-03 11:07:16 +00001036 show_sizes: Show image size deltas
1037 show_detail: Show detail for each board
1038 show_bloat: Show detail for each function
Simon Glassdb17fb82015-02-05 22:06:15 -07001039 show_config: Show config changes
Simon Glassc05694f2013-04-03 11:07:16 +00001040 """
Simon Glass03749d42014-08-28 09:43:44 -06001041 def _BoardList(line, line_boards):
Simon Glass3394c9f2014-08-28 09:43:43 -06001042 """Helper function to get a line of boards containing a line
1043
1044 Args:
1045 line: Error line to search for
1046 Return:
1047 String containing a list of boards with that error line, or
1048 '' if the user has not requested such a list
1049 """
1050 if self._list_error_boards:
1051 names = []
Simon Glass03749d42014-08-28 09:43:44 -06001052 for board in line_boards[line]:
Simon Glass85620bb2014-10-16 01:05:55 -06001053 if not board.target in names:
1054 names.append(board.target)
Simon Glass3394c9f2014-08-28 09:43:43 -06001055 names_str = '(%s) ' % ','.join(names)
1056 else:
1057 names_str = ''
1058 return names_str
1059
Simon Glass03749d42014-08-28 09:43:44 -06001060 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1061 char):
1062 better_lines = []
1063 worse_lines = []
1064 for line in lines:
1065 if line not in base_lines:
1066 worse_lines.append(char + '+' +
1067 _BoardList(line, line_boards) + line)
1068 for line in base_lines:
1069 if line not in lines:
1070 better_lines.append(char + '-' +
1071 _BoardList(line, base_line_boards) + line)
1072 return better_lines, worse_lines
1073
Simon Glassdb17fb82015-02-05 22:06:15 -07001074 def _CalcConfig(delta, name, config):
1075 """Calculate configuration changes
1076
1077 Args:
1078 delta: Type of the delta, e.g. '+'
1079 name: name of the file which changed (e.g. .config)
1080 config: configuration change dictionary
1081 key: config name
1082 value: config value
1083 Returns:
1084 String containing the configuration changes which can be
1085 printed
1086 """
1087 out = ''
1088 for key in sorted(config.keys()):
1089 out += '%s=%s ' % (key, config[key])
Simon Glasscad8abf2015-08-25 21:52:14 -06001090 return '%s %s: %s' % (delta, name, out)
Simon Glassdb17fb82015-02-05 22:06:15 -07001091
Simon Glasscad8abf2015-08-25 21:52:14 -06001092 def _AddConfig(lines, name, config_plus, config_minus, config_change):
1093 """Add changes in configuration to a list
Simon Glassdb17fb82015-02-05 22:06:15 -07001094
1095 Args:
Simon Glasscad8abf2015-08-25 21:52:14 -06001096 lines: list to add to
1097 name: config file name
Simon Glassdb17fb82015-02-05 22:06:15 -07001098 config_plus: configurations added, dictionary
1099 key: config name
1100 value: config value
1101 config_minus: configurations removed, dictionary
1102 key: config name
1103 value: config value
1104 config_change: configurations changed, dictionary
1105 key: config name
1106 value: config value
1107 """
1108 if config_plus:
Simon Glasscad8abf2015-08-25 21:52:14 -06001109 lines.append(_CalcConfig('+', name, config_plus))
Simon Glassdb17fb82015-02-05 22:06:15 -07001110 if config_minus:
Simon Glasscad8abf2015-08-25 21:52:14 -06001111 lines.append(_CalcConfig('-', name, config_minus))
Simon Glassdb17fb82015-02-05 22:06:15 -07001112 if config_change:
Simon Glasscad8abf2015-08-25 21:52:14 -06001113 lines.append(_CalcConfig('c', name, config_change))
1114
1115 def _OutputConfigInfo(lines):
1116 for line in lines:
1117 if not line:
1118 continue
1119 if line[0] == '+':
1120 col = self.col.GREEN
1121 elif line[0] == '-':
1122 col = self.col.RED
1123 elif line[0] == 'c':
1124 col = self.col.YELLOW
1125 Print(' ' + line, newline=True, colour=col)
1126
Simon Glassdb17fb82015-02-05 22:06:15 -07001127
Simon Glassc05694f2013-04-03 11:07:16 +00001128 better = [] # List of boards fixed since last commit
1129 worse = [] # List of new broken boards since last commit
1130 new = [] # List of boards that didn't exist last time
1131 unknown = [] # List of boards that were not built
1132
1133 for target in board_dict:
1134 if target not in board_selected:
1135 continue
1136
1137 # If the board was built last time, add its outcome to a list
1138 if target in self._base_board_dict:
1139 base_outcome = self._base_board_dict[target].rc
1140 outcome = board_dict[target]
1141 if outcome.rc == OUTCOME_UNKNOWN:
1142 unknown.append(target)
1143 elif outcome.rc < base_outcome:
1144 better.append(target)
1145 elif outcome.rc > base_outcome:
1146 worse.append(target)
1147 else:
1148 new.append(target)
1149
1150 # Get a list of errors that have appeared, and disappeared
Simon Glass03749d42014-08-28 09:43:44 -06001151 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1152 self._base_err_line_boards, err_lines, err_line_boards, '')
1153 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1154 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
Simon Glassc05694f2013-04-03 11:07:16 +00001155
1156 # Display results by arch
Simon Glass03749d42014-08-28 09:43:44 -06001157 if (better or worse or unknown or new or worse_err or better_err
1158 or worse_warn or better_warn):
Simon Glassc05694f2013-04-03 11:07:16 +00001159 arch_list = {}
1160 self.AddOutcome(board_selected, arch_list, better, '',
1161 self.col.GREEN)
1162 self.AddOutcome(board_selected, arch_list, worse, '+',
1163 self.col.RED)
1164 self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE)
1165 if self._show_unknown:
1166 self.AddOutcome(board_selected, arch_list, unknown, '?',
1167 self.col.MAGENTA)
1168 for arch, target_list in arch_list.iteritems():
Simon Glass4433aa92014-09-05 19:00:07 -06001169 Print('%10s: %s' % (arch, target_list))
Simon Glassbb4dffb2014-08-09 15:33:06 -06001170 self._error_lines += 1
Simon Glassc05694f2013-04-03 11:07:16 +00001171 if better_err:
Simon Glass4433aa92014-09-05 19:00:07 -06001172 Print('\n'.join(better_err), colour=self.col.GREEN)
Simon Glassbb4dffb2014-08-09 15:33:06 -06001173 self._error_lines += 1
Simon Glassc05694f2013-04-03 11:07:16 +00001174 if worse_err:
Simon Glass4433aa92014-09-05 19:00:07 -06001175 Print('\n'.join(worse_err), colour=self.col.RED)
Simon Glassbb4dffb2014-08-09 15:33:06 -06001176 self._error_lines += 1
Simon Glass03749d42014-08-28 09:43:44 -06001177 if better_warn:
Simon Glass4433aa92014-09-05 19:00:07 -06001178 Print('\n'.join(better_warn), colour=self.col.CYAN)
Simon Glass03749d42014-08-28 09:43:44 -06001179 self._error_lines += 1
1180 if worse_warn:
Simon Glass4433aa92014-09-05 19:00:07 -06001181 Print('\n'.join(worse_warn), colour=self.col.MAGENTA)
Simon Glass03749d42014-08-28 09:43:44 -06001182 self._error_lines += 1
Simon Glassc05694f2013-04-03 11:07:16 +00001183
1184 if show_sizes:
1185 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1186 show_bloat)
1187
Simon Glasscad8abf2015-08-25 21:52:14 -06001188 if show_config and self._base_config:
1189 summary = {}
1190 arch_config_plus = {}
1191 arch_config_minus = {}
1192 arch_config_change = {}
1193 arch_list = []
1194
1195 for target in board_dict:
1196 if target not in board_selected:
1197 continue
1198 arch = board_selected[target].arch
1199 if arch not in arch_list:
1200 arch_list.append(arch)
1201
1202 for arch in arch_list:
1203 arch_config_plus[arch] = {}
1204 arch_config_minus[arch] = {}
1205 arch_config_change[arch] = {}
Simon Glasscde5c302016-11-13 14:25:53 -07001206 for name in self.config_filenames:
Simon Glasscad8abf2015-08-25 21:52:14 -06001207 arch_config_plus[arch][name] = {}
1208 arch_config_minus[arch][name] = {}
1209 arch_config_change[arch][name] = {}
1210
1211 for target in board_dict:
1212 if target not in board_selected:
Simon Glassdb17fb82015-02-05 22:06:15 -07001213 continue
Simon Glasscad8abf2015-08-25 21:52:14 -06001214
1215 arch = board_selected[target].arch
1216
1217 all_config_plus = {}
1218 all_config_minus = {}
1219 all_config_change = {}
1220 tbase = self._base_config[target]
1221 tconfig = config[target]
1222 lines = []
Simon Glasscde5c302016-11-13 14:25:53 -07001223 for name in self.config_filenames:
Simon Glasscad8abf2015-08-25 21:52:14 -06001224 if not tconfig.config[name]:
1225 continue
1226 config_plus = {}
1227 config_minus = {}
1228 config_change = {}
1229 base = tbase.config[name]
1230 for key, value in tconfig.config[name].iteritems():
1231 if key not in base:
1232 config_plus[key] = value
1233 all_config_plus[key] = value
1234 for key, value in base.iteritems():
1235 if key not in tconfig.config[name]:
1236 config_minus[key] = value
1237 all_config_minus[key] = value
1238 for key, value in base.iteritems():
1239 new_value = tconfig.config.get(key)
1240 if new_value and value != new_value:
1241 desc = '%s -> %s' % (value, new_value)
1242 config_change[key] = desc
1243 all_config_change[key] = desc
1244
1245 arch_config_plus[arch][name].update(config_plus)
1246 arch_config_minus[arch][name].update(config_minus)
1247 arch_config_change[arch][name].update(config_change)
1248
1249 _AddConfig(lines, name, config_plus, config_minus,
1250 config_change)
1251 _AddConfig(lines, 'all', all_config_plus, all_config_minus,
1252 all_config_change)
1253 summary[target] = '\n'.join(lines)
1254
1255 lines_by_target = {}
1256 for target, lines in summary.iteritems():
1257 if lines in lines_by_target:
1258 lines_by_target[lines].append(target)
1259 else:
1260 lines_by_target[lines] = [target]
1261
1262 for arch in arch_list:
1263 lines = []
1264 all_plus = {}
1265 all_minus = {}
1266 all_change = {}
Simon Glasscde5c302016-11-13 14:25:53 -07001267 for name in self.config_filenames:
Simon Glasscad8abf2015-08-25 21:52:14 -06001268 all_plus.update(arch_config_plus[arch][name])
1269 all_minus.update(arch_config_minus[arch][name])
1270 all_change.update(arch_config_change[arch][name])
1271 _AddConfig(lines, name, arch_config_plus[arch][name],
1272 arch_config_minus[arch][name],
1273 arch_config_change[arch][name])
1274 _AddConfig(lines, 'all', all_plus, all_minus, all_change)
1275 #arch_summary[target] = '\n'.join(lines)
1276 if lines:
1277 Print('%s:' % arch)
1278 _OutputConfigInfo(lines)
1279
1280 for lines, targets in lines_by_target.iteritems():
1281 if not lines:
1282 continue
1283 Print('%s :' % ' '.join(sorted(targets)))
1284 _OutputConfigInfo(lines.split('\n'))
1285
Simon Glassdb17fb82015-02-05 22:06:15 -07001286
Simon Glassc05694f2013-04-03 11:07:16 +00001287 # Save our updated information for the next call to this function
1288 self._base_board_dict = board_dict
1289 self._base_err_lines = err_lines
Simon Glass03749d42014-08-28 09:43:44 -06001290 self._base_warn_lines = warn_lines
1291 self._base_err_line_boards = err_line_boards
1292 self._base_warn_line_boards = warn_line_boards
Simon Glassdb17fb82015-02-05 22:06:15 -07001293 self._base_config = config
Simon Glassc05694f2013-04-03 11:07:16 +00001294
1295 # Get a list of boards that did not get built, if needed
1296 not_built = []
1297 for board in board_selected:
1298 if not board in board_dict:
1299 not_built.append(board)
1300 if not_built:
Simon Glass4433aa92014-09-05 19:00:07 -06001301 Print("Boards not built (%d): %s" % (len(not_built),
1302 ', '.join(not_built)))
Simon Glassc05694f2013-04-03 11:07:16 +00001303
Simon Glasseb48bbc2014-08-09 15:33:02 -06001304 def ProduceResultSummary(self, commit_upto, commits, board_selected):
Simon Glass03749d42014-08-28 09:43:44 -06001305 (board_dict, err_lines, err_line_boards, warn_lines,
Simon Glassdb17fb82015-02-05 22:06:15 -07001306 warn_line_boards, config) = self.GetResultSummary(
Simon Glass3394c9f2014-08-28 09:43:43 -06001307 board_selected, commit_upto,
Simon Glassdb17fb82015-02-05 22:06:15 -07001308 read_func_sizes=self._show_bloat,
1309 read_config=self._show_config)
Simon Glasseb48bbc2014-08-09 15:33:02 -06001310 if commits:
1311 msg = '%02d: %s' % (commit_upto + 1,
1312 commits[commit_upto].subject)
Simon Glass4433aa92014-09-05 19:00:07 -06001313 Print(msg, colour=self.col.BLUE)
Simon Glasseb48bbc2014-08-09 15:33:02 -06001314 self.PrintResultSummary(board_selected, board_dict,
Simon Glass3394c9f2014-08-28 09:43:43 -06001315 err_lines if self._show_errors else [], err_line_boards,
Simon Glass03749d42014-08-28 09:43:44 -06001316 warn_lines if self._show_errors else [], warn_line_boards,
Simon Glassdb17fb82015-02-05 22:06:15 -07001317 config, self._show_sizes, self._show_detail,
1318 self._show_bloat, self._show_config)
Simon Glassc05694f2013-04-03 11:07:16 +00001319
Simon Glasseb48bbc2014-08-09 15:33:02 -06001320 def ShowSummary(self, commits, board_selected):
Simon Glassc05694f2013-04-03 11:07:16 +00001321 """Show a build summary for U-Boot for a given board list.
1322
1323 Reset the result summary, then repeatedly call GetResultSummary on
1324 each commit's results, then display the differences we see.
1325
1326 Args:
1327 commit: Commit objects to summarise
1328 board_selected: Dict containing boards to summarise
Simon Glassc05694f2013-04-03 11:07:16 +00001329 """
Simon Glassd326ad72014-08-09 15:32:59 -06001330 self.commit_count = len(commits) if commits else 1
Simon Glassc05694f2013-04-03 11:07:16 +00001331 self.commits = commits
1332 self.ResetResultSummary(board_selected)
Simon Glassbb4dffb2014-08-09 15:33:06 -06001333 self._error_lines = 0
Simon Glassc05694f2013-04-03 11:07:16 +00001334
1335 for commit_upto in range(0, self.commit_count, self._step):
Simon Glasseb48bbc2014-08-09 15:33:02 -06001336 self.ProduceResultSummary(commit_upto, commits, board_selected)
Simon Glassbb4dffb2014-08-09 15:33:06 -06001337 if not self._error_lines:
Simon Glass4433aa92014-09-05 19:00:07 -06001338 Print('(no errors to report)', colour=self.col.GREEN)
Simon Glassc05694f2013-04-03 11:07:16 +00001339
1340
1341 def SetupBuild(self, board_selected, commits):
1342 """Set up ready to start a build.
1343
1344 Args:
1345 board_selected: Selected boards to build
1346 commits: Selected commits to build
1347 """
1348 # First work out how many commits we will build
Simon Glassd326ad72014-08-09 15:32:59 -06001349 count = (self.commit_count + self._step - 1) / self._step
Simon Glassc05694f2013-04-03 11:07:16 +00001350 self.count = len(board_selected) * count
1351 self.upto = self.warned = self.fail = 0
1352 self._timestamps = collections.deque()
1353
Simon Glassc05694f2013-04-03 11:07:16 +00001354 def GetThreadDir(self, thread_num):
1355 """Get the directory path to the working dir for a thread.
1356
1357 Args:
1358 thread_num: Number of thread to check.
1359 """
1360 return os.path.join(self._working_dir, '%02d' % thread_num)
1361
Simon Glassd326ad72014-08-09 15:32:59 -06001362 def _PrepareThread(self, thread_num, setup_git):
Simon Glassc05694f2013-04-03 11:07:16 +00001363 """Prepare the working directory for a thread.
1364
1365 This clones or fetches the repo into the thread's work directory.
1366
1367 Args:
1368 thread_num: Thread number (0, 1, ...)
Simon Glassd326ad72014-08-09 15:32:59 -06001369 setup_git: True to set up a git repo clone
Simon Glassc05694f2013-04-03 11:07:16 +00001370 """
1371 thread_dir = self.GetThreadDir(thread_num)
Simon Glass4a1e88b2014-08-09 15:33:00 -06001372 builderthread.Mkdir(thread_dir)
Simon Glassc05694f2013-04-03 11:07:16 +00001373 git_dir = os.path.join(thread_dir, '.git')
1374
1375 # Clone the repo if it doesn't already exist
1376 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1377 # we have a private index but uses the origin repo's contents?
Simon Glassd326ad72014-08-09 15:32:59 -06001378 if setup_git and self.git_dir:
Simon Glassc05694f2013-04-03 11:07:16 +00001379 src_dir = os.path.abspath(self.git_dir)
1380 if os.path.exists(git_dir):
1381 gitutil.Fetch(git_dir, thread_dir)
1382 else:
Simon Glass2e2a6e62016-09-18 16:48:31 -06001383 Print('\rCloning repo for thread %d' % thread_num,
1384 newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +00001385 gitutil.Clone(src_dir, thread_dir)
Simon Glass2e2a6e62016-09-18 16:48:31 -06001386 Print('\r%s\r' % (' ' * 30), newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +00001387
Simon Glassd326ad72014-08-09 15:32:59 -06001388 def _PrepareWorkingSpace(self, max_threads, setup_git):
Simon Glassc05694f2013-04-03 11:07:16 +00001389 """Prepare the working directory for use.
1390
1391 Set up the git repo for each thread.
1392
1393 Args:
1394 max_threads: Maximum number of threads we expect to need.
Simon Glassd326ad72014-08-09 15:32:59 -06001395 setup_git: True to set up a git repo clone
Simon Glassc05694f2013-04-03 11:07:16 +00001396 """
Simon Glass4a1e88b2014-08-09 15:33:00 -06001397 builderthread.Mkdir(self._working_dir)
Simon Glassc05694f2013-04-03 11:07:16 +00001398 for thread in range(max_threads):
Simon Glassd326ad72014-08-09 15:32:59 -06001399 self._PrepareThread(thread, setup_git)
Simon Glassc05694f2013-04-03 11:07:16 +00001400
1401 def _PrepareOutputSpace(self):
1402 """Get the output directories ready to receive files.
1403
1404 We delete any output directories which look like ones we need to
1405 create. Having left over directories is confusing when the user wants
1406 to check the output manually.
1407 """
Simon Glassb9a9b7c2014-12-01 17:33:53 -07001408 if not self.commits:
1409 return
Simon Glassc05694f2013-04-03 11:07:16 +00001410 dir_list = []
1411 for commit_upto in range(self.commit_count):
1412 dir_list.append(self._GetOutputDir(commit_upto))
1413
Simon Glass83cb6cc2016-09-18 16:48:32 -06001414 to_remove = []
Simon Glassc05694f2013-04-03 11:07:16 +00001415 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1416 if dirname not in dir_list:
Simon Glass83cb6cc2016-09-18 16:48:32 -06001417 to_remove.append(dirname)
1418 if to_remove:
1419 Print('Removing %d old build directories' % len(to_remove),
1420 newline=False)
1421 for dirname in to_remove:
Simon Glassc05694f2013-04-03 11:07:16 +00001422 shutil.rmtree(dirname)
1423
Simon Glass78e418e2014-08-09 15:33:03 -06001424 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
Simon Glassc05694f2013-04-03 11:07:16 +00001425 """Build all commits for a list of boards
1426
1427 Args:
1428 commits: List of commits to be build, each a Commit object
1429 boards_selected: Dict of selected boards, key is target name,
1430 value is Board object
Simon Glassc05694f2013-04-03 11:07:16 +00001431 keep_outputs: True to save build output files
Simon Glass78e418e2014-08-09 15:33:03 -06001432 verbose: Display build results as they are completed
Simon Glassc2f91072014-08-28 09:43:39 -06001433 Returns:
1434 Tuple containing:
1435 - number of boards that failed to build
1436 - number of boards that issued warnings
Simon Glassc05694f2013-04-03 11:07:16 +00001437 """
Simon Glassd326ad72014-08-09 15:32:59 -06001438 self.commit_count = len(commits) if commits else 1
Simon Glassc05694f2013-04-03 11:07:16 +00001439 self.commits = commits
Simon Glass78e418e2014-08-09 15:33:03 -06001440 self._verbose = verbose
Simon Glassc05694f2013-04-03 11:07:16 +00001441
1442 self.ResetResultSummary(board_selected)
Thierry Reding336d5bd2014-08-19 10:22:39 +02001443 builderthread.Mkdir(self.base_dir, parents = True)
Simon Glassd326ad72014-08-09 15:32:59 -06001444 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1445 commits is not None)
Simon Glassc05694f2013-04-03 11:07:16 +00001446 self._PrepareOutputSpace()
Simon Glasse0f23602016-09-18 16:48:33 -06001447 Print('\rStarting build...', newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +00001448 self.SetupBuild(board_selected, commits)
1449 self.ProcessResult(None)
1450
1451 # Create jobs to build all commits for each board
1452 for brd in board_selected.itervalues():
Simon Glass4a1e88b2014-08-09 15:33:00 -06001453 job = builderthread.BuilderJob()
Simon Glassc05694f2013-04-03 11:07:16 +00001454 job.board = brd
1455 job.commits = commits
1456 job.keep_outputs = keep_outputs
1457 job.step = self._step
1458 self.queue.put(job)
1459
Simon Glassd26e1442016-09-18 16:48:35 -06001460 term = threading.Thread(target=self.queue.join)
1461 term.setDaemon(True)
1462 term.start()
1463 while term.isAlive():
1464 term.join(100)
Simon Glassc05694f2013-04-03 11:07:16 +00001465
1466 # Wait until we have processed all output
1467 self.out_queue.join()
Simon Glass4433aa92014-09-05 19:00:07 -06001468 Print()
Simon Glassc05694f2013-04-03 11:07:16 +00001469 self.ClearLine(0)
Simon Glassc2f91072014-08-28 09:43:39 -06001470 return (self.fail, self.warned)