bsp.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. # Copyright (C) 2017 Intel Corporation
  2. #
  3. # SPDX-License-Identifier: MIT
  4. #
  5. import unittest
  6. from checklayer import LayerType, get_signatures, check_command, get_depgraph
  7. from checklayer.case import OECheckLayerTestCase
  8. class BSPCheckLayer(OECheckLayerTestCase):
  9. @classmethod
  10. def setUpClass(self):
  11. if self.tc.layer['type'] not in (LayerType.BSP, LayerType.CORE):
  12. raise unittest.SkipTest("BSPCheckLayer: Layer %s isn't BSP one." %\
  13. self.tc.layer['name'])
  14. def test_bsp_defines_machines(self):
  15. self.assertTrue(self.tc.layer['conf']['machines'],
  16. "Layer is BSP but doesn't defines machines.")
  17. def test_bsp_no_set_machine(self):
  18. from oeqa.utils.commands import get_bb_var
  19. machine = get_bb_var('MACHINE')
  20. self.assertEqual(self.td['bbvars']['MACHINE'], machine,
  21. msg="Layer %s modified machine %s -> %s" % \
  22. (self.tc.layer['name'], self.td['bbvars']['MACHINE'], machine))
  23. def test_machine_world(self):
  24. '''
  25. "bitbake world" is expected to work regardless which machine is selected.
  26. BSP layers sometimes break that by enabling a recipe for a certain machine
  27. without checking whether that recipe actually can be built in the current
  28. distro configuration (for example, OpenGL might not enabled).
  29. This test iterates over all machines. It would be nicer to instantiate
  30. it once per machine. It merely checks for errors during parse
  31. time. It does not actually attempt to build anything.
  32. '''
  33. if not self.td['machines']:
  34. self.skipTest('No machines set with --machines.')
  35. msg = []
  36. for machine in self.td['machines']:
  37. # In contrast to test_machine_signatures() below, errors are fatal here.
  38. try:
  39. get_signatures(self.td['builddir'], failsafe=False, machine=machine)
  40. except RuntimeError as ex:
  41. msg.append(str(ex))
  42. if msg:
  43. msg.insert(0, 'The following machines broke a world build:')
  44. self.fail('\n'.join(msg))
  45. def test_machine_signatures(self):
  46. '''
  47. Selecting a machine may only affect the signature of tasks that are specific
  48. to that machine. In other words, when MACHINE=A and MACHINE=B share a recipe
  49. foo and the output of foo, then both machine configurations must build foo
  50. in exactly the same way. Otherwise it is not possible to use both machines
  51. in the same distribution.
  52. This criteria can only be tested by testing different machines in combination,
  53. i.e. one main layer, potentially several additional BSP layers and an explicit
  54. choice of machines:
  55. yocto-check-layer --additional-layers .../meta-intel --machines intel-corei7-64 imx6slevk -- .../meta-freescale
  56. '''
  57. if not self.td['machines']:
  58. self.skipTest('No machines set with --machines.')
  59. # Collect signatures for all machines that we are testing
  60. # and merge that into a hash:
  61. # tune -> task -> signature -> list of machines with that combination
  62. #
  63. # It is an error if any tune/task pair has more than one signature,
  64. # because that implies that the machines that caused those different
  65. # signatures do not agree on how to execute the task.
  66. tunes = {}
  67. # Preserve ordering of machines as chosen by the user.
  68. for machine in self.td['machines']:
  69. curr_sigs, tune2tasks = get_signatures(self.td['builddir'], failsafe=True, machine=machine)
  70. # Invert the tune -> [tasks] mapping.
  71. tasks2tune = {}
  72. for tune, tasks in tune2tasks.items():
  73. for task in tasks:
  74. tasks2tune[task] = tune
  75. for task, sighash in curr_sigs.items():
  76. tunes.setdefault(tasks2tune[task], {}).setdefault(task, {}).setdefault(sighash, []).append(machine)
  77. msg = []
  78. pruned = 0
  79. last_line_key = None
  80. # do_fetch, do_unpack, ..., do_build
  81. taskname_list = []
  82. if tunes:
  83. # The output below is most useful when we start with tasks that are at
  84. # the bottom of the dependency chain, i.e. those that run first. If
  85. # those tasks differ, the rest also does.
  86. #
  87. # To get an ordering of tasks, we do a topological sort of the entire
  88. # depgraph for the base configuration, then on-the-fly flatten that list by stripping
  89. # out the recipe names and removing duplicates. The base configuration
  90. # is not necessarily representative, but should be close enough. Tasks
  91. # that were not encountered get a default priority.
  92. depgraph = get_depgraph()
  93. depends = depgraph['tdepends']
  94. WHITE = 1
  95. GRAY = 2
  96. BLACK = 3
  97. color = {}
  98. found = set()
  99. def visit(task):
  100. color[task] = GRAY
  101. for dep in depends.get(task, ()):
  102. if color.setdefault(dep, WHITE) == WHITE:
  103. visit(dep)
  104. color[task] = BLACK
  105. pn, taskname = task.rsplit('.', 1)
  106. if taskname not in found:
  107. taskname_list.append(taskname)
  108. found.add(taskname)
  109. for task in depends.keys():
  110. if color.setdefault(task, WHITE) == WHITE:
  111. visit(task)
  112. taskname_order = dict([(task, index) for index, task in enumerate(taskname_list) ])
  113. def task_key(task):
  114. pn, taskname = task.rsplit(':', 1)
  115. return (pn, taskname_order.get(taskname, len(taskname_list)), taskname)
  116. for tune in sorted(tunes.keys()):
  117. tasks = tunes[tune]
  118. # As for test_signatures it would be nicer to sort tasks
  119. # by dependencies here, but that is harder because we have
  120. # to report on tasks from different machines, which might
  121. # have different dependencies. We resort to pruning the
  122. # output by reporting only one task per recipe if the set
  123. # of machines matches.
  124. #
  125. # "bitbake-diffsigs -t -s" is intelligent enough to print
  126. # diffs recursively, so often it does not matter that much
  127. # if we don't pick the underlying difference
  128. # here. However, sometimes recursion fails
  129. # (https://bugzilla.yoctoproject.org/show_bug.cgi?id=6428).
  130. #
  131. # To mitigate that a bit, we use a hard-coded ordering of
  132. # tasks that represents how they normally run and prefer
  133. # to print the ones that run first.
  134. for task in sorted(tasks.keys(), key=task_key):
  135. signatures = tasks[task]
  136. # do_build can be ignored: it is know to have
  137. # different signatures in some cases, for example in
  138. # the allarch ca-certificates due to RDEPENDS=openssl.
  139. # That particular dependency is marked via
  140. # SIGGEN_EXCLUDE_SAFE_RECIPE_DEPS, but still shows up
  141. # in the sstate signature hash because filtering it
  142. # out would be hard and running do_build multiple
  143. # times doesn't really matter.
  144. if len(signatures.keys()) > 1 and \
  145. not task.endswith(':do_build'):
  146. # Error!
  147. #
  148. # Sort signatures by machines, because the hex values don't mean anything.
  149. # => all-arch adwaita-icon-theme:do_build: 1234... (beaglebone, qemux86) != abcdf... (qemux86-64)
  150. #
  151. # Skip the line if it is covered already by the predecessor (same pn, same sets of machines).
  152. pn, taskname = task.rsplit(':', 1)
  153. next_line_key = (pn, sorted(signatures.values()))
  154. if next_line_key != last_line_key:
  155. line = ' %s %s: ' % (tune, task)
  156. line += ' != '.join(['%s (%s)' % (signature, ', '.join([m for m in signatures[signature]])) for
  157. signature in sorted(signatures.keys(), key=lambda s: signatures[s])])
  158. last_line_key = next_line_key
  159. msg.append(line)
  160. # Randomly pick two mismatched signatures and remember how to invoke
  161. # bitbake-diffsigs for them.
  162. iterator = iter(signatures.items())
  163. a = next(iterator)
  164. b = next(iterator)
  165. diffsig_machines = '(%s) != (%s)' % (', '.join(a[1]), ', '.join(b[1]))
  166. diffsig_params = '-t %s %s -s %s %s' % (pn, taskname, a[0], b[0])
  167. else:
  168. pruned += 1
  169. if msg:
  170. msg.insert(0, 'The machines have conflicting signatures for some shared tasks:')
  171. if pruned > 0:
  172. msg.append('')
  173. msg.append('%d tasks where not listed because some other task of the recipe already differed.' % pruned)
  174. msg.append('It is likely that differences from different recipes also have the same root cause.')
  175. msg.append('')
  176. # Explain how to investigate...
  177. msg.append('To investigate, run bitbake-diffsigs -t recipename taskname -s fromsig tosig.')
  178. cmd = 'bitbake-diffsigs %s' % diffsig_params
  179. msg.append('Example: %s in the last line' % diffsig_machines)
  180. msg.append('Command: %s' % cmd)
  181. # ... and actually do it automatically for that example, but without aborting
  182. # when that fails.
  183. try:
  184. output = check_command('Comparing signatures failed.', cmd).decode('utf-8')
  185. except RuntimeError as ex:
  186. output = str(ex)
  187. msg.extend([' ' + line for line in output.splitlines()])
  188. self.fail('\n'.join(msg))