Source code for dockeroo.docker.gentoo_build


# -*- coding: utf-8 -*-
#
# Copyright (c) 2016, Giacomo Cariello. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


import re
import shutil
import tempfile

from shellescape import quote

from dockeroo import BaseGroupRecipe
from dockeroo.docker import Archive, BaseDockerSubRecipe
from dockeroo.utils import merge, string_as_bool


class DockerGentooBuildSubRecipe(BaseDockerSubRecipe): # pylint: disable=too-many-instance-attributes

    def initialize(self):
        super(DockerGentooBuildSubRecipe, self).initialize()

        base_name = re.sub(r'\W+', '_', self.name)

        self.archives = []
        for url, prefix, md5sum in \
            [merge([None, None, None], x.split())[:3]
             for x in [f for f in
                       [x.strip() for x in \
                            self.options.get(
                                'archives', self.options.get('archive', '')).splitlines()]
                       if f]]:
            if prefix == '/':
                prefix = None
            self.archives.append(
                Archive(url=url, prefix=prefix, md5sum=md5sum))

        self.accept_keywords = [f for f in [x.strip() for x in \
            self.options.get('accept-keywords', '').splitlines()] if f]
        self.build_dependencies = [f for f in [x.strip() for x in \
            self.options.get('build-dependencies', '').splitlines()] if f]
        self.build_command = self.options.get('build-command', "/bin/freeze")
        self.build_container = "{}_build".format(base_name)
        self.build_layout = self.options.get('build-layout', None)
        self.build_image = self.options.get('build-image', None)
        self.build_env = dict([y for y in [x.strip().split(
            '=') for x in self.options.get('build-env', '').splitlines()] if y[0]])
        self.build_volumes_from = self.options.get('build-volumes-from', None)
        self.build_script_user = self.options.get('build-script-user', None)
        self.build_script_shell = self.options.get(
            'build-script-shell', self.shell)
        self.build_script = "#!{}\n{}".format(
            self.build_script_shell,
            '\n'.join([f for f in [x.strip() for x in
                                   self.options.get('build-script').replace('$$', '$').splitlines()]
                       if f])) if self.options.get('build-script', None) is not None else None

        self.assemble_container = "{}_assemble".format(base_name)
        self.copy = [merge([None, None], y.split()[:2]) for y in
                     [f for f in [x.strip() for x in self.options.get('copy', '').splitlines()]
                      if f]]
        self.base_image = self.options.get('base-image', None)
        self.keep = string_as_bool(self.options.get('keep', False))
        self.layout = self.options.get('layout', None)
        self.layout_uid = self.options.get('layout-uid', 0)
        self.layout_gid = self.options.get('layout-gid', 0)
        self.packages = [f for f in
                         [x.strip() for x in self.options.get('packages', '').splitlines()]
                         if f]
        self.platform = self.options.get('platform', self.engine.platform)
        self.arch = self.options.get('arch', self.platform)
        self.processor = self.options.get('processor', self.platform)
        self.variant = self.options.get('variant', 'dockeroo')
        self.abi = self.options.get('abi', 'gnu')

        self.assemble_script_user = self.options.get('assemble-script-user', None)
        self.assemble_script_shell = self.options.get('assemble-script-shell', self.shell)
        self.assemble_script = "#!{}\n{}".format(
            self.assemble_script_shell,
            '\n'.join([_f for _f in \
                [x.strip() for x in \
                 self.options.get('assemble-script').replace('$$', '$').splitlines()]
                       if _f])) \
            if self.options.get('assemble-script', None) is not None else None
        self.tty = string_as_bool(self.options.get('tty', False))
        self.masks = [_f for _f in [x.strip() for x in
                                    self.options.get('mask', '').splitlines()]
                      if _f]
        self.unmasks = [_f for _f in [x.strip() for x in
                                      self.options.get('unmask', '').splitlines()]
                        if _f]
        self.uses = [_f for _f in [x.strip() for x in
                                   self.options.get('use', '').splitlines()]
                     if _f]

        self.command = self.options.get('command', "/bin/freeze")
        self.user = self.options.get('user', None)
        self.labels = dict([y for y in [x.strip().split('=')
                                        for x in self.options.get('labels', '').splitlines()]
                            if y[0]])
        self.expose = [_f for _f in [x.strip() for x in self.options.get('expose', '').splitlines()]
                       if _f]
        self.volumes = [y for y in [x.strip().split(
            ':', 1) for x in self.options.get('volumes', '').splitlines()] if y[0]]
        self.volumes_from = self.options.get('volumes-from', None)

    def add_package_modifier(self, name, modifiers):
        for modifier in modifiers:
            self.engine.run_cmd(
                self.build_container,
                "chroot-{arch}-docker -c \"echo {modifier} >>/etc/portage/package.{name}\"".format(
                    arch=self.arch, modifier=quote(modifier), name=name))

    def create_base_image(self, name):
        if self.archives:
            for archive in self.archives:
                archive.download(self.recipe.buildout)
            self.engine.import_archives(name, *self.archives)
        else:
            root = tempfile.mkdtemp()
            self.engine.import_path(root, name)
            shutil.rmtree(root)
        return name

    def install(self):
        if self.base_image:
            base_image = self.base_image
        else:
            base_image = self.create_base_image(self.name)
        self.engine.remove_container(self.assemble_container)
        self.engine.create_container(self.assemble_container, base_image, command="/bin/freeze",
                                     privileged=True, tty=self.tty, volumes_from=self.volumes_from)
        self.engine.install_freeze(self.assemble_container)
        self.engine.start_container(self.assemble_container)

        if self.build_image:
            self.engine.remove_container(self.build_container)
            self.engine.create_container(self.build_container, self.build_image,
                                         command=self.build_command,
                                         privileged=True, tty=self.tty,
                                         volumes_from=self.build_volumes_from)
            self.engine.start_container(self.build_container)
            if self.platform != self.engine.platform:
                self.engine.config_binfmt(self.build_container, self.platform)
            if self.build_layout:
                self.engine.load_layout(self.build_container, self.build_layout)
            self.add_package_modifier('accept_keywords', self.accept_keywords)
            self.add_package_modifier('mask', self.masks)
            self.add_package_modifier('unmask', self.unmasks)
            self.add_package_modifier('use', self.uses)
            self.engine.run_cmd(
                self.build_container,
                "chroot-{arch}-docker -c \"eclean packages && emaint binhost --fix\""
                .format(arch=self.arch))
            self.engine.run_cmd(
                self.build_container,
                "env {env} chroot-{arch}-docker -c \"emerge -kb --binpkg-respect-use=y {packages}\""
                .format(arch=self.arch, packages=' '.join(self.build_dependencies + self.packages),
                        env=' '.join(['='.join(x) for x in self.build_env.items()])))
            package_atoms = ["={}".format(
                self.engine.run_cmd(
                    self.build_container,
                    "chroot-{arch}-docker -c \"equery list --format=\"\\$cpv\" {package}\" | "
                    "head -1"
                    .format(arch=self.arch, package=package),
                    quiet=True, return_output=True)) for package in self.packages]
            self.engine.run_cmd(
                self.build_container,
                "chroot-{arch}-docker -c \"ROOT=/dockeroo-root emerge -OK {packages}\"".format(
                    arch=self.arch, packages=' '.join(package_atoms)))
            if self.build_script:
                self.engine.run_script(self.build_container, self.build_script,
                                       shell=self.build_script_shell, user=self.build_script_user)
            self.engine.copy_path(self.build_container, self.assemble_container,
                                  "/usr/{processor}-{variant}-linux-{abi}/dockeroo-root/".format(
                                      processor=self.processor, variant=self.variant, abi=self.abi),
                                  dst="/")
            for src, dst in self.copy:
                self.engine.copy_path(self.build_container,
                                      self.assemble_container, src, dst=dst)
            self.engine.remove_container(self.build_container)
        if self.layout:
            self.engine.load_layout(self.assemble_container, self.layout,
                                    uid=self.layout_uid, gid=self.layout_gid)
        if self.assemble_script:
            if self.platform != self.engine.platform:
                self.engine.config_binfmt(self.assemble_container, self.platform)
            self.engine.run_script(self.assemble_container, self.assemble_script,
                                   shell=self.assemble_script_shell, user=self.assemble_script_user)
        self.engine.commit_container(self.assemble_container, self.name,
                                     command=self.command, user=self.user, labels=self.labels,
                                     expose=self.expose, volumes=self.volumes)
        self.engine.remove_container(self.assemble_container)
        self.engine.clean_stale_images()
        return self.mark_completed()

    def update(self):
        # pylint: disable=too-many-boolean-expressions
        if (self.layout and self.is_layout_updated(self.layout)) or \
            (self.build_layout and self.is_layout_updated(self.build_layout)) or \
            (self.build_image and self.is_image_updated(self.build_image)) or \
            (self.base_image and self.is_image_updated(self.base_image)) or \
                not next(self.engine.images(name=self.name), None):
            return self.install()
        return self.mark_completed()

    def uninstall(self):
        self.engine.remove_container(self.build_container)
        self.engine.remove_container(self.assemble_container)
        if not self.keep:
            self.engine.remove_image(self.name)


[docs]class DockerGentooBuildRecipe(BaseGroupRecipe): """ This recipe builds a docker image by assembling an optional base image, a layout and a list of Gentoo binary packages. .. describe:: Usage The following example buildout part shows how to build a base image using a **builder** image produced with :py:class:`dockeroo.docker.gentoo_bootstrap.DockerGentooBootstrapRecipe`. .. code-block:: ini recipe = dockeroo:docker.gentoo-build layout = ${buildout:directory}/base use = sys-apps/busybox static accept-keywords = app-admin/monit ** sys-apps/s6 ** sys-apps/s6-rc ** dev-lang/execline ** dev-libs/skalibs ** packages = sys-libs/ncurses:0/5 sys-libs/ncurses:5/5 sys-libs/readline sys-apps/busybox app-shells/bash sys-libs/glibc sys-apps/gentoo-functions dev-lang/execline dev-libs/skalibs sys-apps/s6 sys-apps/s6-rc app-admin/monit shell = /bin/bash assemble-script = /bin/busybox --help | \\ /bin/busybox sed -e '1,/^Currently defined functions:/d' \\ -e 's/[ \\t]//g' -e 's/,$$//' -e 's/,/\\n/g' | \\ while read a ; do if [ "$$a" != "" ]; then /bin/busybox ln -sf "busybox" "/bin/$$a" fi done /sbin/ldconfig -v /usr/sbin/locale-gen /bin/s6-rc-compile /etc/s6-rc/compiled /etc/s6-rc/services chown 65534:65534 /var/log/s6-svscan rm -rf /usr/include /usr/share/doc /usr/share/info /usr/share/man tty = true .. describe:: Configuration options abi Target Application Binary Interface. Defaults to "gnu". accept-keywords Sets /etc/portage/package.accept-keywords on builder container's chrooted environment, one per line. arch Target architecture. Defaults to machine architecture. archives List of URLs of operating system initial filesystem contents for **assemble-image**. assemble-container Name of assemble container. Defaults to <partname>_assemble. base-image Name of image to use for instantiation of **assemble-container**. If unset, **archives** will be used to populate if available, otherwise an empty image will be created. build-command Command to launch on builder container upon creation. Defaults to "/bin/freeze". build-container Name of build container. Defaults to <partname>_build. build-dependencies List of packages to be installed in builder container's chrooted environment, but not installed on **assemble-container**. build-env List of environment variables to be set for packages building. build-image Name of build image. If unset, no building will be performed. build-layout Copies a local folder to **build-container**'s root with **docker cp**. build-script This shell script is executed after building Gentoo packages. build-script-shell Shell to use for script execution. Defaults to "/bin/sh". build-script-user User which executes the **build-script**. If unset, docker default is applied. build-volumes-from Volumes to be mounted on build container upon creation. command Sets **COMMAND** parameter on target image. copy List of extra paths to copy from builder container to assemble container, separated by newline. To copy directories, end pathname with path separator. To change destination name, append destination path on the same line, separated by space. expose Sets **EXPOSE** parameter on target image. keep Don't delete image upon uninstall. labels Sets **LABEL** parameters on target image, one per line with format KEY=VALUE. layout Copies a local folder to **assemble-container**'s root with **docker cp**. layout-gid When copying a layout onto **assemble-container**, this GID is set on destination files. layout-uid When copying a layout onto **assemble-container**, this UID is set on destination files. mask Sets /etc/portage/package.mask on builder container's chrooted environment, one per line. name Name of target image. Defaults to part name. packages List of packages to be built in builder container's chrooted environment and installed on **assemble-container**. platform Target platform. Defaults to machine's platform. processor Target processor type. Defaults to machine's processor type. assemble-script Executes a shell script on **assemble-container** after installing Gentoo binary packages. assemble-script-shell Shell for **script** execution. Defaults to "/bin/sh". assemble-script-user User for **script** execution. Defaults to docker default. tty Assign a **Pseudo-TTY** to the **build-container** and **assemble-container**. unmask Sets /etc/portage/package.unmask on builder container's chrooted environment, one per line. use Sets /etc/portage/package.use on builder container's chrooted environment, one per line. user Sets **USER** parameter on target image. variant Target variant. Defaults to "dockeroo". volumes Sets **VOLUME** parameter on target image, one volume per line. volumes-from Mount volumes from specified container. """ subrecipe_class = DockerGentooBuildSubRecipe