builder.py 75 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828
  1. # SPDX-License-Identifier: GPL-2.0+
  2. # Copyright (c) 2013 The Chromium OS Authors.
  3. #
  4. # Bloat-o-meter code used here Copyright 2004 Matt Mackall <mpm@selenic.com>
  5. #
  6. import collections
  7. from datetime import datetime, timedelta
  8. import glob
  9. import os
  10. import re
  11. import queue
  12. import shutil
  13. import signal
  14. import string
  15. import sys
  16. import threading
  17. import time
  18. from buildman import builderthread
  19. from buildman import toolchain
  20. from patman import gitutil
  21. from u_boot_pylib import command
  22. from u_boot_pylib import terminal
  23. from u_boot_pylib.terminal import tprint
  24. # This indicates an new int or hex Kconfig property with no default
  25. # It hangs the build since the 'conf' tool cannot proceed without valid input.
  26. #
  27. # We get a repeat sequence of something like this:
  28. # >>
  29. # Break things (BREAK_ME) [] (NEW)
  30. # Error in reading or end of file.
  31. # <<
  32. # which indicates that BREAK_ME has an empty default
  33. RE_NO_DEFAULT = re.compile(b'\((\w+)\) \[] \(NEW\)')
  34. """
  35. Theory of Operation
  36. Please see README for user documentation, and you should be familiar with
  37. that before trying to make sense of this.
  38. Buildman works by keeping the machine as busy as possible, building different
  39. commits for different boards on multiple CPUs at once.
  40. The source repo (self.git_dir) contains all the commits to be built. Each
  41. thread works on a single board at a time. It checks out the first commit,
  42. configures it for that board, then builds it. Then it checks out the next
  43. commit and builds it (typically without re-configuring). When it runs out
  44. of commits, it gets another job from the builder and starts again with that
  45. board.
  46. Clearly the builder threads could work either way - they could check out a
  47. commit and then built it for all boards. Using separate directories for each
  48. commit/board pair they could leave their build product around afterwards
  49. also.
  50. The intent behind building a single board for multiple commits, is to make
  51. use of incremental builds. Since each commit is built incrementally from
  52. the previous one, builds are faster. Reconfiguring for a different board
  53. removes all intermediate object files.
  54. Many threads can be working at once, but each has its own working directory.
  55. When a thread finishes a build, it puts the output files into a result
  56. directory.
  57. The base directory used by buildman is normally '../<branch>', i.e.
  58. a directory higher than the source repository and named after the branch
  59. being built.
  60. Within the base directory, we have one subdirectory for each commit. Within
  61. that is one subdirectory for each board. Within that is the build output for
  62. that commit/board combination.
  63. Buildman also create working directories for each thread, in a .bm-work/
  64. subdirectory in the base dir.
  65. As an example, say we are building branch 'us-net' for boards 'sandbox' and
  66. 'seaboard', and say that us-net has two commits. We will have directories
  67. like this:
  68. us-net/ base directory
  69. 01_g4ed4ebc_net--Add-tftp-speed-/
  70. sandbox/
  71. u-boot.bin
  72. seaboard/
  73. u-boot.bin
  74. 02_g4ed4ebc_net--Check-tftp-comp/
  75. sandbox/
  76. u-boot.bin
  77. seaboard/
  78. u-boot.bin
  79. .bm-work/
  80. 00/ working directory for thread 0 (contains source checkout)
  81. build/ build output
  82. 01/ working directory for thread 1
  83. build/ build output
  84. ...
  85. u-boot/ source directory
  86. .git/ repository
  87. """
  88. """Holds information about a particular error line we are outputing
  89. char: Character representation: '+': error, '-': fixed error, 'w+': warning,
  90. 'w-' = fixed warning
  91. boards: List of Board objects which have line in the error/warning output
  92. errline: The text of the error line
  93. """
  94. ErrLine = collections.namedtuple('ErrLine', 'char,brds,errline')
  95. # Possible build outcomes
  96. OUTCOME_OK, OUTCOME_WARNING, OUTCOME_ERROR, OUTCOME_UNKNOWN = list(range(4))
  97. # Translate a commit subject into a valid filename (and handle unicode)
  98. trans_valid_chars = str.maketrans('/: ', '---')
  99. BASE_CONFIG_FILENAMES = [
  100. 'u-boot.cfg', 'u-boot-spl.cfg', 'u-boot-tpl.cfg'
  101. ]
  102. EXTRA_CONFIG_FILENAMES = [
  103. '.config', '.config-spl', '.config-tpl',
  104. 'autoconf.mk', 'autoconf-spl.mk', 'autoconf-tpl.mk',
  105. 'autoconf.h', 'autoconf-spl.h','autoconf-tpl.h',
  106. ]
  107. class Config:
  108. """Holds information about configuration settings for a board."""
  109. def __init__(self, config_filename, target):
  110. self.target = target
  111. self.config = {}
  112. for fname in config_filename:
  113. self.config[fname] = {}
  114. def add(self, fname, key, value):
  115. self.config[fname][key] = value
  116. def __hash__(self):
  117. val = 0
  118. for fname in self.config:
  119. for key, value in self.config[fname].items():
  120. print(key, value)
  121. val = val ^ hash(key) & hash(value)
  122. return val
  123. class Environment:
  124. """Holds information about environment variables for a board."""
  125. def __init__(self, target):
  126. self.target = target
  127. self.environment = {}
  128. def add(self, key, value):
  129. self.environment[key] = value
  130. class Builder:
  131. """Class for building U-Boot for a particular commit.
  132. Public members: (many should ->private)
  133. already_done: Number of builds already completed
  134. base_dir: Base directory to use for builder
  135. checkout: True to check out source, False to skip that step.
  136. This is used for testing.
  137. col: terminal.Color() object
  138. count: Total number of commits to build, which is the number of commits
  139. multiplied by the number of boards
  140. do_make: Method to call to invoke Make
  141. fail: Number of builds that failed due to error
  142. force_build: Force building even if a build already exists
  143. force_config_on_failure: If a commit fails for a board, disable
  144. incremental building for the next commit we build for that
  145. board, so that we will see all warnings/errors again.
  146. force_build_failures: If a previously-built build (i.e. built on
  147. a previous run of buildman) is marked as failed, rebuild it.
  148. git_dir: Git directory containing source repository
  149. num_jobs: Number of jobs to run at once (passed to make as -j)
  150. num_threads: Number of builder threads to run
  151. out_queue: Queue of results to process
  152. re_make_err: Compiled regular expression for ignore_lines
  153. queue: Queue of jobs to run
  154. threads: List of active threads
  155. toolchains: Toolchains object to use for building
  156. upto: Current commit number we are building (0.count-1)
  157. warned: Number of builds that produced at least one warning
  158. force_reconfig: Reconfigure U-Boot on each comiit. This disables
  159. incremental building, where buildman reconfigures on the first
  160. commit for a baord, and then just does an incremental build for
  161. the following commits. In fact buildman will reconfigure and
  162. retry for any failing commits, so generally the only effect of
  163. this option is to slow things down.
  164. in_tree: Build U-Boot in-tree instead of specifying an output
  165. directory separate from the source code. This option is really
  166. only useful for testing in-tree builds.
  167. work_in_output: Use the output directory as the work directory and
  168. don't write to a separate output directory.
  169. thread_exceptions: List of exceptions raised by thread jobs
  170. no_lto (bool): True to set the NO_LTO flag when building
  171. reproducible_builds (bool): True to set SOURCE_DATE_EPOCH=0 for builds
  172. Private members:
  173. _base_board_dict: Last-summarised Dict of boards
  174. _base_err_lines: Last-summarised list of errors
  175. _base_warn_lines: Last-summarised list of warnings
  176. _build_period_us: Time taken for a single build (float object).
  177. _complete_delay: Expected delay until completion (timedelta)
  178. _next_delay_update: Next time we plan to display a progress update
  179. (datatime)
  180. _show_unknown: Show unknown boards (those not built) in summary
  181. _start_time: Start time for the build
  182. _timestamps: List of timestamps for the completion of the last
  183. last _timestamp_count builds. Each is a datetime object.
  184. _timestamp_count: Number of timestamps to keep in our list.
  185. _working_dir: Base working directory containing all threads
  186. _single_builder: BuilderThread object for the singer builder, if
  187. threading is not being used
  188. _terminated: Thread was terminated due to an error
  189. _restarting_config: True if 'Restart config' is detected in output
  190. _ide: Produce output suitable for an Integrated Development Environment,
  191. i.e. dont emit progress information and put errors/warnings on stderr
  192. """
  193. class Outcome:
  194. """Records a build outcome for a single make invocation
  195. Public Members:
  196. rc: Outcome value (OUTCOME_...)
  197. err_lines: List of error lines or [] if none
  198. sizes: Dictionary of image size information, keyed by filename
  199. - Each value is itself a dictionary containing
  200. values for 'text', 'data' and 'bss', being the integer
  201. size in bytes of each section.
  202. func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
  203. value is itself a dictionary:
  204. key: function name
  205. value: Size of function in bytes
  206. config: Dictionary keyed by filename - e.g. '.config'. Each
  207. value is itself a dictionary:
  208. key: config name
  209. value: config value
  210. environment: Dictionary keyed by environment variable, Each
  211. value is the value of environment variable.
  212. """
  213. def __init__(self, rc, err_lines, sizes, func_sizes, config,
  214. environment):
  215. self.rc = rc
  216. self.err_lines = err_lines
  217. self.sizes = sizes
  218. self.func_sizes = func_sizes
  219. self.config = config
  220. self.environment = environment
  221. def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
  222. gnu_make='make', checkout=True, show_unknown=True, step=1,
  223. no_subdirs=False, full_path=False, verbose_build=False,
  224. mrproper=False, per_board_out_dir=False,
  225. config_only=False, squash_config_y=False,
  226. warnings_as_errors=False, work_in_output=False,
  227. test_thread_exceptions=False, adjust_cfg=None,
  228. allow_missing=False, no_lto=False, reproducible_builds=False,
  229. force_build=False, force_build_failures=False,
  230. force_reconfig=False, in_tree=False,
  231. force_config_on_failure=False, make_func=None):
  232. """Create a new Builder object
  233. Args:
  234. toolchains: Toolchains object to use for building
  235. base_dir: Base directory to use for builder
  236. git_dir: Git directory containing source repository
  237. num_threads: Number of builder threads to run
  238. num_jobs: Number of jobs to run at once (passed to make as -j)
  239. gnu_make: the command name of GNU Make.
  240. checkout: True to check out source, False to skip that step.
  241. This is used for testing.
  242. show_unknown: Show unknown boards (those not built) in summary
  243. step: 1 to process every commit, n to process every nth commit
  244. no_subdirs: Don't create subdirectories when building current
  245. source for a single board
  246. full_path: Return the full path in CROSS_COMPILE and don't set
  247. PATH
  248. verbose_build: Run build with V=1 and don't use 'make -s'
  249. mrproper: Always run 'make mrproper' when configuring
  250. per_board_out_dir: Build in a separate persistent directory per
  251. board rather than a thread-specific directory
  252. config_only: Only configure each build, don't build it
  253. squash_config_y: Convert CONFIG options with the value 'y' to '1'
  254. warnings_as_errors: Treat all compiler warnings as errors
  255. work_in_output: Use the output directory as the work directory and
  256. don't write to a separate output directory.
  257. test_thread_exceptions: Uses for tests only, True to make the
  258. threads raise an exception instead of reporting their result.
  259. This simulates a failure in the code somewhere
  260. adjust_cfg_list (list of str): List of changes to make to .config
  261. file before building. Each is one of (where C is the config
  262. option with or without the CONFIG_ prefix)
  263. C to enable C
  264. ~C to disable C
  265. C=val to set the value of C (val must have quotes if C is
  266. a string Kconfig
  267. allow_missing: Run build with BINMAN_ALLOW_MISSING=1
  268. no_lto (bool): True to set the NO_LTO flag when building
  269. force_build (bool): Rebuild even commits that are already built
  270. force_build_failures (bool): Rebuild commits that have not been
  271. built, or failed to build
  272. force_reconfig (bool): Reconfigure on each commit
  273. in_tree (bool): Bulid in tree instead of out-of-tree
  274. force_config_on_failure (bool): Reconfigure the build before
  275. retrying a failed build
  276. make_func (function): Function to call to run 'make'
  277. """
  278. self.toolchains = toolchains
  279. self.base_dir = base_dir
  280. if work_in_output:
  281. self._working_dir = base_dir
  282. else:
  283. self._working_dir = os.path.join(base_dir, '.bm-work')
  284. self.threads = []
  285. self.do_make = make_func or self.make
  286. self.gnu_make = gnu_make
  287. self.checkout = checkout
  288. self.num_threads = num_threads
  289. self.num_jobs = num_jobs
  290. self.already_done = 0
  291. self.force_build = False
  292. self.git_dir = git_dir
  293. self._show_unknown = show_unknown
  294. self._timestamp_count = 10
  295. self._build_period_us = None
  296. self._complete_delay = None
  297. self._next_delay_update = datetime.now()
  298. self._start_time = datetime.now()
  299. self._step = step
  300. self._error_lines = 0
  301. self.no_subdirs = no_subdirs
  302. self.full_path = full_path
  303. self.verbose_build = verbose_build
  304. self.config_only = config_only
  305. self.squash_config_y = squash_config_y
  306. self.config_filenames = BASE_CONFIG_FILENAMES
  307. self.work_in_output = work_in_output
  308. self.adjust_cfg = adjust_cfg
  309. self.allow_missing = allow_missing
  310. self._ide = False
  311. self.no_lto = no_lto
  312. self.reproducible_builds = reproducible_builds
  313. self.force_build = force_build
  314. self.force_build_failures = force_build_failures
  315. self.force_reconfig = force_reconfig
  316. self.in_tree = in_tree
  317. self.force_config_on_failure = force_config_on_failure
  318. if not self.squash_config_y:
  319. self.config_filenames += EXTRA_CONFIG_FILENAMES
  320. self._terminated = False
  321. self._restarting_config = False
  322. self.warnings_as_errors = warnings_as_errors
  323. self.col = terminal.Color()
  324. self._re_function = re.compile('(.*): In function.*')
  325. self._re_files = re.compile('In file included from.*')
  326. self._re_warning = re.compile('(.*):(\d*):(\d*): warning: .*')
  327. self._re_dtb_warning = re.compile('(.*): Warning .*')
  328. self._re_note = re.compile('(.*):(\d*):(\d*): note: this is the location of the previous.*')
  329. self._re_migration_warning = re.compile(r'^={21} WARNING ={22}\n.*\n=+\n',
  330. re.MULTILINE | re.DOTALL)
  331. self.thread_exceptions = []
  332. self.test_thread_exceptions = test_thread_exceptions
  333. if self.num_threads:
  334. self._single_builder = None
  335. self.queue = queue.Queue()
  336. self.out_queue = queue.Queue()
  337. for i in range(self.num_threads):
  338. t = builderthread.BuilderThread(
  339. self, i, mrproper, per_board_out_dir,
  340. test_exception=test_thread_exceptions)
  341. t.setDaemon(True)
  342. t.start()
  343. self.threads.append(t)
  344. t = builderthread.ResultThread(self)
  345. t.setDaemon(True)
  346. t.start()
  347. self.threads.append(t)
  348. else:
  349. self._single_builder = builderthread.BuilderThread(
  350. self, -1, mrproper, per_board_out_dir)
  351. ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
  352. self.re_make_err = re.compile('|'.join(ignore_lines))
  353. # Handle existing graceful with SIGINT / Ctrl-C
  354. signal.signal(signal.SIGINT, self.signal_handler)
  355. def __del__(self):
  356. """Get rid of all threads created by the builder"""
  357. for t in self.threads:
  358. del t
  359. def signal_handler(self, signal, frame):
  360. sys.exit(1)
  361. def set_display_options(self, show_errors=False, show_sizes=False,
  362. show_detail=False, show_bloat=False,
  363. list_error_boards=False, show_config=False,
  364. show_environment=False, filter_dtb_warnings=False,
  365. filter_migration_warnings=False, ide=False):
  366. """Setup display options for the builder.
  367. Args:
  368. show_errors: True to show summarised error/warning info
  369. show_sizes: Show size deltas
  370. show_detail: Show size delta detail for each board if show_sizes
  371. show_bloat: Show detail for each function
  372. list_error_boards: Show the boards which caused each error/warning
  373. show_config: Show config deltas
  374. show_environment: Show environment deltas
  375. filter_dtb_warnings: Filter out any warnings from the device-tree
  376. compiler
  377. filter_migration_warnings: Filter out any warnings about migrating
  378. a board to driver model
  379. ide: Create output that can be parsed by an IDE. There is no '+' prefix on
  380. error lines and output on stderr stays on stderr.
  381. """
  382. self._show_errors = show_errors
  383. self._show_sizes = show_sizes
  384. self._show_detail = show_detail
  385. self._show_bloat = show_bloat
  386. self._list_error_boards = list_error_boards
  387. self._show_config = show_config
  388. self._show_environment = show_environment
  389. self._filter_dtb_warnings = filter_dtb_warnings
  390. self._filter_migration_warnings = filter_migration_warnings
  391. self._ide = ide
  392. def _add_timestamp(self):
  393. """Add a new timestamp to the list and record the build period.
  394. The build period is the length of time taken to perform a single
  395. build (one board, one commit).
  396. """
  397. now = datetime.now()
  398. self._timestamps.append(now)
  399. count = len(self._timestamps)
  400. delta = self._timestamps[-1] - self._timestamps[0]
  401. seconds = delta.total_seconds()
  402. # If we have enough data, estimate build period (time taken for a
  403. # single build) and therefore completion time.
  404. if count > 1 and self._next_delay_update < now:
  405. self._next_delay_update = now + timedelta(seconds=2)
  406. if seconds > 0:
  407. self._build_period = float(seconds) / count
  408. todo = self.count - self.upto
  409. self._complete_delay = timedelta(microseconds=
  410. self._build_period * todo * 1000000)
  411. # Round it
  412. self._complete_delay -= timedelta(
  413. microseconds=self._complete_delay.microseconds)
  414. if seconds > 60:
  415. self._timestamps.popleft()
  416. count -= 1
  417. def select_commit(self, commit, checkout=True):
  418. """Checkout the selected commit for this build
  419. """
  420. self.commit = commit
  421. if checkout and self.checkout:
  422. gitutil.checkout(commit.hash)
  423. def make(self, commit, brd, stage, cwd, *args, **kwargs):
  424. """Run make
  425. Args:
  426. commit: Commit object that is being built
  427. brd: Board object that is being built
  428. stage: Stage that we are at (mrproper, config, build)
  429. cwd: Directory where make should be run
  430. args: Arguments to pass to make
  431. kwargs: Arguments to pass to command.run_pipe()
  432. """
  433. def check_output(stream, data):
  434. if b'Restart config' in data:
  435. self._restarting_config = True
  436. # If we see 'Restart config' following by multiple errors
  437. if self._restarting_config:
  438. m = RE_NO_DEFAULT.findall(data)
  439. # Number of occurences of each Kconfig item
  440. multiple = [m.count(val) for val in set(m)]
  441. # If any of them occur more than once, we have a loop
  442. if [val for val in multiple if val > 1]:
  443. self._terminated = True
  444. return True
  445. return False
  446. self._restarting_config = False
  447. self._terminated = False
  448. cmd = [self.gnu_make] + list(args)
  449. result = command.run_pipe([cmd], capture=True, capture_stderr=True,
  450. cwd=cwd, raise_on_error=False, infile='/dev/null',
  451. output_func=check_output, **kwargs)
  452. if self._terminated:
  453. # Try to be helpful
  454. result.stderr += '(** did you define an int/hex Kconfig with no default? **)'
  455. if self.verbose_build:
  456. result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout
  457. result.combined = '%s\n' % (' '.join(cmd)) + result.combined
  458. return result
  459. def process_result(self, result):
  460. """Process the result of a build, showing progress information
  461. Args:
  462. result: A CommandResult object, which indicates the result for
  463. a single build
  464. """
  465. col = terminal.Color()
  466. if result:
  467. target = result.brd.target
  468. self.upto += 1
  469. if result.return_code != 0:
  470. self.fail += 1
  471. elif result.stderr:
  472. self.warned += 1
  473. if result.already_done:
  474. self.already_done += 1
  475. if self._verbose:
  476. terminal.print_clear()
  477. boards_selected = {target : result.brd}
  478. self.reset_result_summary(boards_selected)
  479. self.produce_result_summary(result.commit_upto, self.commits,
  480. boards_selected)
  481. else:
  482. target = '(starting)'
  483. # Display separate counts for ok, warned and fail
  484. ok = self.upto - self.warned - self.fail
  485. line = '\r' + self.col.build(self.col.GREEN, '%5d' % ok)
  486. line += self.col.build(self.col.YELLOW, '%5d' % self.warned)
  487. line += self.col.build(self.col.RED, '%5d' % self.fail)
  488. line += ' /%-5d ' % self.count
  489. remaining = self.count - self.upto
  490. if remaining:
  491. line += self.col.build(self.col.MAGENTA, ' -%-5d ' % remaining)
  492. else:
  493. line += ' ' * 8
  494. # Add our current completion time estimate
  495. self._add_timestamp()
  496. if self._complete_delay:
  497. line += '%s : ' % self._complete_delay
  498. line += target
  499. if not self._ide:
  500. terminal.print_clear()
  501. tprint(line, newline=False, limit_to_line=True)
  502. def get_output_dir(self, commit_upto):
  503. """Get the name of the output directory for a commit number
  504. The output directory is typically .../<branch>/<commit>.
  505. Args:
  506. commit_upto: Commit number to use (0..self.count-1)
  507. """
  508. if self.work_in_output:
  509. return self._working_dir
  510. commit_dir = None
  511. if self.commits:
  512. commit = self.commits[commit_upto]
  513. subject = commit.subject.translate(trans_valid_chars)
  514. # See _get_output_space_removals() which parses this name
  515. commit_dir = ('%02d_g%s_%s' % (commit_upto + 1,
  516. commit.hash, subject[:20]))
  517. elif not self.no_subdirs:
  518. commit_dir = 'current'
  519. if not commit_dir:
  520. return self.base_dir
  521. return os.path.join(self.base_dir, commit_dir)
  522. def get_build_dir(self, commit_upto, target):
  523. """Get the name of the build directory for a commit number
  524. The build directory is typically .../<branch>/<commit>/<target>.
  525. Args:
  526. commit_upto: Commit number to use (0..self.count-1)
  527. target: Target name
  528. """
  529. output_dir = self.get_output_dir(commit_upto)
  530. if self.work_in_output:
  531. return output_dir
  532. return os.path.join(output_dir, target)
  533. def get_done_file(self, commit_upto, target):
  534. """Get the name of the done file for a commit number
  535. Args:
  536. commit_upto: Commit number to use (0..self.count-1)
  537. target: Target name
  538. """
  539. return os.path.join(self.get_build_dir(commit_upto, target), 'done')
  540. def get_sizes_file(self, commit_upto, target):
  541. """Get the name of the sizes file for a commit number
  542. Args:
  543. commit_upto: Commit number to use (0..self.count-1)
  544. target: Target name
  545. """
  546. return os.path.join(self.get_build_dir(commit_upto, target), 'sizes')
  547. def get_func_sizes_file(self, commit_upto, target, elf_fname):
  548. """Get the name of the funcsizes file for a commit number and ELF file
  549. Args:
  550. commit_upto: Commit number to use (0..self.count-1)
  551. target: Target name
  552. elf_fname: Filename of elf image
  553. """
  554. return os.path.join(self.get_build_dir(commit_upto, target),
  555. '%s.sizes' % elf_fname.replace('/', '-'))
  556. def get_objdump_file(self, commit_upto, target, elf_fname):
  557. """Get the name of the objdump file for a commit number and ELF file
  558. Args:
  559. commit_upto: Commit number to use (0..self.count-1)
  560. target: Target name
  561. elf_fname: Filename of elf image
  562. """
  563. return os.path.join(self.get_build_dir(commit_upto, target),
  564. '%s.objdump' % elf_fname.replace('/', '-'))
  565. def get_err_file(self, commit_upto, target):
  566. """Get the name of the err file for a commit number
  567. Args:
  568. commit_upto: Commit number to use (0..self.count-1)
  569. target: Target name
  570. """
  571. output_dir = self.get_build_dir(commit_upto, target)
  572. return os.path.join(output_dir, 'err')
  573. def filter_errors(self, lines):
  574. """Filter out errors in which we have no interest
  575. We should probably use map().
  576. Args:
  577. lines: List of error lines, each a string
  578. Returns:
  579. New list with only interesting lines included
  580. """
  581. out_lines = []
  582. if self._filter_migration_warnings:
  583. text = '\n'.join(lines)
  584. text = self._re_migration_warning.sub('', text)
  585. lines = text.splitlines()
  586. for line in lines:
  587. if self.re_make_err.search(line):
  588. continue
  589. if self._filter_dtb_warnings and self._re_dtb_warning.search(line):
  590. continue
  591. out_lines.append(line)
  592. return out_lines
  593. def read_func_sizes(self, fname, fd):
  594. """Read function sizes from the output of 'nm'
  595. Args:
  596. fd: File containing data to read
  597. fname: Filename we are reading from (just for errors)
  598. Returns:
  599. Dictionary containing size of each function in bytes, indexed by
  600. function name.
  601. """
  602. sym = {}
  603. for line in fd.readlines():
  604. line = line.strip()
  605. parts = line.split()
  606. if line and len(parts) == 3:
  607. size, type, name = line.split()
  608. if type in 'tTdDbB':
  609. # function names begin with '.' on 64-bit powerpc
  610. if '.' in name[1:]:
  611. name = 'static.' + name.split('.')[0]
  612. sym[name] = sym.get(name, 0) + int(size, 16)
  613. return sym
  614. def _process_config(self, fname):
  615. """Read in a .config, autoconf.mk or autoconf.h file
  616. This function handles all config file types. It ignores comments and
  617. any #defines which don't start with CONFIG_.
  618. Args:
  619. fname: Filename to read
  620. Returns:
  621. Dictionary:
  622. key: Config name (e.g. CONFIG_DM)
  623. value: Config value (e.g. 1)
  624. """
  625. config = {}
  626. if os.path.exists(fname):
  627. with open(fname) as fd:
  628. for line in fd:
  629. line = line.strip()
  630. if line.startswith('#define'):
  631. values = line[8:].split(' ', 1)
  632. if len(values) > 1:
  633. key, value = values
  634. else:
  635. key = values[0]
  636. value = '1' if self.squash_config_y else ''
  637. if not key.startswith('CONFIG_'):
  638. continue
  639. elif not line or line[0] in ['#', '*', '/']:
  640. continue
  641. else:
  642. key, value = line.split('=', 1)
  643. if self.squash_config_y and value == 'y':
  644. value = '1'
  645. config[key] = value
  646. return config
  647. def _process_environment(self, fname):
  648. """Read in a uboot.env file
  649. This function reads in environment variables from a file.
  650. Args:
  651. fname: Filename to read
  652. Returns:
  653. Dictionary:
  654. key: environment variable (e.g. bootlimit)
  655. value: value of environment variable (e.g. 1)
  656. """
  657. environment = {}
  658. if os.path.exists(fname):
  659. with open(fname) as fd:
  660. for line in fd.read().split('\0'):
  661. try:
  662. key, value = line.split('=', 1)
  663. environment[key] = value
  664. except ValueError:
  665. # ignore lines we can't parse
  666. pass
  667. return environment
  668. def get_build_outcome(self, commit_upto, target, read_func_sizes,
  669. read_config, read_environment):
  670. """Work out the outcome of a build.
  671. Args:
  672. commit_upto: Commit number to check (0..n-1)
  673. target: Target board to check
  674. read_func_sizes: True to read function size information
  675. read_config: True to read .config and autoconf.h files
  676. read_environment: True to read uboot.env files
  677. Returns:
  678. Outcome object
  679. """
  680. done_file = self.get_done_file(commit_upto, target)
  681. sizes_file = self.get_sizes_file(commit_upto, target)
  682. sizes = {}
  683. func_sizes = {}
  684. config = {}
  685. environment = {}
  686. if os.path.exists(done_file):
  687. with open(done_file, 'r') as fd:
  688. try:
  689. return_code = int(fd.readline())
  690. except ValueError:
  691. # The file may be empty due to running out of disk space.
  692. # Try a rebuild
  693. return_code = 1
  694. err_lines = []
  695. err_file = self.get_err_file(commit_upto, target)
  696. if os.path.exists(err_file):
  697. with open(err_file, 'r') as fd:
  698. err_lines = self.filter_errors(fd.readlines())
  699. # Decide whether the build was ok, failed or created warnings
  700. if return_code:
  701. rc = OUTCOME_ERROR
  702. elif len(err_lines):
  703. rc = OUTCOME_WARNING
  704. else:
  705. rc = OUTCOME_OK
  706. # Convert size information to our simple format
  707. if os.path.exists(sizes_file):
  708. with open(sizes_file, 'r') as fd:
  709. for line in fd.readlines():
  710. values = line.split()
  711. rodata = 0
  712. if len(values) > 6:
  713. rodata = int(values[6], 16)
  714. size_dict = {
  715. 'all' : int(values[0]) + int(values[1]) +
  716. int(values[2]),
  717. 'text' : int(values[0]) - rodata,
  718. 'data' : int(values[1]),
  719. 'bss' : int(values[2]),
  720. 'rodata' : rodata,
  721. }
  722. sizes[values[5]] = size_dict
  723. if read_func_sizes:
  724. pattern = self.get_func_sizes_file(commit_upto, target, '*')
  725. for fname in glob.glob(pattern):
  726. with open(fname, 'r') as fd:
  727. dict_name = os.path.basename(fname).replace('.sizes',
  728. '')
  729. func_sizes[dict_name] = self.read_func_sizes(fname, fd)
  730. if read_config:
  731. output_dir = self.get_build_dir(commit_upto, target)
  732. for name in self.config_filenames:
  733. fname = os.path.join(output_dir, name)
  734. config[name] = self._process_config(fname)
  735. if read_environment:
  736. output_dir = self.get_build_dir(commit_upto, target)
  737. fname = os.path.join(output_dir, 'uboot.env')
  738. environment = self._process_environment(fname)
  739. return Builder.Outcome(rc, err_lines, sizes, func_sizes, config,
  740. environment)
  741. return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {}, {})
  742. def get_result_summary(self, boards_selected, commit_upto, read_func_sizes,
  743. read_config, read_environment):
  744. """Calculate a summary of the results of building a commit.
  745. Args:
  746. board_selected: Dict containing boards to summarise
  747. commit_upto: Commit number to summarize (0..self.count-1)
  748. read_func_sizes: True to read function size information
  749. read_config: True to read .config and autoconf.h files
  750. read_environment: True to read uboot.env files
  751. Returns:
  752. Tuple:
  753. Dict containing boards which built this commit:
  754. key: board.target
  755. value: Builder.Outcome object
  756. List containing a summary of error lines
  757. Dict keyed by error line, containing a list of the Board
  758. objects with that error
  759. List containing a summary of warning lines
  760. Dict keyed by error line, containing a list of the Board
  761. objects with that warning
  762. Dictionary keyed by board.target. Each value is a dictionary:
  763. key: filename - e.g. '.config'
  764. value is itself a dictionary:
  765. key: config name
  766. value: config value
  767. Dictionary keyed by board.target. Each value is a dictionary:
  768. key: environment variable
  769. value: value of environment variable
  770. """
  771. def add_line(lines_summary, lines_boards, line, board):
  772. line = line.rstrip()
  773. if line in lines_boards:
  774. lines_boards[line].append(board)
  775. else:
  776. lines_boards[line] = [board]
  777. lines_summary.append(line)
  778. board_dict = {}
  779. err_lines_summary = []
  780. err_lines_boards = {}
  781. warn_lines_summary = []
  782. warn_lines_boards = {}
  783. config = {}
  784. environment = {}
  785. for brd in boards_selected.values():
  786. outcome = self.get_build_outcome(commit_upto, brd.target,
  787. read_func_sizes, read_config,
  788. read_environment)
  789. board_dict[brd.target] = outcome
  790. last_func = None
  791. last_was_warning = False
  792. for line in outcome.err_lines:
  793. if line:
  794. if (self._re_function.match(line) or
  795. self._re_files.match(line)):
  796. last_func = line
  797. else:
  798. is_warning = (self._re_warning.match(line) or
  799. self._re_dtb_warning.match(line))
  800. is_note = self._re_note.match(line)
  801. if is_warning or (last_was_warning and is_note):
  802. if last_func:
  803. add_line(warn_lines_summary, warn_lines_boards,
  804. last_func, brd)
  805. add_line(warn_lines_summary, warn_lines_boards,
  806. line, brd)
  807. else:
  808. if last_func:
  809. add_line(err_lines_summary, err_lines_boards,
  810. last_func, brd)
  811. add_line(err_lines_summary, err_lines_boards,
  812. line, brd)
  813. last_was_warning = is_warning
  814. last_func = None
  815. tconfig = Config(self.config_filenames, brd.target)
  816. for fname in self.config_filenames:
  817. if outcome.config:
  818. for key, value in outcome.config[fname].items():
  819. tconfig.add(fname, key, value)
  820. config[brd.target] = tconfig
  821. tenvironment = Environment(brd.target)
  822. if outcome.environment:
  823. for key, value in outcome.environment.items():
  824. tenvironment.add(key, value)
  825. environment[brd.target] = tenvironment
  826. return (board_dict, err_lines_summary, err_lines_boards,
  827. warn_lines_summary, warn_lines_boards, config, environment)
  828. def add_outcome(self, board_dict, arch_list, changes, char, color):
  829. """Add an output to our list of outcomes for each architecture
  830. This simple function adds failing boards (changes) to the
  831. relevant architecture string, so we can print the results out
  832. sorted by architecture.
  833. Args:
  834. board_dict: Dict containing all boards
  835. arch_list: Dict keyed by arch name. Value is a string containing
  836. a list of board names which failed for that arch.
  837. changes: List of boards to add to arch_list
  838. color: terminal.Colour object
  839. """
  840. done_arch = {}
  841. for target in changes:
  842. if target in board_dict:
  843. arch = board_dict[target].arch
  844. else:
  845. arch = 'unknown'
  846. str = self.col.build(color, ' ' + target)
  847. if not arch in done_arch:
  848. str = ' %s %s' % (self.col.build(color, char), str)
  849. done_arch[arch] = True
  850. if not arch in arch_list:
  851. arch_list[arch] = str
  852. else:
  853. arch_list[arch] += str
  854. def colour_num(self, num):
  855. color = self.col.RED if num > 0 else self.col.GREEN
  856. if num == 0:
  857. return '0'
  858. return self.col.build(color, str(num))
  859. def reset_result_summary(self, board_selected):
  860. """Reset the results summary ready for use.
  861. Set up the base board list to be all those selected, and set the
  862. error lines to empty.
  863. Following this, calls to print_result_summary() will use this
  864. information to work out what has changed.
  865. Args:
  866. board_selected: Dict containing boards to summarise, keyed by
  867. board.target
  868. """
  869. self._base_board_dict = {}
  870. for brd in board_selected:
  871. self._base_board_dict[brd] = Builder.Outcome(0, [], [], {}, {}, {})
  872. self._base_err_lines = []
  873. self._base_warn_lines = []
  874. self._base_err_line_boards = {}
  875. self._base_warn_line_boards = {}
  876. self._base_config = None
  877. self._base_environment = None
  878. def print_func_size_detail(self, fname, old, new):
  879. grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
  880. delta, common = [], {}
  881. for a in old:
  882. if a in new:
  883. common[a] = 1
  884. for name in old:
  885. if name not in common:
  886. remove += 1
  887. down += old[name]
  888. delta.append([-old[name], name])
  889. for name in new:
  890. if name not in common:
  891. add += 1
  892. up += new[name]
  893. delta.append([new[name], name])
  894. for name in common:
  895. diff = new.get(name, 0) - old.get(name, 0)
  896. if diff > 0:
  897. grow, up = grow + 1, up + diff
  898. elif diff < 0:
  899. shrink, down = shrink + 1, down - diff
  900. delta.append([diff, name])
  901. delta.sort()
  902. delta.reverse()
  903. args = [add, -remove, grow, -shrink, up, -down, up - down]
  904. if max(args) == 0 and min(args) == 0:
  905. return
  906. args = [self.colour_num(x) for x in args]
  907. indent = ' ' * 15
  908. tprint('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
  909. tuple([indent, self.col.build(self.col.YELLOW, fname)] + args))
  910. tprint('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
  911. 'delta'))
  912. for diff, name in delta:
  913. if diff:
  914. color = self.col.RED if diff > 0 else self.col.GREEN
  915. msg = '%s %-38s %7s %7s %+7d' % (indent, name,
  916. old.get(name, '-'), new.get(name,'-'), diff)
  917. tprint(msg, colour=color)
  918. def print_size_detail(self, target_list, show_bloat):
  919. """Show details size information for each board
  920. Args:
  921. target_list: List of targets, each a dict containing:
  922. 'target': Target name
  923. 'total_diff': Total difference in bytes across all areas
  924. <part_name>: Difference for that part
  925. show_bloat: Show detail for each function
  926. """
  927. targets_by_diff = sorted(target_list, reverse=True,
  928. key=lambda x: x['_total_diff'])
  929. for result in targets_by_diff:
  930. printed_target = False
  931. for name in sorted(result):
  932. diff = result[name]
  933. if name.startswith('_'):
  934. continue
  935. if diff != 0:
  936. color = self.col.RED if diff > 0 else self.col.GREEN
  937. msg = ' %s %+d' % (name, diff)
  938. if not printed_target:
  939. tprint('%10s %-15s:' % ('', result['_target']),
  940. newline=False)
  941. printed_target = True
  942. tprint(msg, colour=color, newline=False)
  943. if printed_target:
  944. tprint()
  945. if show_bloat:
  946. target = result['_target']
  947. outcome = result['_outcome']
  948. base_outcome = self._base_board_dict[target]
  949. for fname in outcome.func_sizes:
  950. self.print_func_size_detail(fname,
  951. base_outcome.func_sizes[fname],
  952. outcome.func_sizes[fname])
  953. def print_size_summary(self, board_selected, board_dict, show_detail,
  954. show_bloat):
  955. """Print a summary of image sizes broken down by section.
  956. The summary takes the form of one line per architecture. The
  957. line contains deltas for each of the sections (+ means the section
  958. got bigger, - means smaller). The numbers are the average number
  959. of bytes that a board in this section increased by.
  960. For example:
  961. powerpc: (622 boards) text -0.0
  962. arm: (285 boards) text -0.0
  963. Args:
  964. board_selected: Dict containing boards to summarise, keyed by
  965. board.target
  966. board_dict: Dict containing boards for which we built this
  967. commit, keyed by board.target. The value is an Outcome object.
  968. show_detail: Show size delta detail for each board
  969. show_bloat: Show detail for each function
  970. """
  971. arch_list = {}
  972. arch_count = {}
  973. # Calculate changes in size for different image parts
  974. # The previous sizes are in Board.sizes, for each board
  975. for target in board_dict:
  976. if target not in board_selected:
  977. continue
  978. base_sizes = self._base_board_dict[target].sizes
  979. outcome = board_dict[target]
  980. sizes = outcome.sizes
  981. # Loop through the list of images, creating a dict of size
  982. # changes for each image/part. We end up with something like
  983. # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
  984. # which means that U-Boot data increased by 5 bytes and SPL
  985. # text decreased by 4.
  986. err = {'_target' : target}
  987. for image in sizes:
  988. if image in base_sizes:
  989. base_image = base_sizes[image]
  990. # Loop through the text, data, bss parts
  991. for part in sorted(sizes[image]):
  992. diff = sizes[image][part] - base_image[part]
  993. col = None
  994. if diff:
  995. if image == 'u-boot':
  996. name = part
  997. else:
  998. name = image + ':' + part
  999. err[name] = diff
  1000. arch = board_selected[target].arch
  1001. if not arch in arch_count:
  1002. arch_count[arch] = 1
  1003. else:
  1004. arch_count[arch] += 1
  1005. if not sizes:
  1006. pass # Only add to our list when we have some stats
  1007. elif not arch in arch_list:
  1008. arch_list[arch] = [err]
  1009. else:
  1010. arch_list[arch].append(err)
  1011. # We now have a list of image size changes sorted by arch
  1012. # Print out a summary of these
  1013. for arch, target_list in arch_list.items():
  1014. # Get total difference for each type
  1015. totals = {}
  1016. for result in target_list:
  1017. total = 0
  1018. for name, diff in result.items():
  1019. if name.startswith('_'):
  1020. continue
  1021. total += diff
  1022. if name in totals:
  1023. totals[name] += diff
  1024. else:
  1025. totals[name] = diff
  1026. result['_total_diff'] = total
  1027. result['_outcome'] = board_dict[result['_target']]
  1028. count = len(target_list)
  1029. printed_arch = False
  1030. for name in sorted(totals):
  1031. diff = totals[name]
  1032. if diff:
  1033. # Display the average difference in this name for this
  1034. # architecture
  1035. avg_diff = float(diff) / count
  1036. color = self.col.RED if avg_diff > 0 else self.col.GREEN
  1037. msg = ' %s %+1.1f' % (name, avg_diff)
  1038. if not printed_arch:
  1039. tprint('%10s: (for %d/%d boards)' % (arch, count,
  1040. arch_count[arch]), newline=False)
  1041. printed_arch = True
  1042. tprint(msg, colour=color, newline=False)
  1043. if printed_arch:
  1044. tprint()
  1045. if show_detail:
  1046. self.print_size_detail(target_list, show_bloat)
  1047. def print_result_summary(self, board_selected, board_dict, err_lines,
  1048. err_line_boards, warn_lines, warn_line_boards,
  1049. config, environment, show_sizes, show_detail,
  1050. show_bloat, show_config, show_environment):
  1051. """Compare results with the base results and display delta.
  1052. Only boards mentioned in board_selected will be considered. This
  1053. function is intended to be called repeatedly with the results of
  1054. each commit. It therefore shows a 'diff' between what it saw in
  1055. the last call and what it sees now.
  1056. Args:
  1057. board_selected: Dict containing boards to summarise, keyed by
  1058. board.target
  1059. board_dict: Dict containing boards for which we built this
  1060. commit, keyed by board.target. The value is an Outcome object.
  1061. err_lines: A list of errors for this commit, or [] if there is
  1062. none, or we don't want to print errors
  1063. err_line_boards: Dict keyed by error line, containing a list of
  1064. the Board objects with that error
  1065. warn_lines: A list of warnings for this commit, or [] if there is
  1066. none, or we don't want to print errors
  1067. warn_line_boards: Dict keyed by warning line, containing a list of
  1068. the Board objects with that warning
  1069. config: Dictionary keyed by filename - e.g. '.config'. Each
  1070. value is itself a dictionary:
  1071. key: config name
  1072. value: config value
  1073. environment: Dictionary keyed by environment variable, Each
  1074. value is the value of environment variable.
  1075. show_sizes: Show image size deltas
  1076. show_detail: Show size delta detail for each board if show_sizes
  1077. show_bloat: Show detail for each function
  1078. show_config: Show config changes
  1079. show_environment: Show environment changes
  1080. """
  1081. def _board_list(line, line_boards):
  1082. """Helper function to get a line of boards containing a line
  1083. Args:
  1084. line: Error line to search for
  1085. line_boards: boards to search, each a Board
  1086. Return:
  1087. List of boards with that error line, or [] if the user has not
  1088. requested such a list
  1089. """
  1090. brds = []
  1091. board_set = set()
  1092. if self._list_error_boards:
  1093. for brd in line_boards[line]:
  1094. if not brd in board_set:
  1095. brds.append(brd)
  1096. board_set.add(brd)
  1097. return brds
  1098. def _calc_error_delta(base_lines, base_line_boards, lines, line_boards,
  1099. char):
  1100. """Calculate the required output based on changes in errors
  1101. Args:
  1102. base_lines: List of errors/warnings for previous commit
  1103. base_line_boards: Dict keyed by error line, containing a list
  1104. of the Board objects with that error in the previous commit
  1105. lines: List of errors/warning for this commit, each a str
  1106. line_boards: Dict keyed by error line, containing a list
  1107. of the Board objects with that error in this commit
  1108. char: Character representing error ('') or warning ('w'). The
  1109. broken ('+') or fixed ('-') characters are added in this
  1110. function
  1111. Returns:
  1112. Tuple
  1113. List of ErrLine objects for 'better' lines
  1114. List of ErrLine objects for 'worse' lines
  1115. """
  1116. better_lines = []
  1117. worse_lines = []
  1118. for line in lines:
  1119. if line not in base_lines:
  1120. errline = ErrLine(char + '+', _board_list(line, line_boards),
  1121. line)
  1122. worse_lines.append(errline)
  1123. for line in base_lines:
  1124. if line not in lines:
  1125. errline = ErrLine(char + '-',
  1126. _board_list(line, base_line_boards), line)
  1127. better_lines.append(errline)
  1128. return better_lines, worse_lines
  1129. def _calc_config(delta, name, config):
  1130. """Calculate configuration changes
  1131. Args:
  1132. delta: Type of the delta, e.g. '+'
  1133. name: name of the file which changed (e.g. .config)
  1134. config: configuration change dictionary
  1135. key: config name
  1136. value: config value
  1137. Returns:
  1138. String containing the configuration changes which can be
  1139. printed
  1140. """
  1141. out = ''
  1142. for key in sorted(config.keys()):
  1143. out += '%s=%s ' % (key, config[key])
  1144. return '%s %s: %s' % (delta, name, out)
  1145. def _add_config(lines, name, config_plus, config_minus, config_change):
  1146. """Add changes in configuration to a list
  1147. Args:
  1148. lines: list to add to
  1149. name: config file name
  1150. config_plus: configurations added, dictionary
  1151. key: config name
  1152. value: config value
  1153. config_minus: configurations removed, dictionary
  1154. key: config name
  1155. value: config value
  1156. config_change: configurations changed, dictionary
  1157. key: config name
  1158. value: config value
  1159. """
  1160. if config_plus:
  1161. lines.append(_calc_config('+', name, config_plus))
  1162. if config_minus:
  1163. lines.append(_calc_config('-', name, config_minus))
  1164. if config_change:
  1165. lines.append(_calc_config('c', name, config_change))
  1166. def _output_config_info(lines):
  1167. for line in lines:
  1168. if not line:
  1169. continue
  1170. if line[0] == '+':
  1171. col = self.col.GREEN
  1172. elif line[0] == '-':
  1173. col = self.col.RED
  1174. elif line[0] == 'c':
  1175. col = self.col.YELLOW
  1176. tprint(' ' + line, newline=True, colour=col)
  1177. def _output_err_lines(err_lines, colour):
  1178. """Output the line of error/warning lines, if not empty
  1179. Also increments self._error_lines if err_lines not empty
  1180. Args:
  1181. err_lines: List of ErrLine objects, each an error or warning
  1182. line, possibly including a list of boards with that
  1183. error/warning
  1184. colour: Colour to use for output
  1185. """
  1186. if err_lines:
  1187. out_list = []
  1188. for line in err_lines:
  1189. names = [brd.target for brd in line.brds]
  1190. board_str = ' '.join(names) if names else ''
  1191. if board_str:
  1192. out = self.col.build(colour, line.char + '(')
  1193. out += self.col.build(self.col.MAGENTA, board_str,
  1194. bright=False)
  1195. out += self.col.build(colour, ') %s' % line.errline)
  1196. else:
  1197. out = self.col.build(colour, line.char + line.errline)
  1198. out_list.append(out)
  1199. tprint('\n'.join(out_list))
  1200. self._error_lines += 1
  1201. ok_boards = [] # List of boards fixed since last commit
  1202. warn_boards = [] # List of boards with warnings since last commit
  1203. err_boards = [] # List of new broken boards since last commit
  1204. new_boards = [] # List of boards that didn't exist last time
  1205. unknown_boards = [] # List of boards that were not built
  1206. for target in board_dict:
  1207. if target not in board_selected:
  1208. continue
  1209. # If the board was built last time, add its outcome to a list
  1210. if target in self._base_board_dict:
  1211. base_outcome = self._base_board_dict[target].rc
  1212. outcome = board_dict[target]
  1213. if outcome.rc == OUTCOME_UNKNOWN:
  1214. unknown_boards.append(target)
  1215. elif outcome.rc < base_outcome:
  1216. if outcome.rc == OUTCOME_WARNING:
  1217. warn_boards.append(target)
  1218. else:
  1219. ok_boards.append(target)
  1220. elif outcome.rc > base_outcome:
  1221. if outcome.rc == OUTCOME_WARNING:
  1222. warn_boards.append(target)
  1223. else:
  1224. err_boards.append(target)
  1225. else:
  1226. new_boards.append(target)
  1227. # Get a list of errors and warnings that have appeared, and disappeared
  1228. better_err, worse_err = _calc_error_delta(self._base_err_lines,
  1229. self._base_err_line_boards, err_lines, err_line_boards, '')
  1230. better_warn, worse_warn = _calc_error_delta(self._base_warn_lines,
  1231. self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
  1232. # For the IDE mode, print out all the output
  1233. if self._ide:
  1234. outcome = board_dict[target]
  1235. for line in outcome.err_lines:
  1236. sys.stderr.write(line)
  1237. # Display results by arch
  1238. elif any((ok_boards, warn_boards, err_boards, unknown_boards, new_boards,
  1239. worse_err, better_err, worse_warn, better_warn)):
  1240. arch_list = {}
  1241. self.add_outcome(board_selected, arch_list, ok_boards, '',
  1242. self.col.GREEN)
  1243. self.add_outcome(board_selected, arch_list, warn_boards, 'w+',
  1244. self.col.YELLOW)
  1245. self.add_outcome(board_selected, arch_list, err_boards, '+',
  1246. self.col.RED)
  1247. self.add_outcome(board_selected, arch_list, new_boards, '*', self.col.BLUE)
  1248. if self._show_unknown:
  1249. self.add_outcome(board_selected, arch_list, unknown_boards, '?',
  1250. self.col.MAGENTA)
  1251. for arch, target_list in arch_list.items():
  1252. tprint('%10s: %s' % (arch, target_list))
  1253. self._error_lines += 1
  1254. _output_err_lines(better_err, colour=self.col.GREEN)
  1255. _output_err_lines(worse_err, colour=self.col.RED)
  1256. _output_err_lines(better_warn, colour=self.col.CYAN)
  1257. _output_err_lines(worse_warn, colour=self.col.YELLOW)
  1258. if show_sizes:
  1259. self.print_size_summary(board_selected, board_dict, show_detail,
  1260. show_bloat)
  1261. if show_environment and self._base_environment:
  1262. lines = []
  1263. for target in board_dict:
  1264. if target not in board_selected:
  1265. continue
  1266. tbase = self._base_environment[target]
  1267. tenvironment = environment[target]
  1268. environment_plus = {}
  1269. environment_minus = {}
  1270. environment_change = {}
  1271. base = tbase.environment
  1272. for key, value in tenvironment.environment.items():
  1273. if key not in base:
  1274. environment_plus[key] = value
  1275. for key, value in base.items():
  1276. if key not in tenvironment.environment:
  1277. environment_minus[key] = value
  1278. for key, value in base.items():
  1279. new_value = tenvironment.environment.get(key)
  1280. if new_value and value != new_value:
  1281. desc = '%s -> %s' % (value, new_value)
  1282. environment_change[key] = desc
  1283. _add_config(lines, target, environment_plus, environment_minus,
  1284. environment_change)
  1285. _output_config_info(lines)
  1286. if show_config and self._base_config:
  1287. summary = {}
  1288. arch_config_plus = {}
  1289. arch_config_minus = {}
  1290. arch_config_change = {}
  1291. arch_list = []
  1292. for target in board_dict:
  1293. if target not in board_selected:
  1294. continue
  1295. arch = board_selected[target].arch
  1296. if arch not in arch_list:
  1297. arch_list.append(arch)
  1298. for arch in arch_list:
  1299. arch_config_plus[arch] = {}
  1300. arch_config_minus[arch] = {}
  1301. arch_config_change[arch] = {}
  1302. for name in self.config_filenames:
  1303. arch_config_plus[arch][name] = {}
  1304. arch_config_minus[arch][name] = {}
  1305. arch_config_change[arch][name] = {}
  1306. for target in board_dict:
  1307. if target not in board_selected:
  1308. continue
  1309. arch = board_selected[target].arch
  1310. all_config_plus = {}
  1311. all_config_minus = {}
  1312. all_config_change = {}
  1313. tbase = self._base_config[target]
  1314. tconfig = config[target]
  1315. lines = []
  1316. for name in self.config_filenames:
  1317. if not tconfig.config[name]:
  1318. continue
  1319. config_plus = {}
  1320. config_minus = {}
  1321. config_change = {}
  1322. base = tbase.config[name]
  1323. for key, value in tconfig.config[name].items():
  1324. if key not in base:
  1325. config_plus[key] = value
  1326. all_config_plus[key] = value
  1327. for key, value in base.items():
  1328. if key not in tconfig.config[name]:
  1329. config_minus[key] = value
  1330. all_config_minus[key] = value
  1331. for key, value in base.items():
  1332. new_value = tconfig.config.get(key)
  1333. if new_value and value != new_value:
  1334. desc = '%s -> %s' % (value, new_value)
  1335. config_change[key] = desc
  1336. all_config_change[key] = desc
  1337. arch_config_plus[arch][name].update(config_plus)
  1338. arch_config_minus[arch][name].update(config_minus)
  1339. arch_config_change[arch][name].update(config_change)
  1340. _add_config(lines, name, config_plus, config_minus,
  1341. config_change)
  1342. _add_config(lines, 'all', all_config_plus, all_config_minus,
  1343. all_config_change)
  1344. summary[target] = '\n'.join(lines)
  1345. lines_by_target = {}
  1346. for target, lines in summary.items():
  1347. if lines in lines_by_target:
  1348. lines_by_target[lines].append(target)
  1349. else:
  1350. lines_by_target[lines] = [target]
  1351. for arch in arch_list:
  1352. lines = []
  1353. all_plus = {}
  1354. all_minus = {}
  1355. all_change = {}
  1356. for name in self.config_filenames:
  1357. all_plus.update(arch_config_plus[arch][name])
  1358. all_minus.update(arch_config_minus[arch][name])
  1359. all_change.update(arch_config_change[arch][name])
  1360. _add_config(lines, name, arch_config_plus[arch][name],
  1361. arch_config_minus[arch][name],
  1362. arch_config_change[arch][name])
  1363. _add_config(lines, 'all', all_plus, all_minus, all_change)
  1364. #arch_summary[target] = '\n'.join(lines)
  1365. if lines:
  1366. tprint('%s:' % arch)
  1367. _output_config_info(lines)
  1368. for lines, targets in lines_by_target.items():
  1369. if not lines:
  1370. continue
  1371. tprint('%s :' % ' '.join(sorted(targets)))
  1372. _output_config_info(lines.split('\n'))
  1373. # Save our updated information for the next call to this function
  1374. self._base_board_dict = board_dict
  1375. self._base_err_lines = err_lines
  1376. self._base_warn_lines = warn_lines
  1377. self._base_err_line_boards = err_line_boards
  1378. self._base_warn_line_boards = warn_line_boards
  1379. self._base_config = config
  1380. self._base_environment = environment
  1381. # Get a list of boards that did not get built, if needed
  1382. not_built = []
  1383. for brd in board_selected:
  1384. if not brd in board_dict:
  1385. not_built.append(brd)
  1386. if not_built:
  1387. tprint("Boards not built (%d): %s" % (len(not_built),
  1388. ', '.join(not_built)))
  1389. def produce_result_summary(self, commit_upto, commits, board_selected):
  1390. (board_dict, err_lines, err_line_boards, warn_lines,
  1391. warn_line_boards, config, environment) = self.get_result_summary(
  1392. board_selected, commit_upto,
  1393. read_func_sizes=self._show_bloat,
  1394. read_config=self._show_config,
  1395. read_environment=self._show_environment)
  1396. if commits:
  1397. msg = '%02d: %s' % (commit_upto + 1,
  1398. commits[commit_upto].subject)
  1399. tprint(msg, colour=self.col.BLUE)
  1400. self.print_result_summary(board_selected, board_dict,
  1401. err_lines if self._show_errors else [], err_line_boards,
  1402. warn_lines if self._show_errors else [], warn_line_boards,
  1403. config, environment, self._show_sizes, self._show_detail,
  1404. self._show_bloat, self._show_config, self._show_environment)
  1405. def show_summary(self, commits, board_selected):
  1406. """Show a build summary for U-Boot for a given board list.
  1407. Reset the result summary, then repeatedly call GetResultSummary on
  1408. each commit's results, then display the differences we see.
  1409. Args:
  1410. commit: Commit objects to summarise
  1411. board_selected: Dict containing boards to summarise
  1412. """
  1413. self.commit_count = len(commits) if commits else 1
  1414. self.commits = commits
  1415. self.reset_result_summary(board_selected)
  1416. self._error_lines = 0
  1417. for commit_upto in range(0, self.commit_count, self._step):
  1418. self.produce_result_summary(commit_upto, commits, board_selected)
  1419. if not self._error_lines:
  1420. tprint('(no errors to report)', colour=self.col.GREEN)
  1421. def setup_build(self, board_selected, commits):
  1422. """Set up ready to start a build.
  1423. Args:
  1424. board_selected: Selected boards to build
  1425. commits: Selected commits to build
  1426. """
  1427. # First work out how many commits we will build
  1428. count = (self.commit_count + self._step - 1) // self._step
  1429. self.count = len(board_selected) * count
  1430. self.upto = self.warned = self.fail = 0
  1431. self._timestamps = collections.deque()
  1432. def get_thread_dir(self, thread_num):
  1433. """Get the directory path to the working dir for a thread.
  1434. Args:
  1435. thread_num: Number of thread to check (-1 for main process, which
  1436. is treated as 0)
  1437. """
  1438. if self.work_in_output:
  1439. return self._working_dir
  1440. return os.path.join(self._working_dir, '%02d' % max(thread_num, 0))
  1441. def _prepare_thread(self, thread_num, setup_git):
  1442. """Prepare the working directory for a thread.
  1443. This clones or fetches the repo into the thread's work directory.
  1444. Optionally, it can create a linked working tree of the repo in the
  1445. thread's work directory instead.
  1446. Args:
  1447. thread_num: Thread number (0, 1, ...)
  1448. setup_git:
  1449. 'clone' to set up a git clone
  1450. 'worktree' to set up a git worktree
  1451. """
  1452. thread_dir = self.get_thread_dir(thread_num)
  1453. builderthread.mkdir(thread_dir)
  1454. git_dir = os.path.join(thread_dir, '.git')
  1455. # Create a worktree or a git repo clone for this thread if it
  1456. # doesn't already exist
  1457. if setup_git and self.git_dir:
  1458. src_dir = os.path.abspath(self.git_dir)
  1459. if os.path.isdir(git_dir):
  1460. # This is a clone of the src_dir repo, we can keep using
  1461. # it but need to fetch from src_dir.
  1462. tprint('\rFetching repo for thread %d' % thread_num,
  1463. newline=False)
  1464. gitutil.fetch(git_dir, thread_dir)
  1465. terminal.print_clear()
  1466. elif os.path.isfile(git_dir):
  1467. # This is a worktree of the src_dir repo, we don't need to
  1468. # create it again or update it in any way.
  1469. pass
  1470. elif os.path.exists(git_dir):
  1471. # Don't know what could trigger this, but we probably
  1472. # can't create a git worktree/clone here.
  1473. raise ValueError('Git dir %s exists, but is not a file '
  1474. 'or a directory.' % git_dir)
  1475. elif setup_git == 'worktree':
  1476. tprint('\rChecking out worktree for thread %d' % thread_num,
  1477. newline=False)
  1478. gitutil.add_worktree(src_dir, thread_dir)
  1479. terminal.print_clear()
  1480. elif setup_git == 'clone' or setup_git == True:
  1481. tprint('\rCloning repo for thread %d' % thread_num,
  1482. newline=False)
  1483. gitutil.clone(src_dir, thread_dir)
  1484. terminal.print_clear()
  1485. else:
  1486. raise ValueError("Can't setup git repo with %s." % setup_git)
  1487. def _prepare_working_space(self, max_threads, setup_git):
  1488. """Prepare the working directory for use.
  1489. Set up the git repo for each thread. Creates a linked working tree
  1490. if git-worktree is available, or clones the repo if it isn't.
  1491. Args:
  1492. max_threads: Maximum number of threads we expect to need. If 0 then
  1493. 1 is set up, since the main process still needs somewhere to
  1494. work
  1495. setup_git: True to set up a git worktree or a git clone
  1496. """
  1497. builderthread.mkdir(self._working_dir)
  1498. if setup_git and self.git_dir:
  1499. src_dir = os.path.abspath(self.git_dir)
  1500. if gitutil.check_worktree_is_available(src_dir):
  1501. setup_git = 'worktree'
  1502. # If we previously added a worktree but the directory for it
  1503. # got deleted, we need to prune its files from the repo so
  1504. # that we can check out another in its place.
  1505. gitutil.prune_worktrees(src_dir)
  1506. else:
  1507. setup_git = 'clone'
  1508. # Always do at least one thread
  1509. for thread in range(max(max_threads, 1)):
  1510. self._prepare_thread(thread, setup_git)
  1511. def _get_output_space_removals(self):
  1512. """Get the output directories ready to receive files.
  1513. Figure out what needs to be deleted in the output directory before it
  1514. can be used. We only delete old buildman directories which have the
  1515. expected name pattern. See get_output_dir().
  1516. Returns:
  1517. List of full paths of directories to remove
  1518. """
  1519. if not self.commits:
  1520. return
  1521. dir_list = []
  1522. for commit_upto in range(self.commit_count):
  1523. dir_list.append(self.get_output_dir(commit_upto))
  1524. to_remove = []
  1525. for dirname in glob.glob(os.path.join(self.base_dir, '*')):
  1526. if dirname not in dir_list:
  1527. leaf = dirname[len(self.base_dir) + 1:]
  1528. m = re.match('[0-9]+_g[0-9a-f]+_.*', leaf)
  1529. if m:
  1530. to_remove.append(dirname)
  1531. return to_remove
  1532. def _prepare_output_space(self):
  1533. """Get the output directories ready to receive files.
  1534. We delete any output directories which look like ones we need to
  1535. create. Having left over directories is confusing when the user wants
  1536. to check the output manually.
  1537. """
  1538. to_remove = self._get_output_space_removals()
  1539. if to_remove:
  1540. tprint('Removing %d old build directories...' % len(to_remove),
  1541. newline=False)
  1542. for dirname in to_remove:
  1543. shutil.rmtree(dirname)
  1544. terminal.print_clear()
  1545. def build_boards(self, commits, board_selected, keep_outputs, verbose):
  1546. """Build all commits for a list of boards
  1547. Args:
  1548. commits: List of commits to be build, each a Commit object
  1549. boards_selected: Dict of selected boards, key is target name,
  1550. value is Board object
  1551. keep_outputs: True to save build output files
  1552. verbose: Display build results as they are completed
  1553. Returns:
  1554. Tuple containing:
  1555. - number of boards that failed to build
  1556. - number of boards that issued warnings
  1557. - list of thread exceptions raised
  1558. """
  1559. self.commit_count = len(commits) if commits else 1
  1560. self.commits = commits
  1561. self._verbose = verbose
  1562. self.reset_result_summary(board_selected)
  1563. builderthread.mkdir(self.base_dir, parents = True)
  1564. self._prepare_working_space(min(self.num_threads, len(board_selected)),
  1565. commits is not None)
  1566. self._prepare_output_space()
  1567. if not self._ide:
  1568. tprint('\rStarting build...', newline=False)
  1569. self.setup_build(board_selected, commits)
  1570. self.process_result(None)
  1571. self.thread_exceptions = []
  1572. # Create jobs to build all commits for each board
  1573. for brd in board_selected.values():
  1574. job = builderthread.BuilderJob()
  1575. job.brd = brd
  1576. job.commits = commits
  1577. job.keep_outputs = keep_outputs
  1578. job.work_in_output = self.work_in_output
  1579. job.adjust_cfg = self.adjust_cfg
  1580. job.step = self._step
  1581. if self.num_threads:
  1582. self.queue.put(job)
  1583. else:
  1584. self._single_builder.run_job(job)
  1585. if self.num_threads:
  1586. term = threading.Thread(target=self.queue.join)
  1587. term.setDaemon(True)
  1588. term.start()
  1589. while term.is_alive():
  1590. term.join(100)
  1591. # Wait until we have processed all output
  1592. self.out_queue.join()
  1593. if not self._ide:
  1594. tprint()
  1595. msg = 'Completed: %d total built' % self.count
  1596. if self.already_done:
  1597. msg += ' (%d previously' % self.already_done
  1598. if self.already_done != self.count:
  1599. msg += ', %d newly' % (self.count - self.already_done)
  1600. msg += ')'
  1601. duration = datetime.now() - self._start_time
  1602. if duration > timedelta(microseconds=1000000):
  1603. if duration.microseconds >= 500000:
  1604. duration = duration + timedelta(seconds=1)
  1605. duration = duration - timedelta(microseconds=duration.microseconds)
  1606. rate = float(self.count) / duration.total_seconds()
  1607. msg += ', duration %s, rate %1.2f' % (duration, rate)
  1608. tprint(msg)
  1609. if self.thread_exceptions:
  1610. tprint('Failed: %d thread exceptions' % len(self.thread_exceptions),
  1611. colour=self.col.RED)
  1612. return (self.fail, self.warned, self.thread_exceptions)