layerindex.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. #
  2. # Copyright BitBake Contributors
  3. #
  4. # SPDX-License-Identifier: GPL-2.0-only
  5. #
  6. import layerindexlib
  7. import argparse
  8. import logging
  9. import os
  10. import subprocess
  11. from bblayers.action import ActionPlugin
  12. logger = logging.getLogger('bitbake-layers')
  13. def plugin_init(plugins):
  14. return LayerIndexPlugin()
  15. class LayerIndexPlugin(ActionPlugin):
  16. """Subcommands for interacting with the layer index.
  17. This class inherits ActionPlugin to get do_add_layer.
  18. """
  19. def get_fetch_layer(self, fetchdir, url, subdir, fetch_layer, branch, shallow=False):
  20. layername = self.get_layer_name(url)
  21. if os.path.splitext(layername)[1] == '.git':
  22. layername = os.path.splitext(layername)[0]
  23. repodir = os.path.join(fetchdir, layername)
  24. layerdir = os.path.join(repodir, subdir)
  25. if not os.path.exists(repodir):
  26. if fetch_layer:
  27. cmd = ['git', 'clone']
  28. if shallow:
  29. cmd.extend(['--depth', '1'])
  30. if branch:
  31. cmd.extend(['-b' , branch])
  32. cmd.extend([url, repodir])
  33. result = subprocess.call(cmd)
  34. if result:
  35. logger.error("Failed to download %s (%s)" % (url, branch))
  36. return None, None, None
  37. else:
  38. return subdir, layername, layerdir
  39. else:
  40. logger.plain("Repository %s needs to be fetched" % url)
  41. return subdir, layername, layerdir
  42. elif os.path.exists(repodir) and branch:
  43. """
  44. If the repo is already cloned, ensure it is on the correct branch,
  45. switching branches if necessary and possible.
  46. """
  47. base_cmd = ['git', '--git-dir=%s/.git' % repodir, '--work-tree=%s' % repodir]
  48. cmd = base_cmd + ['branch']
  49. completed_proc = subprocess.run(cmd, text=True, capture_output=True)
  50. if completed_proc.returncode:
  51. logger.error("Unable to validate repo %s (%s)" % (repodir, stderr))
  52. return None, None, None
  53. else:
  54. if branch != completed_proc.stdout[2:-1]:
  55. cmd = base_cmd + ['status', '--short']
  56. completed_proc = subprocess.run(cmd, text=True, capture_output=True)
  57. if completed_proc.stdout.count('\n') != 0:
  58. logger.warning("There are uncommitted changes in repo %s" % repodir)
  59. cmd = base_cmd + ['checkout', branch]
  60. completed_proc = subprocess.run(cmd, text=True, capture_output=True)
  61. if completed_proc.returncode:
  62. # Could be due to original shallow clone on a different branch for example
  63. logger.error("Unable to automatically switch %s to desired branch '%s' (%s)"
  64. % (repodir, branch, completed_proc.stderr))
  65. return None, None, None
  66. return subdir, layername, layerdir
  67. elif os.path.exists(layerdir):
  68. return subdir, layername, layerdir
  69. else:
  70. logger.error("%s is not in %s" % (url, subdir))
  71. return None, None, None
  72. def do_layerindex_fetch(self, args):
  73. """Fetches a layer from a layer index along with its dependent layers, and adds them to conf/bblayers.conf.
  74. """
  75. def _construct_url(baseurls, branches):
  76. urls = []
  77. for baseurl in baseurls:
  78. if baseurl[-1] != '/':
  79. baseurl += '/'
  80. if not baseurl.startswith('cooker'):
  81. baseurl += "api/"
  82. if branches:
  83. baseurl += ";branch=%s" % ','.join(branches)
  84. urls.append(baseurl)
  85. return urls
  86. # Set the default...
  87. if args.branch:
  88. branches = [args.branch]
  89. else:
  90. branches = (self.tinfoil.config_data.getVar('LAYERSERIES_CORENAMES') or 'master').split()
  91. logger.debug('Trying branches: %s' % branches)
  92. ignore_layers = []
  93. if args.ignore:
  94. ignore_layers.extend(args.ignore.split(','))
  95. # Load the cooker DB
  96. cookerIndex = layerindexlib.LayerIndex(self.tinfoil.config_data)
  97. cookerIndex.load_layerindex('cooker://', load='layerDependencies')
  98. # Fast path, check if we already have what has been requested!
  99. (dependencies, invalidnames) = cookerIndex.find_dependencies(names=args.layername, ignores=ignore_layers)
  100. if not args.show_only and not invalidnames:
  101. logger.plain("You already have the requested layer(s): %s" % args.layername)
  102. return 0
  103. # The information to show is already in the cookerIndex
  104. if invalidnames:
  105. # General URL to use to access the layer index
  106. # While there is ONE right now, we're expect users could enter several
  107. apiurl = self.tinfoil.config_data.getVar('BBLAYERS_LAYERINDEX_URL').split()
  108. if not apiurl:
  109. logger.error("Cannot get BBLAYERS_LAYERINDEX_URL")
  110. return 1
  111. remoteIndex = layerindexlib.LayerIndex(self.tinfoil.config_data)
  112. for remoteurl in _construct_url(apiurl, branches):
  113. logger.plain("Loading %s..." % remoteurl)
  114. remoteIndex.load_layerindex(remoteurl)
  115. if remoteIndex.is_empty():
  116. logger.error("Remote layer index %s is empty for branches %s" % (apiurl, branches))
  117. return 1
  118. lIndex = cookerIndex + remoteIndex
  119. (dependencies, invalidnames) = lIndex.find_dependencies(names=args.layername, ignores=ignore_layers)
  120. if invalidnames:
  121. for invaluename in invalidnames:
  122. logger.error('Layer "%s" not found in layer index' % invaluename)
  123. return 1
  124. logger.plain("%s %s %s" % ("Layer".ljust(49), "Git repository (branch)".ljust(54), "Subdirectory"))
  125. logger.plain('=' * 125)
  126. for deplayerbranch in dependencies:
  127. layerBranch = dependencies[deplayerbranch][0]
  128. # TODO: Determine display behavior
  129. # This is the local content, uncomment to hide local
  130. # layers from the display.
  131. #if layerBranch.index.config['TYPE'] == 'cooker':
  132. # continue
  133. layerDeps = dependencies[deplayerbranch][1:]
  134. requiredby = []
  135. recommendedby = []
  136. for dep in layerDeps:
  137. if dep.required:
  138. requiredby.append(dep.layer.name)
  139. else:
  140. recommendedby.append(dep.layer.name)
  141. logger.plain('%s %s %s' % (("%s:%s:%s" %
  142. (layerBranch.index.config['DESCRIPTION'],
  143. layerBranch.branch.name,
  144. layerBranch.layer.name)).ljust(50),
  145. ("%s (%s)" % (layerBranch.layer.vcs_url,
  146. layerBranch.actual_branch)).ljust(55),
  147. layerBranch.vcs_subdir
  148. ))
  149. if requiredby:
  150. logger.plain(' required by: %s' % ' '.join(requiredby))
  151. if recommendedby:
  152. logger.plain(' recommended by: %s' % ' '.join(recommendedby))
  153. if dependencies:
  154. if args.fetchdir:
  155. fetchdir = args.fetchdir
  156. else:
  157. fetchdir = self.tinfoil.config_data.getVar('BBLAYERS_FETCH_DIR')
  158. if not fetchdir:
  159. logger.error("Cannot get BBLAYERS_FETCH_DIR")
  160. return 1
  161. if not os.path.exists(fetchdir):
  162. os.makedirs(fetchdir)
  163. addlayers = []
  164. for deplayerbranch in dependencies:
  165. layerBranch = dependencies[deplayerbranch][0]
  166. if layerBranch.index.config['TYPE'] == 'cooker':
  167. # Anything loaded via cooker is already local, skip it
  168. continue
  169. subdir, name, layerdir = self.get_fetch_layer(fetchdir,
  170. layerBranch.layer.vcs_url,
  171. layerBranch.vcs_subdir,
  172. not args.show_only,
  173. layerBranch.actual_branch,
  174. args.shallow)
  175. if not name:
  176. # Error already shown
  177. return 1
  178. addlayers.append((subdir, name, layerdir))
  179. if not args.show_only:
  180. localargs = argparse.Namespace()
  181. localargs.layerdir = []
  182. localargs.force = args.force
  183. for subdir, name, layerdir in addlayers:
  184. if os.path.exists(layerdir):
  185. if subdir:
  186. logger.plain("Adding layer \"%s\" (%s) to conf/bblayers.conf" % (subdir, layerdir))
  187. else:
  188. logger.plain("Adding layer \"%s\" (%s) to conf/bblayers.conf" % (name, layerdir))
  189. localargs.layerdir.append(layerdir)
  190. else:
  191. break
  192. if localargs.layerdir:
  193. self.do_add_layer(localargs)
  194. def do_layerindex_show_depends(self, args):
  195. """Find layer dependencies from layer index.
  196. """
  197. args.show_only = True
  198. args.ignore = []
  199. args.fetchdir = ""
  200. args.shallow = True
  201. self.do_layerindex_fetch(args)
  202. def register_commands(self, sp):
  203. parser_layerindex_fetch = self.add_command(sp, 'layerindex-fetch', self.do_layerindex_fetch, parserecipes=False)
  204. parser_layerindex_fetch.add_argument('-n', '--show-only', help='show dependencies and do nothing else', action='store_true')
  205. parser_layerindex_fetch.add_argument('-b', '--branch', help='branch name to fetch')
  206. parser_layerindex_fetch.add_argument('-s', '--shallow', help='do only shallow clones (--depth=1)', action='store_true')
  207. parser_layerindex_fetch.add_argument('-i', '--ignore', help='assume the specified layers do not need to be fetched/added (separate multiple layers with commas, no spaces)', metavar='LAYER')
  208. parser_layerindex_fetch.add_argument('-f', '--fetchdir', help='directory to fetch the layer(s) into (will be created if it does not exist)')
  209. parser_layerindex_fetch.add_argument('layername', nargs='+', help='layer to fetch')
  210. parser_layerindex_show_depends = self.add_command(sp, 'layerindex-show-depends', self.do_layerindex_show_depends, parserecipes=False)
  211. parser_layerindex_show_depends.add_argument('-b', '--branch', help='branch name to fetch')
  212. parser_layerindex_show_depends.add_argument('layername', nargs='+', help='layer to query')