__init__.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  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. # Don't add a layer that is already present.
  161. added = set()
  162. output = check_command('Getting existing layers failed.', 'bitbake-layers show-layers').decode('utf-8')
  163. for layer, path, pri in re.findall(r'^(\S+) +([^\n]*?) +(\d+)$', output, re.MULTILINE):
  164. added.add(path)
  165. for layer_depend in layer_depends:
  166. name = layer_depend['name']
  167. path = layer_depend['path']
  168. if path in added:
  169. continue
  170. else:
  171. added.add(path)
  172. logger.info('Adding layer dependency %s' % name)
  173. with open(bblayersconf, 'a+') as f:
  174. f.write("\nBBLAYERS += \"%s\"\n" % path)
  175. return True
  176. def add_layer(bblayersconf, layer, layers, logger):
  177. logger.info('Adding layer %s' % layer['name'])
  178. with open(bblayersconf, 'a+') as f:
  179. f.write("\nBBLAYERS += \"%s\"\n" % layer['path'])
  180. return True
  181. def check_command(error_msg, cmd, cwd=None):
  182. '''
  183. Run a command under a shell, capture stdout and stderr in a single stream,
  184. throw an error when command returns non-zero exit code. Returns the output.
  185. '''
  186. p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=cwd)
  187. output, _ = p.communicate()
  188. if p.returncode:
  189. msg = "%s\nCommand: %s\nOutput:\n%s" % (error_msg, cmd, output.decode('utf-8'))
  190. raise RuntimeError(msg)
  191. return output
  192. def get_signatures(builddir, failsafe=False, machine=None):
  193. import re
  194. # some recipes needs to be excluded like meta-world-pkgdata
  195. # because a layer can add recipes to a world build so signature
  196. # will be change
  197. exclude_recipes = ('meta-world-pkgdata',)
  198. sigs = {}
  199. tune2tasks = {}
  200. cmd = ''
  201. if machine:
  202. cmd += 'MACHINE=%s ' % machine
  203. cmd += 'bitbake '
  204. if failsafe:
  205. cmd += '-k '
  206. cmd += '-S none world'
  207. sigs_file = os.path.join(builddir, 'locked-sigs.inc')
  208. if os.path.exists(sigs_file):
  209. os.unlink(sigs_file)
  210. try:
  211. check_command('Generating signatures failed. This might be due to some parse error and/or general layer incompatibilities.',
  212. cmd, builddir)
  213. except RuntimeError as ex:
  214. if failsafe and os.path.exists(sigs_file):
  215. # Ignore the error here. Most likely some recipes active
  216. # in a world build lack some dependencies. There is a
  217. # separate test_machine_world_build which exposes the
  218. # failure.
  219. pass
  220. else:
  221. raise
  222. sig_regex = re.compile("^(?P<task>.*:.*):(?P<hash>.*) .$")
  223. tune_regex = re.compile("(^|\s)SIGGEN_LOCKEDSIGS_t-(?P<tune>\S*)\s*=\s*")
  224. current_tune = None
  225. with open(sigs_file, 'r') as f:
  226. for line in f.readlines():
  227. line = line.strip()
  228. t = tune_regex.search(line)
  229. if t:
  230. current_tune = t.group('tune')
  231. s = sig_regex.match(line)
  232. if s:
  233. exclude = False
  234. for er in exclude_recipes:
  235. (recipe, task) = s.group('task').split(':')
  236. if er == recipe:
  237. exclude = True
  238. break
  239. if exclude:
  240. continue
  241. sigs[s.group('task')] = s.group('hash')
  242. tune2tasks.setdefault(current_tune, []).append(s.group('task'))
  243. if not sigs:
  244. raise RuntimeError('Can\'t load signatures from %s' % sigs_file)
  245. return (sigs, tune2tasks)
  246. def get_depgraph(targets=['world'], failsafe=False):
  247. '''
  248. Returns the dependency graph for the given target(s).
  249. The dependency graph is taken directly from DepTreeEvent.
  250. '''
  251. depgraph = None
  252. with bb.tinfoil.Tinfoil() as tinfoil:
  253. tinfoil.prepare(config_only=False)
  254. tinfoil.set_event_mask(['bb.event.NoProvider', 'bb.event.DepTreeGenerated', 'bb.command.CommandCompleted'])
  255. if not tinfoil.run_command('generateDepTreeEvent', targets, 'do_build'):
  256. raise RuntimeError('starting generateDepTreeEvent failed')
  257. while True:
  258. event = tinfoil.wait_event(timeout=1000)
  259. if event:
  260. if isinstance(event, bb.command.CommandFailed):
  261. raise RuntimeError('Generating dependency information failed: %s' % event.error)
  262. elif isinstance(event, bb.command.CommandCompleted):
  263. break
  264. elif isinstance(event, bb.event.NoProvider):
  265. if failsafe:
  266. # The event is informational, we will get information about the
  267. # remaining dependencies eventually and thus can ignore this
  268. # here like we do in get_signatures(), if desired.
  269. continue
  270. if event._reasons:
  271. raise RuntimeError('Nothing provides %s: %s' % (event._item, event._reasons))
  272. else:
  273. raise RuntimeError('Nothing provides %s.' % (event._item))
  274. elif isinstance(event, bb.event.DepTreeGenerated):
  275. depgraph = event._depgraph
  276. if depgraph is None:
  277. raise RuntimeError('Could not retrieve the depgraph.')
  278. return depgraph
  279. def compare_signatures(old_sigs, curr_sigs):
  280. '''
  281. Compares the result of two get_signatures() calls. Returns None if no
  282. problems found, otherwise a string that can be used as additional
  283. explanation in self.fail().
  284. '''
  285. # task -> (old signature, new signature)
  286. sig_diff = {}
  287. for task in old_sigs:
  288. if task in curr_sigs and \
  289. old_sigs[task] != curr_sigs[task]:
  290. sig_diff[task] = (old_sigs[task], curr_sigs[task])
  291. if not sig_diff:
  292. return None
  293. # Beware, depgraph uses task=<pn>.<taskname> whereas get_signatures()
  294. # uses <pn>:<taskname>. Need to convert sometimes. The output follows
  295. # the convention from get_signatures() because that seems closer to
  296. # normal bitbake output.
  297. def sig2graph(task):
  298. pn, taskname = task.rsplit(':', 1)
  299. return pn + '.' + taskname
  300. def graph2sig(task):
  301. pn, taskname = task.rsplit('.', 1)
  302. return pn + ':' + taskname
  303. depgraph = get_depgraph(failsafe=True)
  304. depends = depgraph['tdepends']
  305. # If a task A has a changed signature, but none of its
  306. # dependencies, then we need to report it because it is
  307. # the one which introduces a change. Any task depending on
  308. # A (directly or indirectly) will also have a changed
  309. # signature, but we don't need to report it. It might have
  310. # its own changes, which will become apparent once the
  311. # issues that we do report are fixed and the test gets run
  312. # again.
  313. sig_diff_filtered = []
  314. for task, (old_sig, new_sig) in sig_diff.items():
  315. deps_tainted = False
  316. for dep in depends.get(sig2graph(task), ()):
  317. if graph2sig(dep) in sig_diff:
  318. deps_tainted = True
  319. break
  320. if not deps_tainted:
  321. sig_diff_filtered.append((task, old_sig, new_sig))
  322. msg = []
  323. msg.append('%d signatures changed, initial differences (first hash before, second after):' %
  324. len(sig_diff))
  325. for diff in sorted(sig_diff_filtered):
  326. recipe, taskname = diff[0].rsplit(':', 1)
  327. cmd = 'bitbake-diffsigs --task %s %s --signature %s %s' % \
  328. (recipe, taskname, diff[1], diff[2])
  329. msg.append(' %s: %s -> %s' % diff)
  330. msg.append(' %s' % cmd)
  331. try:
  332. output = check_command('Determining signature difference failed.',
  333. cmd).decode('utf-8')
  334. except RuntimeError as error:
  335. output = str(error)
  336. if output:
  337. msg.extend([' ' + line for line in output.splitlines()])
  338. msg.append('')
  339. return '\n'.join(msg)