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