builder.py 53 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340
  1. # Copyright (c) 2013 The Chromium OS Authors.
  2. #
  3. # Bloat-o-meter code used here Copyright 2004 Matt Mackall <mpm@selenic.com>
  4. #
  5. # SPDX-License-Identifier: GPL-2.0+
  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 string
  15. import sys
  16. import time
  17. import builderthread
  18. import command
  19. import gitutil
  20. import terminal
  21. from terminal import Print
  22. import toolchain
  23. """
  24. Theory of Operation
  25. Please see README for user documentation, and you should be familiar with
  26. that before trying to make sense of this.
  27. Buildman works by keeping the machine as busy as possible, building different
  28. commits for different boards on multiple CPUs at once.
  29. The source repo (self.git_dir) contains all the commits to be built. Each
  30. thread works on a single board at a time. It checks out the first commit,
  31. configures it for that board, then builds it. Then it checks out the next
  32. commit and builds it (typically without re-configuring). When it runs out
  33. of commits, it gets another job from the builder and starts again with that
  34. board.
  35. Clearly the builder threads could work either way - they could check out a
  36. commit and then built it for all boards. Using separate directories for each
  37. commit/board pair they could leave their build product around afterwards
  38. also.
  39. The intent behind building a single board for multiple commits, is to make
  40. use of incremental builds. Since each commit is built incrementally from
  41. the previous one, builds are faster. Reconfiguring for a different board
  42. removes all intermediate object files.
  43. Many threads can be working at once, but each has its own working directory.
  44. When a thread finishes a build, it puts the output files into a result
  45. directory.
  46. The base directory used by buildman is normally '../<branch>', i.e.
  47. a directory higher than the source repository and named after the branch
  48. being built.
  49. Within the base directory, we have one subdirectory for each commit. Within
  50. that is one subdirectory for each board. Within that is the build output for
  51. that commit/board combination.
  52. Buildman also create working directories for each thread, in a .bm-work/
  53. subdirectory in the base dir.
  54. As an example, say we are building branch 'us-net' for boards 'sandbox' and
  55. 'seaboard', and say that us-net has two commits. We will have directories
  56. like this:
  57. us-net/ base directory
  58. 01_of_02_g4ed4ebc_net--Add-tftp-speed-/
  59. sandbox/
  60. u-boot.bin
  61. seaboard/
  62. u-boot.bin
  63. 02_of_02_g4ed4ebc_net--Check-tftp-comp/
  64. sandbox/
  65. u-boot.bin
  66. seaboard/
  67. u-boot.bin
  68. .bm-work/
  69. 00/ working directory for thread 0 (contains source checkout)
  70. build/ build output
  71. 01/ working directory for thread 1
  72. build/ build output
  73. ...
  74. u-boot/ source directory
  75. .git/ repository
  76. """
  77. # Possible build outcomes
  78. OUTCOME_OK, OUTCOME_WARNING, OUTCOME_ERROR, OUTCOME_UNKNOWN = range(4)
  79. # Translate a commit subject into a valid filename
  80. trans_valid_chars = string.maketrans("/: ", "---")
  81. CONFIG_FILENAMES = [
  82. '.config', '.config-spl', '.config-tpl',
  83. 'autoconf.mk', 'autoconf-spl.mk', 'autoconf-tpl.mk',
  84. 'autoconf.h', 'autoconf-spl.h','autoconf-tpl.h',
  85. 'u-boot.cfg', 'u-boot-spl.cfg', 'u-boot-tpl.cfg'
  86. ]
  87. class Builder:
  88. """Class for building U-Boot for a particular commit.
  89. Public members: (many should ->private)
  90. active: True if the builder is active and has not been stopped
  91. already_done: Number of builds already completed
  92. base_dir: Base directory to use for builder
  93. checkout: True to check out source, False to skip that step.
  94. This is used for testing.
  95. col: terminal.Color() object
  96. count: Number of commits to build
  97. do_make: Method to call to invoke Make
  98. fail: Number of builds that failed due to error
  99. force_build: Force building even if a build already exists
  100. force_config_on_failure: If a commit fails for a board, disable
  101. incremental building for the next commit we build for that
  102. board, so that we will see all warnings/errors again.
  103. force_build_failures: If a previously-built build (i.e. built on
  104. a previous run of buildman) is marked as failed, rebuild it.
  105. git_dir: Git directory containing source repository
  106. last_line_len: Length of the last line we printed (used for erasing
  107. it with new progress information)
  108. num_jobs: Number of jobs to run at once (passed to make as -j)
  109. num_threads: Number of builder threads to run
  110. out_queue: Queue of results to process
  111. re_make_err: Compiled regular expression for ignore_lines
  112. queue: Queue of jobs to run
  113. threads: List of active threads
  114. toolchains: Toolchains object to use for building
  115. upto: Current commit number we are building (0.count-1)
  116. warned: Number of builds that produced at least one warning
  117. force_reconfig: Reconfigure U-Boot on each comiit. This disables
  118. incremental building, where buildman reconfigures on the first
  119. commit for a baord, and then just does an incremental build for
  120. the following commits. In fact buildman will reconfigure and
  121. retry for any failing commits, so generally the only effect of
  122. this option is to slow things down.
  123. in_tree: Build U-Boot in-tree instead of specifying an output
  124. directory separate from the source code. This option is really
  125. only useful for testing in-tree builds.
  126. Private members:
  127. _base_board_dict: Last-summarised Dict of boards
  128. _base_err_lines: Last-summarised list of errors
  129. _base_warn_lines: Last-summarised list of warnings
  130. _build_period_us: Time taken for a single build (float object).
  131. _complete_delay: Expected delay until completion (timedelta)
  132. _next_delay_update: Next time we plan to display a progress update
  133. (datatime)
  134. _show_unknown: Show unknown boards (those not built) in summary
  135. _timestamps: List of timestamps for the completion of the last
  136. last _timestamp_count builds. Each is a datetime object.
  137. _timestamp_count: Number of timestamps to keep in our list.
  138. _working_dir: Base working directory containing all threads
  139. """
  140. class Outcome:
  141. """Records a build outcome for a single make invocation
  142. Public Members:
  143. rc: Outcome value (OUTCOME_...)
  144. err_lines: List of error lines or [] if none
  145. sizes: Dictionary of image size information, keyed by filename
  146. - Each value is itself a dictionary containing
  147. values for 'text', 'data' and 'bss', being the integer
  148. size in bytes of each section.
  149. func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
  150. value is itself a dictionary:
  151. key: function name
  152. value: Size of function in bytes
  153. config: Dictionary keyed by filename - e.g. '.config'. Each
  154. value is itself a dictionary:
  155. key: config name
  156. value: config value
  157. """
  158. def __init__(self, rc, err_lines, sizes, func_sizes, config):
  159. self.rc = rc
  160. self.err_lines = err_lines
  161. self.sizes = sizes
  162. self.func_sizes = func_sizes
  163. self.config = config
  164. def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
  165. gnu_make='make', checkout=True, show_unknown=True, step=1,
  166. no_subdirs=False, full_path=False, verbose_build=False):
  167. """Create a new Builder object
  168. Args:
  169. toolchains: Toolchains object to use for building
  170. base_dir: Base directory to use for builder
  171. git_dir: Git directory containing source repository
  172. num_threads: Number of builder threads to run
  173. num_jobs: Number of jobs to run at once (passed to make as -j)
  174. gnu_make: the command name of GNU Make.
  175. checkout: True to check out source, False to skip that step.
  176. This is used for testing.
  177. show_unknown: Show unknown boards (those not built) in summary
  178. step: 1 to process every commit, n to process every nth commit
  179. no_subdirs: Don't create subdirectories when building current
  180. source for a single board
  181. full_path: Return the full path in CROSS_COMPILE and don't set
  182. PATH
  183. verbose_build: Run build with V=1 and don't use 'make -s'
  184. """
  185. self.toolchains = toolchains
  186. self.base_dir = base_dir
  187. self._working_dir = os.path.join(base_dir, '.bm-work')
  188. self.threads = []
  189. self.active = True
  190. self.do_make = self.Make
  191. self.gnu_make = gnu_make
  192. self.checkout = checkout
  193. self.num_threads = num_threads
  194. self.num_jobs = num_jobs
  195. self.already_done = 0
  196. self.force_build = False
  197. self.git_dir = git_dir
  198. self._show_unknown = show_unknown
  199. self._timestamp_count = 10
  200. self._build_period_us = None
  201. self._complete_delay = None
  202. self._next_delay_update = datetime.now()
  203. self.force_config_on_failure = True
  204. self.force_build_failures = False
  205. self.force_reconfig = False
  206. self._step = step
  207. self.in_tree = False
  208. self._error_lines = 0
  209. self.no_subdirs = no_subdirs
  210. self.full_path = full_path
  211. self.verbose_build = verbose_build
  212. self.col = terminal.Color()
  213. self._re_function = re.compile('(.*): In function.*')
  214. self._re_files = re.compile('In file included from.*')
  215. self._re_warning = re.compile('(.*):(\d*):(\d*): warning: .*')
  216. self._re_note = re.compile('(.*):(\d*):(\d*): note: this is the location of the previous.*')
  217. self.queue = Queue.Queue()
  218. self.out_queue = Queue.Queue()
  219. for i in range(self.num_threads):
  220. t = builderthread.BuilderThread(self, i)
  221. t.setDaemon(True)
  222. t.start()
  223. self.threads.append(t)
  224. self.last_line_len = 0
  225. t = builderthread.ResultThread(self)
  226. t.setDaemon(True)
  227. t.start()
  228. self.threads.append(t)
  229. ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
  230. self.re_make_err = re.compile('|'.join(ignore_lines))
  231. def __del__(self):
  232. """Get rid of all threads created by the builder"""
  233. for t in self.threads:
  234. del t
  235. def SetDisplayOptions(self, show_errors=False, show_sizes=False,
  236. show_detail=False, show_bloat=False,
  237. list_error_boards=False, show_config=False):
  238. """Setup display options for the builder.
  239. show_errors: True to show summarised error/warning info
  240. show_sizes: Show size deltas
  241. show_detail: Show detail for each board
  242. show_bloat: Show detail for each function
  243. list_error_boards: Show the boards which caused each error/warning
  244. show_config: Show config deltas
  245. """
  246. self._show_errors = show_errors
  247. self._show_sizes = show_sizes
  248. self._show_detail = show_detail
  249. self._show_bloat = show_bloat
  250. self._list_error_boards = list_error_boards
  251. self._show_config = show_config
  252. def _AddTimestamp(self):
  253. """Add a new timestamp to the list and record the build period.
  254. The build period is the length of time taken to perform a single
  255. build (one board, one commit).
  256. """
  257. now = datetime.now()
  258. self._timestamps.append(now)
  259. count = len(self._timestamps)
  260. delta = self._timestamps[-1] - self._timestamps[0]
  261. seconds = delta.total_seconds()
  262. # If we have enough data, estimate build period (time taken for a
  263. # single build) and therefore completion time.
  264. if count > 1 and self._next_delay_update < now:
  265. self._next_delay_update = now + timedelta(seconds=2)
  266. if seconds > 0:
  267. self._build_period = float(seconds) / count
  268. todo = self.count - self.upto
  269. self._complete_delay = timedelta(microseconds=
  270. self._build_period * todo * 1000000)
  271. # Round it
  272. self._complete_delay -= timedelta(
  273. microseconds=self._complete_delay.microseconds)
  274. if seconds > 60:
  275. self._timestamps.popleft()
  276. count -= 1
  277. def ClearLine(self, length):
  278. """Clear any characters on the current line
  279. Make way for a new line of length 'length', by outputting enough
  280. spaces to clear out the old line. Then remember the new length for
  281. next time.
  282. Args:
  283. length: Length of new line, in characters
  284. """
  285. if length < self.last_line_len:
  286. Print(' ' * (self.last_line_len - length), newline=False)
  287. Print('\r', newline=False)
  288. self.last_line_len = length
  289. sys.stdout.flush()
  290. def SelectCommit(self, commit, checkout=True):
  291. """Checkout the selected commit for this build
  292. """
  293. self.commit = commit
  294. if checkout and self.checkout:
  295. gitutil.Checkout(commit.hash)
  296. def Make(self, commit, brd, stage, cwd, *args, **kwargs):
  297. """Run make
  298. Args:
  299. commit: Commit object that is being built
  300. brd: Board object that is being built
  301. stage: Stage that we are at (mrproper, config, build)
  302. cwd: Directory where make should be run
  303. args: Arguments to pass to make
  304. kwargs: Arguments to pass to command.RunPipe()
  305. """
  306. cmd = [self.gnu_make] + list(args)
  307. result = command.RunPipe([cmd], capture=True, capture_stderr=True,
  308. cwd=cwd, raise_on_error=False, **kwargs)
  309. if self.verbose_build:
  310. result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout
  311. result.combined = '%s\n' % (' '.join(cmd)) + result.combined
  312. return result
  313. def ProcessResult(self, result):
  314. """Process the result of a build, showing progress information
  315. Args:
  316. result: A CommandResult object, which indicates the result for
  317. a single build
  318. """
  319. col = terminal.Color()
  320. if result:
  321. target = result.brd.target
  322. if result.return_code < 0:
  323. self.active = False
  324. command.StopAll()
  325. return
  326. self.upto += 1
  327. if result.return_code != 0:
  328. self.fail += 1
  329. elif result.stderr:
  330. self.warned += 1
  331. if result.already_done:
  332. self.already_done += 1
  333. if self._verbose:
  334. Print('\r', newline=False)
  335. self.ClearLine(0)
  336. boards_selected = {target : result.brd}
  337. self.ResetResultSummary(boards_selected)
  338. self.ProduceResultSummary(result.commit_upto, self.commits,
  339. boards_selected)
  340. else:
  341. target = '(starting)'
  342. # Display separate counts for ok, warned and fail
  343. ok = self.upto - self.warned - self.fail
  344. line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
  345. line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
  346. line += self.col.Color(self.col.RED, '%5d' % self.fail)
  347. name = ' /%-5d ' % self.count
  348. # Add our current completion time estimate
  349. self._AddTimestamp()
  350. if self._complete_delay:
  351. name += '%s : ' % self._complete_delay
  352. # When building all boards for a commit, we can print a commit
  353. # progress message.
  354. if result and result.commit_upto is None:
  355. name += 'commit %2d/%-3d' % (self.commit_upto + 1,
  356. self.commit_count)
  357. name += target
  358. Print(line + name, newline=False)
  359. length = 14 + len(name)
  360. self.ClearLine(length)
  361. def _GetOutputDir(self, commit_upto):
  362. """Get the name of the output directory for a commit number
  363. The output directory is typically .../<branch>/<commit>.
  364. Args:
  365. commit_upto: Commit number to use (0..self.count-1)
  366. """
  367. commit_dir = None
  368. if self.commits:
  369. commit = self.commits[commit_upto]
  370. subject = commit.subject.translate(trans_valid_chars)
  371. commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
  372. self.commit_count, commit.hash, subject[:20]))
  373. elif not self.no_subdirs:
  374. commit_dir = 'current'
  375. if not commit_dir:
  376. return self.base_dir
  377. return os.path.join(self.base_dir, commit_dir)
  378. def GetBuildDir(self, commit_upto, target):
  379. """Get the name of the build directory for a commit number
  380. The build directory is typically .../<branch>/<commit>/<target>.
  381. Args:
  382. commit_upto: Commit number to use (0..self.count-1)
  383. target: Target name
  384. """
  385. output_dir = self._GetOutputDir(commit_upto)
  386. return os.path.join(output_dir, target)
  387. def GetDoneFile(self, commit_upto, target):
  388. """Get the name of the done file for a commit number
  389. Args:
  390. commit_upto: Commit number to use (0..self.count-1)
  391. target: Target name
  392. """
  393. return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
  394. def GetSizesFile(self, commit_upto, target):
  395. """Get the name of the sizes file for a commit number
  396. Args:
  397. commit_upto: Commit number to use (0..self.count-1)
  398. target: Target name
  399. """
  400. return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
  401. def GetFuncSizesFile(self, commit_upto, target, elf_fname):
  402. """Get the name of the funcsizes file for a commit number and ELF file
  403. Args:
  404. commit_upto: Commit number to use (0..self.count-1)
  405. target: Target name
  406. elf_fname: Filename of elf image
  407. """
  408. return os.path.join(self.GetBuildDir(commit_upto, target),
  409. '%s.sizes' % elf_fname.replace('/', '-'))
  410. def GetObjdumpFile(self, commit_upto, target, elf_fname):
  411. """Get the name of the objdump file for a commit number and ELF file
  412. Args:
  413. commit_upto: Commit number to use (0..self.count-1)
  414. target: Target name
  415. elf_fname: Filename of elf image
  416. """
  417. return os.path.join(self.GetBuildDir(commit_upto, target),
  418. '%s.objdump' % elf_fname.replace('/', '-'))
  419. def GetErrFile(self, commit_upto, target):
  420. """Get the name of the err file for a commit number
  421. Args:
  422. commit_upto: Commit number to use (0..self.count-1)
  423. target: Target name
  424. """
  425. output_dir = self.GetBuildDir(commit_upto, target)
  426. return os.path.join(output_dir, 'err')
  427. def FilterErrors(self, lines):
  428. """Filter out errors in which we have no interest
  429. We should probably use map().
  430. Args:
  431. lines: List of error lines, each a string
  432. Returns:
  433. New list with only interesting lines included
  434. """
  435. out_lines = []
  436. for line in lines:
  437. if not self.re_make_err.search(line):
  438. out_lines.append(line)
  439. return out_lines
  440. def ReadFuncSizes(self, fname, fd):
  441. """Read function sizes from the output of 'nm'
  442. Args:
  443. fd: File containing data to read
  444. fname: Filename we are reading from (just for errors)
  445. Returns:
  446. Dictionary containing size of each function in bytes, indexed by
  447. function name.
  448. """
  449. sym = {}
  450. for line in fd.readlines():
  451. try:
  452. size, type, name = line[:-1].split()
  453. except:
  454. Print("Invalid line in file '%s': '%s'" % (fname, line[:-1]))
  455. continue
  456. if type in 'tTdDbB':
  457. # function names begin with '.' on 64-bit powerpc
  458. if '.' in name[1:]:
  459. name = 'static.' + name.split('.')[0]
  460. sym[name] = sym.get(name, 0) + int(size, 16)
  461. return sym
  462. def _ProcessConfig(self, fname):
  463. """Read in a .config, autoconf.mk or autoconf.h file
  464. This function handles all config file types. It ignores comments and
  465. any #defines which don't start with CONFIG_.
  466. Args:
  467. fname: Filename to read
  468. Returns:
  469. Dictionary:
  470. key: Config name (e.g. CONFIG_DM)
  471. value: Config value (e.g. 1)
  472. """
  473. config = {}
  474. if os.path.exists(fname):
  475. with open(fname) as fd:
  476. for line in fd:
  477. line = line.strip()
  478. if line.startswith('#define'):
  479. values = line[8:].split(' ', 1)
  480. if len(values) > 1:
  481. key, value = values
  482. else:
  483. key = values[0]
  484. value = ''
  485. if not key.startswith('CONFIG_'):
  486. continue
  487. elif not line or line[0] in ['#', '*', '/']:
  488. continue
  489. else:
  490. key, value = line.split('=', 1)
  491. config[key] = value
  492. return config
  493. def GetBuildOutcome(self, commit_upto, target, read_func_sizes,
  494. read_config):
  495. """Work out the outcome of a build.
  496. Args:
  497. commit_upto: Commit number to check (0..n-1)
  498. target: Target board to check
  499. read_func_sizes: True to read function size information
  500. read_config: True to read .config and autoconf.h files
  501. Returns:
  502. Outcome object
  503. """
  504. done_file = self.GetDoneFile(commit_upto, target)
  505. sizes_file = self.GetSizesFile(commit_upto, target)
  506. sizes = {}
  507. func_sizes = {}
  508. config = {}
  509. if os.path.exists(done_file):
  510. with open(done_file, 'r') as fd:
  511. return_code = int(fd.readline())
  512. err_lines = []
  513. err_file = self.GetErrFile(commit_upto, target)
  514. if os.path.exists(err_file):
  515. with open(err_file, 'r') as fd:
  516. err_lines = self.FilterErrors(fd.readlines())
  517. # Decide whether the build was ok, failed or created warnings
  518. if return_code:
  519. rc = OUTCOME_ERROR
  520. elif len(err_lines):
  521. rc = OUTCOME_WARNING
  522. else:
  523. rc = OUTCOME_OK
  524. # Convert size information to our simple format
  525. if os.path.exists(sizes_file):
  526. with open(sizes_file, 'r') as fd:
  527. for line in fd.readlines():
  528. values = line.split()
  529. rodata = 0
  530. if len(values) > 6:
  531. rodata = int(values[6], 16)
  532. size_dict = {
  533. 'all' : int(values[0]) + int(values[1]) +
  534. int(values[2]),
  535. 'text' : int(values[0]) - rodata,
  536. 'data' : int(values[1]),
  537. 'bss' : int(values[2]),
  538. 'rodata' : rodata,
  539. }
  540. sizes[values[5]] = size_dict
  541. if read_func_sizes:
  542. pattern = self.GetFuncSizesFile(commit_upto, target, '*')
  543. for fname in glob.glob(pattern):
  544. with open(fname, 'r') as fd:
  545. dict_name = os.path.basename(fname).replace('.sizes',
  546. '')
  547. func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
  548. if read_config:
  549. output_dir = self.GetBuildDir(commit_upto, target)
  550. for name in CONFIG_FILENAMES:
  551. fname = os.path.join(output_dir, name)
  552. config[name] = self._ProcessConfig(fname)
  553. return Builder.Outcome(rc, err_lines, sizes, func_sizes, config)
  554. return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {})
  555. def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
  556. read_config):
  557. """Calculate a summary of the results of building a commit.
  558. Args:
  559. board_selected: Dict containing boards to summarise
  560. commit_upto: Commit number to summarize (0..self.count-1)
  561. read_func_sizes: True to read function size information
  562. read_config: True to read .config and autoconf.h files
  563. Returns:
  564. Tuple:
  565. Dict containing boards which passed building this commit.
  566. keyed by board.target
  567. List containing a summary of error lines
  568. Dict keyed by error line, containing a list of the Board
  569. objects with that error
  570. List containing a summary of warning lines
  571. Dict keyed by error line, containing a list of the Board
  572. objects with that warning
  573. Dictionary keyed by filename - e.g. '.config'. Each
  574. value is itself a dictionary:
  575. key: config name
  576. value: config value
  577. """
  578. def AddLine(lines_summary, lines_boards, line, board):
  579. line = line.rstrip()
  580. if line in lines_boards:
  581. lines_boards[line].append(board)
  582. else:
  583. lines_boards[line] = [board]
  584. lines_summary.append(line)
  585. board_dict = {}
  586. err_lines_summary = []
  587. err_lines_boards = {}
  588. warn_lines_summary = []
  589. warn_lines_boards = {}
  590. config = {}
  591. for fname in CONFIG_FILENAMES:
  592. config[fname] = {}
  593. for board in boards_selected.itervalues():
  594. outcome = self.GetBuildOutcome(commit_upto, board.target,
  595. read_func_sizes, read_config)
  596. board_dict[board.target] = outcome
  597. last_func = None
  598. last_was_warning = False
  599. for line in outcome.err_lines:
  600. if line:
  601. if (self._re_function.match(line) or
  602. self._re_files.match(line)):
  603. last_func = line
  604. else:
  605. is_warning = self._re_warning.match(line)
  606. is_note = self._re_note.match(line)
  607. if is_warning or (last_was_warning and is_note):
  608. if last_func:
  609. AddLine(warn_lines_summary, warn_lines_boards,
  610. last_func, board)
  611. AddLine(warn_lines_summary, warn_lines_boards,
  612. line, board)
  613. else:
  614. if last_func:
  615. AddLine(err_lines_summary, err_lines_boards,
  616. last_func, board)
  617. AddLine(err_lines_summary, err_lines_boards,
  618. line, board)
  619. last_was_warning = is_warning
  620. last_func = None
  621. for fname in CONFIG_FILENAMES:
  622. config[fname] = {}
  623. if outcome.config:
  624. for key, value in outcome.config[fname].iteritems():
  625. config[fname][key] = value
  626. return (board_dict, err_lines_summary, err_lines_boards,
  627. warn_lines_summary, warn_lines_boards, config)
  628. def AddOutcome(self, board_dict, arch_list, changes, char, color):
  629. """Add an output to our list of outcomes for each architecture
  630. This simple function adds failing boards (changes) to the
  631. relevant architecture string, so we can print the results out
  632. sorted by architecture.
  633. Args:
  634. board_dict: Dict containing all boards
  635. arch_list: Dict keyed by arch name. Value is a string containing
  636. a list of board names which failed for that arch.
  637. changes: List of boards to add to arch_list
  638. color: terminal.Colour object
  639. """
  640. done_arch = {}
  641. for target in changes:
  642. if target in board_dict:
  643. arch = board_dict[target].arch
  644. else:
  645. arch = 'unknown'
  646. str = self.col.Color(color, ' ' + target)
  647. if not arch in done_arch:
  648. str = ' %s %s' % (self.col.Color(color, char), str)
  649. done_arch[arch] = True
  650. if not arch in arch_list:
  651. arch_list[arch] = str
  652. else:
  653. arch_list[arch] += str
  654. def ColourNum(self, num):
  655. color = self.col.RED if num > 0 else self.col.GREEN
  656. if num == 0:
  657. return '0'
  658. return self.col.Color(color, str(num))
  659. def ResetResultSummary(self, board_selected):
  660. """Reset the results summary ready for use.
  661. Set up the base board list to be all those selected, and set the
  662. error lines to empty.
  663. Following this, calls to PrintResultSummary() will use this
  664. information to work out what has changed.
  665. Args:
  666. board_selected: Dict containing boards to summarise, keyed by
  667. board.target
  668. """
  669. self._base_board_dict = {}
  670. for board in board_selected:
  671. self._base_board_dict[board] = Builder.Outcome(0, [], [], {}, {})
  672. self._base_err_lines = []
  673. self._base_warn_lines = []
  674. self._base_err_line_boards = {}
  675. self._base_warn_line_boards = {}
  676. self._base_config = {}
  677. for fname in CONFIG_FILENAMES:
  678. self._base_config[fname] = {}
  679. def PrintFuncSizeDetail(self, fname, old, new):
  680. grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
  681. delta, common = [], {}
  682. for a in old:
  683. if a in new:
  684. common[a] = 1
  685. for name in old:
  686. if name not in common:
  687. remove += 1
  688. down += old[name]
  689. delta.append([-old[name], name])
  690. for name in new:
  691. if name not in common:
  692. add += 1
  693. up += new[name]
  694. delta.append([new[name], name])
  695. for name in common:
  696. diff = new.get(name, 0) - old.get(name, 0)
  697. if diff > 0:
  698. grow, up = grow + 1, up + diff
  699. elif diff < 0:
  700. shrink, down = shrink + 1, down - diff
  701. delta.append([diff, name])
  702. delta.sort()
  703. delta.reverse()
  704. args = [add, -remove, grow, -shrink, up, -down, up - down]
  705. if max(args) == 0:
  706. return
  707. args = [self.ColourNum(x) for x in args]
  708. indent = ' ' * 15
  709. Print('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
  710. tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
  711. Print('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
  712. 'delta'))
  713. for diff, name in delta:
  714. if diff:
  715. color = self.col.RED if diff > 0 else self.col.GREEN
  716. msg = '%s %-38s %7s %7s %+7d' % (indent, name,
  717. old.get(name, '-'), new.get(name,'-'), diff)
  718. Print(msg, colour=color)
  719. def PrintSizeDetail(self, target_list, show_bloat):
  720. """Show details size information for each board
  721. Args:
  722. target_list: List of targets, each a dict containing:
  723. 'target': Target name
  724. 'total_diff': Total difference in bytes across all areas
  725. <part_name>: Difference for that part
  726. show_bloat: Show detail for each function
  727. """
  728. targets_by_diff = sorted(target_list, reverse=True,
  729. key=lambda x: x['_total_diff'])
  730. for result in targets_by_diff:
  731. printed_target = False
  732. for name in sorted(result):
  733. diff = result[name]
  734. if name.startswith('_'):
  735. continue
  736. if diff != 0:
  737. color = self.col.RED if diff > 0 else self.col.GREEN
  738. msg = ' %s %+d' % (name, diff)
  739. if not printed_target:
  740. Print('%10s %-15s:' % ('', result['_target']),
  741. newline=False)
  742. printed_target = True
  743. Print(msg, colour=color, newline=False)
  744. if printed_target:
  745. Print()
  746. if show_bloat:
  747. target = result['_target']
  748. outcome = result['_outcome']
  749. base_outcome = self._base_board_dict[target]
  750. for fname in outcome.func_sizes:
  751. self.PrintFuncSizeDetail(fname,
  752. base_outcome.func_sizes[fname],
  753. outcome.func_sizes[fname])
  754. def PrintSizeSummary(self, board_selected, board_dict, show_detail,
  755. show_bloat):
  756. """Print a summary of image sizes broken down by section.
  757. The summary takes the form of one line per architecture. The
  758. line contains deltas for each of the sections (+ means the section
  759. got bigger, - means smaller). The nunmbers are the average number
  760. of bytes that a board in this section increased by.
  761. For example:
  762. powerpc: (622 boards) text -0.0
  763. arm: (285 boards) text -0.0
  764. nds32: (3 boards) text -8.0
  765. Args:
  766. board_selected: Dict containing boards to summarise, keyed by
  767. board.target
  768. board_dict: Dict containing boards for which we built this
  769. commit, keyed by board.target. The value is an Outcome object.
  770. show_detail: Show detail for each board
  771. show_bloat: Show detail for each function
  772. """
  773. arch_list = {}
  774. arch_count = {}
  775. # Calculate changes in size for different image parts
  776. # The previous sizes are in Board.sizes, for each board
  777. for target in board_dict:
  778. if target not in board_selected:
  779. continue
  780. base_sizes = self._base_board_dict[target].sizes
  781. outcome = board_dict[target]
  782. sizes = outcome.sizes
  783. # Loop through the list of images, creating a dict of size
  784. # changes for each image/part. We end up with something like
  785. # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
  786. # which means that U-Boot data increased by 5 bytes and SPL
  787. # text decreased by 4.
  788. err = {'_target' : target}
  789. for image in sizes:
  790. if image in base_sizes:
  791. base_image = base_sizes[image]
  792. # Loop through the text, data, bss parts
  793. for part in sorted(sizes[image]):
  794. diff = sizes[image][part] - base_image[part]
  795. col = None
  796. if diff:
  797. if image == 'u-boot':
  798. name = part
  799. else:
  800. name = image + ':' + part
  801. err[name] = diff
  802. arch = board_selected[target].arch
  803. if not arch in arch_count:
  804. arch_count[arch] = 1
  805. else:
  806. arch_count[arch] += 1
  807. if not sizes:
  808. pass # Only add to our list when we have some stats
  809. elif not arch in arch_list:
  810. arch_list[arch] = [err]
  811. else:
  812. arch_list[arch].append(err)
  813. # We now have a list of image size changes sorted by arch
  814. # Print out a summary of these
  815. for arch, target_list in arch_list.iteritems():
  816. # Get total difference for each type
  817. totals = {}
  818. for result in target_list:
  819. total = 0
  820. for name, diff in result.iteritems():
  821. if name.startswith('_'):
  822. continue
  823. total += diff
  824. if name in totals:
  825. totals[name] += diff
  826. else:
  827. totals[name] = diff
  828. result['_total_diff'] = total
  829. result['_outcome'] = board_dict[result['_target']]
  830. count = len(target_list)
  831. printed_arch = False
  832. for name in sorted(totals):
  833. diff = totals[name]
  834. if diff:
  835. # Display the average difference in this name for this
  836. # architecture
  837. avg_diff = float(diff) / count
  838. color = self.col.RED if avg_diff > 0 else self.col.GREEN
  839. msg = ' %s %+1.1f' % (name, avg_diff)
  840. if not printed_arch:
  841. Print('%10s: (for %d/%d boards)' % (arch, count,
  842. arch_count[arch]), newline=False)
  843. printed_arch = True
  844. Print(msg, colour=color, newline=False)
  845. if printed_arch:
  846. Print()
  847. if show_detail:
  848. self.PrintSizeDetail(target_list, show_bloat)
  849. def PrintResultSummary(self, board_selected, board_dict, err_lines,
  850. err_line_boards, warn_lines, warn_line_boards,
  851. config, show_sizes, show_detail, show_bloat,
  852. show_config):
  853. """Compare results with the base results and display delta.
  854. Only boards mentioned in board_selected will be considered. This
  855. function is intended to be called repeatedly with the results of
  856. each commit. It therefore shows a 'diff' between what it saw in
  857. the last call and what it sees now.
  858. Args:
  859. board_selected: Dict containing boards to summarise, keyed by
  860. board.target
  861. board_dict: Dict containing boards for which we built this
  862. commit, keyed by board.target. The value is an Outcome object.
  863. err_lines: A list of errors for this commit, or [] if there is
  864. none, or we don't want to print errors
  865. err_line_boards: Dict keyed by error line, containing a list of
  866. the Board objects with that error
  867. warn_lines: A list of warnings for this commit, or [] if there is
  868. none, or we don't want to print errors
  869. warn_line_boards: Dict keyed by warning line, containing a list of
  870. the Board objects with that warning
  871. config: Dictionary keyed by filename - e.g. '.config'. Each
  872. value is itself a dictionary:
  873. key: config name
  874. value: config value
  875. show_sizes: Show image size deltas
  876. show_detail: Show detail for each board
  877. show_bloat: Show detail for each function
  878. show_config: Show config changes
  879. """
  880. def _BoardList(line, line_boards):
  881. """Helper function to get a line of boards containing a line
  882. Args:
  883. line: Error line to search for
  884. Return:
  885. String containing a list of boards with that error line, or
  886. '' if the user has not requested such a list
  887. """
  888. if self._list_error_boards:
  889. names = []
  890. for board in line_boards[line]:
  891. if not board.target in names:
  892. names.append(board.target)
  893. names_str = '(%s) ' % ','.join(names)
  894. else:
  895. names_str = ''
  896. return names_str
  897. def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
  898. char):
  899. better_lines = []
  900. worse_lines = []
  901. for line in lines:
  902. if line not in base_lines:
  903. worse_lines.append(char + '+' +
  904. _BoardList(line, line_boards) + line)
  905. for line in base_lines:
  906. if line not in lines:
  907. better_lines.append(char + '-' +
  908. _BoardList(line, base_line_boards) + line)
  909. return better_lines, worse_lines
  910. def _CalcConfig(delta, name, config):
  911. """Calculate configuration changes
  912. Args:
  913. delta: Type of the delta, e.g. '+'
  914. name: name of the file which changed (e.g. .config)
  915. config: configuration change dictionary
  916. key: config name
  917. value: config value
  918. Returns:
  919. String containing the configuration changes which can be
  920. printed
  921. """
  922. out = ''
  923. for key in sorted(config.keys()):
  924. out += '%s=%s ' % (key, config[key])
  925. return '%5s %s: %s' % (delta, name, out)
  926. def _ShowConfig(name, config_plus, config_minus, config_change):
  927. """Show changes in configuration
  928. Args:
  929. config_plus: configurations added, dictionary
  930. key: config name
  931. value: config value
  932. config_minus: configurations removed, dictionary
  933. key: config name
  934. value: config value
  935. config_change: configurations changed, dictionary
  936. key: config name
  937. value: config value
  938. """
  939. if config_plus:
  940. Print(_CalcConfig('+', name, config_plus),
  941. colour=self.col.GREEN)
  942. if config_minus:
  943. Print(_CalcConfig('-', name, config_minus),
  944. colour=self.col.RED)
  945. if config_change:
  946. Print(_CalcConfig('+/-', name, config_change),
  947. colour=self.col.YELLOW)
  948. better = [] # List of boards fixed since last commit
  949. worse = [] # List of new broken boards since last commit
  950. new = [] # List of boards that didn't exist last time
  951. unknown = [] # List of boards that were not built
  952. for target in board_dict:
  953. if target not in board_selected:
  954. continue
  955. # If the board was built last time, add its outcome to a list
  956. if target in self._base_board_dict:
  957. base_outcome = self._base_board_dict[target].rc
  958. outcome = board_dict[target]
  959. if outcome.rc == OUTCOME_UNKNOWN:
  960. unknown.append(target)
  961. elif outcome.rc < base_outcome:
  962. better.append(target)
  963. elif outcome.rc > base_outcome:
  964. worse.append(target)
  965. else:
  966. new.append(target)
  967. # Get a list of errors that have appeared, and disappeared
  968. better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
  969. self._base_err_line_boards, err_lines, err_line_boards, '')
  970. better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
  971. self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
  972. # Display results by arch
  973. if (better or worse or unknown or new or worse_err or better_err
  974. or worse_warn or better_warn):
  975. arch_list = {}
  976. self.AddOutcome(board_selected, arch_list, better, '',
  977. self.col.GREEN)
  978. self.AddOutcome(board_selected, arch_list, worse, '+',
  979. self.col.RED)
  980. self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE)
  981. if self._show_unknown:
  982. self.AddOutcome(board_selected, arch_list, unknown, '?',
  983. self.col.MAGENTA)
  984. for arch, target_list in arch_list.iteritems():
  985. Print('%10s: %s' % (arch, target_list))
  986. self._error_lines += 1
  987. if better_err:
  988. Print('\n'.join(better_err), colour=self.col.GREEN)
  989. self._error_lines += 1
  990. if worse_err:
  991. Print('\n'.join(worse_err), colour=self.col.RED)
  992. self._error_lines += 1
  993. if better_warn:
  994. Print('\n'.join(better_warn), colour=self.col.CYAN)
  995. self._error_lines += 1
  996. if worse_warn:
  997. Print('\n'.join(worse_warn), colour=self.col.MAGENTA)
  998. self._error_lines += 1
  999. if show_sizes:
  1000. self.PrintSizeSummary(board_selected, board_dict, show_detail,
  1001. show_bloat)
  1002. if show_config:
  1003. all_config_plus = {}
  1004. all_config_minus = {}
  1005. all_config_change = {}
  1006. for name in CONFIG_FILENAMES:
  1007. if not config[name]:
  1008. continue
  1009. config_plus = {}
  1010. config_minus = {}
  1011. config_change = {}
  1012. base = self._base_config[name]
  1013. for key, value in config[name].iteritems():
  1014. if key not in base:
  1015. config_plus[key] = value
  1016. all_config_plus[key] = value
  1017. for key, value in base.iteritems():
  1018. if key not in config[name]:
  1019. config_minus[key] = value
  1020. all_config_minus[key] = value
  1021. for key, value in base.iteritems():
  1022. new_value = base[key]
  1023. if key in config[name] and value != new_value:
  1024. desc = '%s -> %s' % (value, new_value)
  1025. config_change[key] = desc
  1026. all_config_change[key] = desc
  1027. _ShowConfig(name, config_plus, config_minus, config_change)
  1028. _ShowConfig('all', all_config_plus, all_config_minus,
  1029. all_config_change)
  1030. # Save our updated information for the next call to this function
  1031. self._base_board_dict = board_dict
  1032. self._base_err_lines = err_lines
  1033. self._base_warn_lines = warn_lines
  1034. self._base_err_line_boards = err_line_boards
  1035. self._base_warn_line_boards = warn_line_boards
  1036. self._base_config = config
  1037. # Get a list of boards that did not get built, if needed
  1038. not_built = []
  1039. for board in board_selected:
  1040. if not board in board_dict:
  1041. not_built.append(board)
  1042. if not_built:
  1043. Print("Boards not built (%d): %s" % (len(not_built),
  1044. ', '.join(not_built)))
  1045. def ProduceResultSummary(self, commit_upto, commits, board_selected):
  1046. (board_dict, err_lines, err_line_boards, warn_lines,
  1047. warn_line_boards, config) = self.GetResultSummary(
  1048. board_selected, commit_upto,
  1049. read_func_sizes=self._show_bloat,
  1050. read_config=self._show_config)
  1051. if commits:
  1052. msg = '%02d: %s' % (commit_upto + 1,
  1053. commits[commit_upto].subject)
  1054. Print(msg, colour=self.col.BLUE)
  1055. self.PrintResultSummary(board_selected, board_dict,
  1056. err_lines if self._show_errors else [], err_line_boards,
  1057. warn_lines if self._show_errors else [], warn_line_boards,
  1058. config, self._show_sizes, self._show_detail,
  1059. self._show_bloat, self._show_config)
  1060. def ShowSummary(self, commits, board_selected):
  1061. """Show a build summary for U-Boot for a given board list.
  1062. Reset the result summary, then repeatedly call GetResultSummary on
  1063. each commit's results, then display the differences we see.
  1064. Args:
  1065. commit: Commit objects to summarise
  1066. board_selected: Dict containing boards to summarise
  1067. """
  1068. self.commit_count = len(commits) if commits else 1
  1069. self.commits = commits
  1070. self.ResetResultSummary(board_selected)
  1071. self._error_lines = 0
  1072. for commit_upto in range(0, self.commit_count, self._step):
  1073. self.ProduceResultSummary(commit_upto, commits, board_selected)
  1074. if not self._error_lines:
  1075. Print('(no errors to report)', colour=self.col.GREEN)
  1076. def SetupBuild(self, board_selected, commits):
  1077. """Set up ready to start a build.
  1078. Args:
  1079. board_selected: Selected boards to build
  1080. commits: Selected commits to build
  1081. """
  1082. # First work out how many commits we will build
  1083. count = (self.commit_count + self._step - 1) / self._step
  1084. self.count = len(board_selected) * count
  1085. self.upto = self.warned = self.fail = 0
  1086. self._timestamps = collections.deque()
  1087. def GetThreadDir(self, thread_num):
  1088. """Get the directory path to the working dir for a thread.
  1089. Args:
  1090. thread_num: Number of thread to check.
  1091. """
  1092. return os.path.join(self._working_dir, '%02d' % thread_num)
  1093. def _PrepareThread(self, thread_num, setup_git):
  1094. """Prepare the working directory for a thread.
  1095. This clones or fetches the repo into the thread's work directory.
  1096. Args:
  1097. thread_num: Thread number (0, 1, ...)
  1098. setup_git: True to set up a git repo clone
  1099. """
  1100. thread_dir = self.GetThreadDir(thread_num)
  1101. builderthread.Mkdir(thread_dir)
  1102. git_dir = os.path.join(thread_dir, '.git')
  1103. # Clone the repo if it doesn't already exist
  1104. # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
  1105. # we have a private index but uses the origin repo's contents?
  1106. if setup_git and self.git_dir:
  1107. src_dir = os.path.abspath(self.git_dir)
  1108. if os.path.exists(git_dir):
  1109. gitutil.Fetch(git_dir, thread_dir)
  1110. else:
  1111. Print('Cloning repo for thread %d' % thread_num)
  1112. gitutil.Clone(src_dir, thread_dir)
  1113. def _PrepareWorkingSpace(self, max_threads, setup_git):
  1114. """Prepare the working directory for use.
  1115. Set up the git repo for each thread.
  1116. Args:
  1117. max_threads: Maximum number of threads we expect to need.
  1118. setup_git: True to set up a git repo clone
  1119. """
  1120. builderthread.Mkdir(self._working_dir)
  1121. for thread in range(max_threads):
  1122. self._PrepareThread(thread, setup_git)
  1123. def _PrepareOutputSpace(self):
  1124. """Get the output directories ready to receive files.
  1125. We delete any output directories which look like ones we need to
  1126. create. Having left over directories is confusing when the user wants
  1127. to check the output manually.
  1128. """
  1129. if not self.commits:
  1130. return
  1131. dir_list = []
  1132. for commit_upto in range(self.commit_count):
  1133. dir_list.append(self._GetOutputDir(commit_upto))
  1134. for dirname in glob.glob(os.path.join(self.base_dir, '*')):
  1135. if dirname not in dir_list:
  1136. shutil.rmtree(dirname)
  1137. def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
  1138. """Build all commits for a list of boards
  1139. Args:
  1140. commits: List of commits to be build, each a Commit object
  1141. boards_selected: Dict of selected boards, key is target name,
  1142. value is Board object
  1143. keep_outputs: True to save build output files
  1144. verbose: Display build results as they are completed
  1145. Returns:
  1146. Tuple containing:
  1147. - number of boards that failed to build
  1148. - number of boards that issued warnings
  1149. """
  1150. self.commit_count = len(commits) if commits else 1
  1151. self.commits = commits
  1152. self._verbose = verbose
  1153. self.ResetResultSummary(board_selected)
  1154. builderthread.Mkdir(self.base_dir, parents = True)
  1155. self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
  1156. commits is not None)
  1157. self._PrepareOutputSpace()
  1158. self.SetupBuild(board_selected, commits)
  1159. self.ProcessResult(None)
  1160. # Create jobs to build all commits for each board
  1161. for brd in board_selected.itervalues():
  1162. job = builderthread.BuilderJob()
  1163. job.board = brd
  1164. job.commits = commits
  1165. job.keep_outputs = keep_outputs
  1166. job.step = self._step
  1167. self.queue.put(job)
  1168. # Wait until all jobs are started
  1169. self.queue.join()
  1170. # Wait until we have processed all output
  1171. self.out_queue.join()
  1172. Print()
  1173. self.ClearLine(0)
  1174. return (self.fail, self.warned)