debian-koji/plugins/builder/kiwi.py
Neal Gompa a5dd795043 kiwi: Add support for overriding version and releasever
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.
2024-08-26 10:02:43 -04:00

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