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