]> git.ipfire.org Git - people/ms/u-boot.git/blame - tools/buildman/builder.py
buildman: add option -E for treating compiler warnings as errors
[people/ms/u-boot.git] / tools / buildman / builder.py
CommitLineData
fc3fe1c2
SG
1# Copyright (c) 2013 The Chromium OS Authors.
2#
3# Bloat-o-meter code used here Copyright 2004 Matt Mackall <mpm@selenic.com>
4#
1a459660 5# SPDX-License-Identifier: GPL-2.0+
fc3fe1c2
SG
6#
7
8import collections
fc3fe1c2
SG
9from datetime import datetime, timedelta
10import glob
11import os
12import re
13import Queue
14import shutil
2f256648 15import signal
fc3fe1c2
SG
16import string
17import sys
d436e381 18import threading
fc3fe1c2
SG
19import time
20
190064b4 21import builderthread
fc3fe1c2
SG
22import command
23import gitutil
24import terminal
4653a882 25from terminal import Print
fc3fe1c2
SG
26import 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
9a6d2e2a
SG
98# Translate a commit subject into a valid filename (and handle unicode)
99trans_valid_chars = string.maketrans('/: ', '---')
100trans_valid_chars = trans_valid_chars.decode('latin-1')
fc3fe1c2 101
b464f8e7
SG
102BASE_CONFIG_FILENAMES = [
103 'u-boot.cfg', 'u-boot-spl.cfg', 'u-boot-tpl.cfg'
104]
105
106EXTRA_CONFIG_FILENAMES = [
843312dc
SG
107 '.config', '.config-spl', '.config-tpl',
108 'autoconf.mk', 'autoconf-spl.mk', 'autoconf-tpl.mk',
109 'autoconf.h', 'autoconf-spl.h','autoconf-tpl.h',
843312dc
SG
110]
111
8270e3c1
SG
112class Config:
113 """Holds information about configuration settings for a board."""
b464f8e7 114 def __init__(self, config_filename, target):
8270e3c1
SG
115 self.target = target
116 self.config = {}
b464f8e7 117 for fname in config_filename:
8270e3c1
SG
118 self.config[fname] = {}
119
120 def Add(self, fname, key, value):
121 self.config[fname][key] = value
122
123 def __hash__(self):
124 val = 0
125 for fname in self.config:
126 for key, value in self.config[fname].iteritems():
127 print key, value
128 val = val ^ hash(key) & hash(value)
129 return val
fc3fe1c2 130
fc3fe1c2
SG
131class Builder:
132 """Class for building U-Boot for a particular commit.
133
134 Public members: (many should ->private)
fc3fe1c2
SG
135 already_done: Number of builds already completed
136 base_dir: Base directory to use for builder
137 checkout: True to check out source, False to skip that step.
138 This is used for testing.
139 col: terminal.Color() object
140 count: Number of commits to build
141 do_make: Method to call to invoke Make
142 fail: Number of builds that failed due to error
143 force_build: Force building even if a build already exists
144 force_config_on_failure: If a commit fails for a board, disable
145 incremental building for the next commit we build for that
146 board, so that we will see all warnings/errors again.
4266dc28
SG
147 force_build_failures: If a previously-built build (i.e. built on
148 a previous run of buildman) is marked as failed, rebuild it.
fc3fe1c2
SG
149 git_dir: Git directory containing source repository
150 last_line_len: Length of the last line we printed (used for erasing
151 it with new progress information)
152 num_jobs: Number of jobs to run at once (passed to make as -j)
153 num_threads: Number of builder threads to run
154 out_queue: Queue of results to process
155 re_make_err: Compiled regular expression for ignore_lines
156 queue: Queue of jobs to run
157 threads: List of active threads
158 toolchains: Toolchains object to use for building
159 upto: Current commit number we are building (0.count-1)
160 warned: Number of builds that produced at least one warning
97e91526
SG
161 force_reconfig: Reconfigure U-Boot on each comiit. This disables
162 incremental building, where buildman reconfigures on the first
163 commit for a baord, and then just does an incremental build for
164 the following commits. In fact buildman will reconfigure and
165 retry for any failing commits, so generally the only effect of
166 this option is to slow things down.
189a4968
SG
167 in_tree: Build U-Boot in-tree instead of specifying an output
168 directory separate from the source code. This option is really
169 only useful for testing in-tree builds.
fc3fe1c2
SG
170
171 Private members:
172 _base_board_dict: Last-summarised Dict of boards
173 _base_err_lines: Last-summarised list of errors
e30965db 174 _base_warn_lines: Last-summarised list of warnings
fc3fe1c2
SG
175 _build_period_us: Time taken for a single build (float object).
176 _complete_delay: Expected delay until completion (timedelta)
177 _next_delay_update: Next time we plan to display a progress update
178 (datatime)
179 _show_unknown: Show unknown boards (those not built) in summary
180 _timestamps: List of timestamps for the completion of the last
181 last _timestamp_count builds. Each is a datetime object.
182 _timestamp_count: Number of timestamps to keep in our list.
183 _working_dir: Base working directory containing all threads
184 """
185 class Outcome:
186 """Records a build outcome for a single make invocation
187
188 Public Members:
189 rc: Outcome value (OUTCOME_...)
190 err_lines: List of error lines or [] if none
191 sizes: Dictionary of image size information, keyed by filename
192 - Each value is itself a dictionary containing
193 values for 'text', 'data' and 'bss', being the integer
194 size in bytes of each section.
195 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
196 value is itself a dictionary:
197 key: function name
198 value: Size of function in bytes
843312dc
SG
199 config: Dictionary keyed by filename - e.g. '.config'. Each
200 value is itself a dictionary:
201 key: config name
202 value: config value
fc3fe1c2 203 """
843312dc 204 def __init__(self, rc, err_lines, sizes, func_sizes, config):
fc3fe1c2
SG
205 self.rc = rc
206 self.err_lines = err_lines
207 self.sizes = sizes
208 self.func_sizes = func_sizes
843312dc 209 self.config = config
fc3fe1c2
SG
210
211 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
5971ab5c 212 gnu_make='make', checkout=True, show_unknown=True, step=1,
f79f1e0c 213 no_subdirs=False, full_path=False, verbose_build=False,
b50113f3 214 incremental=False, per_board_out_dir=False,
2371d1bc
DS
215 config_only=False, squash_config_y=False,
216 warnings_as_errors=False):
fc3fe1c2
SG
217 """Create a new Builder object
218
219 Args:
220 toolchains: Toolchains object to use for building
221 base_dir: Base directory to use for builder
222 git_dir: Git directory containing source repository
223 num_threads: Number of builder threads to run
224 num_jobs: Number of jobs to run at once (passed to make as -j)
99796923 225 gnu_make: the command name of GNU Make.
fc3fe1c2
SG
226 checkout: True to check out source, False to skip that step.
227 This is used for testing.
228 show_unknown: Show unknown boards (those not built) in summary
229 step: 1 to process every commit, n to process every nth commit
bb1501f2
SG
230 no_subdirs: Don't create subdirectories when building current
231 source for a single board
232 full_path: Return the full path in CROSS_COMPILE and don't set
233 PATH
d2ce658d 234 verbose_build: Run build with V=1 and don't use 'make -s'
f79f1e0c
SW
235 incremental: Always perform incremental builds; don't run make
236 mrproper when configuring
237 per_board_out_dir: Build in a separate persistent directory per
238 board rather than a thread-specific directory
b50113f3 239 config_only: Only configure each build, don't build it
b464f8e7 240 squash_config_y: Convert CONFIG options with the value 'y' to '1'
2371d1bc 241 warnings_as_errors: Treat all compiler warnings as errors
fc3fe1c2
SG
242 """
243 self.toolchains = toolchains
244 self.base_dir = base_dir
245 self._working_dir = os.path.join(base_dir, '.bm-work')
246 self.threads = []
fc3fe1c2 247 self.do_make = self.Make
99796923 248 self.gnu_make = gnu_make
fc3fe1c2
SG
249 self.checkout = checkout
250 self.num_threads = num_threads
251 self.num_jobs = num_jobs
252 self.already_done = 0
253 self.force_build = False
254 self.git_dir = git_dir
255 self._show_unknown = show_unknown
256 self._timestamp_count = 10
257 self._build_period_us = None
258 self._complete_delay = None
259 self._next_delay_update = datetime.now()
260 self.force_config_on_failure = True
4266dc28 261 self.force_build_failures = False
97e91526 262 self.force_reconfig = False
fc3fe1c2 263 self._step = step
189a4968 264 self.in_tree = False
28370c1b 265 self._error_lines = 0
5971ab5c 266 self.no_subdirs = no_subdirs
bb1501f2 267 self.full_path = full_path
d2ce658d 268 self.verbose_build = verbose_build
b50113f3 269 self.config_only = config_only
b464f8e7
SG
270 self.squash_config_y = squash_config_y
271 self.config_filenames = BASE_CONFIG_FILENAMES
272 if not self.squash_config_y:
273 self.config_filenames += EXTRA_CONFIG_FILENAMES
fc3fe1c2 274
2371d1bc 275 self.warnings_as_errors = warnings_as_errors
fc3fe1c2
SG
276 self.col = terminal.Color()
277
e30965db
SG
278 self._re_function = re.compile('(.*): In function.*')
279 self._re_files = re.compile('In file included from.*')
280 self._re_warning = re.compile('(.*):(\d*):(\d*): warning: .*')
281 self._re_note = re.compile('(.*):(\d*):(\d*): note: this is the location of the previous.*')
282
fc3fe1c2
SG
283 self.queue = Queue.Queue()
284 self.out_queue = Queue.Queue()
285 for i in range(self.num_threads):
f79f1e0c
SW
286 t = builderthread.BuilderThread(self, i, incremental,
287 per_board_out_dir)
fc3fe1c2
SG
288 t.setDaemon(True)
289 t.start()
290 self.threads.append(t)
291
292 self.last_line_len = 0
190064b4 293 t = builderthread.ResultThread(self)
fc3fe1c2
SG
294 t.setDaemon(True)
295 t.start()
296 self.threads.append(t)
297
298 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
299 self.re_make_err = re.compile('|'.join(ignore_lines))
300
2f256648
SG
301 # Handle existing graceful with SIGINT / Ctrl-C
302 signal.signal(signal.SIGINT, self.signal_handler)
303
fc3fe1c2
SG
304 def __del__(self):
305 """Get rid of all threads created by the builder"""
306 for t in self.threads:
307 del t
308
2f256648
SG
309 def signal_handler(self, signal, frame):
310 sys.exit(1)
311
b2ea7ab2 312 def SetDisplayOptions(self, show_errors=False, show_sizes=False,
ed966657 313 show_detail=False, show_bloat=False,
843312dc 314 list_error_boards=False, show_config=False):
b2ea7ab2
SG
315 """Setup display options for the builder.
316
317 show_errors: True to show summarised error/warning info
318 show_sizes: Show size deltas
319 show_detail: Show detail for each board
320 show_bloat: Show detail for each function
ed966657 321 list_error_boards: Show the boards which caused each error/warning
843312dc 322 show_config: Show config deltas
b2ea7ab2
SG
323 """
324 self._show_errors = show_errors
325 self._show_sizes = show_sizes
326 self._show_detail = show_detail
327 self._show_bloat = show_bloat
ed966657 328 self._list_error_boards = list_error_boards
843312dc 329 self._show_config = show_config
b2ea7ab2 330
fc3fe1c2
SG
331 def _AddTimestamp(self):
332 """Add a new timestamp to the list and record the build period.
333
334 The build period is the length of time taken to perform a single
335 build (one board, one commit).
336 """
337 now = datetime.now()
338 self._timestamps.append(now)
339 count = len(self._timestamps)
340 delta = self._timestamps[-1] - self._timestamps[0]
341 seconds = delta.total_seconds()
342
343 # If we have enough data, estimate build period (time taken for a
344 # single build) and therefore completion time.
345 if count > 1 and self._next_delay_update < now:
346 self._next_delay_update = now + timedelta(seconds=2)
347 if seconds > 0:
348 self._build_period = float(seconds) / count
349 todo = self.count - self.upto
350 self._complete_delay = timedelta(microseconds=
351 self._build_period * todo * 1000000)
352 # Round it
353 self._complete_delay -= timedelta(
354 microseconds=self._complete_delay.microseconds)
355
356 if seconds > 60:
357 self._timestamps.popleft()
358 count -= 1
359
360 def ClearLine(self, length):
361 """Clear any characters on the current line
362
363 Make way for a new line of length 'length', by outputting enough
364 spaces to clear out the old line. Then remember the new length for
365 next time.
366
367 Args:
368 length: Length of new line, in characters
369 """
370 if length < self.last_line_len:
4653a882
SG
371 Print(' ' * (self.last_line_len - length), newline=False)
372 Print('\r', newline=False)
fc3fe1c2
SG
373 self.last_line_len = length
374 sys.stdout.flush()
375
376 def SelectCommit(self, commit, checkout=True):
377 """Checkout the selected commit for this build
378 """
379 self.commit = commit
380 if checkout and self.checkout:
381 gitutil.Checkout(commit.hash)
382
383 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
384 """Run make
385
386 Args:
387 commit: Commit object that is being built
388 brd: Board object that is being built
fd18a89e 389 stage: Stage that we are at (mrproper, config, build)
fc3fe1c2
SG
390 cwd: Directory where make should be run
391 args: Arguments to pass to make
392 kwargs: Arguments to pass to command.RunPipe()
393 """
99796923 394 cmd = [self.gnu_make] + list(args)
fc3fe1c2
SG
395 result = command.RunPipe([cmd], capture=True, capture_stderr=True,
396 cwd=cwd, raise_on_error=False, **kwargs)
40f11fce
SG
397 if self.verbose_build:
398 result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout
399 result.combined = '%s\n' % (' '.join(cmd)) + result.combined
fc3fe1c2
SG
400 return result
401
402 def ProcessResult(self, result):
403 """Process the result of a build, showing progress information
404
405 Args:
e5a0e5d8
SG
406 result: A CommandResult object, which indicates the result for
407 a single build
fc3fe1c2
SG
408 """
409 col = terminal.Color()
410 if result:
411 target = result.brd.target
412
fc3fe1c2
SG
413 self.upto += 1
414 if result.return_code != 0:
415 self.fail += 1
416 elif result.stderr:
417 self.warned += 1
418 if result.already_done:
419 self.already_done += 1
e5a0e5d8 420 if self._verbose:
4653a882 421 Print('\r', newline=False)
e5a0e5d8
SG
422 self.ClearLine(0)
423 boards_selected = {target : result.brd}
424 self.ResetResultSummary(boards_selected)
425 self.ProduceResultSummary(result.commit_upto, self.commits,
426 boards_selected)
fc3fe1c2
SG
427 else:
428 target = '(starting)'
429
430 # Display separate counts for ok, warned and fail
431 ok = self.upto - self.warned - self.fail
432 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
433 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
434 line += self.col.Color(self.col.RED, '%5d' % self.fail)
435
436 name = ' /%-5d ' % self.count
437
438 # Add our current completion time estimate
439 self._AddTimestamp()
440 if self._complete_delay:
441 name += '%s : ' % self._complete_delay
442 # When building all boards for a commit, we can print a commit
443 # progress message.
444 if result and result.commit_upto is None:
445 name += 'commit %2d/%-3d' % (self.commit_upto + 1,
446 self.commit_count)
447
448 name += target
4653a882 449 Print(line + name, newline=False)
960421ec 450 length = 16 + len(name)
fc3fe1c2
SG
451 self.ClearLine(length)
452
453 def _GetOutputDir(self, commit_upto):
454 """Get the name of the output directory for a commit number
455
456 The output directory is typically .../<branch>/<commit>.
457
458 Args:
459 commit_upto: Commit number to use (0..self.count-1)
460 """
5971ab5c 461 commit_dir = None
fea5858e
SG
462 if self.commits:
463 commit = self.commits[commit_upto]
464 subject = commit.subject.translate(trans_valid_chars)
465 commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
466 self.commit_count, commit.hash, subject[:20]))
5971ab5c 467 elif not self.no_subdirs:
fea5858e 468 commit_dir = 'current'
5971ab5c
SG
469 if not commit_dir:
470 return self.base_dir
471 return os.path.join(self.base_dir, commit_dir)
fc3fe1c2
SG
472
473 def GetBuildDir(self, commit_upto, target):
474 """Get the name of the build directory for a commit number
475
476 The build directory is typically .../<branch>/<commit>/<target>.
477
478 Args:
479 commit_upto: Commit number to use (0..self.count-1)
480 target: Target name
481 """
482 output_dir = self._GetOutputDir(commit_upto)
483 return os.path.join(output_dir, target)
484
485 def GetDoneFile(self, commit_upto, target):
486 """Get the name of the done file for a commit number
487
488 Args:
489 commit_upto: Commit number to use (0..self.count-1)
490 target: Target name
491 """
492 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
493
494 def GetSizesFile(self, commit_upto, target):
495 """Get the name of the sizes file for a commit number
496
497 Args:
498 commit_upto: Commit number to use (0..self.count-1)
499 target: Target name
500 """
501 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
502
503 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
504 """Get the name of the funcsizes file for a commit number and ELF file
505
506 Args:
507 commit_upto: Commit number to use (0..self.count-1)
508 target: Target name
509 elf_fname: Filename of elf image
510 """
511 return os.path.join(self.GetBuildDir(commit_upto, target),
512 '%s.sizes' % elf_fname.replace('/', '-'))
513
514 def GetObjdumpFile(self, commit_upto, target, elf_fname):
515 """Get the name of the objdump file for a commit number and ELF file
516
517 Args:
518 commit_upto: Commit number to use (0..self.count-1)
519 target: Target name
520 elf_fname: Filename of elf image
521 """
522 return os.path.join(self.GetBuildDir(commit_upto, target),
523 '%s.objdump' % elf_fname.replace('/', '-'))
524
525 def GetErrFile(self, commit_upto, target):
526 """Get the name of the err file for a commit number
527
528 Args:
529 commit_upto: Commit number to use (0..self.count-1)
530 target: Target name
531 """
532 output_dir = self.GetBuildDir(commit_upto, target)
533 return os.path.join(output_dir, 'err')
534
535 def FilterErrors(self, lines):
536 """Filter out errors in which we have no interest
537
538 We should probably use map().
539
540 Args:
541 lines: List of error lines, each a string
542 Returns:
543 New list with only interesting lines included
544 """
545 out_lines = []
546 for line in lines:
547 if not self.re_make_err.search(line):
548 out_lines.append(line)
549 return out_lines
550
551 def ReadFuncSizes(self, fname, fd):
552 """Read function sizes from the output of 'nm'
553
554 Args:
555 fd: File containing data to read
556 fname: Filename we are reading from (just for errors)
557
558 Returns:
559 Dictionary containing size of each function in bytes, indexed by
560 function name.
561 """
562 sym = {}
563 for line in fd.readlines():
564 try:
565 size, type, name = line[:-1].split()
566 except:
4653a882 567 Print("Invalid line in file '%s': '%s'" % (fname, line[:-1]))
fc3fe1c2
SG
568 continue
569 if type in 'tTdDbB':
570 # function names begin with '.' on 64-bit powerpc
571 if '.' in name[1:]:
572 name = 'static.' + name.split('.')[0]
573 sym[name] = sym.get(name, 0) + int(size, 16)
574 return sym
575
843312dc
SG
576 def _ProcessConfig(self, fname):
577 """Read in a .config, autoconf.mk or autoconf.h file
578
579 This function handles all config file types. It ignores comments and
580 any #defines which don't start with CONFIG_.
581
582 Args:
583 fname: Filename to read
584
585 Returns:
586 Dictionary:
587 key: Config name (e.g. CONFIG_DM)
588 value: Config value (e.g. 1)
589 """
590 config = {}
591 if os.path.exists(fname):
592 with open(fname) as fd:
593 for line in fd:
594 line = line.strip()
595 if line.startswith('#define'):
596 values = line[8:].split(' ', 1)
597 if len(values) > 1:
598 key, value = values
599 else:
600 key = values[0]
b464f8e7 601 value = '1' if self.squash_config_y else ''
843312dc
SG
602 if not key.startswith('CONFIG_'):
603 continue
604 elif not line or line[0] in ['#', '*', '/']:
605 continue
606 else:
607 key, value = line.split('=', 1)
b464f8e7
SG
608 if self.squash_config_y and value == 'y':
609 value = '1'
843312dc
SG
610 config[key] = value
611 return config
612
613 def GetBuildOutcome(self, commit_upto, target, read_func_sizes,
614 read_config):
fc3fe1c2
SG
615 """Work out the outcome of a build.
616
617 Args:
618 commit_upto: Commit number to check (0..n-1)
619 target: Target board to check
620 read_func_sizes: True to read function size information
843312dc 621 read_config: True to read .config and autoconf.h files
fc3fe1c2
SG
622
623 Returns:
624 Outcome object
625 """
626 done_file = self.GetDoneFile(commit_upto, target)
627 sizes_file = self.GetSizesFile(commit_upto, target)
628 sizes = {}
629 func_sizes = {}
843312dc 630 config = {}
fc3fe1c2
SG
631 if os.path.exists(done_file):
632 with open(done_file, 'r') as fd:
633 return_code = int(fd.readline())
634 err_lines = []
635 err_file = self.GetErrFile(commit_upto, target)
636 if os.path.exists(err_file):
637 with open(err_file, 'r') as fd:
638 err_lines = self.FilterErrors(fd.readlines())
639
640 # Decide whether the build was ok, failed or created warnings
641 if return_code:
642 rc = OUTCOME_ERROR
643 elif len(err_lines):
644 rc = OUTCOME_WARNING
645 else:
646 rc = OUTCOME_OK
647
648 # Convert size information to our simple format
649 if os.path.exists(sizes_file):
650 with open(sizes_file, 'r') as fd:
651 for line in fd.readlines():
652 values = line.split()
653 rodata = 0
654 if len(values) > 6:
655 rodata = int(values[6], 16)
656 size_dict = {
657 'all' : int(values[0]) + int(values[1]) +
658 int(values[2]),
659 'text' : int(values[0]) - rodata,
660 'data' : int(values[1]),
661 'bss' : int(values[2]),
662 'rodata' : rodata,
663 }
664 sizes[values[5]] = size_dict
665
666 if read_func_sizes:
667 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
668 for fname in glob.glob(pattern):
669 with open(fname, 'r') as fd:
670 dict_name = os.path.basename(fname).replace('.sizes',
671 '')
672 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
673
843312dc
SG
674 if read_config:
675 output_dir = self.GetBuildDir(commit_upto, target)
b464f8e7 676 for name in self.config_filenames:
843312dc
SG
677 fname = os.path.join(output_dir, name)
678 config[name] = self._ProcessConfig(fname)
679
680 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config)
fc3fe1c2 681
843312dc 682 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {})
fc3fe1c2 683
843312dc
SG
684 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
685 read_config):
fc3fe1c2
SG
686 """Calculate a summary of the results of building a commit.
687
688 Args:
689 board_selected: Dict containing boards to summarise
690 commit_upto: Commit number to summarize (0..self.count-1)
691 read_func_sizes: True to read function size information
843312dc 692 read_config: True to read .config and autoconf.h files
fc3fe1c2
SG
693
694 Returns:
695 Tuple:
696 Dict containing boards which passed building this commit.
697 keyed by board.target
e30965db 698 List containing a summary of error lines
ed966657
SG
699 Dict keyed by error line, containing a list of the Board
700 objects with that error
e30965db
SG
701 List containing a summary of warning lines
702 Dict keyed by error line, containing a list of the Board
703 objects with that warning
8270e3c1
SG
704 Dictionary keyed by board.target. Each value is a dictionary:
705 key: filename - e.g. '.config'
843312dc
SG
706 value is itself a dictionary:
707 key: config name
708 value: config value
fc3fe1c2 709 """
e30965db
SG
710 def AddLine(lines_summary, lines_boards, line, board):
711 line = line.rstrip()
712 if line in lines_boards:
713 lines_boards[line].append(board)
714 else:
715 lines_boards[line] = [board]
716 lines_summary.append(line)
717
fc3fe1c2
SG
718 board_dict = {}
719 err_lines_summary = []
ed966657 720 err_lines_boards = {}
e30965db
SG
721 warn_lines_summary = []
722 warn_lines_boards = {}
843312dc 723 config = {}
fc3fe1c2
SG
724
725 for board in boards_selected.itervalues():
726 outcome = self.GetBuildOutcome(commit_upto, board.target,
843312dc 727 read_func_sizes, read_config)
fc3fe1c2 728 board_dict[board.target] = outcome
e30965db
SG
729 last_func = None
730 last_was_warning = False
731 for line in outcome.err_lines:
732 if line:
733 if (self._re_function.match(line) or
734 self._re_files.match(line)):
735 last_func = line
ed966657 736 else:
e30965db
SG
737 is_warning = self._re_warning.match(line)
738 is_note = self._re_note.match(line)
739 if is_warning or (last_was_warning and is_note):
740 if last_func:
741 AddLine(warn_lines_summary, warn_lines_boards,
742 last_func, board)
743 AddLine(warn_lines_summary, warn_lines_boards,
744 line, board)
745 else:
746 if last_func:
747 AddLine(err_lines_summary, err_lines_boards,
748 last_func, board)
749 AddLine(err_lines_summary, err_lines_boards,
750 line, board)
751 last_was_warning = is_warning
752 last_func = None
b464f8e7
SG
753 tconfig = Config(self.config_filenames, board.target)
754 for fname in self.config_filenames:
843312dc
SG
755 if outcome.config:
756 for key, value in outcome.config[fname].iteritems():
8270e3c1
SG
757 tconfig.Add(fname, key, value)
758 config[board.target] = tconfig
843312dc 759
e30965db 760 return (board_dict, err_lines_summary, err_lines_boards,
843312dc 761 warn_lines_summary, warn_lines_boards, config)
fc3fe1c2
SG
762
763 def AddOutcome(self, board_dict, arch_list, changes, char, color):
764 """Add an output to our list of outcomes for each architecture
765
766 This simple function adds failing boards (changes) to the
767 relevant architecture string, so we can print the results out
768 sorted by architecture.
769
770 Args:
771 board_dict: Dict containing all boards
772 arch_list: Dict keyed by arch name. Value is a string containing
773 a list of board names which failed for that arch.
774 changes: List of boards to add to arch_list
775 color: terminal.Colour object
776 """
777 done_arch = {}
778 for target in changes:
779 if target in board_dict:
780 arch = board_dict[target].arch
781 else:
782 arch = 'unknown'
783 str = self.col.Color(color, ' ' + target)
784 if not arch in done_arch:
63c619ee 785 str = ' %s %s' % (self.col.Color(color, char), str)
fc3fe1c2
SG
786 done_arch[arch] = True
787 if not arch in arch_list:
788 arch_list[arch] = str
789 else:
790 arch_list[arch] += str
791
792
793 def ColourNum(self, num):
794 color = self.col.RED if num > 0 else self.col.GREEN
795 if num == 0:
796 return '0'
797 return self.col.Color(color, str(num))
798
799 def ResetResultSummary(self, board_selected):
800 """Reset the results summary ready for use.
801
802 Set up the base board list to be all those selected, and set the
803 error lines to empty.
804
805 Following this, calls to PrintResultSummary() will use this
806 information to work out what has changed.
807
808 Args:
809 board_selected: Dict containing boards to summarise, keyed by
810 board.target
811 """
812 self._base_board_dict = {}
813 for board in board_selected:
843312dc 814 self._base_board_dict[board] = Builder.Outcome(0, [], [], {}, {})
fc3fe1c2 815 self._base_err_lines = []
e30965db
SG
816 self._base_warn_lines = []
817 self._base_err_line_boards = {}
818 self._base_warn_line_boards = {}
8270e3c1 819 self._base_config = None
fc3fe1c2
SG
820
821 def PrintFuncSizeDetail(self, fname, old, new):
822 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
823 delta, common = [], {}
824
825 for a in old:
826 if a in new:
827 common[a] = 1
828
829 for name in old:
830 if name not in common:
831 remove += 1
832 down += old[name]
833 delta.append([-old[name], name])
834
835 for name in new:
836 if name not in common:
837 add += 1
838 up += new[name]
839 delta.append([new[name], name])
840
841 for name in common:
842 diff = new.get(name, 0) - old.get(name, 0)
843 if diff > 0:
844 grow, up = grow + 1, up + diff
845 elif diff < 0:
846 shrink, down = shrink + 1, down - diff
847 delta.append([diff, name])
848
849 delta.sort()
850 delta.reverse()
851
852 args = [add, -remove, grow, -shrink, up, -down, up - down]
d5686a61 853 if max(args) == 0 and min(args) == 0:
fc3fe1c2
SG
854 return
855 args = [self.ColourNum(x) for x in args]
856 indent = ' ' * 15
4653a882
SG
857 Print('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
858 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
859 Print('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
860 'delta'))
fc3fe1c2
SG
861 for diff, name in delta:
862 if diff:
863 color = self.col.RED if diff > 0 else self.col.GREEN
864 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
865 old.get(name, '-'), new.get(name,'-'), diff)
4653a882 866 Print(msg, colour=color)
fc3fe1c2
SG
867
868
869 def PrintSizeDetail(self, target_list, show_bloat):
870 """Show details size information for each board
871
872 Args:
873 target_list: List of targets, each a dict containing:
874 'target': Target name
875 'total_diff': Total difference in bytes across all areas
876 <part_name>: Difference for that part
877 show_bloat: Show detail for each function
878 """
879 targets_by_diff = sorted(target_list, reverse=True,
880 key=lambda x: x['_total_diff'])
881 for result in targets_by_diff:
882 printed_target = False
883 for name in sorted(result):
884 diff = result[name]
885 if name.startswith('_'):
886 continue
887 if diff != 0:
888 color = self.col.RED if diff > 0 else self.col.GREEN
889 msg = ' %s %+d' % (name, diff)
890 if not printed_target:
4653a882
SG
891 Print('%10s %-15s:' % ('', result['_target']),
892 newline=False)
fc3fe1c2 893 printed_target = True
4653a882 894 Print(msg, colour=color, newline=False)
fc3fe1c2 895 if printed_target:
4653a882 896 Print()
fc3fe1c2
SG
897 if show_bloat:
898 target = result['_target']
899 outcome = result['_outcome']
900 base_outcome = self._base_board_dict[target]
901 for fname in outcome.func_sizes:
902 self.PrintFuncSizeDetail(fname,
903 base_outcome.func_sizes[fname],
904 outcome.func_sizes[fname])
905
906
907 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
908 show_bloat):
909 """Print a summary of image sizes broken down by section.
910
911 The summary takes the form of one line per architecture. The
912 line contains deltas for each of the sections (+ means the section
913 got bigger, - means smaller). The nunmbers are the average number
914 of bytes that a board in this section increased by.
915
916 For example:
917 powerpc: (622 boards) text -0.0
918 arm: (285 boards) text -0.0
919 nds32: (3 boards) text -8.0
920
921 Args:
922 board_selected: Dict containing boards to summarise, keyed by
923 board.target
924 board_dict: Dict containing boards for which we built this
925 commit, keyed by board.target. The value is an Outcome object.
926 show_detail: Show detail for each board
927 show_bloat: Show detail for each function
928 """
929 arch_list = {}
930 arch_count = {}
931
932 # Calculate changes in size for different image parts
933 # The previous sizes are in Board.sizes, for each board
934 for target in board_dict:
935 if target not in board_selected:
936 continue
937 base_sizes = self._base_board_dict[target].sizes
938 outcome = board_dict[target]
939 sizes = outcome.sizes
940
941 # Loop through the list of images, creating a dict of size
942 # changes for each image/part. We end up with something like
943 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
944 # which means that U-Boot data increased by 5 bytes and SPL
945 # text decreased by 4.
946 err = {'_target' : target}
947 for image in sizes:
948 if image in base_sizes:
949 base_image = base_sizes[image]
950 # Loop through the text, data, bss parts
951 for part in sorted(sizes[image]):
952 diff = sizes[image][part] - base_image[part]
953 col = None
954 if diff:
955 if image == 'u-boot':
956 name = part
957 else:
958 name = image + ':' + part
959 err[name] = diff
960 arch = board_selected[target].arch
961 if not arch in arch_count:
962 arch_count[arch] = 1
963 else:
964 arch_count[arch] += 1
965 if not sizes:
966 pass # Only add to our list when we have some stats
967 elif not arch in arch_list:
968 arch_list[arch] = [err]
969 else:
970 arch_list[arch].append(err)
971
972 # We now have a list of image size changes sorted by arch
973 # Print out a summary of these
974 for arch, target_list in arch_list.iteritems():
975 # Get total difference for each type
976 totals = {}
977 for result in target_list:
978 total = 0
979 for name, diff in result.iteritems():
980 if name.startswith('_'):
981 continue
982 total += diff
983 if name in totals:
984 totals[name] += diff
985 else:
986 totals[name] = diff
987 result['_total_diff'] = total
988 result['_outcome'] = board_dict[result['_target']]
989
990 count = len(target_list)
991 printed_arch = False
992 for name in sorted(totals):
993 diff = totals[name]
994 if diff:
995 # Display the average difference in this name for this
996 # architecture
997 avg_diff = float(diff) / count
998 color = self.col.RED if avg_diff > 0 else self.col.GREEN
999 msg = ' %s %+1.1f' % (name, avg_diff)
1000 if not printed_arch:
4653a882
SG
1001 Print('%10s: (for %d/%d boards)' % (arch, count,
1002 arch_count[arch]), newline=False)
fc3fe1c2 1003 printed_arch = True
4653a882 1004 Print(msg, colour=color, newline=False)
fc3fe1c2
SG
1005
1006 if printed_arch:
4653a882 1007 Print()
fc3fe1c2
SG
1008 if show_detail:
1009 self.PrintSizeDetail(target_list, show_bloat)
1010
1011
1012 def PrintResultSummary(self, board_selected, board_dict, err_lines,
e30965db 1013 err_line_boards, warn_lines, warn_line_boards,
843312dc
SG
1014 config, show_sizes, show_detail, show_bloat,
1015 show_config):
fc3fe1c2
SG
1016 """Compare results with the base results and display delta.
1017
1018 Only boards mentioned in board_selected will be considered. This
1019 function is intended to be called repeatedly with the results of
1020 each commit. It therefore shows a 'diff' between what it saw in
1021 the last call and what it sees now.
1022
1023 Args:
1024 board_selected: Dict containing boards to summarise, keyed by
1025 board.target
1026 board_dict: Dict containing boards for which we built this
1027 commit, keyed by board.target. The value is an Outcome object.
1028 err_lines: A list of errors for this commit, or [] if there is
1029 none, or we don't want to print errors
ed966657
SG
1030 err_line_boards: Dict keyed by error line, containing a list of
1031 the Board objects with that error
e30965db
SG
1032 warn_lines: A list of warnings for this commit, or [] if there is
1033 none, or we don't want to print errors
1034 warn_line_boards: Dict keyed by warning line, containing a list of
1035 the Board objects with that warning
843312dc
SG
1036 config: Dictionary keyed by filename - e.g. '.config'. Each
1037 value is itself a dictionary:
1038 key: config name
1039 value: config value
fc3fe1c2
SG
1040 show_sizes: Show image size deltas
1041 show_detail: Show detail for each board
1042 show_bloat: Show detail for each function
843312dc 1043 show_config: Show config changes
fc3fe1c2 1044 """
e30965db 1045 def _BoardList(line, line_boards):
ed966657
SG
1046 """Helper function to get a line of boards containing a line
1047
1048 Args:
1049 line: Error line to search for
1050 Return:
1051 String containing a list of boards with that error line, or
1052 '' if the user has not requested such a list
1053 """
1054 if self._list_error_boards:
1055 names = []
e30965db 1056 for board in line_boards[line]:
f66153be
SG
1057 if not board.target in names:
1058 names.append(board.target)
ed966657
SG
1059 names_str = '(%s) ' % ','.join(names)
1060 else:
1061 names_str = ''
1062 return names_str
1063
e30965db
SG
1064 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1065 char):
1066 better_lines = []
1067 worse_lines = []
1068 for line in lines:
1069 if line not in base_lines:
1070 worse_lines.append(char + '+' +
1071 _BoardList(line, line_boards) + line)
1072 for line in base_lines:
1073 if line not in lines:
1074 better_lines.append(char + '-' +
1075 _BoardList(line, base_line_boards) + line)
1076 return better_lines, worse_lines
1077
843312dc
SG
1078 def _CalcConfig(delta, name, config):
1079 """Calculate configuration changes
1080
1081 Args:
1082 delta: Type of the delta, e.g. '+'
1083 name: name of the file which changed (e.g. .config)
1084 config: configuration change dictionary
1085 key: config name
1086 value: config value
1087 Returns:
1088 String containing the configuration changes which can be
1089 printed
1090 """
1091 out = ''
1092 for key in sorted(config.keys()):
1093 out += '%s=%s ' % (key, config[key])
8270e3c1 1094 return '%s %s: %s' % (delta, name, out)
843312dc 1095
8270e3c1
SG
1096 def _AddConfig(lines, name, config_plus, config_minus, config_change):
1097 """Add changes in configuration to a list
843312dc
SG
1098
1099 Args:
8270e3c1
SG
1100 lines: list to add to
1101 name: config file name
843312dc
SG
1102 config_plus: configurations added, dictionary
1103 key: config name
1104 value: config value
1105 config_minus: configurations removed, dictionary
1106 key: config name
1107 value: config value
1108 config_change: configurations changed, dictionary
1109 key: config name
1110 value: config value
1111 """
1112 if config_plus:
8270e3c1 1113 lines.append(_CalcConfig('+', name, config_plus))
843312dc 1114 if config_minus:
8270e3c1 1115 lines.append(_CalcConfig('-', name, config_minus))
843312dc 1116 if config_change:
8270e3c1
SG
1117 lines.append(_CalcConfig('c', name, config_change))
1118
1119 def _OutputConfigInfo(lines):
1120 for line in lines:
1121 if not line:
1122 continue
1123 if line[0] == '+':
1124 col = self.col.GREEN
1125 elif line[0] == '-':
1126 col = self.col.RED
1127 elif line[0] == 'c':
1128 col = self.col.YELLOW
1129 Print(' ' + line, newline=True, colour=col)
1130
843312dc 1131
fc3fe1c2
SG
1132 better = [] # List of boards fixed since last commit
1133 worse = [] # List of new broken boards since last commit
1134 new = [] # List of boards that didn't exist last time
1135 unknown = [] # List of boards that were not built
1136
1137 for target in board_dict:
1138 if target not in board_selected:
1139 continue
1140
1141 # If the board was built last time, add its outcome to a list
1142 if target in self._base_board_dict:
1143 base_outcome = self._base_board_dict[target].rc
1144 outcome = board_dict[target]
1145 if outcome.rc == OUTCOME_UNKNOWN:
1146 unknown.append(target)
1147 elif outcome.rc < base_outcome:
1148 better.append(target)
1149 elif outcome.rc > base_outcome:
1150 worse.append(target)
1151 else:
1152 new.append(target)
1153
1154 # Get a list of errors that have appeared, and disappeared
e30965db
SG
1155 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1156 self._base_err_line_boards, err_lines, err_line_boards, '')
1157 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1158 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
fc3fe1c2
SG
1159
1160 # Display results by arch
e30965db
SG
1161 if (better or worse or unknown or new or worse_err or better_err
1162 or worse_warn or better_warn):
fc3fe1c2
SG
1163 arch_list = {}
1164 self.AddOutcome(board_selected, arch_list, better, '',
1165 self.col.GREEN)
1166 self.AddOutcome(board_selected, arch_list, worse, '+',
1167 self.col.RED)
1168 self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE)
1169 if self._show_unknown:
1170 self.AddOutcome(board_selected, arch_list, unknown, '?',
1171 self.col.MAGENTA)
1172 for arch, target_list in arch_list.iteritems():
4653a882 1173 Print('%10s: %s' % (arch, target_list))
28370c1b 1174 self._error_lines += 1
fc3fe1c2 1175 if better_err:
4653a882 1176 Print('\n'.join(better_err), colour=self.col.GREEN)
28370c1b 1177 self._error_lines += 1
fc3fe1c2 1178 if worse_err:
4653a882 1179 Print('\n'.join(worse_err), colour=self.col.RED)
28370c1b 1180 self._error_lines += 1
e30965db 1181 if better_warn:
4653a882 1182 Print('\n'.join(better_warn), colour=self.col.CYAN)
e30965db
SG
1183 self._error_lines += 1
1184 if worse_warn:
4653a882 1185 Print('\n'.join(worse_warn), colour=self.col.MAGENTA)
e30965db 1186 self._error_lines += 1
fc3fe1c2
SG
1187
1188 if show_sizes:
1189 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1190 show_bloat)
1191
8270e3c1
SG
1192 if show_config and self._base_config:
1193 summary = {}
1194 arch_config_plus = {}
1195 arch_config_minus = {}
1196 arch_config_change = {}
1197 arch_list = []
1198
1199 for target in board_dict:
1200 if target not in board_selected:
1201 continue
1202 arch = board_selected[target].arch
1203 if arch not in arch_list:
1204 arch_list.append(arch)
1205
1206 for arch in arch_list:
1207 arch_config_plus[arch] = {}
1208 arch_config_minus[arch] = {}
1209 arch_config_change[arch] = {}
b464f8e7 1210 for name in self.config_filenames:
8270e3c1
SG
1211 arch_config_plus[arch][name] = {}
1212 arch_config_minus[arch][name] = {}
1213 arch_config_change[arch][name] = {}
1214
1215 for target in board_dict:
1216 if target not in board_selected:
843312dc 1217 continue
8270e3c1
SG
1218
1219 arch = board_selected[target].arch
1220
1221 all_config_plus = {}
1222 all_config_minus = {}
1223 all_config_change = {}
1224 tbase = self._base_config[target]
1225 tconfig = config[target]
1226 lines = []
b464f8e7 1227 for name in self.config_filenames:
8270e3c1
SG
1228 if not tconfig.config[name]:
1229 continue
1230 config_plus = {}
1231 config_minus = {}
1232 config_change = {}
1233 base = tbase.config[name]
1234 for key, value in tconfig.config[name].iteritems():
1235 if key not in base:
1236 config_plus[key] = value
1237 all_config_plus[key] = value
1238 for key, value in base.iteritems():
1239 if key not in tconfig.config[name]:
1240 config_minus[key] = value
1241 all_config_minus[key] = value
1242 for key, value in base.iteritems():
1243 new_value = tconfig.config.get(key)
1244 if new_value and value != new_value:
1245 desc = '%s -> %s' % (value, new_value)
1246 config_change[key] = desc
1247 all_config_change[key] = desc
1248
1249 arch_config_plus[arch][name].update(config_plus)
1250 arch_config_minus[arch][name].update(config_minus)
1251 arch_config_change[arch][name].update(config_change)
1252
1253 _AddConfig(lines, name, config_plus, config_minus,
1254 config_change)
1255 _AddConfig(lines, 'all', all_config_plus, all_config_minus,
1256 all_config_change)
1257 summary[target] = '\n'.join(lines)
1258
1259 lines_by_target = {}
1260 for target, lines in summary.iteritems():
1261 if lines in lines_by_target:
1262 lines_by_target[lines].append(target)
1263 else:
1264 lines_by_target[lines] = [target]
1265
1266 for arch in arch_list:
1267 lines = []
1268 all_plus = {}
1269 all_minus = {}
1270 all_change = {}
b464f8e7 1271 for name in self.config_filenames:
8270e3c1
SG
1272 all_plus.update(arch_config_plus[arch][name])
1273 all_minus.update(arch_config_minus[arch][name])
1274 all_change.update(arch_config_change[arch][name])
1275 _AddConfig(lines, name, arch_config_plus[arch][name],
1276 arch_config_minus[arch][name],
1277 arch_config_change[arch][name])
1278 _AddConfig(lines, 'all', all_plus, all_minus, all_change)
1279 #arch_summary[target] = '\n'.join(lines)
1280 if lines:
1281 Print('%s:' % arch)
1282 _OutputConfigInfo(lines)
1283
1284 for lines, targets in lines_by_target.iteritems():
1285 if not lines:
1286 continue
1287 Print('%s :' % ' '.join(sorted(targets)))
1288 _OutputConfigInfo(lines.split('\n'))
1289
843312dc 1290
fc3fe1c2
SG
1291 # Save our updated information for the next call to this function
1292 self._base_board_dict = board_dict
1293 self._base_err_lines = err_lines
e30965db
SG
1294 self._base_warn_lines = warn_lines
1295 self._base_err_line_boards = err_line_boards
1296 self._base_warn_line_boards = warn_line_boards
843312dc 1297 self._base_config = config
fc3fe1c2
SG
1298
1299 # Get a list of boards that did not get built, if needed
1300 not_built = []
1301 for board in board_selected:
1302 if not board in board_dict:
1303 not_built.append(board)
1304 if not_built:
4653a882
SG
1305 Print("Boards not built (%d): %s" % (len(not_built),
1306 ', '.join(not_built)))
fc3fe1c2 1307
b2ea7ab2 1308 def ProduceResultSummary(self, commit_upto, commits, board_selected):
e30965db 1309 (board_dict, err_lines, err_line_boards, warn_lines,
843312dc 1310 warn_line_boards, config) = self.GetResultSummary(
ed966657 1311 board_selected, commit_upto,
843312dc
SG
1312 read_func_sizes=self._show_bloat,
1313 read_config=self._show_config)
b2ea7ab2
SG
1314 if commits:
1315 msg = '%02d: %s' % (commit_upto + 1,
1316 commits[commit_upto].subject)
4653a882 1317 Print(msg, colour=self.col.BLUE)
b2ea7ab2 1318 self.PrintResultSummary(board_selected, board_dict,
ed966657 1319 err_lines if self._show_errors else [], err_line_boards,
e30965db 1320 warn_lines if self._show_errors else [], warn_line_boards,
843312dc
SG
1321 config, self._show_sizes, self._show_detail,
1322 self._show_bloat, self._show_config)
fc3fe1c2 1323
b2ea7ab2 1324 def ShowSummary(self, commits, board_selected):
fc3fe1c2
SG
1325 """Show a build summary for U-Boot for a given board list.
1326
1327 Reset the result summary, then repeatedly call GetResultSummary on
1328 each commit's results, then display the differences we see.
1329
1330 Args:
1331 commit: Commit objects to summarise
1332 board_selected: Dict containing boards to summarise
fc3fe1c2 1333 """
fea5858e 1334 self.commit_count = len(commits) if commits else 1
fc3fe1c2
SG
1335 self.commits = commits
1336 self.ResetResultSummary(board_selected)
28370c1b 1337 self._error_lines = 0
fc3fe1c2
SG
1338
1339 for commit_upto in range(0, self.commit_count, self._step):
b2ea7ab2 1340 self.ProduceResultSummary(commit_upto, commits, board_selected)
28370c1b 1341 if not self._error_lines:
4653a882 1342 Print('(no errors to report)', colour=self.col.GREEN)
fc3fe1c2
SG
1343
1344
1345 def SetupBuild(self, board_selected, commits):
1346 """Set up ready to start a build.
1347
1348 Args:
1349 board_selected: Selected boards to build
1350 commits: Selected commits to build
1351 """
1352 # First work out how many commits we will build
fea5858e 1353 count = (self.commit_count + self._step - 1) / self._step
fc3fe1c2
SG
1354 self.count = len(board_selected) * count
1355 self.upto = self.warned = self.fail = 0
1356 self._timestamps = collections.deque()
1357
fc3fe1c2
SG
1358 def GetThreadDir(self, thread_num):
1359 """Get the directory path to the working dir for a thread.
1360
1361 Args:
1362 thread_num: Number of thread to check.
1363 """
1364 return os.path.join(self._working_dir, '%02d' % thread_num)
1365
fea5858e 1366 def _PrepareThread(self, thread_num, setup_git):
fc3fe1c2
SG
1367 """Prepare the working directory for a thread.
1368
1369 This clones or fetches the repo into the thread's work directory.
1370
1371 Args:
1372 thread_num: Thread number (0, 1, ...)
fea5858e 1373 setup_git: True to set up a git repo clone
fc3fe1c2
SG
1374 """
1375 thread_dir = self.GetThreadDir(thread_num)
190064b4 1376 builderthread.Mkdir(thread_dir)
fc3fe1c2
SG
1377 git_dir = os.path.join(thread_dir, '.git')
1378
1379 # Clone the repo if it doesn't already exist
1380 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1381 # we have a private index but uses the origin repo's contents?
fea5858e 1382 if setup_git and self.git_dir:
fc3fe1c2
SG
1383 src_dir = os.path.abspath(self.git_dir)
1384 if os.path.exists(git_dir):
1385 gitutil.Fetch(git_dir, thread_dir)
1386 else:
21f0eb33
SG
1387 Print('\rCloning repo for thread %d' % thread_num,
1388 newline=False)
fc3fe1c2 1389 gitutil.Clone(src_dir, thread_dir)
21f0eb33 1390 Print('\r%s\r' % (' ' * 30), newline=False)
fc3fe1c2 1391
fea5858e 1392 def _PrepareWorkingSpace(self, max_threads, setup_git):
fc3fe1c2
SG
1393 """Prepare the working directory for use.
1394
1395 Set up the git repo for each thread.
1396
1397 Args:
1398 max_threads: Maximum number of threads we expect to need.
fea5858e 1399 setup_git: True to set up a git repo clone
fc3fe1c2 1400 """
190064b4 1401 builderthread.Mkdir(self._working_dir)
fc3fe1c2 1402 for thread in range(max_threads):
fea5858e 1403 self._PrepareThread(thread, setup_git)
fc3fe1c2
SG
1404
1405 def _PrepareOutputSpace(self):
1406 """Get the output directories ready to receive files.
1407
1408 We delete any output directories which look like ones we need to
1409 create. Having left over directories is confusing when the user wants
1410 to check the output manually.
1411 """
1a915675
SG
1412 if not self.commits:
1413 return
fc3fe1c2
SG
1414 dir_list = []
1415 for commit_upto in range(self.commit_count):
1416 dir_list.append(self._GetOutputDir(commit_upto))
1417
b222abe7 1418 to_remove = []
fc3fe1c2
SG
1419 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1420 if dirname not in dir_list:
b222abe7
SG
1421 to_remove.append(dirname)
1422 if to_remove:
1423 Print('Removing %d old build directories' % len(to_remove),
1424 newline=False)
1425 for dirname in to_remove:
fc3fe1c2
SG
1426 shutil.rmtree(dirname)
1427
e5a0e5d8 1428 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
fc3fe1c2
SG
1429 """Build all commits for a list of boards
1430
1431 Args:
1432 commits: List of commits to be build, each a Commit object
1433 boards_selected: Dict of selected boards, key is target name,
1434 value is Board object
fc3fe1c2 1435 keep_outputs: True to save build output files
e5a0e5d8 1436 verbose: Display build results as they are completed
2c3deb97
SG
1437 Returns:
1438 Tuple containing:
1439 - number of boards that failed to build
1440 - number of boards that issued warnings
fc3fe1c2 1441 """
fea5858e 1442 self.commit_count = len(commits) if commits else 1
fc3fe1c2 1443 self.commits = commits
e5a0e5d8 1444 self._verbose = verbose
fc3fe1c2
SG
1445
1446 self.ResetResultSummary(board_selected)
f3d015cb 1447 builderthread.Mkdir(self.base_dir, parents = True)
fea5858e
SG
1448 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1449 commits is not None)
fc3fe1c2 1450 self._PrepareOutputSpace()
745b395a 1451 Print('\rStarting build...', newline=False)
fc3fe1c2
SG
1452 self.SetupBuild(board_selected, commits)
1453 self.ProcessResult(None)
1454
1455 # Create jobs to build all commits for each board
1456 for brd in board_selected.itervalues():
190064b4 1457 job = builderthread.BuilderJob()
fc3fe1c2
SG
1458 job.board = brd
1459 job.commits = commits
1460 job.keep_outputs = keep_outputs
1461 job.step = self._step
1462 self.queue.put(job)
1463
d436e381
SG
1464 term = threading.Thread(target=self.queue.join)
1465 term.setDaemon(True)
1466 term.start()
1467 while term.isAlive():
1468 term.join(100)
fc3fe1c2
SG
1469
1470 # Wait until we have processed all output
1471 self.out_queue.join()
4653a882 1472 Print()
fc3fe1c2 1473 self.ClearLine(0)
2c3deb97 1474 return (self.fail, self.warned)