Source code for singularity_autobuild.singularity_builder
# -*- coding: utf-8 -*-
""" Build singularity images. """
import os
from subprocess import call
from singularity_autobuild.image_recipe_tools import (
    get_version_from_recipe,
    get_image_name_from_recipe,
    get_collection_from_recipe_path
    )
[docs]class Builder(object):
    """ Facilitate the building of a Singularity image from a recipe.
    Information about the image to be build is gathered at
    instantiation, using the passed recipe file path.
    Building is done by calling the Builder.build() method.
    This method calls the singularity installation on the
    system using the subprocess library.
    Output of the singularity call is piped into a logfile,
    inside the folder build_logs. build_logs will be created
    at runtime if it does not exist.
    :param recipe_path: The full path to the singularity recipe
    :param image_type:  The image type to be produces. identified by used suffix.
    """
    # Directory to create log files, to pipe singularity build output into.
    SUBPROCESS_LOGDIR = '%s/%s' % (
        os.path.dirname(os.path.abspath(__file__)),
        'build_logs'
        )
    def __init__(self, recipe_path: str, image_type: str = 'simg'):
        self.recipe_path = recipe_path
        self.image_type = image_type
        self.build_status = False
        self.build_folder = os.path.dirname(self.recipe_path)
        _filename = os.path.basename(self.recipe_path)
        self.version = get_version_from_recipe(recipe_file_name=_filename)
        # Match everything till the first literal . as the image_name.
        self.image_name = get_image_name_from_recipe(recipe_file_name=_filename)
        """
        Make sure that the subprocess logdir exists.
        GitLab ci will want the directory to be there,
        if it was defined as artifact in the pipeline defintion,
        even if nothing was build.
        """
        if not os.path.exists(self.SUBPROCESS_LOGDIR):
            os.makedirs(self.SUBPROCESS_LOGDIR)
[docs]    def build(self) -> dict:
        """ Calls singularity to build the image.
        :returns: Information about the build image:
                  Full Path to it, name of its parent folder
                  as collection name, version of the image and
                  name of the container at the destination:
                  .. code-block:: python
                        {
                            'image_full_path': '/path/to/image.simg',
                            'collection_name': 'image_parent_folder',
                            'image_version':   '1.0',
                            'container_name':  'image_name'
                        }
        :raises OSError: When Singularity could not be found/executed.
        :raises AttributeError: When Singularity failed with its given parameters.
        """
        _image_info = self.image_info()
        if self.is_build():
            return _image_info
        _subprocess_logpath = "%s/%s.%s.%s.log" % (
            self.SUBPROCESS_LOGDIR,
            _image_info['collection_name'],
            _image_info['container_name'],
            _image_info['image_version']
            )
        try:
            with open(
                _subprocess_logpath,
                'w'
                ) as _subprocess_logfile:
                call(
                    [
                        "singularity",
                        "build",
                        _image_info['image_full_path'],
                        self.recipe_path
                    ],
                    stdout=_subprocess_logfile,
                    stderr=_subprocess_logfile,
                    shell=False)
            self.build_status = True
        except OSError as error:
            raise OSError("singularity build failed with %s." % error)
        return _image_info 
[docs]    def is_build(self) -> bool:
        """ Checks, updates and returns current build status of the image.
        :returns: Build status of the image.
        """
        if self.build_status:
            _info = self.image_info()
            self.build_status = os.path.isfile(
                _info['image_full_path']
            )
        return self.build_status 
[docs]    def image_info(self) -> dict(
            image_full_path='/FULLPATH/TO/image_name.image_type',
            collection_name='image_parent_folder',
            image_version='image_version',
            container_name='image_name'):
        """ Collects data about the image.
        The container_name key returned through the dict is
        the same as the image name. It is intended to be used to
        set the container name in the sregistry.
        :returns: Information about the build image:
                  Full Path to it, name of its parent folder
                  as collection name, version of the image and
                  name of the container at the destination.
                  .. code-block:: python
                        {
                            'image_full_path': '/FULLPATH/TO/image_name.image_type',
                            'collection_name': 'image_parent_folder',
                            'image_version':   'image_version',
                            'container_name':  'image_name'
                        }
        """
        _image_info = {}
        _image_info['image_full_path'] = "%s/%s.%s" % (
            self.build_folder,
            self.image_name,
            self.image_type
        )
        _image_info['collection_name'] = get_collection_from_recipe_path(
            _image_info['image_full_path']
        )
        _image_info['image_version'] = self.version
        _image_info['container_name'] = self.image_name
        return _image_info