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