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 """Build a particular commit.
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.
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
206 - CommandResult object containing the results of the build
207 - boolean indicating whether 'make config' is still needed
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')
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'
226 # We are going to have to build it. First, get a toolchain
227 if not self.toolchain:
229 self.toolchain = self.builder.toolchains.Select(brd.arch)
230 except ValueError as err:
231 result.return_code = 10
233 result.stderr = str(err)
234 # TODO(sjg@chromium.org): This gets swallowed, but needs
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,
246 commit = self.builder.commit # Ick, fix this for BuildCommits()
248 # Set up the environment and command line
249 env = self.toolchain.MakeEnvironment()
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]
256 args.extend(self.builder.toolchains.GetMakeArguments(brd))
258 # If we need to reconfigure, do that now
260 result = self.Make(commit, brd, 'distclean', work_dir,
261 'distclean', *args, env=env)
262 result = self.Make(commit, brd, 'config', work_dir,
263 *(args + config_args), env=env)
264 config_out = result.combined
265 do_config = False # No need to configure next time
266 if result.return_code == 0:
267 result = self.Make(commit, brd, 'build', work_dir, *args,
269 result.stdout = config_out + result.stdout
271 result.return_code = 1
272 result.stderr = 'No tool chain for %s\n' % brd.arch
273 result.already_done = False
275 result.toolchain = self.toolchain
277 result.commit_upto = commit_upto
278 result.out_dir = out_dir
279 return result, do_config
281 def _WriteResult(self, result, keep_outputs):
282 """Write a built result to the output directory.
285 result: CommandResult object containing result to write
286 keep_outputs: True to store the output binaries, False
290 if result.return_code < 0:
294 if result.stderr and 'No child processes' in result.stderr:
297 if result.already_done:
300 # Write the output and stderr
301 output_dir = self.builder._GetOutputDir(result.commit_upto)
303 build_dir = self.builder.GetBuildDir(result.commit_upto,
307 outfile = os.path.join(build_dir, 'log')
308 with open(outfile, 'w') as fd:
310 fd.write(result.stdout)
312 errfile = self.builder.GetErrFile(result.commit_upto,
315 with open(errfile, 'w') as fd:
316 fd.write(result.stderr)
317 elif os.path.exists(errfile):
321 # Write the build result and toolchain information.
322 done_file = self.builder.GetDoneFile(result.commit_upto,
324 with open(done_file, 'w') as fd:
325 fd.write('%s' % result.return_code)
326 with open(os.path.join(build_dir, 'toolchain'), 'w') as fd:
327 print >>fd, 'gcc', result.toolchain.gcc
328 print >>fd, 'path', result.toolchain.path
329 print >>fd, 'cross', result.toolchain.cross
330 print >>fd, 'arch', result.toolchain.arch
331 fd.write('%s' % result.return_code)
333 with open(os.path.join(build_dir, 'toolchain'), 'w') as fd:
334 print >>fd, 'gcc', result.toolchain.gcc
335 print >>fd, 'path', result.toolchain.path
337 # Write out the image and function size information and an objdump
338 env = result.toolchain.MakeEnvironment()
340 for fname in ['u-boot', 'spl/u-boot-spl']:
341 cmd = ['%snm' % self.toolchain.cross, '--size-sort', fname]
342 nm_result = command.RunPipe([cmd], capture=True,
343 capture_stderr=True, cwd=result.out_dir,
344 raise_on_error=False, env=env)
346 nm = self.builder.GetFuncSizesFile(result.commit_upto,
347 result.brd.target, fname)
348 with open(nm, 'w') as fd:
349 print >>fd, nm_result.stdout,
351 cmd = ['%sobjdump' % self.toolchain.cross, '-h', fname]
352 dump_result = command.RunPipe([cmd], capture=True,
353 capture_stderr=True, cwd=result.out_dir,
354 raise_on_error=False, env=env)
356 if dump_result.stdout:
357 objdump = self.builder.GetObjdumpFile(result.commit_upto,
358 result.brd.target, fname)
359 with open(objdump, 'w') as fd:
360 print >>fd, dump_result.stdout,
361 for line in dump_result.stdout.splitlines():
362 fields = line.split()
363 if len(fields) > 5 and fields[1] == '.rodata':
364 rodata_size = fields[2]
366 cmd = ['%ssize' % self.toolchain.cross, fname]
367 size_result = command.RunPipe([cmd], capture=True,
368 capture_stderr=True, cwd=result.out_dir,
369 raise_on_error=False, env=env)
370 if size_result.stdout:
371 lines.append(size_result.stdout.splitlines()[1] + ' ' +
374 # Write out the image sizes file. This is similar to the output
375 # of binutil's 'size' utility, but it omits the header line and
376 # adds an additional hex value at the end of each line for the
379 sizes = self.builder.GetSizesFile(result.commit_upto,
381 with open(sizes, 'w') as fd:
382 print >>fd, '\n'.join(lines)
384 # Now write the actual build output
386 patterns = ['u-boot', '*.bin', 'u-boot.dtb', '*.map',
387 'include/autoconf.mk', 'spl/u-boot-spl',
388 'spl/u-boot-spl.bin']
389 for pattern in patterns:
390 file_list = glob.glob(os.path.join(result.out_dir, pattern))
391 for fname in file_list:
392 shutil.copy(fname, build_dir)
395 def RunJob(self, job):
398 A job consists of a building a list of commits for a particular board.
404 work_dir = self.builder.GetThreadDir(self.thread_num)
405 self.toolchain = None
407 # Run 'make board_config' on the first commit
411 for commit_upto in range(0, len(job.commits), job.step):
412 result, request_config = self.RunCommit(commit_upto, brd,
414 force_build or self.builder.force_build)
415 failed = result.return_code or result.stderr
416 if failed and not do_config:
417 # If our incremental build failed, try building again
419 if self.builder.force_config_on_failure:
420 result, request_config = self.RunCommit(commit_upto,
421 brd, work_dir, True, True)
422 do_config = request_config
424 # If we built that commit, then config is done. But if we got
425 # an warning, reconfig next time to force it to build the same
426 # files that created warnings this time. Otherwise an
427 # incremental build may not build the same file, and we will
428 # think that the warning has gone away.
429 # We could avoid this by using -Werror everywhere...
430 # For errors, the problem doesn't happen, since presumably
431 # the build stopped and didn't generate output, so will retry
432 # that file next time. So we could detect warnings and deal
433 # with them specially here. For now, we just reconfigure if
434 # anything goes work.
435 # Of course this is substantially slower if there are build
436 # errors/warnings (e.g. 2-3x slower even if only 10% of builds
438 if (failed and not result.already_done and not do_config and
439 self.builder.force_config_on_failure):
440 # If this build failed, try the next one with a
442 # Sometimes if the board_config.h file changes it can mess
443 # with dependencies, and we get:
444 # make: *** No rule to make target `include/autoconf.mk',
445 # needed by `depend'.
450 if self.builder.force_config_on_failure:
453 result.commit_upto = commit_upto
454 if result.return_code < 0:
455 raise ValueError('Interrupt')
457 # We have the build results, so output the result
458 self._WriteResult(result, job.keep_outputs)
459 self.builder.out_queue.put(result)
461 # Just build the currently checked-out build
462 result = self.RunCommit(None, True)
463 result.commit_upto = self.builder.upto
464 self.builder.out_queue.put(result)
467 """Our thread's run function
469 This thread picks a job from the queue, runs it, and then goes to the
474 job = self.builder.queue.get()
476 if self.builder.active and alive:
478 except Exception as err:
481 self.builder.queue.task_done()
485 """Class for building U-Boot for a particular commit.
487 Public members: (many should ->private)
488 active: True if the builder is active and has not been stopped
489 already_done: Number of builds already completed
490 base_dir: Base directory to use for builder
491 checkout: True to check out source, False to skip that step.
492 This is used for testing.
493 col: terminal.Color() object
494 count: Number of commits to build
495 do_make: Method to call to invoke Make
496 fail: Number of builds that failed due to error
497 force_build: Force building even if a build already exists
498 force_config_on_failure: If a commit fails for a board, disable
499 incremental building for the next commit we build for that
500 board, so that we will see all warnings/errors again.
501 git_dir: Git directory containing source repository
502 last_line_len: Length of the last line we printed (used for erasing
503 it with new progress information)
504 num_jobs: Number of jobs to run at once (passed to make as -j)
505 num_threads: Number of builder threads to run
506 out_queue: Queue of results to process
507 re_make_err: Compiled regular expression for ignore_lines
508 queue: Queue of jobs to run
509 threads: List of active threads
510 toolchains: Toolchains object to use for building
511 upto: Current commit number we are building (0.count-1)
512 warned: Number of builds that produced at least one warning
515 _base_board_dict: Last-summarised Dict of boards
516 _base_err_lines: Last-summarised list of errors
517 _build_period_us: Time taken for a single build (float object).
518 _complete_delay: Expected delay until completion (timedelta)
519 _next_delay_update: Next time we plan to display a progress update
521 _show_unknown: Show unknown boards (those not built) in summary
522 _timestamps: List of timestamps for the completion of the last
523 last _timestamp_count builds. Each is a datetime object.
524 _timestamp_count: Number of timestamps to keep in our list.
525 _working_dir: Base working directory containing all threads
528 """Records a build outcome for a single make invocation
531 rc: Outcome value (OUTCOME_...)
532 err_lines: List of error lines or [] if none
533 sizes: Dictionary of image size information, keyed by filename
534 - Each value is itself a dictionary containing
535 values for 'text', 'data' and 'bss', being the integer
536 size in bytes of each section.
537 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
538 value is itself a dictionary:
540 value: Size of function in bytes
542 def __init__(self, rc, err_lines, sizes, func_sizes):
544 self.err_lines = err_lines
546 self.func_sizes = func_sizes
548 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
549 checkout=True, show_unknown=True, step=1):
550 """Create a new Builder object
553 toolchains: Toolchains object to use for building
554 base_dir: Base directory to use for builder
555 git_dir: Git directory containing source repository
556 num_threads: Number of builder threads to run
557 num_jobs: Number of jobs to run at once (passed to make as -j)
558 checkout: True to check out source, False to skip that step.
559 This is used for testing.
560 show_unknown: Show unknown boards (those not built) in summary
561 step: 1 to process every commit, n to process every nth commit
563 self.toolchains = toolchains
564 self.base_dir = base_dir
565 self._working_dir = os.path.join(base_dir, '.bm-work')
568 self.do_make = self.Make
569 self.checkout = checkout
570 self.num_threads = num_threads
571 self.num_jobs = num_jobs
572 self.already_done = 0
573 self.force_build = False
574 self.git_dir = git_dir
575 self._show_unknown = show_unknown
576 self._timestamp_count = 10
577 self._build_period_us = None
578 self._complete_delay = None
579 self._next_delay_update = datetime.now()
580 self.force_config_on_failure = True
583 self.col = terminal.Color()
585 self.queue = Queue.Queue()
586 self.out_queue = Queue.Queue()
587 for i in range(self.num_threads):
588 t = BuilderThread(self, i)
591 self.threads.append(t)
593 self.last_line_len = 0
594 t = ResultThread(self)
597 self.threads.append(t)
599 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
600 self.re_make_err = re.compile('|'.join(ignore_lines))
603 """Get rid of all threads created by the builder"""
604 for t in self.threads:
607 def _AddTimestamp(self):
608 """Add a new timestamp to the list and record the build period.
610 The build period is the length of time taken to perform a single
611 build (one board, one commit).
614 self._timestamps.append(now)
615 count = len(self._timestamps)
616 delta = self._timestamps[-1] - self._timestamps[0]
617 seconds = delta.total_seconds()
619 # If we have enough data, estimate build period (time taken for a
620 # single build) and therefore completion time.
621 if count > 1 and self._next_delay_update < now:
622 self._next_delay_update = now + timedelta(seconds=2)
624 self._build_period = float(seconds) / count
625 todo = self.count - self.upto
626 self._complete_delay = timedelta(microseconds=
627 self._build_period * todo * 1000000)
629 self._complete_delay -= timedelta(
630 microseconds=self._complete_delay.microseconds)
633 self._timestamps.popleft()
636 def ClearLine(self, length):
637 """Clear any characters on the current line
639 Make way for a new line of length 'length', by outputting enough
640 spaces to clear out the old line. Then remember the new length for
644 length: Length of new line, in characters
646 if length < self.last_line_len:
647 print ' ' * (self.last_line_len - length),
649 self.last_line_len = length
652 def SelectCommit(self, commit, checkout=True):
653 """Checkout the selected commit for this build
656 if checkout and self.checkout:
657 gitutil.Checkout(commit.hash)
659 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
663 commit: Commit object that is being built
664 brd: Board object that is being built
665 stage: Stage that we are at (distclean, config, build)
666 cwd: Directory where make should be run
667 args: Arguments to pass to make
668 kwargs: Arguments to pass to command.RunPipe()
670 cmd = ['make'] + list(args)
671 result = command.RunPipe([cmd], capture=True, capture_stderr=True,
672 cwd=cwd, raise_on_error=False, **kwargs)
675 def ProcessResult(self, result):
676 """Process the result of a build, showing progress information
679 result: A CommandResult object
681 col = terminal.Color()
683 target = result.brd.target
685 if result.return_code < 0:
691 if result.return_code != 0:
695 if result.already_done:
696 self.already_done += 1
698 target = '(starting)'
700 # Display separate counts for ok, warned and fail
701 ok = self.upto - self.warned - self.fail
702 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
703 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
704 line += self.col.Color(self.col.RED, '%5d' % self.fail)
706 name = ' /%-5d ' % self.count
708 # Add our current completion time estimate
710 if self._complete_delay:
711 name += '%s : ' % self._complete_delay
712 # When building all boards for a commit, we can print a commit
714 if result and result.commit_upto is None:
715 name += 'commit %2d/%-3d' % (self.commit_upto + 1,
720 length = 13 + len(name)
721 self.ClearLine(length)
723 def _GetOutputDir(self, commit_upto):
724 """Get the name of the output directory for a commit number
726 The output directory is typically .../<branch>/<commit>.
729 commit_upto: Commit number to use (0..self.count-1)
731 commit = self.commits[commit_upto]
732 subject = commit.subject.translate(trans_valid_chars)
733 commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
734 self.commit_count, commit.hash, subject[:20]))
735 output_dir = os.path.join(self.base_dir, commit_dir)
738 def GetBuildDir(self, commit_upto, target):
739 """Get the name of the build directory for a commit number
741 The build directory is typically .../<branch>/<commit>/<target>.
744 commit_upto: Commit number to use (0..self.count-1)
747 output_dir = self._GetOutputDir(commit_upto)
748 return os.path.join(output_dir, target)
750 def GetDoneFile(self, commit_upto, target):
751 """Get the name of the done file for a commit number
754 commit_upto: Commit number to use (0..self.count-1)
757 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
759 def GetSizesFile(self, commit_upto, target):
760 """Get the name of the sizes file for a commit number
763 commit_upto: Commit number to use (0..self.count-1)
766 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
768 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
769 """Get the name of the funcsizes file for a commit number and ELF file
772 commit_upto: Commit number to use (0..self.count-1)
774 elf_fname: Filename of elf image
776 return os.path.join(self.GetBuildDir(commit_upto, target),
777 '%s.sizes' % elf_fname.replace('/', '-'))
779 def GetObjdumpFile(self, commit_upto, target, elf_fname):
780 """Get the name of the objdump file for a commit number and ELF file
783 commit_upto: Commit number to use (0..self.count-1)
785 elf_fname: Filename of elf image
787 return os.path.join(self.GetBuildDir(commit_upto, target),
788 '%s.objdump' % elf_fname.replace('/', '-'))
790 def GetErrFile(self, commit_upto, target):
791 """Get the name of the err file for a commit number
794 commit_upto: Commit number to use (0..self.count-1)
797 output_dir = self.GetBuildDir(commit_upto, target)
798 return os.path.join(output_dir, 'err')
800 def FilterErrors(self, lines):
801 """Filter out errors in which we have no interest
803 We should probably use map().
806 lines: List of error lines, each a string
808 New list with only interesting lines included
812 if not self.re_make_err.search(line):
813 out_lines.append(line)
816 def ReadFuncSizes(self, fname, fd):
817 """Read function sizes from the output of 'nm'
820 fd: File containing data to read
821 fname: Filename we are reading from (just for errors)
824 Dictionary containing size of each function in bytes, indexed by
828 for line in fd.readlines():
830 size, type, name = line[:-1].split()
832 print "Invalid line in file '%s': '%s'" % (fname, line[:-1])
835 # function names begin with '.' on 64-bit powerpc
837 name = 'static.' + name.split('.')[0]
838 sym[name] = sym.get(name, 0) + int(size, 16)
841 def GetBuildOutcome(self, commit_upto, target, read_func_sizes):
842 """Work out the outcome of a build.
845 commit_upto: Commit number to check (0..n-1)
846 target: Target board to check
847 read_func_sizes: True to read function size information
852 done_file = self.GetDoneFile(commit_upto, target)
853 sizes_file = self.GetSizesFile(commit_upto, target)
856 if os.path.exists(done_file):
857 with open(done_file, 'r') as fd:
858 return_code = int(fd.readline())
860 err_file = self.GetErrFile(commit_upto, target)
861 if os.path.exists(err_file):
862 with open(err_file, 'r') as fd:
863 err_lines = self.FilterErrors(fd.readlines())
865 # Decide whether the build was ok, failed or created warnings
873 # Convert size information to our simple format
874 if os.path.exists(sizes_file):
875 with open(sizes_file, 'r') as fd:
876 for line in fd.readlines():
877 values = line.split()
880 rodata = int(values[6], 16)
882 'all' : int(values[0]) + int(values[1]) +
884 'text' : int(values[0]) - rodata,
885 'data' : int(values[1]),
886 'bss' : int(values[2]),
889 sizes[values[5]] = size_dict
892 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
893 for fname in glob.glob(pattern):
894 with open(fname, 'r') as fd:
895 dict_name = os.path.basename(fname).replace('.sizes',
897 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
899 return Builder.Outcome(rc, err_lines, sizes, func_sizes)
901 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {})
903 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes):
904 """Calculate a summary of the results of building a commit.
907 board_selected: Dict containing boards to summarise
908 commit_upto: Commit number to summarize (0..self.count-1)
909 read_func_sizes: True to read function size information
913 Dict containing boards which passed building this commit.
914 keyed by board.target
915 List containing a summary of error/warning lines
918 err_lines_summary = []
920 for board in boards_selected.itervalues():
921 outcome = self.GetBuildOutcome(commit_upto, board.target,
923 board_dict[board.target] = outcome
924 for err in outcome.err_lines:
925 if err and not err.rstrip() in err_lines_summary:
926 err_lines_summary.append(err.rstrip())
927 return board_dict, err_lines_summary
929 def AddOutcome(self, board_dict, arch_list, changes, char, color):
930 """Add an output to our list of outcomes for each architecture
932 This simple function adds failing boards (changes) to the
933 relevant architecture string, so we can print the results out
934 sorted by architecture.
937 board_dict: Dict containing all boards
938 arch_list: Dict keyed by arch name. Value is a string containing
939 a list of board names which failed for that arch.
940 changes: List of boards to add to arch_list
941 color: terminal.Colour object
944 for target in changes:
945 if target in board_dict:
946 arch = board_dict[target].arch
949 str = self.col.Color(color, ' ' + target)
950 if not arch in done_arch:
951 str = self.col.Color(color, char) + ' ' + str
952 done_arch[arch] = True
953 if not arch in arch_list:
954 arch_list[arch] = str
956 arch_list[arch] += str
959 def ColourNum(self, num):
960 color = self.col.RED if num > 0 else self.col.GREEN
963 return self.col.Color(color, str(num))
965 def ResetResultSummary(self, board_selected):
966 """Reset the results summary ready for use.
968 Set up the base board list to be all those selected, and set the
969 error lines to empty.
971 Following this, calls to PrintResultSummary() will use this
972 information to work out what has changed.
975 board_selected: Dict containing boards to summarise, keyed by
978 self._base_board_dict = {}
979 for board in board_selected:
980 self._base_board_dict[board] = Builder.Outcome(0, [], [], {})
981 self._base_err_lines = []
983 def PrintFuncSizeDetail(self, fname, old, new):
984 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
985 delta, common = [], {}
992 if name not in common:
995 delta.append([-old[name], name])
998 if name not in common:
1001 delta.append([new[name], name])
1004 diff = new.get(name, 0) - old.get(name, 0)
1006 grow, up = grow + 1, up + diff
1008 shrink, down = shrink + 1, down - diff
1009 delta.append([diff, name])
1014 args = [add, -remove, grow, -shrink, up, -down, up - down]
1017 args = [self.ColourNum(x) for x in args]
1019 print ('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
1020 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
1021 print '%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
1023 for diff, name in delta:
1025 color = self.col.RED if diff > 0 else self.col.GREEN
1026 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
1027 old.get(name, '-'), new.get(name,'-'), diff)
1028 print self.col.Color(color, msg)
1031 def PrintSizeDetail(self, target_list, show_bloat):
1032 """Show details size information for each board
1035 target_list: List of targets, each a dict containing:
1036 'target': Target name
1037 'total_diff': Total difference in bytes across all areas
1038 <part_name>: Difference for that part
1039 show_bloat: Show detail for each function
1041 targets_by_diff = sorted(target_list, reverse=True,
1042 key=lambda x: x['_total_diff'])
1043 for result in targets_by_diff:
1044 printed_target = False
1045 for name in sorted(result):
1047 if name.startswith('_'):
1050 color = self.col.RED if diff > 0 else self.col.GREEN
1051 msg = ' %s %+d' % (name, diff)
1052 if not printed_target:
1053 print '%10s %-15s:' % ('', result['_target']),
1054 printed_target = True
1055 print self.col.Color(color, msg),
1059 target = result['_target']
1060 outcome = result['_outcome']
1061 base_outcome = self._base_board_dict[target]
1062 for fname in outcome.func_sizes:
1063 self.PrintFuncSizeDetail(fname,
1064 base_outcome.func_sizes[fname],
1065 outcome.func_sizes[fname])
1068 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
1070 """Print a summary of image sizes broken down by section.
1072 The summary takes the form of one line per architecture. The
1073 line contains deltas for each of the sections (+ means the section
1074 got bigger, - means smaller). The nunmbers are the average number
1075 of bytes that a board in this section increased by.
1078 powerpc: (622 boards) text -0.0
1079 arm: (285 boards) text -0.0
1080 nds32: (3 boards) text -8.0
1083 board_selected: Dict containing boards to summarise, keyed by
1085 board_dict: Dict containing boards for which we built this
1086 commit, keyed by board.target. The value is an Outcome object.
1087 show_detail: Show detail for each board
1088 show_bloat: Show detail for each function
1093 # Calculate changes in size for different image parts
1094 # The previous sizes are in Board.sizes, for each board
1095 for target in board_dict:
1096 if target not in board_selected:
1098 base_sizes = self._base_board_dict[target].sizes
1099 outcome = board_dict[target]
1100 sizes = outcome.sizes
1102 # Loop through the list of images, creating a dict of size
1103 # changes for each image/part. We end up with something like
1104 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
1105 # which means that U-Boot data increased by 5 bytes and SPL
1106 # text decreased by 4.
1107 err = {'_target' : target}
1109 if image in base_sizes:
1110 base_image = base_sizes[image]
1111 # Loop through the text, data, bss parts
1112 for part in sorted(sizes[image]):
1113 diff = sizes[image][part] - base_image[part]
1116 if image == 'u-boot':
1119 name = image + ':' + part
1121 arch = board_selected[target].arch
1122 if not arch in arch_count:
1123 arch_count[arch] = 1
1125 arch_count[arch] += 1
1127 pass # Only add to our list when we have some stats
1128 elif not arch in arch_list:
1129 arch_list[arch] = [err]
1131 arch_list[arch].append(err)
1133 # We now have a list of image size changes sorted by arch
1134 # Print out a summary of these
1135 for arch, target_list in arch_list.iteritems():
1136 # Get total difference for each type
1138 for result in target_list:
1140 for name, diff in result.iteritems():
1141 if name.startswith('_'):
1145 totals[name] += diff
1148 result['_total_diff'] = total
1149 result['_outcome'] = board_dict[result['_target']]
1151 count = len(target_list)
1152 printed_arch = False
1153 for name in sorted(totals):
1156 # Display the average difference in this name for this
1158 avg_diff = float(diff) / count
1159 color = self.col.RED if avg_diff > 0 else self.col.GREEN
1160 msg = ' %s %+1.1f' % (name, avg_diff)
1161 if not printed_arch:
1162 print '%10s: (for %d/%d boards)' % (arch, count,
1165 print self.col.Color(color, msg),
1170 self.PrintSizeDetail(target_list, show_bloat)
1173 def PrintResultSummary(self, board_selected, board_dict, err_lines,
1174 show_sizes, show_detail, show_bloat):
1175 """Compare results with the base results and display delta.
1177 Only boards mentioned in board_selected will be considered. This
1178 function is intended to be called repeatedly with the results of
1179 each commit. It therefore shows a 'diff' between what it saw in
1180 the last call and what it sees now.
1183 board_selected: Dict containing boards to summarise, keyed by
1185 board_dict: Dict containing boards for which we built this
1186 commit, keyed by board.target. The value is an Outcome object.
1187 err_lines: A list of errors for this commit, or [] if there is
1188 none, or we don't want to print errors
1189 show_sizes: Show image size deltas
1190 show_detail: Show detail for each board
1191 show_bloat: Show detail for each function
1193 better = [] # List of boards fixed since last commit
1194 worse = [] # List of new broken boards since last commit
1195 new = [] # List of boards that didn't exist last time
1196 unknown = [] # List of boards that were not built
1198 for target in board_dict:
1199 if target not in board_selected:
1202 # If the board was built last time, add its outcome to a list
1203 if target in self._base_board_dict:
1204 base_outcome = self._base_board_dict[target].rc
1205 outcome = board_dict[target]
1206 if outcome.rc == OUTCOME_UNKNOWN:
1207 unknown.append(target)
1208 elif outcome.rc < base_outcome:
1209 better.append(target)
1210 elif outcome.rc > base_outcome:
1211 worse.append(target)
1215 # Get a list of errors that have appeared, and disappeared
1218 for line in err_lines:
1219 if line not in self._base_err_lines:
1220 worse_err.append('+' + line)
1221 for line in self._base_err_lines:
1222 if line not in err_lines:
1223 better_err.append('-' + line)
1225 # Display results by arch
1226 if better or worse or unknown or new or worse_err or better_err:
1228 self.AddOutcome(board_selected, arch_list, better, '',
1230 self.AddOutcome(board_selected, arch_list, worse, '+',
1232 self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE)
1233 if self._show_unknown:
1234 self.AddOutcome(board_selected, arch_list, unknown, '?',
1236 for arch, target_list in arch_list.iteritems():
1237 print '%10s: %s' % (arch, target_list)
1239 print self.col.Color(self.col.GREEN, '\n'.join(better_err))
1241 print self.col.Color(self.col.RED, '\n'.join(worse_err))
1244 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1247 # Save our updated information for the next call to this function
1248 self._base_board_dict = board_dict
1249 self._base_err_lines = err_lines
1251 # Get a list of boards that did not get built, if needed
1253 for board in board_selected:
1254 if not board in board_dict:
1255 not_built.append(board)
1257 print "Boards not built (%d): %s" % (len(not_built),
1258 ', '.join(not_built))
1261 def ShowSummary(self, commits, board_selected, show_errors, show_sizes,
1262 show_detail, show_bloat):
1263 """Show a build summary for U-Boot for a given board list.
1265 Reset the result summary, then repeatedly call GetResultSummary on
1266 each commit's results, then display the differences we see.
1269 commit: Commit objects to summarise
1270 board_selected: Dict containing boards to summarise
1271 show_errors: Show errors that occured
1272 show_sizes: Show size deltas
1273 show_detail: Show detail for each board
1274 show_bloat: Show detail for each function
1276 self.commit_count = len(commits)
1277 self.commits = commits
1278 self.ResetResultSummary(board_selected)
1280 for commit_upto in range(0, self.commit_count, self._step):
1281 board_dict, err_lines = self.GetResultSummary(board_selected,
1282 commit_upto, read_func_sizes=show_bloat)
1283 msg = '%02d: %s' % (commit_upto + 1, commits[commit_upto].subject)
1284 print self.col.Color(self.col.BLUE, msg)
1285 self.PrintResultSummary(board_selected, board_dict,
1286 err_lines if show_errors else [], show_sizes, show_detail,
1290 def SetupBuild(self, board_selected, commits):
1291 """Set up ready to start a build.
1294 board_selected: Selected boards to build
1295 commits: Selected commits to build
1297 # First work out how many commits we will build
1298 count = (len(commits) + self._step - 1) / self._step
1299 self.count = len(board_selected) * count
1300 self.upto = self.warned = self.fail = 0
1301 self._timestamps = collections.deque()
1303 def BuildBoardsForCommit(self, board_selected, keep_outputs):
1304 """Build all boards for a single commit"""
1305 self.SetupBuild(board_selected)
1306 self.count = len(board_selected)
1307 for brd in board_selected.itervalues():
1311 job.keep_outputs = keep_outputs
1315 self.out_queue.join()
1319 def BuildCommits(self, commits, board_selected, show_errors, keep_outputs):
1320 """Build all boards for all commits (non-incremental)"""
1321 self.commit_count = len(commits)
1323 self.ResetResultSummary(board_selected)
1324 for self.commit_upto in range(self.commit_count):
1325 self.SelectCommit(commits[self.commit_upto])
1326 self.SelectOutputDir()
1327 Mkdir(self.output_dir)
1329 self.BuildBoardsForCommit(board_selected, keep_outputs)
1330 board_dict, err_lines = self.GetResultSummary()
1331 self.PrintResultSummary(board_selected, board_dict,
1332 err_lines if show_errors else [])
1334 if self.already_done:
1335 print '%d builds already done' % self.already_done
1337 def GetThreadDir(self, thread_num):
1338 """Get the directory path to the working dir for a thread.
1341 thread_num: Number of thread to check.
1343 return os.path.join(self._working_dir, '%02d' % thread_num)
1345 def _PrepareThread(self, thread_num):
1346 """Prepare the working directory for a thread.
1348 This clones or fetches the repo into the thread's work directory.
1351 thread_num: Thread number (0, 1, ...)
1353 thread_dir = self.GetThreadDir(thread_num)
1355 git_dir = os.path.join(thread_dir, '.git')
1357 # Clone the repo if it doesn't already exist
1358 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1359 # we have a private index but uses the origin repo's contents?
1361 src_dir = os.path.abspath(self.git_dir)
1362 if os.path.exists(git_dir):
1363 gitutil.Fetch(git_dir, thread_dir)
1365 print 'Cloning repo for thread %d' % thread_num
1366 gitutil.Clone(src_dir, thread_dir)
1368 def _PrepareWorkingSpace(self, max_threads):
1369 """Prepare the working directory for use.
1371 Set up the git repo for each thread.
1374 max_threads: Maximum number of threads we expect to need.
1376 Mkdir(self._working_dir)
1377 for thread in range(max_threads):
1378 self._PrepareThread(thread)
1380 def _PrepareOutputSpace(self):
1381 """Get the output directories ready to receive files.
1383 We delete any output directories which look like ones we need to
1384 create. Having left over directories is confusing when the user wants
1385 to check the output manually.
1388 for commit_upto in range(self.commit_count):
1389 dir_list.append(self._GetOutputDir(commit_upto))
1391 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1392 if dirname not in dir_list:
1393 shutil.rmtree(dirname)
1395 def BuildBoards(self, commits, board_selected, show_errors, keep_outputs):
1396 """Build all commits for a list of boards
1399 commits: List of commits to be build, each a Commit object
1400 boards_selected: Dict of selected boards, key is target name,
1401 value is Board object
1402 show_errors: True to show summarised error/warning info
1403 keep_outputs: True to save build output files
1405 self.commit_count = len(commits)
1406 self.commits = commits
1408 self.ResetResultSummary(board_selected)
1409 Mkdir(self.base_dir)
1410 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)))
1411 self._PrepareOutputSpace()
1412 self.SetupBuild(board_selected, commits)
1413 self.ProcessResult(None)
1415 # Create jobs to build all commits for each board
1416 for brd in board_selected.itervalues():
1419 job.commits = commits
1420 job.keep_outputs = keep_outputs
1421 job.step = self._step
1424 # Wait until all jobs are started
1427 # Wait until we have processed all output
1428 self.out_queue.join()