#
|
# BitBake Toaster Implementation
|
#
|
# Copyright (C) 2014 Intel Corporation
|
#
|
# SPDX-License-Identifier: GPL-2.0-only
|
#
|
|
import os
|
import re
|
import shutil
|
import time
|
from bldcontrol.models import BuildEnvironment, BuildRequest, Build
|
from orm.models import CustomImageRecipe, Layer, Layer_Version, Project, ToasterSetting
|
from orm.models import signal_runbuilds
|
import subprocess
|
|
from toastermain import settings
|
|
from bldcontrol.bbcontroller import BuildEnvironmentController, ShellCmdException, BuildSetupException
|
|
import logging
|
logger = logging.getLogger("toaster")
|
|
install_dir = os.environ.get('TOASTER_DIR')
|
|
from pprint import pformat
|
|
class LocalhostBEController(BuildEnvironmentController):
|
""" Implementation of the BuildEnvironmentController for the localhost;
|
this controller manages the default build directory,
|
the server setup and system start and stop for the localhost-type build environment
|
|
"""
|
|
def __init__(self, be):
|
super(LocalhostBEController, self).__init__(be)
|
self.pokydirname = None
|
self.islayerset = False
|
|
def _shellcmd(self, command, cwd=None, nowait=False,env=None):
|
if cwd is None:
|
cwd = self.be.sourcedir
|
if env is None:
|
env=os.environ.copy()
|
|
logger.debug("lbc_shellcmd: (%s) %s" % (cwd, command))
|
p = subprocess.Popen(command, cwd = cwd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
|
if nowait:
|
return
|
(out,err) = p.communicate()
|
p.wait()
|
if p.returncode:
|
if len(err) == 0:
|
err = "command: %s \n%s" % (command, out)
|
else:
|
err = "command: %s \n%s" % (command, err)
|
logger.warning("localhostbecontroller: shellcmd error %s" % err)
|
raise ShellCmdException(err)
|
else:
|
logger.debug("localhostbecontroller: shellcmd success")
|
return out.decode('utf-8')
|
|
def getGitCloneDirectory(self, url, branch):
|
"""Construct unique clone directory name out of url and branch."""
|
if branch != "HEAD":
|
return "_toaster_clones/_%s_%s" % (re.sub('[:/@+%]', '_', url), branch)
|
|
# word of attention; this is a localhost-specific issue; only on the localhost we expect to have "HEAD" releases
|
# which _ALWAYS_ means the current poky checkout
|
from os.path import dirname as DN
|
local_checkout_path = DN(DN(DN(DN(DN(os.path.abspath(__file__))))))
|
#logger.debug("localhostbecontroller: using HEAD checkout in %s" % local_checkout_path)
|
return local_checkout_path
|
|
def setCloneStatus(self,bitbake,status,total,current,repo_name):
|
bitbake.req.build.repos_cloned=current
|
bitbake.req.build.repos_to_clone=total
|
bitbake.req.build.progress_item=repo_name
|
bitbake.req.build.save()
|
|
def setLayers(self, bitbake, layers, targets):
|
""" a word of attention: by convention, the first layer for any build will be poky! """
|
|
assert self.be.sourcedir is not None
|
|
layerlist = []
|
nongitlayerlist = []
|
layer_index = 0
|
git_env = os.environ.copy()
|
# (note: add custom environment settings here)
|
|
# set layers in the layersource
|
|
# 1. get a list of repos with branches, and map dirpaths for each layer
|
gitrepos = {}
|
|
# if we're using a remotely fetched version of bitbake add its git
|
# details to the list of repos to clone
|
if bitbake.giturl and bitbake.commit:
|
gitrepos[(bitbake.giturl, bitbake.commit)] = []
|
gitrepos[(bitbake.giturl, bitbake.commit)].append(
|
("bitbake", bitbake.dirpath, 0))
|
|
for layer in layers:
|
# We don't need to git clone the layer for the CustomImageRecipe
|
# as it's generated by us layer on if needed
|
if CustomImageRecipe.LAYER_NAME in layer.name:
|
continue
|
|
# If we have local layers then we don't need clone them
|
# For local layers giturl will be empty
|
if not layer.giturl:
|
nongitlayerlist.append( "%03d:%s" % (layer_index,layer.local_source_dir) )
|
continue
|
|
if not (layer.giturl, layer.commit) in gitrepos:
|
gitrepos[(layer.giturl, layer.commit)] = []
|
gitrepos[(layer.giturl, layer.commit)].append( (layer.name,layer.dirpath,layer_index) )
|
layer_index += 1
|
|
|
logger.debug("localhostbecontroller, our git repos are %s" % pformat(gitrepos))
|
|
|
# 2. Note for future use if the current source directory is a
|
# checked-out git repos that could match a layer's vcs_url and therefore
|
# be used to speed up cloning (rather than fetching it again).
|
|
cached_layers = {}
|
|
try:
|
for remotes in self._shellcmd("git remote -v", self.be.sourcedir,env=git_env).split("\n"):
|
try:
|
remote = remotes.split("\t")[1].split(" ")[0]
|
if remote not in cached_layers:
|
cached_layers[remote] = self.be.sourcedir
|
except IndexError:
|
pass
|
except ShellCmdException:
|
# ignore any errors in collecting git remotes this is an optional
|
# step
|
pass
|
|
logger.info("Using pre-checked out source for layer %s", cached_layers)
|
|
# 3. checkout the repositories
|
clone_count=0
|
clone_total=len(gitrepos.keys())
|
self.setCloneStatus(bitbake,'Started',clone_total,clone_count,'')
|
for giturl, commit in gitrepos.keys():
|
self.setCloneStatus(bitbake,'progress',clone_total,clone_count,gitrepos[(giturl, commit)][0][0])
|
clone_count += 1
|
|
localdirname = os.path.join(self.be.sourcedir, self.getGitCloneDirectory(giturl, commit))
|
logger.debug("localhostbecontroller: giturl %s:%s checking out in current directory %s" % (giturl, commit, localdirname))
|
|
# see if our directory is a git repository
|
if os.path.exists(localdirname):
|
try:
|
localremotes = self._shellcmd("git remote -v",
|
localdirname,env=git_env)
|
# NOTE: this nice-to-have check breaks when using git remaping to get past firewall
|
# Re-enable later with .gitconfig remapping checks
|
#if not giturl in localremotes and commit != 'HEAD':
|
# 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))
|
pass
|
except ShellCmdException:
|
# our localdirname might not be a git repository
|
#- that's fine
|
pass
|
else:
|
if giturl in cached_layers:
|
logger.debug("localhostbecontroller git-copying %s to %s" % (cached_layers[giturl], localdirname))
|
self._shellcmd("git clone \"%s\" \"%s\"" % (cached_layers[giturl], localdirname),env=git_env)
|
self._shellcmd("git remote remove origin", localdirname,env=git_env)
|
self._shellcmd("git remote add origin \"%s\"" % giturl, localdirname,env=git_env)
|
else:
|
logger.debug("localhostbecontroller: cloning %s in %s" % (giturl, localdirname))
|
self._shellcmd('git clone "%s" "%s"' % (giturl, localdirname),env=git_env)
|
|
# branch magic name "HEAD" will inhibit checkout
|
if commit != "HEAD":
|
logger.debug("localhostbecontroller: checking out commit %s to %s " % (commit, localdirname))
|
ref = commit if re.match('^[a-fA-F0-9]+$', commit) else 'origin/%s' % commit
|
self._shellcmd('git fetch && git reset --hard "%s"' % ref, localdirname,env=git_env)
|
|
# take the localdirname as poky dir if we can find the oe-init-build-env
|
if self.pokydirname is None and os.path.exists(os.path.join(localdirname, "oe-init-build-env")):
|
logger.debug("localhostbecontroller: selected poky dir name %s" % localdirname)
|
self.pokydirname = localdirname
|
|
# make sure we have a working bitbake
|
if not os.path.exists(os.path.join(self.pokydirname, 'bitbake')):
|
logger.debug("localhostbecontroller: checking bitbake into the poky dirname %s " % self.pokydirname)
|
self._shellcmd("git clone -b \"%s\" \"%s\" \"%s\" " % (bitbake.commit, bitbake.giturl, os.path.join(self.pokydirname, 'bitbake')),env=git_env)
|
|
# verify our repositories
|
for name, dirpath, index in gitrepos[(giturl, commit)]:
|
localdirpath = os.path.join(localdirname, dirpath)
|
logger.debug("localhostbecontroller: localdirpath expects '%s'" % localdirpath)
|
if not os.path.exists(localdirpath):
|
raise BuildSetupException("Cannot find layer git path '%s' in checked out repository '%s:%s'. Aborting." % (localdirpath, giturl, commit))
|
|
if name != "bitbake":
|
layerlist.append("%03d:%s" % (index,localdirpath.rstrip("/")))
|
|
self.setCloneStatus(bitbake,'complete',clone_total,clone_count,'')
|
logger.debug("localhostbecontroller: current layer list %s " % pformat(layerlist))
|
|
# Resolve self.pokydirname if not resolved yet, consider the scenario
|
# where all layers are local, that's the else clause
|
if self.pokydirname is None:
|
if os.path.exists(os.path.join(self.be.sourcedir, "oe-init-build-env")):
|
logger.debug("localhostbecontroller: selected poky dir name %s" % self.be.sourcedir)
|
self.pokydirname = self.be.sourcedir
|
else:
|
# Alternatively, scan local layers for relative "oe-init-build-env" location
|
for layer in layers:
|
if os.path.exists(os.path.join(layer.layer_version.layer.local_source_dir,"..","oe-init-build-env")):
|
logger.debug("localhostbecontroller, setting pokydirname to %s" % (layer.layer_version.layer.local_source_dir))
|
self.pokydirname = os.path.join(layer.layer_version.layer.local_source_dir,"..")
|
break
|
else:
|
logger.error("pokydirname is not set, you will run into trouble!")
|
|
# 5. create custom layer and add custom recipes to it
|
for target in targets:
|
try:
|
customrecipe = CustomImageRecipe.objects.get(
|
name=target.target,
|
project=bitbake.req.project)
|
|
custom_layer_path = self.setup_custom_image_recipe(
|
customrecipe, layers)
|
|
if os.path.isdir(custom_layer_path):
|
layerlist.append("%03d:%s" % (layer_index,custom_layer_path))
|
|
except CustomImageRecipe.DoesNotExist:
|
continue # not a custom recipe, skip
|
|
layerlist.extend(nongitlayerlist)
|
logger.debug("\n\nset layers gives this list %s" % pformat(layerlist))
|
self.islayerset = True
|
|
# restore the order of layer list for bblayers.conf
|
layerlist.sort()
|
sorted_layerlist = [l[4:] for l in layerlist]
|
return sorted_layerlist
|
|
def setup_custom_image_recipe(self, customrecipe, layers):
|
""" Set up toaster-custom-images layer and recipe files """
|
layerpath = os.path.join(self.be.builddir,
|
CustomImageRecipe.LAYER_NAME)
|
|
# create directory structure
|
for name in ("conf", "recipes"):
|
path = os.path.join(layerpath, name)
|
if not os.path.isdir(path):
|
os.makedirs(path)
|
|
# create layer.conf
|
config = os.path.join(layerpath, "conf", "layer.conf")
|
if not os.path.isfile(config):
|
with open(config, "w") as conf:
|
conf.write('BBPATH .= ":${LAYERDIR}"\nBBFILES += "${LAYERDIR}/recipes/*.bb"\n')
|
|
# Update the Layer_Version dirpath that has our base_recipe in
|
# to be able to read the base recipe to then generate the
|
# custom recipe.
|
br_layer_base_recipe = layers.get(
|
layer_version=customrecipe.base_recipe.layer_version)
|
|
# If the layer is one that we've cloned we know where it lives
|
if br_layer_base_recipe.giturl and br_layer_base_recipe.commit:
|
layer_path = self.getGitCloneDirectory(
|
br_layer_base_recipe.giturl,
|
br_layer_base_recipe.commit)
|
# Otherwise it's a local layer
|
elif br_layer_base_recipe.local_source_dir:
|
layer_path = br_layer_base_recipe.local_source_dir
|
else:
|
logger.error("Unable to workout the dir path for the custom"
|
" image recipe")
|
|
br_layer_base_dirpath = os.path.join(
|
self.be.sourcedir,
|
layer_path,
|
customrecipe.base_recipe.layer_version.dirpath)
|
|
customrecipe.base_recipe.layer_version.dirpath = br_layer_base_dirpath
|
|
customrecipe.base_recipe.layer_version.save()
|
|
# create recipe
|
recipe_path = os.path.join(layerpath, "recipes", "%s.bb" %
|
customrecipe.name)
|
with open(recipe_path, "w") as recipef:
|
recipef.write(customrecipe.generate_recipe_file_contents())
|
|
# Update the layer and recipe objects
|
customrecipe.layer_version.dirpath = layerpath
|
customrecipe.layer_version.layer.local_source_dir = layerpath
|
customrecipe.layer_version.layer.save()
|
customrecipe.layer_version.save()
|
|
customrecipe.file_path = recipe_path
|
customrecipe.save()
|
|
return layerpath
|
|
|
def readServerLogFile(self):
|
return open(os.path.join(self.be.builddir, "toaster_server.log"), "r").read()
|
|
|
def triggerBuild(self, bitbake, layers, variables, targets, brbe):
|
layers = self.setLayers(bitbake, layers, targets)
|
is_merged_attr = bitbake.req.project.merged_attr
|
|
git_env = os.environ.copy()
|
# (note: add custom environment settings here)
|
try:
|
# insure that the project init/build uses the selected bitbake, and not Toaster's
|
del git_env['TEMPLATECONF']
|
del git_env['BBBASEDIR']
|
del git_env['BUILDDIR']
|
except KeyError:
|
pass
|
|
# init build environment from the clone
|
if bitbake.req.project.builddir:
|
builddir = bitbake.req.project.builddir
|
else:
|
builddir = '%s-toaster-%d' % (self.be.builddir, bitbake.req.project.id)
|
oe_init = os.path.join(self.pokydirname, 'oe-init-build-env')
|
# init build environment
|
try:
|
custom_script = ToasterSetting.objects.get(name="CUSTOM_BUILD_INIT_SCRIPT").value
|
custom_script = custom_script.replace("%BUILDDIR%" ,builddir)
|
self._shellcmd("bash -c 'source %s'" % (custom_script),env=git_env)
|
except ToasterSetting.DoesNotExist:
|
self._shellcmd("bash -c 'source %s %s'" % (oe_init, builddir),
|
self.be.sourcedir,env=git_env)
|
|
# update bblayers.conf
|
if not is_merged_attr:
|
bblconfpath = os.path.join(builddir, "conf/toaster-bblayers.conf")
|
with open(bblconfpath, 'w') as bblayers:
|
bblayers.write('# line added by toaster build control\n'
|
'BBLAYERS = "%s"' % ' '.join(layers))
|
|
# write configuration file
|
confpath = os.path.join(builddir, 'conf/toaster.conf')
|
with open(confpath, 'w') as conf:
|
for var in variables:
|
conf.write('%s="%s"\n' % (var.name, var.value))
|
conf.write('INHERIT+="toaster buildhistory"')
|
else:
|
# Append the Toaster-specific values directly to the bblayers.conf
|
bblconfpath = os.path.join(builddir, "conf/bblayers.conf")
|
bblconfpath_save = os.path.join(builddir, "conf/bblayers.conf.save")
|
shutil.copyfile(bblconfpath, bblconfpath_save)
|
with open(bblconfpath) as bblayers:
|
content = bblayers.readlines()
|
do_write = True
|
was_toaster = False
|
with open(bblconfpath,'w') as bblayers:
|
for line in content:
|
#line = line.strip('\n')
|
if 'TOASTER_CONFIG_PROLOG' in line:
|
do_write = False
|
was_toaster = True
|
elif 'TOASTER_CONFIG_EPILOG' in line:
|
do_write = True
|
elif do_write:
|
bblayers.write(line)
|
if not was_toaster:
|
bblayers.write('\n')
|
bblayers.write('#=== TOASTER_CONFIG_PROLOG ===\n')
|
bblayers.write('BBLAYERS = "\\\n')
|
for layer in layers:
|
bblayers.write(' %s \\\n' % layer)
|
bblayers.write(' "\n')
|
bblayers.write('#=== TOASTER_CONFIG_EPILOG ===\n')
|
# Append the Toaster-specific values directly to the local.conf
|
bbconfpath = os.path.join(builddir, "conf/local.conf")
|
bbconfpath_save = os.path.join(builddir, "conf/local.conf.save")
|
shutil.copyfile(bbconfpath, bbconfpath_save)
|
with open(bbconfpath) as f:
|
content = f.readlines()
|
do_write = True
|
was_toaster = False
|
with open(bbconfpath,'w') as conf:
|
for line in content:
|
#line = line.strip('\n')
|
if 'TOASTER_CONFIG_PROLOG' in line:
|
do_write = False
|
was_toaster = True
|
elif 'TOASTER_CONFIG_EPILOG' in line:
|
do_write = True
|
elif do_write:
|
conf.write(line)
|
if not was_toaster:
|
conf.write('\n')
|
conf.write('#=== TOASTER_CONFIG_PROLOG ===\n')
|
for var in variables:
|
if (not var.name.startswith("INTERNAL_")) and (not var.name == "BBLAYERS"):
|
conf.write('%s="%s"\n' % (var.name, var.value))
|
conf.write('#=== TOASTER_CONFIG_EPILOG ===\n')
|
|
# If 'target' is just the project preparation target, then we are done
|
for target in targets:
|
if "_PROJECT_PREPARE_" == target.target:
|
logger.debug('localhostbecontroller: Project has been prepared. Done.')
|
# Update the Build Request and release the build environment
|
bitbake.req.state = BuildRequest.REQ_COMPLETED
|
bitbake.req.save()
|
self.be.lock = BuildEnvironment.LOCK_FREE
|
self.be.save()
|
# Close the project build and progress bar
|
bitbake.req.build.outcome = Build.SUCCEEDED
|
bitbake.req.build.save()
|
# Update the project status
|
bitbake.req.project.set_variable(Project.PROJECT_SPECIFIC_STATUS,Project.PROJECT_SPECIFIC_CLONING_SUCCESS)
|
signal_runbuilds()
|
return
|
|
# clean the Toaster to build environment
|
env_clean = 'unset BBPATH;' # clean BBPATH for <= YP-2.4.0
|
|
# run bitbake server from the clone if available
|
# otherwise pick it from the PATH
|
bitbake = os.path.join(self.pokydirname, 'bitbake', 'bin', 'bitbake')
|
if not os.path.exists(bitbake):
|
logger.info("Bitbake not available under %s, will try to use it from PATH" %
|
self.pokydirname)
|
for path in os.environ["PATH"].split(os.pathsep):
|
if os.path.exists(os.path.join(path, 'bitbake')):
|
bitbake = os.path.join(path, 'bitbake')
|
break
|
else:
|
logger.error("Looks like Bitbake is not available, please fix your environment")
|
|
toasterlayers = os.path.join(builddir,"conf/toaster-bblayers.conf")
|
if not is_merged_attr:
|
self._shellcmd('%s bash -c \"source %s %s; BITBAKE_UI="knotty" %s --read %s --read %s '
|
'--server-only -B 0.0.0.0:0\"' % (env_clean, oe_init,
|
builddir, bitbake, confpath, toasterlayers), self.be.sourcedir)
|
else:
|
self._shellcmd('%s bash -c \"source %s %s; BITBAKE_UI="knotty" %s '
|
'--server-only -B 0.0.0.0:0\"' % (env_clean, oe_init,
|
builddir, bitbake), self.be.sourcedir)
|
|
# read port number from bitbake.lock
|
self.be.bbport = -1
|
bblock = os.path.join(builddir, 'bitbake.lock')
|
# allow 10 seconds for bb lock file to appear but also be populated
|
for lock_check in range(10):
|
if not os.path.exists(bblock):
|
logger.debug("localhostbecontroller: waiting for bblock file to appear")
|
time.sleep(1)
|
continue
|
if 10 < os.stat(bblock).st_size:
|
break
|
logger.debug("localhostbecontroller: waiting for bblock content to appear")
|
time.sleep(1)
|
else:
|
raise BuildSetupException("Cannot find bitbake server lock file '%s'. Aborting." % bblock)
|
|
with open(bblock) as fplock:
|
for line in fplock:
|
if ":" in line:
|
self.be.bbport = line.split(":")[-1].strip()
|
logger.debug("localhostbecontroller: bitbake port %s", self.be.bbport)
|
break
|
|
if -1 == self.be.bbport:
|
raise BuildSetupException("localhostbecontroller: can't read bitbake port from %s" % bblock)
|
|
self.be.bbaddress = "localhost"
|
self.be.bbstate = BuildEnvironment.SERVER_STARTED
|
self.be.lock = BuildEnvironment.LOCK_RUNNING
|
self.be.save()
|
|
bbtargets = ''
|
for target in targets:
|
task = target.task
|
if task:
|
if not task.startswith('do_'):
|
task = 'do_' + task
|
task = ':%s' % task
|
bbtargets += '%s%s ' % (target.target, task)
|
|
# run build with local bitbake. stop the server after the build.
|
log = os.path.join(builddir, 'toaster_ui.log')
|
local_bitbake = os.path.join(os.path.dirname(os.getenv('BBBASEDIR')),
|
'bitbake')
|
if not is_merged_attr:
|
self._shellcmd(['%s bash -c \"(TOASTER_BRBE="%s" BBSERVER="0.0.0.0:%s" '
|
'%s %s -u toasterui --read %s --read %s --token="" >>%s 2>&1;'
|
'BITBAKE_UI="knotty" BBSERVER=0.0.0.0:%s %s -m)&\"' \
|
% (env_clean, brbe, self.be.bbport, local_bitbake, bbtargets, confpath, toasterlayers, log,
|
self.be.bbport, bitbake,)],
|
builddir, nowait=True)
|
else:
|
self._shellcmd(['%s bash -c \"(TOASTER_BRBE="%s" BBSERVER="0.0.0.0:%s" '
|
'%s %s -u toasterui --token="" >>%s 2>&1;'
|
'BITBAKE_UI="knotty" BBSERVER=0.0.0.0:%s %s -m)&\"' \
|
% (env_clean, brbe, self.be.bbport, local_bitbake, bbtargets, log,
|
self.be.bbport, bitbake,)],
|
builddir, nowait=True)
|
|
logger.debug('localhostbecontroller: Build launched, exiting. '
|
'Follow build logs at %s' % log)
|