Blob Blame History Raw
From 0ef58ca01d680c74f9776bf593990e8e9cadb98d Mon Sep 17 00:00:00 2001
From: "Owen W. Taylor" <otaylor@fishsoup.net>
Date: Tue, 31 Jul 2018 09:14:35 -0400
Subject: [PATCH 3/3] Move to using flatpak-module-tools as a library

https://github.com/projectatomic/atomic-reactor/pull/1052

Instead of sharing code cut-and-paste between atomic-reactor and
https://pagure.io/flatpak-module-tools, make atomic-reactor use
flatpak-module-tools as a library for the shared code. This will
prevent the code getting out of sync, and make future maintenance
easier.

The flatpak-module-tools version has major improvements to how
the filesystem tree is postprocessed to make a Flatpak using configuration
options from container.yaml, needing some small adjustments of the
tests here.
---
 atomic_reactor/plugins/exit_koji_import.py         |   6 +-
 atomic_reactor/plugins/exit_koji_promote.py        |   6 +-
 .../plugins/pre_flatpak_create_dockerfile.py       | 152 ++-------
 .../plugins/pre_resolve_module_compose.py          |  12 +-
 .../plugins/prepub_flatpak_create_oci.py           | 368 +--------------------
 images/dockerhost-builder/Dockerfile               |   2 +-
 images/privileged-builder/Dockerfile               |   2 +-
 requirements-flatpak.txt                           |   1 +
 tests/flatpak.py                                   |  17 +-
 9 files changed, 57 insertions(+), 509 deletions(-)

diff --git a/atomic_reactor/plugins/exit_koji_import.py b/atomic_reactor/plugins/exit_koji_import.py
index 4b1e1c9..e59b557 100644
--- a/atomic_reactor/plugins/exit_koji_import.py
+++ b/atomic_reactor/plugins/exit_koji_import.py
@@ -24,6 +24,7 @@ from atomic_reactor.plugins.pre_reactor_config import get_openshift_session
 
 try:
     from atomic_reactor.plugins.pre_flatpak_create_dockerfile import get_flatpak_source_info
+    from atomic_reactor.plugins.pre_resolve_module_compose import get_compose_info
 except ImportError:
     # modulemd and/or pdc_client not available
     def get_flatpak_source_info(_):
@@ -353,7 +354,10 @@ class KojiImportPlugin(ExitPlugin):
 
         flatpak_source_info = get_flatpak_source_info(self.workflow)
         if flatpak_source_info is not None:
-            extra['image'].update(flatpak_source_info.koji_metadata())
+            compose_info = get_compose_info(self.workflow)
+            koji_metadata = compose_info.koji_metadata()
+            koji_metadata['flatpak'] = True
+            extra['image'].update(koji_metadata)
 
         koji_task_owner = get_koji_task_owner(self.session, koji_task_id).get('name')
         extra['submitter'] = self.session.getLoggedInUser()['name']
diff --git a/atomic_reactor/plugins/exit_koji_promote.py b/atomic_reactor/plugins/exit_koji_promote.py
index da33380..0fe1f47 100644
--- a/atomic_reactor/plugins/exit_koji_promote.py
+++ b/atomic_reactor/plugins/exit_koji_promote.py
@@ -30,6 +30,7 @@ from atomic_reactor.util import get_parent_image_koji_data
 
 try:
     from atomic_reactor.plugins.pre_flatpak_create_dockerfile import get_flatpak_source_info
+    from atomic_reactor.plugins.pre_resolve_module_compose import get_compose_info
 except ImportError:
     # modulemd and/or pdc_client not available
     def get_flatpak_source_info(_):
@@ -564,7 +565,10 @@ class KojiPromotePlugin(ExitPlugin):
 
         flatpak_source_info = get_flatpak_source_info(self.workflow)
         if flatpak_source_info is not None:
-            extra['image'].update(flatpak_source_info.koji_metadata())
+            compose_info = get_compose_info(self.workflow)
+            koji_metadata = compose_info.koji_metadata()
+            koji_metadata['flatpak'] = True
+            extra['image'].update(koji_metadata)
 
         resolve_comp_result = self.workflow.prebuild_results.get(PLUGIN_RESOLVE_COMPOSES_KEY)
         if resolve_comp_result:
diff --git a/atomic_reactor/plugins/pre_flatpak_create_dockerfile.py b/atomic_reactor/plugins/pre_flatpak_create_dockerfile.py
index 148be70..a53e0da 100644
--- a/atomic_reactor/plugins/pre_flatpak_create_dockerfile.py
+++ b/atomic_reactor/plugins/pre_flatpak_create_dockerfile.py
@@ -20,6 +20,8 @@ Example configuration:
 
 import os
 
+from flatpak_module_tools.flatpak_builder import FlatpakSourceInfo, FlatpakBuilder
+
 from atomic_reactor.constants import DOCKERFILE_FILENAME, YUM_REPOS_DIR
 from atomic_reactor.plugin import PreBuildPlugin
 from atomic_reactor.plugins.pre_resolve_module_compose import get_compose_info
@@ -52,67 +54,6 @@ RUN chroot /var/tmp/flatpak-build/ /bin/sh /tmp/cleanup.sh
 '''
 
 
-class FlatpakSourceInfo(object):
-    def __init__(self, flatpak_yaml, compose):
-        self.flatpak_yaml = flatpak_yaml
-        self.compose = compose
-
-        mmd = compose.base_module.mmd
-        # A runtime module must have a 'runtime' profile, but can have other
-        # profiles for SDKs, minimal runtimes, etc.
-        self.runtime = 'runtime' in mmd.peek_profiles()
-
-        module_spec = split_module_spec(compose.source_spec)
-        if module_spec.profile:
-            self.profile = module_spec.profile
-        elif self.runtime:
-            self.profile = 'runtime'
-        else:
-            self.profile = 'default'
-
-        assert self.profile in mmd.peek_profiles()
-
-    # The module for the Flatpak runtime that this app runs against
-    @property
-    def runtime_module(self):
-        assert not self.runtime
-        compose = self.compose
-
-        dependencies = compose.base_module.mmd.props.dependencies
-        # A built module should have its dependencies already expanded
-        assert len(dependencies) == 1
-
-        for key in dependencies[0].peek_buildrequires().keys():
-            try:
-                module = compose.modules[key]
-                if 'runtime' in module.mmd.peek_profiles():
-                    return module
-            except KeyError:
-                pass
-
-        raise RuntimeError("Failed to identify runtime module in the buildrequires for {}"
-                           .format(compose.base_module.name))
-
-    # All modules that were build against the Flatpak runtime,
-    # and thus were built with prefix=/app. This is primarily the app module
-    # but might contain modules shared between multiple flatpaks as well.
-    @property
-    def app_modules(self):
-        runtime_module_name = self.runtime_module.mmd.props.name
-
-        def is_app_module(m):
-            dependencies = m.mmd.props.dependencies
-            return runtime_module_name in dependencies[0].peek_buildrequires()
-
-        return [m for m in self.compose.modules.values() if is_app_module(m)]
-
-    def koji_metadata(self):
-        metadata = self.compose.koji_metadata()
-        metadata['flatpak'] = True
-
-        return metadata
-
-
 WORKSPACE_SOURCE_KEY = 'source_info'
 
 
@@ -157,7 +98,12 @@ class FlatpakCreateDockerfilePlugin(PreBuildPlugin):
             raise RuntimeError(
                 "resolve_module_compose must be run before flatpak_create_dockerfile")
 
-        return FlatpakSourceInfo(flatpak_yaml, compose_info)
+        module_spec = split_module_spec(compose_info.source_spec)
+
+        return FlatpakSourceInfo(flatpak_yaml,
+                                 compose_info.modules,
+                                 compose_info.base_module,
+                                 module_spec.profile)
 
     def run(self):
         """
@@ -168,38 +114,18 @@ class FlatpakCreateDockerfilePlugin(PreBuildPlugin):
 
         set_flatpak_source_info(self.workflow, source)
 
-        module_info = source.compose.base_module
-
-        # For a runtime, certain information is duplicated between the container.yaml
-        # and the modulemd, check that it matches
-        if source.runtime:
-            flatpak_yaml = source.flatpak_yaml
-            flatpak_xmd = module_info.mmd.props.xmd['flatpak']
-
-            def check(condition, what):
-                if not condition:
-                    raise RuntimeError(
-                        "Mismatch for {} betweeen module xmd and container.yaml".format(what))
+        builder = FlatpakBuilder(source, None, None)
 
-            check(flatpak_yaml['branch'] == flatpak_xmd['branch'], "'branch'")
-            check(source.profile in flatpak_xmd['runtimes'], 'profile name')
-
-            profile_xmd = flatpak_xmd['runtimes'][source.profile]
-
-            check(flatpak_yaml['id'] == profile_xmd['id'], "'id'")
-            check(flatpak_yaml.get('runtime', None) ==
-                  profile_xmd.get('runtime', None), "'runtime'")
-            check(flatpak_yaml.get('sdk', None) == profile_xmd.get('sdk', None), "'sdk'")
+        builder.precheck()
 
         # Create the dockerfile
 
+        module_info = source.base_module
+
         # We need to enable all the modules other than the platform pseudo-module
-        modules_str = ' '.join(sorted(m.mmd.props.name + ':' + m.mmd.props.stream
-                                      for m in source.compose.modules.values()
-                                      if m.mmd.props.name != 'platform'))
+        modules_str = ' '.join(builder.get_enable_modules())
 
-        install_packages = module_info.mmd.peek_profiles()[source.profile].props.rpms.get()
-        install_packages_str = ' '.join(install_packages)
+        install_packages_str = ' '.join(builder.get_install_packages())
 
         df_path = os.path.join(self.workflow.builder.df_dir, DOCKERFILE_FILENAME)
         with open(df_path, 'w') as fp:
@@ -213,54 +139,16 @@ class FlatpakCreateDockerfilePlugin(PreBuildPlugin):
 
         self.workflow.builder.set_df_path(df_path)
 
-        # For a runtime, we want to make sure that the set of RPMs that is installed
-        # into the filesystem is *exactly* the set that is listed in the runtime
-        # profile. Requiring the full listed set of RPMs to be listed makes it
-        # easier to catch unintentional changes in the package list that might break
-        # applications depending on the runtime. It also simplifies the checking we
-        # do for application flatpaks, since we can simply look at the runtime
-        # modulemd to find out what packages are present in the runtime.
-        #
-        # For an application, we want to make sure that each RPM that is installed
-        # into the filesystem is *either* an RPM that is part of the 'runtime'
-        # profile of the base runtime, or from a module that was built with
-        # flatpak-rpm-macros in the install root and, thus, prefix=/app.
-        #
-        # We achieve this by restricting the set of available packages in the dnf
-        # configuration to just the ones that we want.
-        #
-        # The advantage of doing this upfront, rather than just checking after the
-        # fact is that this makes sure that when a application is being installed,
-        # we don't get a different package to satisfy a dependency than the one
-        # in the runtime - e.g. aajohan-comfortaa-fonts to satisfy font(:lang=en)
-        # because it's alphabetically first.
-
-        if not source.runtime:
-            runtime_module = source.runtime_module
-            runtime_profile = runtime_module.mmd.peek_profiles()['runtime']
-            available_packages = sorted(runtime_profile.props.rpms.get())
-
-            for m in source.app_modules:
-                # Strip off the '.rpm' suffix from the filename to get something
-                # that DNF can parse.
-                available_packages.extend(x[:-4] for x in m.rpms)
-        else:
-            base_module = source.compose.base_module
-            runtime_profile = base_module.mmd.peek_profiles()['runtime']
-            available_packages = sorted(runtime_profile.props.rpms.get())
-
+        includepkgs = builder.get_includepkgs()
         includepkgs_path = os.path.join(self.workflow.builder.df_dir, 'atomic-reactor-includepkgs')
         with open(includepkgs_path, 'w') as f:
-            f.write('includepkgs = ' + ','.join(available_packages) + '\n')
+            f.write('includepkgs = ' + ','.join(includepkgs) + '\n')
 
         # Create the cleanup script
 
         cleanupscript = os.path.join(self.workflow.builder.df_dir, "cleanup.sh")
         with open(cleanupscript, 'w') as f:
-            cleanup_commands = source.flatpak_yaml.get('cleanup-commands')
-            if cleanup_commands is not None:
-                f.write(cleanup_commands.rstrip())
-                f.write("\n")
+            f.write(builder.get_cleanup_script())
         os.chmod(cleanupscript, 0o0755)
 
         # Add a yum-repository pointing to the compose
@@ -270,9 +158,11 @@ class FlatpakCreateDockerfilePlugin(PreBuildPlugin):
             stream=module_info.stream,
             version=module_info.version)
 
+        compose_info = get_compose_info(self.workflow)
+
         repo = {
             'name': repo_name,
-            'baseurl': source.compose.repo_url,
+            'baseurl': compose_info.repo_url,
             'enabled': 1,
             'gpgcheck': 0,
         }
@@ -280,4 +170,4 @@ class FlatpakCreateDockerfilePlugin(PreBuildPlugin):
         path = os.path.join(YUM_REPOS_DIR, repo_name + '.repo')
         self.workflow.files[path] = render_yum_repo(repo, escape_dollars=False)
 
-        override_build_kwarg(self.workflow, 'module_compose_id', source.compose.compose_id)
+        override_build_kwarg(self.workflow, 'module_compose_id', compose_info.compose_id)
diff --git a/atomic_reactor/plugins/pre_resolve_module_compose.py b/atomic_reactor/plugins/pre_resolve_module_compose.py
index 5ce6e29..df5f040 100644
--- a/atomic_reactor/plugins/pre_resolve_module_compose.py
+++ b/atomic_reactor/plugins/pre_resolve_module_compose.py
@@ -23,6 +23,9 @@ Example configuration:
 }
 """
 
+
+from flatpak_module_tools.flatpak_builder import ModuleInfo
+
 import gi
 try:
     gi.require_version('Modulemd', '1.0')
@@ -37,15 +40,6 @@ from atomic_reactor.plugins.pre_reactor_config import (get_pdc_session, get_odcs
                                                        get_pdc, get_odcs)
 
 
-class ModuleInfo(object):
-    def __init__(self, name, stream, version, mmd, rpms):
-        self.name = name
-        self.stream = stream
-        self.version = version
-        self.mmd = mmd
-        self.rpms = rpms
-
-
 class ComposeInfo(object):
     def __init__(self, source_spec, compose_id, base_module, modules, repo_url):
         self.source_spec = source_spec
diff --git a/atomic_reactor/plugins/prepub_flatpak_create_oci.py b/atomic_reactor/plugins/prepub_flatpak_create_oci.py
index b86e792..d1b6463 100644
--- a/atomic_reactor/plugins/prepub_flatpak_create_oci.py
+++ b/atomic_reactor/plugins/prepub_flatpak_create_oci.py
@@ -10,14 +10,7 @@ pre_flatpak_create_dockerfile, extracts the tree at /var/tmp/flatpak-build
 and turns it into a Flatpak application or runtime.
 """
 
-import os
-from six.moves import configparser
-import re
-import shlex
-import shutil
-import subprocess
-import tarfile
-from textwrap import dedent
+from flatpak_module_tools.flatpak_builder import FlatpakBuilder
 
 from atomic_reactor.constants import IMAGE_TYPE_OCI, IMAGE_TYPE_OCI_TAR
 from atomic_reactor.plugin import PrePublishPlugin
@@ -26,99 +19,6 @@ from atomic_reactor.rpm_util import parse_rpm_output
 from atomic_reactor.util import get_exported_image_metadata
 
 
-# Returns flatpak's name for the current arch
-def get_arch():
-    return subprocess.check_output(['flatpak', '--default-arch'],
-                                   universal_newlines=True).strip()
-
-
-# flatpak build-init requires the sdk and runtime to be installed on the
-# build system (so that subsequent build steps can execute things with
-# the SDK). While it isn't impossible to download the runtime image and
-# install the flatpak, that would be a lot of unnecessary complexity
-# since our build step is just unpacking the filesystem we've already
-# created. This is a stub implementation of 'flatpak build-init' that
-# doesn't check for the SDK or use it to set up the build filesystem.
-def build_init(directory, appname, sdk, runtime, runtime_branch, tags=[]):
-    if not os.path.isdir(directory):
-        os.mkdir(directory)
-    with open(os.path.join(directory, "metadata"), "w") as f:
-        f.write(dedent("""\
-                       [Application]
-                       name={appname}
-                       runtime={runtime}/{arch}/{runtime_branch}
-                       sdk={sdk}/{arch}/{runtime_branch}
-                       """.format(appname=appname,
-                                  sdk=sdk,
-                                  runtime=runtime,
-                                  runtime_branch=runtime_branch,
-                                  arch=get_arch())))
-        if tags:
-            f.write("tags=" + ";".join(tags) + "\n")
-    os.mkdir(os.path.join(directory, "files"))
-
-
-# add_app_prefix('org.gimp', 'gimp, 'gimp.desktop') => org.gimp.desktop
-# add_app_prefix('org.gnome', 'eog, 'eog.desktop') => org.gnome.eog.desktop
-def add_app_prefix(app_id, root, full):
-    prefix = app_id
-    if prefix.endswith('.' + root):
-        prefix = prefix[:-(1 + len(root))]
-    return prefix + '.' + full
-
-
-def find_desktop_files(builddir):
-    desktopdir = os.path.join(builddir, 'files/share/applications')
-    for (dirpath, dirnames, filenames) in os.walk(desktopdir):
-        for filename in filenames:
-            if filename.endswith('.desktop'):
-                yield os.path.join(dirpath, filename)
-
-
-def find_icons(builddir, name):
-    icondir = os.path.join(builddir, 'files/share/icons/hicolor')
-    for (dirpath, dirnames, filenames) in os.walk(icondir):
-        for filename in filenames:
-            if filename.startswith(name + '.'):
-                yield os.path.join(dirpath, filename)
-
-
-def update_desktop_files(app_id, builddir):
-    for full_path in find_desktop_files(builddir):
-        cp = configparser.RawConfigParser()
-        cp.read([full_path])
-        try:
-            icon = cp.get('Desktop Entry', 'Icon')
-        except configparser.NoOptionError:
-            icon = None
-
-        # Does it have an icon?
-        if icon and not icon.startswith(app_id):
-            found_icon = False
-
-            # Rename any matching icons
-            for icon_file in find_icons(builddir, icon):
-                shutil.copy(icon_file,
-                            os.path.join(os.path.dirname(icon_file),
-                                         add_app_prefix(app_id, icon, os.path.basename(icon_file))))
-                found_icon = True
-
-            # If we renamed the icon, change the desktop file
-            if found_icon:
-                subprocess.check_call(['desktop-file-edit',
-                                       '--set-icon',
-                                       add_app_prefix(app_id, icon, icon), full_path])
-
-        # Is the desktop file not prefixed with the app id, then prefix it
-        basename = os.path.basename(full_path)
-        if not basename.startswith(app_id):
-            shutil.move(full_path,
-                        os.path.join(os.path.dirname(full_path),
-                                     add_app_prefix(app_id,
-                                                    basename[:-len('.desktop')],
-                                                    basename)))
-
-
 # This converts the generator provided by the export() operation to a file-like
 # object with a read that we can pass to tarfile.
 class StreamAdapter(object):
@@ -165,133 +65,11 @@ class FlatpakCreateOciPlugin(PrePublishPlugin):
         """
         super(FlatpakCreateOciPlugin, self).__init__(tasker, workflow)
 
-    # Compiles a list of path mapping rules to a simple function that matches
-    # against a list of fixed patterns, see below for rule syntax
-    def _compile_target_rules(rules):
-        ROOT = "var/tmp/flatpak-build"
-
-        patterns = []
-        for source, target in rules:
-            source = re.sub("^ROOT", ROOT, source)
-            if source.endswith("/"):
-                patterns.append((re.compile(source + "(.*)"), target, False))
-                patterns.append((source[:-1], target, True))
-            else:
-                patterns.append((source, target, True))
-
-        def get_target_func(self, path):
-            for source, target, is_exact_match in patterns:
-                if is_exact_match:
-                    if source == path:
-                        return target
-                else:
-                    m = source.match(path)
-                    if m:
-                        return os.path.join(target, m.group(1))
-
-            return None
-
-        return get_target_func
-
-    # Rules for mapping paths within the exported filesystem image to their
-    # location in the final flatpak filesystem
-    #
-    # ROOT = /var/tmp/flatpak-build
-    # No trailing slash - map a directory itself exactly
-    # trailing slash - map a directory and everything inside of it
-
-    _get_target_path_runtime = _compile_target_rules([
-        # We need to make sure that 'files' is created before 'files/etc',
-        # which wouldn't happen if just relied on ROOT/usr/ => files.
-        # Instead map ROOT => files and omit ROOT/usr
-        ("ROOT", "files"),
-        ("ROOT/usr", None),
-
-        # We map ROOT/usr => files and ROOT/etc => files/etc. This creates
-        # A conflict between ROOT/usr/etc and /ROOT/etc. Just assume there
-        # is nothing useful in /ROOT/usr/etc.
-        ("ROOT/usr/etc/", None),
-
-        ("ROOT/usr/", "files"),
-        ("ROOT/etc/", "files/etc")
-    ])
-
-    _get_target_path_app = _compile_target_rules([
-        ("ROOT/app/", "files")
-    ])
-
-    def _get_target_path(self, export_path):
-        if self.source.runtime:
-            return self._get_target_path_runtime(export_path)
-        else:
-            return self._get_target_path_app(export_path)
-
     def _export_container(self, container_id):
-        outfile = os.path.join(self.workflow.source.workdir, 'filesystem.tar.gz')
-        manifestfile = os.path.join(self.workflow.source.workdir, 'flatpak-build.rpm_qf')
-
         export_generator = self.tasker.d.export(container_id)
         export_stream = StreamAdapter(export_generator)
 
-        out_fileobj = open(outfile, "wb")
-        compress_process = subprocess.Popen(['gzip', '-c'],
-                                            stdin=subprocess.PIPE,
-                                            stdout=out_fileobj)
-        in_tf = tarfile.open(fileobj=export_stream, mode='r|')
-        out_tf = tarfile.open(fileobj=compress_process.stdin, mode='w|')
-
-        for member in in_tf:
-            if member.name == 'var/tmp/flatpak-build.rpm_qf':
-                reader = in_tf.extractfile(member)
-                with open(manifestfile, 'wb') as out:
-                    out.write(reader.read())
-                reader.close()
-            target_name = self._get_target_path(member.name)
-            if target_name is None:
-                continue
-
-            # Match the ownership/permissions changes done by 'flatpak build-export'.
-            # See commit_filter() in:
-            #   https://github.com/flatpak/flatpak/blob/master/app/flatpak-builtins-build-export.c
-            #
-            # We'll run build-export anyways in the app case, but in the runtime case we skip
-            # flatpak build-export and use ostree directly.
-            member.uid = 0
-            member.gid = 0
-            member.uname = "root"
-            member.gname = "root"
-
-            if member.isdir():
-                member.mode = 0o0755
-            elif member.mode & 0o0100:
-                member.mode = 0o0755
-            else:
-                member.mode = 0o0644
-
-            member.name = target_name
-            if member.islnk():
-                # Hard links have full paths within the archive (no leading /)
-                link_target = self._get_target_path(member.linkname)
-                if link_target is None:
-                    self.log.debug("Skipping %s, hard link to %s", target_name, link_target)
-                    continue
-                member.linkname = link_target
-                out_tf.addfile(member)
-            elif member.issym():
-                # Symlinks have the literal link target, which will be
-                # relative to the chroot and doesn't need rewriting
-                out_tf.addfile(member)
-            else:
-                f = in_tf.extractfile(member)
-                out_tf.addfile(member, fileobj=f)
-
-        in_tf.close()
-        out_tf.close()
-        export_stream.close()
-        compress_process.stdin.close()
-        if compress_process.wait() != 0:
-            raise RuntimeError("gzip failed")
-        out_fileobj.close()
+        outfile, manifestfile = self.builder._export_from_stream(export_stream)
 
         return outfile, manifestfile
 
@@ -310,148 +88,23 @@ class FlatpakCreateOciPlugin(PrePublishPlugin):
             self.log.info("Cleaning up docker container")
             self.tasker.d.remove_container(container_id)
 
-    def _get_components(self, manifest):
-        with open(manifest, 'r') as f:
-            lines = f.readlines()
-
-        return parse_rpm_output(lines)
-
-    def _filter_app_manifest(self, components):
-        runtime_rpms = self.source.runtime_module.mmd.peek_profiles()['runtime'].props.rpms
-
-        return [c for c in components if not runtime_rpms.contains(c['name'])]
-
-    def _create_runtime_oci(self, tarred_filesystem, outfile):
-        info = self.source.flatpak_yaml
-
-        builddir = os.path.join(self.workflow.source.workdir, "build")
-        os.mkdir(builddir)
-
-        repo = os.path.join(self.workflow.source.workdir, "repo")
-        subprocess.check_call(['ostree', 'init', '--mode=archive-z2', '--repo', repo])
-
-        id_ = info['id']
-        runtime_id = info.get('runtime', id_)
-        sdk_id = info.get('sdk', id_)
-        branch = info['branch']
-
-        args = {
-            'id': id_,
-            'runtime_id': runtime_id,
-            'sdk_id': sdk_id,
-            'arch': get_arch(),
-            'branch': branch
-        }
-
-        METADATA_TEMPLATE = dedent("""\
-            [Runtime]
-            name={id}
-            runtime={runtime_id}/{arch}/{branch}
-            sdk={sdk_id}/{arch}/{branch}
-
-            [Environment]
-            LD_LIBRARY_PATH=/app/lib64:/app/lib
-            GI_TYPELIB_PATH=/app/lib64/girepository-1.0
-            """)
-
-        with open(os.path.join(builddir, 'metadata'), 'w') as f:
-            f.write(METADATA_TEMPLATE.format(**args))
-
-        runtime_ref = 'runtime/{id}/{arch}/{branch}'.format(**args)
-
-        subprocess.check_call(['ostree', 'commit',
-                               '--repo', repo, '--owner-uid=0',
-                               '--owner-gid=0', '--no-xattrs',
-                               '--branch', runtime_ref,
-                               '-s', 'build of ' + runtime_ref,
-                               '--tree=tar=' + tarred_filesystem,
-                               '--tree=dir=' + builddir])
-        subprocess.check_call(['ostree', 'summary', '-u', '--repo', repo])
-
-        subprocess.check_call(['flatpak', 'build-bundle', repo,
-                               '--oci', '--runtime',
-                               outfile, id_, branch])
-
-        return runtime_ref
-
-    def _find_runtime_info(self):
-        runtime_module = self.source.runtime_module
-
-        flatpak_xmd = runtime_module.mmd.props.xmd['flatpak']
-        runtime_id = flatpak_xmd['runtimes']['runtime']['id']
-        sdk_id = flatpak_xmd['runtimes']['runtime'].get('sdk', runtime_id)
-        runtime_version = flatpak_xmd['branch']
-
-        return runtime_id, sdk_id, runtime_version
-
-    def _create_app_oci(self, tarred_filesystem, outfile):
-        info = self.source.flatpak_yaml
-        app_id = info['id']
-        app_branch = info.get('branch', 'master')
-
-        builddir = os.path.join(self.workflow.source.workdir, "build")
-        os.mkdir(builddir)
-
-        repo = os.path.join(self.workflow.source.workdir, "repo")
-
-        runtime_id, sdk_id, runtime_version = self._find_runtime_info()
-
-        # See comment for build_init() for why we can't use 'flatpak build-init'
-        # subprocess.check_call(['flatpak', 'build-init',
-        #                        builddir, app_id, runtime_id, runtime_id, runtime_version])
-        build_init(builddir, app_id, sdk_id, runtime_id, runtime_version, tags=info.get('tags', []))
-
-        # with gzip'ed tarball, tar is several seconds faster than tarfile.extractall
-        subprocess.check_call(['tar', 'xCfz', builddir, tarred_filesystem])
-
-        update_desktop_files(app_id, builddir)
-
-        finish_args = []
-        if 'finish-args' in info:
-            # shlex.split(None) reads from standard input, so avoid that
-            finish_args = shlex.split(info['finish-args'] or '')
-        if 'command' in info:
-            finish_args = ['--command', info['command']] + finish_args
-
-        subprocess.check_call(['flatpak', 'build-finish'] + finish_args + [builddir])
-        subprocess.check_call(['flatpak', 'build-export', repo, builddir, app_branch])
-
-        subprocess.check_call(['flatpak', 'build-bundle', repo, '--oci',
-                               outfile, app_id, app_branch])
-
-        app_ref = 'app/{app_id}/{arch}/{branch}'.format(app_id=app_id,
-                                                        arch=get_arch(),
-                                                        branch=app_branch)
-
-        return app_ref
-
     def run(self):
         self.source = get_flatpak_source_info(self.workflow)
         if self.source is None:
             raise RuntimeError("flatpak_create_dockerfile must be run before flatpak_create_oci")
 
+        self.builder = FlatpakBuilder(self.source, self.workflow.source.workdir,
+                                      'var/tmp/flatpak-build',
+                                      parse_manifest=parse_rpm_output)
+
         tarred_filesystem, manifest = self._export_filesystem()
         self.log.info('filesystem tarfile written to %s', tarred_filesystem)
         self.log.info('manifest written to %s', manifest)
 
-        all_components = self._get_components(manifest)
-        if self.source.runtime:
-            image_components = all_components
-        else:
-            image_components = self._filter_app_manifest(all_components)
-
-        self.log.info("Components:\n%s",
-                      "\n".join("        {name}-{epoch}:{version}-{release}.{arch}.rpm"
-                                .format(**c) for c in image_components))
-
+        image_components = self.builder.get_components(manifest)
         self.workflow.image_components = image_components
 
-        outfile = os.path.join(self.workflow.source.workdir, 'flatpak-oci-image')
-
-        if self.source.runtime:
-            ref_name = self._create_runtime_oci(tarred_filesystem, outfile)
-        else:
-            ref_name = self._create_app_oci(tarred_filesystem, outfile)
+        ref_name, outfile, tarred_outfile = self.builder.build_container(tarred_filesystem)
 
         metadata = get_exported_image_metadata(outfile, IMAGE_TYPE_OCI)
         metadata['ref_name'] = ref_name
@@ -459,11 +112,6 @@ class FlatpakCreateOciPlugin(PrePublishPlugin):
 
         self.log.info('OCI image is available as %s', outfile)
 
-        tarred_outfile = outfile + '.tar'
-        with tarfile.TarFile(tarred_outfile, "w") as tf:
-            for f in os.listdir(outfile):
-                tf.add(os.path.join(outfile, f), f)
-
         metadata = get_exported_image_metadata(tarred_outfile, IMAGE_TYPE_OCI_TAR)
         metadata['ref_name'] = ref_name
         self.workflow.exported_image_sequence.append(metadata)
diff --git a/images/dockerhost-builder/Dockerfile b/images/dockerhost-builder/Dockerfile
index 11368b9..7e5be3e 100644
--- a/images/dockerhost-builder/Dockerfile
+++ b/images/dockerhost-builder/Dockerfile
@@ -1,5 +1,5 @@
 FROM fedora:latest
-RUN dnf -y install docker git python-docker-py python-setuptools desktop-file-utils e2fsprogs flatpak koji libmodulemd ostree python2-gobject-base python-backports-lzma osbs gssproxy && dnf clean all
+RUN dnf -y install docker git python-docker-py python-setuptools desktop-file-utils e2fsprogs flatpak koji libmodulemd ostree python2-gobject-base python2-flatpak-module-tools python-backports-lzma osbs gssproxy && dnf clean all
 ADD ./atomic-reactor.tar.gz /tmp/
 RUN cd /tmp/atomic-reactor-*/ && python setup.py install
 CMD ["atomic-reactor", "--verbose", "inside-build"]
diff --git a/images/privileged-builder/Dockerfile b/images/privileged-builder/Dockerfile
index a6fa7fd..75653bf 100644
--- a/images/privileged-builder/Dockerfile
+++ b/images/privileged-builder/Dockerfile
@@ -1,5 +1,5 @@
 FROM fedora:latest
-RUN dnf -y install docker git python-docker-py python-setuptools desktop-file-utils e2fsprogs flatpak koji libmodulemd ostree python2-gobject-base python-backports-lzma osbs gssproxy && dnf clean all
+RUN dnf -y install docker git python-docker-py python-setuptools desktop-file-utils e2fsprogs flatpak koji libmodulemd ostree python2-gobject-base python2-flatpak-module-tools python-backports-lzma osbs gssproxy && dnf clean all
 ADD ./atomic-reactor.tar.gz /tmp/
 RUN cd /tmp/atomic-reactor-*/ && python setup.py install
 ADD ./docker.sh /tmp/docker.sh
diff --git a/requirements-flatpak.txt b/requirements-flatpak.txt
index a751b49..acb27ca 100644
--- a/requirements-flatpak.txt
+++ b/requirements-flatpak.txt
@@ -1 +1,2 @@
+flatpak-module-tools
 pdc-client
diff --git a/tests/flatpak.py b/tests/flatpak.py
index 2ac42e9..c412284 100644
--- a/tests/flatpak.py
+++ b/tests/flatpak.py
@@ -1,11 +1,12 @@
 import yaml
 
+from atomic_reactor.util import split_module_spec
+
 try:
-    from atomic_reactor.plugins.pre_resolve_module_compose import (ModuleInfo,
-                                                                   ComposeInfo,
+    from atomic_reactor.plugins.pre_resolve_module_compose import (ComposeInfo,
                                                                    set_compose_info)
-    from atomic_reactor.plugins.pre_flatpak_create_dockerfile import (FlatpakSourceInfo,
-                                                                      set_flatpak_source_info)
+    from atomic_reactor.plugins.pre_flatpak_create_dockerfile import set_flatpak_source_info
+    from flatpak_module_tools.flatpak_builder import FlatpakSourceInfo, ModuleInfo
     from gi.repository import Modulemd
     MODULEMD_AVAILABLE = True
 except ImportError:
@@ -123,6 +124,9 @@ flatpak:
     # Test overriding the automatic "first executable in /usr/bin'
     command: eog2
     tags: ["Viewer"]
+    copy-icon: true
+    rename-desktop-file: eog.desktop
+    rename-icon: eog
     finish-args: >
 """ + "".join("        {}\n".format(a) for a in FLATPAK_APP_FINISH_ARGS)
 
@@ -342,7 +346,10 @@ def setup_flatpak_source_info(workflow, config=APP_CONFIG):
 
     flatpak_yaml = yaml.safe_load(config['container_yaml'])['flatpak']
 
-    source = FlatpakSourceInfo(flatpak_yaml, compose)
+    module_spec = split_module_spec(compose.source_spec)
+
+    source = FlatpakSourceInfo(flatpak_yaml, compose.modules, compose.base_module,
+                               module_spec.profile)
     set_flatpak_source_info(workflow, source)
 
     return source
-- 
2.14.3