scriptutils.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. # Script utility functions
  2. #
  3. # Copyright (C) 2014 Intel Corporation
  4. #
  5. # SPDX-License-Identifier: GPL-2.0-only
  6. #
  7. import glob
  8. import logging
  9. import os
  10. import random
  11. import shlex
  12. import shutil
  13. import string
  14. import subprocess
  15. import sys
  16. import tempfile
  17. import threading
  18. import importlib
  19. import importlib.machinery
  20. import importlib.util
  21. class KeepAliveStreamHandler(logging.StreamHandler):
  22. def __init__(self, keepalive=True, **kwargs):
  23. super().__init__(**kwargs)
  24. if keepalive is True:
  25. keepalive = 5000 # default timeout
  26. self._timeout = threading.Condition()
  27. self._stop = False
  28. # background thread waits on condition, if the condition does not
  29. # happen emit a keep alive message
  30. def thread():
  31. while not self._stop:
  32. with self._timeout:
  33. if not self._timeout.wait(keepalive):
  34. self.emit(logging.LogRecord("keepalive", logging.INFO,
  35. None, None, "Keepalive message", None, None))
  36. self._thread = threading.Thread(target=thread, daemon=True)
  37. self._thread.start()
  38. def close(self):
  39. # mark the thread to stop and notify it
  40. self._stop = True
  41. with self._timeout:
  42. self._timeout.notify()
  43. # wait for it to join
  44. self._thread.join()
  45. super().close()
  46. def emit(self, record):
  47. super().emit(record)
  48. # trigger timer reset
  49. with self._timeout:
  50. self._timeout.notify()
  51. def logger_create(name, stream=None, keepalive=None):
  52. logger = logging.getLogger(name)
  53. if keepalive is not None:
  54. loggerhandler = KeepAliveStreamHandler(stream=stream, keepalive=keepalive)
  55. else:
  56. loggerhandler = logging.StreamHandler(stream=stream)
  57. loggerhandler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
  58. logger.addHandler(loggerhandler)
  59. logger.setLevel(logging.INFO)
  60. return logger
  61. def logger_setup_color(logger, color='auto'):
  62. from bb.msg import BBLogFormatter
  63. for handler in logger.handlers:
  64. if (isinstance(handler, logging.StreamHandler) and
  65. isinstance(handler.formatter, BBLogFormatter)):
  66. if color == 'always' or (color == 'auto' and handler.stream.isatty()):
  67. handler.formatter.enable_color()
  68. def load_plugins(logger, plugins, pluginpath):
  69. def load_plugin(name):
  70. logger.debug('Loading plugin %s' % name)
  71. spec = importlib.machinery.PathFinder.find_spec(name, path=[pluginpath])
  72. if spec:
  73. mod = importlib.util.module_from_spec(spec)
  74. spec.loader.exec_module(mod)
  75. return mod
  76. def plugin_name(filename):
  77. return os.path.splitext(os.path.basename(filename))[0]
  78. known_plugins = [plugin_name(p.__name__) for p in plugins]
  79. logger.debug('Loading plugins from %s...' % pluginpath)
  80. for fn in glob.glob(os.path.join(pluginpath, '*.py')):
  81. name = plugin_name(fn)
  82. if name != '__init__' and name not in known_plugins:
  83. plugin = load_plugin(name)
  84. if hasattr(plugin, 'plugin_init'):
  85. plugin.plugin_init(plugins)
  86. plugins.append(plugin)
  87. def git_convert_standalone_clone(repodir):
  88. """If specified directory is a git repository, ensure it's a standalone clone"""
  89. import bb.process
  90. if os.path.exists(os.path.join(repodir, '.git')):
  91. alternatesfile = os.path.join(repodir, '.git', 'objects', 'info', 'alternates')
  92. if os.path.exists(alternatesfile):
  93. # This will have been cloned with -s, so we need to convert it so none
  94. # of the contents is shared
  95. bb.process.run('git repack -a', cwd=repodir)
  96. os.remove(alternatesfile)
  97. def _get_temp_recipe_dir(d):
  98. # This is a little bit hacky but we need to find a place where we can put
  99. # the recipe so that bitbake can find it. We're going to delete it at the
  100. # end so it doesn't really matter where we put it.
  101. bbfiles = d.getVar('BBFILES').split()
  102. fetchrecipedir = None
  103. for pth in bbfiles:
  104. if pth.endswith('.bb'):
  105. pthdir = os.path.dirname(pth)
  106. if os.access(os.path.dirname(os.path.dirname(pthdir)), os.W_OK):
  107. fetchrecipedir = pthdir.replace('*', 'recipetool')
  108. if pthdir.endswith('workspace/recipes/*'):
  109. # Prefer the workspace
  110. break
  111. return fetchrecipedir
  112. class FetchUrlFailure(Exception):
  113. def __init__(self, url):
  114. self.url = url
  115. def __str__(self):
  116. return "Failed to fetch URL %s" % self.url
  117. def fetch_url(tinfoil, srcuri, srcrev, destdir, logger, preserve_tmp=False, mirrors=False):
  118. """
  119. Fetch the specified URL using normal do_fetch and do_unpack tasks, i.e.
  120. any dependencies that need to be satisfied in order to support the fetch
  121. operation will be taken care of
  122. """
  123. import bb
  124. checksums = {}
  125. fetchrecipepn = None
  126. # We need to put our temp directory under ${BASE_WORKDIR} otherwise
  127. # we may have problems with the recipe-specific sysroot population
  128. tmpparent = tinfoil.config_data.getVar('BASE_WORKDIR')
  129. bb.utils.mkdirhier(tmpparent)
  130. tmpdir = tempfile.mkdtemp(prefix='recipetool-', dir=tmpparent)
  131. try:
  132. tmpworkdir = os.path.join(tmpdir, 'work')
  133. logger.debug('fetch_url: temp dir is %s' % tmpdir)
  134. fetchrecipedir = _get_temp_recipe_dir(tinfoil.config_data)
  135. if not fetchrecipedir:
  136. logger.error('Searched BBFILES but unable to find a writeable place to put temporary recipe')
  137. sys.exit(1)
  138. fetchrecipe = None
  139. bb.utils.mkdirhier(fetchrecipedir)
  140. try:
  141. # Generate a dummy recipe so we can follow more or less normal paths
  142. # for do_fetch and do_unpack
  143. # I'd use tempfile functions here but underscores can be produced by that and those
  144. # aren't allowed in recipe file names except to separate the version
  145. rndstring = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(8))
  146. fetchrecipe = os.path.join(fetchrecipedir, 'tmp-recipetool-%s.bb' % rndstring)
  147. fetchrecipepn = os.path.splitext(os.path.basename(fetchrecipe))[0]
  148. logger.debug('Generating initial recipe %s for fetching' % fetchrecipe)
  149. with open(fetchrecipe, 'w') as f:
  150. # We don't want to have to specify LIC_FILES_CHKSUM
  151. f.write('LICENSE = "CLOSED"\n')
  152. # We don't need the cross-compiler
  153. f.write('INHIBIT_DEFAULT_DEPS = "1"\n')
  154. # We don't have the checksums yet so we can't require them
  155. f.write('BB_STRICT_CHECKSUM = "ignore"\n')
  156. f.write('SRC_URI = "%s"\n' % srcuri)
  157. f.write('SRCREV = "%s"\n' % srcrev)
  158. f.write('PV = "0.0+"\n')
  159. f.write('WORKDIR = "%s"\n' % tmpworkdir)
  160. # Set S out of the way so it doesn't get created under the workdir
  161. f.write('S = "%s"\n' % os.path.join(tmpdir, 'emptysrc'))
  162. if not mirrors:
  163. # We do not need PREMIRRORS since we are almost certainly
  164. # fetching new source rather than something that has already
  165. # been fetched. Hence, we disable them by default.
  166. # However, we provide an option for users to enable it.
  167. f.write('PREMIRRORS = ""\n')
  168. f.write('MIRRORS = ""\n')
  169. logger.info('Fetching %s...' % srcuri)
  170. # FIXME this is too noisy at the moment
  171. # Parse recipes so our new recipe gets picked up
  172. tinfoil.parse_recipes()
  173. def eventhandler(event):
  174. if isinstance(event, bb.fetch2.MissingChecksumEvent):
  175. checksums.update(event.checksums)
  176. return True
  177. return False
  178. # Run the fetch + unpack tasks
  179. res = tinfoil.build_targets(fetchrecipepn,
  180. 'do_unpack',
  181. handle_events=True,
  182. extra_events=['bb.fetch2.MissingChecksumEvent'],
  183. event_callback=eventhandler)
  184. if not res:
  185. raise FetchUrlFailure(srcuri)
  186. # Remove unneeded directories
  187. rd = tinfoil.parse_recipe(fetchrecipepn)
  188. if rd:
  189. pathvars = ['T', 'RECIPE_SYSROOT', 'RECIPE_SYSROOT_NATIVE']
  190. for pathvar in pathvars:
  191. path = rd.getVar(pathvar)
  192. if os.path.exists(path):
  193. shutil.rmtree(path)
  194. finally:
  195. if fetchrecipe:
  196. try:
  197. os.remove(fetchrecipe)
  198. except FileNotFoundError:
  199. pass
  200. try:
  201. os.rmdir(fetchrecipedir)
  202. except OSError as e:
  203. import errno
  204. if e.errno != errno.ENOTEMPTY:
  205. raise
  206. bb.utils.mkdirhier(destdir)
  207. for fn in os.listdir(tmpworkdir):
  208. shutil.move(os.path.join(tmpworkdir, fn), destdir)
  209. finally:
  210. if not preserve_tmp:
  211. shutil.rmtree(tmpdir)
  212. tmpdir = None
  213. return checksums, tmpdir
  214. def run_editor(fn, logger=None):
  215. if isinstance(fn, str):
  216. files = [fn]
  217. else:
  218. files = fn
  219. editor = os.getenv('VISUAL', os.getenv('EDITOR', 'vi'))
  220. try:
  221. #print(shlex.split(editor) + files)
  222. return subprocess.check_call(shlex.split(editor) + files)
  223. except subprocess.CalledProcessError as exc:
  224. logger.error("Execution of '%s' failed: %s" % (editor, exc))
  225. return 1
  226. def is_src_url(param):
  227. """
  228. Check if a parameter is a URL and return True if so
  229. NOTE: be careful about changing this as it will influence how devtool/recipetool command line handling works
  230. """
  231. if not param:
  232. return False
  233. elif '://' in param:
  234. return True
  235. elif param.startswith('git@') or ('@' in param and param.endswith('.git')):
  236. return True
  237. return False
  238. def filter_src_subdirs(pth):
  239. """
  240. Filter out subdirectories of initial unpacked source trees that we do not care about.
  241. Used by devtool and recipetool.
  242. """
  243. dirlist = os.listdir(pth)
  244. filterout = ['git.indirectionsymlink', 'source-date-epoch', 'sstate-install-recipe_qa']
  245. dirlist = [x for x in dirlist if x not in filterout]
  246. return dirlist