1 # Copyright (c) 2013 The Chromium OS Authors.
3 # Bloat-o-meter code used here Copyright 2004 Matt Mackall <mpm@selenic.com>
5 # SPDX-License-Identifier: GPL-2.0+
10 from datetime import datetime, timedelta
30 Please see README for user documentation, and you should be familiar with
31 that before trying to make sense of this.
33 Buildman works by keeping the machine as busy as possible, building different
34 commits for different boards on multiple CPUs at once.
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
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
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.
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
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
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.
65 Buildman also create working directories for each thread, in a .bm-work/
66 subdirectory in the base dir.
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
72 us-net/ base directory
73 01_of_02_g4ed4ebc_net--Add-tftp-speed-/
78 02_of_02_g4ed4ebc_net--Check-tftp-comp/
84 00/ working directory for thread 0 (contains source checkout)
86 01/ working directory for thread 1
89 u-boot/ source directory
93 # Possible build outcomes
94 OUTCOME_OK, OUTCOME_WARNING, OUTCOME_ERROR, OUTCOME_UNKNOWN = range(4)
96 # Translate a commit subject into a valid filename
97 trans_valid_chars = string.maketrans("/: ", "---")
101 """Make a directory if it doesn't already exist.
104 dirname: Directory to create
108 except OSError as err:
109 if err.errno == errno.EEXIST:
115 """Holds information about a job to be performed by a thread
118 board: Board object to build
119 commits: List of commit options to build.
126 class ResultThread(threading.Thread):
127 """This thread processes results from builder threads.
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.
132 def __init__(self, builder):
133 """Set up a new result thread
136 builder: Builder which will be sent each result
138 threading.Thread.__init__(self)
139 self.builder = builder
142 """Called to start up the result thread.
144 We collect the next result job and pass it on to the build.
147 result = self.builder.out_queue.get()
148 self.builder.ProcessResult(result)
149 self.builder.out_queue.task_done()
152 class BuilderThread(threading.Thread):
153 """This thread builds U-Boot for a particular board.
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.
159 builder: The builder which contains information we might need
160 thread_num: Our thread number (0-n-1), used to decide on a
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
169 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
170 """Run 'make' on a particular commit and board.
172 The source code will already be checked out, so the 'commit'
173 argument is only for information.
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()
188 return self.builder.do_make(commit, brd, stage, cwd, *args,
191 def RunCommit(self, commit_upto, brd, work_dir, do_config, force_build,
192 force_build_failures):
193 """Build a particular commit.
195 If the build is already done, and we are not forcing a build, we skip
196 the build and just return the previously-saved results.
199 commit_upto: Commit number to build (0...n-1)
200 brd: Board object to build
201 work_dir: Directory to which the source will be checked out
202 do_config: True to run a make <board>_config on the source
203 force_build: Force a build even if one was previously done
204 force_build_failures: Force a bulid if the previous result showed
209 - CommandResult object containing the results of the build
210 - boolean indicating whether 'make config' is still needed
212 # Create a default result - it will be overwritte by the call to
213 # self.Make() below, in the event that we do a build.
214 result = command.CommandResult()
215 result.return_code = 0
216 if self.builder.in_tree:
219 out_dir = os.path.join(work_dir, 'build')
221 # Check if the job was already completed last time
222 done_file = self.builder.GetDoneFile(commit_upto, brd.target)
223 result.already_done = os.path.exists(done_file)
224 will_build = (force_build or force_build_failures or
225 not result.already_done)
226 if result.already_done and will_build:
227 # Get the return code from that build and use it
228 with open(done_file, 'r') as fd:
229 result.return_code = int(fd.readline())
230 err_file = self.builder.GetErrFile(commit_upto, brd.target)
231 if os.path.exists(err_file) and os.stat(err_file).st_size:
232 result.stderr = 'bad'
233 elif not force_build:
234 # The build passed, so no need to build it again
238 # We are going to have to build it. First, get a toolchain
239 if not self.toolchain:
241 self.toolchain = self.builder.toolchains.Select(brd.arch)
242 except ValueError as err:
243 result.return_code = 10
245 result.stderr = str(err)
246 # TODO(sjg@chromium.org): This gets swallowed, but needs
250 # Checkout the right commit
251 if commit_upto is not None:
252 commit = self.builder.commits[commit_upto]
253 if self.builder.checkout:
254 git_dir = os.path.join(work_dir, '.git')
255 gitutil.Checkout(commit.hash, git_dir, work_dir,
258 commit = self.builder.commit # Ick, fix this for BuildCommits()
260 # Set up the environment and command line
261 env = self.toolchain.MakeEnvironment()
264 if not self.builder.in_tree:
265 args.append('O=build')
267 if self.builder.num_jobs is not None:
268 args.extend(['-j', str(self.builder.num_jobs)])
269 config_args = ['%s_config' % brd.target]
271 args.extend(self.builder.toolchains.GetMakeArguments(brd))
273 # If we need to reconfigure, do that now
275 result = self.Make(commit, brd, 'distclean', work_dir,
276 'distclean', *args, env=env)
277 result = self.Make(commit, brd, 'config', work_dir,
278 *(args + config_args), env=env)
279 config_out = result.combined
280 do_config = False # No need to configure next time
281 if result.return_code == 0:
282 result = self.Make(commit, brd, 'build', work_dir, *args,
284 result.stdout = config_out + result.stdout
286 result.return_code = 1
287 result.stderr = 'No tool chain for %s\n' % brd.arch
288 result.already_done = False
290 result.toolchain = self.toolchain
292 result.commit_upto = commit_upto
293 result.out_dir = out_dir
294 return result, do_config
296 def _WriteResult(self, result, keep_outputs):
297 """Write a built result to the output directory.
300 result: CommandResult object containing result to write
301 keep_outputs: True to store the output binaries, False
305 if result.return_code < 0:
309 if result.stderr and 'No child processes' in result.stderr:
312 if result.already_done:
315 # Write the output and stderr
316 output_dir = self.builder._GetOutputDir(result.commit_upto)
318 build_dir = self.builder.GetBuildDir(result.commit_upto,
322 outfile = os.path.join(build_dir, 'log')
323 with open(outfile, 'w') as fd:
325 fd.write(result.stdout)
327 errfile = self.builder.GetErrFile(result.commit_upto,
330 with open(errfile, 'w') as fd:
331 fd.write(result.stderr)
332 elif os.path.exists(errfile):
336 # Write the build result and toolchain information.
337 done_file = self.builder.GetDoneFile(result.commit_upto,
339 with open(done_file, 'w') as fd:
340 fd.write('%s' % result.return_code)
341 with open(os.path.join(build_dir, 'toolchain'), 'w') as fd:
342 print >>fd, 'gcc', result.toolchain.gcc
343 print >>fd, 'path', result.toolchain.path
344 print >>fd, 'cross', result.toolchain.cross
345 print >>fd, 'arch', result.toolchain.arch
346 fd.write('%s' % result.return_code)
348 with open(os.path.join(build_dir, 'toolchain'), 'w') as fd:
349 print >>fd, 'gcc', result.toolchain.gcc
350 print >>fd, 'path', result.toolchain.path
352 # Write out the image and function size information and an objdump
353 env = result.toolchain.MakeEnvironment()
355 for fname in ['u-boot', 'spl/u-boot-spl']:
356 cmd = ['%snm' % self.toolchain.cross, '--size-sort', fname]
357 nm_result = command.RunPipe([cmd], capture=True,
358 capture_stderr=True, cwd=result.out_dir,
359 raise_on_error=False, env=env)
361 nm = self.builder.GetFuncSizesFile(result.commit_upto,
362 result.brd.target, fname)
363 with open(nm, 'w') as fd:
364 print >>fd, nm_result.stdout,
366 cmd = ['%sobjdump' % self.toolchain.cross, '-h', fname]
367 dump_result = command.RunPipe([cmd], capture=True,
368 capture_stderr=True, cwd=result.out_dir,
369 raise_on_error=False, env=env)
371 if dump_result.stdout:
372 objdump = self.builder.GetObjdumpFile(result.commit_upto,
373 result.brd.target, fname)
374 with open(objdump, 'w') as fd:
375 print >>fd, dump_result.stdout,
376 for line in dump_result.stdout.splitlines():
377 fields = line.split()
378 if len(fields) > 5 and fields[1] == '.rodata':
379 rodata_size = fields[2]
381 cmd = ['%ssize' % self.toolchain.cross, fname]
382 size_result = command.RunPipe([cmd], capture=True,
383 capture_stderr=True, cwd=result.out_dir,
384 raise_on_error=False, env=env)
385 if size_result.stdout:
386 lines.append(size_result.stdout.splitlines()[1] + ' ' +
389 # Write out the image sizes file. This is similar to the output
390 # of binutil's 'size' utility, but it omits the header line and
391 # adds an additional hex value at the end of each line for the
394 sizes = self.builder.GetSizesFile(result.commit_upto,
396 with open(sizes, 'w') as fd:
397 print >>fd, '\n'.join(lines)
399 # Now write the actual build output
401 patterns = ['u-boot', '*.bin', 'u-boot.dtb', '*.map',
402 'include/autoconf.mk', 'spl/u-boot-spl',
403 'spl/u-boot-spl.bin']
404 for pattern in patterns:
405 file_list = glob.glob(os.path.join(result.out_dir, pattern))
406 for fname in file_list:
407 shutil.copy(fname, build_dir)
410 def RunJob(self, job):
413 A job consists of a building a list of commits for a particular board.
419 work_dir = self.builder.GetThreadDir(self.thread_num)
420 self.toolchain = None
422 # Run 'make board_config' on the first commit
426 for commit_upto in range(0, len(job.commits), job.step):
427 result, request_config = self.RunCommit(commit_upto, brd,
429 force_build or self.builder.force_build,
430 self.builder.force_build_failures)
431 failed = result.return_code or result.stderr
432 did_config = do_config
433 if failed and not do_config:
434 # If our incremental build failed, try building again
436 if self.builder.force_config_on_failure:
437 result, request_config = self.RunCommit(commit_upto,
438 brd, work_dir, True, True, False)
440 if not self.builder.force_reconfig:
441 do_config = request_config
443 # If we built that commit, then config is done. But if we got
444 # an warning, reconfig next time to force it to build the same
445 # files that created warnings this time. Otherwise an
446 # incremental build may not build the same file, and we will
447 # think that the warning has gone away.
448 # We could avoid this by using -Werror everywhere...
449 # For errors, the problem doesn't happen, since presumably
450 # the build stopped and didn't generate output, so will retry
451 # that file next time. So we could detect warnings and deal
452 # with them specially here. For now, we just reconfigure if
453 # anything goes work.
454 # Of course this is substantially slower if there are build
455 # errors/warnings (e.g. 2-3x slower even if only 10% of builds
457 if (failed and not result.already_done and not did_config and
458 self.builder.force_config_on_failure):
459 # If this build failed, try the next one with a
461 # Sometimes if the board_config.h file changes it can mess
462 # with dependencies, and we get:
463 # make: *** No rule to make target `include/autoconf.mk',
464 # needed by `depend'.
469 if self.builder.force_config_on_failure:
472 result.commit_upto = commit_upto
473 if result.return_code < 0:
474 raise ValueError('Interrupt')
476 # We have the build results, so output the result
477 self._WriteResult(result, job.keep_outputs)
478 self.builder.out_queue.put(result)
480 # Just build the currently checked-out build
481 result = self.RunCommit(None, True)
482 result.commit_upto = self.builder.upto
483 self.builder.out_queue.put(result)
486 """Our thread's run function
488 This thread picks a job from the queue, runs it, and then goes to the
493 job = self.builder.queue.get()
495 if self.builder.active and alive:
497 except Exception as err:
500 self.builder.queue.task_done()
504 """Class for building U-Boot for a particular commit.
506 Public members: (many should ->private)
507 active: True if the builder is active and has not been stopped
508 already_done: Number of builds already completed
509 base_dir: Base directory to use for builder
510 checkout: True to check out source, False to skip that step.
511 This is used for testing.
512 col: terminal.Color() object
513 count: Number of commits to build
514 do_make: Method to call to invoke Make
515 fail: Number of builds that failed due to error
516 force_build: Force building even if a build already exists
517 force_config_on_failure: If a commit fails for a board, disable
518 incremental building for the next commit we build for that
519 board, so that we will see all warnings/errors again.
520 force_build_failures: If a previously-built build (i.e. built on
521 a previous run of buildman) is marked as failed, rebuild it.
522 git_dir: Git directory containing source repository
523 last_line_len: Length of the last line we printed (used for erasing
524 it with new progress information)
525 num_jobs: Number of jobs to run at once (passed to make as -j)
526 num_threads: Number of builder threads to run
527 out_queue: Queue of results to process
528 re_make_err: Compiled regular expression for ignore_lines
529 queue: Queue of jobs to run
530 threads: List of active threads
531 toolchains: Toolchains object to use for building
532 upto: Current commit number we are building (0.count-1)
533 warned: Number of builds that produced at least one warning
534 force_reconfig: Reconfigure U-Boot on each comiit. This disables
535 incremental building, where buildman reconfigures on the first
536 commit for a baord, and then just does an incremental build for
537 the following commits. In fact buildman will reconfigure and
538 retry for any failing commits, so generally the only effect of
539 this option is to slow things down.
540 in_tree: Build U-Boot in-tree instead of specifying an output
541 directory separate from the source code. This option is really
542 only useful for testing in-tree builds.
545 _base_board_dict: Last-summarised Dict of boards
546 _base_err_lines: Last-summarised list of errors
547 _build_period_us: Time taken for a single build (float object).
548 _complete_delay: Expected delay until completion (timedelta)
549 _next_delay_update: Next time we plan to display a progress update
551 _show_unknown: Show unknown boards (those not built) in summary
552 _timestamps: List of timestamps for the completion of the last
553 last _timestamp_count builds. Each is a datetime object.
554 _timestamp_count: Number of timestamps to keep in our list.
555 _working_dir: Base working directory containing all threads
558 """Records a build outcome for a single make invocation
561 rc: Outcome value (OUTCOME_...)
562 err_lines: List of error lines or [] if none
563 sizes: Dictionary of image size information, keyed by filename
564 - Each value is itself a dictionary containing
565 values for 'text', 'data' and 'bss', being the integer
566 size in bytes of each section.
567 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
568 value is itself a dictionary:
570 value: Size of function in bytes
572 def __init__(self, rc, err_lines, sizes, func_sizes):
574 self.err_lines = err_lines
576 self.func_sizes = func_sizes
578 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
579 gnu_make='make', checkout=True, show_unknown=True, step=1):
580 """Create a new Builder object
583 toolchains: Toolchains object to use for building
584 base_dir: Base directory to use for builder
585 git_dir: Git directory containing source repository
586 num_threads: Number of builder threads to run
587 num_jobs: Number of jobs to run at once (passed to make as -j)
588 gnu_make: the command name of GNU Make.
589 checkout: True to check out source, False to skip that step.
590 This is used for testing.
591 show_unknown: Show unknown boards (those not built) in summary
592 step: 1 to process every commit, n to process every nth commit
594 self.toolchains = toolchains
595 self.base_dir = base_dir
596 self._working_dir = os.path.join(base_dir, '.bm-work')
599 self.do_make = self.Make
600 self.gnu_make = gnu_make
601 self.checkout = checkout
602 self.num_threads = num_threads
603 self.num_jobs = num_jobs
604 self.already_done = 0
605 self.force_build = False
606 self.git_dir = git_dir
607 self._show_unknown = show_unknown
608 self._timestamp_count = 10
609 self._build_period_us = None
610 self._complete_delay = None
611 self._next_delay_update = datetime.now()
612 self.force_config_on_failure = True
613 self.force_build_failures = False
614 self.force_reconfig = False
618 self.col = terminal.Color()
620 self.queue = Queue.Queue()
621 self.out_queue = Queue.Queue()
622 for i in range(self.num_threads):
623 t = BuilderThread(self, i)
626 self.threads.append(t)
628 self.last_line_len = 0
629 t = ResultThread(self)
632 self.threads.append(t)
634 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
635 self.re_make_err = re.compile('|'.join(ignore_lines))
638 """Get rid of all threads created by the builder"""
639 for t in self.threads:
642 def _AddTimestamp(self):
643 """Add a new timestamp to the list and record the build period.
645 The build period is the length of time taken to perform a single
646 build (one board, one commit).
649 self._timestamps.append(now)
650 count = len(self._timestamps)
651 delta = self._timestamps[-1] - self._timestamps[0]
652 seconds = delta.total_seconds()
654 # If we have enough data, estimate build period (time taken for a
655 # single build) and therefore completion time.
656 if count > 1 and self._next_delay_update < now:
657 self._next_delay_update = now + timedelta(seconds=2)
659 self._build_period = float(seconds) / count
660 todo = self.count - self.upto
661 self._complete_delay = timedelta(microseconds=
662 self._build_period * todo * 1000000)
664 self._complete_delay -= timedelta(
665 microseconds=self._complete_delay.microseconds)
668 self._timestamps.popleft()
671 def ClearLine(self, length):
672 """Clear any characters on the current line
674 Make way for a new line of length 'length', by outputting enough
675 spaces to clear out the old line. Then remember the new length for
679 length: Length of new line, in characters
681 if length < self.last_line_len:
682 print ' ' * (self.last_line_len - length),
684 self.last_line_len = length
687 def SelectCommit(self, commit, checkout=True):
688 """Checkout the selected commit for this build
691 if checkout and self.checkout:
692 gitutil.Checkout(commit.hash)
694 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
698 commit: Commit object that is being built
699 brd: Board object that is being built
700 stage: Stage that we are at (distclean, config, build)
701 cwd: Directory where make should be run
702 args: Arguments to pass to make
703 kwargs: Arguments to pass to command.RunPipe()
705 cmd = [self.gnu_make] + list(args)
706 result = command.RunPipe([cmd], capture=True, capture_stderr=True,
707 cwd=cwd, raise_on_error=False, **kwargs)
710 def ProcessResult(self, result):
711 """Process the result of a build, showing progress information
714 result: A CommandResult object
716 col = terminal.Color()
718 target = result.brd.target
720 if result.return_code < 0:
726 if result.return_code != 0:
730 if result.already_done:
731 self.already_done += 1
733 target = '(starting)'
735 # Display separate counts for ok, warned and fail
736 ok = self.upto - self.warned - self.fail
737 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
738 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
739 line += self.col.Color(self.col.RED, '%5d' % self.fail)
741 name = ' /%-5d ' % self.count
743 # Add our current completion time estimate
745 if self._complete_delay:
746 name += '%s : ' % self._complete_delay
747 # When building all boards for a commit, we can print a commit
749 if result and result.commit_upto is None:
750 name += 'commit %2d/%-3d' % (self.commit_upto + 1,
755 length = 13 + len(name)
756 self.ClearLine(length)
758 def _GetOutputDir(self, commit_upto):
759 """Get the name of the output directory for a commit number
761 The output directory is typically .../<branch>/<commit>.
764 commit_upto: Commit number to use (0..self.count-1)
766 commit = self.commits[commit_upto]
767 subject = commit.subject.translate(trans_valid_chars)
768 commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
769 self.commit_count, commit.hash, subject[:20]))
770 output_dir = os.path.join(self.base_dir, commit_dir)
773 def GetBuildDir(self, commit_upto, target):
774 """Get the name of the build directory for a commit number
776 The build directory is typically .../<branch>/<commit>/<target>.
779 commit_upto: Commit number to use (0..self.count-1)
782 output_dir = self._GetOutputDir(commit_upto)
783 return os.path.join(output_dir, target)
785 def GetDoneFile(self, commit_upto, target):
786 """Get the name of the done file for a commit number
789 commit_upto: Commit number to use (0..self.count-1)
792 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
794 def GetSizesFile(self, commit_upto, target):
795 """Get the name of the sizes file for a commit number
798 commit_upto: Commit number to use (0..self.count-1)
801 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
803 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
804 """Get the name of the funcsizes file for a commit number and ELF file
807 commit_upto: Commit number to use (0..self.count-1)
809 elf_fname: Filename of elf image
811 return os.path.join(self.GetBuildDir(commit_upto, target),
812 '%s.sizes' % elf_fname.replace('/', '-'))
814 def GetObjdumpFile(self, commit_upto, target, elf_fname):
815 """Get the name of the objdump file for a commit number and ELF file
818 commit_upto: Commit number to use (0..self.count-1)
820 elf_fname: Filename of elf image
822 return os.path.join(self.GetBuildDir(commit_upto, target),
823 '%s.objdump' % elf_fname.replace('/', '-'))
825 def GetErrFile(self, commit_upto, target):
826 """Get the name of the err file for a commit number
829 commit_upto: Commit number to use (0..self.count-1)
832 output_dir = self.GetBuildDir(commit_upto, target)
833 return os.path.join(output_dir, 'err')
835 def FilterErrors(self, lines):
836 """Filter out errors in which we have no interest
838 We should probably use map().
841 lines: List of error lines, each a string
843 New list with only interesting lines included
847 if not self.re_make_err.search(line):
848 out_lines.append(line)
851 def ReadFuncSizes(self, fname, fd):
852 """Read function sizes from the output of 'nm'
855 fd: File containing data to read
856 fname: Filename we are reading from (just for errors)
859 Dictionary containing size of each function in bytes, indexed by
863 for line in fd.readlines():
865 size, type, name = line[:-1].split()
867 print "Invalid line in file '%s': '%s'" % (fname, line[:-1])
870 # function names begin with '.' on 64-bit powerpc
872 name = 'static.' + name.split('.')[0]
873 sym[name] = sym.get(name, 0) + int(size, 16)
876 def GetBuildOutcome(self, commit_upto, target, read_func_sizes):
877 """Work out the outcome of a build.
880 commit_upto: Commit number to check (0..n-1)
881 target: Target board to check
882 read_func_sizes: True to read function size information
887 done_file = self.GetDoneFile(commit_upto, target)
888 sizes_file = self.GetSizesFile(commit_upto, target)
891 if os.path.exists(done_file):
892 with open(done_file, 'r') as fd:
893 return_code = int(fd.readline())
895 err_file = self.GetErrFile(commit_upto, target)
896 if os.path.exists(err_file):
897 with open(err_file, 'r') as fd:
898 err_lines = self.FilterErrors(fd.readlines())
900 # Decide whether the build was ok, failed or created warnings
908 # Convert size information to our simple format
909 if os.path.exists(sizes_file):
910 with open(sizes_file, 'r') as fd:
911 for line in fd.readlines():
912 values = line.split()
915 rodata = int(values[6], 16)
917 'all' : int(values[0]) + int(values[1]) +
919 'text' : int(values[0]) - rodata,
920 'data' : int(values[1]),
921 'bss' : int(values[2]),
924 sizes[values[5]] = size_dict
927 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
928 for fname in glob.glob(pattern):
929 with open(fname, 'r') as fd:
930 dict_name = os.path.basename(fname).replace('.sizes',
932 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
934 return Builder.Outcome(rc, err_lines, sizes, func_sizes)
936 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {})
938 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes):
939 """Calculate a summary of the results of building a commit.
942 board_selected: Dict containing boards to summarise
943 commit_upto: Commit number to summarize (0..self.count-1)
944 read_func_sizes: True to read function size information
948 Dict containing boards which passed building this commit.
949 keyed by board.target
950 List containing a summary of error/warning lines
953 err_lines_summary = []
955 for board in boards_selected.itervalues():
956 outcome = self.GetBuildOutcome(commit_upto, board.target,
958 board_dict[board.target] = outcome
959 for err in outcome.err_lines:
960 if err and not err.rstrip() in err_lines_summary:
961 err_lines_summary.append(err.rstrip())
962 return board_dict, err_lines_summary
964 def AddOutcome(self, board_dict, arch_list, changes, char, color):
965 """Add an output to our list of outcomes for each architecture
967 This simple function adds failing boards (changes) to the
968 relevant architecture string, so we can print the results out
969 sorted by architecture.
972 board_dict: Dict containing all boards
973 arch_list: Dict keyed by arch name. Value is a string containing
974 a list of board names which failed for that arch.
975 changes: List of boards to add to arch_list
976 color: terminal.Colour object
979 for target in changes:
980 if target in board_dict:
981 arch = board_dict[target].arch
984 str = self.col.Color(color, ' ' + target)
985 if not arch in done_arch:
986 str = self.col.Color(color, char) + ' ' + str
987 done_arch[arch] = True
988 if not arch in arch_list:
989 arch_list[arch] = str
991 arch_list[arch] += str
994 def ColourNum(self, num):
995 color = self.col.RED if num > 0 else self.col.GREEN
998 return self.col.Color(color, str(num))
1000 def ResetResultSummary(self, board_selected):
1001 """Reset the results summary ready for use.
1003 Set up the base board list to be all those selected, and set the
1004 error lines to empty.
1006 Following this, calls to PrintResultSummary() will use this
1007 information to work out what has changed.
1010 board_selected: Dict containing boards to summarise, keyed by
1013 self._base_board_dict = {}
1014 for board in board_selected:
1015 self._base_board_dict[board] = Builder.Outcome(0, [], [], {})
1016 self._base_err_lines = []
1018 def PrintFuncSizeDetail(self, fname, old, new):
1019 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
1020 delta, common = [], {}
1027 if name not in common:
1030 delta.append([-old[name], name])
1033 if name not in common:
1036 delta.append([new[name], name])
1039 diff = new.get(name, 0) - old.get(name, 0)
1041 grow, up = grow + 1, up + diff
1043 shrink, down = shrink + 1, down - diff
1044 delta.append([diff, name])
1049 args = [add, -remove, grow, -shrink, up, -down, up - down]
1052 args = [self.ColourNum(x) for x in args]
1054 print ('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
1055 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
1056 print '%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
1058 for diff, name in delta:
1060 color = self.col.RED if diff > 0 else self.col.GREEN
1061 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
1062 old.get(name, '-'), new.get(name,'-'), diff)
1063 print self.col.Color(color, msg)
1066 def PrintSizeDetail(self, target_list, show_bloat):
1067 """Show details size information for each board
1070 target_list: List of targets, each a dict containing:
1071 'target': Target name
1072 'total_diff': Total difference in bytes across all areas
1073 <part_name>: Difference for that part
1074 show_bloat: Show detail for each function
1076 targets_by_diff = sorted(target_list, reverse=True,
1077 key=lambda x: x['_total_diff'])
1078 for result in targets_by_diff:
1079 printed_target = False
1080 for name in sorted(result):
1082 if name.startswith('_'):
1085 color = self.col.RED if diff > 0 else self.col.GREEN
1086 msg = ' %s %+d' % (name, diff)
1087 if not printed_target:
1088 print '%10s %-15s:' % ('', result['_target']),
1089 printed_target = True
1090 print self.col.Color(color, msg),
1094 target = result['_target']
1095 outcome = result['_outcome']
1096 base_outcome = self._base_board_dict[target]
1097 for fname in outcome.func_sizes:
1098 self.PrintFuncSizeDetail(fname,
1099 base_outcome.func_sizes[fname],
1100 outcome.func_sizes[fname])
1103 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
1105 """Print a summary of image sizes broken down by section.
1107 The summary takes the form of one line per architecture. The
1108 line contains deltas for each of the sections (+ means the section
1109 got bigger, - means smaller). The nunmbers are the average number
1110 of bytes that a board in this section increased by.
1113 powerpc: (622 boards) text -0.0
1114 arm: (285 boards) text -0.0
1115 nds32: (3 boards) text -8.0
1118 board_selected: Dict containing boards to summarise, keyed by
1120 board_dict: Dict containing boards for which we built this
1121 commit, keyed by board.target. The value is an Outcome object.
1122 show_detail: Show detail for each board
1123 show_bloat: Show detail for each function
1128 # Calculate changes in size for different image parts
1129 # The previous sizes are in Board.sizes, for each board
1130 for target in board_dict:
1131 if target not in board_selected:
1133 base_sizes = self._base_board_dict[target].sizes
1134 outcome = board_dict[target]
1135 sizes = outcome.sizes
1137 # Loop through the list of images, creating a dict of size
1138 # changes for each image/part. We end up with something like
1139 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
1140 # which means that U-Boot data increased by 5 bytes and SPL
1141 # text decreased by 4.
1142 err = {'_target' : target}
1144 if image in base_sizes:
1145 base_image = base_sizes[image]
1146 # Loop through the text, data, bss parts
1147 for part in sorted(sizes[image]):
1148 diff = sizes[image][part] - base_image[part]
1151 if image == 'u-boot':
1154 name = image + ':' + part
1156 arch = board_selected[target].arch
1157 if not arch in arch_count:
1158 arch_count[arch] = 1
1160 arch_count[arch] += 1
1162 pass # Only add to our list when we have some stats
1163 elif not arch in arch_list:
1164 arch_list[arch] = [err]
1166 arch_list[arch].append(err)
1168 # We now have a list of image size changes sorted by arch
1169 # Print out a summary of these
1170 for arch, target_list in arch_list.iteritems():
1171 # Get total difference for each type
1173 for result in target_list:
1175 for name, diff in result.iteritems():
1176 if name.startswith('_'):
1180 totals[name] += diff
1183 result['_total_diff'] = total
1184 result['_outcome'] = board_dict[result['_target']]
1186 count = len(target_list)
1187 printed_arch = False
1188 for name in sorted(totals):
1191 # Display the average difference in this name for this
1193 avg_diff = float(diff) / count
1194 color = self.col.RED if avg_diff > 0 else self.col.GREEN
1195 msg = ' %s %+1.1f' % (name, avg_diff)
1196 if not printed_arch:
1197 print '%10s: (for %d/%d boards)' % (arch, count,
1200 print self.col.Color(color, msg),
1205 self.PrintSizeDetail(target_list, show_bloat)
1208 def PrintResultSummary(self, board_selected, board_dict, err_lines,
1209 show_sizes, show_detail, show_bloat):
1210 """Compare results with the base results and display delta.
1212 Only boards mentioned in board_selected will be considered. This
1213 function is intended to be called repeatedly with the results of
1214 each commit. It therefore shows a 'diff' between what it saw in
1215 the last call and what it sees now.
1218 board_selected: Dict containing boards to summarise, keyed by
1220 board_dict: Dict containing boards for which we built this
1221 commit, keyed by board.target. The value is an Outcome object.
1222 err_lines: A list of errors for this commit, or [] if there is
1223 none, or we don't want to print errors
1224 show_sizes: Show image size deltas
1225 show_detail: Show detail for each board
1226 show_bloat: Show detail for each function
1228 better = [] # List of boards fixed since last commit
1229 worse = [] # List of new broken boards since last commit
1230 new = [] # List of boards that didn't exist last time
1231 unknown = [] # List of boards that were not built
1233 for target in board_dict:
1234 if target not in board_selected:
1237 # If the board was built last time, add its outcome to a list
1238 if target in self._base_board_dict:
1239 base_outcome = self._base_board_dict[target].rc
1240 outcome = board_dict[target]
1241 if outcome.rc == OUTCOME_UNKNOWN:
1242 unknown.append(target)
1243 elif outcome.rc < base_outcome:
1244 better.append(target)
1245 elif outcome.rc > base_outcome:
1246 worse.append(target)
1250 # Get a list of errors that have appeared, and disappeared
1253 for line in err_lines:
1254 if line not in self._base_err_lines:
1255 worse_err.append('+' + line)
1256 for line in self._base_err_lines:
1257 if line not in err_lines:
1258 better_err.append('-' + line)
1260 # Display results by arch
1261 if better or worse or unknown or new or worse_err or better_err:
1263 self.AddOutcome(board_selected, arch_list, better, '',
1265 self.AddOutcome(board_selected, arch_list, worse, '+',
1267 self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE)
1268 if self._show_unknown:
1269 self.AddOutcome(board_selected, arch_list, unknown, '?',
1271 for arch, target_list in arch_list.iteritems():
1272 print '%10s: %s' % (arch, target_list)
1274 print self.col.Color(self.col.GREEN, '\n'.join(better_err))
1276 print self.col.Color(self.col.RED, '\n'.join(worse_err))
1279 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1282 # Save our updated information for the next call to this function
1283 self._base_board_dict = board_dict
1284 self._base_err_lines = err_lines
1286 # Get a list of boards that did not get built, if needed
1288 for board in board_selected:
1289 if not board in board_dict:
1290 not_built.append(board)
1292 print "Boards not built (%d): %s" % (len(not_built),
1293 ', '.join(not_built))
1296 def ShowSummary(self, commits, board_selected, show_errors, show_sizes,
1297 show_detail, show_bloat):
1298 """Show a build summary for U-Boot for a given board list.
1300 Reset the result summary, then repeatedly call GetResultSummary on
1301 each commit's results, then display the differences we see.
1304 commit: Commit objects to summarise
1305 board_selected: Dict containing boards to summarise
1306 show_errors: Show errors that occured
1307 show_sizes: Show size deltas
1308 show_detail: Show detail for each board
1309 show_bloat: Show detail for each function
1311 self.commit_count = len(commits)
1312 self.commits = commits
1313 self.ResetResultSummary(board_selected)
1315 for commit_upto in range(0, self.commit_count, self._step):
1316 board_dict, err_lines = self.GetResultSummary(board_selected,
1317 commit_upto, read_func_sizes=show_bloat)
1318 msg = '%02d: %s' % (commit_upto + 1, commits[commit_upto].subject)
1319 print self.col.Color(self.col.BLUE, msg)
1320 self.PrintResultSummary(board_selected, board_dict,
1321 err_lines if show_errors else [], show_sizes, show_detail,
1325 def SetupBuild(self, board_selected, commits):
1326 """Set up ready to start a build.
1329 board_selected: Selected boards to build
1330 commits: Selected commits to build
1332 # First work out how many commits we will build
1333 count = (len(commits) + self._step - 1) / self._step
1334 self.count = len(board_selected) * count
1335 self.upto = self.warned = self.fail = 0
1336 self._timestamps = collections.deque()
1338 def BuildBoardsForCommit(self, board_selected, keep_outputs):
1339 """Build all boards for a single commit"""
1340 self.SetupBuild(board_selected)
1341 self.count = len(board_selected)
1342 for brd in board_selected.itervalues():
1346 job.keep_outputs = keep_outputs
1350 self.out_queue.join()
1354 def BuildCommits(self, commits, board_selected, show_errors, keep_outputs):
1355 """Build all boards for all commits (non-incremental)"""
1356 self.commit_count = len(commits)
1358 self.ResetResultSummary(board_selected)
1359 for self.commit_upto in range(self.commit_count):
1360 self.SelectCommit(commits[self.commit_upto])
1361 self.SelectOutputDir()
1362 Mkdir(self.output_dir)
1364 self.BuildBoardsForCommit(board_selected, keep_outputs)
1365 board_dict, err_lines = self.GetResultSummary()
1366 self.PrintResultSummary(board_selected, board_dict,
1367 err_lines if show_errors else [])
1369 if self.already_done:
1370 print '%d builds already done' % self.already_done
1372 def GetThreadDir(self, thread_num):
1373 """Get the directory path to the working dir for a thread.
1376 thread_num: Number of thread to check.
1378 return os.path.join(self._working_dir, '%02d' % thread_num)
1380 def _PrepareThread(self, thread_num):
1381 """Prepare the working directory for a thread.
1383 This clones or fetches the repo into the thread's work directory.
1386 thread_num: Thread number (0, 1, ...)
1388 thread_dir = self.GetThreadDir(thread_num)
1390 git_dir = os.path.join(thread_dir, '.git')
1392 # Clone the repo if it doesn't already exist
1393 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1394 # we have a private index but uses the origin repo's contents?
1396 src_dir = os.path.abspath(self.git_dir)
1397 if os.path.exists(git_dir):
1398 gitutil.Fetch(git_dir, thread_dir)
1400 print 'Cloning repo for thread %d' % thread_num
1401 gitutil.Clone(src_dir, thread_dir)
1403 def _PrepareWorkingSpace(self, max_threads):
1404 """Prepare the working directory for use.
1406 Set up the git repo for each thread.
1409 max_threads: Maximum number of threads we expect to need.
1411 Mkdir(self._working_dir)
1412 for thread in range(max_threads):
1413 self._PrepareThread(thread)
1415 def _PrepareOutputSpace(self):
1416 """Get the output directories ready to receive files.
1418 We delete any output directories which look like ones we need to
1419 create. Having left over directories is confusing when the user wants
1420 to check the output manually.
1423 for commit_upto in range(self.commit_count):
1424 dir_list.append(self._GetOutputDir(commit_upto))
1426 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1427 if dirname not in dir_list:
1428 shutil.rmtree(dirname)
1430 def BuildBoards(self, commits, board_selected, show_errors, keep_outputs):
1431 """Build all commits for a list of boards
1434 commits: List of commits to be build, each a Commit object
1435 boards_selected: Dict of selected boards, key is target name,
1436 value is Board object
1437 show_errors: True to show summarised error/warning info
1438 keep_outputs: True to save build output files
1440 self.commit_count = len(commits)
1441 self.commits = commits
1443 self.ResetResultSummary(board_selected)
1444 Mkdir(self.base_dir)
1445 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)))
1446 self._PrepareOutputSpace()
1447 self.SetupBuild(board_selected, commits)
1448 self.ProcessResult(None)
1450 # Create jobs to build all commits for each board
1451 for brd in board_selected.itervalues():
1454 job.commits = commits
1455 job.keep_outputs = keep_outputs
1456 job.step = self._step
1459 # Wait until all jobs are started
1462 # Wait until we have processed all output
1463 self.out_queue.join()