builderthread.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. # Copyright (c) 2014 Google, Inc
  2. #
  3. # SPDX-License-Identifier: GPL-2.0+
  4. #
  5. import errno
  6. import glob
  7. import os
  8. import shutil
  9. import threading
  10. import command
  11. import gitutil
  12. RETURN_CODE_RETRY = -1
  13. def Mkdir(dirname, parents = False):
  14. """Make a directory if it doesn't already exist.
  15. Args:
  16. dirname: Directory to create
  17. """
  18. try:
  19. if parents:
  20. os.makedirs(dirname)
  21. else:
  22. os.mkdir(dirname)
  23. except OSError as err:
  24. if err.errno == errno.EEXIST:
  25. pass
  26. else:
  27. raise
  28. class BuilderJob:
  29. """Holds information about a job to be performed by a thread
  30. Members:
  31. board: Board object to build
  32. commits: List of commit options to build.
  33. """
  34. def __init__(self):
  35. self.board = None
  36. self.commits = []
  37. class ResultThread(threading.Thread):
  38. """This thread processes results from builder threads.
  39. It simply passes the results on to the builder. There is only one
  40. result thread, and this helps to serialise the build output.
  41. """
  42. def __init__(self, builder):
  43. """Set up a new result thread
  44. Args:
  45. builder: Builder which will be sent each result
  46. """
  47. threading.Thread.__init__(self)
  48. self.builder = builder
  49. def run(self):
  50. """Called to start up the result thread.
  51. We collect the next result job and pass it on to the build.
  52. """
  53. while True:
  54. result = self.builder.out_queue.get()
  55. self.builder.ProcessResult(result)
  56. self.builder.out_queue.task_done()
  57. class BuilderThread(threading.Thread):
  58. """This thread builds U-Boot for a particular board.
  59. An input queue provides each new job. We run 'make' to build U-Boot
  60. and then pass the results on to the output queue.
  61. Members:
  62. builder: The builder which contains information we might need
  63. thread_num: Our thread number (0-n-1), used to decide on a
  64. temporary directory
  65. """
  66. def __init__(self, builder, thread_num):
  67. """Set up a new builder thread"""
  68. threading.Thread.__init__(self)
  69. self.builder = builder
  70. self.thread_num = thread_num
  71. def Make(self, commit, brd, stage, cwd, *args, **kwargs):
  72. """Run 'make' on a particular commit and board.
  73. The source code will already be checked out, so the 'commit'
  74. argument is only for information.
  75. Args:
  76. commit: Commit object that is being built
  77. brd: Board object that is being built
  78. stage: Stage of the build. Valid stages are:
  79. mrproper - can be called to clean source
  80. config - called to configure for a board
  81. build - the main make invocation - it does the build
  82. args: A list of arguments to pass to 'make'
  83. kwargs: A list of keyword arguments to pass to command.RunPipe()
  84. Returns:
  85. CommandResult object
  86. """
  87. return self.builder.do_make(commit, brd, stage, cwd, *args,
  88. **kwargs)
  89. def RunCommit(self, commit_upto, brd, work_dir, do_config, force_build,
  90. force_build_failures):
  91. """Build a particular commit.
  92. If the build is already done, and we are not forcing a build, we skip
  93. the build and just return the previously-saved results.
  94. Args:
  95. commit_upto: Commit number to build (0...n-1)
  96. brd: Board object to build
  97. work_dir: Directory to which the source will be checked out
  98. do_config: True to run a make <board>_defconfig on the source
  99. force_build: Force a build even if one was previously done
  100. force_build_failures: Force a bulid if the previous result showed
  101. failure
  102. Returns:
  103. tuple containing:
  104. - CommandResult object containing the results of the build
  105. - boolean indicating whether 'make config' is still needed
  106. """
  107. # Create a default result - it will be overwritte by the call to
  108. # self.Make() below, in the event that we do a build.
  109. result = command.CommandResult()
  110. result.return_code = 0
  111. if self.builder.in_tree:
  112. out_dir = work_dir
  113. else:
  114. out_dir = os.path.join(work_dir, 'build')
  115. # Check if the job was already completed last time
  116. done_file = self.builder.GetDoneFile(commit_upto, brd.target)
  117. result.already_done = os.path.exists(done_file)
  118. will_build = (force_build or force_build_failures or
  119. not result.already_done)
  120. if result.already_done:
  121. # Get the return code from that build and use it
  122. with open(done_file, 'r') as fd:
  123. result.return_code = int(fd.readline())
  124. # Check the signal that the build needs to be retried
  125. if result.return_code == RETURN_CODE_RETRY:
  126. will_build = True
  127. elif will_build:
  128. err_file = self.builder.GetErrFile(commit_upto, brd.target)
  129. if os.path.exists(err_file) and os.stat(err_file).st_size:
  130. result.stderr = 'bad'
  131. elif not force_build:
  132. # The build passed, so no need to build it again
  133. will_build = False
  134. if will_build:
  135. # We are going to have to build it. First, get a toolchain
  136. if not self.toolchain:
  137. try:
  138. self.toolchain = self.builder.toolchains.Select(brd.arch)
  139. except ValueError as err:
  140. result.return_code = 10
  141. result.stdout = ''
  142. result.stderr = str(err)
  143. # TODO(sjg@chromium.org): This gets swallowed, but needs
  144. # to be reported.
  145. if self.toolchain:
  146. # Checkout the right commit
  147. if self.builder.commits:
  148. commit = self.builder.commits[commit_upto]
  149. if self.builder.checkout:
  150. git_dir = os.path.join(work_dir, '.git')
  151. gitutil.Checkout(commit.hash, git_dir, work_dir,
  152. force=True)
  153. else:
  154. commit = 'current'
  155. # Set up the environment and command line
  156. env = self.toolchain.MakeEnvironment(self.builder.full_path)
  157. Mkdir(out_dir)
  158. args = []
  159. cwd = work_dir
  160. src_dir = os.path.realpath(work_dir)
  161. if not self.builder.in_tree:
  162. if commit_upto is None:
  163. # In this case we are building in the original source
  164. # directory (i.e. the current directory where buildman
  165. # is invoked. The output directory is set to this
  166. # thread's selected work directory.
  167. #
  168. # Symlinks can confuse U-Boot's Makefile since
  169. # we may use '..' in our path, so remove them.
  170. work_dir = os.path.realpath(work_dir)
  171. args.append('O=%s/build' % work_dir)
  172. cwd = None
  173. src_dir = os.getcwd()
  174. else:
  175. args.append('O=build')
  176. if self.builder.verbose_build:
  177. args.append('V=1')
  178. else:
  179. args.append('-s')
  180. if self.builder.num_jobs is not None:
  181. args.extend(['-j', str(self.builder.num_jobs)])
  182. config_args = ['%s_defconfig' % brd.target]
  183. config_out = ''
  184. args.extend(self.builder.toolchains.GetMakeArguments(brd))
  185. # If we need to reconfigure, do that now
  186. if do_config:
  187. result = self.Make(commit, brd, 'mrproper', cwd,
  188. 'mrproper', *args, env=env)
  189. config_out = result.combined
  190. result = self.Make(commit, brd, 'config', cwd,
  191. *(args + config_args), env=env)
  192. config_out += result.combined
  193. do_config = False # No need to configure next time
  194. if result.return_code == 0:
  195. result = self.Make(commit, brd, 'build', cwd, *args,
  196. env=env)
  197. result.stderr = result.stderr.replace(src_dir + '/', '')
  198. if self.builder.verbose_build:
  199. result.stdout = config_out + result.stdout
  200. else:
  201. result.return_code = 1
  202. result.stderr = 'No tool chain for %s\n' % brd.arch
  203. result.already_done = False
  204. result.toolchain = self.toolchain
  205. result.brd = brd
  206. result.commit_upto = commit_upto
  207. result.out_dir = out_dir
  208. return result, do_config
  209. def _WriteResult(self, result, keep_outputs):
  210. """Write a built result to the output directory.
  211. Args:
  212. result: CommandResult object containing result to write
  213. keep_outputs: True to store the output binaries, False
  214. to delete them
  215. """
  216. # Fatal error
  217. if result.return_code < 0:
  218. return
  219. # If we think this might have been aborted with Ctrl-C, record the
  220. # failure but not that we are 'done' with this board. A retry may fix
  221. # it.
  222. maybe_aborted = result.stderr and 'No child processes' in result.stderr
  223. if result.already_done:
  224. return
  225. # Write the output and stderr
  226. output_dir = self.builder._GetOutputDir(result.commit_upto)
  227. Mkdir(output_dir)
  228. build_dir = self.builder.GetBuildDir(result.commit_upto,
  229. result.brd.target)
  230. Mkdir(build_dir)
  231. outfile = os.path.join(build_dir, 'log')
  232. with open(outfile, 'w') as fd:
  233. if result.stdout:
  234. fd.write(result.stdout)
  235. errfile = self.builder.GetErrFile(result.commit_upto,
  236. result.brd.target)
  237. if result.stderr:
  238. with open(errfile, 'w') as fd:
  239. fd.write(result.stderr)
  240. elif os.path.exists(errfile):
  241. os.remove(errfile)
  242. if result.toolchain:
  243. # Write the build result and toolchain information.
  244. done_file = self.builder.GetDoneFile(result.commit_upto,
  245. result.brd.target)
  246. with open(done_file, 'w') as fd:
  247. if maybe_aborted:
  248. # Special code to indicate we need to retry
  249. fd.write('%s' % RETURN_CODE_RETRY)
  250. else:
  251. fd.write('%s' % result.return_code)
  252. with open(os.path.join(build_dir, 'toolchain'), 'w') as fd:
  253. print >>fd, 'gcc', result.toolchain.gcc
  254. print >>fd, 'path', result.toolchain.path
  255. print >>fd, 'cross', result.toolchain.cross
  256. print >>fd, 'arch', result.toolchain.arch
  257. fd.write('%s' % result.return_code)
  258. with open(os.path.join(build_dir, 'toolchain'), 'w') as fd:
  259. print >>fd, 'gcc', result.toolchain.gcc
  260. print >>fd, 'path', result.toolchain.path
  261. # Write out the image and function size information and an objdump
  262. env = result.toolchain.MakeEnvironment(self.builder.full_path)
  263. lines = []
  264. for fname in ['u-boot', 'spl/u-boot-spl']:
  265. cmd = ['%snm' % self.toolchain.cross, '--size-sort', fname]
  266. nm_result = command.RunPipe([cmd], capture=True,
  267. capture_stderr=True, cwd=result.out_dir,
  268. raise_on_error=False, env=env)
  269. if nm_result.stdout:
  270. nm = self.builder.GetFuncSizesFile(result.commit_upto,
  271. result.brd.target, fname)
  272. with open(nm, 'w') as fd:
  273. print >>fd, nm_result.stdout,
  274. cmd = ['%sobjdump' % self.toolchain.cross, '-h', fname]
  275. dump_result = command.RunPipe([cmd], capture=True,
  276. capture_stderr=True, cwd=result.out_dir,
  277. raise_on_error=False, env=env)
  278. rodata_size = ''
  279. if dump_result.stdout:
  280. objdump = self.builder.GetObjdumpFile(result.commit_upto,
  281. result.brd.target, fname)
  282. with open(objdump, 'w') as fd:
  283. print >>fd, dump_result.stdout,
  284. for line in dump_result.stdout.splitlines():
  285. fields = line.split()
  286. if len(fields) > 5 and fields[1] == '.rodata':
  287. rodata_size = fields[2]
  288. cmd = ['%ssize' % self.toolchain.cross, fname]
  289. size_result = command.RunPipe([cmd], capture=True,
  290. capture_stderr=True, cwd=result.out_dir,
  291. raise_on_error=False, env=env)
  292. if size_result.stdout:
  293. lines.append(size_result.stdout.splitlines()[1] + ' ' +
  294. rodata_size)
  295. # Write out the image sizes file. This is similar to the output
  296. # of binutil's 'size' utility, but it omits the header line and
  297. # adds an additional hex value at the end of each line for the
  298. # rodata size
  299. if len(lines):
  300. sizes = self.builder.GetSizesFile(result.commit_upto,
  301. result.brd.target)
  302. with open(sizes, 'w') as fd:
  303. print >>fd, '\n'.join(lines)
  304. # Write out the configuration files, with a special case for SPL
  305. for dirname in ['', 'spl', 'tpl']:
  306. self.CopyFiles(result.out_dir, build_dir, dirname, ['u-boot.cfg',
  307. 'spl/u-boot-spl.cfg', 'tpl/u-boot-tpl.cfg', '.config',
  308. 'include/autoconf.mk', 'include/generated/autoconf.h'])
  309. # Now write the actual build output
  310. if keep_outputs:
  311. self.CopyFiles(result.out_dir, build_dir, '', ['u-boot*', '*.bin',
  312. '*.map', '*.img', 'MLO', 'include/autoconf.mk',
  313. 'spl/u-boot-spl*'])
  314. def CopyFiles(self, out_dir, build_dir, dirname, patterns):
  315. """Copy files from the build directory to the output.
  316. Args:
  317. out_dir: Path to output directory containing the files
  318. build_dir: Place to copy the files
  319. dirname: Source directory, '' for normal U-Boot, 'spl' for SPL
  320. patterns: A list of filenames (strings) to copy, each relative
  321. to the build directory
  322. """
  323. for pattern in patterns:
  324. file_list = glob.glob(os.path.join(out_dir, dirname, pattern))
  325. for fname in file_list:
  326. target = os.path.basename(fname)
  327. if dirname:
  328. base, ext = os.path.splitext(target)
  329. if ext:
  330. target = '%s-%s%s' % (base, dirname, ext)
  331. shutil.copy(fname, os.path.join(build_dir, target))
  332. def RunJob(self, job):
  333. """Run a single job
  334. A job consists of a building a list of commits for a particular board.
  335. Args:
  336. job: Job to build
  337. """
  338. brd = job.board
  339. work_dir = self.builder.GetThreadDir(self.thread_num)
  340. self.toolchain = None
  341. if job.commits:
  342. # Run 'make board_defconfig' on the first commit
  343. do_config = True
  344. commit_upto = 0
  345. force_build = False
  346. for commit_upto in range(0, len(job.commits), job.step):
  347. result, request_config = self.RunCommit(commit_upto, brd,
  348. work_dir, do_config,
  349. force_build or self.builder.force_build,
  350. self.builder.force_build_failures)
  351. failed = result.return_code or result.stderr
  352. did_config = do_config
  353. if failed and not do_config:
  354. # If our incremental build failed, try building again
  355. # with a reconfig.
  356. if self.builder.force_config_on_failure:
  357. result, request_config = self.RunCommit(commit_upto,
  358. brd, work_dir, True, True, False)
  359. did_config = True
  360. if not self.builder.force_reconfig:
  361. do_config = request_config
  362. # If we built that commit, then config is done. But if we got
  363. # an warning, reconfig next time to force it to build the same
  364. # files that created warnings this time. Otherwise an
  365. # incremental build may not build the same file, and we will
  366. # think that the warning has gone away.
  367. # We could avoid this by using -Werror everywhere...
  368. # For errors, the problem doesn't happen, since presumably
  369. # the build stopped and didn't generate output, so will retry
  370. # that file next time. So we could detect warnings and deal
  371. # with them specially here. For now, we just reconfigure if
  372. # anything goes work.
  373. # Of course this is substantially slower if there are build
  374. # errors/warnings (e.g. 2-3x slower even if only 10% of builds
  375. # have problems).
  376. if (failed and not result.already_done and not did_config and
  377. self.builder.force_config_on_failure):
  378. # If this build failed, try the next one with a
  379. # reconfigure.
  380. # Sometimes if the board_config.h file changes it can mess
  381. # with dependencies, and we get:
  382. # make: *** No rule to make target `include/autoconf.mk',
  383. # needed by `depend'.
  384. do_config = True
  385. force_build = True
  386. else:
  387. force_build = False
  388. if self.builder.force_config_on_failure:
  389. if failed:
  390. do_config = True
  391. result.commit_upto = commit_upto
  392. if result.return_code < 0:
  393. raise ValueError('Interrupt')
  394. # We have the build results, so output the result
  395. self._WriteResult(result, job.keep_outputs)
  396. self.builder.out_queue.put(result)
  397. else:
  398. # Just build the currently checked-out build
  399. result, request_config = self.RunCommit(None, brd, work_dir, True,
  400. True, self.builder.force_build_failures)
  401. result.commit_upto = 0
  402. self._WriteResult(result, job.keep_outputs)
  403. self.builder.out_queue.put(result)
  404. def run(self):
  405. """Our thread's run function
  406. This thread picks a job from the queue, runs it, and then goes to the
  407. next job.
  408. """
  409. alive = True
  410. while True:
  411. job = self.builder.queue.get()
  412. if self.builder.active and alive:
  413. self.RunJob(job)
  414. '''
  415. try:
  416. if self.builder.active and alive:
  417. self.RunJob(job)
  418. except Exception as err:
  419. alive = False
  420. print err
  421. '''
  422. self.builder.queue.task_done()