__init__.py 15 KB

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