builder.py 56 KB

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