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