__init__.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. # Yocto Project layer check tool
  2. #
  3. # Copyright (C) 2017 Intel Corporation
  4. # Released under the MIT license (see COPYING.MIT)
  5. import os
  6. import re
  7. import subprocess
  8. from enum import Enum
  9. import bb.tinfoil
  10. class LayerType(Enum):
  11. BSP = 0
  12. DISTRO = 1
  13. SOFTWARE = 2
  14. ERROR_NO_LAYER_CONF = 98
  15. ERROR_BSP_DISTRO = 99
  16. def _get_configurations(path):
  17. configs = []
  18. for f in os.listdir(path):
  19. file_path = os.path.join(path, f)
  20. if os.path.isfile(file_path) and f.endswith('.conf'):
  21. configs.append(f[:-5]) # strip .conf
  22. return configs
  23. def _get_layer_collections(layer_path, lconf=None, data=None):
  24. import bb.parse
  25. import bb.data
  26. if lconf is None:
  27. lconf = os.path.join(layer_path, 'conf', 'layer.conf')
  28. if data is None:
  29. ldata = bb.data.init()
  30. bb.parse.init_parser(ldata)
  31. else:
  32. ldata = data.createCopy()
  33. ldata.setVar('LAYERDIR', layer_path)
  34. try:
  35. ldata = bb.parse.handle(lconf, ldata, include=True)
  36. except:
  37. raise RuntimeError("Parsing of layer.conf from layer: %s failed" % layer_path)
  38. ldata.expandVarref('LAYERDIR')
  39. collections = (ldata.getVar('BBFILE_COLLECTIONS') or '').split()
  40. if not collections:
  41. name = os.path.basename(layer_path)
  42. collections = [name]
  43. collections = {c: {} for c in collections}
  44. for name in collections:
  45. priority = ldata.getVar('BBFILE_PRIORITY_%s' % name)
  46. pattern = ldata.getVar('BBFILE_PATTERN_%s' % name)
  47. depends = ldata.getVar('LAYERDEPENDS_%s' % name)
  48. compat = ldata.getVar('LAYERSERIES_COMPAT_%s' % name)
  49. collections[name]['priority'] = priority
  50. collections[name]['pattern'] = pattern
  51. collections[name]['depends'] = depends
  52. collections[name]['compat'] = compat
  53. return collections
  54. def _detect_layer(layer_path):
  55. """
  56. Scans layer directory to detect what type of layer
  57. is BSP, Distro or Software.
  58. Returns a dictionary with layer name, type and path.
  59. """
  60. layer = {}
  61. layer_name = os.path.basename(layer_path)
  62. layer['name'] = layer_name
  63. layer['path'] = layer_path
  64. layer['conf'] = {}
  65. if not os.path.isfile(os.path.join(layer_path, 'conf', 'layer.conf')):
  66. layer['type'] = LayerType.ERROR_NO_LAYER_CONF
  67. return layer
  68. machine_conf = os.path.join(layer_path, 'conf', 'machine')
  69. distro_conf = os.path.join(layer_path, 'conf', 'distro')
  70. is_bsp = False
  71. is_distro = False
  72. if os.path.isdir(machine_conf):
  73. machines = _get_configurations(machine_conf)
  74. if machines:
  75. is_bsp = True
  76. if os.path.isdir(distro_conf):
  77. distros = _get_configurations(distro_conf)
  78. if distros:
  79. is_distro = True
  80. if is_bsp and is_distro:
  81. layer['type'] = LayerType.ERROR_BSP_DISTRO
  82. elif is_bsp:
  83. layer['type'] = LayerType.BSP
  84. layer['conf']['machines'] = machines
  85. elif is_distro:
  86. layer['type'] = LayerType.DISTRO
  87. layer['conf']['distros'] = distros
  88. else:
  89. layer['type'] = LayerType.SOFTWARE
  90. layer['collections'] = _get_layer_collections(layer['path'])
  91. return layer
  92. def detect_layers(layer_directories, no_auto):
  93. layers = []
  94. for directory in layer_directories:
  95. directory = os.path.realpath(directory)
  96. if directory[-1] == '/':
  97. directory = directory[0:-1]
  98. if no_auto:
  99. conf_dir = os.path.join(directory, 'conf')
  100. if os.path.isdir(conf_dir):
  101. layer = _detect_layer(directory)
  102. if layer:
  103. layers.append(layer)
  104. else:
  105. for root, dirs, files in os.walk(directory):
  106. dir_name = os.path.basename(root)
  107. conf_dir = os.path.join(root, 'conf')
  108. if os.path.isdir(conf_dir):
  109. layer = _detect_layer(root)
  110. if layer:
  111. layers.append(layer)
  112. return layers
  113. def _find_layer_depends(depend, layers):
  114. for layer in layers:
  115. for collection in layer['collections']:
  116. if depend == collection:
  117. return layer
  118. return None
  119. def add_layer_dependencies(bblayersconf, layer, layers, logger):
  120. def recurse_dependencies(depends, layer, layers, logger, ret = []):
  121. logger.debug('Processing dependencies %s for layer %s.' % \
  122. (depends, layer['name']))
  123. for depend in depends.split():
  124. # core (oe-core) is suppose to be provided
  125. if depend == 'core':
  126. continue
  127. layer_depend = _find_layer_depends(depend, layers)
  128. if not layer_depend:
  129. logger.error('Layer %s depends on %s and isn\'t found.' % \
  130. (layer['name'], depend))
  131. ret = None
  132. continue
  133. # We keep processing, even if ret is None, this allows us to report
  134. # multiple errors at once
  135. if ret is not None and layer_depend not in ret:
  136. ret.append(layer_depend)
  137. else:
  138. # we might have processed this dependency already, in which case
  139. # we should not do it again (avoid recursive loop)
  140. continue
  141. # Recursively process...
  142. if 'collections' not in layer_depend:
  143. continue
  144. for collection in layer_depend['collections']:
  145. collect_deps = layer_depend['collections'][collection]['depends']
  146. if not collect_deps:
  147. continue
  148. ret = recurse_dependencies(collect_deps, layer_depend, layers, logger, ret)
  149. return ret
  150. layer_depends = []
  151. for collection in layer['collections']:
  152. depends = layer['collections'][collection]['depends']
  153. if not depends:
  154. continue
  155. layer_depends = recurse_dependencies(depends, layer, layers, logger, layer_depends)
  156. # Note: [] (empty) is allowed, None is not!
  157. if layer_depends is None:
  158. return False
  159. else:
  160. add_layers(bblayersconf, layer_depends, logger)
  161. return True
  162. def add_layers(bblayersconf, layers, logger):
  163. # Don't add a layer that is already present.
  164. added = set()
  165. output = check_command('Getting existing layers failed.', 'bitbake-layers show-layers').decode('utf-8')
  166. for layer, path, pri in re.findall(r'^(\S+) +([^\n]*?) +(\d+)$', output, re.MULTILINE):
  167. added.add(path)
  168. with open(bblayersconf, 'a+') as f:
  169. for layer in layers:
  170. logger.info('Adding layer %s' % layer['name'])
  171. name = layer['name']
  172. path = layer['path']
  173. if path in added:
  174. logger.info('%s is already in %s' % (name, bblayersconf))
  175. else:
  176. added.add(path)
  177. f.write("\nBBLAYERS += \"%s\"\n" % path)
  178. return True
  179. def check_command(error_msg, cmd, cwd=None):
  180. '''
  181. Run a command under a shell, capture stdout and stderr in a single stream,
  182. throw an error when command returns non-zero exit code. Returns the output.
  183. '''
  184. p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=cwd)
  185. output, _ = p.communicate()
  186. if p.returncode:
  187. msg = "%s\nCommand: %s\nOutput:\n%s" % (error_msg, cmd, output.decode('utf-8'))
  188. raise RuntimeError(msg)
  189. return output
  190. def get_signatures(builddir, failsafe=False, machine=None):
  191. import re
  192. # some recipes needs to be excluded like meta-world-pkgdata
  193. # because a layer can add recipes to a world build so signature
  194. # will be change
  195. exclude_recipes = ('meta-world-pkgdata',)
  196. sigs = {}
  197. tune2tasks = {}
  198. cmd = ''
  199. if machine:
  200. cmd += 'MACHINE=%s ' % machine
  201. cmd += 'bitbake '
  202. if failsafe:
  203. cmd += '-k '
  204. cmd += '-S none world'
  205. sigs_file = os.path.join(builddir, 'locked-sigs.inc')
  206. if os.path.exists(sigs_file):
  207. os.unlink(sigs_file)
  208. try:
  209. check_command('Generating signatures failed. This might be due to some parse error and/or general layer incompatibilities.',
  210. cmd, builddir)
  211. except RuntimeError as ex:
  212. if failsafe and os.path.exists(sigs_file):
  213. # Ignore the error here. Most likely some recipes active
  214. # in a world build lack some dependencies. There is a
  215. # separate test_machine_world_build which exposes the
  216. # failure.
  217. pass
  218. else:
  219. raise
  220. sig_regex = re.compile("^(?P<task>.*:.*):(?P<hash>.*) .$")
  221. tune_regex = re.compile("(^|\s)SIGGEN_LOCKEDSIGS_t-(?P<tune>\S*)\s*=\s*")
  222. current_tune = None
  223. with open(sigs_file, 'r') as f:
  224. for line in f.readlines():
  225. line = line.strip()
  226. t = tune_regex.search(line)
  227. if t:
  228. current_tune = t.group('tune')
  229. s = sig_regex.match(line)
  230. if s:
  231. exclude = False
  232. for er in exclude_recipes:
  233. (recipe, task) = s.group('task').split(':')
  234. if er == recipe:
  235. exclude = True
  236. break
  237. if exclude:
  238. continue
  239. sigs[s.group('task')] = s.group('hash')
  240. tune2tasks.setdefault(current_tune, []).append(s.group('task'))
  241. if not sigs:
  242. raise RuntimeError('Can\'t load signatures from %s' % sigs_file)
  243. return (sigs, tune2tasks)
  244. def get_depgraph(targets=['world'], failsafe=False):
  245. '''
  246. Returns the dependency graph for the given target(s).
  247. The dependency graph is taken directly from DepTreeEvent.
  248. '''
  249. depgraph = None
  250. with bb.tinfoil.Tinfoil() as tinfoil:
  251. tinfoil.prepare(config_only=False)
  252. tinfoil.set_event_mask(['bb.event.NoProvider', 'bb.event.DepTreeGenerated', 'bb.command.CommandCompleted'])
  253. if not tinfoil.run_command('generateDepTreeEvent', targets, 'do_build'):
  254. raise RuntimeError('starting generateDepTreeEvent failed')
  255. while True:
  256. event = tinfoil.wait_event(timeout=1000)
  257. if event:
  258. if isinstance(event, bb.command.CommandFailed):
  259. raise RuntimeError('Generating dependency information failed: %s' % event.error)
  260. elif isinstance(event, bb.command.CommandCompleted):
  261. break
  262. elif isinstance(event, bb.event.NoProvider):
  263. if failsafe:
  264. # The event is informational, we will get information about the
  265. # remaining dependencies eventually and thus can ignore this
  266. # here like we do in get_signatures(), if desired.
  267. continue
  268. if event._reasons:
  269. raise RuntimeError('Nothing provides %s: %s' % (event._item, event._reasons))
  270. else:
  271. raise RuntimeError('Nothing provides %s.' % (event._item))
  272. elif isinstance(event, bb.event.DepTreeGenerated):
  273. depgraph = event._depgraph
  274. if depgraph is None:
  275. raise RuntimeError('Could not retrieve the depgraph.')
  276. return depgraph
  277. def compare_signatures(old_sigs, curr_sigs):
  278. '''
  279. Compares the result of two get_signatures() calls. Returns None if no
  280. problems found, otherwise a string that can be used as additional
  281. explanation in self.fail().
  282. '''
  283. # task -> (old signature, new signature)
  284. sig_diff = {}
  285. for task in old_sigs:
  286. if task in curr_sigs and \
  287. old_sigs[task] != curr_sigs[task]:
  288. sig_diff[task] = (old_sigs[task], curr_sigs[task])
  289. if not sig_diff:
  290. return None
  291. # Beware, depgraph uses task=<pn>.<taskname> whereas get_signatures()
  292. # uses <pn>:<taskname>. Need to convert sometimes. The output follows
  293. # the convention from get_signatures() because that seems closer to
  294. # normal bitbake output.
  295. def sig2graph(task):
  296. pn, taskname = task.rsplit(':', 1)
  297. return pn + '.' + taskname
  298. def graph2sig(task):
  299. pn, taskname = task.rsplit('.', 1)
  300. return pn + ':' + taskname
  301. depgraph = get_depgraph(failsafe=True)
  302. depends = depgraph['tdepends']
  303. # If a task A has a changed signature, but none of its
  304. # dependencies, then we need to report it because it is
  305. # the one which introduces a change. Any task depending on
  306. # A (directly or indirectly) will also have a changed
  307. # signature, but we don't need to report it. It might have
  308. # its own changes, which will become apparent once the
  309. # issues that we do report are fixed and the test gets run
  310. # again.
  311. sig_diff_filtered = []
  312. for task, (old_sig, new_sig) in sig_diff.items():
  313. deps_tainted = False
  314. for dep in depends.get(sig2graph(task), ()):
  315. if graph2sig(dep) in sig_diff:
  316. deps_tainted = True
  317. break
  318. if not deps_tainted:
  319. sig_diff_filtered.append((task, old_sig, new_sig))
  320. msg = []
  321. msg.append('%d signatures changed, initial differences (first hash before, second after):' %
  322. len(sig_diff))
  323. for diff in sorted(sig_diff_filtered):
  324. recipe, taskname = diff[0].rsplit(':', 1)
  325. cmd = 'bitbake-diffsigs --task %s %s --signature %s %s' % \
  326. (recipe, taskname, diff[1], diff[2])
  327. msg.append(' %s: %s -> %s' % diff)
  328. msg.append(' %s' % cmd)
  329. try:
  330. output = check_command('Determining signature difference failed.',
  331. cmd).decode('utf-8')
  332. except RuntimeError as error:
  333. output = str(error)
  334. if output:
  335. msg.extend([' ' + line for line in output.splitlines()])
  336. msg.append('')
  337. return '\n'.join(msg)