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 out_dir = os.path.join(work_dir, 'build')
218 # Check if the job was already completed last time
219 done_file = self.builder.GetDoneFile(commit_upto, brd.target)
220 result.already_done = os.path.exists(done_file)
221 will_build = (force_build or force_build_failures or
222 not result.already_done)
223 if result.already_done and will_build:
224 # Get the return code from that build and use it
225 with open(done_file, 'r') as fd:
226 result.return_code = int(fd.readline())
227 err_file = self.builder.GetErrFile(commit_upto, brd.target)
228 if os.path.exists(err_file) and os.stat(err_file).st_size:
229 result.stderr = 'bad'
230 elif not force_build:
231 # The build passed, so no need to build it again
235 # We are going to have to build it. First, get a toolchain
236 if not self.toolchain:
238 self.toolchain = self.builder.toolchains.Select(brd.arch)
239 except ValueError as err:
240 result.return_code = 10
242 result.stderr = str(err)
243 # TODO(sjg@chromium.org): This gets swallowed, but needs
247 # Checkout the right commit
248 if commit_upto is not None:
249 commit = self.builder.commits[commit_upto]
250 if self.builder.checkout:
251 git_dir = os.path.join(work_dir, '.git')
252 gitutil.Checkout(commit.hash, git_dir, work_dir,
255 commit = self.builder.commit # Ick, fix this for BuildCommits()
257 # Set up the environment and command line
258 env = self.toolchain.MakeEnvironment()
260 args = ['O=build', '-s']
261 if self.builder.num_jobs is not None:
262 args.extend(['-j', str(self.builder.num_jobs)])
263 config_args = ['%s_config' % brd.target]
265 args.extend(self.builder.toolchains.GetMakeArguments(brd))
267 # If we need to reconfigure, do that now
269 result = self.Make(commit, brd, 'distclean', work_dir,
270 'distclean', *args, env=env)
271 result = self.Make(commit, brd, 'config', work_dir,
272 *(args + config_args), env=env)
273 config_out = result.combined
274 do_config = False # No need to configure next time
275 if result.return_code == 0:
276 result = self.Make(commit, brd, 'build', work_dir, *args,
278 result.stdout = config_out + result.stdout
280 result.return_code = 1
281 result.stderr = 'No tool chain for %s\n' % brd.arch
282 result.already_done = False
284 result.toolchain = self.toolchain
286 result.commit_upto = commit_upto
287 result.out_dir = out_dir
288 return result, do_config
290 def _WriteResult(self, result, keep_outputs):
291 """Write a built result to the output directory.
294 result: CommandResult object containing result to write
295 keep_outputs: True to store the output binaries, False
299 if result.return_code < 0:
303 if result.stderr and 'No child processes' in result.stderr:
306 if result.already_done:
309 # Write the output and stderr
310 output_dir = self.builder._GetOutputDir(result.commit_upto)
312 build_dir = self.builder.GetBuildDir(result.commit_upto,
316 outfile = os.path.join(build_dir, 'log')
317 with open(outfile, 'w') as fd:
319 fd.write(result.stdout)
321 errfile = self.builder.GetErrFile(result.commit_upto,
324 with open(errfile, 'w') as fd:
325 fd.write(result.stderr)
326 elif os.path.exists(errfile):
330 # Write the build result and toolchain information.
331 done_file = self.builder.GetDoneFile(result.commit_upto,
333 with open(done_file, 'w') as fd:
334 fd.write('%s' % result.return_code)
335 with open(os.path.join(build_dir, 'toolchain'), 'w') as fd:
336 print >>fd, 'gcc', result.toolchain.gcc
337 print >>fd, 'path', result.toolchain.path
338 print >>fd, 'cross', result.toolchain.cross
339 print >>fd, 'arch', result.toolchain.arch
340 fd.write('%s' % result.return_code)
342 with open(os.path.join(build_dir, 'toolchain'), 'w') as fd:
343 print >>fd, 'gcc', result.toolchain.gcc
344 print >>fd, 'path', result.toolchain.path
346 # Write out the image and function size information and an objdump
347 env = result.toolchain.MakeEnvironment()
349 for fname in ['u-boot', 'spl/u-boot-spl']:
350 cmd = ['%snm' % self.toolchain.cross, '--size-sort', fname]
351 nm_result = command.RunPipe([cmd], capture=True,
352 capture_stderr=True, cwd=result.out_dir,
353 raise_on_error=False, env=env)
355 nm = self.builder.GetFuncSizesFile(result.commit_upto,
356 result.brd.target, fname)
357 with open(nm, 'w') as fd:
358 print >>fd, nm_result.stdout,
360 cmd = ['%sobjdump' % self.toolchain.cross, '-h', fname]
361 dump_result = command.RunPipe([cmd], capture=True,
362 capture_stderr=True, cwd=result.out_dir,
363 raise_on_error=False, env=env)
365 if dump_result.stdout:
366 objdump = self.builder.GetObjdumpFile(result.commit_upto,
367 result.brd.target, fname)
368 with open(objdump, 'w') as fd:
369 print >>fd, dump_result.stdout,
370 for line in dump_result.stdout.splitlines():
371 fields = line.split()
372 if len(fields) > 5 and fields[1] == '.rodata':
373 rodata_size = fields[2]
375 cmd = ['%ssize' % self.toolchain.cross, fname]
376 size_result = command.RunPipe([cmd], capture=True,
377 capture_stderr=True, cwd=result.out_dir,
378 raise_on_error=False, env=env)
379 if size_result.stdout:
380 lines.append(size_result.stdout.splitlines()[1] + ' ' +
383 # Write out the image sizes file. This is similar to the output
384 # of binutil's 'size' utility, but it omits the header line and
385 # adds an additional hex value at the end of each line for the
388 sizes = self.builder.GetSizesFile(result.commit_upto,
390 with open(sizes, 'w') as fd:
391 print >>fd, '\n'.join(lines)
393 # Now write the actual build output
395 patterns = ['u-boot', '*.bin', 'u-boot.dtb', '*.map',
396 'include/autoconf.mk', 'spl/u-boot-spl',
397 'spl/u-boot-spl.bin']
398 for pattern in patterns:
399 file_list = glob.glob(os.path.join(result.out_dir, pattern))
400 for fname in file_list:
401 shutil.copy(fname, build_dir)
404 def RunJob(self, job):
407 A job consists of a building a list of commits for a particular board.
413 work_dir = self.builder.GetThreadDir(self.thread_num)
414 self.toolchain = None
416 # Run 'make board_config' on the first commit
420 for commit_upto in range(0, len(job.commits), job.step):
421 result, request_config = self.RunCommit(commit_upto, brd,
423 force_build or self.builder.force_build,
424 self.builder.force_build_failures)
425 failed = result.return_code or result.stderr
426 if failed and not do_config:
427 # If our incremental build failed, try building again
429 if self.builder.force_config_on_failure:
430 result, request_config = self.RunCommit(commit_upto,
431 brd, work_dir, True, True, False)
432 do_config = request_config
434 # If we built that commit, then config is done. But if we got
435 # an warning, reconfig next time to force it to build the same
436 # files that created warnings this time. Otherwise an
437 # incremental build may not build the same file, and we will
438 # think that the warning has gone away.
439 # We could avoid this by using -Werror everywhere...
440 # For errors, the problem doesn't happen, since presumably
441 # the build stopped and didn't generate output, so will retry
442 # that file next time. So we could detect warnings and deal
443 # with them specially here. For now, we just reconfigure if
444 # anything goes work.
445 # Of course this is substantially slower if there are build
446 # errors/warnings (e.g. 2-3x slower even if only 10% of builds
448 if (failed and not result.already_done and not do_config and
449 self.builder.force_config_on_failure):
450 # If this build failed, try the next one with a
452 # Sometimes if the board_config.h file changes it can mess
453 # with dependencies, and we get:
454 # make: *** No rule to make target `include/autoconf.mk',
455 # needed by `depend'.
460 if self.builder.force_config_on_failure:
463 result.commit_upto = commit_upto
464 if result.return_code < 0:
465 raise ValueError('Interrupt')
467 # We have the build results, so output the result
468 self._WriteResult(result, job.keep_outputs)
469 self.builder.out_queue.put(result)
471 # Just build the currently checked-out build
472 result = self.RunCommit(None, True)
473 result.commit_upto = self.builder.upto
474 self.builder.out_queue.put(result)
477 """Our thread's run function
479 This thread picks a job from the queue, runs it, and then goes to the
484 job = self.builder.queue.get()
486 if self.builder.active and alive:
488 except Exception as err:
491 self.builder.queue.task_done()
495 """Class for building U-Boot for a particular commit.
497 Public members: (many should ->private)
498 active: True if the builder is active and has not been stopped
499 already_done: Number of builds already completed
500 base_dir: Base directory to use for builder
501 checkout: True to check out source, False to skip that step.
502 This is used for testing.
503 col: terminal.Color() object
504 count: Number of commits to build
505 do_make: Method to call to invoke Make
506 fail: Number of builds that failed due to error
507 force_build: Force building even if a build already exists
508 force_config_on_failure: If a commit fails for a board, disable
509 incremental building for the next commit we build for that
510 board, so that we will see all warnings/errors again.
511 force_build_failures: If a previously-built build (i.e. built on
512 a previous run of buildman) is marked as failed, rebuild it.
513 git_dir: Git directory containing source repository
514 last_line_len: Length of the last line we printed (used for erasing
515 it with new progress information)
516 num_jobs: Number of jobs to run at once (passed to make as -j)
517 num_threads: Number of builder threads to run
518 out_queue: Queue of results to process
519 re_make_err: Compiled regular expression for ignore_lines
520 queue: Queue of jobs to run
521 threads: List of active threads
522 toolchains: Toolchains object to use for building
523 upto: Current commit number we are building (0.count-1)
524 warned: Number of builds that produced at least one warning
527 _base_board_dict: Last-summarised Dict of boards
528 _base_err_lines: Last-summarised list of errors
529 _build_period_us: Time taken for a single build (float object).
530 _complete_delay: Expected delay until completion (timedelta)
531 _next_delay_update: Next time we plan to display a progress update
533 _show_unknown: Show unknown boards (those not built) in summary
534 _timestamps: List of timestamps for the completion of the last
535 last _timestamp_count builds. Each is a datetime object.
536 _timestamp_count: Number of timestamps to keep in our list.
537 _working_dir: Base working directory containing all threads
540 """Records a build outcome for a single make invocation
543 rc: Outcome value (OUTCOME_...)
544 err_lines: List of error lines or [] if none
545 sizes: Dictionary of image size information, keyed by filename
546 - Each value is itself a dictionary containing
547 values for 'text', 'data' and 'bss', being the integer
548 size in bytes of each section.
549 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
550 value is itself a dictionary:
552 value: Size of function in bytes
554 def __init__(self, rc, err_lines, sizes, func_sizes):
556 self.err_lines = err_lines
558 self.func_sizes = func_sizes
560 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
561 checkout=True, show_unknown=True, step=1):
562 """Create a new Builder object
565 toolchains: Toolchains object to use for building
566 base_dir: Base directory to use for builder
567 git_dir: Git directory containing source repository
568 num_threads: Number of builder threads to run
569 num_jobs: Number of jobs to run at once (passed to make as -j)
570 checkout: True to check out source, False to skip that step.
571 This is used for testing.
572 show_unknown: Show unknown boards (those not built) in summary
573 step: 1 to process every commit, n to process every nth commit
575 self.toolchains = toolchains
576 self.base_dir = base_dir
577 self._working_dir = os.path.join(base_dir, '.bm-work')
580 self.do_make = self.Make
581 self.checkout = checkout
582 self.num_threads = num_threads
583 self.num_jobs = num_jobs
584 self.already_done = 0
585 self.force_build = False
586 self.git_dir = git_dir
587 self._show_unknown = show_unknown
588 self._timestamp_count = 10
589 self._build_period_us = None
590 self._complete_delay = None
591 self._next_delay_update = datetime.now()
592 self.force_config_on_failure = True
593 self.force_build_failures = False
596 self.col = terminal.Color()
598 self.queue = Queue.Queue()
599 self.out_queue = Queue.Queue()
600 for i in range(self.num_threads):
601 t = BuilderThread(self, i)
604 self.threads.append(t)
606 self.last_line_len = 0
607 t = ResultThread(self)
610 self.threads.append(t)
612 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
613 self.re_make_err = re.compile('|'.join(ignore_lines))
616 """Get rid of all threads created by the builder"""
617 for t in self.threads:
620 def _AddTimestamp(self):
621 """Add a new timestamp to the list and record the build period.
623 The build period is the length of time taken to perform a single
624 build (one board, one commit).
627 self._timestamps.append(now)
628 count = len(self._timestamps)
629 delta = self._timestamps[-1] - self._timestamps[0]
630 seconds = delta.total_seconds()
632 # If we have enough data, estimate build period (time taken for a
633 # single build) and therefore completion time.
634 if count > 1 and self._next_delay_update < now:
635 self._next_delay_update = now + timedelta(seconds=2)
637 self._build_period = float(seconds) / count
638 todo = self.count - self.upto
639 self._complete_delay = timedelta(microseconds=
640 self._build_period * todo * 1000000)
642 self._complete_delay -= timedelta(
643 microseconds=self._complete_delay.microseconds)
646 self._timestamps.popleft()
649 def ClearLine(self, length):
650 """Clear any characters on the current line
652 Make way for a new line of length 'length', by outputting enough
653 spaces to clear out the old line. Then remember the new length for
657 length: Length of new line, in characters
659 if length < self.last_line_len:
660 print ' ' * (self.last_line_len - length),
662 self.last_line_len = length
665 def SelectCommit(self, commit, checkout=True):
666 """Checkout the selected commit for this build
669 if checkout and self.checkout:
670 gitutil.Checkout(commit.hash)
672 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
676 commit: Commit object that is being built
677 brd: Board object that is being built
678 stage: Stage that we are at (distclean, config, build)
679 cwd: Directory where make should be run
680 args: Arguments to pass to make
681 kwargs: Arguments to pass to command.RunPipe()
683 cmd = ['make'] + list(args)
684 result = command.RunPipe([cmd], capture=True, capture_stderr=True,
685 cwd=cwd, raise_on_error=False, **kwargs)
688 def ProcessResult(self, result):
689 """Process the result of a build, showing progress information
692 result: A CommandResult object
694 col = terminal.Color()
696 target = result.brd.target
698 if result.return_code < 0:
704 if result.return_code != 0:
708 if result.already_done:
709 self.already_done += 1
711 target = '(starting)'
713 # Display separate counts for ok, warned and fail
714 ok = self.upto - self.warned - self.fail
715 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
716 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
717 line += self.col.Color(self.col.RED, '%5d' % self.fail)
719 name = ' /%-5d ' % self.count
721 # Add our current completion time estimate
723 if self._complete_delay:
724 name += '%s : ' % self._complete_delay
725 # When building all boards for a commit, we can print a commit
727 if result and result.commit_upto is None:
728 name += 'commit %2d/%-3d' % (self.commit_upto + 1,
733 length = 13 + len(name)
734 self.ClearLine(length)
736 def _GetOutputDir(self, commit_upto):
737 """Get the name of the output directory for a commit number
739 The output directory is typically .../<branch>/<commit>.
742 commit_upto: Commit number to use (0..self.count-1)
744 commit = self.commits[commit_upto]
745 subject = commit.subject.translate(trans_valid_chars)
746 commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
747 self.commit_count, commit.hash, subject[:20]))
748 output_dir = os.path.join(self.base_dir, commit_dir)
751 def GetBuildDir(self, commit_upto, target):
752 """Get the name of the build directory for a commit number
754 The build directory is typically .../<branch>/<commit>/<target>.
757 commit_upto: Commit number to use (0..self.count-1)
760 output_dir = self._GetOutputDir(commit_upto)
761 return os.path.join(output_dir, target)
763 def GetDoneFile(self, commit_upto, target):
764 """Get the name of the done file for a commit number
767 commit_upto: Commit number to use (0..self.count-1)
770 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
772 def GetSizesFile(self, commit_upto, target):
773 """Get the name of the sizes file for a commit number
776 commit_upto: Commit number to use (0..self.count-1)
779 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
781 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
782 """Get the name of the funcsizes file for a commit number and ELF file
785 commit_upto: Commit number to use (0..self.count-1)
787 elf_fname: Filename of elf image
789 return os.path.join(self.GetBuildDir(commit_upto, target),
790 '%s.sizes' % elf_fname.replace('/', '-'))
792 def GetObjdumpFile(self, commit_upto, target, elf_fname):
793 """Get the name of the objdump file for a commit number and ELF file
796 commit_upto: Commit number to use (0..self.count-1)
798 elf_fname: Filename of elf image
800 return os.path.join(self.GetBuildDir(commit_upto, target),
801 '%s.objdump' % elf_fname.replace('/', '-'))
803 def GetErrFile(self, commit_upto, target):
804 """Get the name of the err file for a commit number
807 commit_upto: Commit number to use (0..self.count-1)
810 output_dir = self.GetBuildDir(commit_upto, target)
811 return os.path.join(output_dir, 'err')
813 def FilterErrors(self, lines):
814 """Filter out errors in which we have no interest
816 We should probably use map().
819 lines: List of error lines, each a string
821 New list with only interesting lines included
825 if not self.re_make_err.search(line):
826 out_lines.append(line)
829 def ReadFuncSizes(self, fname, fd):
830 """Read function sizes from the output of 'nm'
833 fd: File containing data to read
834 fname: Filename we are reading from (just for errors)
837 Dictionary containing size of each function in bytes, indexed by
841 for line in fd.readlines():
843 size, type, name = line[:-1].split()
845 print "Invalid line in file '%s': '%s'" % (fname, line[:-1])
848 # function names begin with '.' on 64-bit powerpc
850 name = 'static.' + name.split('.')[0]
851 sym[name] = sym.get(name, 0) + int(size, 16)
854 def GetBuildOutcome(self, commit_upto, target, read_func_sizes):
855 """Work out the outcome of a build.
858 commit_upto: Commit number to check (0..n-1)
859 target: Target board to check
860 read_func_sizes: True to read function size information
865 done_file = self.GetDoneFile(commit_upto, target)
866 sizes_file = self.GetSizesFile(commit_upto, target)
869 if os.path.exists(done_file):
870 with open(done_file, 'r') as fd:
871 return_code = int(fd.readline())
873 err_file = self.GetErrFile(commit_upto, target)
874 if os.path.exists(err_file):
875 with open(err_file, 'r') as fd:
876 err_lines = self.FilterErrors(fd.readlines())
878 # Decide whether the build was ok, failed or created warnings
886 # Convert size information to our simple format
887 if os.path.exists(sizes_file):
888 with open(sizes_file, 'r') as fd:
889 for line in fd.readlines():
890 values = line.split()
893 rodata = int(values[6], 16)
895 'all' : int(values[0]) + int(values[1]) +
897 'text' : int(values[0]) - rodata,
898 'data' : int(values[1]),
899 'bss' : int(values[2]),
902 sizes[values[5]] = size_dict
905 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
906 for fname in glob.glob(pattern):
907 with open(fname, 'r') as fd:
908 dict_name = os.path.basename(fname).replace('.sizes',
910 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
912 return Builder.Outcome(rc, err_lines, sizes, func_sizes)
914 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {})
916 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes):
917 """Calculate a summary of the results of building a commit.
920 board_selected: Dict containing boards to summarise
921 commit_upto: Commit number to summarize (0..self.count-1)
922 read_func_sizes: True to read function size information
926 Dict containing boards which passed building this commit.
927 keyed by board.target
928 List containing a summary of error/warning lines
931 err_lines_summary = []
933 for board in boards_selected.itervalues():
934 outcome = self.GetBuildOutcome(commit_upto, board.target,
936 board_dict[board.target] = outcome
937 for err in outcome.err_lines:
938 if err and not err.rstrip() in err_lines_summary:
939 err_lines_summary.append(err.rstrip())
940 return board_dict, err_lines_summary
942 def AddOutcome(self, board_dict, arch_list, changes, char, color):
943 """Add an output to our list of outcomes for each architecture
945 This simple function adds failing boards (changes) to the
946 relevant architecture string, so we can print the results out
947 sorted by architecture.
950 board_dict: Dict containing all boards
951 arch_list: Dict keyed by arch name. Value is a string containing
952 a list of board names which failed for that arch.
953 changes: List of boards to add to arch_list
954 color: terminal.Colour object
957 for target in changes:
958 if target in board_dict:
959 arch = board_dict[target].arch
962 str = self.col.Color(color, ' ' + target)
963 if not arch in done_arch:
964 str = self.col.Color(color, char) + ' ' + str
965 done_arch[arch] = True
966 if not arch in arch_list:
967 arch_list[arch] = str
969 arch_list[arch] += str
972 def ColourNum(self, num):
973 color = self.col.RED if num > 0 else self.col.GREEN
976 return self.col.Color(color, str(num))
978 def ResetResultSummary(self, board_selected):
979 """Reset the results summary ready for use.
981 Set up the base board list to be all those selected, and set the
982 error lines to empty.
984 Following this, calls to PrintResultSummary() will use this
985 information to work out what has changed.
988 board_selected: Dict containing boards to summarise, keyed by
991 self._base_board_dict = {}
992 for board in board_selected:
993 self._base_board_dict[board] = Builder.Outcome(0, [], [], {})
994 self._base_err_lines = []
996 def PrintFuncSizeDetail(self, fname, old, new):
997 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
998 delta, common = [], {}
1005 if name not in common:
1008 delta.append([-old[name], name])
1011 if name not in common:
1014 delta.append([new[name], name])
1017 diff = new.get(name, 0) - old.get(name, 0)
1019 grow, up = grow + 1, up + diff
1021 shrink, down = shrink + 1, down - diff
1022 delta.append([diff, name])
1027 args = [add, -remove, grow, -shrink, up, -down, up - down]
1030 args = [self.ColourNum(x) for x in args]
1032 print ('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
1033 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
1034 print '%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
1036 for diff, name in delta:
1038 color = self.col.RED if diff > 0 else self.col.GREEN
1039 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
1040 old.get(name, '-'), new.get(name,'-'), diff)
1041 print self.col.Color(color, msg)
1044 def PrintSizeDetail(self, target_list, show_bloat):
1045 """Show details size information for each board
1048 target_list: List of targets, each a dict containing:
1049 'target': Target name
1050 'total_diff': Total difference in bytes across all areas
1051 <part_name>: Difference for that part
1052 show_bloat: Show detail for each function
1054 targets_by_diff = sorted(target_list, reverse=True,
1055 key=lambda x: x['_total_diff'])
1056 for result in targets_by_diff:
1057 printed_target = False
1058 for name in sorted(result):
1060 if name.startswith('_'):
1063 color = self.col.RED if diff > 0 else self.col.GREEN
1064 msg = ' %s %+d' % (name, diff)
1065 if not printed_target:
1066 print '%10s %-15s:' % ('', result['_target']),
1067 printed_target = True
1068 print self.col.Color(color, msg),
1072 target = result['_target']
1073 outcome = result['_outcome']
1074 base_outcome = self._base_board_dict[target]
1075 for fname in outcome.func_sizes:
1076 self.PrintFuncSizeDetail(fname,
1077 base_outcome.func_sizes[fname],
1078 outcome.func_sizes[fname])
1081 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
1083 """Print a summary of image sizes broken down by section.
1085 The summary takes the form of one line per architecture. The
1086 line contains deltas for each of the sections (+ means the section
1087 got bigger, - means smaller). The nunmbers are the average number
1088 of bytes that a board in this section increased by.
1091 powerpc: (622 boards) text -0.0
1092 arm: (285 boards) text -0.0
1093 nds32: (3 boards) text -8.0
1096 board_selected: Dict containing boards to summarise, keyed by
1098 board_dict: Dict containing boards for which we built this
1099 commit, keyed by board.target. The value is an Outcome object.
1100 show_detail: Show detail for each board
1101 show_bloat: Show detail for each function
1106 # Calculate changes in size for different image parts
1107 # The previous sizes are in Board.sizes, for each board
1108 for target in board_dict:
1109 if target not in board_selected:
1111 base_sizes = self._base_board_dict[target].sizes
1112 outcome = board_dict[target]
1113 sizes = outcome.sizes
1115 # Loop through the list of images, creating a dict of size
1116 # changes for each image/part. We end up with something like
1117 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
1118 # which means that U-Boot data increased by 5 bytes and SPL
1119 # text decreased by 4.
1120 err = {'_target' : target}
1122 if image in base_sizes:
1123 base_image = base_sizes[image]
1124 # Loop through the text, data, bss parts
1125 for part in sorted(sizes[image]):
1126 diff = sizes[image][part] - base_image[part]
1129 if image == 'u-boot':
1132 name = image + ':' + part
1134 arch = board_selected[target].arch
1135 if not arch in arch_count:
1136 arch_count[arch] = 1
1138 arch_count[arch] += 1
1140 pass # Only add to our list when we have some stats
1141 elif not arch in arch_list:
1142 arch_list[arch] = [err]
1144 arch_list[arch].append(err)
1146 # We now have a list of image size changes sorted by arch
1147 # Print out a summary of these
1148 for arch, target_list in arch_list.iteritems():
1149 # Get total difference for each type
1151 for result in target_list:
1153 for name, diff in result.iteritems():
1154 if name.startswith('_'):
1158 totals[name] += diff
1161 result['_total_diff'] = total
1162 result['_outcome'] = board_dict[result['_target']]
1164 count = len(target_list)
1165 printed_arch = False
1166 for name in sorted(totals):
1169 # Display the average difference in this name for this
1171 avg_diff = float(diff) / count
1172 color = self.col.RED if avg_diff > 0 else self.col.GREEN
1173 msg = ' %s %+1.1f' % (name, avg_diff)
1174 if not printed_arch:
1175 print '%10s: (for %d/%d boards)' % (arch, count,
1178 print self.col.Color(color, msg),
1183 self.PrintSizeDetail(target_list, show_bloat)
1186 def PrintResultSummary(self, board_selected, board_dict, err_lines,
1187 show_sizes, show_detail, show_bloat):
1188 """Compare results with the base results and display delta.
1190 Only boards mentioned in board_selected will be considered. This
1191 function is intended to be called repeatedly with the results of
1192 each commit. It therefore shows a 'diff' between what it saw in
1193 the last call and what it sees now.
1196 board_selected: Dict containing boards to summarise, keyed by
1198 board_dict: Dict containing boards for which we built this
1199 commit, keyed by board.target. The value is an Outcome object.
1200 err_lines: A list of errors for this commit, or [] if there is
1201 none, or we don't want to print errors
1202 show_sizes: Show image size deltas
1203 show_detail: Show detail for each board
1204 show_bloat: Show detail for each function
1206 better = [] # List of boards fixed since last commit
1207 worse = [] # List of new broken boards since last commit
1208 new = [] # List of boards that didn't exist last time
1209 unknown = [] # List of boards that were not built
1211 for target in board_dict:
1212 if target not in board_selected:
1215 # If the board was built last time, add its outcome to a list
1216 if target in self._base_board_dict:
1217 base_outcome = self._base_board_dict[target].rc
1218 outcome = board_dict[target]
1219 if outcome.rc == OUTCOME_UNKNOWN:
1220 unknown.append(target)
1221 elif outcome.rc < base_outcome:
1222 better.append(target)
1223 elif outcome.rc > base_outcome:
1224 worse.append(target)
1228 # Get a list of errors that have appeared, and disappeared
1231 for line in err_lines:
1232 if line not in self._base_err_lines:
1233 worse_err.append('+' + line)
1234 for line in self._base_err_lines:
1235 if line not in err_lines:
1236 better_err.append('-' + line)
1238 # Display results by arch
1239 if better or worse or unknown or new or worse_err or better_err:
1241 self.AddOutcome(board_selected, arch_list, better, '',
1243 self.AddOutcome(board_selected, arch_list, worse, '+',
1245 self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE)
1246 if self._show_unknown:
1247 self.AddOutcome(board_selected, arch_list, unknown, '?',
1249 for arch, target_list in arch_list.iteritems():
1250 print '%10s: %s' % (arch, target_list)
1252 print self.col.Color(self.col.GREEN, '\n'.join(better_err))
1254 print self.col.Color(self.col.RED, '\n'.join(worse_err))
1257 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1260 # Save our updated information for the next call to this function
1261 self._base_board_dict = board_dict
1262 self._base_err_lines = err_lines
1264 # Get a list of boards that did not get built, if needed
1266 for board in board_selected:
1267 if not board in board_dict:
1268 not_built.append(board)
1270 print "Boards not built (%d): %s" % (len(not_built),
1271 ', '.join(not_built))
1274 def ShowSummary(self, commits, board_selected, show_errors, show_sizes,
1275 show_detail, show_bloat):
1276 """Show a build summary for U-Boot for a given board list.
1278 Reset the result summary, then repeatedly call GetResultSummary on
1279 each commit's results, then display the differences we see.
1282 commit: Commit objects to summarise
1283 board_selected: Dict containing boards to summarise
1284 show_errors: Show errors that occured
1285 show_sizes: Show size deltas
1286 show_detail: Show detail for each board
1287 show_bloat: Show detail for each function
1289 self.commit_count = len(commits)
1290 self.commits = commits
1291 self.ResetResultSummary(board_selected)
1293 for commit_upto in range(0, self.commit_count, self._step):
1294 board_dict, err_lines = self.GetResultSummary(board_selected,
1295 commit_upto, read_func_sizes=show_bloat)
1296 msg = '%02d: %s' % (commit_upto + 1, commits[commit_upto].subject)
1297 print self.col.Color(self.col.BLUE, msg)
1298 self.PrintResultSummary(board_selected, board_dict,
1299 err_lines if show_errors else [], show_sizes, show_detail,
1303 def SetupBuild(self, board_selected, commits):
1304 """Set up ready to start a build.
1307 board_selected: Selected boards to build
1308 commits: Selected commits to build
1310 # First work out how many commits we will build
1311 count = (len(commits) + self._step - 1) / self._step
1312 self.count = len(board_selected) * count
1313 self.upto = self.warned = self.fail = 0
1314 self._timestamps = collections.deque()
1316 def BuildBoardsForCommit(self, board_selected, keep_outputs):
1317 """Build all boards for a single commit"""
1318 self.SetupBuild(board_selected)
1319 self.count = len(board_selected)
1320 for brd in board_selected.itervalues():
1324 job.keep_outputs = keep_outputs
1328 self.out_queue.join()
1332 def BuildCommits(self, commits, board_selected, show_errors, keep_outputs):
1333 """Build all boards for all commits (non-incremental)"""
1334 self.commit_count = len(commits)
1336 self.ResetResultSummary(board_selected)
1337 for self.commit_upto in range(self.commit_count):
1338 self.SelectCommit(commits[self.commit_upto])
1339 self.SelectOutputDir()
1340 Mkdir(self.output_dir)
1342 self.BuildBoardsForCommit(board_selected, keep_outputs)
1343 board_dict, err_lines = self.GetResultSummary()
1344 self.PrintResultSummary(board_selected, board_dict,
1345 err_lines if show_errors else [])
1347 if self.already_done:
1348 print '%d builds already done' % self.already_done
1350 def GetThreadDir(self, thread_num):
1351 """Get the directory path to the working dir for a thread.
1354 thread_num: Number of thread to check.
1356 return os.path.join(self._working_dir, '%02d' % thread_num)
1358 def _PrepareThread(self, thread_num):
1359 """Prepare the working directory for a thread.
1361 This clones or fetches the repo into the thread's work directory.
1364 thread_num: Thread number (0, 1, ...)
1366 thread_dir = self.GetThreadDir(thread_num)
1368 git_dir = os.path.join(thread_dir, '.git')
1370 # Clone the repo if it doesn't already exist
1371 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1372 # we have a private index but uses the origin repo's contents?
1374 src_dir = os.path.abspath(self.git_dir)
1375 if os.path.exists(git_dir):
1376 gitutil.Fetch(git_dir, thread_dir)
1378 print 'Cloning repo for thread %d' % thread_num
1379 gitutil.Clone(src_dir, thread_dir)
1381 def _PrepareWorkingSpace(self, max_threads):
1382 """Prepare the working directory for use.
1384 Set up the git repo for each thread.
1387 max_threads: Maximum number of threads we expect to need.
1389 Mkdir(self._working_dir)
1390 for thread in range(max_threads):
1391 self._PrepareThread(thread)
1393 def _PrepareOutputSpace(self):
1394 """Get the output directories ready to receive files.
1396 We delete any output directories which look like ones we need to
1397 create. Having left over directories is confusing when the user wants
1398 to check the output manually.
1401 for commit_upto in range(self.commit_count):
1402 dir_list.append(self._GetOutputDir(commit_upto))
1404 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1405 if dirname not in dir_list:
1406 shutil.rmtree(dirname)
1408 def BuildBoards(self, commits, board_selected, show_errors, keep_outputs):
1409 """Build all commits for a list of boards
1412 commits: List of commits to be build, each a Commit object
1413 boards_selected: Dict of selected boards, key is target name,
1414 value is Board object
1415 show_errors: True to show summarised error/warning info
1416 keep_outputs: True to save build output files
1418 self.commit_count = len(commits)
1419 self.commits = commits
1421 self.ResetResultSummary(board_selected)
1422 Mkdir(self.base_dir)
1423 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)))
1424 self._PrepareOutputSpace()
1425 self.SetupBuild(board_selected, commits)
1426 self.ProcessResult(None)
1428 # Create jobs to build all commits for each board
1429 for brd in board_selected.itervalues():
1432 job.commits = commits
1433 job.keep_outputs = keep_outputs
1434 job.step = self._step
1437 # Wait until all jobs are started
1440 # Wait until we have processed all output
1441 self.out_queue.join()