blob: 141bf6469136aa7f5f18bb6e07701dfcfd5d135d [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
Simon Glassfc3fe1c2013-04-03 11:07:16 +00009from datetime import datetime, timedelta
10import glob
11import os
12import re
13import Queue
14import shutil
15import string
16import sys
Simon Glassfc3fe1c2013-04-03 11:07:16 +000017import time
18
Simon Glass190064b2014-08-09 15:33:00 -060019import builderthread
Simon Glassfc3fe1c2013-04-03 11:07:16 +000020import command
21import gitutil
22import terminal
Simon Glass4653a882014-09-05 19:00:07 -060023from terminal import Print
Simon Glassfc3fe1c2013-04-03 11:07:16 +000024import 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
Simon Glass843312d2015-02-05 22:06:15 -070099CONFIG_FILENAMES = [
100 '.config', '.config-spl', '.config-tpl',
101 'autoconf.mk', 'autoconf-spl.mk', 'autoconf-tpl.mk',
102 'autoconf.h', 'autoconf-spl.h','autoconf-tpl.h',
103 'u-boot.cfg', 'u-boot-spl.cfg', 'u-boot-tpl.cfg'
104]
105
Simon Glass8270e3c2015-08-25 21:52:14 -0600106class Config:
107 """Holds information about configuration settings for a board."""
108 def __init__(self, target):
109 self.target = target
110 self.config = {}
111 for fname in CONFIG_FILENAMES:
112 self.config[fname] = {}
113
114 def Add(self, fname, key, value):
115 self.config[fname][key] = value
116
117 def __hash__(self):
118 val = 0
119 for fname in self.config:
120 for key, value in self.config[fname].iteritems():
121 print key, value
122 val = val ^ hash(key) & hash(value)
123 return val
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000124
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000125class Builder:
126 """Class for building U-Boot for a particular commit.
127
128 Public members: (many should ->private)
129 active: True if the builder is active and has not been stopped
130 already_done: Number of builds already completed
131 base_dir: Base directory to use for builder
132 checkout: True to check out source, False to skip that step.
133 This is used for testing.
134 col: terminal.Color() object
135 count: Number of commits to build
136 do_make: Method to call to invoke Make
137 fail: Number of builds that failed due to error
138 force_build: Force building even if a build already exists
139 force_config_on_failure: If a commit fails for a board, disable
140 incremental building for the next commit we build for that
141 board, so that we will see all warnings/errors again.
Simon Glass4266dc22014-07-13 12:22:31 -0600142 force_build_failures: If a previously-built build (i.e. built on
143 a previous run of buildman) is marked as failed, rebuild it.
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000144 git_dir: Git directory containing source repository
145 last_line_len: Length of the last line we printed (used for erasing
146 it with new progress information)
147 num_jobs: Number of jobs to run at once (passed to make as -j)
148 num_threads: Number of builder threads to run
149 out_queue: Queue of results to process
150 re_make_err: Compiled regular expression for ignore_lines
151 queue: Queue of jobs to run
152 threads: List of active threads
153 toolchains: Toolchains object to use for building
154 upto: Current commit number we are building (0.count-1)
155 warned: Number of builds that produced at least one warning
Simon Glass97e91522014-07-14 17:51:02 -0600156 force_reconfig: Reconfigure U-Boot on each comiit. This disables
157 incremental building, where buildman reconfigures on the first
158 commit for a baord, and then just does an incremental build for
159 the following commits. In fact buildman will reconfigure and
160 retry for any failing commits, so generally the only effect of
161 this option is to slow things down.
Simon Glass189a4962014-07-14 17:51:03 -0600162 in_tree: Build U-Boot in-tree instead of specifying an output
163 directory separate from the source code. This option is really
164 only useful for testing in-tree builds.
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000165
166 Private members:
167 _base_board_dict: Last-summarised Dict of boards
168 _base_err_lines: Last-summarised list of errors
Simon Glasse30965d2014-08-28 09:43:44 -0600169 _base_warn_lines: Last-summarised list of warnings
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000170 _build_period_us: Time taken for a single build (float object).
171 _complete_delay: Expected delay until completion (timedelta)
172 _next_delay_update: Next time we plan to display a progress update
173 (datatime)
174 _show_unknown: Show unknown boards (those not built) in summary
175 _timestamps: List of timestamps for the completion of the last
176 last _timestamp_count builds. Each is a datetime object.
177 _timestamp_count: Number of timestamps to keep in our list.
178 _working_dir: Base working directory containing all threads
179 """
180 class Outcome:
181 """Records a build outcome for a single make invocation
182
183 Public Members:
184 rc: Outcome value (OUTCOME_...)
185 err_lines: List of error lines or [] if none
186 sizes: Dictionary of image size information, keyed by filename
187 - Each value is itself a dictionary containing
188 values for 'text', 'data' and 'bss', being the integer
189 size in bytes of each section.
190 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
191 value is itself a dictionary:
192 key: function name
193 value: Size of function in bytes
Simon Glass843312d2015-02-05 22:06:15 -0700194 config: Dictionary keyed by filename - e.g. '.config'. Each
195 value is itself a dictionary:
196 key: config name
197 value: config value
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000198 """
Simon Glass843312d2015-02-05 22:06:15 -0700199 def __init__(self, rc, err_lines, sizes, func_sizes, config):
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000200 self.rc = rc
201 self.err_lines = err_lines
202 self.sizes = sizes
203 self.func_sizes = func_sizes
Simon Glass843312d2015-02-05 22:06:15 -0700204 self.config = config
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000205
206 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
Simon Glass5971ab52014-12-01 17:33:55 -0700207 gnu_make='make', checkout=True, show_unknown=True, step=1,
Simon Glassd2ce6582014-12-01 17:34:07 -0700208 no_subdirs=False, full_path=False, verbose_build=False):
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000209 """Create a new Builder object
210
211 Args:
212 toolchains: Toolchains object to use for building
213 base_dir: Base directory to use for builder
214 git_dir: Git directory containing source repository
215 num_threads: Number of builder threads to run
216 num_jobs: Number of jobs to run at once (passed to make as -j)
Masahiro Yamada99796922014-07-22 11:19:09 +0900217 gnu_make: the command name of GNU Make.
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000218 checkout: True to check out source, False to skip that step.
219 This is used for testing.
220 show_unknown: Show unknown boards (those not built) in summary
221 step: 1 to process every commit, n to process every nth commit
Simon Glassbb1501f2014-12-01 17:34:00 -0700222 no_subdirs: Don't create subdirectories when building current
223 source for a single board
224 full_path: Return the full path in CROSS_COMPILE and don't set
225 PATH
Simon Glassd2ce6582014-12-01 17:34:07 -0700226 verbose_build: Run build with V=1 and don't use 'make -s'
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000227 """
228 self.toolchains = toolchains
229 self.base_dir = base_dir
230 self._working_dir = os.path.join(base_dir, '.bm-work')
231 self.threads = []
232 self.active = True
233 self.do_make = self.Make
Masahiro Yamada99796922014-07-22 11:19:09 +0900234 self.gnu_make = gnu_make
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000235 self.checkout = checkout
236 self.num_threads = num_threads
237 self.num_jobs = num_jobs
238 self.already_done = 0
239 self.force_build = False
240 self.git_dir = git_dir
241 self._show_unknown = show_unknown
242 self._timestamp_count = 10
243 self._build_period_us = None
244 self._complete_delay = None
245 self._next_delay_update = datetime.now()
246 self.force_config_on_failure = True
Simon Glass4266dc22014-07-13 12:22:31 -0600247 self.force_build_failures = False
Simon Glass97e91522014-07-14 17:51:02 -0600248 self.force_reconfig = False
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000249 self._step = step
Simon Glass189a4962014-07-14 17:51:03 -0600250 self.in_tree = False
Simon Glass28370c12014-08-09 15:33:06 -0600251 self._error_lines = 0
Simon Glass5971ab52014-12-01 17:33:55 -0700252 self.no_subdirs = no_subdirs
Simon Glassbb1501f2014-12-01 17:34:00 -0700253 self.full_path = full_path
Simon Glassd2ce6582014-12-01 17:34:07 -0700254 self.verbose_build = verbose_build
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000255
256 self.col = terminal.Color()
257
Simon Glasse30965d2014-08-28 09:43:44 -0600258 self._re_function = re.compile('(.*): In function.*')
259 self._re_files = re.compile('In file included from.*')
260 self._re_warning = re.compile('(.*):(\d*):(\d*): warning: .*')
261 self._re_note = re.compile('(.*):(\d*):(\d*): note: this is the location of the previous.*')
262
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000263 self.queue = Queue.Queue()
264 self.out_queue = Queue.Queue()
265 for i in range(self.num_threads):
Simon Glass190064b2014-08-09 15:33:00 -0600266 t = builderthread.BuilderThread(self, i)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000267 t.setDaemon(True)
268 t.start()
269 self.threads.append(t)
270
271 self.last_line_len = 0
Simon Glass190064b2014-08-09 15:33:00 -0600272 t = builderthread.ResultThread(self)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000273 t.setDaemon(True)
274 t.start()
275 self.threads.append(t)
276
277 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
278 self.re_make_err = re.compile('|'.join(ignore_lines))
279
280 def __del__(self):
281 """Get rid of all threads created by the builder"""
282 for t in self.threads:
283 del t
284
Simon Glassb2ea7ab2014-08-09 15:33:02 -0600285 def SetDisplayOptions(self, show_errors=False, show_sizes=False,
Simon Glassed966652014-08-28 09:43:43 -0600286 show_detail=False, show_bloat=False,
Simon Glass843312d2015-02-05 22:06:15 -0700287 list_error_boards=False, show_config=False):
Simon Glassb2ea7ab2014-08-09 15:33:02 -0600288 """Setup display options for the builder.
289
290 show_errors: True to show summarised error/warning info
291 show_sizes: Show size deltas
292 show_detail: Show detail for each board
293 show_bloat: Show detail for each function
Simon Glassed966652014-08-28 09:43:43 -0600294 list_error_boards: Show the boards which caused each error/warning
Simon Glass843312d2015-02-05 22:06:15 -0700295 show_config: Show config deltas
Simon Glassb2ea7ab2014-08-09 15:33:02 -0600296 """
297 self._show_errors = show_errors
298 self._show_sizes = show_sizes
299 self._show_detail = show_detail
300 self._show_bloat = show_bloat
Simon Glassed966652014-08-28 09:43:43 -0600301 self._list_error_boards = list_error_boards
Simon Glass843312d2015-02-05 22:06:15 -0700302 self._show_config = show_config
Simon Glassb2ea7ab2014-08-09 15:33:02 -0600303
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000304 def _AddTimestamp(self):
305 """Add a new timestamp to the list and record the build period.
306
307 The build period is the length of time taken to perform a single
308 build (one board, one commit).
309 """
310 now = datetime.now()
311 self._timestamps.append(now)
312 count = len(self._timestamps)
313 delta = self._timestamps[-1] - self._timestamps[0]
314 seconds = delta.total_seconds()
315
316 # If we have enough data, estimate build period (time taken for a
317 # single build) and therefore completion time.
318 if count > 1 and self._next_delay_update < now:
319 self._next_delay_update = now + timedelta(seconds=2)
320 if seconds > 0:
321 self._build_period = float(seconds) / count
322 todo = self.count - self.upto
323 self._complete_delay = timedelta(microseconds=
324 self._build_period * todo * 1000000)
325 # Round it
326 self._complete_delay -= timedelta(
327 microseconds=self._complete_delay.microseconds)
328
329 if seconds > 60:
330 self._timestamps.popleft()
331 count -= 1
332
333 def ClearLine(self, length):
334 """Clear any characters on the current line
335
336 Make way for a new line of length 'length', by outputting enough
337 spaces to clear out the old line. Then remember the new length for
338 next time.
339
340 Args:
341 length: Length of new line, in characters
342 """
343 if length < self.last_line_len:
Simon Glass4653a882014-09-05 19:00:07 -0600344 Print(' ' * (self.last_line_len - length), newline=False)
345 Print('\r', newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000346 self.last_line_len = length
347 sys.stdout.flush()
348
349 def SelectCommit(self, commit, checkout=True):
350 """Checkout the selected commit for this build
351 """
352 self.commit = commit
353 if checkout and self.checkout:
354 gitutil.Checkout(commit.hash)
355
356 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
357 """Run make
358
359 Args:
360 commit: Commit object that is being built
361 brd: Board object that is being built
Roger Meierfd18a892014-08-20 22:10:29 +0200362 stage: Stage that we are at (mrproper, config, build)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000363 cwd: Directory where make should be run
364 args: Arguments to pass to make
365 kwargs: Arguments to pass to command.RunPipe()
366 """
Masahiro Yamada99796922014-07-22 11:19:09 +0900367 cmd = [self.gnu_make] + list(args)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000368 result = command.RunPipe([cmd], capture=True, capture_stderr=True,
369 cwd=cwd, raise_on_error=False, **kwargs)
Simon Glass40f11fc2015-02-05 22:06:12 -0700370 if self.verbose_build:
371 result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout
372 result.combined = '%s\n' % (' '.join(cmd)) + result.combined
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000373 return result
374
375 def ProcessResult(self, result):
376 """Process the result of a build, showing progress information
377
378 Args:
Simon Glasse5a0e5d2014-08-09 15:33:03 -0600379 result: A CommandResult object, which indicates the result for
380 a single build
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000381 """
382 col = terminal.Color()
383 if result:
384 target = result.brd.target
385
386 if result.return_code < 0:
387 self.active = False
388 command.StopAll()
389 return
390
391 self.upto += 1
392 if result.return_code != 0:
393 self.fail += 1
394 elif result.stderr:
395 self.warned += 1
396 if result.already_done:
397 self.already_done += 1
Simon Glasse5a0e5d2014-08-09 15:33:03 -0600398 if self._verbose:
Simon Glass4653a882014-09-05 19:00:07 -0600399 Print('\r', newline=False)
Simon Glasse5a0e5d2014-08-09 15:33:03 -0600400 self.ClearLine(0)
401 boards_selected = {target : result.brd}
402 self.ResetResultSummary(boards_selected)
403 self.ProduceResultSummary(result.commit_upto, self.commits,
404 boards_selected)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000405 else:
406 target = '(starting)'
407
408 # Display separate counts for ok, warned and fail
409 ok = self.upto - self.warned - self.fail
410 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
411 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
412 line += self.col.Color(self.col.RED, '%5d' % self.fail)
413
414 name = ' /%-5d ' % self.count
415
416 # Add our current completion time estimate
417 self._AddTimestamp()
418 if self._complete_delay:
419 name += '%s : ' % self._complete_delay
420 # When building all boards for a commit, we can print a commit
421 # progress message.
422 if result and result.commit_upto is None:
423 name += 'commit %2d/%-3d' % (self.commit_upto + 1,
424 self.commit_count)
425
426 name += target
Simon Glass4653a882014-09-05 19:00:07 -0600427 Print(line + name, newline=False)
Simon Glasse5a0e5d2014-08-09 15:33:03 -0600428 length = 14 + len(name)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000429 self.ClearLine(length)
430
431 def _GetOutputDir(self, commit_upto):
432 """Get the name of the output directory for a commit number
433
434 The output directory is typically .../<branch>/<commit>.
435
436 Args:
437 commit_upto: Commit number to use (0..self.count-1)
438 """
Simon Glass5971ab52014-12-01 17:33:55 -0700439 commit_dir = None
Simon Glassfea58582014-08-09 15:32:59 -0600440 if self.commits:
441 commit = self.commits[commit_upto]
442 subject = commit.subject.translate(trans_valid_chars)
443 commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
444 self.commit_count, commit.hash, subject[:20]))
Simon Glass5971ab52014-12-01 17:33:55 -0700445 elif not self.no_subdirs:
Simon Glassfea58582014-08-09 15:32:59 -0600446 commit_dir = 'current'
Simon Glass5971ab52014-12-01 17:33:55 -0700447 if not commit_dir:
448 return self.base_dir
449 return os.path.join(self.base_dir, commit_dir)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000450
451 def GetBuildDir(self, commit_upto, target):
452 """Get the name of the build directory for a commit number
453
454 The build directory is typically .../<branch>/<commit>/<target>.
455
456 Args:
457 commit_upto: Commit number to use (0..self.count-1)
458 target: Target name
459 """
460 output_dir = self._GetOutputDir(commit_upto)
461 return os.path.join(output_dir, target)
462
463 def GetDoneFile(self, commit_upto, target):
464 """Get the name of the done file for a commit number
465
466 Args:
467 commit_upto: Commit number to use (0..self.count-1)
468 target: Target name
469 """
470 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
471
472 def GetSizesFile(self, commit_upto, target):
473 """Get the name of the sizes file for a commit number
474
475 Args:
476 commit_upto: Commit number to use (0..self.count-1)
477 target: Target name
478 """
479 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
480
481 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
482 """Get the name of the funcsizes file for a commit number and ELF file
483
484 Args:
485 commit_upto: Commit number to use (0..self.count-1)
486 target: Target name
487 elf_fname: Filename of elf image
488 """
489 return os.path.join(self.GetBuildDir(commit_upto, target),
490 '%s.sizes' % elf_fname.replace('/', '-'))
491
492 def GetObjdumpFile(self, commit_upto, target, elf_fname):
493 """Get the name of the objdump file for a commit number and ELF file
494
495 Args:
496 commit_upto: Commit number to use (0..self.count-1)
497 target: Target name
498 elf_fname: Filename of elf image
499 """
500 return os.path.join(self.GetBuildDir(commit_upto, target),
501 '%s.objdump' % elf_fname.replace('/', '-'))
502
503 def GetErrFile(self, commit_upto, target):
504 """Get the name of the err file for a commit number
505
506 Args:
507 commit_upto: Commit number to use (0..self.count-1)
508 target: Target name
509 """
510 output_dir = self.GetBuildDir(commit_upto, target)
511 return os.path.join(output_dir, 'err')
512
513 def FilterErrors(self, lines):
514 """Filter out errors in which we have no interest
515
516 We should probably use map().
517
518 Args:
519 lines: List of error lines, each a string
520 Returns:
521 New list with only interesting lines included
522 """
523 out_lines = []
524 for line in lines:
525 if not self.re_make_err.search(line):
526 out_lines.append(line)
527 return out_lines
528
529 def ReadFuncSizes(self, fname, fd):
530 """Read function sizes from the output of 'nm'
531
532 Args:
533 fd: File containing data to read
534 fname: Filename we are reading from (just for errors)
535
536 Returns:
537 Dictionary containing size of each function in bytes, indexed by
538 function name.
539 """
540 sym = {}
541 for line in fd.readlines():
542 try:
543 size, type, name = line[:-1].split()
544 except:
Simon Glass4653a882014-09-05 19:00:07 -0600545 Print("Invalid line in file '%s': '%s'" % (fname, line[:-1]))
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000546 continue
547 if type in 'tTdDbB':
548 # function names begin with '.' on 64-bit powerpc
549 if '.' in name[1:]:
550 name = 'static.' + name.split('.')[0]
551 sym[name] = sym.get(name, 0) + int(size, 16)
552 return sym
553
Simon Glass843312d2015-02-05 22:06:15 -0700554 def _ProcessConfig(self, fname):
555 """Read in a .config, autoconf.mk or autoconf.h file
556
557 This function handles all config file types. It ignores comments and
558 any #defines which don't start with CONFIG_.
559
560 Args:
561 fname: Filename to read
562
563 Returns:
564 Dictionary:
565 key: Config name (e.g. CONFIG_DM)
566 value: Config value (e.g. 1)
567 """
568 config = {}
569 if os.path.exists(fname):
570 with open(fname) as fd:
571 for line in fd:
572 line = line.strip()
573 if line.startswith('#define'):
574 values = line[8:].split(' ', 1)
575 if len(values) > 1:
576 key, value = values
577 else:
578 key = values[0]
579 value = ''
580 if not key.startswith('CONFIG_'):
581 continue
582 elif not line or line[0] in ['#', '*', '/']:
583 continue
584 else:
585 key, value = line.split('=', 1)
586 config[key] = value
587 return config
588
589 def GetBuildOutcome(self, commit_upto, target, read_func_sizes,
590 read_config):
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000591 """Work out the outcome of a build.
592
593 Args:
594 commit_upto: Commit number to check (0..n-1)
595 target: Target board to check
596 read_func_sizes: True to read function size information
Simon Glass843312d2015-02-05 22:06:15 -0700597 read_config: True to read .config and autoconf.h files
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000598
599 Returns:
600 Outcome object
601 """
602 done_file = self.GetDoneFile(commit_upto, target)
603 sizes_file = self.GetSizesFile(commit_upto, target)
604 sizes = {}
605 func_sizes = {}
Simon Glass843312d2015-02-05 22:06:15 -0700606 config = {}
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000607 if os.path.exists(done_file):
608 with open(done_file, 'r') as fd:
609 return_code = int(fd.readline())
610 err_lines = []
611 err_file = self.GetErrFile(commit_upto, target)
612 if os.path.exists(err_file):
613 with open(err_file, 'r') as fd:
614 err_lines = self.FilterErrors(fd.readlines())
615
616 # Decide whether the build was ok, failed or created warnings
617 if return_code:
618 rc = OUTCOME_ERROR
619 elif len(err_lines):
620 rc = OUTCOME_WARNING
621 else:
622 rc = OUTCOME_OK
623
624 # Convert size information to our simple format
625 if os.path.exists(sizes_file):
626 with open(sizes_file, 'r') as fd:
627 for line in fd.readlines():
628 values = line.split()
629 rodata = 0
630 if len(values) > 6:
631 rodata = int(values[6], 16)
632 size_dict = {
633 'all' : int(values[0]) + int(values[1]) +
634 int(values[2]),
635 'text' : int(values[0]) - rodata,
636 'data' : int(values[1]),
637 'bss' : int(values[2]),
638 'rodata' : rodata,
639 }
640 sizes[values[5]] = size_dict
641
642 if read_func_sizes:
643 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
644 for fname in glob.glob(pattern):
645 with open(fname, 'r') as fd:
646 dict_name = os.path.basename(fname).replace('.sizes',
647 '')
648 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
649
Simon Glass843312d2015-02-05 22:06:15 -0700650 if read_config:
651 output_dir = self.GetBuildDir(commit_upto, target)
652 for name in CONFIG_FILENAMES:
653 fname = os.path.join(output_dir, name)
654 config[name] = self._ProcessConfig(fname)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000655
Simon Glass843312d2015-02-05 22:06:15 -0700656 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000657
Simon Glass843312d2015-02-05 22:06:15 -0700658 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {})
659
660 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
661 read_config):
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000662 """Calculate a summary of the results of building a commit.
663
664 Args:
665 board_selected: Dict containing boards to summarise
666 commit_upto: Commit number to summarize (0..self.count-1)
667 read_func_sizes: True to read function size information
Simon Glass843312d2015-02-05 22:06:15 -0700668 read_config: True to read .config and autoconf.h files
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000669
670 Returns:
671 Tuple:
672 Dict containing boards which passed building this commit.
673 keyed by board.target
Simon Glasse30965d2014-08-28 09:43:44 -0600674 List containing a summary of error lines
Simon Glassed966652014-08-28 09:43:43 -0600675 Dict keyed by error line, containing a list of the Board
676 objects with that error
Simon Glasse30965d2014-08-28 09:43:44 -0600677 List containing a summary of warning lines
678 Dict keyed by error line, containing a list of the Board
679 objects with that warning
Simon Glass8270e3c2015-08-25 21:52:14 -0600680 Dictionary keyed by board.target. Each value is a dictionary:
681 key: filename - e.g. '.config'
Simon Glass843312d2015-02-05 22:06:15 -0700682 value is itself a dictionary:
683 key: config name
684 value: config value
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000685 """
Simon Glasse30965d2014-08-28 09:43:44 -0600686 def AddLine(lines_summary, lines_boards, line, board):
687 line = line.rstrip()
688 if line in lines_boards:
689 lines_boards[line].append(board)
690 else:
691 lines_boards[line] = [board]
692 lines_summary.append(line)
693
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000694 board_dict = {}
695 err_lines_summary = []
Simon Glassed966652014-08-28 09:43:43 -0600696 err_lines_boards = {}
Simon Glasse30965d2014-08-28 09:43:44 -0600697 warn_lines_summary = []
698 warn_lines_boards = {}
Simon Glass843312d2015-02-05 22:06:15 -0700699 config = {}
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000700
701 for board in boards_selected.itervalues():
702 outcome = self.GetBuildOutcome(commit_upto, board.target,
Simon Glass843312d2015-02-05 22:06:15 -0700703 read_func_sizes, read_config)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000704 board_dict[board.target] = outcome
Simon Glasse30965d2014-08-28 09:43:44 -0600705 last_func = None
706 last_was_warning = False
707 for line in outcome.err_lines:
708 if line:
709 if (self._re_function.match(line) or
710 self._re_files.match(line)):
711 last_func = line
Simon Glassed966652014-08-28 09:43:43 -0600712 else:
Simon Glasse30965d2014-08-28 09:43:44 -0600713 is_warning = self._re_warning.match(line)
714 is_note = self._re_note.match(line)
715 if is_warning or (last_was_warning and is_note):
716 if last_func:
717 AddLine(warn_lines_summary, warn_lines_boards,
718 last_func, board)
719 AddLine(warn_lines_summary, warn_lines_boards,
720 line, board)
721 else:
722 if last_func:
723 AddLine(err_lines_summary, err_lines_boards,
724 last_func, board)
725 AddLine(err_lines_summary, err_lines_boards,
726 line, board)
727 last_was_warning = is_warning
728 last_func = None
Simon Glass8270e3c2015-08-25 21:52:14 -0600729 tconfig = Config(board.target)
Simon Glass843312d2015-02-05 22:06:15 -0700730 for fname in CONFIG_FILENAMES:
Simon Glass843312d2015-02-05 22:06:15 -0700731 if outcome.config:
732 for key, value in outcome.config[fname].iteritems():
Simon Glass8270e3c2015-08-25 21:52:14 -0600733 tconfig.Add(fname, key, value)
734 config[board.target] = tconfig
Simon Glass843312d2015-02-05 22:06:15 -0700735
Simon Glasse30965d2014-08-28 09:43:44 -0600736 return (board_dict, err_lines_summary, err_lines_boards,
Simon Glass843312d2015-02-05 22:06:15 -0700737 warn_lines_summary, warn_lines_boards, config)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000738
739 def AddOutcome(self, board_dict, arch_list, changes, char, color):
740 """Add an output to our list of outcomes for each architecture
741
742 This simple function adds failing boards (changes) to the
743 relevant architecture string, so we can print the results out
744 sorted by architecture.
745
746 Args:
747 board_dict: Dict containing all boards
748 arch_list: Dict keyed by arch name. Value is a string containing
749 a list of board names which failed for that arch.
750 changes: List of boards to add to arch_list
751 color: terminal.Colour object
752 """
753 done_arch = {}
754 for target in changes:
755 if target in board_dict:
756 arch = board_dict[target].arch
757 else:
758 arch = 'unknown'
759 str = self.col.Color(color, ' ' + target)
760 if not arch in done_arch:
Simon Glass63c619e2015-02-05 22:06:11 -0700761 str = ' %s %s' % (self.col.Color(color, char), str)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000762 done_arch[arch] = True
763 if not arch in arch_list:
764 arch_list[arch] = str
765 else:
766 arch_list[arch] += str
767
768
769 def ColourNum(self, num):
770 color = self.col.RED if num > 0 else self.col.GREEN
771 if num == 0:
772 return '0'
773 return self.col.Color(color, str(num))
774
775 def ResetResultSummary(self, board_selected):
776 """Reset the results summary ready for use.
777
778 Set up the base board list to be all those selected, and set the
779 error lines to empty.
780
781 Following this, calls to PrintResultSummary() will use this
782 information to work out what has changed.
783
784 Args:
785 board_selected: Dict containing boards to summarise, keyed by
786 board.target
787 """
788 self._base_board_dict = {}
789 for board in board_selected:
Simon Glass843312d2015-02-05 22:06:15 -0700790 self._base_board_dict[board] = Builder.Outcome(0, [], [], {}, {})
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000791 self._base_err_lines = []
Simon Glasse30965d2014-08-28 09:43:44 -0600792 self._base_warn_lines = []
793 self._base_err_line_boards = {}
794 self._base_warn_line_boards = {}
Simon Glass8270e3c2015-08-25 21:52:14 -0600795 self._base_config = None
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000796
797 def PrintFuncSizeDetail(self, fname, old, new):
798 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
799 delta, common = [], {}
800
801 for a in old:
802 if a in new:
803 common[a] = 1
804
805 for name in old:
806 if name not in common:
807 remove += 1
808 down += old[name]
809 delta.append([-old[name], name])
810
811 for name in new:
812 if name not in common:
813 add += 1
814 up += new[name]
815 delta.append([new[name], name])
816
817 for name in common:
818 diff = new.get(name, 0) - old.get(name, 0)
819 if diff > 0:
820 grow, up = grow + 1, up + diff
821 elif diff < 0:
822 shrink, down = shrink + 1, down - diff
823 delta.append([diff, name])
824
825 delta.sort()
826 delta.reverse()
827
828 args = [add, -remove, grow, -shrink, up, -down, up - down]
829 if max(args) == 0:
830 return
831 args = [self.ColourNum(x) for x in args]
832 indent = ' ' * 15
Simon Glass4653a882014-09-05 19:00:07 -0600833 Print('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
834 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
835 Print('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
836 'delta'))
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000837 for diff, name in delta:
838 if diff:
839 color = self.col.RED if diff > 0 else self.col.GREEN
840 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
841 old.get(name, '-'), new.get(name,'-'), diff)
Simon Glass4653a882014-09-05 19:00:07 -0600842 Print(msg, colour=color)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000843
844
845 def PrintSizeDetail(self, target_list, show_bloat):
846 """Show details size information for each board
847
848 Args:
849 target_list: List of targets, each a dict containing:
850 'target': Target name
851 'total_diff': Total difference in bytes across all areas
852 <part_name>: Difference for that part
853 show_bloat: Show detail for each function
854 """
855 targets_by_diff = sorted(target_list, reverse=True,
856 key=lambda x: x['_total_diff'])
857 for result in targets_by_diff:
858 printed_target = False
859 for name in sorted(result):
860 diff = result[name]
861 if name.startswith('_'):
862 continue
863 if diff != 0:
864 color = self.col.RED if diff > 0 else self.col.GREEN
865 msg = ' %s %+d' % (name, diff)
866 if not printed_target:
Simon Glass4653a882014-09-05 19:00:07 -0600867 Print('%10s %-15s:' % ('', result['_target']),
868 newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000869 printed_target = True
Simon Glass4653a882014-09-05 19:00:07 -0600870 Print(msg, colour=color, newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000871 if printed_target:
Simon Glass4653a882014-09-05 19:00:07 -0600872 Print()
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000873 if show_bloat:
874 target = result['_target']
875 outcome = result['_outcome']
876 base_outcome = self._base_board_dict[target]
877 for fname in outcome.func_sizes:
878 self.PrintFuncSizeDetail(fname,
879 base_outcome.func_sizes[fname],
880 outcome.func_sizes[fname])
881
882
883 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
884 show_bloat):
885 """Print a summary of image sizes broken down by section.
886
887 The summary takes the form of one line per architecture. The
888 line contains deltas for each of the sections (+ means the section
889 got bigger, - means smaller). The nunmbers are the average number
890 of bytes that a board in this section increased by.
891
892 For example:
893 powerpc: (622 boards) text -0.0
894 arm: (285 boards) text -0.0
895 nds32: (3 boards) text -8.0
896
897 Args:
898 board_selected: Dict containing boards to summarise, keyed by
899 board.target
900 board_dict: Dict containing boards for which we built this
901 commit, keyed by board.target. The value is an Outcome object.
902 show_detail: Show detail for each board
903 show_bloat: Show detail for each function
904 """
905 arch_list = {}
906 arch_count = {}
907
908 # Calculate changes in size for different image parts
909 # The previous sizes are in Board.sizes, for each board
910 for target in board_dict:
911 if target not in board_selected:
912 continue
913 base_sizes = self._base_board_dict[target].sizes
914 outcome = board_dict[target]
915 sizes = outcome.sizes
916
917 # Loop through the list of images, creating a dict of size
918 # changes for each image/part. We end up with something like
919 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
920 # which means that U-Boot data increased by 5 bytes and SPL
921 # text decreased by 4.
922 err = {'_target' : target}
923 for image in sizes:
924 if image in base_sizes:
925 base_image = base_sizes[image]
926 # Loop through the text, data, bss parts
927 for part in sorted(sizes[image]):
928 diff = sizes[image][part] - base_image[part]
929 col = None
930 if diff:
931 if image == 'u-boot':
932 name = part
933 else:
934 name = image + ':' + part
935 err[name] = diff
936 arch = board_selected[target].arch
937 if not arch in arch_count:
938 arch_count[arch] = 1
939 else:
940 arch_count[arch] += 1
941 if not sizes:
942 pass # Only add to our list when we have some stats
943 elif not arch in arch_list:
944 arch_list[arch] = [err]
945 else:
946 arch_list[arch].append(err)
947
948 # We now have a list of image size changes sorted by arch
949 # Print out a summary of these
950 for arch, target_list in arch_list.iteritems():
951 # Get total difference for each type
952 totals = {}
953 for result in target_list:
954 total = 0
955 for name, diff in result.iteritems():
956 if name.startswith('_'):
957 continue
958 total += diff
959 if name in totals:
960 totals[name] += diff
961 else:
962 totals[name] = diff
963 result['_total_diff'] = total
964 result['_outcome'] = board_dict[result['_target']]
965
966 count = len(target_list)
967 printed_arch = False
968 for name in sorted(totals):
969 diff = totals[name]
970 if diff:
971 # Display the average difference in this name for this
972 # architecture
973 avg_diff = float(diff) / count
974 color = self.col.RED if avg_diff > 0 else self.col.GREEN
975 msg = ' %s %+1.1f' % (name, avg_diff)
976 if not printed_arch:
Simon Glass4653a882014-09-05 19:00:07 -0600977 Print('%10s: (for %d/%d boards)' % (arch, count,
978 arch_count[arch]), newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000979 printed_arch = True
Simon Glass4653a882014-09-05 19:00:07 -0600980 Print(msg, colour=color, newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000981
982 if printed_arch:
Simon Glass4653a882014-09-05 19:00:07 -0600983 Print()
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000984 if show_detail:
985 self.PrintSizeDetail(target_list, show_bloat)
986
987
988 def PrintResultSummary(self, board_selected, board_dict, err_lines,
Simon Glasse30965d2014-08-28 09:43:44 -0600989 err_line_boards, warn_lines, warn_line_boards,
Simon Glass843312d2015-02-05 22:06:15 -0700990 config, show_sizes, show_detail, show_bloat,
991 show_config):
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000992 """Compare results with the base results and display delta.
993
994 Only boards mentioned in board_selected will be considered. This
995 function is intended to be called repeatedly with the results of
996 each commit. It therefore shows a 'diff' between what it saw in
997 the last call and what it sees now.
998
999 Args:
1000 board_selected: Dict containing boards to summarise, keyed by
1001 board.target
1002 board_dict: Dict containing boards for which we built this
1003 commit, keyed by board.target. The value is an Outcome object.
1004 err_lines: A list of errors for this commit, or [] if there is
1005 none, or we don't want to print errors
Simon Glassed966652014-08-28 09:43:43 -06001006 err_line_boards: Dict keyed by error line, containing a list of
1007 the Board objects with that error
Simon Glasse30965d2014-08-28 09:43:44 -06001008 warn_lines: A list of warnings for this commit, or [] if there is
1009 none, or we don't want to print errors
1010 warn_line_boards: Dict keyed by warning line, containing a list of
1011 the Board objects with that warning
Simon Glass843312d2015-02-05 22:06:15 -07001012 config: Dictionary keyed by filename - e.g. '.config'. Each
1013 value is itself a dictionary:
1014 key: config name
1015 value: config value
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001016 show_sizes: Show image size deltas
1017 show_detail: Show detail for each board
1018 show_bloat: Show detail for each function
Simon Glass843312d2015-02-05 22:06:15 -07001019 show_config: Show config changes
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001020 """
Simon Glasse30965d2014-08-28 09:43:44 -06001021 def _BoardList(line, line_boards):
Simon Glassed966652014-08-28 09:43:43 -06001022 """Helper function to get a line of boards containing a line
1023
1024 Args:
1025 line: Error line to search for
1026 Return:
1027 String containing a list of boards with that error line, or
1028 '' if the user has not requested such a list
1029 """
1030 if self._list_error_boards:
1031 names = []
Simon Glasse30965d2014-08-28 09:43:44 -06001032 for board in line_boards[line]:
Simon Glassf66153b2014-10-16 01:05:55 -06001033 if not board.target in names:
1034 names.append(board.target)
Simon Glassed966652014-08-28 09:43:43 -06001035 names_str = '(%s) ' % ','.join(names)
1036 else:
1037 names_str = ''
1038 return names_str
1039
Simon Glasse30965d2014-08-28 09:43:44 -06001040 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1041 char):
1042 better_lines = []
1043 worse_lines = []
1044 for line in lines:
1045 if line not in base_lines:
1046 worse_lines.append(char + '+' +
1047 _BoardList(line, line_boards) + line)
1048 for line in base_lines:
1049 if line not in lines:
1050 better_lines.append(char + '-' +
1051 _BoardList(line, base_line_boards) + line)
1052 return better_lines, worse_lines
1053
Simon Glass843312d2015-02-05 22:06:15 -07001054 def _CalcConfig(delta, name, config):
1055 """Calculate configuration changes
1056
1057 Args:
1058 delta: Type of the delta, e.g. '+'
1059 name: name of the file which changed (e.g. .config)
1060 config: configuration change dictionary
1061 key: config name
1062 value: config value
1063 Returns:
1064 String containing the configuration changes which can be
1065 printed
1066 """
1067 out = ''
1068 for key in sorted(config.keys()):
1069 out += '%s=%s ' % (key, config[key])
Simon Glass8270e3c2015-08-25 21:52:14 -06001070 return '%s %s: %s' % (delta, name, out)
Simon Glass843312d2015-02-05 22:06:15 -07001071
Simon Glass8270e3c2015-08-25 21:52:14 -06001072 def _AddConfig(lines, name, config_plus, config_minus, config_change):
1073 """Add changes in configuration to a list
Simon Glass843312d2015-02-05 22:06:15 -07001074
1075 Args:
Simon Glass8270e3c2015-08-25 21:52:14 -06001076 lines: list to add to
1077 name: config file name
Simon Glass843312d2015-02-05 22:06:15 -07001078 config_plus: configurations added, dictionary
1079 key: config name
1080 value: config value
1081 config_minus: configurations removed, dictionary
1082 key: config name
1083 value: config value
1084 config_change: configurations changed, dictionary
1085 key: config name
1086 value: config value
1087 """
1088 if config_plus:
Simon Glass8270e3c2015-08-25 21:52:14 -06001089 lines.append(_CalcConfig('+', name, config_plus))
Simon Glass843312d2015-02-05 22:06:15 -07001090 if config_minus:
Simon Glass8270e3c2015-08-25 21:52:14 -06001091 lines.append(_CalcConfig('-', name, config_minus))
Simon Glass843312d2015-02-05 22:06:15 -07001092 if config_change:
Simon Glass8270e3c2015-08-25 21:52:14 -06001093 lines.append(_CalcConfig('c', name, config_change))
1094
1095 def _OutputConfigInfo(lines):
1096 for line in lines:
1097 if not line:
1098 continue
1099 if line[0] == '+':
1100 col = self.col.GREEN
1101 elif line[0] == '-':
1102 col = self.col.RED
1103 elif line[0] == 'c':
1104 col = self.col.YELLOW
1105 Print(' ' + line, newline=True, colour=col)
1106
Simon Glass843312d2015-02-05 22:06:15 -07001107
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001108 better = [] # List of boards fixed since last commit
1109 worse = [] # List of new broken boards since last commit
1110 new = [] # List of boards that didn't exist last time
1111 unknown = [] # List of boards that were not built
1112
1113 for target in board_dict:
1114 if target not in board_selected:
1115 continue
1116
1117 # If the board was built last time, add its outcome to a list
1118 if target in self._base_board_dict:
1119 base_outcome = self._base_board_dict[target].rc
1120 outcome = board_dict[target]
1121 if outcome.rc == OUTCOME_UNKNOWN:
1122 unknown.append(target)
1123 elif outcome.rc < base_outcome:
1124 better.append(target)
1125 elif outcome.rc > base_outcome:
1126 worse.append(target)
1127 else:
1128 new.append(target)
1129
1130 # Get a list of errors that have appeared, and disappeared
Simon Glasse30965d2014-08-28 09:43:44 -06001131 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1132 self._base_err_line_boards, err_lines, err_line_boards, '')
1133 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1134 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001135
1136 # Display results by arch
Simon Glasse30965d2014-08-28 09:43:44 -06001137 if (better or worse or unknown or new or worse_err or better_err
1138 or worse_warn or better_warn):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001139 arch_list = {}
1140 self.AddOutcome(board_selected, arch_list, better, '',
1141 self.col.GREEN)
1142 self.AddOutcome(board_selected, arch_list, worse, '+',
1143 self.col.RED)
1144 self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE)
1145 if self._show_unknown:
1146 self.AddOutcome(board_selected, arch_list, unknown, '?',
1147 self.col.MAGENTA)
1148 for arch, target_list in arch_list.iteritems():
Simon Glass4653a882014-09-05 19:00:07 -06001149 Print('%10s: %s' % (arch, target_list))
Simon Glass28370c12014-08-09 15:33:06 -06001150 self._error_lines += 1
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001151 if better_err:
Simon Glass4653a882014-09-05 19:00:07 -06001152 Print('\n'.join(better_err), colour=self.col.GREEN)
Simon Glass28370c12014-08-09 15:33:06 -06001153 self._error_lines += 1
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001154 if worse_err:
Simon Glass4653a882014-09-05 19:00:07 -06001155 Print('\n'.join(worse_err), colour=self.col.RED)
Simon Glass28370c12014-08-09 15:33:06 -06001156 self._error_lines += 1
Simon Glasse30965d2014-08-28 09:43:44 -06001157 if better_warn:
Simon Glass4653a882014-09-05 19:00:07 -06001158 Print('\n'.join(better_warn), colour=self.col.CYAN)
Simon Glasse30965d2014-08-28 09:43:44 -06001159 self._error_lines += 1
1160 if worse_warn:
Simon Glass4653a882014-09-05 19:00:07 -06001161 Print('\n'.join(worse_warn), colour=self.col.MAGENTA)
Simon Glasse30965d2014-08-28 09:43:44 -06001162 self._error_lines += 1
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001163
1164 if show_sizes:
1165 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1166 show_bloat)
1167
Simon Glass8270e3c2015-08-25 21:52:14 -06001168 if show_config and self._base_config:
1169 summary = {}
1170 arch_config_plus = {}
1171 arch_config_minus = {}
1172 arch_config_change = {}
1173 arch_list = []
1174
1175 for target in board_dict:
1176 if target not in board_selected:
Simon Glass843312d2015-02-05 22:06:15 -07001177 continue
Simon Glass8270e3c2015-08-25 21:52:14 -06001178 arch = board_selected[target].arch
1179 if arch not in arch_list:
1180 arch_list.append(arch)
1181
1182 for arch in arch_list:
1183 arch_config_plus[arch] = {}
1184 arch_config_minus[arch] = {}
1185 arch_config_change[arch] = {}
1186 for name in CONFIG_FILENAMES:
1187 arch_config_plus[arch][name] = {}
1188 arch_config_minus[arch][name] = {}
1189 arch_config_change[arch][name] = {}
1190
1191 for target in board_dict:
1192 if target not in board_selected:
1193 continue
1194
1195 arch = board_selected[target].arch
1196
1197 all_config_plus = {}
1198 all_config_minus = {}
1199 all_config_change = {}
1200 tbase = self._base_config[target]
1201 tconfig = config[target]
1202 lines = []
1203 for name in CONFIG_FILENAMES:
1204 if not tconfig.config[name]:
1205 continue
1206 config_plus = {}
1207 config_minus = {}
1208 config_change = {}
1209 base = tbase.config[name]
1210 for key, value in tconfig.config[name].iteritems():
1211 if key not in base:
1212 config_plus[key] = value
1213 all_config_plus[key] = value
1214 for key, value in base.iteritems():
1215 if key not in tconfig.config[name]:
1216 config_minus[key] = value
1217 all_config_minus[key] = value
1218 for key, value in base.iteritems():
1219 new_value = tconfig.config.get(key)
1220 if new_value and value != new_value:
1221 desc = '%s -> %s' % (value, new_value)
1222 config_change[key] = desc
1223 all_config_change[key] = desc
1224
1225 arch_config_plus[arch][name].update(config_plus)
1226 arch_config_minus[arch][name].update(config_minus)
1227 arch_config_change[arch][name].update(config_change)
1228
1229 _AddConfig(lines, name, config_plus, config_minus,
1230 config_change)
1231 _AddConfig(lines, 'all', all_config_plus, all_config_minus,
1232 all_config_change)
1233 summary[target] = '\n'.join(lines)
1234
1235 lines_by_target = {}
1236 for target, lines in summary.iteritems():
1237 if lines in lines_by_target:
1238 lines_by_target[lines].append(target)
1239 else:
1240 lines_by_target[lines] = [target]
1241
1242 for arch in arch_list:
1243 lines = []
1244 all_plus = {}
1245 all_minus = {}
1246 all_change = {}
1247 for name in CONFIG_FILENAMES:
1248 all_plus.update(arch_config_plus[arch][name])
1249 all_minus.update(arch_config_minus[arch][name])
1250 all_change.update(arch_config_change[arch][name])
1251 _AddConfig(lines, name, arch_config_plus[arch][name],
1252 arch_config_minus[arch][name],
1253 arch_config_change[arch][name])
1254 _AddConfig(lines, 'all', all_plus, all_minus, all_change)
1255 #arch_summary[target] = '\n'.join(lines)
1256 if lines:
1257 Print('%s:' % arch)
1258 _OutputConfigInfo(lines)
1259
1260 for lines, targets in lines_by_target.iteritems():
1261 if not lines:
1262 continue
1263 Print('%s :' % ' '.join(sorted(targets)))
1264 _OutputConfigInfo(lines.split('\n'))
1265
Simon Glass843312d2015-02-05 22:06:15 -07001266
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001267 # Save our updated information for the next call to this function
1268 self._base_board_dict = board_dict
1269 self._base_err_lines = err_lines
Simon Glasse30965d2014-08-28 09:43:44 -06001270 self._base_warn_lines = warn_lines
1271 self._base_err_line_boards = err_line_boards
1272 self._base_warn_line_boards = warn_line_boards
Simon Glass843312d2015-02-05 22:06:15 -07001273 self._base_config = config
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001274
1275 # Get a list of boards that did not get built, if needed
1276 not_built = []
1277 for board in board_selected:
1278 if not board in board_dict:
1279 not_built.append(board)
1280 if not_built:
Simon Glass4653a882014-09-05 19:00:07 -06001281 Print("Boards not built (%d): %s" % (len(not_built),
1282 ', '.join(not_built)))
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001283
Simon Glassb2ea7ab2014-08-09 15:33:02 -06001284 def ProduceResultSummary(self, commit_upto, commits, board_selected):
Simon Glasse30965d2014-08-28 09:43:44 -06001285 (board_dict, err_lines, err_line_boards, warn_lines,
Simon Glass843312d2015-02-05 22:06:15 -07001286 warn_line_boards, config) = self.GetResultSummary(
Simon Glassed966652014-08-28 09:43:43 -06001287 board_selected, commit_upto,
Simon Glass843312d2015-02-05 22:06:15 -07001288 read_func_sizes=self._show_bloat,
1289 read_config=self._show_config)
Simon Glassb2ea7ab2014-08-09 15:33:02 -06001290 if commits:
1291 msg = '%02d: %s' % (commit_upto + 1,
1292 commits[commit_upto].subject)
Simon Glass4653a882014-09-05 19:00:07 -06001293 Print(msg, colour=self.col.BLUE)
Simon Glassb2ea7ab2014-08-09 15:33:02 -06001294 self.PrintResultSummary(board_selected, board_dict,
Simon Glassed966652014-08-28 09:43:43 -06001295 err_lines if self._show_errors else [], err_line_boards,
Simon Glasse30965d2014-08-28 09:43:44 -06001296 warn_lines if self._show_errors else [], warn_line_boards,
Simon Glass843312d2015-02-05 22:06:15 -07001297 config, self._show_sizes, self._show_detail,
1298 self._show_bloat, self._show_config)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001299
Simon Glassb2ea7ab2014-08-09 15:33:02 -06001300 def ShowSummary(self, commits, board_selected):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001301 """Show a build summary for U-Boot for a given board list.
1302
1303 Reset the result summary, then repeatedly call GetResultSummary on
1304 each commit's results, then display the differences we see.
1305
1306 Args:
1307 commit: Commit objects to summarise
1308 board_selected: Dict containing boards to summarise
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001309 """
Simon Glassfea58582014-08-09 15:32:59 -06001310 self.commit_count = len(commits) if commits else 1
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001311 self.commits = commits
1312 self.ResetResultSummary(board_selected)
Simon Glass28370c12014-08-09 15:33:06 -06001313 self._error_lines = 0
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001314
1315 for commit_upto in range(0, self.commit_count, self._step):
Simon Glassb2ea7ab2014-08-09 15:33:02 -06001316 self.ProduceResultSummary(commit_upto, commits, board_selected)
Simon Glass28370c12014-08-09 15:33:06 -06001317 if not self._error_lines:
Simon Glass4653a882014-09-05 19:00:07 -06001318 Print('(no errors to report)', colour=self.col.GREEN)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001319
1320
1321 def SetupBuild(self, board_selected, commits):
1322 """Set up ready to start a build.
1323
1324 Args:
1325 board_selected: Selected boards to build
1326 commits: Selected commits to build
1327 """
1328 # First work out how many commits we will build
Simon Glassfea58582014-08-09 15:32:59 -06001329 count = (self.commit_count + self._step - 1) / self._step
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001330 self.count = len(board_selected) * count
1331 self.upto = self.warned = self.fail = 0
1332 self._timestamps = collections.deque()
1333
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001334 def GetThreadDir(self, thread_num):
1335 """Get the directory path to the working dir for a thread.
1336
1337 Args:
1338 thread_num: Number of thread to check.
1339 """
1340 return os.path.join(self._working_dir, '%02d' % thread_num)
1341
Simon Glassfea58582014-08-09 15:32:59 -06001342 def _PrepareThread(self, thread_num, setup_git):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001343 """Prepare the working directory for a thread.
1344
1345 This clones or fetches the repo into the thread's work directory.
1346
1347 Args:
1348 thread_num: Thread number (0, 1, ...)
Simon Glassfea58582014-08-09 15:32:59 -06001349 setup_git: True to set up a git repo clone
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001350 """
1351 thread_dir = self.GetThreadDir(thread_num)
Simon Glass190064b2014-08-09 15:33:00 -06001352 builderthread.Mkdir(thread_dir)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001353 git_dir = os.path.join(thread_dir, '.git')
1354
1355 # Clone the repo if it doesn't already exist
1356 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1357 # we have a private index but uses the origin repo's contents?
Simon Glassfea58582014-08-09 15:32:59 -06001358 if setup_git and self.git_dir:
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001359 src_dir = os.path.abspath(self.git_dir)
1360 if os.path.exists(git_dir):
1361 gitutil.Fetch(git_dir, thread_dir)
1362 else:
Simon Glass4653a882014-09-05 19:00:07 -06001363 Print('Cloning repo for thread %d' % thread_num)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001364 gitutil.Clone(src_dir, thread_dir)
1365
Simon Glassfea58582014-08-09 15:32:59 -06001366 def _PrepareWorkingSpace(self, max_threads, setup_git):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001367 """Prepare the working directory for use.
1368
1369 Set up the git repo for each thread.
1370
1371 Args:
1372 max_threads: Maximum number of threads we expect to need.
Simon Glassfea58582014-08-09 15:32:59 -06001373 setup_git: True to set up a git repo clone
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001374 """
Simon Glass190064b2014-08-09 15:33:00 -06001375 builderthread.Mkdir(self._working_dir)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001376 for thread in range(max_threads):
Simon Glassfea58582014-08-09 15:32:59 -06001377 self._PrepareThread(thread, setup_git)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001378
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 """
Simon Glass1a915672014-12-01 17:33:53 -07001386 if not self.commits:
1387 return
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001388 dir_list = []
1389 for commit_upto in range(self.commit_count):
1390 dir_list.append(self._GetOutputDir(commit_upto))
1391
1392 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1393 if dirname not in dir_list:
1394 shutil.rmtree(dirname)
1395
Simon Glasse5a0e5d2014-08-09 15:33:03 -06001396 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001397 """Build all commits for a list of boards
1398
1399 Args:
1400 commits: List of commits to be build, each a Commit object
1401 boards_selected: Dict of selected boards, key is target name,
1402 value is Board object
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001403 keep_outputs: True to save build output files
Simon Glasse5a0e5d2014-08-09 15:33:03 -06001404 verbose: Display build results as they are completed
Simon Glass2c3deb92014-08-28 09:43:39 -06001405 Returns:
1406 Tuple containing:
1407 - number of boards that failed to build
1408 - number of boards that issued warnings
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001409 """
Simon Glassfea58582014-08-09 15:32:59 -06001410 self.commit_count = len(commits) if commits else 1
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001411 self.commits = commits
Simon Glasse5a0e5d2014-08-09 15:33:03 -06001412 self._verbose = verbose
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001413
1414 self.ResetResultSummary(board_selected)
Thierry Redingf3d015c2014-08-19 10:22:39 +02001415 builderthread.Mkdir(self.base_dir, parents = True)
Simon Glassfea58582014-08-09 15:32:59 -06001416 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1417 commits is not None)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001418 self._PrepareOutputSpace()
1419 self.SetupBuild(board_selected, commits)
1420 self.ProcessResult(None)
1421
1422 # Create jobs to build all commits for each board
1423 for brd in board_selected.itervalues():
Simon Glass190064b2014-08-09 15:33:00 -06001424 job = builderthread.BuilderJob()
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001425 job.board = brd
1426 job.commits = commits
1427 job.keep_outputs = keep_outputs
1428 job.step = self._step
1429 self.queue.put(job)
1430
1431 # Wait until all jobs are started
1432 self.queue.join()
1433
1434 # Wait until we have processed all output
1435 self.out_queue.join()
Simon Glass4653a882014-09-05 19:00:07 -06001436 Print()
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001437 self.ClearLine(0)
Simon Glass2c3deb92014-08-28 09:43:39 -06001438 return (self.fail, self.warned)