]> git.kernelconcepts.de Git - karo-tx-uboot.git/blob - tools/buildman/builder.py
buildman: Add verbose option to display errors as they happen
[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
214         self.col = terminal.Color()
215
216         self.queue = Queue.Queue()
217         self.out_queue = Queue.Queue()
218         for i in range(self.num_threads):
219             t = builderthread.BuilderThread(self, i)
220             t.setDaemon(True)
221             t.start()
222             self.threads.append(t)
223
224         self.last_line_len = 0
225         t = builderthread.ResultThread(self)
226         t.setDaemon(True)
227         t.start()
228         self.threads.append(t)
229
230         ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
231         self.re_make_err = re.compile('|'.join(ignore_lines))
232
233     def __del__(self):
234         """Get rid of all threads created by the builder"""
235         for t in self.threads:
236             del t
237
238     def SetDisplayOptions(self, show_errors=False, show_sizes=False,
239                           show_detail=False, show_bloat=False):
240         """Setup display options for the builder.
241
242         show_errors: True to show summarised error/warning info
243         show_sizes: Show size deltas
244         show_detail: Show detail for each board
245         show_bloat: Show detail for each function
246         """
247         self._show_errors = show_errors
248         self._show_sizes = show_sizes
249         self._show_detail = show_detail
250         self._show_bloat = show_bloat
251
252     def _AddTimestamp(self):
253         """Add a new timestamp to the list and record the build period.
254
255         The build period is the length of time taken to perform a single
256         build (one board, one commit).
257         """
258         now = datetime.now()
259         self._timestamps.append(now)
260         count = len(self._timestamps)
261         delta = self._timestamps[-1] - self._timestamps[0]
262         seconds = delta.total_seconds()
263
264         # If we have enough data, estimate build period (time taken for a
265         # single build) and therefore completion time.
266         if count > 1 and self._next_delay_update < now:
267             self._next_delay_update = now + timedelta(seconds=2)
268             if seconds > 0:
269                 self._build_period = float(seconds) / count
270                 todo = self.count - self.upto
271                 self._complete_delay = timedelta(microseconds=
272                         self._build_period * todo * 1000000)
273                 # Round it
274                 self._complete_delay -= timedelta(
275                         microseconds=self._complete_delay.microseconds)
276
277         if seconds > 60:
278             self._timestamps.popleft()
279             count -= 1
280
281     def ClearLine(self, length):
282         """Clear any characters on the current line
283
284         Make way for a new line of length 'length', by outputting enough
285         spaces to clear out the old line. Then remember the new length for
286         next time.
287
288         Args:
289             length: Length of new line, in characters
290         """
291         if length < self.last_line_len:
292             print ' ' * (self.last_line_len - length),
293             print '\r',
294         self.last_line_len = length
295         sys.stdout.flush()
296
297     def SelectCommit(self, commit, checkout=True):
298         """Checkout the selected commit for this build
299         """
300         self.commit = commit
301         if checkout and self.checkout:
302             gitutil.Checkout(commit.hash)
303
304     def Make(self, commit, brd, stage, cwd, *args, **kwargs):
305         """Run make
306
307         Args:
308             commit: Commit object that is being built
309             brd: Board object that is being built
310             stage: Stage that we are at (distclean, config, build)
311             cwd: Directory where make should be run
312             args: Arguments to pass to make
313             kwargs: Arguments to pass to command.RunPipe()
314         """
315         cmd = [self.gnu_make] + list(args)
316         result = command.RunPipe([cmd], capture=True, capture_stderr=True,
317                 cwd=cwd, raise_on_error=False, **kwargs)
318         return result
319
320     def ProcessResult(self, result):
321         """Process the result of a build, showing progress information
322
323         Args:
324             result: A CommandResult object, which indicates the result for
325                     a single build
326         """
327         col = terminal.Color()
328         if result:
329             target = result.brd.target
330
331             if result.return_code < 0:
332                 self.active = False
333                 command.StopAll()
334                 return
335
336             self.upto += 1
337             if result.return_code != 0:
338                 self.fail += 1
339             elif result.stderr:
340                 self.warned += 1
341             if result.already_done:
342                 self.already_done += 1
343             if self._verbose:
344                 print '\r',
345                 self.ClearLine(0)
346                 boards_selected = {target : result.brd}
347                 self.ResetResultSummary(boards_selected)
348                 self.ProduceResultSummary(result.commit_upto, self.commits,
349                                           boards_selected)
350         else:
351             target = '(starting)'
352
353         # Display separate counts for ok, warned and fail
354         ok = self.upto - self.warned - self.fail
355         line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
356         line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
357         line += self.col.Color(self.col.RED, '%5d' % self.fail)
358
359         name = ' /%-5d  ' % self.count
360
361         # Add our current completion time estimate
362         self._AddTimestamp()
363         if self._complete_delay:
364             name += '%s  : ' % self._complete_delay
365         # When building all boards for a commit, we can print a commit
366         # progress message.
367         if result and result.commit_upto is None:
368             name += 'commit %2d/%-3d' % (self.commit_upto + 1,
369                     self.commit_count)
370
371         name += target
372         print line + name,
373         length = 14 + len(name)
374         self.ClearLine(length)
375
376     def _GetOutputDir(self, commit_upto):
377         """Get the name of the output directory for a commit number
378
379         The output directory is typically .../<branch>/<commit>.
380
381         Args:
382             commit_upto: Commit number to use (0..self.count-1)
383         """
384         if self.commits:
385             commit = self.commits[commit_upto]
386             subject = commit.subject.translate(trans_valid_chars)
387             commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
388                     self.commit_count, commit.hash, subject[:20]))
389         else:
390             commit_dir = 'current'
391         output_dir = os.path.join(self.base_dir, commit_dir)
392         return output_dir
393
394     def GetBuildDir(self, commit_upto, target):
395         """Get the name of the build directory for a commit number
396
397         The build directory is typically .../<branch>/<commit>/<target>.
398
399         Args:
400             commit_upto: Commit number to use (0..self.count-1)
401             target: Target name
402         """
403         output_dir = self._GetOutputDir(commit_upto)
404         return os.path.join(output_dir, target)
405
406     def GetDoneFile(self, commit_upto, target):
407         """Get the name of the done file for a commit number
408
409         Args:
410             commit_upto: Commit number to use (0..self.count-1)
411             target: Target name
412         """
413         return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
414
415     def GetSizesFile(self, commit_upto, target):
416         """Get the name of the sizes file for a commit number
417
418         Args:
419             commit_upto: Commit number to use (0..self.count-1)
420             target: Target name
421         """
422         return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
423
424     def GetFuncSizesFile(self, commit_upto, target, elf_fname):
425         """Get the name of the funcsizes file for a commit number and ELF file
426
427         Args:
428             commit_upto: Commit number to use (0..self.count-1)
429             target: Target name
430             elf_fname: Filename of elf image
431         """
432         return os.path.join(self.GetBuildDir(commit_upto, target),
433                             '%s.sizes' % elf_fname.replace('/', '-'))
434
435     def GetObjdumpFile(self, commit_upto, target, elf_fname):
436         """Get the name of the objdump file for a commit number and ELF file
437
438         Args:
439             commit_upto: Commit number to use (0..self.count-1)
440             target: Target name
441             elf_fname: Filename of elf image
442         """
443         return os.path.join(self.GetBuildDir(commit_upto, target),
444                             '%s.objdump' % elf_fname.replace('/', '-'))
445
446     def GetErrFile(self, commit_upto, target):
447         """Get the name of the err file for a commit number
448
449         Args:
450             commit_upto: Commit number to use (0..self.count-1)
451             target: Target name
452         """
453         output_dir = self.GetBuildDir(commit_upto, target)
454         return os.path.join(output_dir, 'err')
455
456     def FilterErrors(self, lines):
457         """Filter out errors in which we have no interest
458
459         We should probably use map().
460
461         Args:
462             lines: List of error lines, each a string
463         Returns:
464             New list with only interesting lines included
465         """
466         out_lines = []
467         for line in lines:
468             if not self.re_make_err.search(line):
469                 out_lines.append(line)
470         return out_lines
471
472     def ReadFuncSizes(self, fname, fd):
473         """Read function sizes from the output of 'nm'
474
475         Args:
476             fd: File containing data to read
477             fname: Filename we are reading from (just for errors)
478
479         Returns:
480             Dictionary containing size of each function in bytes, indexed by
481             function name.
482         """
483         sym = {}
484         for line in fd.readlines():
485             try:
486                 size, type, name = line[:-1].split()
487             except:
488                 print "Invalid line in file '%s': '%s'" % (fname, line[:-1])
489                 continue
490             if type in 'tTdDbB':
491                 # function names begin with '.' on 64-bit powerpc
492                 if '.' in name[1:]:
493                     name = 'static.' + name.split('.')[0]
494                 sym[name] = sym.get(name, 0) + int(size, 16)
495         return sym
496
497     def GetBuildOutcome(self, commit_upto, target, read_func_sizes):
498         """Work out the outcome of a build.
499
500         Args:
501             commit_upto: Commit number to check (0..n-1)
502             target: Target board to check
503             read_func_sizes: True to read function size information
504
505         Returns:
506             Outcome object
507         """
508         done_file = self.GetDoneFile(commit_upto, target)
509         sizes_file = self.GetSizesFile(commit_upto, target)
510         sizes = {}
511         func_sizes = {}
512         if os.path.exists(done_file):
513             with open(done_file, 'r') as fd:
514                 return_code = int(fd.readline())
515                 err_lines = []
516                 err_file = self.GetErrFile(commit_upto, target)
517                 if os.path.exists(err_file):
518                     with open(err_file, 'r') as fd:
519                         err_lines = self.FilterErrors(fd.readlines())
520
521                 # Decide whether the build was ok, failed or created warnings
522                 if return_code:
523                     rc = OUTCOME_ERROR
524                 elif len(err_lines):
525                     rc = OUTCOME_WARNING
526                 else:
527                     rc = OUTCOME_OK
528
529                 # Convert size information to our simple format
530                 if os.path.exists(sizes_file):
531                     with open(sizes_file, 'r') as fd:
532                         for line in fd.readlines():
533                             values = line.split()
534                             rodata = 0
535                             if len(values) > 6:
536                                 rodata = int(values[6], 16)
537                             size_dict = {
538                                 'all' : int(values[0]) + int(values[1]) +
539                                         int(values[2]),
540                                 'text' : int(values[0]) - rodata,
541                                 'data' : int(values[1]),
542                                 'bss' : int(values[2]),
543                                 'rodata' : rodata,
544                             }
545                             sizes[values[5]] = size_dict
546
547             if read_func_sizes:
548                 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
549                 for fname in glob.glob(pattern):
550                     with open(fname, 'r') as fd:
551                         dict_name = os.path.basename(fname).replace('.sizes',
552                                                                     '')
553                         func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
554
555             return Builder.Outcome(rc, err_lines, sizes, func_sizes)
556
557         return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {})
558
559     def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes):
560         """Calculate a summary of the results of building a commit.
561
562         Args:
563             board_selected: Dict containing boards to summarise
564             commit_upto: Commit number to summarize (0..self.count-1)
565             read_func_sizes: True to read function size information
566
567         Returns:
568             Tuple:
569                 Dict containing boards which passed building this commit.
570                     keyed by board.target
571                 List containing a summary of error/warning lines
572         """
573         board_dict = {}
574         err_lines_summary = []
575
576         for board in boards_selected.itervalues():
577             outcome = self.GetBuildOutcome(commit_upto, board.target,
578                                            read_func_sizes)
579             board_dict[board.target] = outcome
580             for err in outcome.err_lines:
581                 if err and not err.rstrip() in err_lines_summary:
582                     err_lines_summary.append(err.rstrip())
583         return board_dict, err_lines_summary
584
585     def AddOutcome(self, board_dict, arch_list, changes, char, color):
586         """Add an output to our list of outcomes for each architecture
587
588         This simple function adds failing boards (changes) to the
589         relevant architecture string, so we can print the results out
590         sorted by architecture.
591
592         Args:
593              board_dict: Dict containing all boards
594              arch_list: Dict keyed by arch name. Value is a string containing
595                     a list of board names which failed for that arch.
596              changes: List of boards to add to arch_list
597              color: terminal.Colour object
598         """
599         done_arch = {}
600         for target in changes:
601             if target in board_dict:
602                 arch = board_dict[target].arch
603             else:
604                 arch = 'unknown'
605             str = self.col.Color(color, ' ' + target)
606             if not arch in done_arch:
607                 str = self.col.Color(color, char) + '  ' + str
608                 done_arch[arch] = True
609             if not arch in arch_list:
610                 arch_list[arch] = str
611             else:
612                 arch_list[arch] += str
613
614
615     def ColourNum(self, num):
616         color = self.col.RED if num > 0 else self.col.GREEN
617         if num == 0:
618             return '0'
619         return self.col.Color(color, str(num))
620
621     def ResetResultSummary(self, board_selected):
622         """Reset the results summary ready for use.
623
624         Set up the base board list to be all those selected, and set the
625         error lines to empty.
626
627         Following this, calls to PrintResultSummary() will use this
628         information to work out what has changed.
629
630         Args:
631             board_selected: Dict containing boards to summarise, keyed by
632                 board.target
633         """
634         self._base_board_dict = {}
635         for board in board_selected:
636             self._base_board_dict[board] = Builder.Outcome(0, [], [], {})
637         self._base_err_lines = []
638
639     def PrintFuncSizeDetail(self, fname, old, new):
640         grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
641         delta, common = [], {}
642
643         for a in old:
644             if a in new:
645                 common[a] = 1
646
647         for name in old:
648             if name not in common:
649                 remove += 1
650                 down += old[name]
651                 delta.append([-old[name], name])
652
653         for name in new:
654             if name not in common:
655                 add += 1
656                 up += new[name]
657                 delta.append([new[name], name])
658
659         for name in common:
660                 diff = new.get(name, 0) - old.get(name, 0)
661                 if diff > 0:
662                     grow, up = grow + 1, up + diff
663                 elif diff < 0:
664                     shrink, down = shrink + 1, down - diff
665                 delta.append([diff, name])
666
667         delta.sort()
668         delta.reverse()
669
670         args = [add, -remove, grow, -shrink, up, -down, up - down]
671         if max(args) == 0:
672             return
673         args = [self.ColourNum(x) for x in args]
674         indent = ' ' * 15
675         print ('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
676                tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
677         print '%s  %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
678                                         'delta')
679         for diff, name in delta:
680             if diff:
681                 color = self.col.RED if diff > 0 else self.col.GREEN
682                 msg = '%s  %-38s %7s %7s %+7d' % (indent, name,
683                         old.get(name, '-'), new.get(name,'-'), diff)
684                 print self.col.Color(color, msg)
685
686
687     def PrintSizeDetail(self, target_list, show_bloat):
688         """Show details size information for each board
689
690         Args:
691             target_list: List of targets, each a dict containing:
692                     'target': Target name
693                     'total_diff': Total difference in bytes across all areas
694                     <part_name>: Difference for that part
695             show_bloat: Show detail for each function
696         """
697         targets_by_diff = sorted(target_list, reverse=True,
698         key=lambda x: x['_total_diff'])
699         for result in targets_by_diff:
700             printed_target = False
701             for name in sorted(result):
702                 diff = result[name]
703                 if name.startswith('_'):
704                     continue
705                 if diff != 0:
706                     color = self.col.RED if diff > 0 else self.col.GREEN
707                 msg = ' %s %+d' % (name, diff)
708                 if not printed_target:
709                     print '%10s  %-15s:' % ('', result['_target']),
710                     printed_target = True
711                 print self.col.Color(color, msg),
712             if printed_target:
713                 print
714                 if show_bloat:
715                     target = result['_target']
716                     outcome = result['_outcome']
717                     base_outcome = self._base_board_dict[target]
718                     for fname in outcome.func_sizes:
719                         self.PrintFuncSizeDetail(fname,
720                                                  base_outcome.func_sizes[fname],
721                                                  outcome.func_sizes[fname])
722
723
724     def PrintSizeSummary(self, board_selected, board_dict, show_detail,
725                          show_bloat):
726         """Print a summary of image sizes broken down by section.
727
728         The summary takes the form of one line per architecture. The
729         line contains deltas for each of the sections (+ means the section
730         got bigger, - means smaller). The nunmbers are the average number
731         of bytes that a board in this section increased by.
732
733         For example:
734            powerpc: (622 boards)   text -0.0
735           arm: (285 boards)   text -0.0
736           nds32: (3 boards)   text -8.0
737
738         Args:
739             board_selected: Dict containing boards to summarise, keyed by
740                 board.target
741             board_dict: Dict containing boards for which we built this
742                 commit, keyed by board.target. The value is an Outcome object.
743             show_detail: Show detail for each board
744             show_bloat: Show detail for each function
745         """
746         arch_list = {}
747         arch_count = {}
748
749         # Calculate changes in size for different image parts
750         # The previous sizes are in Board.sizes, for each board
751         for target in board_dict:
752             if target not in board_selected:
753                 continue
754             base_sizes = self._base_board_dict[target].sizes
755             outcome = board_dict[target]
756             sizes = outcome.sizes
757
758             # Loop through the list of images, creating a dict of size
759             # changes for each image/part. We end up with something like
760             # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
761             # which means that U-Boot data increased by 5 bytes and SPL
762             # text decreased by 4.
763             err = {'_target' : target}
764             for image in sizes:
765                 if image in base_sizes:
766                     base_image = base_sizes[image]
767                     # Loop through the text, data, bss parts
768                     for part in sorted(sizes[image]):
769                         diff = sizes[image][part] - base_image[part]
770                         col = None
771                         if diff:
772                             if image == 'u-boot':
773                                 name = part
774                             else:
775                                 name = image + ':' + part
776                             err[name] = diff
777             arch = board_selected[target].arch
778             if not arch in arch_count:
779                 arch_count[arch] = 1
780             else:
781                 arch_count[arch] += 1
782             if not sizes:
783                 pass    # Only add to our list when we have some stats
784             elif not arch in arch_list:
785                 arch_list[arch] = [err]
786             else:
787                 arch_list[arch].append(err)
788
789         # We now have a list of image size changes sorted by arch
790         # Print out a summary of these
791         for arch, target_list in arch_list.iteritems():
792             # Get total difference for each type
793             totals = {}
794             for result in target_list:
795                 total = 0
796                 for name, diff in result.iteritems():
797                     if name.startswith('_'):
798                         continue
799                     total += diff
800                     if name in totals:
801                         totals[name] += diff
802                     else:
803                         totals[name] = diff
804                 result['_total_diff'] = total
805                 result['_outcome'] = board_dict[result['_target']]
806
807             count = len(target_list)
808             printed_arch = False
809             for name in sorted(totals):
810                 diff = totals[name]
811                 if diff:
812                     # Display the average difference in this name for this
813                     # architecture
814                     avg_diff = float(diff) / count
815                     color = self.col.RED if avg_diff > 0 else self.col.GREEN
816                     msg = ' %s %+1.1f' % (name, avg_diff)
817                     if not printed_arch:
818                         print '%10s: (for %d/%d boards)' % (arch, count,
819                                 arch_count[arch]),
820                         printed_arch = True
821                     print self.col.Color(color, msg),
822
823             if printed_arch:
824                 print
825                 if show_detail:
826                     self.PrintSizeDetail(target_list, show_bloat)
827
828
829     def PrintResultSummary(self, board_selected, board_dict, err_lines,
830                            show_sizes, show_detail, show_bloat):
831         """Compare results with the base results and display delta.
832
833         Only boards mentioned in board_selected will be considered. This
834         function is intended to be called repeatedly with the results of
835         each commit. It therefore shows a 'diff' between what it saw in
836         the last call and what it sees now.
837
838         Args:
839             board_selected: Dict containing boards to summarise, keyed by
840                 board.target
841             board_dict: Dict containing boards for which we built this
842                 commit, keyed by board.target. The value is an Outcome object.
843             err_lines: A list of errors for this commit, or [] if there is
844                 none, or we don't want to print errors
845             show_sizes: Show image size deltas
846             show_detail: Show detail for each board
847             show_bloat: Show detail for each function
848         """
849         better = []     # List of boards fixed since last commit
850         worse = []      # List of new broken boards since last commit
851         new = []        # List of boards that didn't exist last time
852         unknown = []    # List of boards that were not built
853
854         for target in board_dict:
855             if target not in board_selected:
856                 continue
857
858             # If the board was built last time, add its outcome to a list
859             if target in self._base_board_dict:
860                 base_outcome = self._base_board_dict[target].rc
861                 outcome = board_dict[target]
862                 if outcome.rc == OUTCOME_UNKNOWN:
863                     unknown.append(target)
864                 elif outcome.rc < base_outcome:
865                     better.append(target)
866                 elif outcome.rc > base_outcome:
867                     worse.append(target)
868             else:
869                 new.append(target)
870
871         # Get a list of errors that have appeared, and disappeared
872         better_err = []
873         worse_err = []
874         for line in err_lines:
875             if line not in self._base_err_lines:
876                 worse_err.append('+' + line)
877         for line in self._base_err_lines:
878             if line not in err_lines:
879                 better_err.append('-' + line)
880
881         # Display results by arch
882         if better or worse or unknown or new or worse_err or better_err:
883             arch_list = {}
884             self.AddOutcome(board_selected, arch_list, better, '',
885                     self.col.GREEN)
886             self.AddOutcome(board_selected, arch_list, worse, '+',
887                     self.col.RED)
888             self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE)
889             if self._show_unknown:
890                 self.AddOutcome(board_selected, arch_list, unknown, '?',
891                         self.col.MAGENTA)
892             for arch, target_list in arch_list.iteritems():
893                 print '%10s: %s' % (arch, target_list)
894             if better_err:
895                 print self.col.Color(self.col.GREEN, '\n'.join(better_err))
896             if worse_err:
897                 print self.col.Color(self.col.RED, '\n'.join(worse_err))
898
899         if show_sizes:
900             self.PrintSizeSummary(board_selected, board_dict, show_detail,
901                                   show_bloat)
902
903         # Save our updated information for the next call to this function
904         self._base_board_dict = board_dict
905         self._base_err_lines = err_lines
906
907         # Get a list of boards that did not get built, if needed
908         not_built = []
909         for board in board_selected:
910             if not board in board_dict:
911                 not_built.append(board)
912         if not_built:
913             print "Boards not built (%d): %s" % (len(not_built),
914                     ', '.join(not_built))
915
916     def ProduceResultSummary(self, commit_upto, commits, board_selected):
917             board_dict, err_lines = self.GetResultSummary(board_selected,
918                     commit_upto, read_func_sizes=self._show_bloat)
919             if commits:
920                 msg = '%02d: %s' % (commit_upto + 1,
921                         commits[commit_upto].subject)
922                 print self.col.Color(self.col.BLUE, msg)
923             self.PrintResultSummary(board_selected, board_dict,
924                     err_lines if self._show_errors else [],
925                     self._show_sizes, self._show_detail, self._show_bloat)
926
927     def ShowSummary(self, commits, board_selected):
928         """Show a build summary for U-Boot for a given board list.
929
930         Reset the result summary, then repeatedly call GetResultSummary on
931         each commit's results, then display the differences we see.
932
933         Args:
934             commit: Commit objects to summarise
935             board_selected: Dict containing boards to summarise
936         """
937         self.commit_count = len(commits) if commits else 1
938         self.commits = commits
939         self.ResetResultSummary(board_selected)
940
941         for commit_upto in range(0, self.commit_count, self._step):
942             self.ProduceResultSummary(commit_upto, commits, board_selected)
943
944
945     def SetupBuild(self, board_selected, commits):
946         """Set up ready to start a build.
947
948         Args:
949             board_selected: Selected boards to build
950             commits: Selected commits to build
951         """
952         # First work out how many commits we will build
953         count = (self.commit_count + self._step - 1) / self._step
954         self.count = len(board_selected) * count
955         self.upto = self.warned = self.fail = 0
956         self._timestamps = collections.deque()
957
958     def BuildBoardsForCommit(self, board_selected, keep_outputs):
959         """Build all boards for a single commit"""
960         self.SetupBuild(board_selected)
961         self.count = len(board_selected)
962         for brd in board_selected.itervalues():
963             job = BuilderJob()
964             job.board = brd
965             job.commits = None
966             job.keep_outputs = keep_outputs
967             self.queue.put(brd)
968
969         self.queue.join()
970         self.out_queue.join()
971         print
972         self.ClearLine(0)
973
974     def BuildCommits(self, commits, board_selected, show_errors, keep_outputs):
975         """Build all boards for all commits (non-incremental)"""
976         self.commit_count = len(commits)
977
978         self.ResetResultSummary(board_selected)
979         for self.commit_upto in range(self.commit_count):
980             self.SelectCommit(commits[self.commit_upto])
981             self.SelectOutputDir()
982             builderthread.Mkdir(self.output_dir)
983
984             self.BuildBoardsForCommit(board_selected, keep_outputs)
985             board_dict, err_lines = self.GetResultSummary()
986             self.PrintResultSummary(board_selected, board_dict,
987                 err_lines if show_errors else [])
988
989         if self.already_done:
990             print '%d builds already done' % self.already_done
991
992     def GetThreadDir(self, thread_num):
993         """Get the directory path to the working dir for a thread.
994
995         Args:
996             thread_num: Number of thread to check.
997         """
998         return os.path.join(self._working_dir, '%02d' % thread_num)
999
1000     def _PrepareThread(self, thread_num, setup_git):
1001         """Prepare the working directory for a thread.
1002
1003         This clones or fetches the repo into the thread's work directory.
1004
1005         Args:
1006             thread_num: Thread number (0, 1, ...)
1007             setup_git: True to set up a git repo clone
1008         """
1009         thread_dir = self.GetThreadDir(thread_num)
1010         builderthread.Mkdir(thread_dir)
1011         git_dir = os.path.join(thread_dir, '.git')
1012
1013         # Clone the repo if it doesn't already exist
1014         # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1015         # we have a private index but uses the origin repo's contents?
1016         if setup_git and self.git_dir:
1017             src_dir = os.path.abspath(self.git_dir)
1018             if os.path.exists(git_dir):
1019                 gitutil.Fetch(git_dir, thread_dir)
1020             else:
1021                 print 'Cloning repo for thread %d' % thread_num
1022                 gitutil.Clone(src_dir, thread_dir)
1023
1024     def _PrepareWorkingSpace(self, max_threads, setup_git):
1025         """Prepare the working directory for use.
1026
1027         Set up the git repo for each thread.
1028
1029         Args:
1030             max_threads: Maximum number of threads we expect to need.
1031             setup_git: True to set up a git repo clone
1032         """
1033         builderthread.Mkdir(self._working_dir)
1034         for thread in range(max_threads):
1035             self._PrepareThread(thread, setup_git)
1036
1037     def _PrepareOutputSpace(self):
1038         """Get the output directories ready to receive files.
1039
1040         We delete any output directories which look like ones we need to
1041         create. Having left over directories is confusing when the user wants
1042         to check the output manually.
1043         """
1044         dir_list = []
1045         for commit_upto in range(self.commit_count):
1046             dir_list.append(self._GetOutputDir(commit_upto))
1047
1048         for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1049             if dirname not in dir_list:
1050                 shutil.rmtree(dirname)
1051
1052     def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
1053         """Build all commits for a list of boards
1054
1055         Args:
1056             commits: List of commits to be build, each a Commit object
1057             boards_selected: Dict of selected boards, key is target name,
1058                     value is Board object
1059             keep_outputs: True to save build output files
1060             verbose: Display build results as they are completed
1061         """
1062         self.commit_count = len(commits) if commits else 1
1063         self.commits = commits
1064         self._verbose = verbose
1065
1066         self.ResetResultSummary(board_selected)
1067         builderthread.Mkdir(self.base_dir)
1068         self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1069                 commits is not None)
1070         self._PrepareOutputSpace()
1071         self.SetupBuild(board_selected, commits)
1072         self.ProcessResult(None)
1073
1074         # Create jobs to build all commits for each board
1075         for brd in board_selected.itervalues():
1076             job = builderthread.BuilderJob()
1077             job.board = brd
1078             job.commits = commits
1079             job.keep_outputs = keep_outputs
1080             job.step = self._step
1081             self.queue.put(job)
1082
1083         # Wait until all jobs are started
1084         self.queue.join()
1085
1086         # Wait until we have processed all output
1087         self.out_queue.join()
1088         print
1089         self.ClearLine(0)