builder.py 55 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430
  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. import errno
  9. from datetime import datetime, timedelta
  10. import glob
  11. import os
  12. import re
  13. import Queue
  14. import shutil
  15. import string
  16. import sys
  17. import threading
  18. import time
  19. import command
  20. import gitutil
  21. import terminal
  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. def Mkdir(dirname):
  82. """Make a directory if it doesn't already exist.
  83. Args:
  84. dirname: Directory to create
  85. """
  86. try:
  87. os.mkdir(dirname)
  88. except OSError as err:
  89. if err.errno == errno.EEXIST:
  90. pass
  91. else:
  92. raise
  93. class BuilderJob:
  94. """Holds information about a job to be performed by a thread
  95. Members:
  96. board: Board object to build
  97. commits: List of commit options to build.
  98. """
  99. def __init__(self):
  100. self.board = None
  101. self.commits = []
  102. class ResultThread(threading.Thread):
  103. """This thread processes results from builder threads.
  104. It simply passes the results on to the builder. There is only one
  105. result thread, and this helps to serialise the build output.
  106. """
  107. def __init__(self, builder):
  108. """Set up a new result thread
  109. Args:
  110. builder: Builder which will be sent each result
  111. """
  112. threading.Thread.__init__(self)
  113. self.builder = builder
  114. def run(self):
  115. """Called to start up the result thread.
  116. We collect the next result job and pass it on to the build.
  117. """
  118. while True:
  119. result = self.builder.out_queue.get()
  120. self.builder.ProcessResult(result)
  121. self.builder.out_queue.task_done()
  122. class BuilderThread(threading.Thread):
  123. """This thread builds U-Boot for a particular board.
  124. An input queue provides each new job. We run 'make' to build U-Boot
  125. and then pass the results on to the output queue.
  126. Members:
  127. builder: The builder which contains information we might need
  128. thread_num: Our thread number (0-n-1), used to decide on a
  129. temporary directory
  130. """
  131. def __init__(self, builder, thread_num):
  132. """Set up a new builder thread"""
  133. threading.Thread.__init__(self)
  134. self.builder = builder
  135. self.thread_num = thread_num
  136. def Make(self, commit, brd, stage, cwd, *args, **kwargs):
  137. """Run 'make' on a particular commit and board.
  138. The source code will already be checked out, so the 'commit'
  139. argument is only for information.
  140. Args:
  141. commit: Commit object that is being built
  142. brd: Board object that is being built
  143. stage: Stage of the build. Valid stages are:
  144. distclean - can be called to clean source
  145. config - called to configure for a board
  146. build - the main make invocation - it does the build
  147. args: A list of arguments to pass to 'make'
  148. kwargs: A list of keyword arguments to pass to command.RunPipe()
  149. Returns:
  150. CommandResult object
  151. """
  152. return self.builder.do_make(commit, brd, stage, cwd, *args,
  153. **kwargs)
  154. def RunCommit(self, commit_upto, brd, work_dir, do_config, force_build):
  155. """Build a particular commit.
  156. If the build is already done, and we are not forcing a build, we skip
  157. the build and just return the previously-saved results.
  158. Args:
  159. commit_upto: Commit number to build (0...n-1)
  160. brd: Board object to build
  161. work_dir: Directory to which the source will be checked out
  162. do_config: True to run a make <board>_config on the source
  163. force_build: Force a build even if one was previously done
  164. Returns:
  165. tuple containing:
  166. - CommandResult object containing the results of the build
  167. - boolean indicating whether 'make config' is still needed
  168. """
  169. # Create a default result - it will be overwritte by the call to
  170. # self.Make() below, in the event that we do a build.
  171. result = command.CommandResult()
  172. result.return_code = 0
  173. out_dir = os.path.join(work_dir, 'build')
  174. # Check if the job was already completed last time
  175. done_file = self.builder.GetDoneFile(commit_upto, brd.target)
  176. result.already_done = os.path.exists(done_file)
  177. if result.already_done and not force_build:
  178. # Get the return code from that build and use it
  179. with open(done_file, 'r') as fd:
  180. result.return_code = int(fd.readline())
  181. err_file = self.builder.GetErrFile(commit_upto, brd.target)
  182. if os.path.exists(err_file) and os.stat(err_file).st_size:
  183. result.stderr = 'bad'
  184. else:
  185. # We are going to have to build it. First, get a toolchain
  186. if not self.toolchain:
  187. try:
  188. self.toolchain = self.builder.toolchains.Select(brd.arch)
  189. except ValueError as err:
  190. result.return_code = 10
  191. result.stdout = ''
  192. result.stderr = str(err)
  193. # TODO(sjg@chromium.org): This gets swallowed, but needs
  194. # to be reported.
  195. if self.toolchain:
  196. # Checkout the right commit
  197. if commit_upto is not None:
  198. commit = self.builder.commits[commit_upto]
  199. if self.builder.checkout:
  200. git_dir = os.path.join(work_dir, '.git')
  201. gitutil.Checkout(commit.hash, git_dir, work_dir,
  202. force=True)
  203. else:
  204. commit = self.builder.commit # Ick, fix this for BuildCommits()
  205. # Set up the environment and command line
  206. env = self.toolchain.MakeEnvironment()
  207. Mkdir(out_dir)
  208. args = ['O=build', '-s']
  209. if self.builder.num_jobs is not None:
  210. args.extend(['-j', str(self.builder.num_jobs)])
  211. config_args = ['%s_config' % brd.target]
  212. config_out = ''
  213. args.extend(self.builder.toolchains.GetMakeArguments(brd))
  214. # If we need to reconfigure, do that now
  215. if do_config:
  216. result = self.Make(commit, brd, 'distclean', work_dir,
  217. 'distclean', *args, env=env)
  218. result = self.Make(commit, brd, 'config', work_dir,
  219. *(args + config_args), env=env)
  220. config_out = result.combined
  221. do_config = False # No need to configure next time
  222. if result.return_code == 0:
  223. result = self.Make(commit, brd, 'build', work_dir, *args,
  224. env=env)
  225. result.stdout = config_out + result.stdout
  226. else:
  227. result.return_code = 1
  228. result.stderr = 'No tool chain for %s\n' % brd.arch
  229. result.already_done = False
  230. result.toolchain = self.toolchain
  231. result.brd = brd
  232. result.commit_upto = commit_upto
  233. result.out_dir = out_dir
  234. return result, do_config
  235. def _WriteResult(self, result, keep_outputs):
  236. """Write a built result to the output directory.
  237. Args:
  238. result: CommandResult object containing result to write
  239. keep_outputs: True to store the output binaries, False
  240. to delete them
  241. """
  242. # Fatal error
  243. if result.return_code < 0:
  244. return
  245. # Aborted?
  246. if result.stderr and 'No child processes' in result.stderr:
  247. return
  248. if result.already_done:
  249. return
  250. # Write the output and stderr
  251. output_dir = self.builder._GetOutputDir(result.commit_upto)
  252. Mkdir(output_dir)
  253. build_dir = self.builder.GetBuildDir(result.commit_upto,
  254. result.brd.target)
  255. Mkdir(build_dir)
  256. outfile = os.path.join(build_dir, 'log')
  257. with open(outfile, 'w') as fd:
  258. if result.stdout:
  259. fd.write(result.stdout)
  260. errfile = self.builder.GetErrFile(result.commit_upto,
  261. result.brd.target)
  262. if result.stderr:
  263. with open(errfile, 'w') as fd:
  264. fd.write(result.stderr)
  265. elif os.path.exists(errfile):
  266. os.remove(errfile)
  267. if result.toolchain:
  268. # Write the build result and toolchain information.
  269. done_file = self.builder.GetDoneFile(result.commit_upto,
  270. result.brd.target)
  271. with open(done_file, 'w') as fd:
  272. fd.write('%s' % result.return_code)
  273. with open(os.path.join(build_dir, 'toolchain'), 'w') as fd:
  274. print >>fd, 'gcc', result.toolchain.gcc
  275. print >>fd, 'path', result.toolchain.path
  276. print >>fd, 'cross', result.toolchain.cross
  277. print >>fd, 'arch', result.toolchain.arch
  278. fd.write('%s' % result.return_code)
  279. with open(os.path.join(build_dir, 'toolchain'), 'w') as fd:
  280. print >>fd, 'gcc', result.toolchain.gcc
  281. print >>fd, 'path', result.toolchain.path
  282. # Write out the image and function size information and an objdump
  283. env = result.toolchain.MakeEnvironment()
  284. lines = []
  285. for fname in ['u-boot', 'spl/u-boot-spl']:
  286. cmd = ['%snm' % self.toolchain.cross, '--size-sort', fname]
  287. nm_result = command.RunPipe([cmd], capture=True,
  288. capture_stderr=True, cwd=result.out_dir,
  289. raise_on_error=False, env=env)
  290. if nm_result.stdout:
  291. nm = self.builder.GetFuncSizesFile(result.commit_upto,
  292. result.brd.target, fname)
  293. with open(nm, 'w') as fd:
  294. print >>fd, nm_result.stdout,
  295. cmd = ['%sobjdump' % self.toolchain.cross, '-h', fname]
  296. dump_result = command.RunPipe([cmd], capture=True,
  297. capture_stderr=True, cwd=result.out_dir,
  298. raise_on_error=False, env=env)
  299. rodata_size = ''
  300. if dump_result.stdout:
  301. objdump = self.builder.GetObjdumpFile(result.commit_upto,
  302. result.brd.target, fname)
  303. with open(objdump, 'w') as fd:
  304. print >>fd, dump_result.stdout,
  305. for line in dump_result.stdout.splitlines():
  306. fields = line.split()
  307. if len(fields) > 5 and fields[1] == '.rodata':
  308. rodata_size = fields[2]
  309. cmd = ['%ssize' % self.toolchain.cross, fname]
  310. size_result = command.RunPipe([cmd], capture=True,
  311. capture_stderr=True, cwd=result.out_dir,
  312. raise_on_error=False, env=env)
  313. if size_result.stdout:
  314. lines.append(size_result.stdout.splitlines()[1] + ' ' +
  315. rodata_size)
  316. # Write out the image sizes file. This is similar to the output
  317. # of binutil's 'size' utility, but it omits the header line and
  318. # adds an additional hex value at the end of each line for the
  319. # rodata size
  320. if len(lines):
  321. sizes = self.builder.GetSizesFile(result.commit_upto,
  322. result.brd.target)
  323. with open(sizes, 'w') as fd:
  324. print >>fd, '\n'.join(lines)
  325. # Now write the actual build output
  326. if keep_outputs:
  327. patterns = ['u-boot', '*.bin', 'u-boot.dtb', '*.map',
  328. 'include/autoconf.mk', 'spl/u-boot-spl',
  329. 'spl/u-boot-spl.bin']
  330. for pattern in patterns:
  331. file_list = glob.glob(os.path.join(result.out_dir, pattern))
  332. for fname in file_list:
  333. shutil.copy(fname, build_dir)
  334. def RunJob(self, job):
  335. """Run a single job
  336. A job consists of a building a list of commits for a particular board.
  337. Args:
  338. job: Job to build
  339. """
  340. brd = job.board
  341. work_dir = self.builder.GetThreadDir(self.thread_num)
  342. self.toolchain = None
  343. if job.commits:
  344. # Run 'make board_config' on the first commit
  345. do_config = True
  346. commit_upto = 0
  347. force_build = False
  348. for commit_upto in range(0, len(job.commits), job.step):
  349. result, request_config = self.RunCommit(commit_upto, brd,
  350. work_dir, do_config,
  351. force_build or self.builder.force_build)
  352. failed = result.return_code or result.stderr
  353. if failed and not do_config:
  354. # If our incremental build failed, try building again
  355. # with a reconfig.
  356. if self.builder.force_config_on_failure:
  357. result, request_config = self.RunCommit(commit_upto,
  358. brd, work_dir, True, True)
  359. do_config = request_config
  360. # If we built that commit, then config is done. But if we got
  361. # an warning, reconfig next time to force it to build the same
  362. # files that created warnings this time. Otherwise an
  363. # incremental build may not build the same file, and we will
  364. # think that the warning has gone away.
  365. # We could avoid this by using -Werror everywhere...
  366. # For errors, the problem doesn't happen, since presumably
  367. # the build stopped and didn't generate output, so will retry
  368. # that file next time. So we could detect warnings and deal
  369. # with them specially here. For now, we just reconfigure if
  370. # anything goes work.
  371. # Of course this is substantially slower if there are build
  372. # errors/warnings (e.g. 2-3x slower even if only 10% of builds
  373. # have problems).
  374. if (failed and not result.already_done and not do_config and
  375. self.builder.force_config_on_failure):
  376. # If this build failed, try the next one with a
  377. # reconfigure.
  378. # Sometimes if the board_config.h file changes it can mess
  379. # with dependencies, and we get:
  380. # make: *** No rule to make target `include/autoconf.mk',
  381. # needed by `depend'.
  382. do_config = True
  383. force_build = True
  384. else:
  385. force_build = False
  386. if self.builder.force_config_on_failure:
  387. if failed:
  388. do_config = True
  389. result.commit_upto = commit_upto
  390. if result.return_code < 0:
  391. raise ValueError('Interrupt')
  392. # We have the build results, so output the result
  393. self._WriteResult(result, job.keep_outputs)
  394. self.builder.out_queue.put(result)
  395. else:
  396. # Just build the currently checked-out build
  397. result = self.RunCommit(None, True)
  398. result.commit_upto = self.builder.upto
  399. self.builder.out_queue.put(result)
  400. def run(self):
  401. """Our thread's run function
  402. This thread picks a job from the queue, runs it, and then goes to the
  403. next job.
  404. """
  405. alive = True
  406. while True:
  407. job = self.builder.queue.get()
  408. try:
  409. if self.builder.active and alive:
  410. self.RunJob(job)
  411. except Exception as err:
  412. alive = False
  413. print err
  414. self.builder.queue.task_done()
  415. class Builder:
  416. """Class for building U-Boot for a particular commit.
  417. Public members: (many should ->private)
  418. active: True if the builder is active and has not been stopped
  419. already_done: Number of builds already completed
  420. base_dir: Base directory to use for builder
  421. checkout: True to check out source, False to skip that step.
  422. This is used for testing.
  423. col: terminal.Color() object
  424. count: Number of commits to build
  425. do_make: Method to call to invoke Make
  426. fail: Number of builds that failed due to error
  427. force_build: Force building even if a build already exists
  428. force_config_on_failure: If a commit fails for a board, disable
  429. incremental building for the next commit we build for that
  430. board, so that we will see all warnings/errors again.
  431. git_dir: Git directory containing source repository
  432. last_line_len: Length of the last line we printed (used for erasing
  433. it with new progress information)
  434. num_jobs: Number of jobs to run at once (passed to make as -j)
  435. num_threads: Number of builder threads to run
  436. out_queue: Queue of results to process
  437. re_make_err: Compiled regular expression for ignore_lines
  438. queue: Queue of jobs to run
  439. threads: List of active threads
  440. toolchains: Toolchains object to use for building
  441. upto: Current commit number we are building (0.count-1)
  442. warned: Number of builds that produced at least one warning
  443. Private members:
  444. _base_board_dict: Last-summarised Dict of boards
  445. _base_err_lines: Last-summarised list of errors
  446. _build_period_us: Time taken for a single build (float object).
  447. _complete_delay: Expected delay until completion (timedelta)
  448. _next_delay_update: Next time we plan to display a progress update
  449. (datatime)
  450. _show_unknown: Show unknown boards (those not built) in summary
  451. _timestamps: List of timestamps for the completion of the last
  452. last _timestamp_count builds. Each is a datetime object.
  453. _timestamp_count: Number of timestamps to keep in our list.
  454. _working_dir: Base working directory containing all threads
  455. """
  456. class Outcome:
  457. """Records a build outcome for a single make invocation
  458. Public Members:
  459. rc: Outcome value (OUTCOME_...)
  460. err_lines: List of error lines or [] if none
  461. sizes: Dictionary of image size information, keyed by filename
  462. - Each value is itself a dictionary containing
  463. values for 'text', 'data' and 'bss', being the integer
  464. size in bytes of each section.
  465. func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
  466. value is itself a dictionary:
  467. key: function name
  468. value: Size of function in bytes
  469. """
  470. def __init__(self, rc, err_lines, sizes, func_sizes):
  471. self.rc = rc
  472. self.err_lines = err_lines
  473. self.sizes = sizes
  474. self.func_sizes = func_sizes
  475. def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
  476. checkout=True, show_unknown=True, step=1):
  477. """Create a new Builder object
  478. Args:
  479. toolchains: Toolchains object to use for building
  480. base_dir: Base directory to use for builder
  481. git_dir: Git directory containing source repository
  482. num_threads: Number of builder threads to run
  483. num_jobs: Number of jobs to run at once (passed to make as -j)
  484. checkout: True to check out source, False to skip that step.
  485. This is used for testing.
  486. show_unknown: Show unknown boards (those not built) in summary
  487. step: 1 to process every commit, n to process every nth commit
  488. """
  489. self.toolchains = toolchains
  490. self.base_dir = base_dir
  491. self._working_dir = os.path.join(base_dir, '.bm-work')
  492. self.threads = []
  493. self.active = True
  494. self.do_make = self.Make
  495. self.checkout = checkout
  496. self.num_threads = num_threads
  497. self.num_jobs = num_jobs
  498. self.already_done = 0
  499. self.force_build = False
  500. self.git_dir = git_dir
  501. self._show_unknown = show_unknown
  502. self._timestamp_count = 10
  503. self._build_period_us = None
  504. self._complete_delay = None
  505. self._next_delay_update = datetime.now()
  506. self.force_config_on_failure = True
  507. self._step = step
  508. self.col = terminal.Color()
  509. self.queue = Queue.Queue()
  510. self.out_queue = Queue.Queue()
  511. for i in range(self.num_threads):
  512. t = BuilderThread(self, i)
  513. t.setDaemon(True)
  514. t.start()
  515. self.threads.append(t)
  516. self.last_line_len = 0
  517. t = ResultThread(self)
  518. t.setDaemon(True)
  519. t.start()
  520. self.threads.append(t)
  521. ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
  522. self.re_make_err = re.compile('|'.join(ignore_lines))
  523. def __del__(self):
  524. """Get rid of all threads created by the builder"""
  525. for t in self.threads:
  526. del t
  527. def _AddTimestamp(self):
  528. """Add a new timestamp to the list and record the build period.
  529. The build period is the length of time taken to perform a single
  530. build (one board, one commit).
  531. """
  532. now = datetime.now()
  533. self._timestamps.append(now)
  534. count = len(self._timestamps)
  535. delta = self._timestamps[-1] - self._timestamps[0]
  536. seconds = delta.total_seconds()
  537. # If we have enough data, estimate build period (time taken for a
  538. # single build) and therefore completion time.
  539. if count > 1 and self._next_delay_update < now:
  540. self._next_delay_update = now + timedelta(seconds=2)
  541. if seconds > 0:
  542. self._build_period = float(seconds) / count
  543. todo = self.count - self.upto
  544. self._complete_delay = timedelta(microseconds=
  545. self._build_period * todo * 1000000)
  546. # Round it
  547. self._complete_delay -= timedelta(
  548. microseconds=self._complete_delay.microseconds)
  549. if seconds > 60:
  550. self._timestamps.popleft()
  551. count -= 1
  552. def ClearLine(self, length):
  553. """Clear any characters on the current line
  554. Make way for a new line of length 'length', by outputting enough
  555. spaces to clear out the old line. Then remember the new length for
  556. next time.
  557. Args:
  558. length: Length of new line, in characters
  559. """
  560. if length < self.last_line_len:
  561. print ' ' * (self.last_line_len - length),
  562. print '\r',
  563. self.last_line_len = length
  564. sys.stdout.flush()
  565. def SelectCommit(self, commit, checkout=True):
  566. """Checkout the selected commit for this build
  567. """
  568. self.commit = commit
  569. if checkout and self.checkout:
  570. gitutil.Checkout(commit.hash)
  571. def Make(self, commit, brd, stage, cwd, *args, **kwargs):
  572. """Run make
  573. Args:
  574. commit: Commit object that is being built
  575. brd: Board object that is being built
  576. stage: Stage that we are at (distclean, config, build)
  577. cwd: Directory where make should be run
  578. args: Arguments to pass to make
  579. kwargs: Arguments to pass to command.RunPipe()
  580. """
  581. cmd = ['make'] + list(args)
  582. result = command.RunPipe([cmd], capture=True, capture_stderr=True,
  583. cwd=cwd, raise_on_error=False, **kwargs)
  584. return result
  585. def ProcessResult(self, result):
  586. """Process the result of a build, showing progress information
  587. Args:
  588. result: A CommandResult object
  589. """
  590. col = terminal.Color()
  591. if result:
  592. target = result.brd.target
  593. if result.return_code < 0:
  594. self.active = False
  595. command.StopAll()
  596. return
  597. self.upto += 1
  598. if result.return_code != 0:
  599. self.fail += 1
  600. elif result.stderr:
  601. self.warned += 1
  602. if result.already_done:
  603. self.already_done += 1
  604. else:
  605. target = '(starting)'
  606. # Display separate counts for ok, warned and fail
  607. ok = self.upto - self.warned - self.fail
  608. line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
  609. line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
  610. line += self.col.Color(self.col.RED, '%5d' % self.fail)
  611. name = ' /%-5d ' % self.count
  612. # Add our current completion time estimate
  613. self._AddTimestamp()
  614. if self._complete_delay:
  615. name += '%s : ' % self._complete_delay
  616. # When building all boards for a commit, we can print a commit
  617. # progress message.
  618. if result and result.commit_upto is None:
  619. name += 'commit %2d/%-3d' % (self.commit_upto + 1,
  620. self.commit_count)
  621. name += target
  622. print line + name,
  623. length = 13 + len(name)
  624. self.ClearLine(length)
  625. def _GetOutputDir(self, commit_upto):
  626. """Get the name of the output directory for a commit number
  627. The output directory is typically .../<branch>/<commit>.
  628. Args:
  629. commit_upto: Commit number to use (0..self.count-1)
  630. """
  631. commit = self.commits[commit_upto]
  632. subject = commit.subject.translate(trans_valid_chars)
  633. commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
  634. self.commit_count, commit.hash, subject[:20]))
  635. output_dir = os.path.join(self.base_dir, commit_dir)
  636. return output_dir
  637. def GetBuildDir(self, commit_upto, target):
  638. """Get the name of the build directory for a commit number
  639. The build directory is typically .../<branch>/<commit>/<target>.
  640. Args:
  641. commit_upto: Commit number to use (0..self.count-1)
  642. target: Target name
  643. """
  644. output_dir = self._GetOutputDir(commit_upto)
  645. return os.path.join(output_dir, target)
  646. def GetDoneFile(self, commit_upto, target):
  647. """Get the name of the done file for a commit number
  648. Args:
  649. commit_upto: Commit number to use (0..self.count-1)
  650. target: Target name
  651. """
  652. return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
  653. def GetSizesFile(self, commit_upto, target):
  654. """Get the name of the sizes file for a commit number
  655. Args:
  656. commit_upto: Commit number to use (0..self.count-1)
  657. target: Target name
  658. """
  659. return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
  660. def GetFuncSizesFile(self, commit_upto, target, elf_fname):
  661. """Get the name of the funcsizes file for a commit number and ELF file
  662. Args:
  663. commit_upto: Commit number to use (0..self.count-1)
  664. target: Target name
  665. elf_fname: Filename of elf image
  666. """
  667. return os.path.join(self.GetBuildDir(commit_upto, target),
  668. '%s.sizes' % elf_fname.replace('/', '-'))
  669. def GetObjdumpFile(self, commit_upto, target, elf_fname):
  670. """Get the name of the objdump file for a commit number and ELF file
  671. Args:
  672. commit_upto: Commit number to use (0..self.count-1)
  673. target: Target name
  674. elf_fname: Filename of elf image
  675. """
  676. return os.path.join(self.GetBuildDir(commit_upto, target),
  677. '%s.objdump' % elf_fname.replace('/', '-'))
  678. def GetErrFile(self, commit_upto, target):
  679. """Get the name of the err file for a commit number
  680. Args:
  681. commit_upto: Commit number to use (0..self.count-1)
  682. target: Target name
  683. """
  684. output_dir = self.GetBuildDir(commit_upto, target)
  685. return os.path.join(output_dir, 'err')
  686. def FilterErrors(self, lines):
  687. """Filter out errors in which we have no interest
  688. We should probably use map().
  689. Args:
  690. lines: List of error lines, each a string
  691. Returns:
  692. New list with only interesting lines included
  693. """
  694. out_lines = []
  695. for line in lines:
  696. if not self.re_make_err.search(line):
  697. out_lines.append(line)
  698. return out_lines
  699. def ReadFuncSizes(self, fname, fd):
  700. """Read function sizes from the output of 'nm'
  701. Args:
  702. fd: File containing data to read
  703. fname: Filename we are reading from (just for errors)
  704. Returns:
  705. Dictionary containing size of each function in bytes, indexed by
  706. function name.
  707. """
  708. sym = {}
  709. for line in fd.readlines():
  710. try:
  711. size, type, name = line[:-1].split()
  712. except:
  713. print "Invalid line in file '%s': '%s'" % (fname, line[:-1])
  714. continue
  715. if type in 'tTdDbB':
  716. # function names begin with '.' on 64-bit powerpc
  717. if '.' in name[1:]:
  718. name = 'static.' + name.split('.')[0]
  719. sym[name] = sym.get(name, 0) + int(size, 16)
  720. return sym
  721. def GetBuildOutcome(self, commit_upto, target, read_func_sizes):
  722. """Work out the outcome of a build.
  723. Args:
  724. commit_upto: Commit number to check (0..n-1)
  725. target: Target board to check
  726. read_func_sizes: True to read function size information
  727. Returns:
  728. Outcome object
  729. """
  730. done_file = self.GetDoneFile(commit_upto, target)
  731. sizes_file = self.GetSizesFile(commit_upto, target)
  732. sizes = {}
  733. func_sizes = {}
  734. if os.path.exists(done_file):
  735. with open(done_file, 'r') as fd:
  736. return_code = int(fd.readline())
  737. err_lines = []
  738. err_file = self.GetErrFile(commit_upto, target)
  739. if os.path.exists(err_file):
  740. with open(err_file, 'r') as fd:
  741. err_lines = self.FilterErrors(fd.readlines())
  742. # Decide whether the build was ok, failed or created warnings
  743. if return_code:
  744. rc = OUTCOME_ERROR
  745. elif len(err_lines):
  746. rc = OUTCOME_WARNING
  747. else:
  748. rc = OUTCOME_OK
  749. # Convert size information to our simple format
  750. if os.path.exists(sizes_file):
  751. with open(sizes_file, 'r') as fd:
  752. for line in fd.readlines():
  753. values = line.split()
  754. rodata = 0
  755. if len(values) > 6:
  756. rodata = int(values[6], 16)
  757. size_dict = {
  758. 'all' : int(values[0]) + int(values[1]) +
  759. int(values[2]),
  760. 'text' : int(values[0]) - rodata,
  761. 'data' : int(values[1]),
  762. 'bss' : int(values[2]),
  763. 'rodata' : rodata,
  764. }
  765. sizes[values[5]] = size_dict
  766. if read_func_sizes:
  767. pattern = self.GetFuncSizesFile(commit_upto, target, '*')
  768. for fname in glob.glob(pattern):
  769. with open(fname, 'r') as fd:
  770. dict_name = os.path.basename(fname).replace('.sizes',
  771. '')
  772. func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
  773. return Builder.Outcome(rc, err_lines, sizes, func_sizes)
  774. return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {})
  775. def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes):
  776. """Calculate a summary of the results of building a commit.
  777. Args:
  778. board_selected: Dict containing boards to summarise
  779. commit_upto: Commit number to summarize (0..self.count-1)
  780. read_func_sizes: True to read function size information
  781. Returns:
  782. Tuple:
  783. Dict containing boards which passed building this commit.
  784. keyed by board.target
  785. List containing a summary of error/warning lines
  786. """
  787. board_dict = {}
  788. err_lines_summary = []
  789. for board in boards_selected.itervalues():
  790. outcome = self.GetBuildOutcome(commit_upto, board.target,
  791. read_func_sizes)
  792. board_dict[board.target] = outcome
  793. for err in outcome.err_lines:
  794. if err and not err.rstrip() in err_lines_summary:
  795. err_lines_summary.append(err.rstrip())
  796. return board_dict, err_lines_summary
  797. def AddOutcome(self, board_dict, arch_list, changes, char, color):
  798. """Add an output to our list of outcomes for each architecture
  799. This simple function adds failing boards (changes) to the
  800. relevant architecture string, so we can print the results out
  801. sorted by architecture.
  802. Args:
  803. board_dict: Dict containing all boards
  804. arch_list: Dict keyed by arch name. Value is a string containing
  805. a list of board names which failed for that arch.
  806. changes: List of boards to add to arch_list
  807. color: terminal.Colour object
  808. """
  809. done_arch = {}
  810. for target in changes:
  811. if target in board_dict:
  812. arch = board_dict[target].arch
  813. else:
  814. arch = 'unknown'
  815. str = self.col.Color(color, ' ' + target)
  816. if not arch in done_arch:
  817. str = self.col.Color(color, char) + ' ' + str
  818. done_arch[arch] = True
  819. if not arch in arch_list:
  820. arch_list[arch] = str
  821. else:
  822. arch_list[arch] += str
  823. def ColourNum(self, num):
  824. color = self.col.RED if num > 0 else self.col.GREEN
  825. if num == 0:
  826. return '0'
  827. return self.col.Color(color, str(num))
  828. def ResetResultSummary(self, board_selected):
  829. """Reset the results summary ready for use.
  830. Set up the base board list to be all those selected, and set the
  831. error lines to empty.
  832. Following this, calls to PrintResultSummary() will use this
  833. information to work out what has changed.
  834. Args:
  835. board_selected: Dict containing boards to summarise, keyed by
  836. board.target
  837. """
  838. self._base_board_dict = {}
  839. for board in board_selected:
  840. self._base_board_dict[board] = Builder.Outcome(0, [], [], {})
  841. self._base_err_lines = []
  842. def PrintFuncSizeDetail(self, fname, old, new):
  843. grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
  844. delta, common = [], {}
  845. for a in old:
  846. if a in new:
  847. common[a] = 1
  848. for name in old:
  849. if name not in common:
  850. remove += 1
  851. down += old[name]
  852. delta.append([-old[name], name])
  853. for name in new:
  854. if name not in common:
  855. add += 1
  856. up += new[name]
  857. delta.append([new[name], name])
  858. for name in common:
  859. diff = new.get(name, 0) - old.get(name, 0)
  860. if diff > 0:
  861. grow, up = grow + 1, up + diff
  862. elif diff < 0:
  863. shrink, down = shrink + 1, down - diff
  864. delta.append([diff, name])
  865. delta.sort()
  866. delta.reverse()
  867. args = [add, -remove, grow, -shrink, up, -down, up - down]
  868. if max(args) == 0:
  869. return
  870. args = [self.ColourNum(x) for x in args]
  871. indent = ' ' * 15
  872. print ('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
  873. tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
  874. print '%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
  875. 'delta')
  876. for diff, name in delta:
  877. if diff:
  878. color = self.col.RED if diff > 0 else self.col.GREEN
  879. msg = '%s %-38s %7s %7s %+7d' % (indent, name,
  880. old.get(name, '-'), new.get(name,'-'), diff)
  881. print self.col.Color(color, msg)
  882. def PrintSizeDetail(self, target_list, show_bloat):
  883. """Show details size information for each board
  884. Args:
  885. target_list: List of targets, each a dict containing:
  886. 'target': Target name
  887. 'total_diff': Total difference in bytes across all areas
  888. <part_name>: Difference for that part
  889. show_bloat: Show detail for each function
  890. """
  891. targets_by_diff = sorted(target_list, reverse=True,
  892. key=lambda x: x['_total_diff'])
  893. for result in targets_by_diff:
  894. printed_target = False
  895. for name in sorted(result):
  896. diff = result[name]
  897. if name.startswith('_'):
  898. continue
  899. if diff != 0:
  900. color = self.col.RED if diff > 0 else self.col.GREEN
  901. msg = ' %s %+d' % (name, diff)
  902. if not printed_target:
  903. print '%10s %-15s:' % ('', result['_target']),
  904. printed_target = True
  905. print self.col.Color(color, msg),
  906. if printed_target:
  907. print
  908. if show_bloat:
  909. target = result['_target']
  910. outcome = result['_outcome']
  911. base_outcome = self._base_board_dict[target]
  912. for fname in outcome.func_sizes:
  913. self.PrintFuncSizeDetail(fname,
  914. base_outcome.func_sizes[fname],
  915. outcome.func_sizes[fname])
  916. def PrintSizeSummary(self, board_selected, board_dict, show_detail,
  917. show_bloat):
  918. """Print a summary of image sizes broken down by section.
  919. The summary takes the form of one line per architecture. The
  920. line contains deltas for each of the sections (+ means the section
  921. got bigger, - means smaller). The nunmbers are the average number
  922. of bytes that a board in this section increased by.
  923. For example:
  924. powerpc: (622 boards) text -0.0
  925. arm: (285 boards) text -0.0
  926. nds32: (3 boards) text -8.0
  927. Args:
  928. board_selected: Dict containing boards to summarise, keyed by
  929. board.target
  930. board_dict: Dict containing boards for which we built this
  931. commit, keyed by board.target. The value is an Outcome object.
  932. show_detail: Show detail for each board
  933. show_bloat: Show detail for each function
  934. """
  935. arch_list = {}
  936. arch_count = {}
  937. # Calculate changes in size for different image parts
  938. # The previous sizes are in Board.sizes, for each board
  939. for target in board_dict:
  940. if target not in board_selected:
  941. continue
  942. base_sizes = self._base_board_dict[target].sizes
  943. outcome = board_dict[target]
  944. sizes = outcome.sizes
  945. # Loop through the list of images, creating a dict of size
  946. # changes for each image/part. We end up with something like
  947. # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
  948. # which means that U-Boot data increased by 5 bytes and SPL
  949. # text decreased by 4.
  950. err = {'_target' : target}
  951. for image in sizes:
  952. if image in base_sizes:
  953. base_image = base_sizes[image]
  954. # Loop through the text, data, bss parts
  955. for part in sorted(sizes[image]):
  956. diff = sizes[image][part] - base_image[part]
  957. col = None
  958. if diff:
  959. if image == 'u-boot':
  960. name = part
  961. else:
  962. name = image + ':' + part
  963. err[name] = diff
  964. arch = board_selected[target].arch
  965. if not arch in arch_count:
  966. arch_count[arch] = 1
  967. else:
  968. arch_count[arch] += 1
  969. if not sizes:
  970. pass # Only add to our list when we have some stats
  971. elif not arch in arch_list:
  972. arch_list[arch] = [err]
  973. else:
  974. arch_list[arch].append(err)
  975. # We now have a list of image size changes sorted by arch
  976. # Print out a summary of these
  977. for arch, target_list in arch_list.iteritems():
  978. # Get total difference for each type
  979. totals = {}
  980. for result in target_list:
  981. total = 0
  982. for name, diff in result.iteritems():
  983. if name.startswith('_'):
  984. continue
  985. total += diff
  986. if name in totals:
  987. totals[name] += diff
  988. else:
  989. totals[name] = diff
  990. result['_total_diff'] = total
  991. result['_outcome'] = board_dict[result['_target']]
  992. count = len(target_list)
  993. printed_arch = False
  994. for name in sorted(totals):
  995. diff = totals[name]
  996. if diff:
  997. # Display the average difference in this name for this
  998. # architecture
  999. avg_diff = float(diff) / count
  1000. color = self.col.RED if avg_diff > 0 else self.col.GREEN
  1001. msg = ' %s %+1.1f' % (name, avg_diff)
  1002. if not printed_arch:
  1003. print '%10s: (for %d/%d boards)' % (arch, count,
  1004. arch_count[arch]),
  1005. printed_arch = True
  1006. print self.col.Color(color, msg),
  1007. if printed_arch:
  1008. print
  1009. if show_detail:
  1010. self.PrintSizeDetail(target_list, show_bloat)
  1011. def PrintResultSummary(self, board_selected, board_dict, err_lines,
  1012. show_sizes, show_detail, show_bloat):
  1013. """Compare results with the base results and display delta.
  1014. Only boards mentioned in board_selected will be considered. This
  1015. function is intended to be called repeatedly with the results of
  1016. each commit. It therefore shows a 'diff' between what it saw in
  1017. the last call and what it sees now.
  1018. Args:
  1019. board_selected: Dict containing boards to summarise, keyed by
  1020. board.target
  1021. board_dict: Dict containing boards for which we built this
  1022. commit, keyed by board.target. The value is an Outcome object.
  1023. err_lines: A list of errors for this commit, or [] if there is
  1024. none, or we don't want to print errors
  1025. show_sizes: Show image size deltas
  1026. show_detail: Show detail for each board
  1027. show_bloat: Show detail for each function
  1028. """
  1029. better = [] # List of boards fixed since last commit
  1030. worse = [] # List of new broken boards since last commit
  1031. new = [] # List of boards that didn't exist last time
  1032. unknown = [] # List of boards that were not built
  1033. for target in board_dict:
  1034. if target not in board_selected:
  1035. continue
  1036. # If the board was built last time, add its outcome to a list
  1037. if target in self._base_board_dict:
  1038. base_outcome = self._base_board_dict[target].rc
  1039. outcome = board_dict[target]
  1040. if outcome.rc == OUTCOME_UNKNOWN:
  1041. unknown.append(target)
  1042. elif outcome.rc < base_outcome:
  1043. better.append(target)
  1044. elif outcome.rc > base_outcome:
  1045. worse.append(target)
  1046. else:
  1047. new.append(target)
  1048. # Get a list of errors that have appeared, and disappeared
  1049. better_err = []
  1050. worse_err = []
  1051. for line in err_lines:
  1052. if line not in self._base_err_lines:
  1053. worse_err.append('+' + line)
  1054. for line in self._base_err_lines:
  1055. if line not in err_lines:
  1056. better_err.append('-' + line)
  1057. # Display results by arch
  1058. if better or worse or unknown or new or worse_err or better_err:
  1059. arch_list = {}
  1060. self.AddOutcome(board_selected, arch_list, better, '',
  1061. self.col.GREEN)
  1062. self.AddOutcome(board_selected, arch_list, worse, '+',
  1063. self.col.RED)
  1064. self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE)
  1065. if self._show_unknown:
  1066. self.AddOutcome(board_selected, arch_list, unknown, '?',
  1067. self.col.MAGENTA)
  1068. for arch, target_list in arch_list.iteritems():
  1069. print '%10s: %s' % (arch, target_list)
  1070. if better_err:
  1071. print self.col.Color(self.col.GREEN, '\n'.join(better_err))
  1072. if worse_err:
  1073. print self.col.Color(self.col.RED, '\n'.join(worse_err))
  1074. if show_sizes:
  1075. self.PrintSizeSummary(board_selected, board_dict, show_detail,
  1076. show_bloat)
  1077. # Save our updated information for the next call to this function
  1078. self._base_board_dict = board_dict
  1079. self._base_err_lines = err_lines
  1080. # Get a list of boards that did not get built, if needed
  1081. not_built = []
  1082. for board in board_selected:
  1083. if not board in board_dict:
  1084. not_built.append(board)
  1085. if not_built:
  1086. print "Boards not built (%d): %s" % (len(not_built),
  1087. ', '.join(not_built))
  1088. def ShowSummary(self, commits, board_selected, show_errors, show_sizes,
  1089. show_detail, show_bloat):
  1090. """Show a build summary for U-Boot for a given board list.
  1091. Reset the result summary, then repeatedly call GetResultSummary on
  1092. each commit's results, then display the differences we see.
  1093. Args:
  1094. commit: Commit objects to summarise
  1095. board_selected: Dict containing boards to summarise
  1096. show_errors: Show errors that occured
  1097. show_sizes: Show size deltas
  1098. show_detail: Show detail for each board
  1099. show_bloat: Show detail for each function
  1100. """
  1101. self.commit_count = len(commits)
  1102. self.commits = commits
  1103. self.ResetResultSummary(board_selected)
  1104. for commit_upto in range(0, self.commit_count, self._step):
  1105. board_dict, err_lines = self.GetResultSummary(board_selected,
  1106. commit_upto, read_func_sizes=show_bloat)
  1107. msg = '%02d: %s' % (commit_upto + 1, commits[commit_upto].subject)
  1108. print self.col.Color(self.col.BLUE, msg)
  1109. self.PrintResultSummary(board_selected, board_dict,
  1110. err_lines if show_errors else [], show_sizes, show_detail,
  1111. show_bloat)
  1112. def SetupBuild(self, board_selected, commits):
  1113. """Set up ready to start a build.
  1114. Args:
  1115. board_selected: Selected boards to build
  1116. commits: Selected commits to build
  1117. """
  1118. # First work out how many commits we will build
  1119. count = (len(commits) + self._step - 1) / self._step
  1120. self.count = len(board_selected) * count
  1121. self.upto = self.warned = self.fail = 0
  1122. self._timestamps = collections.deque()
  1123. def BuildBoardsForCommit(self, board_selected, keep_outputs):
  1124. """Build all boards for a single commit"""
  1125. self.SetupBuild(board_selected)
  1126. self.count = len(board_selected)
  1127. for brd in board_selected.itervalues():
  1128. job = BuilderJob()
  1129. job.board = brd
  1130. job.commits = None
  1131. job.keep_outputs = keep_outputs
  1132. self.queue.put(brd)
  1133. self.queue.join()
  1134. self.out_queue.join()
  1135. print
  1136. self.ClearLine(0)
  1137. def BuildCommits(self, commits, board_selected, show_errors, keep_outputs):
  1138. """Build all boards for all commits (non-incremental)"""
  1139. self.commit_count = len(commits)
  1140. self.ResetResultSummary(board_selected)
  1141. for self.commit_upto in range(self.commit_count):
  1142. self.SelectCommit(commits[self.commit_upto])
  1143. self.SelectOutputDir()
  1144. Mkdir(self.output_dir)
  1145. self.BuildBoardsForCommit(board_selected, keep_outputs)
  1146. board_dict, err_lines = self.GetResultSummary()
  1147. self.PrintResultSummary(board_selected, board_dict,
  1148. err_lines if show_errors else [])
  1149. if self.already_done:
  1150. print '%d builds already done' % self.already_done
  1151. def GetThreadDir(self, thread_num):
  1152. """Get the directory path to the working dir for a thread.
  1153. Args:
  1154. thread_num: Number of thread to check.
  1155. """
  1156. return os.path.join(self._working_dir, '%02d' % thread_num)
  1157. def _PrepareThread(self, thread_num):
  1158. """Prepare the working directory for a thread.
  1159. This clones or fetches the repo into the thread's work directory.
  1160. Args:
  1161. thread_num: Thread number (0, 1, ...)
  1162. """
  1163. thread_dir = self.GetThreadDir(thread_num)
  1164. Mkdir(thread_dir)
  1165. git_dir = os.path.join(thread_dir, '.git')
  1166. # Clone the repo if it doesn't already exist
  1167. # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
  1168. # we have a private index but uses the origin repo's contents?
  1169. if self.git_dir:
  1170. src_dir = os.path.abspath(self.git_dir)
  1171. if os.path.exists(git_dir):
  1172. gitutil.Fetch(git_dir, thread_dir)
  1173. else:
  1174. print 'Cloning repo for thread %d' % thread_num
  1175. gitutil.Clone(src_dir, thread_dir)
  1176. def _PrepareWorkingSpace(self, max_threads):
  1177. """Prepare the working directory for use.
  1178. Set up the git repo for each thread.
  1179. Args:
  1180. max_threads: Maximum number of threads we expect to need.
  1181. """
  1182. Mkdir(self._working_dir)
  1183. for thread in range(max_threads):
  1184. self._PrepareThread(thread)
  1185. def _PrepareOutputSpace(self):
  1186. """Get the output directories ready to receive files.
  1187. We delete any output directories which look like ones we need to
  1188. create. Having left over directories is confusing when the user wants
  1189. to check the output manually.
  1190. """
  1191. dir_list = []
  1192. for commit_upto in range(self.commit_count):
  1193. dir_list.append(self._GetOutputDir(commit_upto))
  1194. for dirname in glob.glob(os.path.join(self.base_dir, '*')):
  1195. if dirname not in dir_list:
  1196. shutil.rmtree(dirname)
  1197. def BuildBoards(self, commits, board_selected, show_errors, keep_outputs):
  1198. """Build all commits for a list of boards
  1199. Args:
  1200. commits: List of commits to be build, each a Commit object
  1201. boards_selected: Dict of selected boards, key is target name,
  1202. value is Board object
  1203. show_errors: True to show summarised error/warning info
  1204. keep_outputs: True to save build output files
  1205. """
  1206. self.commit_count = len(commits)
  1207. self.commits = commits
  1208. self.ResetResultSummary(board_selected)
  1209. Mkdir(self.base_dir)
  1210. self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)))
  1211. self._PrepareOutputSpace()
  1212. self.SetupBuild(board_selected, commits)
  1213. self.ProcessResult(None)
  1214. # Create jobs to build all commits for each board
  1215. for brd in board_selected.itervalues():
  1216. job = BuilderJob()
  1217. job.board = brd
  1218. job.commits = commits
  1219. job.keep_outputs = keep_outputs
  1220. job.step = self._step
  1221. self.queue.put(job)
  1222. # Wait until all jobs are started
  1223. self.queue.join()
  1224. # Wait until we have processed all output
  1225. self.out_queue.join()
  1226. print
  1227. self.ClearLine(0)