localhostbecontroller.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. #
  2. # ex:ts=4:sw=4:sts=4:et
  3. # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
  4. #
  5. # BitBake Toaster Implementation
  6. #
  7. # Copyright (C) 2014 Intel Corporation
  8. #
  9. # This program is free software; you can redistribute it and/or modify
  10. # it under the terms of the GNU General Public License version 2 as
  11. # published by the Free Software Foundation.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License along
  19. # with this program; if not, write to the Free Software Foundation, Inc.,
  20. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  21. import os
  22. import sys
  23. import re
  24. import shutil
  25. from django.db import transaction
  26. from django.db.models import Q
  27. from bldcontrol.models import BuildEnvironment, BRLayer, BRVariable, BRTarget, BRBitbake
  28. from orm.models import CustomImageRecipe, Layer, Layer_Version, ProjectLayer
  29. import subprocess
  30. from toastermain import settings
  31. from bbcontroller import BuildEnvironmentController, ShellCmdException, BuildSetupException, BitbakeController
  32. import logging
  33. logger = logging.getLogger("toaster")
  34. from pprint import pprint, pformat
  35. class LocalhostBEController(BuildEnvironmentController):
  36. """ Implementation of the BuildEnvironmentController for the localhost;
  37. this controller manages the default build directory,
  38. the server setup and system start and stop for the localhost-type build environment
  39. """
  40. def __init__(self, be):
  41. super(LocalhostBEController, self).__init__(be)
  42. self.pokydirname = None
  43. self.islayerset = False
  44. def _shellcmd(self, command, cwd=None, nowait=False):
  45. if cwd is None:
  46. cwd = self.be.sourcedir
  47. logger.debug("lbc_shellcmmd: (%s) %s" % (cwd, command))
  48. p = subprocess.Popen(command, cwd = cwd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  49. if nowait:
  50. return
  51. (out,err) = p.communicate()
  52. p.wait()
  53. if p.returncode:
  54. if len(err) == 0:
  55. err = "command: %s \n%s" % (command, out)
  56. else:
  57. err = "command: %s \n%s" % (command, err)
  58. logger.warning("localhostbecontroller: shellcmd error %s" % err)
  59. raise ShellCmdException(err)
  60. else:
  61. logger.debug("localhostbecontroller: shellcmd success")
  62. return out
  63. def getGitCloneDirectory(self, url, branch):
  64. """Construct unique clone directory name out of url and branch."""
  65. if branch != "HEAD":
  66. return "_toaster_clones/_%s_%s" % (re.sub('[:/@%]', '_', url), branch)
  67. # word of attention; this is a localhost-specific issue; only on the localhost we expect to have "HEAD" releases
  68. # which _ALWAYS_ means the current poky checkout
  69. from os.path import dirname as DN
  70. local_checkout_path = DN(DN(DN(DN(DN(os.path.abspath(__file__))))))
  71. #logger.debug("localhostbecontroller: using HEAD checkout in %s" % local_checkout_path)
  72. return local_checkout_path
  73. def setLayers(self, bitbake, layers, targets):
  74. """ a word of attention: by convention, the first layer for any build will be poky! """
  75. assert self.be.sourcedir is not None
  76. # set layers in the layersource
  77. # 1. get a list of repos with branches, and map dirpaths for each layer
  78. gitrepos = {}
  79. gitrepos[(bitbake.giturl, bitbake.commit)] = []
  80. gitrepos[(bitbake.giturl, bitbake.commit)].append( ("bitbake", bitbake.dirpath) )
  81. for layer in layers:
  82. # We don't need to git clone the layer for the CustomImageRecipe
  83. # as it's generated by us layer on if needed
  84. if CustomImageRecipe.LAYER_NAME in layer.name:
  85. continue
  86. if not (layer.giturl, layer.commit) in gitrepos:
  87. gitrepos[(layer.giturl, layer.commit)] = []
  88. gitrepos[(layer.giturl, layer.commit)].append( (layer.name, layer.dirpath) )
  89. logger.debug("localhostbecontroller, our git repos are %s" % pformat(gitrepos))
  90. # 2. Note for future use if the current source directory is a
  91. # checked-out git repos that could match a layer's vcs_url and therefore
  92. # be used to speed up cloning (rather than fetching it again).
  93. cached_layers = {}
  94. try:
  95. for remotes in self._shellcmd("git remote -v", self.be.sourcedir).split("\n"):
  96. try:
  97. remote = remotes.split("\t")[1].split(" ")[0]
  98. if remote not in cached_layers:
  99. cached_layers[remote] = self.be.sourcedir
  100. except IndexError:
  101. pass
  102. except ShellCmdException:
  103. # ignore any errors in collecting git remotes this is an optional
  104. # step
  105. pass
  106. logger.info("Using pre-checked out source for layer %s", cached_layers)
  107. layerlist = []
  108. # 3. checkout the repositories
  109. for giturl, commit in gitrepos.keys():
  110. localdirname = os.path.join(self.be.sourcedir, self.getGitCloneDirectory(giturl, commit))
  111. logger.debug("localhostbecontroller: giturl %s:%s checking out in current directory %s" % (giturl, commit, localdirname))
  112. # make sure our directory is a git repository
  113. if os.path.exists(localdirname):
  114. localremotes = self._shellcmd("git remote -v", localdirname)
  115. if not giturl in localremotes:
  116. raise BuildSetupException("Existing git repository at %s, but with different remotes ('%s', expected '%s'). Toaster will not continue out of fear of damaging something." % (localdirname, ", ".join(localremotes.split("\n")), giturl))
  117. else:
  118. if giturl in cached_layers:
  119. logger.debug("localhostbecontroller git-copying %s to %s" % (cached_layers[giturl], localdirname))
  120. self._shellcmd("git clone \"%s\" \"%s\"" % (cached_layers[giturl], localdirname))
  121. self._shellcmd("git remote remove origin", localdirname)
  122. self._shellcmd("git remote add origin \"%s\"" % giturl, localdirname)
  123. else:
  124. logger.debug("localhostbecontroller: cloning %s in %s" % (giturl, localdirname))
  125. self._shellcmd('git clone "%s" "%s"' % (giturl, localdirname))
  126. # branch magic name "HEAD" will inhibit checkout
  127. if commit != "HEAD":
  128. logger.debug("localhostbecontroller: checking out commit %s to %s " % (commit, localdirname))
  129. ref = commit if re.match('^[a-fA-F0-9]+$', commit) else 'origin/%s' % commit
  130. self._shellcmd('git fetch --all && git reset --hard "%s"' % ref, localdirname)
  131. # take the localdirname as poky dir if we can find the oe-init-build-env
  132. if self.pokydirname is None and os.path.exists(os.path.join(localdirname, "oe-init-build-env")):
  133. logger.debug("localhostbecontroller: selected poky dir name %s" % localdirname)
  134. self.pokydirname = localdirname
  135. # make sure we have a working bitbake
  136. if not os.path.exists(os.path.join(self.pokydirname, 'bitbake')):
  137. logger.debug("localhostbecontroller: checking bitbake into the poky dirname %s " % self.pokydirname)
  138. self._shellcmd("git clone -b \"%s\" \"%s\" \"%s\" " % (bitbake.commit, bitbake.giturl, os.path.join(self.pokydirname, 'bitbake')))
  139. # verify our repositories
  140. for name, dirpath in gitrepos[(giturl, commit)]:
  141. localdirpath = os.path.join(localdirname, dirpath)
  142. logger.debug("localhostbecontroller: localdirpath expected '%s'" % localdirpath)
  143. if not os.path.exists(localdirpath):
  144. raise BuildSetupException("Cannot find layer git path '%s' in checked out repository '%s:%s'. Aborting." % (localdirpath, giturl, commit))
  145. if name != "bitbake":
  146. layerlist.append(localdirpath.rstrip("/"))
  147. logger.debug("localhostbecontroller: current layer list %s " % pformat(layerlist))
  148. # 5. create custom layer and add custom recipes to it
  149. layerpath = os.path.join(self.be.builddir,
  150. CustomImageRecipe.LAYER_NAME)
  151. for target in targets:
  152. try:
  153. customrecipe = CustomImageRecipe.objects.get(name=target.target,
  154. project=bitbake.req.project)
  155. except CustomImageRecipe.DoesNotExist:
  156. continue # not a custom recipe, skip
  157. # create directory structure
  158. for name in ("conf", "recipes"):
  159. path = os.path.join(layerpath, name)
  160. if not os.path.isdir(path):
  161. os.makedirs(path)
  162. # create layer.oonf
  163. config = os.path.join(layerpath, "conf", "layer.conf")
  164. if not os.path.isfile(config):
  165. with open(config, "w") as conf:
  166. conf.write('BBPATH .= ":${LAYERDIR}"\nBBFILES += "${LAYERDIR}/recipes/*.bb"\n')
  167. # Update the Layer_Version dirpath that has our base_recipe in
  168. # to be able to read the base recipe to then generate the
  169. # custom recipe.
  170. br_layer_base_recipe = layers.get(
  171. layer_version=customrecipe.base_recipe.layer_version)
  172. br_layer_base_dirpath = \
  173. os.path.join(self.be.sourcedir,
  174. self.getGitCloneDirectory(
  175. br_layer_base_recipe.giturl,
  176. br_layer_base_recipe.commit),
  177. customrecipe.base_recipe.layer_version.dirpath
  178. )
  179. customrecipe.base_recipe.layer_version.dirpath = \
  180. br_layer_base_dirpath
  181. customrecipe.base_recipe.layer_version.save()
  182. # create recipe
  183. recipe_path = \
  184. os.path.join(layerpath, "recipes", "%s.bb" % target.target)
  185. with open(recipe_path, "w") as recipef:
  186. recipef.write(customrecipe.generate_recipe_file_contents())
  187. # Update the layer and recipe objects
  188. customrecipe.layer_version.dirpath = layerpath
  189. customrecipe.layer_version.save()
  190. customrecipe.file_path = recipe_path
  191. customrecipe.save()
  192. # create *Layer* objects needed for build machinery to work
  193. BRLayer.objects.get_or_create(req=target.req,
  194. name=layer.name,
  195. dirpath=layerpath,
  196. giturl="file://%s" % layerpath)
  197. if os.path.isdir(layerpath):
  198. layerlist.append(layerpath)
  199. self.islayerset = True
  200. return layerlist
  201. def readServerLogFile(self):
  202. return open(os.path.join(self.be.builddir, "toaster_server.log"), "r").read()
  203. def triggerBuild(self, bitbake, layers, variables, targets, brbe):
  204. layers = self.setLayers(bitbake, layers, targets)
  205. # init build environment from the clone
  206. builddir = '%s-toaster-%d' % (self.be.builddir, bitbake.req.project.id)
  207. oe_init = os.path.join(self.pokydirname, 'oe-init-build-env')
  208. # init build environment
  209. self._shellcmd("bash -c 'source %s %s'" % (oe_init, builddir),
  210. self.be.sourcedir)
  211. # update bblayers.conf
  212. bblconfpath = os.path.join(builddir, "conf/bblayers.conf")
  213. conflines = open(bblconfpath, "r").readlines()
  214. skip = False
  215. with open(bblconfpath, 'w') as bblayers:
  216. for line in conflines:
  217. if line.startswith("# line added by toaster"):
  218. skip = True
  219. continue
  220. if skip:
  221. skip = False
  222. else:
  223. bblayers.write(line)
  224. bblayers.write('# line added by toaster build control\n'
  225. 'BBLAYERS = "%s"' % ' '.join(layers))
  226. # write configuration file
  227. confpath = os.path.join(builddir, 'conf/toaster.conf')
  228. with open(confpath, 'w') as conf:
  229. for var in variables:
  230. conf.write('%s="%s"\n' % (var.name, var.value))
  231. conf.write('INHERIT+="toaster buildhistory"')
  232. # run bitbake server from the clone
  233. bitbake = os.path.join(self.pokydirname, 'bitbake', 'bin', 'bitbake')
  234. self._shellcmd('bash -c \"source %s %s; BITBAKE_UI="" %s --read %s '
  235. '--server-only -t xmlrpc -B 0.0.0.0:0\"' % (oe_init,
  236. builddir, bitbake, confpath), self.be.sourcedir)
  237. # read port number from bitbake.lock
  238. self.be.bbport = ""
  239. bblock = os.path.join(builddir, 'bitbake.lock')
  240. with open(bblock) as fplock:
  241. for line in fplock:
  242. if ":" in line:
  243. self.be.bbport = line.split(":")[-1].strip()
  244. logger.debug("localhostbecontroller: bitbake port %s", self.be.bbport)
  245. break
  246. if not self.be.bbport:
  247. raise BuildSetupException("localhostbecontroller: can't read bitbake port from %s" % bblock)
  248. self.be.bbaddress = "localhost"
  249. self.be.bbstate = BuildEnvironment.SERVER_STARTED
  250. self.be.lock = BuildEnvironment.LOCK_RUNNING
  251. self.be.save()
  252. bbtargets = ''
  253. for target in targets:
  254. task = target.task
  255. if task:
  256. if not task.startswith('do_'):
  257. task = 'do_' + task
  258. task = ':%s' % task
  259. bbtargets += '%s%s ' % (target.target, task)
  260. # run build with local bitbake. stop the server after the build.
  261. log = os.path.join(builddir, 'toaster_ui.log')
  262. local_bitbake = os.path.join(os.path.dirname(os.getenv('BBBASEDIR')),
  263. 'bitbake')
  264. self._shellcmd(['bash -c \"(TOASTER_BRBE="%s" BBSERVER="0.0.0.0:-1" '
  265. '%s %s -u toasterui --token="" >>%s 2>&1;'
  266. 'BITBAKE_UI="" BBSERVER=0.0.0.0:-1 %s -m)&\"' \
  267. % (brbe, local_bitbake, bbtargets, log, bitbake)],
  268. builddir, nowait=True)
  269. logger.debug('localhostbecontroller: Build launched, exiting. '
  270. 'Follow build logs at %s' % log)