diff --git a/etc/bash_completion.d/rpkg.bash b/etc/bash_completion.d/rpkg.bash
index 9dd8de3..c564afc 100644
--- a/etc/bash_completion.d/rpkg.bash
+++ b/etc/bash_completion.d/rpkg.bash
@@ -34,8 +34,8 @@ _rpkg()
local options="--help -v -q"
local options_value="--dist --release --user --path"
- local commands="build chain-build ci clean clog clone co container-build container-build-config commit compile copr-build diff gimmespec giturl help \
- gitbuildhash import install lint local mockbuild mock-config new new-sources patch prep pull push scratch-build sources \
+ local commands="build chain-build ci clean clog clone co container-build container-build-config commit compile copr-build diff flatpak-build \
+ gimmespec giturl help gitbuildhash import install lint local mockbuild mock-config new new-sources patch prep pull push scratch-build sources \
srpm switch-branch tag unused-patches upload verify-files verrel"
# parse main options and get command
@@ -152,6 +152,12 @@ _rpkg()
after="file"
after_more=true
;;
+ flatpak-build)
+ options="--scratch --nowait"
+ options_target="--target"
+ options_string="--repo-url"
+ options_arches="--arches"
+ ;;
import)
options="--create"
options_branch="--branch"
diff --git a/pyrpkg/__init__.py b/pyrpkg/__init__.py
index 696c9c3..aa23cbd 100644
--- a/pyrpkg/__init__.py
+++ b/pyrpkg/__init__.py
@@ -39,6 +39,7 @@ from six.moves import configparser
from six.moves import urllib
from six.moves.urllib.parse import urljoin
import requests
+import yaml
from pyrpkg.errors import HashtypeMixingError, rpkgError, rpkgAuthError, \
UnknownTargetError
@@ -47,6 +48,13 @@ from pyrpkg.lookaside import CGILookasideCache
from pyrpkg.sources import SourcesFile
from pyrpkg.utils import cached_property, log_result, find_me
+PY26 = sys.version_info < (2, 7, 0)
+
+if not PY26:
+ import gi
+ gi.require_version('Modulemd', '1.0') # raises ValueError
+ from gi.repository import Modulemd # noqa
+
class NullHandler(logging.Handler):
"""Null logger to avoid spurious messages, add a handler in app code"""
@@ -156,6 +164,8 @@ class Commands(object):
self._target = target
# The build target for containers within the buildsystem
self._container_build_target = target
+ # The build target for flatpaks within the buildsystem
+ self._flatpak_build_target = target
# The top url to our build server
self._topurl = None
# The user to use or discover
@@ -191,6 +201,8 @@ class Commands(object):
self.lookaside_namespaced = lookaside_namespaced
# Deprecates self.module_name
self._repo_name = None
+ # API URL for the module build server
+ self.module_api_url = None
# Define properties here
# Properties allow us to "lazy load" various attributes, which also means
@@ -887,6 +899,99 @@ class Commands(object):
"""This creates a target based on git branch and namespace."""
self._container_build_target = '%s-%s-candidate' % (self.branch_merge, self.ns)
+ @property
+ def flatpak_build_target(self):
+ """This property ensures the target for container builds."""
+ if not self._flatpak_build_target:
+ self.load_flatpak_build_target()
+ return self._flatpak_build_target
+
+ def _find_platform_stream(self, name, stream, version=None):
+ """Recursively search for the platform module in dependencies to find its stream.
+
+ The stream of the 'platform' pseudo-module determines what base package set
+ we need for the runtime - and thus what build target we need.
+ """
+
+ if version is not None:
+ nsvc = name + ':' + stream + ':' + version
+ else:
+ nsvc = name + ':' + stream
+
+ build = self.module_get_latest_build(nsvc)
+
+ if build is None:
+ raise rpkgError("Cannot find any builds for module %s:%s" %
+ (name, stream))
+
+ mmd_str = build['modulemd']
+
+ objects = Modulemd.objects_from_string(mmd_str)
+ modules = [o for o in objects if isinstance(o, Modulemd.Module)]
+ if len(modules) != 1:
+ raise rpkgError("Failed to load modulemd for %s" % nsvc)
+
+ # Streams should already be expanded in the modulemd's that we retrieve
+ # from MBS - modules were built against a particular dependency.
+ def get_stream(req, req_stream):
+ req_stream_list = req_stream.get()
+ if len(req_stream_list) != 1:
+ raise rpkgError("%s: stream list for '%s' is not expanded (%s)" %
+ (nsvc, req, req_stream_list))
+ return req_stream_list[0]
+
+ # We first look for 'platform' as a direct dependency of this module,
+ # before recursing into the dependencies
+ for dep in modules[0].peek_dependencies():
+ for req, req_stream in dep.peek_requires().items():
+ if req == 'platform':
+ return get_stream(req, req_stream)
+
+ # Now recurse into the dependencies
+ for dep in modules[0].peek_dependencies():
+ for req, req_stream in dep.peek_requires().items():
+ platform_stream = self._find_platform_stream(req,
+ get_stream(req, req_stream))
+ if platform_stream:
+ return platform_stream
+
+ return None
+
+ def load_flatpak_build_target(self):
+ """This locates a target appropriate for the runtime that the Flatpak targets."""
+
+ # Find the module we are going to build from container.yaml
+ yaml_path = os.path.join(self.path, "container.yaml")
+ if not os.path.exists(yaml_path):
+ raise rpkgError("Cannot find 'container.yaml' to determine build target.")
+
+ with open(yaml_path) as f:
+ container_yaml = yaml.safe_load(f)
+
+ compose = container_yaml.get('compose', {})
+ modules = compose.get('modules', [])
+ if not modules:
+ raise rpkgError("No modules listed in 'container.yaml'")
+ if len(modules) > 1:
+ raise rpkgError("Multiple modules listed in 'container.yaml'")
+ module = modules[0]
+
+ parts = module.split(':')
+ if len(parts) == 2:
+ name, stream = parts
+ version = None
+ elif len(parts) == 3:
+ name, stream, version = parts
+ else:
+ raise rpkgError("Module in container.yaml should be NAME:STREAM[:VERSION]")
+
+ platform_stream = self._find_platform_stream(name, stream, version=version)
+ if platform_stream is None:
+ raise rpkgError("Unable to find 'platform' module in the dependencies of '%s'; "
+ "can't determine target" % module)
+
+ self._flatpak_build_target = '%s-flatpak-candidate' % platform_stream
+
@property
def topurl(self):
"""This property ensures the topurl attribute"""
@@ -2824,10 +2929,14 @@ class Commands(object):
kojiconfig=None, kojiprofile=None,
build_client=None,
koji_task_watcher=None,
- nowait=False):
+ nowait=False,
+ flatpak=False):
+
# check if repo is dirty and all commits are pushed
self.check_repo()
- container_target = self.target if target_override else self.container_build_target
+ container_target = self.target if target_override \
+ else self.flatpak_build_target if flatpak \
+ else self.container_build_target
# This is for backward-compatibility of deprecated kojiconfig.
# Signature of container_build_koji is not changed in case someone
@@ -2871,6 +2980,9 @@ class Commands(object):
raise rpkgError('Cannot override arches for non-scratch builds')
task_opts['arch_override'] = ' '.join(arches)
+ if flatpak:
+ task_opts['flatpak'] = True
+
priority = opts.get("priority", None)
task_id = self.kojisession.buildContainer(source,
container_target,
@@ -2933,13 +3045,12 @@ class Commands(object):
cmd.extend([project, srpm_name])
self._run_command(cmd)
- def module_build_cancel(self, api_url, build_id, auth_method,
+ def module_build_cancel(self, build_id, auth_method,
oidc_id_provider=None, oidc_client_id=None,
oidc_client_secret=None, oidc_scopes=None):
"""
Cancel an MBS build
- :param str api_url: URL of the MBS API
:param int build_id: build ID to cancel
:param str auth_method: authentication method used by the MBS
:kwarg str oidc_id_provider: the OIDC provider when MBS is using OIDC
@@ -2953,8 +3064,8 @@ class Commands(object):
for authentication
"""
# Make sure the build they are trying to cancel exists
- self.module_get_build(api_url, build_id)
- url = self.module_get_url(api_url, build_id, action='PATCH')
+ self.module_get_build(build_id)
+ url = self.module_get_url(build_id, action='PATCH')
resp = self.module_send_authorized_request(
'PATCH', url, {'state': 'failed'}, auth_method, oidc_id_provider,
oidc_client_id, oidc_client_secret, oidc_scopes, timeout=60)
@@ -2967,18 +3078,17 @@ class Commands(object):
'The cancellation of module build #{0} failed with:\n{1}'
.format(build_id, error_msg))
- def module_build_info(self, api_url, build_id):
+ def module_build_info(self, build_id):
"""
Show information about an MBS build
- :param str api_url: URL of the MBS API
:param int build_id: build ID to query MBS about
"""
# Load the Koji session anonymously so we get access to the Koji web
# URL
self.load_kojisession(anon=True)
state_names = self.module_get_koji_state_dict()
- data = self.module_get_build(api_url, build_id)
+ data = self.module_get_build(build_id)
print('Name: {0}'.format(data['name']))
print('Stream: {0}'.format(data['stream']))
print('Version: {0}'.format(data['version']))
@@ -3000,14 +3110,13 @@ class Commands(object):
state_names[task_data.get('state', None)]))
print(' Koji Task: {0}\n'.format(koji_task_url))
- def module_get_build(self, api_url, build_id):
+ def module_get_build(self, build_id):
"""
Get an MBS build
- :param api_url: a string of the URL of the MBS API
:param build_id: an integer of the build ID to query MBS about
:return: None or a dictionary representing the module build
"""
- url = self.module_get_url(api_url, build_id)
+ url = self.module_get_url(build_id)
response = requests.get(url, timeout=60)
if response.ok:
return response.json()
@@ -3020,11 +3129,42 @@ class Commands(object):
'The following error occurred while getting information on '
'module build #{0}:\n{1}'.format(build_id, error_msg))
- def module_get_url(self, api_url, build_id, action='GET'):
+ def module_get_latest_build(self, nsvc):
+ """
+ Get the latest MBS build for a particular module. If the module is
+ built with multiple contexts, a random one will be returned.
+
+ :param nsvc: a NAME:STREAM:VERSION:CONTEXT to filter the query
+ (may be partial - e.g. only NAME or only NAME:STREAM)
+ :return: the latest build
+ """
+ url = self.module_get_url(None)
+ params = {
+ 'nsvc': nsvc,
+ 'order_desc_by': 'version',
+ 'per_page': 1
+ }
+
+ response = requests.get(url, timeout=60, params=params)
+ if response.ok:
+ j = response.json()
+ if len(j['items']) == 0:
+ return None
+ else:
+ return j['items'][0]
+ else:
+ try:
+ error_msg = response.json()['message']
+ except (ValueError, KeyError):
+ error_msg = response.text
+ raise rpkgError(
+ 'The following error occurred while getting information on '
+ 'module #{0}:\n{1}'.format(nsvc, error_msg))
+
+ def module_get_url(self, build_id, action='GET'):
"""
Get the proper MBS API URL for the desired action
- :param str api_url: a string of the URL of the MBS API
:param int build_id: an integer of the module build desired. If this is
set to None, then the base URL for all module builds is returned.
:param str action: a string determining the HTTP action. If this is set
@@ -3033,7 +3173,7 @@ class Commands(object):
:return: a string of the desired MBS API URL.
:rtype: str
"""
- url = urljoin(api_url, 'module-builds/')
+ url = urljoin(self.module_api_url, 'module-builds/')
if build_id is not None:
url = '{0}{1}'.format(url, build_id)
else:
@@ -3172,11 +3312,10 @@ class Commands(object):
else:
raise
- def module_overview(self, api_url, limit=10, finished=True):
+ def module_overview(self, limit=10, finished=True):
"""
Show the overview of the latest builds in MBS
- :param str api_url: a string of the URL of the MBS API
:param int limit: an integer of the number of most recent module builds
to display. This defaults to 10.
:param bool finished: a boolean that determines if only finished or
@@ -3193,7 +3332,7 @@ class Commands(object):
'failed': 4,
'ready': 5,
}
- baseurl = self.module_get_url(api_url, build_id=None)
+ baseurl = self.module_get_url(build_id=None)
if finished:
# These are the states when a build is finished
states = [build_states['done'], build_states['ready'],
@@ -3339,14 +3478,13 @@ class Commands(object):
raise rpkgError('An unsupported MBS "auth_method" was provided')
return resp
- def module_submit_build(self, api_url, scm_url, branch, auth_method,
+ def module_submit_build(self, scm_url, branch, auth_method,
optional=None, oidc_id_provider=None,
oidc_client_id=None, oidc_client_secret=None,
oidc_scopes=None):
"""
Submit a module build to the MBS
- :param api_url: a string of the URL of the MBS API
:param scm_url: a string of the module's SCM URL
:param branch: a string of the module's branch
:param str auth_method: a string of the authentication method used by
@@ -3378,7 +3516,7 @@ class Commands(object):
'Optional arguments are not in the proper "key=value" format')
body.update(optional_dict)
- url = self.module_get_url(api_url, build_id=None, action='POST')
+ url = self.module_get_url(build_id=None, action='POST')
resp = self.module_send_authorized_request(
'POST', url, body, auth_method, oidc_id_provider, oidc_client_id,
oidc_client_secret, oidc_scopes, timeout=120)
@@ -3399,13 +3537,12 @@ class Commands(object):
builds = data if isinstance(data, list) else [data]
return [build['id'] for build in builds]
- def module_watch_build(self, api_url, build_ids):
+ def module_watch_build(self, build_ids):
"""
Watches the first MBS build in the list in a loop that updates every 15
seconds. The loop ends when the build state is 'failed', 'done', or
'ready'.
- :param str api_url: a string of the URL of the MBS API
:param build_ids: a list of module build IDs
:type build_ids: list[int]
"""
@@ -3423,7 +3560,7 @@ class Commands(object):
done = False
while not done:
state_names = self.module_get_koji_state_dict()
- build = self.module_get_build(api_url, build_id)
+ build = self.module_get_build(build_id)
tasks = {}
if 'rpms' in build['tasks']:
tasks = build['tasks']['rpms']
@@ -3494,6 +3631,10 @@ class Commands(object):
:param api_url: a string of the URL of the MBS API
:return: an int of the API version
"""
+
+ # We don't use self.module_api_url since this is used exclusively by the code
+ # that is loading and validating the API URL before setting it.
+
url = '{0}/about/'.format(api_url.rstrip('/'))
response = requests.get(url, timeout=60)
if response.ok:
diff --git a/pyrpkg/cli.py b/pyrpkg/cli.py
index 44beb9c..36fe710 100644
--- a/pyrpkg/cli.py
+++ b/pyrpkg/cli.py
@@ -31,7 +31,7 @@ import koji_cli.lib
import pyrpkg.utils as utils
import six
-from pyrpkg import rpkgError, log as rpkgLogger
+from pyrpkg import PY26, rpkgError, log as rpkgLogger
from six.moves import configparser
@@ -424,6 +424,7 @@ class cliClient(object):
# Add a common parsers
self.register_build_common()
+ self.register_container_build_common()
self.register_rpm_common()
# Other targets
@@ -438,6 +439,8 @@ class cliClient(object):
self.register_container_build()
self.register_container_build_setup()
self.register_diff()
+ if not PY26:
+ self.register_flatpak_build()
self.register_gimmespec()
self.register_gitbuildhash()
self.register_gitcred()
@@ -1379,11 +1382,54 @@ see API KEY section of copr-cli(1) man page.
'verrel', help='Print the name-version-release')
verrel_parser.set_defaults(command=self.verrel)
+ def register_container_build_common(self):
+ parser = ArgumentParser(
+ 'container_build_common', add_help=False, allow_abbrev=False)
+
+ self.container_build_parser_common = parser
+
+ parser.add_argument(
+ '--target',
+ help='Override the default target',
+ default=None)
+
+ parser.add_argument(
+ '--nowait',
+ action='store_true',
+ default=False,
+ help="Don't wait on build")
+
+ parser.add_argument(
+ '--scratch',
+ help='Scratch build',
+ action="store_true")
+
+ parser.add_argument(
+ '--arches',
+ action='store',
+ nargs='*',
+ help='Limit a scratch build to an arch. May have multiple arches.')
+
+ parser.add_argument(
+ '--skip-remote-rules-validation',
+ action='store_true',
+ default=False,
+ help="Don't check if there's a valid gating.yaml file in the repo")
+
def register_container_build(self):
self.container_build_parser = self.subparsers.add_parser(
'container-build',
help='Build a container',
- description='Build a container')
+ description='Build a container',
+ parents=[self.container_build_parser_common])
+
+ # These arguments are specific to non-Flatpak containers
+ #
+ # --compose-id is implemented for Flatpaks as a side-effect of the internal
+ # implementation, but it is unlikely to be useful to trigger through rpkg.
+ # -- signing-intent is not implemented for Flatpaks, though it could be useful
+ # --repo-url makes no sense for flaptaks, since they must be built from a
+ # compose of a single module.
group = self.container_build_parser.add_mutually_exclusive_group()
group.add_argument(
@@ -1405,35 +1451,16 @@ see API KEY section of copr-cli(1) man page.
'Cannot be used with --signing-intent or --compose-id',
nargs='+')
- self.container_build_parser.add_argument(
- '--target',
- help='Override the default target',
- default=None)
-
- self.container_build_parser.add_argument(
- '--nowait',
- action='store_true',
- default=False,
- help="Don't wait on build")
-
- self.container_build_parser.add_argument(
- '--scratch',
- help='Scratch build',
- action="store_true")
-
- self.container_build_parser.add_argument(
- '--arches',
- action='store',
- nargs='*',
- help='Limit a scratch build to an arch. May have multiple arches.')
+ self.container_build_parser.set_defaults(command=self.container_build)
- self.container_build_parser.add_argument(
- '--skip-remote-rules-validation',
- action='store_true',
- default=False,
- help="Don't check if there's a valid gating.yaml file in the repo")
+ def register_flatpak_build(self):
+ self.flatpak_build_parser = self.subparsers.add_parser(
+ 'flatpak-build',
+ help='Build a Flatpak',
+ description='Build a Flatpak',
+ parents=[self.container_build_parser_common])
- self.container_build_parser.set_defaults(command=self.container_build)
+ self.flatpak_build_parser.set_defaults(command=self.flatpak_build)
def register_container_build_setup(self):
self.container_build_setup_parser = \
@@ -1762,7 +1789,7 @@ see API KEY section of copr-cli(1) man page.
# Keep it around for backward compatibility
self.container_build()
- def container_build(self):
+ def container_build(self, flatpak=False):
target_override = False
# Override the target if we were supplied one
if self.args.target:
@@ -1771,11 +1798,15 @@ see API KEY section of copr-cli(1) man page.
opts = {"scratch": self.args.scratch,
"quiet": self.args.q,
- "yum_repourls": self.args.repo_url,
"git_branch": self.cmd.branch_merge,
- "arches": self.args.arches,
+ "arches": self.args.arches}
+
+ if not flatpak:
+ opts.update({
+ "yum_repourls": self.args.repo_url,
"compose_ids": self.args.compose_ids,
- "signing_intent": self.args.signing_intent}
+ "signing_intent": self.args.signing_intent,
+ })
section_name = "%s.container-build" % self.name
err_msg = "Missing %(option)s option in [%(plugin.section)s] section. " \
@@ -1808,6 +1839,10 @@ see API KEY section of copr-cli(1) man page.
self.check_remote_rules_gating()
+ # We use MBS to find information about the module to build into a Flatpak
+ if flatpak:
+ self.set_module_api_url()
+
self.cmd.container_build_koji(
target_override,
opts=opts,
@@ -1815,7 +1850,11 @@ see API KEY section of copr-cli(1) man page.
kojiprofile=kojiprofile,
build_client=build_client,
koji_task_watcher=koji_cli.lib.watch_tasks,
- nowait=self.args.nowait)
+ nowait=self.args.nowait,
+ flatpak=flatpak)
+
+ def flatpak_build(self):
+ self.container_build(flatpak=True)
def container_build_setup(self):
self.cmd.container_build_setup(get_autorebuild=self.args.get_autorebuild,
@@ -1923,7 +1962,7 @@ see API KEY section of copr-cli(1) man page.
def module_build(self):
"""Builds a module using MBS"""
- api_url = self.module_api_url
+ self.set_module_api_url()
self.module_validate_config()
scm_url, branch = self.cmd.module_get_scm_info(
self.args.scm_url, self.args.branch)
@@ -1933,7 +1972,7 @@ see API KEY section of copr-cli(1) man page.
if not self.args.q:
print('Submitting the module build...')
build_ids = self._cmd.module_submit_build(
- api_url, scm_url, branch, auth_method, self.args.optional,
+ scm_url, branch, auth_method, self.args.optional,
oidc_id_provider, oidc_client_id, oidc_client_secret, oidc_scopes)
if self.args.watch:
self.module_watch_build(build_ids)
@@ -1948,7 +1987,7 @@ see API KEY section of copr-cli(1) man page.
def module_build_cancel(self):
"""Cancel an MBS build"""
- api_url = self.module_api_url
+ self.set_module_api_url()
build_id = self.args.build_id
auth_method, oidc_id_provider, oidc_client_id, oidc_client_secret, \
oidc_scopes = self.module_get_auth_config()
@@ -1956,14 +1995,15 @@ see API KEY section of copr-cli(1) man page.
if not self.args.q:
print('Cancelling module build #{0}...'.format(build_id))
self.cmd.module_build_cancel(
- api_url, build_id, auth_method, oidc_id_provider, oidc_client_id,
+ build_id, auth_method, oidc_id_provider, oidc_client_id,
oidc_client_secret, oidc_scopes)
if not self.args.q:
print('The module build #{0} was cancelled'.format(build_id))
def module_build_info(self):
"""Show information about an MBS build"""
- self.cmd.module_build_info(self.module_api_url, self.args.build_id)
+ self.set_module_api_url()
+ self.cmd.module_build_info(self.args.build_id)
def module_build_local(self):
"""Build a module locally using mbs-manager"""
@@ -2072,14 +2112,18 @@ see API KEY section of copr-cli(1) man page.
api_url.rstrip('/'), api_version)
return self._module_api_url
+ def set_module_api_url(self):
+ self.cmd.module_api_url = self.module_api_url
+
def module_build_watch(self):
"""Watch an MBS build from the command-line"""
self.module_watch_build([self.args.build_id])
def module_overview(self):
"""Show the overview of the latest builds in the MBS"""
+ self.set_module_api_url()
self.cmd.module_overview(
- self.module_api_url, self.args.limit,
+ self.args.limit,
finished=(not self.args.unfinished))
def module_validate_config(self):
@@ -2134,7 +2178,8 @@ see API KEY section of copr-cli(1) man page.
:param build_ids: a list of module build IDs
:type build_ids: list[int]
"""
- self.cmd.module_watch_build(self.module_api_url, build_ids)
+ self.set_module_api_url()
+ self.cmd.module_watch_build(build_ids)
def new(self):
new_diff = self.cmd.new()
diff --git a/requirements/fedora-py2.txt b/requirements/fedora-py2.txt
index 0cf79de..e97c9c8 100644
--- a/requirements/fedora-py2.txt
+++ b/requirements/fedora-py2.txt
@@ -1,10 +1,13 @@
+libmodulemd
python2-cccolutils
python2-GitPython
+python2-gobject-base
python2-koji
python2-pycurl
python-six
python2-rpm # rpm-python originally
python2-requests
+PyYAML
# python2-openidc-client # used for MBS OIDC authentication
# python2-requests-kerberos # used for MBS Kerberos authentication
diff --git a/requirements/fedora-py3.txt b/requirements/fedora-py3.txt
index cc99912..cb71549 100644
--- a/requirements/fedora-py3.txt
+++ b/requirements/fedora-py3.txt
@@ -1,10 +1,13 @@
+libmodulemd
python3-cccolutils
python3-GitPython
+python3-gobject-base
python3-koji
python3-pycurl
python3-six
python3-rpm # rpm-python originally
python3-requests
+python3-yaml
# python3-openidc-client # used for MBS OIDC authentication
# python3-requests-kerberos # used for MBS Kerberos authentication
diff --git a/requirements/pypi.txt b/requirements/pypi.txt
index 5d35f3b..e4c43bb 100644
--- a/requirements/pypi.txt
+++ b/requirements/pypi.txt
@@ -8,6 +8,7 @@ koji >= 1.15
pycurl >= 7.19
requests
six >= 1.9.0
+PyYAML
# rpm-py-installer
#
diff --git a/tests/test_cli.py b/tests/test_cli.py
index ea35afe..745a97c 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -23,6 +23,7 @@ from six.moves import http_client
import git
import pyrpkg.cli
+from pyrpkg import PY26
try:
import openidc_client
@@ -197,7 +198,7 @@ class TestContainerBuildWithKoji(CliTestCase):
self.container_build_koji_patcher.start()
def tearDown(self):
- self.mock_container_build_koji.stop()
+ self.container_build_koji_patcher.stop()
super(TestContainerBuildWithKoji, self).tearDown()
def test_using_kojiprofile(self):
@@ -223,7 +224,8 @@ class TestContainerBuildWithKoji(CliTestCase):
kojiprofile='koji',
build_client=utils.build_client,
koji_task_watcher=koji_cli.lib.watch_tasks,
- nowait=False
+ nowait=False,
+ flatpak=False
)
def test_override_target(self):
@@ -250,7 +252,8 @@ class TestContainerBuildWithKoji(CliTestCase):
kojiprofile='koji',
build_client=utils.build_client,
koji_task_watcher=koji_cli.lib.watch_tasks,
- nowait=False
+ nowait=False,
+ flatpak=False
)
def test_using_deprecated_kojiconfig(self):
@@ -286,7 +289,8 @@ class TestContainerBuildWithKoji(CliTestCase):
kojiprofile=None,
build_client=utils.build_client,
koji_task_watcher=koji_cli.lib.watch_tasks,
- nowait=False
+ nowait=False,
+ flatpak=False
)
def test_use_container_build_own_config(self):
@@ -304,6 +308,43 @@ class TestContainerBuildWithKoji(CliTestCase):
self.assertEqual('koji-container', kwargs['kojiprofile'])
self.assertEqual('koji', kwargs['build_client'])
+ @unittest.skipIf(
+ PY26,
+ 'Skip on old Python versions where libmodulemd is not available.')
+ @patch('requests.get')
+ def test_flatpak(self, mock_get):
+ mock_rv = Mock()
+ mock_rv.ok = True
+ mock_rv.json.return_value = {
+ 'auth_method': 'oidc',
+ 'api_version': 2
+ }
+
+ mock_get.return_value = mock_rv
+
+ cli_cmd = ['rpkg', '--path', self.cloned_repo_path,
+ 'flatpak-build']
+
+ with patch('sys.argv', new=cli_cmd):
+ cli = self.new_cli()
+ cli.flatpak_build()
+
+ self.mock_container_build_koji.assert_called_once_with(
+ False,
+ opts={
+ 'scratch': False,
+ 'quiet': False,
+ 'git_branch': 'eng-rhel-7',
+ 'arches': None,
+ },
+ kojiconfig=None,
+ kojiprofile='koji',
+ build_client=utils.build_client,
+ koji_task_watcher=koji_cli.lib.watch_tasks,
+ nowait=False,
+ flatpak=True
+ )
+
class TestClog(CliTestCase):
diff --git a/tests/test_flatpak_build.py b/tests/test_flatpak_build.py
new file mode 100644
index 0000000..97ab456
--- /dev/null
+++ b/tests/test_flatpak_build.py
@@ -0,0 +1,305 @@
+import os
+import subprocess
+from textwrap import dedent
+
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest
+
+from mock import Mock, patch
+import requests
+
+from pyrpkg import PY26
+from utils import CommandTestCase
+
+EOG_MODULEMD = """
+document: modulemd
+version: 2
+data:
+ name: eog
+ stream: f28
+ version: 20170629213428
+ summary: Eye of GNOME Application Module
+ description: The Eye of GNOME image viewer (eog) is the official image viewer for
+ the GNOME desktop. It can view single image files in a variety of formats, as
+ well as large image collections.
+ license:
+ module: [MIT]
+ dependencies:
+ - buildrequires:
+ flatpak-runtime: [f28]
+ requires:
+ flatpak-runtime: [f28]
+ profiles:
+ default:
+ rpms: [eog]
+ components:
+ rpms: {}
+ xmd:
+ mbs: OMITTED
+"""
+
+FLATPAK_RUNTIME_MODULEMD = """
+document: modulemd
+version: 2
+data:
+ name: flatpak-runtime
+ stream: f28
+ version: 20170701152209
+ summary: Flatpak Runtime
+ description: Libraries and data files shared between applications
+ api:
+ rpms: [librsvg2, gnome-themes-standard, abattis-cantarell-fonts, rest, xkeyboard-config,
+ adwaita-cursor-theme, python3-gobject-base, json-glib, zenity, gsettings-desktop-schemas,
+ glib-networking, gobject-introspection, gobject-introspection-devel, flatpak-rpm-macros,
+ python3-gobject, gvfs-client, colord-libs, flatpak-runtime-config, hunspell-en-GB,
+ libsoup, glib2-devel, hunspell-en-US, at-spi2-core, gtk3, libXtst, adwaita-gtk2-theme,
+ libnotify, adwaita-icon-theme, libgcab1, libxkbcommon, libappstream-glib, python3-cairo,
+ gnome-desktop3, libepoxy, hunspell, libgusb, glib2, enchant, at-spi2-atk]
+ dependencies:
+ - buildrequires:
+ platform: [f28]
+ requires:
+ platform: [f28]
+ license:
+ module: [MIT]
+ profiles:
+ buildroot:
+ rpms: [flatpak-rpm-macros, flatpak-runtime-config]
+ runtime:
+ rpms: [libwayland-server, librsvg2, libX11, libfdisk, adwaita-cursor-theme,
+ libsmartcols, popt, gdbm, libglvnd, openssl-libs, gobject-introspection, systemd,
+ ncurses-base, lcms2, libpcap, crypto-policies, fontconfig, libacl, libwayland-cursor,
+ libseccomp, gmp, jbigkit-libs, bzip2-libs, libunistring, freetype, nettle,
+ libidn, python3-six, gtk2, gtk3, ca-certificates, libdrm, rest, lzo, libcap,
+ gnutls, pango, util-linux, basesystem, p11-kit, libgcab1, iptables-libs, dbus,
+ python3-gobject-base, cryptsetup-libs, krb5-libs, sqlite-libs, kmod-libs,
+ libmodman, libarchive, enchant, libXfixes, systemd-libs, shared-mime-info,
+ coreutils-common, libglvnd-glx, abattis-cantarell-fonts, cairo, audit-libs,
+ libwayland-client, libpciaccess, sed, libgcc, libXrender, json-glib, libxshmfence,
+ glib-networking, libdb, fedora-modular-repos, keyutils-libs, hwdata, glibc,
+ libproxy, python3-pyparsing, device-mapper, libgpg-error, system-python, shadow-utils,
+ libXtst, libstemmer, dbus-libs, libpng, cairo-gobject, libXau, pcre, python3-packaging,
+ at-spi2-core, gawk, mesa-libglapi, libXinerama, adwaita-gtk2-theme, libX11-common,
+ device-mapper-libs, python3-appdirs, libXrandr, bash, glibc-common, libselinux,
+ elfutils-libs, libxkbcommon, libjpeg-turbo, libuuid, atk, acl, libmount, lz4-libs,
+ ncurses, libgusb, glib2, python3, libpwquality, at-spi2-atk, libattr, libcrypt,
+ gnome-themes-standard, libtiff, harfbuzz, libstdc++, libXcomposite, xkeyboard-config,
+ libxcb, libnotify, systemd-pam, readline, libXxf86vm, python3-cairo, gtk-update-icon-cache,
+ python3-pip, mesa-libEGL, zenity, python3-gobject, libXcursor, tzdata, gvfs-client,
+ libverto, libblkid, cracklib, libusbx, libcroco, libdatrie, gdk-pixbuf2, libXi,
+ qrencode-libs, python3-libs, graphite2, mesa-libwayland-egl, mesa-libGL, pixman,
+ libXext, glibc-all-langpacks, info, grep, fedora-modular-release, setup, zlib,
+ libtasn1, libepoxy, hunspell, libsemanage, python3-setuptools, fontpackages-filesystem,
+ libsigsegv, hicolor-icon-theme, libxml2, expat, libgcrypt, emacs-filesystem,
+ gsettings-desktop-schemas, chkconfig, xz-libs, mesa-libgbm, libthai, coreutils,
+ colord-libs, libcap-ng, flatpak-runtime-config, elfutils-libelf, hunspell-en-GB,
+ libsoup, pam, hunspell-en-US, jasper-libs, p11-kit-trust, avahi-libs, elfutils-default-yama-scope,
+ libutempter, adwaita-icon-theme, ncurses-libs, libidn2, system-python-libs,
+ libffi, libXdamage, libglvnd-egl, libXft, cups-libs, ustr, libcom_err, libappstream-glib,
+ gnome-desktop3, gdk-pixbuf2-modules, libsepol, filesystem, gzip, mpfr]
+ sdk:
+ rpms: [gcc]
+ components:
+ rpms: {}
+ xmd:
+ flatpak:
+ # This gives information about how to map this module into Flatpak terms
+ # this is used when building application modules against this module.
+ branch: f28
+ runtimes: # Keys are profile names
+ runtime:
+ id: org.fedoraproject.Platform
+ sdk: org.fedoraproject.Sdk
+ sdk:
+ id: org.fedoraproject.Sdk
+ runtime: org.fedoraproject.Platform
+ mbs: OMITTED
+""" # noqa
+
+UNEXPANDED_MODULEMD = """
+document: modulemd
+version: 2
+data:
+ name: nodeps
+ stream: f28
+ version: 20181234567890
+ summary: No dependencies
+ description: This module has no deps
+ license:
+ module: [MIT]
+ dependencies:
+ - buildrequires:
+ platform: [f27, f28]
+ requires:
+ platform: [f27, f28]
+ components:
+ rpms: {}
+"""
+
+NODEPS_MODULEMD = """
+document: modulemd
+version: 2
+data:
+ name: nodeps
+ stream: f28
+ version: 20181234567890
+ summary: No dependencies
+ description: This module has no deps
+ license:
+ module: [MIT]
+ dependencies: []
+ components:
+ rpms: {}
+"""
+
+BUILDS = {
+ 'eog:f28': [
+ {'modulemd': EOG_MODULEMD}
+ ],
+ 'eog:f28:20170629213428': [
+ {'modulemd': EOG_MODULEMD}
+ ],
+ 'flatpak-runtime:f28': [
+ {'modulemd': FLATPAK_RUNTIME_MODULEMD}
+ ],
+ 'bad-modulemd:f28': [
+ {'modulemd': "BLAH"}
+ ],
+ 'unexpanded:f28': [
+ {'modulemd': UNEXPANDED_MODULEMD}
+ ],
+ 'nodeps:f28': [
+ {'modulemd': NODEPS_MODULEMD}
+ ],
+}
+
+
+@unittest.skipIf(
+ PY26,
+ 'Skip on old Python versions where libmodulemd is not available.')
+class FlatpakBuildCase(CommandTestCase):
+ def set_container_modules(self, container_modules):
+ with open(os.path.join(self.repo_path, 'container.yaml'), 'w') as f:
+ f.write(dedent("""\
+ compose:
+ modules: {0}
+ """.format(container_modules)))
+ git_cmds = [
+ ['git', 'add', 'container.yaml'],
+ ['git', 'commit', '-m', 'Update container.yaml'],
+ ]
+ for cmd in git_cmds:
+ self.run_cmd(cmd, cwd=self.repo_path,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ git_cmds = [
+ ['git', 'fetch', 'origin'],
+ ['git', 'reset', '--hard', 'origin/master'],
+ ]
+ for cmd in git_cmds:
+ self.run_cmd(cmd, cwd=self.cloned_repo_path,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+
+ def setUp(self):
+ super(FlatpakBuildCase, self).setUp()
+
+ self.cmd = self.make_commands()
+ self.cmd.module_api_url = "https://mbs.example.com/module-build-service/1/"
+
+ self.requests_get_p = patch('requests.get')
+ self.mock_requests_get = self.requests_get_p.start()
+
+ def mock_get(url, params=None, timeout=None):
+ nsvc = params['nsvc']
+ del params['nsvc']
+ self.assertEquals(params, {
+ 'order_desc_by': 'version',
+ 'per_page': 1
+ })
+
+ response = Mock(requests.Response)
+ response.ok = True
+ response.json.return_value = {'items': BUILDS.get(nsvc, [])}
+
+ return response
+
+ self.mock_requests_get.side_effect = mock_get
+
+ self.load_krb_user_p = patch('pyrpkg.Commands._load_krb_user')
+ self.mock_load_krb_user = self.load_krb_user_p.start()
+
+ session = Mock()
+ self.kojisession = session
+ session.system.listMethods.return_value = ['buildContainer']
+
+ def load_kojisession(self):
+ self._kojisession = session
+
+ self.load_kojisession_p = patch('pyrpkg.Commands.load_kojisession',
+ new=load_kojisession)
+ self.mock_load_kojisession = self.load_kojisession_p.start()
+
+ session.getBuildTarget.return_value = {'dest_tag': 'f28-flatpak'}
+ session.getTag.return_value = {'locked': False}
+
+ def tearDown(self):
+ self.requests_get_p.stop()
+ self.load_krb_user_p.stop()
+ self.load_kojisession_p.stop()
+
+ super(FlatpakBuildCase, self).tearDown()
+
+ def test_find_target(self):
+ self.set_container_modules(['eog:f28'])
+ assert self.cmd.flatpak_build_target == 'f28-flatpak-candidate'
+
+ def test_find_target_version(self):
+ self.set_container_modules(['eog:f28:20170629213428'])
+ assert self.cmd.flatpak_build_target == 'f28-flatpak-candidate'
+
+ def module_failure(self, container_modules, exception_str):
+ if container_modules is not None:
+ self.set_container_modules(container_modules)
+ with self.assertRaises(Exception) as e:
+ self.cmd.load_flatpak_build_target()
+ self.assertIn(exception_str, str(e.exception))
+
+ def test_find_target_no_container_yaml(self):
+ self.module_failure(None, "Cannot find 'container.yaml'")
+
+ def test_find_target_no_modules(self):
+ self.module_failure([], "No modules listed in 'container.yaml'")
+
+ def test_find_target_multiple_modules(self):
+ self.module_failure(['eog:f28', 'foo:f28'],
+ "Multiple modules listed in 'container.yaml'")
+
+ def test_find_target_bad_nsv(self):
+ self.module_failure(['NOT_A_MODULE'], "should be NAME:STREAM[:VERSION]")
+
+ def test_find_target_no_builds(self):
+ self.module_failure(['eog:f1'], "Cannot find any builds for module")
+
+ def test_find_target_bad_modulemd(self):
+ self.module_failure(['bad-modulemd:f28'], "Failed to load modulemd")
+
+ def test_find_target_unexpected(self):
+ self.module_failure(['unexpanded:f28'], "stream list for 'platform' is not expanded")
+
+ def test_find_target_no_platform(self):
+ self.module_failure(['nodeps:f28'], "Unable to find 'platform' module in the dependencies")
+
+ def test_flatpak_build(self):
+ self.set_container_modules(['eog:f28'])
+ self.cmd.container_build_koji(nowait=True, flatpak=True)
+
+ session = self.kojisession
+ session.getBuildTarget.assert_called_with('f28-flatpak-candidate')
+ session.getTag.assert_called_with('f28-flatpak')
+
+ session.buildContainer.assert_called()
+ args, kwargs = session.buildContainer.call_args
+ source, container_target, taskinfo = args
+
+ assert container_target == 'f28-flatpak-candidate'