f116d93
# -*- coding: utf-8 -*-
f116d93
f116d93
f116d93
# This program is free software; you can redistribute it and/or modify
f116d93
# it under the terms of the GNU General Public License as published by
f116d93
# the Free Software Foundation; version 2 of the License.
f116d93
#
f116d93
# This program is distributed in the hope that it will be useful,
f116d93
# but WITHOUT ANY WARRANTY; without even the implied warranty of
f116d93
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
f116d93
# GNU Library General Public License for more details.
f116d93
#
f116d93
# You should have received a copy of the GNU General Public License
f116d93
# along with this program; if not, write to the Free Software
f116d93
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
f116d93
f116d93
f116d93
__all__ = (
f116d93
    "Compose",
f116d93
)
f116d93
f116d93
f116d93
import errno
f116d93
import os
f116d93
import time
f116d93
import tempfile
f116d93
import shutil
f116d93
f116d93
import kobo.log
Adam Miller 5fa5fcd
from productmd.composeinfo import ComposeInfo
771ed2e
from productmd.images import Images
f116d93
07e90f0
from pungi.wrappers.variants import VariantsXmlParser
07e90f0
from pungi.paths import Paths
07e90f0
from pungi.wrappers.scm import get_file_from_scm
07e90f0
from pungi.util import makedirs
fd8cd62
from pungi.metadata import compose_to_composeinfo
f116d93
f116d93
f116d93
def get_compose_dir(topdir, conf, compose_type="production", compose_date=None, compose_respin=None, compose_label=None, already_exists_callbacks=None):
f116d93
    already_exists_callbacks = already_exists_callbacks or []
f116d93
771ed2e
    # create an incomplete composeinfo to generate compose ID
Adam Miller 5fa5fcd
    ci = ComposeInfo()
Adam Miller 5fa5fcd
    ci.compose.name = conf["product_name"]
Adam Miller 5fa5fcd
    ci.release.name = conf["product_name"]
Adam Miller 5fa5fcd
    ci.compose.short = conf["product_short"]
Adam Miller 5fa5fcd
    ci.release.short = conf["product_short"]
Adam Miller 5fa5fcd
    ci.compose.version = conf["product_version"]
Adam Miller 5fa5fcd
    ci.release.version = conf["product_version"]
Adam Miller 5fa5fcd
    ci.compose.is_layered = bool(conf.get("product_is_layered", False))
Adam Miller 5fa5fcd
    if ci.compose.is_layered:
f116d93
        ci.base_product.name = conf["base_product_name"]
f116d93
        ci.base_product.short = conf["base_product_short"]
f116d93
        ci.base_product.version = conf["base_product_version"]
f116d93
f116d93
    ci.compose.label = compose_label
f116d93
    ci.compose.type = compose_type
f116d93
    ci.compose.date = compose_date or time.strftime("%Y%m%d", time.localtime())
f116d93
    ci.compose.respin = compose_respin or 0
f116d93
f116d93
    while 1:
f116d93
        ci.compose.id = ci.create_compose_id()
f116d93
f116d93
        compose_dir = os.path.join(topdir, ci.compose.id)
f116d93
f116d93
        exists = False
f116d93
        # TODO: callbacks to determine if a composeid was already used
f116d93
        # for callback in already_exists_callbacks:
f116d93
        #     if callback(data):
f116d93
        #         exists = True
f116d93
        #         break
f116d93
f116d93
        # already_exists_callbacks fallback: does target compose_dir exist?
f116d93
        if not exists:
f116d93
            try:
f116d93
                os.makedirs(compose_dir)
f116d93
            except OSError as ex:
f116d93
                if ex.errno == errno.EEXIST:
f116d93
                    exists = True
f116d93
                else:
f116d93
                    raise
f116d93
f116d93
        if exists:
f116d93
            ci.compose.respin += 1
f116d93
            continue
f116d93
        break
f116d93
f116d93
    open(os.path.join(compose_dir, "COMPOSE_ID"), "w").write(ci.compose.id)
f116d93
    work_dir = os.path.join(compose_dir, "work", "global")
f116d93
    makedirs(work_dir)
f116d93
    ci.dump(os.path.join(work_dir, "composeinfo-base.json"))
f116d93
    return compose_dir
f116d93
f116d93
f116d93
class Compose(kobo.log.LoggingBase):
f116d93
    def __init__(self, conf, topdir, debug=False, skip_phases=None, just_phases=None, old_composes=None, koji_event=None, supported=False, logger=None):
f116d93
        kobo.log.LoggingBase.__init__(self, logger)
f116d93
        # TODO: check if minimal conf values are set
f116d93
        self.conf = conf
f116d93
        self.variants = {}
f116d93
        self.topdir = os.path.abspath(topdir)
f116d93
        self.skip_phases = skip_phases or []
f116d93
        self.just_phases = just_phases or []
f116d93
        self.old_composes = old_composes or []
f116d93
        self.koji_event = koji_event
f116d93
f116d93
        # intentionally upper-case (visible in the code)
f116d93
        self.DEBUG = debug
f116d93
f116d93
        # path definitions
f116d93
        self.paths = Paths(self)
f116d93
f116d93
        # to provide compose_id, compose_date and compose_respin
Adam Miller 5fa5fcd
        self.ci_base = ComposeInfo()
f116d93
        self.ci_base.load(os.path.join(self.paths.work.topdir(arch="global"), "composeinfo-base.json"))
f116d93
f116d93
        self.supported = supported
f116d93
        if self.compose_label and self.compose_label.split("-")[0] == "RC":
f116d93
            self.log_info("Automatically setting 'supported' flag for a Release Candidate (%s) compose." % self.compose_label)
f116d93
            self.supported = True
f116d93
Adam Miller 5fa5fcd
        self.im = Images()
f116d93
        if self.DEBUG:
f116d93
            try:
f116d93
                self.im.load(self.paths.compose.metadata("images.json"))
f116d93
            except RuntimeError:
f116d93
                pass
f116d93
        self.im.compose.id = self.compose_id
f116d93
        self.im.compose.type = self.compose_type
f116d93
        self.im.compose.date = self.compose_date
f116d93
        self.im.compose.respin = self.compose_respin
f116d93
        self.im.metadata_path = self.paths.compose.metadata()
f116d93
f116d93
    get_compose_dir = staticmethod(get_compose_dir)
f116d93
f116d93
    def __getitem__(self, name):
f116d93
        return self.variants[name]
f116d93
f116d93
    @property
f116d93
    def compose_id(self):
f116d93
        return self.ci_base.compose.id
f116d93
f116d93
    @property
f116d93
    def compose_date(self):
f116d93
        return self.ci_base.compose.date
f116d93
f116d93
    @property
f116d93
    def compose_respin(self):
f116d93
        return self.ci_base.compose.respin
f116d93
f116d93
    @property
f116d93
    def compose_type(self):
f116d93
        return self.ci_base.compose.type
f116d93
f116d93
    @property
f116d93
    def compose_type_suffix(self):
f116d93
        return self.ci_base.compose.type_suffix
f116d93
f116d93
    @property
f116d93
    def compose_label(self):
f116d93
        return self.ci_base.compose.label
f116d93
f116d93
    @property
f116d93
    def has_comps(self):
f116d93
        return bool(self.conf.get("comps_file", False))
f116d93
f116d93
    @property
f116d93
    def config_dir(self):
f116d93
        return os.path.dirname(self.conf._open_file or "")
f116d93
f116d93
    def read_variants(self):
f116d93
        # TODO: move to phases/init ?
f116d93
        variants_file = self.paths.work.variants_file(arch="global")
f116d93
        msg = "Writing variants file: %s" % variants_file
f116d93
f116d93
        if self.DEBUG and os.path.isfile(variants_file):
f116d93
            self.log_warning("[SKIP ] %s" % msg)
f116d93
        else:
f116d93
            scm_dict = self.conf["variants_file"]
f116d93
            if isinstance(scm_dict, dict):
f116d93
                file_name = os.path.basename(scm_dict["file"])
f116d93
                if scm_dict["scm"] == "file":
f116d93
                    scm_dict["file"] = os.path.join(self.config_dir, os.path.basename(scm_dict["file"]))
f116d93
            else:
f116d93
                file_name = os.path.basename(scm_dict)
f116d93
                scm_dict = os.path.join(self.config_dir, os.path.basename(scm_dict))
f116d93
f116d93
            self.log_debug(msg)
f116d93
            tmp_dir = tempfile.mkdtemp(prefix="variants_file_")
f116d93
            get_file_from_scm(scm_dict, tmp_dir, logger=self._logger)
f116d93
            shutil.copy2(os.path.join(tmp_dir, file_name), variants_file)
f116d93
            shutil.rmtree(tmp_dir)
f116d93
f116d93
        file_obj = open(variants_file, "r")
f116d93
        tree_arches = self.conf.get("tree_arches", None)
f116d93
        self.variants = VariantsXmlParser(file_obj, tree_arches).parse()
f116d93
f116d93
        # populate ci_base with variants - needed for layered-products (compose_id)
Adam Miller 5fa5fcd
        ####FIXME - compose_to_composeinfo is no longer needed and has been
Adam Miller 5fa5fcd
        ####        removed, but I'm not entirely sure what this is needed for
Adam Miller 5fa5fcd
        ####        or if it is at all
fd8cd62
        self.ci_base = compose_to_composeinfo(self)
f116d93
f116d93
    def get_variants(self, types=None, arch=None, recursive=False):
f116d93
        result = []
f116d93
        types = types or ["variant", "optional", "addon", "layered-product"]
f116d93
        for i in self.variants.values():
f116d93
            if i.type in types:
f116d93
                if arch and arch not in i.arches:
f116d93
                    continue
f116d93
                result.append(i)
f116d93
            result.extend(i.get_variants(types=types, arch=arch, recursive=recursive))
f116d93
        return sorted(set(result))
f116d93
f116d93
    def get_arches(self):
f116d93
        result = set()
f116d93
        tree_arches = self.conf.get("tree_arches", None)
f116d93
        for variant in self.get_variants():
f116d93
            for arch in variant.arches:
f116d93
                if tree_arches:
f116d93
                    if arch in tree_arches:
f116d93
                        result.add(arch)
f116d93
                else:
f116d93
                    result.add(arch)
f116d93
        return sorted(result)
f116d93
f116d93
    def write_status(self, stat_msg):
f116d93
        if stat_msg not in ("STARTED", "FINISHED", "DOOMED"):
f116d93
            self.log_warning("Writing nonstandard compose status: %s" % stat_msg)
f116d93
        old_status = self.get_status()
f116d93
        if stat_msg == old_status:
f116d93
            return
f116d93
        if old_status == "FINISHED":
f116d93
            msg = "Could not modify a FINISHED compose: %s" % self.topdir
f116d93
            self.log_error(msg)
f116d93
            raise RuntimeError(msg)
f116d93
        open(os.path.join(self.topdir, "STATUS"), "w").write(stat_msg + "\n")
f116d93
f116d93
    def get_status(self):
f116d93
        path = os.path.join(self.topdir, "STATUS")
f116d93
        if not os.path.isfile(path):
f116d93
            return
f116d93
        return open(path, "r").read().strip()