Blob Blame History Raw
# pylint: disable=line-too-long,abstract-class-not-used

"""
    Top level function library for centpkg
"""

# Author(s):
#            Jesse Keating <jkeating@redhat.com>
#            Pat Riehecky <riehecky@fnal.gov>
#            Brian Stinson <bstinson@ksu.edu>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
# the full text of the license.


import os
import re
import warnings

import git
import rpm
from pyrpkg import Commands, rpkgError
from pyrpkg.utils import cached_property

# doc/centpkg_man_page.py uses the 'cli' import
from . import cli  # noqa
from .lookaside import StreamLookasideCache, SIGLookasideCache, CLLookasideCache


_DEFAULT_VERSION = "9"


class DistGitDirectory(object):

    signame = None
    centosversion = _DEFAULT_VERSION
    projectname = None
    releasename = None
    distrobranch = False
    sigbranch = False
    repo = None
    git_origin_substr = "git@gitlab.com/redhat/centos-stream"

    def __init__(self, branchtext, repo_path=None):
        if repo_path:
            # self.repo = git.cmd.Git(repo_path)
            self.repo = git.repo.Repo(repo_path)
        rhelbranchre = r"rhel-(?P<major>\d+)\.(?P<minor>\d+)(?:\.(?P<appstream>\d+))?"
        sigtobranchre = r"c(?P<centosversion>\d+[s]?)-sig-(?P<signame>\w+)-?(?P<projectname>\w+)?-?(?P<releasename>\w+)?"
        distrobranchre = r"c(?P<centosversion>\d+)-?(?P<projectname>\w+)?"
        javabranchre = r"openjdk-portable-centos-(?P<centosversion>\d+)"
        oldbranchre = r"(?P<signame>\w+)(?P<centosversion>\d)"
        rhelmatch = re.search(rhelbranchre, branchtext)
        sigmatch = re.match(sigtobranchre, branchtext)
        distromatch = re.match(distrobranchre, branchtext)
        javamatch = re.match(javabranchre, branchtext)
        oldbranchmatch = re.match(oldbranchre, branchtext)
        if rhelmatch:
            gd = rhelmatch.groupdict()
            self.distrobranch = True
            self.signame = "centos"
            self.centosversion = gd["major"]
        elif sigmatch:
            gd = sigmatch.groupdict()
            self.sigbranch = True
            self.signame = gd["signame"]
            self.centosversion = gd["centosversion"]

            # Users have the option to specify (or not specify) common in their
            # git repos. Ww need to handle these cases because common is not a
            # project nor is it a release.
            if gd["projectname"] != "common":
                self.projectname = gd["projectname"]
            if gd["releasename"] != "common":
                self.releasename = gd["releasename"]
        elif distromatch:
            gd = distromatch.groupdict()
            self.distrobranch = True
            self.signame = "centos"
            self.centosversion = gd["centosversion"]

            if gd["projectname"] != "common":
                self.projectname = gd["projectname"]
        elif javamatch:
            gd = javamatch.groupdict()
            self.distrobranch = True
            self.signame = "centos"
            self.centosversion = gd["centosversion"]
        elif oldbranchmatch:
            warnings.warn(
                "This branch is deprecated and will be removed soon", DeprecationWarning
            )
        else:
            if not self.is_fork():
                warnings.warn(
                    "Unable to determine if this is a fork or not. Proceeding, but you should double check."
                )
            else:
                self.distrobranch = True
                self.signame = "centos"
                self.projectname = self.get_origin().split("_")[-1].replace(".git", "")

                warnings.warn(
                    'Remote "origin" was detected as a fork, ignoring branch name checking'
                )

    def get_origin(self):
        if self.repo is None:
            return ""
        if "origin" not in self.repo.remotes:
            return ""
        urls = [u for u in self.repo.remotes["origin"].urls]
        if len(urls) == 0:
            return ""
        return urls[0]

    def is_fork(self):
        """
        Check if origin remote repository is using a fork url.

        Returns
        bool
            A boolean flag indicating if origin remote url is using
            a forked repository url.
        """
        # git+ssh://git@gitlab.com/redhat/centos-stream/rpms/binutils.git
        if self.repo is None:
            return False
        return self.git_origin_substr not in self.get_origin()

    @property
    def target(self):
        projectorcommon = self.projectname
        releaseorcommon = self.releasename

        if self.distrobranch:
            if self.centosversion not in ("6", "7"):
                return "c{}s-candidate".format(self.centosversion)
            else:
                return "-".join(
                    filter(None, ["c" + self.centosversion, projectorcommon])
                )

        if not releaseorcommon:
            if not projectorcommon or projectorcommon == "common":
                projectorcommon = "common"
            else:
                releaseorcommon = "common"

        return "-".join(
            filter(
                None,
                [self.signame + self.centosversion, projectorcommon, releaseorcommon],
            )
        ) + "-el{0}".format(self.centosversion)


class Commands(Commands):
    """
    For the pyrpkg commands with centpkg behavior
    """

    def __init__(self, *args, **kwargs):
        """
        Init the object and some configuration details.
        """
        super(Commands, self).__init__(*args, **kwargs)
        # For MD5 we want to use the old format of source files, the BSD format
        # should only be used when configured for SHA512
        self.source_entry_type = "bsd" if self.lookasidehash != "md5" else "old"
        self.branchre = r"c\d{1,}(s)?(tream)?|master"

    @property
    def distgitdir(self):
        return DistGitDirectory(self.branch_merge, repo_path=self.path)

    @cached_property
    def lookasidecache(self):
        if self.layout.sources_file_template == "sources":
            return StreamLookasideCache(
                self.lookasidehash,
                self.lookaside,
                self.lookaside_cgi,
            )
        else:
            if self.distgitdir.sigbranch:
                return SIGLookasideCache(
                    self.lookasidehash,
                    self.lookaside,
                    self.lookaside_cgi,
                    self.repo_name,
                    self.branch_merge,
                )
            else:
                return CLLookasideCache(
                    self.lookasidehash,
                    self.lookaside,
                    self.lookaside_cgi,
                    self.repo_name,
                    self.branch_merge,
                )

    def _define_patchn_compatiblity_macros(self):
        """
        RPM 4.19 deprecated the %patchN macro. RPM 4.20 removed it completely.
        The macro works on c8s, c9s, c10s, but does not work on Fedora 41+.
        We can no longer even parse RPM spec files with the %patchN macros.
        When we build for old streams, we define the %patchN macros manually as %patch -P N.
        Since N can be any number including zero-prefixed numbers,
        we regex-search the spec file for %patchN uses and define only the macros found.
        """
        # Only do this on RPM 4.19.90+ (4.19.9x were pre-releases of 4.20)
        if tuple(int(i) for i in rpm.__version_info__) < (4, 19, 90):
            return
        # Only do this when building for CentOS Stream version with RPM < 4.20
        try:
            if int(self._distval.split("_")[0]) > 10:
                return
        except ValueError as e:
            self.log.debug(
                "Cannot parse major dist version as int: %s",
                self._distval.split("_")[0],
                exc_info=e,
            )
            return
        defined_patchn = False
        try:
            specfile_path = os.path.join(self.layout.specdir, self.spec)
            with open(specfile_path, "rb") as specfile:
                # Find all uses of %patchN in the spec files
                # Using a benevolent regex: commented out macros, etc. match as well
                for patch in re.findall(rb"%{?patch(\d+)\b", specfile.read()):
                    # We operate on bytes becasue we don't know the spec encoding
                    # but the matched part only includes ASCII digits
                    patch = patch.decode("ascii")
                    self._rpmdefines.extend(
                        [
                            "--define",
                            # defines parametric macro %patchN which passes all arguments to %patch -P N
                            "patch%s(-) %%patch -P %s %%{?**}" % (patch, patch),
                        ]
                    )
                    defined_patchn = True
        except OSError as e:
            self.log.debug("Cannot read spec.", exc_info=e)
        if defined_patchn:
            self.log.warn(
                "centpkg defined %patchN compatibility shims to parse the spec file. "
                "%patchN is obsolete, use %patch -P N instead."
            )

    # redefined loaders
    def load_rpmdefines(self):
        """
        Populate rpmdefines based on branch data
        """

        if not self.distgitdir.centosversion:
            raise rpkgError(
                "Could not get the OS version from the branch:{0}".format(
                    self.branch_merge
                )
            )

        self._distvar = self.distgitdir.centosversion
        self._distval = self._distvar.replace(".", "_")

        self._distunset = 'fedora'
        self._disttag = "el%s" % self._distval
        self._rpmdefines = [
            "--define",
            "_sourcedir %s" % self.layout.sourcedir,
            "--define",
            "_specdir %s" % self.layout.specdir,
            "--define",
            "_builddir %s" % self.layout.builddir,
            "--define",
            "_srcrpmdir %s" % self.layout.srcrpmdir,
            "--define",
            "_rpmdir %s" % self.layout.rpmdir,
            "--define",
            "dist .%s" % self._disttag,
            # int and float this to remove the decimal
            "--define",
            "%s 1" % self._disttag,
            # This is so the rhel macro is set for spec files
            "--define",
            "rhel %s" % self._distval.split("_")[0],
            # This is so the centos macro is set for spec files
            "--define",
            "centos %s" % self._distval.split("_")[0],
            # This is so the fedora macro is unset for spec files
            "--eval",
            "%%undefine %s" % self._distunset,
        ]
        self._define_patchn_compatiblity_macros()
        self.log.debug("RPMDefines: %s" % self._rpmdefines)

    def construct_build_url(self, *args, **kwargs):
        """Override build URL for CentOS/Fedora Koji build

        In CentOS/Fedora Koji, anonymous URL should have prefix "git+https://"
        """
        url = super(Commands, self).construct_build_url(*args, **kwargs)
        return "git+{0}".format(url)

    def load_target(self):
        """This sets the target attribute (used for mock and koji)"""

        self._target = self.distgitdir.target