buildman: Add an option to show which boards caused which errors
[karo-tx-uboot.git] / tools / buildman / builder.py
1 # Copyright (c) 2013 The Chromium OS Authors.
2 #
3 # Bloat-o-meter code used here Copyright 2004 Matt Mackall <mpm@selenic.com>
4 #
5 # SPDX-License-Identifier:      GPL-2.0+
6 #
7
8 import collections
9 from datetime import datetime, timedelta
10 import glob
11 import os
12 import re
13 import Queue
14 import shutil
15 import string
16 import sys
17 import time
18
19 import builderthread
20 import command
21 import gitutil
22 import terminal
23 import toolchain
24
25
26 """
27 Theory of Operation
28
29 Please see README for user documentation, and you should be familiar with
30 that before trying to make sense of this.
31
32 Buildman works by keeping the machine as busy as possible, building different
33 commits for different boards on multiple CPUs at once.
34
35 The source repo (self.git_dir) contains all the commits to be built. Each
36 thread works on a single board at a time. It checks out the first commit,
37 configures it for that board, then builds it. Then it checks out the next
38 commit and builds it (typically without re-configuring). When it runs out
39 of commits, it gets another job from the builder and starts again with that
40 board.
41
42 Clearly the builder threads could work either way - they could check out a
43 commit and then built it for all boards. Using separate directories for each
44 commit/board pair they could leave their build product around afterwards
45 also.
46
47 The intent behind building a single board for multiple commits, is to make
48 use of incremental builds. Since each commit is built incrementally from
49 the previous one, builds are faster. Reconfiguring for a different board
50 removes all intermediate object files.
51
52 Many threads can be working at once, but each has its own working directory.
53 When a thread finishes a build, it puts the output files into a result
54 directory.
55
56 The base directory used by buildman is normally '../<branch>', i.e.
57 a directory higher than the source repository and named after the branch
58 being built.
59
60 Within the base directory, we have one subdirectory for each commit. Within
61 that is one subdirectory for each board. Within that is the build output for
62 that commit/board combination.
63
64 Buildman also create working directories for each thread, in a .bm-work/
65 subdirectory in the base dir.
66
67 As an example, say we are building branch 'us-net' for boards 'sandbox' and
68 'seaboard', and say that us-net has two commits. We will have directories
69 like this:
70
71 us-net/             base directory
72     01_of_02_g4ed4ebc_net--Add-tftp-speed-/
73         sandbox/
74             u-boot.bin
75         seaboard/
76             u-boot.bin
77     02_of_02_g4ed4ebc_net--Check-tftp-comp/
78         sandbox/
79             u-boot.bin
80         seaboard/
81             u-boot.bin
82     .bm-work/
83         00/         working directory for thread 0 (contains source checkout)
84             build/  build output
85         01/         working directory for thread 1
86             build/  build output
87         ...
88 u-boot/             source directory
89     .git/           repository
90 """
91
92 # Possible build outcomes
93 OUTCOME_OK, OUTCOME_WARNING, OUTCOME_ERROR, OUTCOME_UNKNOWN = range(4)
94
95 # Translate a commit subject into a valid filename
96 trans_valid_chars = string.maketrans("/: ", "---")
97
98
99 class Builder:
100     """Class for building U-Boot for a particular commit.
101
102     Public members: (many should ->private)
103         active: True if the builder is active and has not been stopped
104         already_done: Number of builds already completed
105         base_dir: Base directory to use for builder
106         checkout: True to check out source, False to skip that step.
107             This is used for testing.
108         col: terminal.Color() object
109         count: Number of commits to build
110         do_make: Method to call to invoke Make
111         fail: Number of builds that failed due to error
112         force_build: Force building even if a build already exists
113         force_config_on_failure: If a commit fails for a board, disable
114             incremental building for the next commit we build for that
115             board, so that we will see all warnings/errors again.
116         force_build_failures: If a previously-built build (i.e. built on
117             a previous run of buildman) is marked as failed, rebuild it.
118         git_dir: Git directory containing source repository
119         last_line_len: Length of the last line we printed (used for erasing
120             it with new progress information)
121         num_jobs: Number of jobs to run at once (passed to make as -j)
122         num_threads: Number of builder threads to run
123         out_queue: Queue of results to process
124         re_make_err: Compiled regular expression for ignore_lines
125         queue: Queue of jobs to run
126         threads: List of active threads
127         toolchains: Toolchains object to use for building
128         upto: Current commit number we are building (0.count-1)
129         warned: Number of builds that produced at least one warning
130         force_reconfig: Reconfigure U-Boot on each comiit. This disables
131             incremental building, where buildman reconfigures on the first
132             commit for a baord, and then just does an incremental build for
133             the following commits. In fact buildman will reconfigure and
134             retry for any failing commits, so generally the only effect of
135             this option is to slow things down.
136         in_tree: Build U-Boot in-tree instead of specifying an output
137             directory separate from the source code. This option is really
138             only useful for testing in-tree builds.
139
140     Private members:
141         _base_board_dict: Last-summarised Dict of boards
142         _base_err_lines: Last-summarised list of errors
143         _build_period_us: Time taken for a single build (float object).
144         _complete_delay: Expected delay until completion (timedelta)
145         _next_delay_update: Next time we plan to display a progress update
146                 (datatime)
147         _show_unknown: Show unknown boards (those not built) in summary
148         _timestamps: List of timestamps for the completion of the last
149             last _timestamp_count builds. Each is a datetime object.
150         _timestamp_count: Number of timestamps to keep in our list.
151         _working_dir: Base working directory containing all threads
152     """
153     class Outcome:
154         """Records a build outcome for a single make invocation
155
156         Public Members:
157             rc: Outcome value (OUTCOME_...)
158             err_lines: List of error lines or [] if none
159             sizes: Dictionary of image size information, keyed by filename
160                 - Each value is itself a dictionary containing
161                     values for 'text', 'data' and 'bss', being the integer
162                     size in bytes of each section.
163             func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
164                     value is itself a dictionary:
165                         key: function name
166                         value: Size of function in bytes
167         """
168         def __init__(self, rc, err_lines, sizes, func_sizes):
169             self.rc = rc
170             self.err_lines = err_lines
171             self.sizes = sizes
172             self.func_sizes = func_sizes
173
174     def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
175                  gnu_make='make', checkout=True, show_unknown=True, step=1):
176         """Create a new Builder object
177
178         Args:
179             toolchains: Toolchains object to use for building
180             base_dir: Base directory to use for builder
181             git_dir: Git directory containing source repository
182             num_threads: Number of builder threads to run
183             num_jobs: Number of jobs to run at once (passed to make as -j)
184             gnu_make: the command name of GNU Make.
185             checkout: True to check out source, False to skip that step.
186                 This is used for testing.
187             show_unknown: Show unknown boards (those not built) in summary
188             step: 1 to process every commit, n to process every nth commit
189         """
190         self.toolchains = toolchains
191         self.base_dir = base_dir
192         self._working_dir = os.path.join(base_dir, '.bm-work')
193         self.threads = []
194         self.active = True
195         self.do_make = self.Make
196         self.gnu_make = gnu_make
197         self.checkout = checkout
198         self.num_threads = num_threads
199         self.num_jobs = num_jobs
200         self.already_done = 0
201         self.force_build = False
202         self.git_dir = git_dir
203         self._show_unknown = show_unknown
204         self._timestamp_count = 10
205         self._build_period_us = None
206         self._complete_delay = None
207         self._next_delay_update = datetime.now()
208         self.force_config_on_failure = True
209         self.force_build_failures = False
210         self.force_reconfig = False
211         self._step = step
212         self.in_tree = False
213         self._error_lines = 0
214
215         self.col = terminal.Color()
216
217         self.queue = Queue.Queue()
218         self.out_queue = Queue.Queue()
219         for i in range(self.num_threads):
220             t = builderthread.BuilderThread(self, i)
221             t.setDaemon(True)
222             t.start()
223             self.threads.append(t)
224
225         self.last_line_len = 0
226         t = builderthread.ResultThread(self)
227         t.setDaemon(True)
228         t.start()
229         self.threads.append(t)
230
231         ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
232         self.re_make_err = re.compile('|'.join(ignore_lines))
233
234     def __del__(self):
235         """Get rid of all threads created by the builder"""
236         for t in self.threads:
237             del t
238
239     def SetDisplayOptions(self, show_errors=False, show_sizes=False,
240                           show_detail=False, show_bloat=False,
241                           list_error_boards=False):
242         """Setup display options for the builder.
243
244         show_errors: True to show summarised error/warning info
245         show_sizes: Show size deltas
246         show_detail: Show detail for each board
247         show_bloat: Show detail for each function
248         list_error_boards: Show the boards which caused each error/warning
249         """
250         self._show_errors = show_errors
251         self._show_sizes = show_sizes
252         self._show_detail = show_detail
253         self._show_bloat = show_bloat
254         self._list_error_boards = list_error_boards
255
256     def _AddTimestamp(self):
257         """Add a new timestamp to the list and record the build period.
258
259         The build period is the length of time taken to perform a single
260         build (one board, one commit).
261         """
262         now = datetime.now()
263         self._timestamps.append(now)
264         count = len(self._timestamps)
265         delta = self._timestamps[-1] - self._timestamps[0]
266         seconds = delta.total_seconds()
267
268         # If we have enough data, estimate build period (time taken for a
269         # single build) and therefore completion time.
270         if count > 1 and self._next_delay_update < now:
271             self._next_delay_update = now + timedelta(seconds=2)
272             if seconds > 0:
273                 self._build_period = float(seconds) / count
274                 todo = self.count - self.upto
275                 self._complete_delay = timedelta(microseconds=
276                         self._build_period * todo * 1000000)
277                 # Round it
278                 self._complete_delay -= timedelta(
279                         microseconds=self._complete_delay.microseconds)
280
281         if seconds > 60:
282             self._timestamps.popleft()
283             count -= 1
284
285     def ClearLine(self, length):
286         """Clear any characters on the current line
287
288         Make way for a new line of length 'length', by outputting enough
289         spaces to clear out the old line. Then remember the new length for
290         next time.
291
292         Args:
293             length: Length of new line, in characters
294         """
295         if length < self.last_line_len:
296             print ' ' * (self.last_line_len - length),
297             print '\r',
298         self.last_line_len = length
299         sys.stdout.flush()
300
301     def SelectCommit(self, commit, checkout=True):
302         """Checkout the selected commit for this build
303         """
304         self.commit = commit
305         if checkout and self.checkout:
306             gitutil.Checkout(commit.hash)
307
308     def Make(self, commit, brd, stage, cwd, *args, **kwargs):
309         """Run make
310
311         Args:
312             commit: Commit object that is being built
313             brd: Board object that is being built
314             stage: Stage that we are at (mrproper, config, build)
315             cwd: Directory where make should be run
316             args: Arguments to pass to make
317             kwargs: Arguments to pass to command.RunPipe()
318         """
319         cmd = [self.gnu_make] + list(args)
320         result = command.RunPipe([cmd], capture=True, capture_stderr=True,
321                 cwd=cwd, raise_on_error=False, **kwargs)
322         return result
323
324     def ProcessResult(self, result):
325         """Process the result of a build, showing progress information
326
327         Args:
328             result: A CommandResult object, which indicates the result for
329                     a single build
330         """
331         col = terminal.Color()
332         if result:
333             target = result.brd.target
334
335             if result.return_code < 0:
336                 self.active = False
337                 command.StopAll()
338                 return
339
340             self.upto += 1
341             if result.return_code != 0:
342                 self.fail += 1
343             elif result.stderr:
344                 self.warned += 1
345             if result.already_done:
346                 self.already_done += 1
347             if self._verbose:
348                 print '\r',
349                 self.ClearLine(0)
350                 boards_selected = {target : result.brd}
351                 self.ResetResultSummary(boards_selected)
352                 self.ProduceResultSummary(result.commit_upto, self.commits,
353                                           boards_selected)
354         else:
355             target = '(starting)'
356
357         # Display separate counts for ok, warned and fail
358         ok = self.upto - self.warned - self.fail
359         line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
360         line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
361         line += self.col.Color(self.col.RED, '%5d' % self.fail)
362
363         name = ' /%-5d  ' % self.count
364
365         # Add our current completion time estimate
366         self._AddTimestamp()
367         if self._complete_delay:
368             name += '%s  : ' % self._complete_delay
369         # When building all boards for a commit, we can print a commit
370         # progress message.
371         if result and result.commit_upto is None:
372             name += 'commit %2d/%-3d' % (self.commit_upto + 1,
373                     self.commit_count)
374
375         name += target
376         print line + name,
377         length = 14 + len(name)
378         self.ClearLine(length)
379
380     def _GetOutputDir(self, commit_upto):
381         """Get the name of the output directory for a commit number
382
383         The output directory is typically .../<branch>/<commit>.
384
385         Args:
386             commit_upto: Commit number to use (0..self.count-1)
387         """
388         if self.commits:
389             commit = self.commits[commit_upto]
390             subject = commit.subject.translate(trans_valid_chars)
391             commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
392                     self.commit_count, commit.hash, subject[:20]))
393         else:
394             commit_dir = 'current'
395         output_dir = os.path.join(self.base_dir, commit_dir)
396         return output_dir
397
398     def GetBuildDir(self, commit_upto, target):
399         """Get the name of the build directory for a commit number
400
401         The build directory is typically .../<branch>/<commit>/<target>.
402
403         Args:
404             commit_upto: Commit number to use (0..self.count-1)
405             target: Target name
406         """
407         output_dir = self._GetOutputDir(commit_upto)
408         return os.path.join(output_dir, target)
409
410     def GetDoneFile(self, commit_upto, target):
411         """Get the name of the done file for a commit number
412
413         Args:
414             commit_upto: Commit number to use (0..self.count-1)
415             target: Target name
416         """
417         return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
418
419     def GetSizesFile(self, commit_upto, target):
420         """Get the name of the sizes file for a commit number
421
422         Args:
423             commit_upto: Commit number to use (0..self.count-1)
424             target: Target name
425         """
426         return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
427
428     def GetFuncSizesFile(self, commit_upto, target, elf_fname):
429         """Get the name of the funcsizes file for a commit number and ELF file
430
431         Args:
432             commit_upto: Commit number to use (0..self.count-1)
433             target: Target name
434             elf_fname: Filename of elf image
435         """
436         return os.path.join(self.GetBuildDir(commit_upto, target),
437                             '%s.sizes' % elf_fname.replace('/', '-'))
438
439     def GetObjdumpFile(self, commit_upto, target, elf_fname):
440         """Get the name of the objdump file for a commit number and ELF file
441
442         Args:
443             commit_upto: Commit number to use (0..self.count-1)
444             target: Target name
445             elf_fname: Filename of elf image
446         """
447         return os.path.join(self.GetBuildDir(commit_upto, target),
448                             '%s.objdump' % elf_fname.replace('/', '-'))
449
450     def GetErrFile(self, commit_upto, target):
451         """Get the name of the err file for a commit number
452
453         Args:
454             commit_upto: Commit number to use (0..self.count-1)
455             target: Target name
456         """
457         output_dir = self.GetBuildDir(commit_upto, target)
458         return os.path.join(output_dir, 'err')
459
460     def FilterErrors(self, lines):
461         """Filter out errors in which we have no interest
462
463         We should probably use map().
464
465         Args:
466             lines: List of error lines, each a string
467         Returns:
468             New list with only interesting lines included
469         """
470         out_lines = []
471         for line in lines:
472             if not self.re_make_err.search(line):
473                 out_lines.append(line)
474         return out_lines
475
476     def ReadFuncSizes(self, fname, fd):
477         """Read function sizes from the output of 'nm'
478
479         Args:
480             fd: File containing data to read
481             fname: Filename we are reading from (just for errors)
482
483         Returns:
484             Dictionary containing size of each function in bytes, indexed by
485             function name.
486         """
487         sym = {}
488         for line in fd.readlines():
489             try:
490                 size, type, name = line[:-1].split()
491             except:
492                 print "Invalid line in file '%s': '%s'" % (fname, line[:-1])
493                 continue
494             if type in 'tTdDbB':
495                 # function names begin with '.' on 64-bit powerpc
496                 if '.' in name[1:]:
497                     name = 'static.' + name.split('.')[0]
498                 sym[name] = sym.get(name, 0) + int(size, 16)
499         return sym
500
501     def GetBuildOutcome(self, commit_upto, target, read_func_sizes):
502         """Work out the outcome of a build.
503
504         Args:
505             commit_upto: Commit number to check (0..n-1)
506             target: Target board to check
507             read_func_sizes: True to read function size information
508
509         Returns:
510             Outcome object
511         """
512         done_file = self.GetDoneFile(commit_upto, target)
513         sizes_file = self.GetSizesFile(commit_upto, target)
514         sizes = {}
515         func_sizes = {}
516         if os.path.exists(done_file):
517             with open(done_file, 'r') as fd:
518                 return_code = int(fd.readline())
519                 err_lines = []
520                 err_file = self.GetErrFile(commit_upto, target)
521                 if os.path.exists(err_file):
522                     with open(err_file, 'r') as fd:
523                         err_lines = self.FilterErrors(fd.readlines())
524
525                 # Decide whether the build was ok, failed or created warnings
526                 if return_code:
527                     rc = OUTCOME_ERROR
528                 elif len(err_lines):
529                     rc = OUTCOME_WARNING
530                 else:
531                     rc = OUTCOME_OK
532
533                 # Convert size information to our simple format
534                 if os.path.exists(sizes_file):
535                     with open(sizes_file, 'r') as fd:
536                         for line in fd.readlines():
537                             values = line.split()
538                             rodata = 0
539                             if len(values) > 6:
540                                 rodata = int(values[6], 16)
541                             size_dict = {
542                                 'all' : int(values[0]) + int(values[1]) +
543                                         int(values[2]),
544                                 'text' : int(values[0]) - rodata,
545                                 'data' : int(values[1]),
546                                 'bss' : int(values[2]),
547                                 'rodata' : rodata,
548                             }
549                             sizes[values[5]] = size_dict
550
551             if read_func_sizes:
552                 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
553                 for fname in glob.glob(pattern):
554                     with open(fname, 'r') as fd:
555                         dict_name = os.path.basename(fname).replace('.sizes',
556                                                                     '')
557                         func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
558
559             return Builder.Outcome(rc, err_lines, sizes, func_sizes)
560
561         return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {})
562
563     def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes):
564         """Calculate a summary of the results of building a commit.
565
566         Args:
567             board_selected: Dict containing boards to summarise
568             commit_upto: Commit number to summarize (0..self.count-1)
569             read_func_sizes: True to read function size information
570
571         Returns:
572             Tuple:
573                 Dict containing boards which passed building this commit.
574                     keyed by board.target
575                 List containing a summary of error/warning lines
576                 Dict keyed by error line, containing a list of the Board
577                     objects with that error
578         """
579         board_dict = {}
580         err_lines_summary = []
581         err_lines_boards = {}
582
583         for board in boards_selected.itervalues():
584             outcome = self.GetBuildOutcome(commit_upto, board.target,
585                                            read_func_sizes)
586             board_dict[board.target] = outcome
587             for err in outcome.err_lines:
588                 if err:
589                     err = err.rstrip()
590                     if err in err_lines_boards:
591                         err_lines_boards[err].append(board)
592                     else:
593                         err_lines_boards[err] = [board]
594                         err_lines_summary.append(err.rstrip())
595         return board_dict, err_lines_summary, err_lines_boards
596
597     def AddOutcome(self, board_dict, arch_list, changes, char, color):
598         """Add an output to our list of outcomes for each architecture
599
600         This simple function adds failing boards (changes) to the
601         relevant architecture string, so we can print the results out
602         sorted by architecture.
603
604         Args:
605              board_dict: Dict containing all boards
606              arch_list: Dict keyed by arch name. Value is a string containing
607                     a list of board names which failed for that arch.
608              changes: List of boards to add to arch_list
609              color: terminal.Colour object
610         """
611         done_arch = {}
612         for target in changes:
613             if target in board_dict:
614                 arch = board_dict[target].arch
615             else:
616                 arch = 'unknown'
617             str = self.col.Color(color, ' ' + target)
618             if not arch in done_arch:
619                 str = self.col.Color(color, char) + '  ' + str
620                 done_arch[arch] = True
621             if not arch in arch_list:
622                 arch_list[arch] = str
623             else:
624                 arch_list[arch] += str
625
626
627     def ColourNum(self, num):
628         color = self.col.RED if num > 0 else self.col.GREEN
629         if num == 0:
630             return '0'
631         return self.col.Color(color, str(num))
632
633     def ResetResultSummary(self, board_selected):
634         """Reset the results summary ready for use.
635
636         Set up the base board list to be all those selected, and set the
637         error lines to empty.
638
639         Following this, calls to PrintResultSummary() will use this
640         information to work out what has changed.
641
642         Args:
643             board_selected: Dict containing boards to summarise, keyed by
644                 board.target
645         """
646         self._base_board_dict = {}
647         for board in board_selected:
648             self._base_board_dict[board] = Builder.Outcome(0, [], [], {})
649         self._base_err_lines = []
650
651     def PrintFuncSizeDetail(self, fname, old, new):
652         grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
653         delta, common = [], {}
654
655         for a in old:
656             if a in new:
657                 common[a] = 1
658
659         for name in old:
660             if name not in common:
661                 remove += 1
662                 down += old[name]
663                 delta.append([-old[name], name])
664
665         for name in new:
666             if name not in common:
667                 add += 1
668                 up += new[name]
669                 delta.append([new[name], name])
670
671         for name in common:
672                 diff = new.get(name, 0) - old.get(name, 0)
673                 if diff > 0:
674                     grow, up = grow + 1, up + diff
675                 elif diff < 0:
676                     shrink, down = shrink + 1, down - diff
677                 delta.append([diff, name])
678
679         delta.sort()
680         delta.reverse()
681
682         args = [add, -remove, grow, -shrink, up, -down, up - down]
683         if max(args) == 0:
684             return
685         args = [self.ColourNum(x) for x in args]
686         indent = ' ' * 15
687         print ('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
688                tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
689         print '%s  %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
690                                         'delta')
691         for diff, name in delta:
692             if diff:
693                 color = self.col.RED if diff > 0 else self.col.GREEN
694                 msg = '%s  %-38s %7s %7s %+7d' % (indent, name,
695                         old.get(name, '-'), new.get(name,'-'), diff)
696                 print self.col.Color(color, msg)
697
698
699     def PrintSizeDetail(self, target_list, show_bloat):
700         """Show details size information for each board
701
702         Args:
703             target_list: List of targets, each a dict containing:
704                     'target': Target name
705                     'total_diff': Total difference in bytes across all areas
706                     <part_name>: Difference for that part
707             show_bloat: Show detail for each function
708         """
709         targets_by_diff = sorted(target_list, reverse=True,
710         key=lambda x: x['_total_diff'])
711         for result in targets_by_diff:
712             printed_target = False
713             for name in sorted(result):
714                 diff = result[name]
715                 if name.startswith('_'):
716                     continue
717                 if diff != 0:
718                     color = self.col.RED if diff > 0 else self.col.GREEN
719                 msg = ' %s %+d' % (name, diff)
720                 if not printed_target:
721                     print '%10s  %-15s:' % ('', result['_target']),
722                     printed_target = True
723                 print self.col.Color(color, msg),
724             if printed_target:
725                 print
726                 if show_bloat:
727                     target = result['_target']
728                     outcome = result['_outcome']
729                     base_outcome = self._base_board_dict[target]
730                     for fname in outcome.func_sizes:
731                         self.PrintFuncSizeDetail(fname,
732                                                  base_outcome.func_sizes[fname],
733                                                  outcome.func_sizes[fname])
734
735
736     def PrintSizeSummary(self, board_selected, board_dict, show_detail,
737                          show_bloat):
738         """Print a summary of image sizes broken down by section.
739
740         The summary takes the form of one line per architecture. The
741         line contains deltas for each of the sections (+ means the section
742         got bigger, - means smaller). The nunmbers are the average number
743         of bytes that a board in this section increased by.
744
745         For example:
746            powerpc: (622 boards)   text -0.0
747           arm: (285 boards)   text -0.0
748           nds32: (3 boards)   text -8.0
749
750         Args:
751             board_selected: Dict containing boards to summarise, keyed by
752                 board.target
753             board_dict: Dict containing boards for which we built this
754                 commit, keyed by board.target. The value is an Outcome object.
755             show_detail: Show detail for each board
756             show_bloat: Show detail for each function
757         """
758         arch_list = {}
759         arch_count = {}
760
761         # Calculate changes in size for different image parts
762         # The previous sizes are in Board.sizes, for each board
763         for target in board_dict:
764             if target not in board_selected:
765                 continue
766             base_sizes = self._base_board_dict[target].sizes
767             outcome = board_dict[target]
768             sizes = outcome.sizes
769
770             # Loop through the list of images, creating a dict of size
771             # changes for each image/part. We end up with something like
772             # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
773             # which means that U-Boot data increased by 5 bytes and SPL
774             # text decreased by 4.
775             err = {'_target' : target}
776             for image in sizes:
777                 if image in base_sizes:
778                     base_image = base_sizes[image]
779                     # Loop through the text, data, bss parts
780                     for part in sorted(sizes[image]):
781                         diff = sizes[image][part] - base_image[part]
782                         col = None
783                         if diff:
784                             if image == 'u-boot':
785                                 name = part
786                             else:
787                                 name = image + ':' + part
788                             err[name] = diff
789             arch = board_selected[target].arch
790             if not arch in arch_count:
791                 arch_count[arch] = 1
792             else:
793                 arch_count[arch] += 1
794             if not sizes:
795                 pass    # Only add to our list when we have some stats
796             elif not arch in arch_list:
797                 arch_list[arch] = [err]
798             else:
799                 arch_list[arch].append(err)
800
801         # We now have a list of image size changes sorted by arch
802         # Print out a summary of these
803         for arch, target_list in arch_list.iteritems():
804             # Get total difference for each type
805             totals = {}
806             for result in target_list:
807                 total = 0
808                 for name, diff in result.iteritems():
809                     if name.startswith('_'):
810                         continue
811                     total += diff
812                     if name in totals:
813                         totals[name] += diff
814                     else:
815                         totals[name] = diff
816                 result['_total_diff'] = total
817                 result['_outcome'] = board_dict[result['_target']]
818
819             count = len(target_list)
820             printed_arch = False
821             for name in sorted(totals):
822                 diff = totals[name]
823                 if diff:
824                     # Display the average difference in this name for this
825                     # architecture
826                     avg_diff = float(diff) / count
827                     color = self.col.RED if avg_diff > 0 else self.col.GREEN
828                     msg = ' %s %+1.1f' % (name, avg_diff)
829                     if not printed_arch:
830                         print '%10s: (for %d/%d boards)' % (arch, count,
831                                 arch_count[arch]),
832                         printed_arch = True
833                     print self.col.Color(color, msg),
834
835             if printed_arch:
836                 print
837                 if show_detail:
838                     self.PrintSizeDetail(target_list, show_bloat)
839
840
841     def PrintResultSummary(self, board_selected, board_dict, err_lines,
842                            err_line_boards, show_sizes, show_detail,
843                            show_bloat):
844         """Compare results with the base results and display delta.
845
846         Only boards mentioned in board_selected will be considered. This
847         function is intended to be called repeatedly with the results of
848         each commit. It therefore shows a 'diff' between what it saw in
849         the last call and what it sees now.
850
851         Args:
852             board_selected: Dict containing boards to summarise, keyed by
853                 board.target
854             board_dict: Dict containing boards for which we built this
855                 commit, keyed by board.target. The value is an Outcome object.
856             err_lines: A list of errors for this commit, or [] if there is
857                 none, or we don't want to print errors
858             err_line_boards: Dict keyed by error line, containing a list of
859                 the Board objects with that error
860             show_sizes: Show image size deltas
861             show_detail: Show detail for each board
862             show_bloat: Show detail for each function
863         """
864         def _BoardList(line):
865             """Helper function to get a line of boards containing a line
866
867             Args:
868                 line: Error line to search for
869             Return:
870                 String containing a list of boards with that error line, or
871                 '' if the user has not requested such a list
872             """
873             if self._list_error_boards:
874                 names = []
875                 for board in err_line_boards[line]:
876                     names.append(board.target)
877                 names_str = '(%s) ' % ','.join(names)
878             else:
879                 names_str = ''
880             return names_str
881
882         better = []     # List of boards fixed since last commit
883         worse = []      # List of new broken boards since last commit
884         new = []        # List of boards that didn't exist last time
885         unknown = []    # List of boards that were not built
886
887         for target in board_dict:
888             if target not in board_selected:
889                 continue
890
891             # If the board was built last time, add its outcome to a list
892             if target in self._base_board_dict:
893                 base_outcome = self._base_board_dict[target].rc
894                 outcome = board_dict[target]
895                 if outcome.rc == OUTCOME_UNKNOWN:
896                     unknown.append(target)
897                 elif outcome.rc < base_outcome:
898                     better.append(target)
899                 elif outcome.rc > base_outcome:
900                     worse.append(target)
901             else:
902                 new.append(target)
903
904         # Get a list of errors that have appeared, and disappeared
905         better_err = []
906         worse_err = []
907         for line in err_lines:
908             if line not in self._base_err_lines:
909                 worse_err.append('+' + _BoardList(line) + line)
910         for line in self._base_err_lines:
911             if line not in err_lines:
912                 better_err.append('-' + line)
913
914         # Display results by arch
915         if better or worse or unknown or new or worse_err or better_err:
916             arch_list = {}
917             self.AddOutcome(board_selected, arch_list, better, '',
918                     self.col.GREEN)
919             self.AddOutcome(board_selected, arch_list, worse, '+',
920                     self.col.RED)
921             self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE)
922             if self._show_unknown:
923                 self.AddOutcome(board_selected, arch_list, unknown, '?',
924                         self.col.MAGENTA)
925             for arch, target_list in arch_list.iteritems():
926                 print '%10s: %s' % (arch, target_list)
927                 self._error_lines += 1
928             if better_err:
929                 print self.col.Color(self.col.GREEN, '\n'.join(better_err))
930                 self._error_lines += 1
931             if worse_err:
932                 print self.col.Color(self.col.RED, '\n'.join(worse_err))
933                 self._error_lines += 1
934
935         if show_sizes:
936             self.PrintSizeSummary(board_selected, board_dict, show_detail,
937                                   show_bloat)
938
939         # Save our updated information for the next call to this function
940         self._base_board_dict = board_dict
941         self._base_err_lines = err_lines
942
943         # Get a list of boards that did not get built, if needed
944         not_built = []
945         for board in board_selected:
946             if not board in board_dict:
947                 not_built.append(board)
948         if not_built:
949             print "Boards not built (%d): %s" % (len(not_built),
950                     ', '.join(not_built))
951
952     def ProduceResultSummary(self, commit_upto, commits, board_selected):
953             board_dict, err_lines, err_line_boards = self.GetResultSummary(
954                     board_selected, commit_upto,
955                     read_func_sizes=self._show_bloat)
956             if commits:
957                 msg = '%02d: %s' % (commit_upto + 1,
958                         commits[commit_upto].subject)
959                 print self.col.Color(self.col.BLUE, msg)
960             self.PrintResultSummary(board_selected, board_dict,
961                     err_lines if self._show_errors else [], err_line_boards,
962                     self._show_sizes, self._show_detail, self._show_bloat)
963
964     def ShowSummary(self, commits, board_selected):
965         """Show a build summary for U-Boot for a given board list.
966
967         Reset the result summary, then repeatedly call GetResultSummary on
968         each commit's results, then display the differences we see.
969
970         Args:
971             commit: Commit objects to summarise
972             board_selected: Dict containing boards to summarise
973         """
974         self.commit_count = len(commits) if commits else 1
975         self.commits = commits
976         self.ResetResultSummary(board_selected)
977         self._error_lines = 0
978
979         for commit_upto in range(0, self.commit_count, self._step):
980             self.ProduceResultSummary(commit_upto, commits, board_selected)
981         if not self._error_lines:
982             print self.col.Color(self.col.GREEN, '(no errors to report)')
983
984
985     def SetupBuild(self, board_selected, commits):
986         """Set up ready to start a build.
987
988         Args:
989             board_selected: Selected boards to build
990             commits: Selected commits to build
991         """
992         # First work out how many commits we will build
993         count = (self.commit_count + self._step - 1) / self._step
994         self.count = len(board_selected) * count
995         self.upto = self.warned = self.fail = 0
996         self._timestamps = collections.deque()
997
998     def GetThreadDir(self, thread_num):
999         """Get the directory path to the working dir for a thread.
1000
1001         Args:
1002             thread_num: Number of thread to check.
1003         """
1004         return os.path.join(self._working_dir, '%02d' % thread_num)
1005
1006     def _PrepareThread(self, thread_num, setup_git):
1007         """Prepare the working directory for a thread.
1008
1009         This clones or fetches the repo into the thread's work directory.
1010
1011         Args:
1012             thread_num: Thread number (0, 1, ...)
1013             setup_git: True to set up a git repo clone
1014         """
1015         thread_dir = self.GetThreadDir(thread_num)
1016         builderthread.Mkdir(thread_dir)
1017         git_dir = os.path.join(thread_dir, '.git')
1018
1019         # Clone the repo if it doesn't already exist
1020         # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1021         # we have a private index but uses the origin repo's contents?
1022         if setup_git and self.git_dir:
1023             src_dir = os.path.abspath(self.git_dir)
1024             if os.path.exists(git_dir):
1025                 gitutil.Fetch(git_dir, thread_dir)
1026             else:
1027                 print 'Cloning repo for thread %d' % thread_num
1028                 gitutil.Clone(src_dir, thread_dir)
1029
1030     def _PrepareWorkingSpace(self, max_threads, setup_git):
1031         """Prepare the working directory for use.
1032
1033         Set up the git repo for each thread.
1034
1035         Args:
1036             max_threads: Maximum number of threads we expect to need.
1037             setup_git: True to set up a git repo clone
1038         """
1039         builderthread.Mkdir(self._working_dir)
1040         for thread in range(max_threads):
1041             self._PrepareThread(thread, setup_git)
1042
1043     def _PrepareOutputSpace(self):
1044         """Get the output directories ready to receive files.
1045
1046         We delete any output directories which look like ones we need to
1047         create. Having left over directories is confusing when the user wants
1048         to check the output manually.
1049         """
1050         dir_list = []
1051         for commit_upto in range(self.commit_count):
1052             dir_list.append(self._GetOutputDir(commit_upto))
1053
1054         for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1055             if dirname not in dir_list:
1056                 shutil.rmtree(dirname)
1057
1058     def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
1059         """Build all commits for a list of boards
1060
1061         Args:
1062             commits: List of commits to be build, each a Commit object
1063             boards_selected: Dict of selected boards, key is target name,
1064                     value is Board object
1065             keep_outputs: True to save build output files
1066             verbose: Display build results as they are completed
1067         Returns:
1068             Tuple containing:
1069                 - number of boards that failed to build
1070                 - number of boards that issued warnings
1071         """
1072         self.commit_count = len(commits) if commits else 1
1073         self.commits = commits
1074         self._verbose = verbose
1075
1076         self.ResetResultSummary(board_selected)
1077         builderthread.Mkdir(self.base_dir)
1078         self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1079                 commits is not None)
1080         self._PrepareOutputSpace()
1081         self.SetupBuild(board_selected, commits)
1082         self.ProcessResult(None)
1083
1084         # Create jobs to build all commits for each board
1085         for brd in board_selected.itervalues():
1086             job = builderthread.BuilderJob()
1087             job.board = brd
1088             job.commits = commits
1089             job.keep_outputs = keep_outputs
1090             job.step = self._step
1091             self.queue.put(job)
1092
1093         # Wait until all jobs are started
1094         self.queue.join()
1095
1096         # Wait until we have processed all output
1097         self.out_queue.join()
1098         print
1099         self.ClearLine(0)
1100         return (self.fail, self.warned)