__init__.py 16 KB

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