# Copyright (C) 2006 Novell Inc.  All rights reserved.
# This program is free software; it may be used, copied, modified
# and distributed under the terms of the GNU General Public Licence,
# either version 2, or (at your option) any later version.

import fnmatch
import getpass
import glob
import os
import re
import shutil
import subprocess
import sys
from tempfile import NamedTemporaryFile, mkdtemp
from typing import List
from typing import Optional
from urllib.parse import urlsplit
from urllib.request import URLError, HTTPError
from xml.etree import ElementTree as ET

from . import conf
from . import connection
from . import core
from . import oscerr
from .core import get_buildinfo, meta_exists, get_buildconfig, dgst
from .core import get_binarylist, get_binary_file, run_external, return_external, raw_input
from .fetch import Fetcher, OscFileGrabber, verify_pacs
from .meter import create_text_meter
from .util import cpio
from .util import archquery, debquery, packagequery, rpmquery
from .util import repodata
from .util.helper import decode_it
from .util.models import *
from .util.xml import xml_parse


change_personality = {
    'i686': 'linux32',
    'i586': 'linux32',
    'i386': 'linux32',
    'ppc': 'powerpc32',
    's390': 's390',
    'sparc': 'linux32',
    'sparcv8': 'linux32',
}

can_also_build = {
    'aarch64': ['aarch64'],  # only needed due to used heuristics in build parameter evaluation
    'armv6l': ['armv4l', 'armv5l', 'armv6l', 'armv5el', 'armv6el'],
    'armv7l': ['armv4l', 'armv5l', 'armv6l', 'armv7l', 'armv5el', 'armv6el', 'armv7el'],
    'armv5el': ['armv4l', 'armv5l', 'armv5el'],  # not existing arch, just for compatibility
    'armv6el': ['armv4l', 'armv5l', 'armv6l', 'armv5el', 'armv6el'],  # not existing arch, just for compatibility
    'armv6hl': ['armv4l', 'armv5l', 'armv6l', 'armv5el', 'armv6el'],
    'armv7el': ['armv4l', 'armv5l', 'armv6l', 'armv7l', 'armv5el', 'armv6el', 'armv7el'],  # not existing arch, just for compatibility
    'armv7hl': ['armv7hl'],  # not existing arch, just for compatibility
    'armv8el': ['armv4l', 'armv5el', 'armv6el', 'armv7el', 'armv8el'],  # not existing arch, just for compatibility
    'armv8l': ['armv4l', 'armv5el', 'armv6el', 'armv7el', 'armv8el'],  # not existing arch, just for compatibility
    'armv5tel': ['armv4l', 'armv5el', 'armv5tel'],
    's390x': ['s390'],
    'ppc64': ['ppc', 'ppc64', 'ppc64p7', 'ppc64le'],
    'ppc64le': ['ppc64le', 'ppc64'],
    'i586': ['i386'],
    'i686': ['i586', 'i386'],
    'x86_64': ['i686', 'i586', 'i386'],
    'sparc64': ['sparc64v', 'sparcv9v', 'sparcv9', 'sparcv8', 'sparc'],
    'parisc': ['hppa'],
}

# real arch of this machine
hostarch = os.uname()[4]
if hostarch == 'i686':  # FIXME
    hostarch = 'i586'

if hostarch == 'parisc':
    hostarch = 'hppa'


class BuildType(BaseModel):
    name: str = Field()
    binary_type: Optional[str] = Field()
    recipes: List[str] = Field()
    package_suffix: Optional[str] = Field()
    binary_packages_paths: List[str] = Field()
    source_packages_paths: List[str] = Field()
    prefer_packages_paths: List[str] = Field(default=[])
    prefer_packages_exclude_paths: List[str] = Field(default=[])

    def _get_files(self, topdir: str, patterns: List[str]) -> List[str]:
        result = []
        for pattern in patterns:
            path = os.path.join(topdir, pattern.lstrip("/"))
            for i in glob.glob(path, recursive=True):
                if not os.path.isfile(i):
                    continue
                result.append(i)
        return result

    def get_binaries(self, topdir: str) -> List[str]:
        return self._get_files(topdir, self.binary_packages_paths)

    def get_sources(self, topdir: str) -> List[str]:
        return self._get_files(topdir, self.source_packages_paths)

    def get_recipes(self, topdir: str) -> List[str]:
        result = []
        for fn in os.listdir(topdir):
            for pattern in self.recipes:
                if fnmatch.fnmatch(fn, pattern):
                    result.append(fn)
        result.sort()
        return result


BUILD_TYPES: List[BuildType] = [
    BuildType(
        name="appimage",
        recipes=["appimage.yml"],
        binary_packages_paths=["OTHER/**/*.AppImage"],
        source_packages_paths=[],
    ),
    BuildType(
        name="arch",
        recipes=["PKGBUILD"],
        package_suffix="arch",
        binary_packages_paths=["ARCHPKGS/**/*.pkg.tar*"],
        source_packages_paths=[],
        prefer_packages_paths=["**/*.pkg.tar*"],
    ),
    BuildType(
        name="collax",
        recipes=["build.collax"],
        package_suffix="deb",
        binary_packages_paths=["DEBS/**/*.deb"],
        source_packages_paths=["SOURCES.DEB/**"],
        prefer_packages_paths=["**/*.deb"],
    ),
    BuildType(
        # IMPORTANT: keep in sync with "podman"
        name="docker",
        recipes=["Containerfile", "Dockerfile", "Containerfile.*", "Dockerfile.*"],
        binary_packages_paths=["DOCKER/**"],
        source_packages_paths=[],
    ),
    BuildType(
        name="dsc",
        recipes=["*.dsc"],
        package_suffix="deb",
        binary_packages_paths=["DEBS/**/*.deb"],
        source_packages_paths=["SOURCES.DEB/**"],
        prefer_packages_paths=["**/*.deb"],
    ),
    BuildType(
        name="fissile",
        recipes=["fissile.yml"],
        binary_packages_paths=["FISSILE/**"],
        source_packages_paths=[],
    ),
    BuildType(
        name="flatpak",
        recipes=["*flatpak.yaml", "*flatpak.yml", "*flatpak.json"],
        binary_packages_paths=["OTHER/**"],
        source_packages_paths=[],
    ),
    BuildType(
        name="helm",
        recipes=["Chart.yaml"],
        binary_packages_paths=["HELM/**"],
        source_packages_paths=[],
    ),
    BuildType(
        name="kiwi",
        recipes=["*.kiwi"],
        binary_packages_paths=["KIWI/**"],
        source_packages_paths=[],
    ),
    BuildType(
        name="livebuild",
        recipes=["*.livebuild"],
        package_suffix="deb",
        binary_packages_paths=["OTHER/**/*.iso"],
        source_packages_paths=[],
        prefer_packages_paths=["**/*.deb"],
    ),
    BuildType(
        name="mkosi",
        recipes=["mkosi.*"],
        binary_packages_paths=["OTHER/**"],
        source_packages_paths=[],
    ),
    BuildType(
        # IMPORTANT: keep in sync with "docker"
        name="podman",
        recipes=["Containerfile", "Dockerfile", "Containerfile.*", "Dockerfile.*"],
        binary_packages_paths=["DOCKER/**"],
        source_packages_paths=[],
    ),
    BuildType(
        name="preinstallimage",
        recipes=["_preinstallimage"],
        binary_packages_paths=["OTHER/**"],
        source_packages_paths=[],
    ),
    BuildType(
        name="productcompose",
        recipes=["*.productcompose"],
        binary_packages_paths=["PRODUCT/**"],
        source_packages_paths=[],
    ),
    BuildType(
        name="simpleimage",
        recipes=["simpleimage"],
        binary_packages_paths=["OTHER/**"],
        source_packages_paths=[],
    ),
    BuildType(
        name="snapcraft",
        recipes=["snapcraft.yaml"],
        package_suffix="deb",
        binary_packages_paths=["OTHER/**/*.snap"],
        source_packages_paths=[],
    ),
    BuildType(
        name="spec",
        binary_type="rpm",
        recipes=["*.spec"],
        package_suffix="rpm",
        binary_packages_paths=["RPMS/**/*.rpm"],
        source_packages_paths=["SRPMS/**/*.src.rpm"],
        prefer_packages_paths=["**/*.rpm"],
        prefer_packages_exclude_paths=["*.src.rpm", "*.nosrc.rpm", "*.patch.rpm", "*.delta.rpm"],
    ),
    BuildType(
        name="spec",
        binary_type="deb",
        recipes=["*.spec"],
        package_suffix="deb",
        binary_packages_paths=["DEBS/**/*.deb"],
        source_packages_paths=["SDEBS/**/*.sdeb"],
        prefer_packages_paths=["**/*.deb"],
    ),
]


def get_build_type(name: str, *, binary_type: Optional[str] = None) -> BuildType:
    for build_type in BUILD_TYPES:
        # compare the binary type only if it's specified in the build_type
        if binary_type and build_type.binary_type:
            if build_type.name == name and build_type.binary_type == binary_type:
                return build_type
        elif build_type.name == name:
            return build_type
    raise oscerr.OscValueError(f"Unknown build type: {name}")


def get_build_type_from_recipe_path(recipe_path: str) -> BuildType:
    recipe_filename = os.path.basename(recipe_path)
    for build_type in BUILD_TYPES:
        for pattern in build_type.recipes:
            if fnmatch.fnmatch(recipe_filename, pattern):
                return build_type
    raise oscerr.OscValueError(f"The specified recipe doesn't match any known build type: {recipe_filename}")


def find_build_recipes(topdir: str, build_type: Optional[str] = None) -> List[str]:
    """
    Return all file names in ``topdir`` that match known build recipes.
    """
    topdir = os.path.abspath(topdir)
    result = []
    for fn in os.listdir(topdir):
        recipe_path = os.path.join(topdir, fn)
        try:
            get_build_type_from_recipe_path(recipe_path)
        except oscerr.OscValueError:
            continue
        result.append(fn)
    return result


class Buildinfo:
    """represent the contents of a buildinfo file"""

    def __init__(self, filename, apiurl, buildtype='spec', localpkgs=None, binarytype='rpm'):
        localpkgs = localpkgs or []
        try:
            tree = xml_parse(filename)
        except ET.ParseError:
            print('could not parse the buildinfo:', file=sys.stderr)
            print(open(filename).read(), file=sys.stderr)
            sys.exit(1)

        root = tree.getroot()

        self.apiurl = apiurl

        if root.find('error') is not None:
            sys.stderr.write('buildinfo is broken... it says:\n')
            error = root.findtext("error")
            if error.startswith('unresolvable: '):
                sys.stderr.write('unresolvable: ')
                sys.stderr.write('\n     '.join(error[14:].split(',')))
            else:
                sys.stderr.write(error)
            sys.stderr.write('\n')
            sys.exit(1)

        if not (apiurl.startswith('https://') or apiurl.startswith('http://')):
            raise URLError('invalid protocol for the apiurl: \'%s\'' % apiurl)

        self.buildtype = buildtype
        self.binarytype = binarytype
        self.apiurl = apiurl

        # are we building .rpm or .deb?
        # XXX: shouldn't we deliver the type via the buildinfo?
        build_type = get_build_type(self.buildtype, binary_type=self.binarytype)
        self.pacsuffix = build_type.package_suffix
        if not self.pacsuffix:
            # the previous osc code did this for 'docker' build type, maybe it's ok to use it as a generic fallback
            self.pacsuffix = self.binarytype

        # The architectures become a bit mad ...
        # buildarch: The architecture of the build result      (host arch in GNU definition)
        # hostarch:  The architecture of the build environment (build arch in GNU defintion)
        # crossarch: Same as hostarch, but indicating that a sysroot with an incompatible architecture exists
        self.buildarch = root.findtext("arch")
        self.crossarch = root.findtext("crossarch")
        self.hostarch = root.findtext("hostarch")
        self.release = root.findtext("release")
        if conf.config['api_host_options'][apiurl]['downloadurl']:
            # Formerly, this was set to False, but we have to set it to True, because a large
            # number of repos in OBS are misconfigured and don't actually have repos setup - they
            # are API only.
            self.enable_cpio = True
            self.downloadurl = conf.config['api_host_options'][apiurl]['downloadurl'] + "/repositories"
            if conf.config['http_debug']:
                print("⚠️   setting dl_url to %s" % conf.config['api_host_options'][apiurl]['downloadurl'])
        else:
            self.enable_cpio = True
            self.downloadurl = root.get('downloadurl')

        self.debuginfo = 0
        if root.find('debuginfo') is not None:
            try:
                self.debuginfo = int(root.findtext("debuginfo"))
            except ValueError:
                pass

        self.deps = []
        self.projects = {}
        self.keys = []
        self.prjkeys = []
        self.pathes = []
        self.urls = {}
        self.modules = []
        for node in root.findall('module'):
            self.modules.append(node.text)
        for node in root.findall('bdep'):
            if node.find('sysroot'):
                p = Pac(node, self.buildarch, self.pacsuffix,
                        apiurl, localpkgs)
            else:
                pac_arch = self.crossarch
                if pac_arch is None:
                    pac_arch = self.buildarch
                p = Pac(node, pac_arch, self.pacsuffix,
                        apiurl, localpkgs)
            if p.project:
                self.projects[p.project] = 1
            self.deps.append(p)
        for node in root.findall('path'):
            # old simple list for compatibility
            # XXX: really old? This is currently used for kiwi builds
            self.pathes.append(node.get('project') + "/" + node.get('repository'))
            # a hash providing the matching URL for specific repos for newer OBS instances
            if node.get('url'):
                baseurl = node.get('url').replace('%', '%%')
                if conf.config['api_host_options'][apiurl]['downloadurl']:
                    # Add the path element to the download url override.
                    baseurl = conf.config['api_host_options'][apiurl]['downloadurl'] + urlsplit(node.get('url'))[2]
                self.urls[node.get('project') + "/" + node.get('repository')] = baseurl + '/%(arch)s/%(filename)s'

        self.vminstall_list = [dep.name for dep in self.deps if dep.vminstall]
        self.preinstall_list = [dep.name for dep in self.deps if dep.preinstall]
        self.runscripts_list = [dep.name for dep in self.deps if dep.runscripts]
        self.noinstall_list = [dep.name for dep in self.deps if dep.noinstall]
        self.installonly_list = [dep.name for dep in self.deps if dep.installonly]

        if root.find('preinstallimage') is not None:
            self.preinstallimage = root.find('preinstallimage')
        else:
            self.preinstallimage = None

        self.containerannotation = root.findtext("containerannotation")

    def has_dep(self, name):
        for i in self.deps:
            if i.name == name:
                return True
        return False

    def remove_dep(self, name):
        # we need to iterate over all deps because if this a
        # kiwi build the same package might appear multiple times
        # NOTE: do not loop and remove items, the second same one would not get catched
        self.deps = [i for i in self.deps if not i.name == name]


class Pac:
    """represent a package to be downloaded

    We build a map that's later used to fill our URL templates
    """

    def __init__(self, node, buildarch, pacsuffix, apiurl, localpkgs=None):
        localpkgs = localpkgs or []

        # set attributes to mute pylint error E1101: Instance of 'Pac' has no '<attr>' member (no-member)
        self.project = None
        self.name = None
        self.canonname = None
        self.repository = None
        self.repoarch = None

        self.mp = {}
        for i in ['binary', 'package',
                  'epoch', 'version', 'release', 'hdrmd5',
                  'project', 'repository', 'sysroot',
                  'preinstall', 'vminstall', 'runscripts',
                  'noinstall', 'installonly', 'notmeta',
                  ]:
            self.mp[i] = node.get(i)

        self.mp['buildarch'] = buildarch
        self.mp['pacsuffix'] = pacsuffix

        self.mp['arch'] = node.get('arch') or self.mp['buildarch']
        self.mp['name'] = node.get('name') or self.mp['binary']

        # this is not the ideal place to check if the package is a localdep or not
        localdep = self.mp['name'] in localpkgs  # and not self.mp['noinstall']
        if not localdep and not (node.get('project') and node.get('repository')):
            raise oscerr.APIError('incomplete information for package %s, may be caused by a broken project configuration.'
                                  % self.mp['name'])

        if not localdep:
            self.mp['extproject'] = node.get('project').replace(':', ':/')
            self.mp['extrepository'] = node.get('repository').replace(':', ':/')
        self.mp['repopackage'] = node.get('package') or '_repository'
        self.mp['repoarch'] = node.get('repoarch') or self.mp['buildarch']

        if pacsuffix == 'deb' and not (self.mp['name'] and self.mp['arch'] and self.mp['version']):
            raise oscerr.APIError(
                "buildinfo for package %s/%s/%s is incomplete"
                % (self.mp['name'], self.mp['arch'], self.mp['version']))

        self.mp['apiurl'] = apiurl

        if self.mp['epoch'] is None:
            epoch = None
        else:
            epoch = self.mp['epoch'].encode()

        if self.mp['release'] is None:
            release = None
        else:
            release = self.mp['release'].encode()

        if self.mp['binary'] and self.mp['binary'] == 'updateinfo.xml':
            canonname = 'updateinfo.xml'
            if self.mp['repopackage'] != '_repository':
                canonname = self.mp['repopackage'] + '.' + self.mp['repoarch'] + '.updateinfo.xml'
        elif self.mp['binary'] and self.mp['binary'] == '_modulemd.yaml':
            canonname = '_modulemd.yaml'
            if self.mp['repopackage'] != '_repository':
                canonname = self.mp['repopackage'] + '.' + self.mp['repoarch'] + '.modulemd.yaml'
        elif self.mp['name'].startswith('container:'):
            canonname = self.mp['name'] + '.tar.xz'
        elif pacsuffix == 'deb':
            canonname = debquery.DebQuery.filename(self.mp['name'].encode(), epoch, self.mp['version'].encode(), release, self.mp['arch'].encode())
        elif pacsuffix == 'arch':
            canonname = archquery.ArchQuery.filename(self.mp['name'].encode(), epoch, self.mp['version'].encode(), release, self.mp['arch'].encode())
        else:
            canonname = rpmquery.RpmQuery.filename(self.mp['name'].encode(), epoch, self.mp['version'].encode(), release or b'0', self.mp['arch'].encode())

        self.mp['canonname'] = decode_it(canonname)
        if not self.mp['binary']:
            self.mp['binary'] = self.mp['canonname']
        # maybe we should rename filename key to binary
        self.mp['filename'] = self.mp['binary']
        if self.mp['repopackage'] == '_repository':
            self.mp['repofilename'] = self.mp['name']
        else:
            # OBS 2.3 puts binary into product bdeps (noinstall ones)
            self.mp['repofilename'] = self.mp['filename']

        # make the content of the dictionary accessible as class attributes
        self.__dict__.update(self.mp)

    def makeurls(self, cachedir, urllist):
        self.localdir = '%s/%s/%s/%s' % (cachedir, self.project, self.repository, self.repoarch)
        self.fullfilename = os.path.join(self.localdir, self.canonname)
        self.urllist = [url % self.mp for url in urllist]

    def __str__(self):
        return self.name or ""

    def __repr__(self):
        return "%s" % self.name


def get_preinstall_image(apiurl, arch, cache_dir, img_info, offline=False):
    """
    Searches preinstall image according to build info and downloads it to cache
    (unless offline is set to ``True`` (default: ``False``)).
    Returns preinstall image path, source and list of image binaries, which can
    be used to create rpmlist.

    .. note::
        preinstall image can be used only for new build roots!
    """
    imagefile = ""
    imagesource = ""
    info_file = "preinstallimage.info"
    img_bins = []
    for bin in img_info.findall('binary'):
        img_bins.append(bin.text)

    img_project = img_info.get('project')
    img_repository = img_info.get('repository')
    img_arch = arch
    img_pkg = img_info.get('package')
    img_file = img_info.get('filename')
    img_hdrmd5 = img_info.get('hdrmd5')
    if not img_hdrmd5:
        img_hdrmd5 = img_file
    cache_path = "%s/%s/%s/%s" % (cache_dir, img_project, img_repository, img_arch)
    ifile_path = "%s/%s" % (cache_path, img_file)
    info_file_path = "%s/%s" % (cache_path, info_file)

    imagefile = ifile_path
    imagesource = "%s/%s/%s [%s]" % (img_project, img_repository, img_pkg, img_hdrmd5)
    imageinfo = info_file_path

    if not os.path.exists(ifile_path):
        if offline:
            return "", "", "", []
        url = "%s/build/%s/%s/%s/%s/%s" % (apiurl, img_project, img_repository, img_arch, img_pkg, img_file)
        print("downloading preinstall image %s" % imagesource)
        if not os.path.exists(cache_path):
            try:
                os.makedirs(cache_path, mode=0o755)
            except OSError as e:
                print('packagecachedir is not writable for you?', file=sys.stderr)
                print(e, file=sys.stderr)
                sys.exit(1)
        progress_obj = None
        if sys.stdout.isatty():
            progress_obj = create_text_meter(use_pb_fallback=False)
        gr = OscFileGrabber(progress_obj=progress_obj)
        with NamedTemporaryFile(dir=cache_path, delete=False) as temp_file:
            try:
                gr.urlgrab(url, filename=temp_file.name, text="fetching image")
                # download ok, rename temp file to final file name
                os.rename(temp_file.name, ifile_path)
            except HTTPError as e:
                print("Failed to download! ecode:%i reason:%s" % (e.code, e.reason))
                # Clean up temp file if it still exists
                if os.path.exists(temp_file.name):
                    os.unlink(temp_file.name)
                return ("", "", "", [])

        # Also download the corresponding .info file
        if not os.path.exists(info_file_path):
            info_url = "%s/build/%s/%s/%s/%s/%s" % (apiurl, img_project, img_repository, img_arch, img_pkg, info_file)
            print("downloading preinstall image info file")
            with NamedTemporaryFile(dir=cache_path, delete=False) as temp_file:
                try:
                    gr.urlgrab(info_url, filename=temp_file.name, text="fetching image info")
                    # download ok, rename temp file to final file name
                    os.rename(temp_file.name, info_file_path)
                except HTTPError as e:
                    print("Failed to download info file! ecode:%i reason:%s" % (e.code, e.reason))
                    # Clean up temp file if it still exists
                    if os.path.exists(temp_file.name):
                        os.unlink(temp_file.name)

    return (imagefile, imagesource, imageinfo, img_bins)


def get_built_files(pacdir, buildtype, *, binary_type: Optional[str] = None) -> Tuple[str, str]:
    build_type = get_build_type(buildtype, binary_type=binary_type)
    sources = build_type.get_sources(pacdir)
    binaries = build_type.get_binaries(pacdir)
    return "\n".join(sources), "\n".join(binaries)


def get_repo(path):
    """
    Walks up path looking for any repodata directories.

    :param path: path to a directory
    :return: path to repository directory containing repodata directory with repomd.xml file
    :rtype: str
    """

    for root, dirs, files in os.walk(path):
        if not "repodata" in dirs:
            continue
        if "repomd.xml" in os.listdir(os.path.join(root, "repodata")):
            return root
    return None


def get_prefer_pkgs(dirs, wanted_arch, type, cpio):
    paths = []
    repositories = []

    for pkgs_dir in dirs:
        # check for repodata
        repository = get_repo(pkgs_dir)
        if repository:
            repositories.append(repository)
        else:
            # we don't know binary type as we haven't received buildconfig yet
            # that's why we add all files for any matching build types
            for build_type in BUILD_TYPES:
                if build_type.name != type:
                    continue
                for pattern in build_type.prefer_packages_paths:
                    for pkg_path in glob.glob(os.path.join(os.path.abspath(pkgs_dir), pattern), recursive=True):
                        use_package = True
                        for exclude_pattern in build_type.prefer_packages_exclude_paths:
                            if fnmatch.fnmatch(pkg_path, exclude_pattern):
                                use_package = False
                                break
                        if use_package:
                            paths.append(pkg_path)

    packageQueries = packagequery.PackageQueries(wanted_arch)

    for repository in repositories:
        repodataPackageQueries = repodata.queries(repository)
        for packageQuery in repodataPackageQueries:
            packageQueries.add(packageQuery)

    for path in paths:
        packageQuery = packagequery.PackageQuery.query(path)
        packageQueries.add(packageQuery)

    prefer_pkgs = {decode_it(name): packageQuery.path()
                   for name, packageQuery in packageQueries.items()}

    depfile = create_deps(packageQueries.values())
    cpio.add(b'deps', b'\n'.join(depfile))
    return prefer_pkgs


def create_deps(pkgqs):
    """
    creates a list of dependencies which corresponds to build's internal
    dependency file format
    """
    depfile = []
    for p in pkgqs:
        id = b'%s.%s-0/0/0: ' % (p.name(), p.arch())
        depfile.append(b'P:%s%s' % (id, b' '.join(p.provides())))
        depfile.append(b'R:%s%s' % (id, b' '.join(p.requires())))
        d = p.conflicts()
        if d:
            depfile.append(b'C:%s%s' % (id, b' '.join(d)))
        d = p.obsoletes()
        if d:
            depfile.append(b'O:%s%s' % (id, b' '.join(d)))
        d = p.recommends()
        if d:
            depfile.append(b'r:%s%s' % (id, b' '.join(d)))
        d = p.supplements()
        if d:
            depfile.append(b's:%s%s' % (id, b' '.join(d)))
        depfile.append(b'I:%s%s-%s 0-%s' % (id, p.name(), p.evr(), p.arch()))
    return depfile


trustprompt = """Would you like to ...
0 - quit (default)
1 - always trust packages from '%(project)s'
2 - trust packages just this time
? """


def check_trusted_projects(apiurl, projects, interactive=True):
    trusted = conf.config['api_host_options'][apiurl]['trusted_prj']
    tlen = len(trusted)
    for prj in projects:
        is_trusted = False
        for pattern in trusted:
            if fnmatch.fnmatch(prj, pattern):
                is_trusted = True
                break
        if not is_trusted:
            print("\nThe build root needs packages from project '%s'." % prj)
            print("Note that malicious packages can compromise the build result or even your system.")

            if interactive:
                r = raw_input(trustprompt % {'project': prj})
            else:
                r = "0"

            if r == '1':
                print("adding '%s' to oscrc: ['%s']['trusted_prj']" % (prj, apiurl))
                trusted.append(prj)
            elif r != '2':
                print("Well, goodbye then :-)")
                raise oscerr.UserAbort()

    if tlen != len(trusted):
        conf.config['api_host_options'][apiurl]['trusted_prj'] = trusted
        conf.config_set_option(apiurl, 'trusted_prj', ' '.join(trusted))


def get_kiwipath_from_buildinfo(bi, prj, repo):
    # If the project does not have a path defined we need to get the config
    # via the repositories in the kiwi file. Unfortunately the buildinfo
    # does not include a hint if this is the case, so we rely on a heuristic
    # here: if the path list contains our own repo, it probably does not
    # come from the kiwi file and thus a path is defined in the config.
    # It is unlikely that our own repo is included in the kiwi file, as it
    # contains no packages.
    myprp = prj + '/' + repo
    if myprp in bi.pathes:
        return None
    kiwipath = bi.pathes
    kiwipath.insert(0, myprp)
    return kiwipath


def calculate_prj_pac(store, opts, descr):
    project = opts.alternative_project or store.project
    if opts.local_package:
        package = os.path.splitext(os.path.basename(descr))[0]
    else:
        store.assert_is_package()
        package = store.package
    return project, package


def calculate_build_root_user(vm_type):
    if vm_type in ("kvm", "podman", "qemu"):
        return getpass.getuser()
    return None


def calculate_build_root(apihost, prj, pac, repo, arch, user=None):
    user = user or ""
    dash_user = f"-{user:s}" if user else ""
    buildroot = conf.config["build-root"] % {
        'apihost': apihost,
        'project': prj,
        'package': pac,
        'repo': repo,
        'arch': arch,
        "user": user,
        "dash_user": dash_user,
    }
    return buildroot


def build_as_user(vm_type=None):
    if not conf.config.su_wrapper:
        return True
    if calculate_build_root_user(vm_type):
        return True
    return False


def su_wrapper(cmd):
    sucmd = conf.config['su-wrapper'].split()
    if sucmd:
        if sucmd[0] == 'su':
            if sucmd[-1] == '-c':
                sucmd.pop()
            cmd = sucmd + ['-s', cmd[0], 'root', '--'] + cmd[1:]
        else:
            cmd = sucmd + cmd
    return cmd


def run_build(opts, *args):
    cmd = [conf.config['build-cmd']]
    cmd += args

    if opts.vm_type:
        cmd.extend(["--vm-type", opts.vm_type])

    user = calculate_build_root_user(opts.vm_type)
    if not user:
        cmd = su_wrapper(cmd)

    if not opts.userootforbuild:
        cmd.append('--norootforbuild')
    return run_external(cmd[0], *cmd[1:])


def create_build_descr_data(
    build_descr_path: Optional[str],
    *,
    build_type: Optional[str],
    repo: Optional[str] = None,
    arch: Optional[str] = None,
    prefer_pkgs: Optional[List[str]] = None,
    define: Optional[List[str]] = None,
    define_with: Optional[List[str]] = None,
    define_without: Optional[List[str]] = None,
):
    if build_descr_path:
        build_descr_path = os.path.abspath(build_descr_path)
        topdir = os.path.dirname(build_descr_path)
    else:
        topdir = None
    result_data = []

    if build_descr_path:
        print(f"Using local file: {os.path.basename(build_descr_path)}", file=sys.stderr)
        with open(build_descr_path, "rb") as f:
            build_descr_data = f.read()

        # HACK: there's no api to provide custom defines
        # TODO: check if we're working with a spec?
        defines: List[bytes] = []
        for i in define or []:
            defines.append(f"%define {i}".encode("utf-8"))
        for i in define_with or []:
            defines.append(f"%define _with_{i} 1".encode("utf-8"))
        for i in define_without or []:
            defines.append(f"%define _without_{i} 1".encode("utf-8"))
        if defines:
            build_descr_data = b"\n".join(defines) + b"\n\n" + build_descr_data

        # build recipe must go first for compatibility with the older OBS versions
        result_data.append((os.path.basename(build_descr_path).encode("utf-8"), build_descr_data))

    if topdir:
        for include_file in glob.glob(os.path.join(topdir, "*.inc")):
            fn = os.path.basename(include_file)
            print(f"Using local file: {fn}", file=sys.stderr)
            with open(include_file, "rb") as f:
                result_data.append((fn.encode("utf-8"), f.read()))

    if topdir:
        buildenv_file = os.path.join(topdir, f"_buildenv.{repo}.{arch}")
        if not os.path.isfile(buildenv_file):
            buildenv_file = os.path.join(topdir, "_buildenv")
        if os.path.isfile(buildenv_file):
            print(f"Using local file: {os.path.basename(buildenv_file)}", file=sys.stderr)
            with open(buildenv_file, "rb") as f:
                result_data.append((b"buildenv", f.read()))

    if topdir:
        service_file = os.path.join(topdir, "_service")
        if os.path.isfile(service_file):
            print("Using local file: _service", file=sys.stderr)
            with open(service_file, "rb") as f:
                result_data.append((b"_service", f.read()))

    if not result_data and not prefer_pkgs:
        return None, {}

    cpio_data = cpio.CpioWrite()
    for key, value in result_data:
        cpio_data.add(key, value)

    if prefer_pkgs:
        print(f"Scanning the following dirs for local preferred packages: {', '.join(prefer_pkgs)}", file=sys.stderr)
        prefer_pkgs_result = get_prefer_pkgs(prefer_pkgs, arch, build_type, cpio_data)
    else:
        prefer_pkgs_result = {}

    return cpio_data.get(), prefer_pkgs_result


def main(apiurl, store, opts, argv):

    repo = argv[0]
    arch = argv[1]
    build_descr = argv[2]
    xp = []
    build_root = None
    cache_dir = None
    build_uid = ''
    config = conf.config
    build_shell_after_fail = config['build-shell-after-fail']
    vm_memory = config['build-memory']
    vm_disk_size = config['build-vmdisk-rootsize']
    vm_type = config['build-type']
    vm_telnet = None

    build_descr = os.path.abspath(build_descr)

    if not os.path.isfile(build_descr):
        raise oscerr.WrongArgs('Error: build description file named \'%s\' does not exist.' % build_descr)

    build_type_obj = get_build_type_from_recipe_path(build_descr)
    build_type = build_type_obj.name

    buildargs = []
    buildargs.append('--statistics')
    if not opts.userootforbuild:
        buildargs.append('--norootforbuild')
    if opts.clean:
        buildargs.append('--clean')
    if opts.checks:
        buildargs.append('--checks')
    if opts.nochecks:
        buildargs.append('--no-checks')
    if not opts.no_changelog:
        buildargs.append('--changelog')
    if opts.root:
        build_root = opts.root
    if opts.target:
        buildargs.append('--target=%s' % opts.target)
    if opts.threads:
        buildargs.append('--threads=%s' % opts.threads)
    if opts.jobs:
        buildargs.append('--jobs=%s' % opts.jobs)
    elif config['build-jobs'] > 0:
        buildargs.append('--jobs=%s' % config['build-jobs'])
    if opts.icecream or config['icecream'] != '0':
        if opts.icecream:
            num = opts.icecream
        else:
            num = config['icecream']

        if int(num) > 0:
            buildargs.append('--icecream=%s' % num)
            xp.append('icecream')
            xp.append('gcc-c++')
    if opts.ccache or config['ccache']:
        buildargs.append('--ccache')
        xp.append('ccache')
    if opts.pkg_ccache:
        buildargs.append('--pkg-ccache=%s' % opts.pkg_ccache)
        xp.append('ccache')
    if opts.linksources:
        buildargs.append('--linksources')
    if opts.baselibs:
        buildargs.append('--baselibs')
    if opts.debuginfo:
        buildargs.append('--debug')
    if opts._with:
        for o in opts._with:
            buildargs.append('--with=%s' % o)
    if opts.without:
        for o in opts.without:
            buildargs.append('--without=%s' % o)
    if opts.define:
        for o in opts.define:
            buildargs.append('--define=%s' % o)
    if config['build-uid']:
        build_uid = config['build-uid']
    if opts.build_uid:
        build_uid = opts.build_uid
    if build_uid:
        buildidre = re.compile('^[0-9]+:[0-9]+$')
        if build_uid == 'caller':
            buildargs.append('--uid=%s:%s' % (os.getuid(), os.getgid()))
        elif buildidre.match(build_uid):
            buildargs.append('--uid=%s' % build_uid)
        else:
            print('Error: build-uid arg must be 2 colon separated numerics: "uid:gid" or "caller"', file=sys.stderr)
            return 1
    if opts.shell_after_fail:
        build_shell_after_fail = opts.shell_after_fail
    if opts.vm_memory:
        vm_memory = opts.vm_memory
    if opts.vm_disk_size:
        vm_disk_size = opts.vm_disk_size
    if opts.vm_type:
        vm_type = opts.vm_type
    if opts.vm_telnet:
        vm_telnet = opts.vm_telnet
    if opts.alternative_project:
        prj = opts.alternative_project
        pac = '_repository'
    else:
        prj = store.project
        if opts.local_package:
            pac = '_repository'
        else:
            pac = store.package
    if opts.multibuild_package:
        buildargs.append('--buildflavor=%s' % opts.multibuild_package)
        pac = pac + ":" + opts.multibuild_package
    if opts.verbose_mode:
        buildargs.append('--verbose=%s' % opts.verbose_mode)
    if opts.no_timestamps:
        buildargs.append('--no-timestamps')
    if opts.wipe:
        buildargs.append("--wipe")

    pacname = pac
    if pacname == '_repository':
        if not opts.local_package:
            try:
                pacname = store.package
            except oscerr.NoWorkingCopy:
                opts.local_package = True
        if opts.local_package:
            pacname = os.path.splitext(os.path.basename(build_descr))[0]
    apihost = urlsplit(apiurl)[1]
    if not build_root:
        user = calculate_build_root_user(vm_type)
        build_root = calculate_build_root(apihost, prj, pacname, repo, arch, user)

    # We configure sccache after pacname, so that in default cases we can have an sccache for each
    # package to prevent cross-cache polutions. It helps to make the local-use case a bit nicer.
    if opts.sccache_uri or config['sccache_uri'] or opts.sccache or config['sccache']:
        if opts.pkg_ccache or opts.ccache or config['ccache']:
            raise oscerr.WrongArgs('Error: sccache and ccache can not be enabled at the same time')
        sccache_arg = "--sccache-uri=/var/tmp/osbuild-sccache-{pkgname}.tar"
        if opts.sccache_uri:
            sccache_arg = '--sccache-uri=%s' % opts.sccache_uri
        elif config['sccache_uri']:
            sccache_arg = '--sccache-uri=%s' % config['sccache_uri']
        # Format the package name.
        sccache_arg = sccache_arg.format(pkgname=pacname)
        buildargs.append(sccache_arg)
        xp.append('sccache')

    # define buildinfo & config local cache
    bi_file = None
    bc_file = None
    bi_filename = '_buildinfo-%s-%s.xml' % (repo, arch)
    bc_filename = '_buildconfig-%s-%s' % (repo, arch)
    if store is not None and hasattr(store, "cache_get_path"):
        bi_filename = store.cache_get_path(bi_filename, makedirs=True)
        bc_filename = store.cache_get_path(bc_filename, makedirs=True)
    elif store is not None and store.is_package and os.access(core.store, os.W_OK):
        bi_filename = os.path.join(os.getcwd(), core.store, bi_filename)
        bc_filename = os.path.join(os.getcwd(), core.store, bc_filename)
    elif not os.access('.', os.W_OK):
        bi_file = NamedTemporaryFile(prefix=bi_filename)
        bi_filename = bi_file.name
        bc_file = NamedTemporaryFile(prefix=bc_filename)
        bc_filename = bc_file.name
    else:
        bi_filename = os.path.abspath(bi_filename)
        bc_filename = os.path.abspath(bc_filename)

    if opts.shell:
        buildargs.append("--shell")

    if build_shell_after_fail:
        buildargs.append("--shell-after-fail")

    if opts.shell_cmd:
        buildargs.append("--shell-cmd")
        buildargs.append(opts.shell_cmd)

    if opts.noinit:
        buildargs.append('--noinit')

    if opts.local_package or not store.is_package:
        opts.skip_local_service_run = True

    # check for source services
    if not opts.offline and not opts.skip_local_service_run:
        p = core.Package(os.curdir)
        r = p.run_source_services(verbose=True)
        if r:
            raise oscerr.ServiceRuntimeError('Source service run failed!')

    cache_dir = config['packagecachedir'] % {'apihost': apihost}

    extra_pkgs = []
    if not opts.extra_pkgs:
        extra_pkgs = config.get('extra-pkgs', [])
    elif opts.extra_pkgs != ['']:
        extra_pkgs = opts.extra_pkgs

    if opts.extra_pkgs_from:
        for filename in opts.extra_pkgs_from:
            with open(filename, encoding="utf-8") as f:
                for line in f:
                    extra_pkgs.append(line.rstrip('\n'))

    if xp:
        extra_pkgs += xp

    build_descr_data, prefer_pkgs = create_build_descr_data(
        build_descr,
        build_type=build_type,
        repo=repo,
        arch=arch,
        prefer_pkgs=opts.prefer_pkgs,
        define=opts.define,
        define_with=opts._with,
        define_without=opts.without,
    )

    # special handling for overlay and rsync-src/dest
    specialcmdopts = []
    if opts.rsyncsrc or opts.rsyncdest:
        if not opts.rsyncsrc or not opts.rsyncdest:
            raise oscerr.WrongOptions('When using --rsync-{src,dest} both parameters have to be specified.')
        myrsyncsrc = os.path.abspath(os.path.expanduser(os.path.expandvars(opts.rsyncsrc)))
        if not os.path.isdir(myrsyncsrc):
            raise oscerr.WrongOptions('--rsync-src %s is no valid directory!' % opts.rsyncsrc)
        # can't check destination - its in the target chroot ;) - but we can check for sanity
        myrsyncdest = os.path.expandvars(opts.rsyncdest)
        if not os.path.isabs(myrsyncdest):
            raise oscerr.WrongOptions('--rsync-dest %s is no absolute path (starting with \'/\')!' % opts.rsyncdest)
        specialcmdopts = ['--rsync-src=' + myrsyncsrc, '--rsync-dest=' + myrsyncdest]
    if opts.overlay:
        myoverlay = os.path.abspath(os.path.expanduser(os.path.expandvars(opts.overlay)))
        if not os.path.isdir(myoverlay):
            raise oscerr.WrongOptions('--overlay %s is no valid directory!' % opts.overlay)
        specialcmdopts += ['--overlay=' + myoverlay]

    try:
        if opts.noinit:
            if not os.path.isfile(bi_filename):
                raise oscerr.WrongOptions('--noinit is not possible, no local buildinfo file')
            print('Use local \'%s\' file as buildinfo' % bi_filename)
            if not os.path.isfile(bc_filename):
                raise oscerr.WrongOptions('--noinit is not possible, no local buildconfig file')
            print('Use local \'%s\' file as buildconfig' % bc_filename)
        elif opts.offline:
            if not os.path.isfile(bi_filename):
                raise oscerr.WrongOptions('--offline is not possible, no local buildinfo file')
            print('Use local \'%s\' file as buildinfo' % bi_filename)
            if not os.path.isfile(bc_filename):
                raise oscerr.WrongOptions('--offline is not possible, no local buildconfig file')
        else:
            print('Getting buildconfig from server and store to %s' % bc_filename)
            bc = get_buildconfig(apiurl, prj, repo)
            if not bc_file:
                bc_file = open(bc_filename, 'w')
            bc_file.write(decode_it(bc))
            bc_file.flush()
            if os.path.exists(config.queryconfig_cmd) and not opts.nodebugpackages:
                debug_pkgs = decode_it(return_external(config.queryconfig_cmd, '--dist', bc_filename, 'substitute', 'obs:cli_debug_packages'))
                if len(debug_pkgs) > 0:
                    extra_pkgs.extend(debug_pkgs.strip().split(" "))

            print('Getting buildinfo from server and store to %s' % bi_filename)
            bi_text = decode_it(get_buildinfo(apiurl,
                                              prj,
                                              pac,
                                              repo,
                                              arch,
                                              specfile=build_descr_data,
                                              addlist=extra_pkgs))
            if not bi_file:
                bi_file = open(bi_filename, 'w')
            # maybe we should check for errors before saving the file
            bi_file.write(bi_text)
            bi_file.flush()
            kiwipath = None
            if build_type == 'kiwi':
                bi = Buildinfo(bi_filename, apiurl, 'kiwi', list(prefer_pkgs.keys()))
                kiwipath = get_kiwipath_from_buildinfo(bi, prj, repo)
                bc = get_buildconfig(apiurl, prj, repo, kiwipath)
                bc_file.seek(0)
                bc_file.write(decode_it(bc))
                bc_file.flush()
    except HTTPError as e:
        if e.code == 404:
            # check what caused the 404
            if meta_exists(metatype='prj', path_args=(prj, ),
                           template_args=None, create_new=False, apiurl=apiurl):
                pkg_meta_e = None
                try:
                    # take care, not to run into double trouble.
                    pkg_meta_e = meta_exists(metatype='pkg', path_args=(prj, pac),
                                             template_args=None, create_new=False,
                                             apiurl=apiurl)
                except:
                    pass

                if pkg_meta_e:
                    print('ERROR: Either wrong repo/arch as parameter or a parse error of .spec/.dsc/.kiwi file due to syntax error', file=sys.stderr)
                else:
                    print('The package \'%s\' does not exist - please '
                          'rerun with \'--local-package\'' % pac, file=sys.stderr)
            else:
                print('The project \'%s\' does not exist - please '
                      'rerun with \'--alternative-project <alternative_project>\'' % prj, file=sys.stderr)
            sys.exit(1)
        else:
            raise

    # Set default binary type if cannot be detected
    binary_type = 'rpm'
    if os.path.exists(config.queryconfig_cmd):
        binary_type = decode_it(return_external(config.queryconfig_cmd, '--dist', bc_filename, 'binarytype')).strip()
    # If binary type is set to a useless value, reset to 'rpm'
    if binary_type == 'UNDEFINED':
        binary_type = 'rpm'

    bi = Buildinfo(bi_filename, apiurl, build_type, list(prefer_pkgs.keys()), binary_type)

    if bi.debuginfo and not (opts.disable_debuginfo or '--debug' in buildargs):
        buildargs.append('--debug')

    if opts.release:
        bi.release = opts.release

    if bi.release:
        buildargs.append('--release')
        buildargs.append(bi.release)

    if opts.stage:
        buildargs.append('--stage')
        buildargs.append(opts.stage)

    if opts.build_opt:
        buildargs += opts.build_opt

    if opts.buildtool_opt:
        buildargs += [f"--buildtool-opt={opt}" for opt in opts.buildtool_opt]

    # real arch of this machine
    # vs.
    # arch we are supposed to build for
    if vm_type != "emulator" and vm_type != "qemu":
        if bi.hostarch is not None:
            if hostarch != bi.hostarch and bi.hostarch not in can_also_build.get(hostarch, []):
                print('Error: hostarch \'%s\' is required.' % (bi.hostarch), file=sys.stderr)
                return 1
        elif hostarch != bi.buildarch:
            if bi.buildarch not in can_also_build.get(hostarch, []):
                print('WARNING: It is guessed to build on hostarch \'%s\' for \'%s\' via QEMU user emulation.' % (hostarch, bi.buildarch), file=sys.stderr)

    rpmlist_prefers = []
    if prefer_pkgs:
        print('Evaluating preferred packages')
        for name, path in prefer_pkgs.items():
            if bi.has_dep(name):
                # We remove a preferred package from the buildinfo, so that the
                # fetcher doesn't take care about them.
                # Instead, we put it in a list which is appended to the rpmlist later.
                # At the same time, this will make sure that these packages are
                # not verified.
                bi.remove_dep(name)
                rpmlist_prefers.append((name, path))
                print(' - %s (%s)' % (name, path))

    print('Updating cache of required packages')

    urllist = []
    if not opts.download_api_only:
        # transform 'url1, url2, url3' form into a list
        if 'urllist' in config:
            if isinstance(config['urllist'], str):
                re_clist = re.compile('[, ]+')
                urllist = [i.strip() for i in re_clist.split(config['urllist'].strip())]
            else:
                urllist = config['urllist']

        # OBS 1.5 and before has no downloadurl defined in buildinfo, but it is obsolete again meanwhile.
        # we have now specific download repositories per repository. Could be removed IMHO, since the api fallback
        # is there. In worst case it could fetch the wrong rpm...
        if bi.downloadurl:
            urllist.append(bi.downloadurl.replace('%', '%%') + '/%(extproject)s/%(extrepository)s/%(arch)s/%(filename)s')
    if opts.disable_cpio_bulk_download:
        urllist.append('%(apiurl)s/build/%(project)s/%(repository)s/%(repoarch)s/%(repopackage)s/%(repofilename)s')

    fetcher = Fetcher(cache_dir,
                      urllist=urllist,
                      offline=opts.noinit or opts.offline,
                      http_debug=config['http_debug'],
                      modules=bi.modules,
                      enable_cpio=not opts.disable_cpio_bulk_download and bi.enable_cpio,
                      cookiejar=connection.CookieJarAuthHandler(apiurl, os.path.expanduser(config["cookiejar"]))._cookiejar,
                      download_api_only=opts.download_api_only)

    if not opts.trust_all_projects:
        # implicitly trust the project we are building for
        check_trusted_projects(apiurl, [i for i in bi.projects.keys() if not i == prj])

    imagefile = ""
    imagesource = ""
    imageinfo = ""
    imagebins = []
    # Check if using container preinstallimage for podman
    using_container_preinstall = (
        vm_type == "podman"
        and opts.build_opt
        and any("--vm-use-container-preinstallimage" in opt for opt in opts.build_opt)
    )

    if build_as_user(vm_type) and not using_container_preinstall:
        # preinstallimage extraction will fail because unprivileged user cannot chroot or extract devices from the tarball
        bi.preinstallimage = None
    if build_type == 'preinstallimage':
        # preinstallimage would repackage just the previously built preinstallimage
        bi.preinstallimage = None

    if (
        not config["no_preinstallimage"]
        and not opts.nopreinstallimage
        and bi.preinstallimage
        and not opts.noinit
        and (
            opts.clean
            or (
                not os.path.exists(build_root + "/installed-pkg")
                and not os.path.exists(build_root + "/.build/init_buildsystem.data")
            )
        )
    ):
        (imagefile, imagesource, imageinfo, imagebins) = get_preinstall_image(
            apiurl, arch, cache_dir, bi.preinstallimage, opts.offline
        )
        if imagefile:
            # remove binaries from build deps which are included in preinstall image
            for i in bi.deps:
                if i.name in imagebins:
                    bi.remove_dep(i.name)

    # now update the package cache
    fetcher.run(bi)

    old_pkg_dir = None
    if opts.oldpackages:
        old_pkg_dir = opts.oldpackages
        if not old_pkg_dir.startswith('/') and not opts.offline:
            data = [prj, pacname, repo, arch]
            if old_pkg_dir == '_link':
                p = core.Package(os.curdir)
                if not p.islink():
                    raise oscerr.WrongOptions('package is not a link')
                data[0] = p.linkinfo.project
                data[1] = p.linkinfo.package
                repos = core.get_repositories_of_project(apiurl, data[0])
                # hack for links to e.g. Factory
                if not data[2] in repos and 'standard' in repos:
                    data[2] = 'standard'
            elif old_pkg_dir != '' and old_pkg_dir != '_self':
                a = old_pkg_dir.split('/')
                for i in range(0, len(a)):
                    data[i] = a[i]

            destdir = os.path.join(cache_dir, data[0], data[2], data[3])
            old_pkg_dir = None
            try:
                print("Downloading previous build from %s ..." % '/'.join(data))
                binaries = get_binarylist(apiurl, data[0], data[2], data[3], package=data[1], verbose=True)
            except Exception as e:
                print("Error: failed to get binaries: %s" % str(e))
                binaries = []

            if binaries:
                class mytmpdir:
                    """ temporary directory that removes itself"""

                    def __init__(self, *args, **kwargs):
                        self.name = mkdtemp(*args, **kwargs)
                    _rmtree = staticmethod(shutil.rmtree)

                    def cleanup(self):
                        self._rmtree(self.name)

                    def __del__(self):
                        self.cleanup()

                    def __exit__(self, exc_type, exc_value, traceback):
                        self.cleanup()

                    def __str__(self):
                        return self.name or ""

                old_pkg_dir = mytmpdir(prefix='.build.oldpackages', dir=os.path.abspath(os.curdir))
                if not os.path.exists(destdir):
                    os.makedirs(destdir)
            for i in binaries:
                fname = os.path.join(destdir, i.name)
                os.symlink(fname, os.path.join(str(old_pkg_dir), i.name))
                if os.path.exists(fname):
                    st = os.stat(fname)
                    if st.st_mtime == i.mtime and st.st_size == i.size:
                        continue
                get_binary_file(apiurl,
                                data[0],
                                data[2], data[3],
                                i.name,
                                package=data[1],
                                target_filename=fname,
                                target_mtime=i.mtime,
                                progress_meter=True)

        if old_pkg_dir is not None:
            buildargs.append('--oldpackages=%s' % old_pkg_dir)

    # Make packages from buildinfo available as repos for kiwi/docker/fissile
    # FIXME: add a new attribute to BuildType and decide based on it
    if build_type in ('kiwi', 'docker', 'podman', 'fissile', 'productcompose'):
        if os.path.lexists('repos'):
            shutil.rmtree('repos')
        if os.path.lexists('containers'):
            shutil.rmtree('containers')
        os.mkdir('repos')
        for i in bi.deps:
            if not i.extproject:
                # remove
                bi.deps.remove(i)
                continue
            if i.notmeta:
                continue
            # project
            pdir = str(i.extproject).replace(':/', ':')
            # repo
            rdir = str(i.extrepository).replace(':/', ':')
            # arch
            adir = i.repoarch
            if i.binary == 'updateinfo.xml':
                adir = 'updateinfo'
            elif i.binary == '_modulemd.yaml':
                adir = 'modulemd'
            # source fullfilename
            sffn = i.fullfilename
            filename = sffn.split("/")[-1]
            # project/repo
            if i.name.startswith("container:"):
                prdir = "containers/" + pdir + "/" + rdir
                pradir = prdir
                filename = filename[10:]
                if build_type == 'kiwi':
                    buildargs.append('--kiwi-parameter')
                    buildargs.append('--set-container-derived-from=dir://./' + prdir + "/" + filename)
            else:
                prdir = "repos/" + pdir + "/" + rdir
                # project/repo/arch
                pradir = prdir + "/" + adir
            # target fullfilename
            tffn = pradir + "/" + filename
            if not os.path.exists(os.path.join(pradir)):
                os.makedirs(os.path.join(pradir))
            if not os.path.exists(tffn):
                print("Using package: " + sffn)
                if opts.linksources:
                    os.link(sffn, tffn)
                else:
                    os.symlink(sffn, tffn)
            if prefer_pkgs:
                for name, path in prefer_pkgs.items():
                    if name == filename:
                        print("Using prefered package: " + path + "/" + filename)
                        os.unlink(tffn)

        if bi.containerannotation:
            if not os.path.exists("containers"):
                os.makedirs("containers")
            with open("containers/annotation", "wb") as f:
                f.write(bi.containerannotation.encode())

        if prefer_pkgs:
            localpkgdir = "repos/_local/"
            os.mkdir(localpkgdir)
            buildargs.append("--kiwi-parameter")
            buildargs.append("--add-repo")
            buildargs.append("--kiwi-parameter")
            buildargs.append(f"dir://./{localpkgdir}")
            buildargs.append("--kiwi-parameter")
            buildargs.append("--add-repotype")
            buildargs.append("--kiwi-parameter")
            buildargs.append("rpm-md")
            for name, path in prefer_pkgs.items():
                tffn = os.path.join(localpkgdir, os.path.basename(path))
                if opts.linksources:
                    os.link(path, tffn)
                else:
                    os.symlink(path, tffn)

    else:
        buildargs.append("--repos-directory=-")
        buildargs.append("--containers-directory=-")

    if build_type == 'kiwi':
        # Is a obsrepositories tag used?
        try:
            tree = xml_parse(build_descr)
        except:
            print('could not parse the kiwi file:', file=sys.stderr)
            print(open(build_descr).read(), file=sys.stderr)
            sys.exit(1)
        root = tree.getroot()

        # product
        if root.find('instsource'):
            # leads to unsigned media, but avoids build failure
            buildargs.append('--signdummy')

        for xml in root.findall('instsource'):
            found_obsrepositories = 0
            for node in xml.findall('instrepo'):
                if node and node.find('source').get('path') == 'obsrepositories:/':
                    for path in bi.pathes:
                        found_obsrepositories += 1
                        new_node = ET.SubElement(xml, 'instrepo')
                        new_node.set('name', node.get('name') + "_" + str(found_obsrepositories))
                        new_node.set('priority', node.get('priority'))
                        new_node.set('local', 'true')
                        new_source_node = ET.SubElement(new_node, 'source')
                        new_source_node.set('path', "obs://" + path)
                    xml.remove(node)

            if found_obsrepositories > 0:
                build_descr = os.getcwd() + '/_service:osc_obsrepositories:' + build_descr.rsplit('/', 1)[-1]
                tree.write(open(build_descr, 'wb'))

        # appliance
        expand_obsrepos = None
        for xml in root.findall('repository'):
            if xml.find('source').get('path') == 'obsrepositories:/':
                expand_obsrepos = True
        if expand_obsrepos:
            buildargs.append('--kiwi-parameter')
            buildargs.append('--ignore-repos')
            for xml in root.findall('repository'):
                if xml.find('source').get('path') == 'obsrepositories:/':
                    for path in bi.pathes:
                        if not os.path.isdir("repos/" + path):
                            continue
                        buildargs.append('--kiwi-parameter')
                        buildargs.append('--add-repo')
                        buildargs.append('--kiwi-parameter')
                        buildargs.append("dir://./repos/" + path)
                        buildargs.append('--kiwi-parameter')
                        buildargs.append('--add-repotype')
                        buildargs.append('--kiwi-parameter')
                        buildargs.append('rpm-md')
                        if xml.get('priority'):
                            buildargs.append('--kiwi-parameter')
                            buildargs.append('--add-repoprio=' + xml.get('priority'))
                else:
                    m = re.match(r"obs://[^/]+/([^/]+)/(\S+)", xml.find('source').get('path'))
                    if not m:
                        # short path without obs instance name
                        m = re.match(r"obs://([^/]+)/(.+)", xml.find('source').get('path'))
                    project = m.group(1).replace(":", ":/")
                    repo = m.group(2)
                    buildargs.append('--kiwi-parameter')
                    buildargs.append('--add-repo')
                    buildargs.append('--kiwi-parameter')
                    buildargs.append("dir://./repos/" + project + "/" + repo)
                    buildargs.append('--kiwi-parameter')
                    buildargs.append('--add-repotype')
                    buildargs.append('--kiwi-parameter')
                    buildargs.append('rpm-md')
                    if xml.get('priority'):
                        buildargs.append('--kiwi-parameter')
                        buildargs.append('--add-repopriority=' + xml.get('priority'))

    if vm_type in ('xen', 'kvm', 'lxc', 'nspawn'):
        print('Skipping verification of package signatures due to secure VM build')
    elif bi.pacsuffix == 'rpm':
        if opts.no_verify:
            print('Skipping verification of package signatures')
        else:
            print('Verifying integrity of cached packages')
            verify_pacs(bi)
    elif bi.pacsuffix == 'deb':
        if opts.no_verify or opts.noinit:
            print('Skipping verification of package signatures')
        else:
            print('WARNING: deb packages get not verified, they can compromise your system !')
    else:
        print('WARNING: unknown packages get not verified, they can compromise your system !')

    for i in bi.deps:
        if i.hdrmd5:
            if i.name.startswith('container:') or i.binary == 'updateinfo.xml' or i.binary == '_modulemd.yaml':
                hdrmd5 = dgst(i.fullfilename)
            elif i.fullfilename.endswith(".rpm"):
                hdrmd5 = packagequery.PackageQuery.queryhdrmd5(i.fullfilename)
            else:
                continue
            if not hdrmd5:
                print("Error: cannot get hdrmd5 for %s" % i.fullfilename)
                sys.exit(1)
            if hdrmd5 != i.hdrmd5:
                if conf.config["api_host_options"][apiurl]["disable_hdrmd5_check"]:
                    print(f"Warning: Ignoring a hdrmd5 mismatch for {i.fullfilename}: {hdrmd5} (actual) != {i.hdrmd5} (expected)")
                else:
                    print(f"Error: hdrmd5 mismatch for {i.fullfilename}: {hdrmd5} (actual) != {i.hdrmd5} (expected)")
                    sys.exit(1)

    print('Writing build configuration')

    if build_type in ('kiwi', 'docker', 'podman', 'fissile', 'productcompose'):
        rpmlist = ['%s %s\n' % (i.name, i.fullfilename) for i in bi.deps if not i.noinstall]
    else:
        rpmlist = []
        for dep in bi.deps:
            if dep.sysroot:
                # packages installed in sysroot subdirectory need to get a prefix for init_buildsystem
                rpmlist.append("sysroot: %s %s\n" % (dep.name, dep.fullfilename))
            else:
                rpmlist.append("%s %s\n" % (dep.name, dep.fullfilename))
    for i in imagebins:
        rpmlist.append("%s preinstallimage\n" % i)
    rpmlist += ["%s %s\n" % (i[0], i[1]) for i in rpmlist_prefers]

    if imagefile:
        rpmlist.append(f"preinstallimage: {imagefile}\n")
    if imagesource:
        rpmlist.append(f"preinstallimagesource: {imagesource}\n")
    if imageinfo:
        rpmlist.append(f"preinstallimageinfo: {imageinfo}\n")

    rpmlist.append('preinstall: ' + ' '.join(bi.preinstall_list) + '\n')
    rpmlist.append('vminstall: ' + ' '.join(bi.vminstall_list) + '\n')
    rpmlist.append('runscripts: ' + ' '.join(bi.runscripts_list) + '\n')
    if build_type != 'kiwi' and build_type != 'docker' and build_type != 'podman' and build_type != 'fissile':
        if bi.noinstall_list:
            rpmlist.append('noinstall: ' + ' '.join(bi.noinstall_list) + '\n')
        if bi.installonly_list:
            rpmlist.append('installonly: ' + ' '.join(bi.installonly_list) + '\n')

    rpmlist_file = NamedTemporaryFile(mode='w+t', prefix='rpmlist.')
    rpmlist_filename = rpmlist_file.name
    rpmlist_file.writelines(rpmlist)
    rpmlist_file.flush()

    subst = {'repo': repo, 'arch': arch, 'project': prj, 'package': pacname}
    vm_options = []
    # XXX check if build-device present
    my_build_device = ''
    if config['build-device']:
        my_build_device = config['build-device'] % subst
    else:
        # obs worker uses /root here but that collides with the
        # /root directory if the build root was used without vm
        # before
        my_build_device = build_root + '/img'

    if vm_type:
        if config['build-swap']:
            my_build_swap = config['build-swap'] % subst
        else:
            my_build_swap = build_root + '/swap'

        vm_options = [f"--vm-type={vm_type}"]
        if vm_telnet:
            vm_options += [f"--vm-telnet={vm_telnet}"]
        if vm_memory:
            vm_options += [f"--memory={vm_memory}"]
        if vm_type != 'lxc' and vm_type != 'nspawn':
            vm_options += [f"--vm-disk={my_build_device}"]
            vm_options += [f"--vm-swap={my_build_swap}"]
            vm_options += [f"--logfile={build_root}/.build.log"]
            if vm_type == 'kvm':
                if config['build-kernel']:
                    vm_options += [f"--vm-kernel={config['build-kernel']}"]
                if config['build-initrd']:
                    vm_options += [f"--vm-initrd={config['build-initrd']}"]

            build_root += '/.mount'
        if vm_disk_size:
            vm_options += [f"--vmdisk-rootsize={vm_disk_size}"]

        if config['build-vmdisk-swapsize']:
            vm_options += [f"--vmdisk-swapsize={config['build-vmdisk-swapsize']}"]
        if config['build-vmdisk-filesystem']:
            vm_options += [f"--vmdisk-filesystem={config['build-vmdisk-filesystem']}"]
        if config['build-vm-user']:
            vm_options += [f"--vm-user={config['build-vm-user']}"]

    if opts.preload:
        print("Preload done for selected repo/arch.")
        sys.exit(0)

    print('Running build')
    cmd = [
        config['build-cmd'],
        f"--root={build_root}",
        f"--rpmlist={rpmlist_filename}",
        f"--dist={bc_filename}",
        f"--arch={bi.buildarch}",
    ]
    cmd += specialcmdopts + vm_options + buildargs
    cmd += [build_descr]

    # determine if we're building under root (user == None) and use su_wrapper accordingly
    if calculate_build_root_user(vm_type) is None:
        cmd = su_wrapper(cmd)

    # change personality, if needed
    if hostarch != bi.buildarch and bi.buildarch in change_personality:
        cmd = [change_personality[bi.buildarch]] + cmd

    # record our settings for later builds
    if store and store.is_package:
        store.last_buildroot = repo, arch, vm_type

    try:
        rc = run_external(cmd[0], *cmd[1:])
        if rc:
            print()
            print(f"Build failed with exit code {rc}")
            print(f"The buildroot was: {build_root}")
            print()
            print("Cleaning the build root may fix the problem or allow you to start debugging from a well-defined state:")
            print("  - add '--clean' option to your 'osc build' command")
            print("  - run 'osc wipe [--vm-type=...]' prior running your 'osc build' command again")
            sys.exit(rc)
    except KeyboardInterrupt as keyboard_interrupt_exception:
        print("keyboard interrupt, killing build ...")
        cmd.append('--kill')
        run_external(cmd[0], *cmd[1:])
        raise keyboard_interrupt_exception

    pacdir = os.path.join(build_root, '.build.packages')
    if os.path.islink(pacdir):
        pacdir = os.readlink(pacdir)
        pacdir = os.path.join(build_root, pacdir)

    if os.path.exists(pacdir):
        (s_built, b_built) = get_built_files(pacdir, bi.buildtype, binary_type=binary_type)

        print()
        if s_built:
            print(decode_it(s_built))
        print()
        print(decode_it(b_built))

        if opts.keep_pkgs:
            for i in b_built.splitlines() + s_built.splitlines():
                shutil.copy2(i, os.path.join(opts.keep_pkgs, os.path.basename(decode_it(i))))

    if bi_file:
        bi_file.close()
    if bc_file:
        bc_file.close()
    rpmlist_file.close()

# vim: sw=4 et
