This allows for kiwi descriptions that are compatible across multiple targets to be easily used without needless modifications. Additionally, it allows for custom values when preparing milestone releases without needlessly modifying the descriptions.
459 lines
19 KiB
Python
459 lines
19 KiB
Python
import os
|
|
import xml.dom.minidom
|
|
from fnmatch import fnmatch
|
|
|
|
import koji
|
|
import koji.util
|
|
from koji.tasks import ServerExit
|
|
from __main__ import BaseBuildTask, BuildImageTask, BuildRoot, SCM
|
|
|
|
|
|
class KiwiBuildTask(BuildImageTask):
|
|
Methods = ['kiwiBuild']
|
|
_taskWeight = 4.0
|
|
|
|
def get_nvrp(self, cfg):
|
|
try:
|
|
newxml = xml.dom.minidom.parse(cfg) # nosec
|
|
except Exception:
|
|
raise koji.GenericError(
|
|
f"Kiwi description {os.path.basename(cfg)} can't be parsed as XML.")
|
|
try:
|
|
image = newxml.getElementsByTagName('image')[0]
|
|
except IndexError:
|
|
raise koji.GenericError(
|
|
f"Kiwi description {os.path.basename(cfg)} doesn't contain <image> tag.")
|
|
|
|
name = image.getAttribute('name')
|
|
version = None
|
|
for preferences in image.getElementsByTagName('preferences'):
|
|
try:
|
|
version = preferences.getElementsByTagName('version')[0].childNodes[0].data
|
|
except Exception:
|
|
pass
|
|
profile = None
|
|
try:
|
|
for p in image.getElementsByTagName('profiles')[0].getElementsByTagName('profile'):
|
|
if p.getAttribute('image') == 'true':
|
|
profile = p.getAttribute('name')
|
|
except IndexError:
|
|
# missing profiles section
|
|
pass
|
|
if not version:
|
|
raise koji.BuildError("Description file doesn't contain preferences/version")
|
|
return name, version, profile
|
|
|
|
def handler(self, target, arches, desc_url, desc_path, opts=None):
|
|
target_info = self.session.getBuildTarget(target, strict=True)
|
|
build_tag = target_info['build_tag']
|
|
repo_info = self.getRepo(build_tag)
|
|
# check requested arches against build tag
|
|
buildconfig = self.session.getBuildConfig(build_tag)
|
|
if not buildconfig['arches']:
|
|
raise koji.BuildError("No arches for tag %(name)s [%(id)s]" % buildconfig)
|
|
tag_archlist = [koji.canonArch(a) for a in buildconfig['arches'].split()]
|
|
if arches:
|
|
for arch in arches:
|
|
if koji.canonArch(arch) not in tag_archlist:
|
|
raise koji.BuildError("Invalid arch for build tag: %s" % arch)
|
|
else:
|
|
arches = tag_archlist
|
|
|
|
if not opts:
|
|
opts = {}
|
|
if not opts.get('scratch'):
|
|
opts['scratch'] = False
|
|
if not opts.get('optional_arches'):
|
|
opts['optional_arches'] = []
|
|
if not buildconfig['extra'].get('mock.new_chroot', True):
|
|
opts['mount_dev'] = True
|
|
self.opts = opts
|
|
|
|
# get configuration
|
|
scm = SCM(desc_url)
|
|
scm.assert_allowed(allowed=self.options.allowed_scms,
|
|
session=self.session,
|
|
by_config=self.options.allowed_scms_use_config,
|
|
by_policy=self.options.allowed_scms_use_policy,
|
|
policy_data={
|
|
'user_id': self.taskinfo['owner'],
|
|
'channel': self.session.getChannel(self.taskinfo['channel_id'],
|
|
strict=True)['name'],
|
|
'scratch': opts['scratch'],
|
|
})
|
|
logfile = os.path.join(self.workdir, 'checkout.log')
|
|
self.run_callbacks('preSCMCheckout', scminfo=scm.get_info(),
|
|
build_tag=build_tag, scratch=opts['scratch'])
|
|
scmdir = self.workdir
|
|
koji.ensuredir(scmdir)
|
|
scmsrcdir = scm.checkout(scmdir, self.session,
|
|
self.getUploadDir(), logfile)
|
|
self.run_callbacks("postSCMCheckout",
|
|
scminfo=scm.get_info(),
|
|
build_tag=build_tag,
|
|
scratch=opts['scratch'],
|
|
srcdir=scmsrcdir)
|
|
|
|
path = os.path.join(scmsrcdir, desc_path)
|
|
|
|
name, version, default_profile = self.get_nvrp(path)
|
|
if opts.get('profile') or default_profile:
|
|
# package name is a combination of name + profile
|
|
# in case profiles are not used, let's use the standalone name
|
|
name = "%s-%s" % (name, opts.get('profile', default_profile))
|
|
|
|
bld_info = {}
|
|
if opts.get('version'):
|
|
version = opts['version']
|
|
if opts.get('release'):
|
|
release = opts['release']
|
|
else:
|
|
release = self.session.getNextRelease({'name': name, 'version': version})
|
|
if not opts['scratch']:
|
|
bld_info = self.initImageBuild(name, version, release, target_info, opts)
|
|
release = bld_info['release']
|
|
|
|
try:
|
|
subtasks = {}
|
|
canfail = []
|
|
self.logger.debug("Spawning jobs for image arches: %r" % (arches))
|
|
for arch in arches:
|
|
subtasks[arch] = self.session.host.subtask(
|
|
method='createKiwiImage',
|
|
arglist=[name, version, release, arch,
|
|
target_info, build_tag, repo_info,
|
|
desc_url, desc_path, opts],
|
|
label=arch, parent=self.id, arch=arch)
|
|
if arch in self.opts['optional_arches']:
|
|
canfail.append(subtasks[arch])
|
|
self.logger.debug("Got image subtasks: %r" % (subtasks))
|
|
self.logger.debug("Waiting on image subtasks (%s can fail)..." % canfail)
|
|
results = self.wait(list(subtasks.values()), all=True,
|
|
failany=True, canfail=canfail)
|
|
|
|
# if everything failed, fail even if all subtasks are in canfail
|
|
self.logger.debug('subtask results: %r', results)
|
|
all_failed = True
|
|
for result in results.values():
|
|
if not isinstance(result, dict) or 'faultCode' not in result:
|
|
all_failed = False
|
|
break
|
|
if all_failed:
|
|
raise koji.GenericError("all subtasks failed")
|
|
|
|
# determine ignored arch failures
|
|
ignored_arches = set()
|
|
for arch in arches:
|
|
if arch in self.opts['optional_arches']:
|
|
task_id = subtasks[arch]
|
|
result = results[task_id]
|
|
if isinstance(result, dict) and 'faultCode' in result:
|
|
ignored_arches.add(arch)
|
|
|
|
self.logger.debug('Image Results for hub: %s' % results)
|
|
results = {str(k): v for k, v in results.items()}
|
|
if opts['scratch']:
|
|
self.session.host.moveImageBuildToScratch(self.id, results)
|
|
else:
|
|
self.session.host.completeImageBuild(self.id, bld_info['id'], results)
|
|
except (SystemExit, ServerExit, KeyboardInterrupt):
|
|
# we do not trap these
|
|
raise
|
|
except Exception:
|
|
if not opts['scratch']:
|
|
if bld_info:
|
|
self.session.host.failBuild(self.id, bld_info['id'])
|
|
raise
|
|
|
|
# tag it
|
|
if not opts['scratch'] and not opts.get('skip_tag'):
|
|
tag_task_id = self.session.host.subtask(method='tagBuild',
|
|
arglist=[target_info['dest_tag'],
|
|
bld_info['id'], False, None, True],
|
|
label='tag', parent=self.id, arch='noarch')
|
|
self.wait(tag_task_id)
|
|
|
|
# report results
|
|
report = ''
|
|
if opts['scratch']:
|
|
respath = ', '.join(
|
|
[os.path.join(koji.pathinfo.work(),
|
|
koji.pathinfo.taskrelpath(tid)) for tid in subtasks.values()])
|
|
report += 'Scratch '
|
|
else:
|
|
respath = koji.pathinfo.imagebuild(bld_info)
|
|
report += 'image build results in: %s' % respath
|
|
return report
|
|
|
|
|
|
class KiwiCreateImageTask(BaseBuildTask):
|
|
Methods = ['createKiwiImage']
|
|
_taskWeight = 2.0
|
|
|
|
def prepareDescription(self, desc_path, name, version, repos, repo_releasever, arch):
|
|
# XML errors should have already been caught by parent task
|
|
newxml = xml.dom.minidom.parse(desc_path) # nosec
|
|
image = newxml.getElementsByTagName('image')[0]
|
|
|
|
# apply includes - kiwi can include only top-level nodes, so we can simply
|
|
# go through "include" elements and replace them with referred content (without
|
|
# doing it recursively)
|
|
for inc_node in image.getElementsByTagName('include'):
|
|
path = inc_node.getAttribute('from')
|
|
if path.startswith('this://'):
|
|
path = koji.util.joinpath(os.path.dirname(desc_path), path[7:])
|
|
else:
|
|
# we want to reject other protocols, e.g. file://, https://
|
|
# reachingoutside of repo
|
|
raise koji.GenericError(f"Unhandled include protocol in include path: {path}.")
|
|
inc = xml.dom.minidom.parse(path) # nosec
|
|
# every included xml has image root element again
|
|
try:
|
|
for node in list(inc.getElementsByTagName('image')[0].childNodes):
|
|
if node.nodeName != 'repository':
|
|
image.appendChild(node)
|
|
except IndexError:
|
|
raise koji.GenericError("Included file needs to contain <image> tag.")
|
|
image.removeChild(inc_node)
|
|
|
|
# remove remaining old repos
|
|
for old_repo in image.getElementsByTagName('repository'):
|
|
image.removeChild(old_repo)
|
|
|
|
# add koji ones
|
|
for repo in sorted(set(repos)):
|
|
repo_node = newxml.createElement('repository')
|
|
repo_node.setAttribute('type', 'rpm-md')
|
|
source = newxml.createElement('source')
|
|
source.setAttribute('path', repo)
|
|
repo_node.appendChild(source)
|
|
image.appendChild(repo_node)
|
|
|
|
image.setAttribute('name', name)
|
|
preferences = image.getElementsByTagName('preferences')[0]
|
|
|
|
# Handle version and release-version
|
|
preferences.getElementsByTagName('version')[0].childNodes[0].data = version
|
|
try:
|
|
preferences.getElementsByTagName('release-version')[0].childNodes[0].data \
|
|
= repo_releasever
|
|
except IndexError:
|
|
releasever_node = newxml.createElement('release-version')
|
|
text = newxml.createTextNode(repo_releasever)
|
|
releasever_node.appendChild(text)
|
|
preferences.appendChild(releasever_node)
|
|
|
|
types = []
|
|
for pref in image.getElementsByTagName('preferences'):
|
|
for type in pref.getElementsByTagName('type'):
|
|
# TODO: if type.getAttribute('primary') == 'true':
|
|
types.append(type.getAttribute('image'))
|
|
|
|
# write new file
|
|
newcfg = os.path.splitext(desc_path)[0] + f'.{arch}.kiwi'
|
|
with open(newcfg, 'wt') as f:
|
|
s = newxml.toprettyxml()
|
|
# toprettyxml adds too many whitespaces/newlines
|
|
s = '\n'.join([x for x in s.splitlines() if x.strip()])
|
|
f.write(s)
|
|
os.unlink(desc_path)
|
|
|
|
return newcfg, types
|
|
|
|
def getImagePackagesFromCache(self, cachepath):
|
|
"""
|
|
Read RPM header information from the yum cache available in the
|
|
given path. Returns a list of dictionaries for each RPM included.
|
|
"""
|
|
found = False
|
|
hdrlist = {}
|
|
fields = ['name', 'version', 'release', 'epoch', 'arch',
|
|
'buildtime', 'sigmd5']
|
|
for root, dirs, files in os.walk(cachepath):
|
|
for f in files:
|
|
if fnmatch(f, '*.rpm'):
|
|
pkgfile = os.path.join(root, f)
|
|
hdr = koji.get_header_fields(pkgfile, fields)
|
|
hdr['size'] = os.path.getsize(pkgfile)
|
|
hdr['payloadhash'] = koji.hex_string(hdr['sigmd5'])
|
|
del hdr['sigmd5']
|
|
hdrlist[os.path.basename(pkgfile)] = hdr
|
|
found = True
|
|
if not found:
|
|
raise koji.LiveCDError('No repos found in yum cache!')
|
|
return list(hdrlist.values())
|
|
|
|
def getImagePackages(self, result):
|
|
"""Proper handler for getting rpminfo from result list,
|
|
it need result list to contain payloadhash, etc. to work correctly"""
|
|
hdrlist = []
|
|
for line in open(result, 'rt'):
|
|
line = line.strip()
|
|
name, epoch, version, release, arch, disturl, license = line.split('|')
|
|
if epoch == '(none)':
|
|
epoch = None
|
|
else:
|
|
epoch = int(epoch)
|
|
hdrlist.append({
|
|
'name': name,
|
|
'epoch': epoch,
|
|
'version': version,
|
|
'release': release,
|
|
'arch': arch,
|
|
'payloadhash': '',
|
|
'size': 0,
|
|
'buildtime': 0,
|
|
})
|
|
|
|
return hdrlist
|
|
|
|
def handler(self, name, version, release, arch,
|
|
target_info, build_tag, repo_info,
|
|
desc_url, desc_path, opts=None):
|
|
self.opts = opts
|
|
build_tag = target_info['build_tag']
|
|
bind_opts = {'dirs': {}}
|
|
if self.opts.get('mount_dev'):
|
|
bind_opts['dirs']['/dev'] = '/dev'
|
|
broot = BuildRoot(self.session, self.options,
|
|
tag=build_tag,
|
|
arch=arch,
|
|
task_id=self.id,
|
|
repo_id=repo_info['id'],
|
|
install_group='kiwi-build',
|
|
setup_dns=True,
|
|
bind_opts=bind_opts)
|
|
broot.workdir = self.workdir
|
|
|
|
# create the mock chroot
|
|
self.logger.debug("Initializing kiwi buildroot")
|
|
broot.init()
|
|
self.logger.debug("Kiwi buildroot ready: " + broot.rootdir())
|
|
|
|
# get configuration
|
|
scm = SCM(desc_url)
|
|
scm.assert_allowed(allowed=self.options.allowed_scms,
|
|
session=self.session,
|
|
by_config=self.options.allowed_scms_use_config,
|
|
by_policy=self.options.allowed_scms_use_policy,
|
|
policy_data={
|
|
'user_id': self.taskinfo['owner'],
|
|
'channel': self.session.getChannel(self.taskinfo['channel_id'],
|
|
strict=True)['name'],
|
|
'scratch': self.opts.get('scratch', False)
|
|
})
|
|
logfile = os.path.join(self.workdir, 'checkout-%s.log' % arch)
|
|
self.run_callbacks('preSCMCheckout', scminfo=scm.get_info(),
|
|
build_tag=build_tag, scratch=self.opts.get('scratch', False))
|
|
scmdir = broot.tmpdir()
|
|
koji.ensuredir(scmdir)
|
|
scmsrcdir = scm.checkout(scmdir, self.session,
|
|
self.getUploadDir(), logfile)
|
|
self.run_callbacks("postSCMCheckout",
|
|
scminfo=scm.get_info(),
|
|
build_tag=build_tag,
|
|
scratch=self.opts.get('scratch', False),
|
|
srcdir=scmsrcdir)
|
|
|
|
# user repos
|
|
repos = self.opts.get('repos', [])
|
|
if self.opts.get('use_buildroot_repo', False):
|
|
path_info = koji.PathInfo(topdir=self.options.topurl)
|
|
repopath = path_info.repo(repo_info['id'], target_info['build_tag_name'])
|
|
baseurl = '%s/%s' % (repopath, arch)
|
|
self.logger.debug('BASEURL: %s' % baseurl)
|
|
repos.append(baseurl)
|
|
repo_releasever = self.opts.get('repo_releasever', version)
|
|
|
|
base_path = os.path.dirname(desc_path)
|
|
if opts.get('make_prep'):
|
|
cmd = ['make', 'prep']
|
|
rv = broot.mock(['--cwd', os.path.join(broot.tmpdir(within=True),
|
|
os.path.basename(scmsrcdir), base_path),
|
|
'--chroot', '--'] + cmd)
|
|
if rv:
|
|
raise koji.GenericError("Preparation step failed")
|
|
|
|
path = os.path.join(scmsrcdir, desc_path)
|
|
desc, types = self.prepareDescription(path, name, version, repos, repo_releasever, arch)
|
|
self.uploadFile(desc)
|
|
|
|
target_dir = '/builddir/result/image'
|
|
os.symlink( # symlink log to resultdir, so it is incrementally uploaded
|
|
os.path.join(broot.rootdir(), f'tmp/image-root.{arch}.log'),
|
|
os.path.join(broot.resultdir(), f'image-root.{arch}.log')
|
|
)
|
|
cmd = ['kiwi-ng']
|
|
if self.opts.get('profile'):
|
|
cmd.extend(['--profile', self.opts['profile']])
|
|
if self.opts.get('type'):
|
|
cmd.extend(['--type', self.opts['type']])
|
|
cmd.extend([
|
|
'--kiwi-file', os.path.basename(desc), # global option for image/system commands
|
|
'--debug',
|
|
'--logfile', f'/tmp/image-root.{arch}.log',
|
|
'system', 'build',
|
|
'--description', os.path.join(os.path.basename(scmsrcdir), base_path),
|
|
'--target-dir', target_dir,
|
|
])
|
|
for typeattr in self.opts.get('type_attr', []):
|
|
cmd.extend(['--set-type-attr', typeattr])
|
|
rv = broot.mock(['--cwd', broot.tmpdir(within=True), '--chroot', '--'] + cmd)
|
|
if rv:
|
|
raise koji.GenericError("Kiwi failed")
|
|
|
|
# rename artifacts accordingly to release
|
|
os.symlink( # symlink log to resultdir, so it is incrementally uploaded
|
|
os.path.join(broot.rootdir(), f'/tmp/kiwi-result-bundle.{arch}.log'),
|
|
os.path.join(broot.resultdir(), f'kiwi-result-bundle.{arch}.log')
|
|
)
|
|
bundle_dir = '/builddir/result/bundle'
|
|
cmd = ['kiwi-ng',
|
|
'--debug',
|
|
'--logfile', f'/tmp/kiwi-result-bundle.{arch}.log',
|
|
'result', 'bundle',
|
|
'--target-dir', target_dir,
|
|
'--bundle-dir', bundle_dir,
|
|
'--id', release]
|
|
if self.opts.get('result_bundle_name_format'):
|
|
cmd.extend(['--bundle-format', self.opts['result_bundle_name_format']])
|
|
rv = broot.mock(['--cwd', broot.tmpdir(within=True), '--chroot', '--'] + cmd)
|
|
if rv:
|
|
raise koji.GenericError("Kiwi failed")
|
|
|
|
imgdata = {
|
|
'arch': arch,
|
|
'task_id': self.id,
|
|
'logs': [
|
|
os.path.basename(desc),
|
|
],
|
|
'name': name,
|
|
'version': version,
|
|
'release': release,
|
|
'rpmlist': [],
|
|
'files': [],
|
|
}
|
|
|
|
bundle_path = os.path.join(broot.rootdir(), bundle_dir[1:])
|
|
for fname in os.listdir(bundle_path):
|
|
self.uploadFile(os.path.join(bundle_path, fname), remoteName=fname)
|
|
imgdata['files'].append(fname)
|
|
|
|
if not self.opts.get('scratch'):
|
|
if False:
|
|
# should be used after kiwi update
|
|
fpath = os.path.join(
|
|
bundle_path,
|
|
next(f for f in imgdata['files'] if f.endswith('.packages')),
|
|
)
|
|
hdrlist = self.getImagePackages(fpath)
|
|
else:
|
|
cachepath = os.path.join(broot.rootdir(), 'var/cache/kiwi/dnf')
|
|
hdrlist = self.getImagePackagesFromCache(cachepath)
|
|
broot.markExternalRPMs(hdrlist)
|
|
imgdata['rpmlist'] = hdrlist
|
|
|
|
broot.expire()
|
|
|
|
self.logger.error("Uploading image data: %s", imgdata)
|
|
return imgdata
|