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