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