builder.py 56 KB

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