]> Git Repo - J-u-boot.git/blame - tools/buildman/builder.py
buildman: Use yellow consistently for warning lines
[J-u-boot.git] / tools / buildman / builder.py
CommitLineData
83d290c5 1# SPDX-License-Identifier: GPL-2.0+
fc3fe1c2
SG
2# Copyright (c) 2013 The Chromium OS Authors.
3#
4# Bloat-o-meter code used here Copyright 2004 Matt Mackall <[email protected]>
5#
fc3fe1c2
SG
6
7import collections
fc3fe1c2
SG
8from datetime import datetime, timedelta
9import glob
10import os
11import re
c05aa036 12import queue
fc3fe1c2 13import shutil
2f256648 14import signal
fc3fe1c2
SG
15import string
16import sys
d436e381 17import threading
fc3fe1c2
SG
18import time
19
190064b4 20import builderthread
fc3fe1c2
SG
21import command
22import gitutil
23import terminal
4653a882 24from terminal import Print
fc3fe1c2
SG
25import toolchain
26
27
28"""
29Theory of Operation
30
31Please see README for user documentation, and you should be familiar with
32that before trying to make sense of this.
33
34Buildman works by keeping the machine as busy as possible, building different
35commits for different boards on multiple CPUs at once.
36
37The source repo (self.git_dir) contains all the commits to be built. Each
38thread works on a single board at a time. It checks out the first commit,
39configures it for that board, then builds it. Then it checks out the next
40commit and builds it (typically without re-configuring). When it runs out
41of commits, it gets another job from the builder and starts again with that
42board.
43
44Clearly the builder threads could work either way - they could check out a
45commit and then built it for all boards. Using separate directories for each
46commit/board pair they could leave their build product around afterwards
47also.
48
49The intent behind building a single board for multiple commits, is to make
50use of incremental builds. Since each commit is built incrementally from
51the previous one, builds are faster. Reconfiguring for a different board
52removes all intermediate object files.
53
54Many threads can be working at once, but each has its own working directory.
55When a thread finishes a build, it puts the output files into a result
56directory.
57
58The base directory used by buildman is normally '../<branch>', i.e.
59a directory higher than the source repository and named after the branch
60being built.
61
62Within the base directory, we have one subdirectory for each commit. Within
63that is one subdirectory for each board. Within that is the build output for
64that commit/board combination.
65
66Buildman also create working directories for each thread, in a .bm-work/
67subdirectory in the base dir.
68
69As an example, say we are building branch 'us-net' for boards 'sandbox' and
70'seaboard', and say that us-net has two commits. We will have directories
71like this:
72
73us-net/ base directory
74 01_of_02_g4ed4ebc_net--Add-tftp-speed-/
75 sandbox/
76 u-boot.bin
77 seaboard/
78 u-boot.bin
79 02_of_02_g4ed4ebc_net--Check-tftp-comp/
80 sandbox/
81 u-boot.bin
82 seaboard/
83 u-boot.bin
84 .bm-work/
85 00/ working directory for thread 0 (contains source checkout)
86 build/ build output
87 01/ working directory for thread 1
88 build/ build output
89 ...
90u-boot/ source directory
91 .git/ repository
92"""
93
94# Possible build outcomes
c05aa036 95OUTCOME_OK, OUTCOME_WARNING, OUTCOME_ERROR, OUTCOME_UNKNOWN = list(range(4))
fc3fe1c2 96
9a6d2e2a 97# Translate a commit subject into a valid filename (and handle unicode)
c05aa036 98trans_valid_chars = str.maketrans('/: ', '---')
fc3fe1c2 99
b464f8e7
SG
100BASE_CONFIG_FILENAMES = [
101 'u-boot.cfg', 'u-boot-spl.cfg', 'u-boot-tpl.cfg'
102]
103
104EXTRA_CONFIG_FILENAMES = [
843312dc
SG
105 '.config', '.config-spl', '.config-tpl',
106 'autoconf.mk', 'autoconf-spl.mk', 'autoconf-tpl.mk',
107 'autoconf.h', 'autoconf-spl.h','autoconf-tpl.h',
843312dc
SG
108]
109
8270e3c1
SG
110class Config:
111 """Holds information about configuration settings for a board."""
b464f8e7 112 def __init__(self, config_filename, target):
8270e3c1
SG
113 self.target = target
114 self.config = {}
b464f8e7 115 for fname in config_filename:
8270e3c1
SG
116 self.config[fname] = {}
117
118 def Add(self, fname, key, value):
119 self.config[fname][key] = value
120
121 def __hash__(self):
122 val = 0
123 for fname in self.config:
c05aa036
SG
124 for key, value in self.config[fname].items():
125 print(key, value)
8270e3c1
SG
126 val = val ^ hash(key) & hash(value)
127 return val
fc3fe1c2 128
48ae4124
AK
129class Environment:
130 """Holds information about environment variables for a board."""
131 def __init__(self, target):
132 self.target = target
133 self.environment = {}
134
135 def Add(self, key, value):
136 self.environment[key] = value
137
fc3fe1c2
SG
138class Builder:
139 """Class for building U-Boot for a particular commit.
140
141 Public members: (many should ->private)
fc3fe1c2
SG
142 already_done: Number of builds already completed
143 base_dir: Base directory to use for builder
144 checkout: True to check out source, False to skip that step.
145 This is used for testing.
146 col: terminal.Color() object
147 count: Number of commits to build
148 do_make: Method to call to invoke Make
149 fail: Number of builds that failed due to error
150 force_build: Force building even if a build already exists
151 force_config_on_failure: If a commit fails for a board, disable
152 incremental building for the next commit we build for that
153 board, so that we will see all warnings/errors again.
4266dc28
SG
154 force_build_failures: If a previously-built build (i.e. built on
155 a previous run of buildman) is marked as failed, rebuild it.
fc3fe1c2
SG
156 git_dir: Git directory containing source repository
157 last_line_len: Length of the last line we printed (used for erasing
158 it with new progress information)
159 num_jobs: Number of jobs to run at once (passed to make as -j)
160 num_threads: Number of builder threads to run
161 out_queue: Queue of results to process
162 re_make_err: Compiled regular expression for ignore_lines
163 queue: Queue of jobs to run
164 threads: List of active threads
165 toolchains: Toolchains object to use for building
166 upto: Current commit number we are building (0.count-1)
167 warned: Number of builds that produced at least one warning
97e91526
SG
168 force_reconfig: Reconfigure U-Boot on each comiit. This disables
169 incremental building, where buildman reconfigures on the first
170 commit for a baord, and then just does an incremental build for
171 the following commits. In fact buildman will reconfigure and
172 retry for any failing commits, so generally the only effect of
173 this option is to slow things down.
189a4968
SG
174 in_tree: Build U-Boot in-tree instead of specifying an output
175 directory separate from the source code. This option is really
176 only useful for testing in-tree builds.
d829f121
SG
177 work_in_output: Use the output directory as the work directory and
178 don't write to a separate output directory.
fc3fe1c2
SG
179
180 Private members:
181 _base_board_dict: Last-summarised Dict of boards
182 _base_err_lines: Last-summarised list of errors
e30965db 183 _base_warn_lines: Last-summarised list of warnings
fc3fe1c2
SG
184 _build_period_us: Time taken for a single build (float object).
185 _complete_delay: Expected delay until completion (timedelta)
186 _next_delay_update: Next time we plan to display a progress update
187 (datatime)
188 _show_unknown: Show unknown boards (those not built) in summary
189 _timestamps: List of timestamps for the completion of the last
190 last _timestamp_count builds. Each is a datetime object.
191 _timestamp_count: Number of timestamps to keep in our list.
192 _working_dir: Base working directory containing all threads
193 """
194 class Outcome:
195 """Records a build outcome for a single make invocation
196
197 Public Members:
198 rc: Outcome value (OUTCOME_...)
199 err_lines: List of error lines or [] if none
200 sizes: Dictionary of image size information, keyed by filename
201 - Each value is itself a dictionary containing
202 values for 'text', 'data' and 'bss', being the integer
203 size in bytes of each section.
204 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
205 value is itself a dictionary:
206 key: function name
207 value: Size of function in bytes
843312dc
SG
208 config: Dictionary keyed by filename - e.g. '.config'. Each
209 value is itself a dictionary:
210 key: config name
211 value: config value
48ae4124
AK
212 environment: Dictionary keyed by environment variable, Each
213 value is the value of environment variable.
fc3fe1c2 214 """
48ae4124
AK
215 def __init__(self, rc, err_lines, sizes, func_sizes, config,
216 environment):
fc3fe1c2
SG
217 self.rc = rc
218 self.err_lines = err_lines
219 self.sizes = sizes
220 self.func_sizes = func_sizes
843312dc 221 self.config = config
48ae4124 222 self.environment = environment
fc3fe1c2
SG
223
224 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
5971ab5c 225 gnu_make='make', checkout=True, show_unknown=True, step=1,
f79f1e0c 226 no_subdirs=False, full_path=False, verbose_build=False,
b50113f3 227 incremental=False, per_board_out_dir=False,
2371d1bc 228 config_only=False, squash_config_y=False,
d829f121 229 warnings_as_errors=False, work_in_output=False):
fc3fe1c2
SG
230 """Create a new Builder object
231
232 Args:
233 toolchains: Toolchains object to use for building
234 base_dir: Base directory to use for builder
235 git_dir: Git directory containing source repository
236 num_threads: Number of builder threads to run
237 num_jobs: Number of jobs to run at once (passed to make as -j)
99796923 238 gnu_make: the command name of GNU Make.
fc3fe1c2
SG
239 checkout: True to check out source, False to skip that step.
240 This is used for testing.
241 show_unknown: Show unknown boards (those not built) in summary
242 step: 1 to process every commit, n to process every nth commit
bb1501f2
SG
243 no_subdirs: Don't create subdirectories when building current
244 source for a single board
245 full_path: Return the full path in CROSS_COMPILE and don't set
246 PATH
d2ce658d 247 verbose_build: Run build with V=1 and don't use 'make -s'
f79f1e0c
SW
248 incremental: Always perform incremental builds; don't run make
249 mrproper when configuring
250 per_board_out_dir: Build in a separate persistent directory per
251 board rather than a thread-specific directory
b50113f3 252 config_only: Only configure each build, don't build it
b464f8e7 253 squash_config_y: Convert CONFIG options with the value 'y' to '1'
2371d1bc 254 warnings_as_errors: Treat all compiler warnings as errors
d829f121
SG
255 work_in_output: Use the output directory as the work directory and
256 don't write to a separate output directory.
fc3fe1c2
SG
257 """
258 self.toolchains = toolchains
259 self.base_dir = base_dir
d829f121
SG
260 if work_in_output:
261 self._working_dir = base_dir
262 else:
263 self._working_dir = os.path.join(base_dir, '.bm-work')
fc3fe1c2 264 self.threads = []
fc3fe1c2 265 self.do_make = self.Make
99796923 266 self.gnu_make = gnu_make
fc3fe1c2
SG
267 self.checkout = checkout
268 self.num_threads = num_threads
269 self.num_jobs = num_jobs
270 self.already_done = 0
271 self.force_build = False
272 self.git_dir = git_dir
273 self._show_unknown = show_unknown
274 self._timestamp_count = 10
275 self._build_period_us = None
276 self._complete_delay = None
277 self._next_delay_update = datetime.now()
278 self.force_config_on_failure = True
4266dc28 279 self.force_build_failures = False
97e91526 280 self.force_reconfig = False
fc3fe1c2 281 self._step = step
189a4968 282 self.in_tree = False
28370c1b 283 self._error_lines = 0
5971ab5c 284 self.no_subdirs = no_subdirs
bb1501f2 285 self.full_path = full_path
d2ce658d 286 self.verbose_build = verbose_build
b50113f3 287 self.config_only = config_only
b464f8e7
SG
288 self.squash_config_y = squash_config_y
289 self.config_filenames = BASE_CONFIG_FILENAMES
d829f121 290 self.work_in_output = work_in_output
b464f8e7
SG
291 if not self.squash_config_y:
292 self.config_filenames += EXTRA_CONFIG_FILENAMES
fc3fe1c2 293
2371d1bc 294 self.warnings_as_errors = warnings_as_errors
fc3fe1c2
SG
295 self.col = terminal.Color()
296
e30965db
SG
297 self._re_function = re.compile('(.*): In function.*')
298 self._re_files = re.compile('In file included from.*')
299 self._re_warning = re.compile('(.*):(\d*):(\d*): warning: .*')
2d48333e 300 self._re_dtb_warning = re.compile('(.*): Warning .*')
e30965db
SG
301 self._re_note = re.compile('(.*):(\d*):(\d*): note: this is the location of the previous.*')
302
c05aa036
SG
303 self.queue = queue.Queue()
304 self.out_queue = queue.Queue()
fc3fe1c2 305 for i in range(self.num_threads):
f79f1e0c
SW
306 t = builderthread.BuilderThread(self, i, incremental,
307 per_board_out_dir)
fc3fe1c2
SG
308 t.setDaemon(True)
309 t.start()
310 self.threads.append(t)
311
312 self.last_line_len = 0
190064b4 313 t = builderthread.ResultThread(self)
fc3fe1c2
SG
314 t.setDaemon(True)
315 t.start()
316 self.threads.append(t)
317
318 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
319 self.re_make_err = re.compile('|'.join(ignore_lines))
320
2f256648
SG
321 # Handle existing graceful with SIGINT / Ctrl-C
322 signal.signal(signal.SIGINT, self.signal_handler)
323
fc3fe1c2
SG
324 def __del__(self):
325 """Get rid of all threads created by the builder"""
326 for t in self.threads:
327 del t
328
2f256648
SG
329 def signal_handler(self, signal, frame):
330 sys.exit(1)
331
b2ea7ab2 332 def SetDisplayOptions(self, show_errors=False, show_sizes=False,
ed966657 333 show_detail=False, show_bloat=False,
48ae4124
AK
334 list_error_boards=False, show_config=False,
335 show_environment=False):
b2ea7ab2
SG
336 """Setup display options for the builder.
337
338 show_errors: True to show summarised error/warning info
339 show_sizes: Show size deltas
f9c094bb 340 show_detail: Show size delta detail for each board if show_sizes
b2ea7ab2 341 show_bloat: Show detail for each function
ed966657 342 list_error_boards: Show the boards which caused each error/warning
843312dc 343 show_config: Show config deltas
48ae4124 344 show_environment: Show environment deltas
b2ea7ab2
SG
345 """
346 self._show_errors = show_errors
347 self._show_sizes = show_sizes
348 self._show_detail = show_detail
349 self._show_bloat = show_bloat
ed966657 350 self._list_error_boards = list_error_boards
843312dc 351 self._show_config = show_config
48ae4124 352 self._show_environment = show_environment
b2ea7ab2 353
fc3fe1c2
SG
354 def _AddTimestamp(self):
355 """Add a new timestamp to the list and record the build period.
356
357 The build period is the length of time taken to perform a single
358 build (one board, one commit).
359 """
360 now = datetime.now()
361 self._timestamps.append(now)
362 count = len(self._timestamps)
363 delta = self._timestamps[-1] - self._timestamps[0]
364 seconds = delta.total_seconds()
365
366 # If we have enough data, estimate build period (time taken for a
367 # single build) and therefore completion time.
368 if count > 1 and self._next_delay_update < now:
369 self._next_delay_update = now + timedelta(seconds=2)
370 if seconds > 0:
371 self._build_period = float(seconds) / count
372 todo = self.count - self.upto
373 self._complete_delay = timedelta(microseconds=
374 self._build_period * todo * 1000000)
375 # Round it
376 self._complete_delay -= timedelta(
377 microseconds=self._complete_delay.microseconds)
378
379 if seconds > 60:
380 self._timestamps.popleft()
381 count -= 1
382
383 def ClearLine(self, length):
384 """Clear any characters on the current line
385
386 Make way for a new line of length 'length', by outputting enough
387 spaces to clear out the old line. Then remember the new length for
388 next time.
389
390 Args:
391 length: Length of new line, in characters
392 """
393 if length < self.last_line_len:
4653a882
SG
394 Print(' ' * (self.last_line_len - length), newline=False)
395 Print('\r', newline=False)
fc3fe1c2
SG
396 self.last_line_len = length
397 sys.stdout.flush()
398
399 def SelectCommit(self, commit, checkout=True):
400 """Checkout the selected commit for this build
401 """
402 self.commit = commit
403 if checkout and self.checkout:
404 gitutil.Checkout(commit.hash)
405
406 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
407 """Run make
408
409 Args:
410 commit: Commit object that is being built
411 brd: Board object that is being built
fd18a89e 412 stage: Stage that we are at (mrproper, config, build)
fc3fe1c2
SG
413 cwd: Directory where make should be run
414 args: Arguments to pass to make
415 kwargs: Arguments to pass to command.RunPipe()
416 """
99796923 417 cmd = [self.gnu_make] + list(args)
fc3fe1c2 418 result = command.RunPipe([cmd], capture=True, capture_stderr=True,
e62a24ce 419 cwd=cwd, raise_on_error=False, infile='/dev/null', **kwargs)
40f11fce
SG
420 if self.verbose_build:
421 result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout
422 result.combined = '%s\n' % (' '.join(cmd)) + result.combined
fc3fe1c2
SG
423 return result
424
425 def ProcessResult(self, result):
426 """Process the result of a build, showing progress information
427
428 Args:
e5a0e5d8
SG
429 result: A CommandResult object, which indicates the result for
430 a single build
fc3fe1c2
SG
431 """
432 col = terminal.Color()
433 if result:
434 target = result.brd.target
435
fc3fe1c2
SG
436 self.upto += 1
437 if result.return_code != 0:
438 self.fail += 1
439 elif result.stderr:
440 self.warned += 1
441 if result.already_done:
442 self.already_done += 1
e5a0e5d8 443 if self._verbose:
4653a882 444 Print('\r', newline=False)
e5a0e5d8
SG
445 self.ClearLine(0)
446 boards_selected = {target : result.brd}
447 self.ResetResultSummary(boards_selected)
448 self.ProduceResultSummary(result.commit_upto, self.commits,
449 boards_selected)
fc3fe1c2
SG
450 else:
451 target = '(starting)'
452
453 # Display separate counts for ok, warned and fail
454 ok = self.upto - self.warned - self.fail
455 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
456 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
457 line += self.col.Color(self.col.RED, '%5d' % self.fail)
458
459 name = ' /%-5d ' % self.count
460
461 # Add our current completion time estimate
462 self._AddTimestamp()
463 if self._complete_delay:
464 name += '%s : ' % self._complete_delay
465 # When building all boards for a commit, we can print a commit
466 # progress message.
467 if result and result.commit_upto is None:
468 name += 'commit %2d/%-3d' % (self.commit_upto + 1,
469 self.commit_count)
470
471 name += target
4653a882 472 Print(line + name, newline=False)
960421ec 473 length = 16 + len(name)
fc3fe1c2
SG
474 self.ClearLine(length)
475
476 def _GetOutputDir(self, commit_upto):
477 """Get the name of the output directory for a commit number
478
479 The output directory is typically .../<branch>/<commit>.
480
481 Args:
482 commit_upto: Commit number to use (0..self.count-1)
483 """
5971ab5c 484 commit_dir = None
fea5858e
SG
485 if self.commits:
486 commit = self.commits[commit_upto]
487 subject = commit.subject.translate(trans_valid_chars)
925f6adf 488 # See _GetOutputSpaceRemovals() which parses this name
fea5858e
SG
489 commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
490 self.commit_count, commit.hash, subject[:20]))
5971ab5c 491 elif not self.no_subdirs:
fea5858e 492 commit_dir = 'current'
5971ab5c
SG
493 if not commit_dir:
494 return self.base_dir
495 return os.path.join(self.base_dir, commit_dir)
fc3fe1c2
SG
496
497 def GetBuildDir(self, commit_upto, target):
498 """Get the name of the build directory for a commit number
499
500 The build directory is typically .../<branch>/<commit>/<target>.
501
502 Args:
503 commit_upto: Commit number to use (0..self.count-1)
504 target: Target name
505 """
506 output_dir = self._GetOutputDir(commit_upto)
507 return os.path.join(output_dir, target)
508
509 def GetDoneFile(self, commit_upto, target):
510 """Get the name of the done file for a commit number
511
512 Args:
513 commit_upto: Commit number to use (0..self.count-1)
514 target: Target name
515 """
516 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
517
518 def GetSizesFile(self, commit_upto, target):
519 """Get the name of the sizes file for a commit number
520
521 Args:
522 commit_upto: Commit number to use (0..self.count-1)
523 target: Target name
524 """
525 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
526
527 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
528 """Get the name of the funcsizes file for a commit number and ELF file
529
530 Args:
531 commit_upto: Commit number to use (0..self.count-1)
532 target: Target name
533 elf_fname: Filename of elf image
534 """
535 return os.path.join(self.GetBuildDir(commit_upto, target),
536 '%s.sizes' % elf_fname.replace('/', '-'))
537
538 def GetObjdumpFile(self, commit_upto, target, elf_fname):
539 """Get the name of the objdump file for a commit number and ELF file
540
541 Args:
542 commit_upto: Commit number to use (0..self.count-1)
543 target: Target name
544 elf_fname: Filename of elf image
545 """
546 return os.path.join(self.GetBuildDir(commit_upto, target),
547 '%s.objdump' % elf_fname.replace('/', '-'))
548
549 def GetErrFile(self, commit_upto, target):
550 """Get the name of the err file for a commit number
551
552 Args:
553 commit_upto: Commit number to use (0..self.count-1)
554 target: Target name
555 """
556 output_dir = self.GetBuildDir(commit_upto, target)
557 return os.path.join(output_dir, 'err')
558
559 def FilterErrors(self, lines):
560 """Filter out errors in which we have no interest
561
562 We should probably use map().
563
564 Args:
565 lines: List of error lines, each a string
566 Returns:
567 New list with only interesting lines included
568 """
569 out_lines = []
570 for line in lines:
571 if not self.re_make_err.search(line):
572 out_lines.append(line)
573 return out_lines
574
575 def ReadFuncSizes(self, fname, fd):
576 """Read function sizes from the output of 'nm'
577
578 Args:
579 fd: File containing data to read
580 fname: Filename we are reading from (just for errors)
581
582 Returns:
583 Dictionary containing size of each function in bytes, indexed by
584 function name.
585 """
586 sym = {}
587 for line in fd.readlines():
588 try:
d08c38c3
TR
589 if line.strip():
590 size, type, name = line[:-1].split()
fc3fe1c2 591 except:
4653a882 592 Print("Invalid line in file '%s': '%s'" % (fname, line[:-1]))
fc3fe1c2
SG
593 continue
594 if type in 'tTdDbB':
595 # function names begin with '.' on 64-bit powerpc
596 if '.' in name[1:]:
597 name = 'static.' + name.split('.')[0]
598 sym[name] = sym.get(name, 0) + int(size, 16)
599 return sym
600
843312dc
SG
601 def _ProcessConfig(self, fname):
602 """Read in a .config, autoconf.mk or autoconf.h file
603
604 This function handles all config file types. It ignores comments and
605 any #defines which don't start with CONFIG_.
606
607 Args:
608 fname: Filename to read
609
610 Returns:
611 Dictionary:
612 key: Config name (e.g. CONFIG_DM)
613 value: Config value (e.g. 1)
614 """
615 config = {}
616 if os.path.exists(fname):
617 with open(fname) as fd:
618 for line in fd:
619 line = line.strip()
620 if line.startswith('#define'):
621 values = line[8:].split(' ', 1)
622 if len(values) > 1:
623 key, value = values
624 else:
625 key = values[0]
b464f8e7 626 value = '1' if self.squash_config_y else ''
843312dc
SG
627 if not key.startswith('CONFIG_'):
628 continue
629 elif not line or line[0] in ['#', '*', '/']:
630 continue
631 else:
632 key, value = line.split('=', 1)
b464f8e7
SG
633 if self.squash_config_y and value == 'y':
634 value = '1'
843312dc
SG
635 config[key] = value
636 return config
637
48ae4124
AK
638 def _ProcessEnvironment(self, fname):
639 """Read in a uboot.env file
640
641 This function reads in environment variables from a file.
642
643 Args:
644 fname: Filename to read
645
646 Returns:
647 Dictionary:
648 key: environment variable (e.g. bootlimit)
649 value: value of environment variable (e.g. 1)
650 """
651 environment = {}
652 if os.path.exists(fname):
653 with open(fname) as fd:
654 for line in fd.read().split('\0'):
655 try:
656 key, value = line.split('=', 1)
657 environment[key] = value
658 except ValueError:
659 # ignore lines we can't parse
660 pass
661 return environment
662
843312dc 663 def GetBuildOutcome(self, commit_upto, target, read_func_sizes,
48ae4124 664 read_config, read_environment):
fc3fe1c2
SG
665 """Work out the outcome of a build.
666
667 Args:
668 commit_upto: Commit number to check (0..n-1)
669 target: Target board to check
670 read_func_sizes: True to read function size information
843312dc 671 read_config: True to read .config and autoconf.h files
48ae4124 672 read_environment: True to read uboot.env files
fc3fe1c2
SG
673
674 Returns:
675 Outcome object
676 """
677 done_file = self.GetDoneFile(commit_upto, target)
678 sizes_file = self.GetSizesFile(commit_upto, target)
679 sizes = {}
680 func_sizes = {}
843312dc 681 config = {}
48ae4124 682 environment = {}
fc3fe1c2
SG
683 if os.path.exists(done_file):
684 with open(done_file, 'r') as fd:
347ea0b6
SG
685 try:
686 return_code = int(fd.readline())
687 except ValueError:
688 # The file may be empty due to running out of disk space.
689 # Try a rebuild
690 return_code = 1
fc3fe1c2
SG
691 err_lines = []
692 err_file = self.GetErrFile(commit_upto, target)
693 if os.path.exists(err_file):
694 with open(err_file, 'r') as fd:
695 err_lines = self.FilterErrors(fd.readlines())
696
697 # Decide whether the build was ok, failed or created warnings
698 if return_code:
699 rc = OUTCOME_ERROR
700 elif len(err_lines):
701 rc = OUTCOME_WARNING
702 else:
703 rc = OUTCOME_OK
704
705 # Convert size information to our simple format
706 if os.path.exists(sizes_file):
707 with open(sizes_file, 'r') as fd:
708 for line in fd.readlines():
709 values = line.split()
710 rodata = 0
711 if len(values) > 6:
712 rodata = int(values[6], 16)
713 size_dict = {
714 'all' : int(values[0]) + int(values[1]) +
715 int(values[2]),
716 'text' : int(values[0]) - rodata,
717 'data' : int(values[1]),
718 'bss' : int(values[2]),
719 'rodata' : rodata,
720 }
721 sizes[values[5]] = size_dict
722
723 if read_func_sizes:
724 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
725 for fname in glob.glob(pattern):
726 with open(fname, 'r') as fd:
727 dict_name = os.path.basename(fname).replace('.sizes',
728 '')
729 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
730
843312dc
SG
731 if read_config:
732 output_dir = self.GetBuildDir(commit_upto, target)
b464f8e7 733 for name in self.config_filenames:
843312dc
SG
734 fname = os.path.join(output_dir, name)
735 config[name] = self._ProcessConfig(fname)
736
48ae4124
AK
737 if read_environment:
738 output_dir = self.GetBuildDir(commit_upto, target)
739 fname = os.path.join(output_dir, 'uboot.env')
740 environment = self._ProcessEnvironment(fname)
741
742 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config,
743 environment)
fc3fe1c2 744
48ae4124 745 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {}, {})
fc3fe1c2 746
843312dc 747 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
48ae4124 748 read_config, read_environment):
fc3fe1c2
SG
749 """Calculate a summary of the results of building a commit.
750
751 Args:
752 board_selected: Dict containing boards to summarise
753 commit_upto: Commit number to summarize (0..self.count-1)
754 read_func_sizes: True to read function size information
843312dc 755 read_config: True to read .config and autoconf.h files
48ae4124 756 read_environment: True to read uboot.env files
fc3fe1c2
SG
757
758 Returns:
759 Tuple:
760 Dict containing boards which passed building this commit.
761 keyed by board.target
e30965db 762 List containing a summary of error lines
ed966657
SG
763 Dict keyed by error line, containing a list of the Board
764 objects with that error
e30965db
SG
765 List containing a summary of warning lines
766 Dict keyed by error line, containing a list of the Board
767 objects with that warning
8270e3c1
SG
768 Dictionary keyed by board.target. Each value is a dictionary:
769 key: filename - e.g. '.config'
843312dc
SG
770 value is itself a dictionary:
771 key: config name
772 value: config value
48ae4124
AK
773 Dictionary keyed by board.target. Each value is a dictionary:
774 key: environment variable
775 value: value of environment variable
fc3fe1c2 776 """
e30965db
SG
777 def AddLine(lines_summary, lines_boards, line, board):
778 line = line.rstrip()
779 if line in lines_boards:
780 lines_boards[line].append(board)
781 else:
782 lines_boards[line] = [board]
783 lines_summary.append(line)
784
fc3fe1c2
SG
785 board_dict = {}
786 err_lines_summary = []
ed966657 787 err_lines_boards = {}
e30965db
SG
788 warn_lines_summary = []
789 warn_lines_boards = {}
843312dc 790 config = {}
48ae4124 791 environment = {}
fc3fe1c2 792
c05aa036 793 for board in boards_selected.values():
fc3fe1c2 794 outcome = self.GetBuildOutcome(commit_upto, board.target,
48ae4124
AK
795 read_func_sizes, read_config,
796 read_environment)
fc3fe1c2 797 board_dict[board.target] = outcome
e30965db
SG
798 last_func = None
799 last_was_warning = False
800 for line in outcome.err_lines:
801 if line:
802 if (self._re_function.match(line) or
803 self._re_files.match(line)):
804 last_func = line
ed966657 805 else:
2d48333e
SG
806 is_warning = (self._re_warning.match(line) or
807 self._re_dtb_warning.match(line))
e30965db
SG
808 is_note = self._re_note.match(line)
809 if is_warning or (last_was_warning and is_note):
810 if last_func:
811 AddLine(warn_lines_summary, warn_lines_boards,
812 last_func, board)
813 AddLine(warn_lines_summary, warn_lines_boards,
814 line, board)
815 else:
816 if last_func:
817 AddLine(err_lines_summary, err_lines_boards,
818 last_func, board)
819 AddLine(err_lines_summary, err_lines_boards,
820 line, board)
821 last_was_warning = is_warning
822 last_func = None
b464f8e7
SG
823 tconfig = Config(self.config_filenames, board.target)
824 for fname in self.config_filenames:
843312dc 825 if outcome.config:
c05aa036 826 for key, value in outcome.config[fname].items():
8270e3c1
SG
827 tconfig.Add(fname, key, value)
828 config[board.target] = tconfig
843312dc 829
48ae4124
AK
830 tenvironment = Environment(board.target)
831 if outcome.environment:
c05aa036 832 for key, value in outcome.environment.items():
48ae4124
AK
833 tenvironment.Add(key, value)
834 environment[board.target] = tenvironment
835
e30965db 836 return (board_dict, err_lines_summary, err_lines_boards,
48ae4124 837 warn_lines_summary, warn_lines_boards, config, environment)
fc3fe1c2
SG
838
839 def AddOutcome(self, board_dict, arch_list, changes, char, color):
840 """Add an output to our list of outcomes for each architecture
841
842 This simple function adds failing boards (changes) to the
843 relevant architecture string, so we can print the results out
844 sorted by architecture.
845
846 Args:
847 board_dict: Dict containing all boards
848 arch_list: Dict keyed by arch name. Value is a string containing
849 a list of board names which failed for that arch.
850 changes: List of boards to add to arch_list
851 color: terminal.Colour object
852 """
853 done_arch = {}
854 for target in changes:
855 if target in board_dict:
856 arch = board_dict[target].arch
857 else:
858 arch = 'unknown'
859 str = self.col.Color(color, ' ' + target)
860 if not arch in done_arch:
63c619ee 861 str = ' %s %s' % (self.col.Color(color, char), str)
fc3fe1c2
SG
862 done_arch[arch] = True
863 if not arch in arch_list:
864 arch_list[arch] = str
865 else:
866 arch_list[arch] += str
867
868
869 def ColourNum(self, num):
870 color = self.col.RED if num > 0 else self.col.GREEN
871 if num == 0:
872 return '0'
873 return self.col.Color(color, str(num))
874
875 def ResetResultSummary(self, board_selected):
876 """Reset the results summary ready for use.
877
878 Set up the base board list to be all those selected, and set the
879 error lines to empty.
880
881 Following this, calls to PrintResultSummary() will use this
882 information to work out what has changed.
883
884 Args:
885 board_selected: Dict containing boards to summarise, keyed by
886 board.target
887 """
888 self._base_board_dict = {}
889 for board in board_selected:
48ae4124
AK
890 self._base_board_dict[board] = Builder.Outcome(0, [], [], {}, {},
891 {})
fc3fe1c2 892 self._base_err_lines = []
e30965db
SG
893 self._base_warn_lines = []
894 self._base_err_line_boards = {}
895 self._base_warn_line_boards = {}
8270e3c1 896 self._base_config = None
48ae4124 897 self._base_environment = None
fc3fe1c2
SG
898
899 def PrintFuncSizeDetail(self, fname, old, new):
900 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
901 delta, common = [], {}
902
903 for a in old:
904 if a in new:
905 common[a] = 1
906
907 for name in old:
908 if name not in common:
909 remove += 1
910 down += old[name]
911 delta.append([-old[name], name])
912
913 for name in new:
914 if name not in common:
915 add += 1
916 up += new[name]
917 delta.append([new[name], name])
918
919 for name in common:
920 diff = new.get(name, 0) - old.get(name, 0)
921 if diff > 0:
922 grow, up = grow + 1, up + diff
923 elif diff < 0:
924 shrink, down = shrink + 1, down - diff
925 delta.append([diff, name])
926
927 delta.sort()
928 delta.reverse()
929
930 args = [add, -remove, grow, -shrink, up, -down, up - down]
d5686a61 931 if max(args) == 0 and min(args) == 0:
fc3fe1c2
SG
932 return
933 args = [self.ColourNum(x) for x in args]
934 indent = ' ' * 15
4653a882
SG
935 Print('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
936 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
937 Print('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
938 'delta'))
fc3fe1c2
SG
939 for diff, name in delta:
940 if diff:
941 color = self.col.RED if diff > 0 else self.col.GREEN
942 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
943 old.get(name, '-'), new.get(name,'-'), diff)
4653a882 944 Print(msg, colour=color)
fc3fe1c2
SG
945
946
947 def PrintSizeDetail(self, target_list, show_bloat):
948 """Show details size information for each board
949
950 Args:
951 target_list: List of targets, each a dict containing:
952 'target': Target name
953 'total_diff': Total difference in bytes across all areas
954 <part_name>: Difference for that part
955 show_bloat: Show detail for each function
956 """
957 targets_by_diff = sorted(target_list, reverse=True,
958 key=lambda x: x['_total_diff'])
959 for result in targets_by_diff:
960 printed_target = False
961 for name in sorted(result):
962 diff = result[name]
963 if name.startswith('_'):
964 continue
965 if diff != 0:
966 color = self.col.RED if diff > 0 else self.col.GREEN
967 msg = ' %s %+d' % (name, diff)
968 if not printed_target:
4653a882
SG
969 Print('%10s %-15s:' % ('', result['_target']),
970 newline=False)
fc3fe1c2 971 printed_target = True
4653a882 972 Print(msg, colour=color, newline=False)
fc3fe1c2 973 if printed_target:
4653a882 974 Print()
fc3fe1c2
SG
975 if show_bloat:
976 target = result['_target']
977 outcome = result['_outcome']
978 base_outcome = self._base_board_dict[target]
979 for fname in outcome.func_sizes:
980 self.PrintFuncSizeDetail(fname,
981 base_outcome.func_sizes[fname],
982 outcome.func_sizes[fname])
983
984
985 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
986 show_bloat):
987 """Print a summary of image sizes broken down by section.
988
989 The summary takes the form of one line per architecture. The
990 line contains deltas for each of the sections (+ means the section
9de5c397 991 got bigger, - means smaller). The numbers are the average number
fc3fe1c2
SG
992 of bytes that a board in this section increased by.
993
994 For example:
995 powerpc: (622 boards) text -0.0
996 arm: (285 boards) text -0.0
997 nds32: (3 boards) text -8.0
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.
f9c094bb 1004 show_detail: Show size delta detail for each board
fc3fe1c2
SG
1005 show_bloat: Show detail for each function
1006 """
1007 arch_list = {}
1008 arch_count = {}
1009
1010 # Calculate changes in size for different image parts
1011 # The previous sizes are in Board.sizes, for each board
1012 for target in board_dict:
1013 if target not in board_selected:
1014 continue
1015 base_sizes = self._base_board_dict[target].sizes
1016 outcome = board_dict[target]
1017 sizes = outcome.sizes
1018
1019 # Loop through the list of images, creating a dict of size
1020 # changes for each image/part. We end up with something like
1021 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
1022 # which means that U-Boot data increased by 5 bytes and SPL
1023 # text decreased by 4.
1024 err = {'_target' : target}
1025 for image in sizes:
1026 if image in base_sizes:
1027 base_image = base_sizes[image]
1028 # Loop through the text, data, bss parts
1029 for part in sorted(sizes[image]):
1030 diff = sizes[image][part] - base_image[part]
1031 col = None
1032 if diff:
1033 if image == 'u-boot':
1034 name = part
1035 else:
1036 name = image + ':' + part
1037 err[name] = diff
1038 arch = board_selected[target].arch
1039 if not arch in arch_count:
1040 arch_count[arch] = 1
1041 else:
1042 arch_count[arch] += 1
1043 if not sizes:
1044 pass # Only add to our list when we have some stats
1045 elif not arch in arch_list:
1046 arch_list[arch] = [err]
1047 else:
1048 arch_list[arch].append(err)
1049
1050 # We now have a list of image size changes sorted by arch
1051 # Print out a summary of these
c05aa036 1052 for arch, target_list in arch_list.items():
fc3fe1c2
SG
1053 # Get total difference for each type
1054 totals = {}
1055 for result in target_list:
1056 total = 0
c05aa036 1057 for name, diff in result.items():
fc3fe1c2
SG
1058 if name.startswith('_'):
1059 continue
1060 total += diff
1061 if name in totals:
1062 totals[name] += diff
1063 else:
1064 totals[name] = diff
1065 result['_total_diff'] = total
1066 result['_outcome'] = board_dict[result['_target']]
1067
1068 count = len(target_list)
1069 printed_arch = False
1070 for name in sorted(totals):
1071 diff = totals[name]
1072 if diff:
1073 # Display the average difference in this name for this
1074 # architecture
1075 avg_diff = float(diff) / count
1076 color = self.col.RED if avg_diff > 0 else self.col.GREEN
1077 msg = ' %s %+1.1f' % (name, avg_diff)
1078 if not printed_arch:
4653a882
SG
1079 Print('%10s: (for %d/%d boards)' % (arch, count,
1080 arch_count[arch]), newline=False)
fc3fe1c2 1081 printed_arch = True
4653a882 1082 Print(msg, colour=color, newline=False)
fc3fe1c2
SG
1083
1084 if printed_arch:
4653a882 1085 Print()
fc3fe1c2
SG
1086 if show_detail:
1087 self.PrintSizeDetail(target_list, show_bloat)
1088
1089
1090 def PrintResultSummary(self, board_selected, board_dict, err_lines,
e30965db 1091 err_line_boards, warn_lines, warn_line_boards,
48ae4124
AK
1092 config, environment, show_sizes, show_detail,
1093 show_bloat, show_config, show_environment):
fc3fe1c2
SG
1094 """Compare results with the base results and display delta.
1095
1096 Only boards mentioned in board_selected will be considered. This
1097 function is intended to be called repeatedly with the results of
1098 each commit. It therefore shows a 'diff' between what it saw in
1099 the last call and what it sees now.
1100
1101 Args:
1102 board_selected: Dict containing boards to summarise, keyed by
1103 board.target
1104 board_dict: Dict containing boards for which we built this
1105 commit, keyed by board.target. The value is an Outcome object.
1106 err_lines: A list of errors for this commit, or [] if there is
1107 none, or we don't want to print errors
ed966657
SG
1108 err_line_boards: Dict keyed by error line, containing a list of
1109 the Board objects with that error
e30965db
SG
1110 warn_lines: A list of warnings for this commit, or [] if there is
1111 none, or we don't want to print errors
1112 warn_line_boards: Dict keyed by warning line, containing a list of
1113 the Board objects with that warning
843312dc
SG
1114 config: Dictionary keyed by filename - e.g. '.config'. Each
1115 value is itself a dictionary:
1116 key: config name
1117 value: config value
48ae4124
AK
1118 environment: Dictionary keyed by environment variable, Each
1119 value is the value of environment variable.
fc3fe1c2 1120 show_sizes: Show image size deltas
f9c094bb 1121 show_detail: Show size delta detail for each board if show_sizes
fc3fe1c2 1122 show_bloat: Show detail for each function
843312dc 1123 show_config: Show config changes
48ae4124 1124 show_environment: Show environment changes
fc3fe1c2 1125 """
e30965db 1126 def _BoardList(line, line_boards):
ed966657
SG
1127 """Helper function to get a line of boards containing a line
1128
1129 Args:
1130 line: Error line to search for
1131 Return:
1132 String containing a list of boards with that error line, or
1133 '' if the user has not requested such a list
1134 """
1135 if self._list_error_boards:
1136 names = []
e30965db 1137 for board in line_boards[line]:
f66153be
SG
1138 if not board.target in names:
1139 names.append(board.target)
ed966657
SG
1140 names_str = '(%s) ' % ','.join(names)
1141 else:
1142 names_str = ''
1143 return names_str
1144
e30965db
SG
1145 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1146 char):
1147 better_lines = []
1148 worse_lines = []
1149 for line in lines:
1150 if line not in base_lines:
1151 worse_lines.append(char + '+' +
1152 _BoardList(line, line_boards) + line)
1153 for line in base_lines:
1154 if line not in lines:
1155 better_lines.append(char + '-' +
1156 _BoardList(line, base_line_boards) + line)
1157 return better_lines, worse_lines
1158
843312dc
SG
1159 def _CalcConfig(delta, name, config):
1160 """Calculate configuration changes
1161
1162 Args:
1163 delta: Type of the delta, e.g. '+'
1164 name: name of the file which changed (e.g. .config)
1165 config: configuration change dictionary
1166 key: config name
1167 value: config value
1168 Returns:
1169 String containing the configuration changes which can be
1170 printed
1171 """
1172 out = ''
1173 for key in sorted(config.keys()):
1174 out += '%s=%s ' % (key, config[key])
8270e3c1 1175 return '%s %s: %s' % (delta, name, out)
843312dc 1176
8270e3c1
SG
1177 def _AddConfig(lines, name, config_plus, config_minus, config_change):
1178 """Add changes in configuration to a list
843312dc
SG
1179
1180 Args:
8270e3c1
SG
1181 lines: list to add to
1182 name: config file name
843312dc
SG
1183 config_plus: configurations added, dictionary
1184 key: config name
1185 value: config value
1186 config_minus: configurations removed, dictionary
1187 key: config name
1188 value: config value
1189 config_change: configurations changed, dictionary
1190 key: config name
1191 value: config value
1192 """
1193 if config_plus:
8270e3c1 1194 lines.append(_CalcConfig('+', name, config_plus))
843312dc 1195 if config_minus:
8270e3c1 1196 lines.append(_CalcConfig('-', name, config_minus))
843312dc 1197 if config_change:
8270e3c1
SG
1198 lines.append(_CalcConfig('c', name, config_change))
1199
1200 def _OutputConfigInfo(lines):
1201 for line in lines:
1202 if not line:
1203 continue
1204 if line[0] == '+':
1205 col = self.col.GREEN
1206 elif line[0] == '-':
1207 col = self.col.RED
1208 elif line[0] == 'c':
1209 col = self.col.YELLOW
1210 Print(' ' + line, newline=True, colour=col)
1211
b206d87d
SG
1212 def _OutputErrLines(err_lines, colour):
1213 """Output the line of error/warning lines, if not empty
1214
1215 Also increments self._error_lines if err_lines not empty
1216
1217 Args:
1218 err_lines: List of strings, each an error or warning line,
1219 possibly including a list of boards with that error/warning
1220 colour: Colour to use for output
1221 """
1222 if err_lines:
1223 Print('\n'.join(err_lines), colour=colour)
1224 self._error_lines += 1
1225
843312dc 1226
4cf2b221 1227 ok_boards = [] # List of boards fixed since last commit
6af7101b 1228 warn_boards = [] # List of boards with warnings since last commit
4cf2b221
SG
1229 err_boards = [] # List of new broken boards since last commit
1230 new_boards = [] # List of boards that didn't exist last time
1231 unknown_boards = [] # List of boards that were not built
fc3fe1c2
SG
1232
1233 for target in board_dict:
1234 if target not in board_selected:
1235 continue
1236
1237 # If the board was built last time, add its outcome to a list
1238 if target in self._base_board_dict:
1239 base_outcome = self._base_board_dict[target].rc
1240 outcome = board_dict[target]
1241 if outcome.rc == OUTCOME_UNKNOWN:
4cf2b221 1242 unknown_boards.append(target)
fc3fe1c2 1243 elif outcome.rc < base_outcome:
6af7101b
SG
1244 if outcome.rc == OUTCOME_WARNING:
1245 warn_boards.append(target)
1246 else:
1247 ok_boards.append(target)
fc3fe1c2 1248 elif outcome.rc > base_outcome:
6af7101b
SG
1249 if outcome.rc == OUTCOME_WARNING:
1250 warn_boards.append(target)
1251 else:
1252 err_boards.append(target)
fc3fe1c2 1253 else:
4cf2b221 1254 new_boards.append(target)
fc3fe1c2 1255
b206d87d 1256 # Get a list of errors and warnings that have appeared, and disappeared
e30965db
SG
1257 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1258 self._base_err_line_boards, err_lines, err_line_boards, '')
1259 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1260 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
fc3fe1c2
SG
1261
1262 # Display results by arch
6af7101b
SG
1263 if any((ok_boards, warn_boards, err_boards, unknown_boards, new_boards,
1264 worse_err, better_err, worse_warn, better_warn)):
fc3fe1c2 1265 arch_list = {}
4cf2b221 1266 self.AddOutcome(board_selected, arch_list, ok_boards, '',
fc3fe1c2 1267 self.col.GREEN)
6af7101b
SG
1268 self.AddOutcome(board_selected, arch_list, warn_boards, 'w+',
1269 self.col.YELLOW)
4cf2b221 1270 self.AddOutcome(board_selected, arch_list, err_boards, '+',
fc3fe1c2 1271 self.col.RED)
4cf2b221 1272 self.AddOutcome(board_selected, arch_list, new_boards, '*', self.col.BLUE)
fc3fe1c2 1273 if self._show_unknown:
4cf2b221 1274 self.AddOutcome(board_selected, arch_list, unknown_boards, '?',
fc3fe1c2 1275 self.col.MAGENTA)
c05aa036 1276 for arch, target_list in arch_list.items():
4653a882 1277 Print('%10s: %s' % (arch, target_list))
28370c1b 1278 self._error_lines += 1
b206d87d
SG
1279 _OutputErrLines(better_err, colour=self.col.GREEN)
1280 _OutputErrLines(worse_err, colour=self.col.RED)
1281 _OutputErrLines(better_warn, colour=self.col.CYAN)
5627bd9d 1282 _OutputErrLines(worse_warn, colour=self.col.YELLOW)
fc3fe1c2
SG
1283
1284 if show_sizes:
1285 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1286 show_bloat)
1287
48ae4124
AK
1288 if show_environment and self._base_environment:
1289 lines = []
1290
1291 for target in board_dict:
1292 if target not in board_selected:
1293 continue
1294
1295 tbase = self._base_environment[target]
1296 tenvironment = environment[target]
1297 environment_plus = {}
1298 environment_minus = {}
1299 environment_change = {}
1300 base = tbase.environment
c05aa036 1301 for key, value in tenvironment.environment.items():
48ae4124
AK
1302 if key not in base:
1303 environment_plus[key] = value
c05aa036 1304 for key, value in base.items():
48ae4124
AK
1305 if key not in tenvironment.environment:
1306 environment_minus[key] = value
c05aa036 1307 for key, value in base.items():
48ae4124
AK
1308 new_value = tenvironment.environment.get(key)
1309 if new_value and value != new_value:
1310 desc = '%s -> %s' % (value, new_value)
1311 environment_change[key] = desc
1312
1313 _AddConfig(lines, target, environment_plus, environment_minus,
1314 environment_change)
1315
1316 _OutputConfigInfo(lines)
1317
8270e3c1
SG
1318 if show_config and self._base_config:
1319 summary = {}
1320 arch_config_plus = {}
1321 arch_config_minus = {}
1322 arch_config_change = {}
1323 arch_list = []
1324
1325 for target in board_dict:
1326 if target not in board_selected:
1327 continue
1328 arch = board_selected[target].arch
1329 if arch not in arch_list:
1330 arch_list.append(arch)
1331
1332 for arch in arch_list:
1333 arch_config_plus[arch] = {}
1334 arch_config_minus[arch] = {}
1335 arch_config_change[arch] = {}
b464f8e7 1336 for name in self.config_filenames:
8270e3c1
SG
1337 arch_config_plus[arch][name] = {}
1338 arch_config_minus[arch][name] = {}
1339 arch_config_change[arch][name] = {}
1340
1341 for target in board_dict:
1342 if target not in board_selected:
843312dc 1343 continue
8270e3c1
SG
1344
1345 arch = board_selected[target].arch
1346
1347 all_config_plus = {}
1348 all_config_minus = {}
1349 all_config_change = {}
1350 tbase = self._base_config[target]
1351 tconfig = config[target]
1352 lines = []
b464f8e7 1353 for name in self.config_filenames:
8270e3c1
SG
1354 if not tconfig.config[name]:
1355 continue
1356 config_plus = {}
1357 config_minus = {}
1358 config_change = {}
1359 base = tbase.config[name]
c05aa036 1360 for key, value in tconfig.config[name].items():
8270e3c1
SG
1361 if key not in base:
1362 config_plus[key] = value
1363 all_config_plus[key] = value
c05aa036 1364 for key, value in base.items():
8270e3c1
SG
1365 if key not in tconfig.config[name]:
1366 config_minus[key] = value
1367 all_config_minus[key] = value
c05aa036 1368 for key, value in base.items():
8270e3c1
SG
1369 new_value = tconfig.config.get(key)
1370 if new_value and value != new_value:
1371 desc = '%s -> %s' % (value, new_value)
1372 config_change[key] = desc
1373 all_config_change[key] = desc
1374
1375 arch_config_plus[arch][name].update(config_plus)
1376 arch_config_minus[arch][name].update(config_minus)
1377 arch_config_change[arch][name].update(config_change)
1378
1379 _AddConfig(lines, name, config_plus, config_minus,
1380 config_change)
1381 _AddConfig(lines, 'all', all_config_plus, all_config_minus,
1382 all_config_change)
1383 summary[target] = '\n'.join(lines)
1384
1385 lines_by_target = {}
c05aa036 1386 for target, lines in summary.items():
8270e3c1
SG
1387 if lines in lines_by_target:
1388 lines_by_target[lines].append(target)
1389 else:
1390 lines_by_target[lines] = [target]
1391
1392 for arch in arch_list:
1393 lines = []
1394 all_plus = {}
1395 all_minus = {}
1396 all_change = {}
b464f8e7 1397 for name in self.config_filenames:
8270e3c1
SG
1398 all_plus.update(arch_config_plus[arch][name])
1399 all_minus.update(arch_config_minus[arch][name])
1400 all_change.update(arch_config_change[arch][name])
1401 _AddConfig(lines, name, arch_config_plus[arch][name],
1402 arch_config_minus[arch][name],
1403 arch_config_change[arch][name])
1404 _AddConfig(lines, 'all', all_plus, all_minus, all_change)
1405 #arch_summary[target] = '\n'.join(lines)
1406 if lines:
1407 Print('%s:' % arch)
1408 _OutputConfigInfo(lines)
1409
c05aa036 1410 for lines, targets in lines_by_target.items():
8270e3c1
SG
1411 if not lines:
1412 continue
1413 Print('%s :' % ' '.join(sorted(targets)))
1414 _OutputConfigInfo(lines.split('\n'))
1415
843312dc 1416
fc3fe1c2
SG
1417 # Save our updated information for the next call to this function
1418 self._base_board_dict = board_dict
1419 self._base_err_lines = err_lines
e30965db
SG
1420 self._base_warn_lines = warn_lines
1421 self._base_err_line_boards = err_line_boards
1422 self._base_warn_line_boards = warn_line_boards
843312dc 1423 self._base_config = config
48ae4124 1424 self._base_environment = environment
fc3fe1c2
SG
1425
1426 # Get a list of boards that did not get built, if needed
1427 not_built = []
1428 for board in board_selected:
1429 if not board in board_dict:
1430 not_built.append(board)
1431 if not_built:
4653a882
SG
1432 Print("Boards not built (%d): %s" % (len(not_built),
1433 ', '.join(not_built)))
fc3fe1c2 1434
b2ea7ab2 1435 def ProduceResultSummary(self, commit_upto, commits, board_selected):
e30965db 1436 (board_dict, err_lines, err_line_boards, warn_lines,
48ae4124 1437 warn_line_boards, config, environment) = self.GetResultSummary(
ed966657 1438 board_selected, commit_upto,
843312dc 1439 read_func_sizes=self._show_bloat,
48ae4124
AK
1440 read_config=self._show_config,
1441 read_environment=self._show_environment)
b2ea7ab2
SG
1442 if commits:
1443 msg = '%02d: %s' % (commit_upto + 1,
1444 commits[commit_upto].subject)
4653a882 1445 Print(msg, colour=self.col.BLUE)
b2ea7ab2 1446 self.PrintResultSummary(board_selected, board_dict,
ed966657 1447 err_lines if self._show_errors else [], err_line_boards,
e30965db 1448 warn_lines if self._show_errors else [], warn_line_boards,
48ae4124
AK
1449 config, environment, self._show_sizes, self._show_detail,
1450 self._show_bloat, self._show_config, self._show_environment)
fc3fe1c2 1451
b2ea7ab2 1452 def ShowSummary(self, commits, board_selected):
fc3fe1c2
SG
1453 """Show a build summary for U-Boot for a given board list.
1454
1455 Reset the result summary, then repeatedly call GetResultSummary on
1456 each commit's results, then display the differences we see.
1457
1458 Args:
1459 commit: Commit objects to summarise
1460 board_selected: Dict containing boards to summarise
fc3fe1c2 1461 """
fea5858e 1462 self.commit_count = len(commits) if commits else 1
fc3fe1c2
SG
1463 self.commits = commits
1464 self.ResetResultSummary(board_selected)
28370c1b 1465 self._error_lines = 0
fc3fe1c2
SG
1466
1467 for commit_upto in range(0, self.commit_count, self._step):
b2ea7ab2 1468 self.ProduceResultSummary(commit_upto, commits, board_selected)
28370c1b 1469 if not self._error_lines:
4653a882 1470 Print('(no errors to report)', colour=self.col.GREEN)
fc3fe1c2
SG
1471
1472
1473 def SetupBuild(self, board_selected, commits):
1474 """Set up ready to start a build.
1475
1476 Args:
1477 board_selected: Selected boards to build
1478 commits: Selected commits to build
1479 """
1480 # First work out how many commits we will build
c05aa036 1481 count = (self.commit_count + self._step - 1) // self._step
fc3fe1c2
SG
1482 self.count = len(board_selected) * count
1483 self.upto = self.warned = self.fail = 0
1484 self._timestamps = collections.deque()
1485
fc3fe1c2
SG
1486 def GetThreadDir(self, thread_num):
1487 """Get the directory path to the working dir for a thread.
1488
1489 Args:
1490 thread_num: Number of thread to check.
1491 """
d829f121
SG
1492 if self.work_in_output:
1493 return self._working_dir
fc3fe1c2
SG
1494 return os.path.join(self._working_dir, '%02d' % thread_num)
1495
fea5858e 1496 def _PrepareThread(self, thread_num, setup_git):
fc3fe1c2
SG
1497 """Prepare the working directory for a thread.
1498
1499 This clones or fetches the repo into the thread's work directory.
1500
1501 Args:
1502 thread_num: Thread number (0, 1, ...)
fea5858e 1503 setup_git: True to set up a git repo clone
fc3fe1c2
SG
1504 """
1505 thread_dir = self.GetThreadDir(thread_num)
190064b4 1506 builderthread.Mkdir(thread_dir)
fc3fe1c2
SG
1507 git_dir = os.path.join(thread_dir, '.git')
1508
1509 # Clone the repo if it doesn't already exist
1510 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1511 # we have a private index but uses the origin repo's contents?
fea5858e 1512 if setup_git and self.git_dir:
fc3fe1c2
SG
1513 src_dir = os.path.abspath(self.git_dir)
1514 if os.path.exists(git_dir):
1515 gitutil.Fetch(git_dir, thread_dir)
1516 else:
21f0eb33
SG
1517 Print('\rCloning repo for thread %d' % thread_num,
1518 newline=False)
fc3fe1c2 1519 gitutil.Clone(src_dir, thread_dir)
21f0eb33 1520 Print('\r%s\r' % (' ' * 30), newline=False)
fc3fe1c2 1521
fea5858e 1522 def _PrepareWorkingSpace(self, max_threads, setup_git):
fc3fe1c2
SG
1523 """Prepare the working directory for use.
1524
1525 Set up the git repo for each thread.
1526
1527 Args:
1528 max_threads: Maximum number of threads we expect to need.
fea5858e 1529 setup_git: True to set up a git repo clone
fc3fe1c2 1530 """
190064b4 1531 builderthread.Mkdir(self._working_dir)
fc3fe1c2 1532 for thread in range(max_threads):
fea5858e 1533 self._PrepareThread(thread, setup_git)
fc3fe1c2 1534
925f6adf 1535 def _GetOutputSpaceRemovals(self):
fc3fe1c2
SG
1536 """Get the output directories ready to receive files.
1537
925f6adf
SG
1538 Figure out what needs to be deleted in the output directory before it
1539 can be used. We only delete old buildman directories which have the
1540 expected name pattern. See _GetOutputDir().
1541
1542 Returns:
1543 List of full paths of directories to remove
fc3fe1c2 1544 """
1a915675
SG
1545 if not self.commits:
1546 return
fc3fe1c2
SG
1547 dir_list = []
1548 for commit_upto in range(self.commit_count):
1549 dir_list.append(self._GetOutputDir(commit_upto))
1550
b222abe7 1551 to_remove = []
fc3fe1c2
SG
1552 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1553 if dirname not in dir_list:
925f6adf
SG
1554 leaf = dirname[len(self.base_dir) + 1:]
1555 m = re.match('[0-9]+_of_[0-9]+_g[0-9a-f]+_.*', leaf)
1556 if m:
1557 to_remove.append(dirname)
1558 return to_remove
1559
1560 def _PrepareOutputSpace(self):
1561 """Get the output directories ready to receive files.
1562
1563 We delete any output directories which look like ones we need to
1564 create. Having left over directories is confusing when the user wants
1565 to check the output manually.
1566 """
1567 to_remove = self._GetOutputSpaceRemovals()
b222abe7 1568 if to_remove:
b2d89bc5 1569 Print('Removing %d old build directories...' % len(to_remove),
b222abe7
SG
1570 newline=False)
1571 for dirname in to_remove:
fc3fe1c2 1572 shutil.rmtree(dirname)
b2d89bc5 1573 Print('done')
fc3fe1c2 1574
e5a0e5d8 1575 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
fc3fe1c2
SG
1576 """Build all commits for a list of boards
1577
1578 Args:
1579 commits: List of commits to be build, each a Commit object
1580 boards_selected: Dict of selected boards, key is target name,
1581 value is Board object
fc3fe1c2 1582 keep_outputs: True to save build output files
e5a0e5d8 1583 verbose: Display build results as they are completed
2c3deb97
SG
1584 Returns:
1585 Tuple containing:
1586 - number of boards that failed to build
1587 - number of boards that issued warnings
fc3fe1c2 1588 """
fea5858e 1589 self.commit_count = len(commits) if commits else 1
fc3fe1c2 1590 self.commits = commits
e5a0e5d8 1591 self._verbose = verbose
fc3fe1c2
SG
1592
1593 self.ResetResultSummary(board_selected)
f3d015cb 1594 builderthread.Mkdir(self.base_dir, parents = True)
fea5858e
SG
1595 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1596 commits is not None)
fc3fe1c2 1597 self._PrepareOutputSpace()
745b395a 1598 Print('\rStarting build...', newline=False)
fc3fe1c2
SG
1599 self.SetupBuild(board_selected, commits)
1600 self.ProcessResult(None)
1601
1602 # Create jobs to build all commits for each board
c05aa036 1603 for brd in board_selected.values():
190064b4 1604 job = builderthread.BuilderJob()
fc3fe1c2
SG
1605 job.board = brd
1606 job.commits = commits
1607 job.keep_outputs = keep_outputs
d829f121 1608 job.work_in_output = self.work_in_output
fc3fe1c2
SG
1609 job.step = self._step
1610 self.queue.put(job)
1611
d436e381
SG
1612 term = threading.Thread(target=self.queue.join)
1613 term.setDaemon(True)
1614 term.start()
1615 while term.isAlive():
1616 term.join(100)
fc3fe1c2
SG
1617
1618 # Wait until we have processed all output
1619 self.out_queue.join()
4653a882 1620 Print()
fc3fe1c2 1621 self.ClearLine(0)
2c3deb97 1622 return (self.fail, self.warned)
This page took 0.587622 seconds and 4 git commands to generate.