# Copyright 1999-2020 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2

from _emerge.SubProcess import SubProcess
import sys
from portage.cache.mappings import slot_dict_class
import portage

portage.proxy.lazyimport.lazyimport(
    globals(),
    "portage.package.ebuild._metadata_invalid:eapi_invalid",
)
from portage import os
from portage import _encodings
from portage import _unicode_decode
from portage import _unicode_encode

import fcntl


class EbuildMetadataPhase(SubProcess):

    """
    Asynchronous interface for the ebuild "depend" phase which is
    used to extract metadata from the ebuild.
    """

    __slots__ = (
        "cpv",
        "eapi_supported",
        "ebuild_hash",
        "fd_pipes",
        "metadata",
        "portdb",
        "repo_path",
        "settings",
        "write_auxdb",
    ) + (
        "_eapi",
        "_eapi_lineno",
        "_raw_metadata",
    )

    _file_names = ("ebuild",)
    _files_dict = slot_dict_class(_file_names, prefix="")

    def _start(self):
        ebuild_path = self.ebuild_hash.location

        with open(
            _unicode_encode(ebuild_path, encoding=_encodings["fs"], errors="strict"),
            encoding=_encodings["repo.content"],
            errors="replace",
        ) as f:
            self._eapi, self._eapi_lineno = portage._parse_eapi_ebuild_head(f)

        parsed_eapi = self._eapi
        if parsed_eapi is None:
            parsed_eapi = "0"

        if not parsed_eapi:
            # An empty EAPI setting is invalid.
            self._eapi_invalid(None)
            self.returncode = 1
            self._async_wait()
            return

        self.eapi_supported = portage.eapi_is_supported(parsed_eapi)
        if not self.eapi_supported:
            self.metadata = {"EAPI": parsed_eapi}
            self.returncode = os.EX_OK
            self._async_wait()
            return

        settings = self.settings
        settings.setcpv(self.cpv)
        settings.configdict["pkg"]["EAPI"] = parsed_eapi

        debug = settings.get("PORTAGE_DEBUG") == "1"
        master_fd = None
        slave_fd = None
        fd_pipes = None
        if self.fd_pipes is not None:
            fd_pipes = self.fd_pipes.copy()
        else:
            fd_pipes = {}

        null_input = open("/dev/null", "rb")
        fd_pipes.setdefault(0, null_input.fileno())
        fd_pipes.setdefault(1, sys.__stdout__.fileno())
        fd_pipes.setdefault(2, sys.__stderr__.fileno())

        # flush any pending output
        stdout_filenos = (sys.__stdout__.fileno(), sys.__stderr__.fileno())
        for fd in fd_pipes.values():
            if fd in stdout_filenos:
                sys.__stdout__.flush()
                sys.__stderr__.flush()
                break

        self._files = self._files_dict()
        files = self._files

        master_fd, slave_fd = os.pipe()

        fcntl.fcntl(
            master_fd,
            fcntl.F_SETFL,
            fcntl.fcntl(master_fd, fcntl.F_GETFL) | os.O_NONBLOCK,
        )

        fd_pipes[slave_fd] = slave_fd
        settings["PORTAGE_PIPE_FD"] = str(slave_fd)

        self._raw_metadata = []
        files.ebuild = master_fd
        self.scheduler.add_reader(files.ebuild, self._output_handler)
        self._registered = True

        retval = portage.doebuild(
            ebuild_path,
            "depend",
            settings=settings,
            debug=debug,
            mydbapi=self.portdb,
            tree="porttree",
            fd_pipes=fd_pipes,
            returnpid=True,
        )
        settings.pop("PORTAGE_PIPE_FD", None)

        os.close(slave_fd)
        null_input.close()

        if isinstance(retval, int):
            # doebuild failed before spawning
            self.returncode = retval
            self._async_wait()
            return

        self.pid = retval[0]

    def _output_handler(self):
        while True:
            buf = self._read_buf(self._files.ebuild)
            if buf is None:
                break  # EAGAIN
            elif buf:
                self._raw_metadata.append(buf)
            else:  # EIO/POLLHUP
                if self.pid is None:
                    self._unregister()
                    self._async_wait()
                else:
                    self._async_waitpid()
                break

    def _unregister(self):
        if self._files is not None:
            self.scheduler.remove_reader(self._files.ebuild)
        SubProcess._unregister(self)

    def _async_waitpid_cb(self, *args, **kwargs):
        """
        Override _async_waitpid_cb to perform cleanup that is
        not necessarily idempotent.
        """
        SubProcess._async_waitpid_cb(self, *args, **kwargs)
        # self._raw_metadata is None when _start returns
        # early due to an unsupported EAPI
        if self.returncode == os.EX_OK and self._raw_metadata is not None:
            metadata_lines = _unicode_decode(
                b"".join(self._raw_metadata),
                encoding=_encodings["repo.content"],
                errors="replace",
            ).splitlines()
            metadata = {}
            metadata_valid = True
            for l in metadata_lines:
                if "=" not in l:
                    metadata_valid = False
                    break
                key, value = l.split("=", 1)
                metadata[key] = value

            if metadata_valid:
                parsed_eapi = self._eapi
                if parsed_eapi is None:
                    parsed_eapi = "0"
                self.eapi_supported = portage.eapi_is_supported(metadata["EAPI"])
                if (not metadata["EAPI"] or self.eapi_supported) and metadata[
                    "EAPI"
                ] != parsed_eapi:
                    self._eapi_invalid(metadata)
                    metadata_valid = False

            if metadata_valid:
                # Since we're supposed to be able to efficiently obtain the
                # EAPI from _parse_eapi_ebuild_head, we don't write cache
                # entries for unsupported EAPIs.
                if self.eapi_supported:
                    if metadata.get("INHERITED", False):
                        metadata[
                            "_eclasses_"
                        ] = self.portdb.repositories.get_repo_for_location(
                            self.repo_path
                        ).eclass_db.get_eclass_data(
                            metadata["INHERITED"].split()
                        )
                    else:
                        metadata["_eclasses_"] = {}
                    metadata.pop("INHERITED", None)

                    # If called by egencache, this cache write is
                    # undesirable when metadata-transfer is disabled.
                    if self.write_auxdb is not False:
                        self.portdb._write_cache(
                            self.cpv, self.repo_path, metadata, self.ebuild_hash
                        )
                else:
                    metadata = {"EAPI": metadata["EAPI"]}
                self.metadata = metadata
            else:
                self.returncode = 1

    def _eapi_invalid(self, metadata):
        repo_name = self.portdb.getRepositoryName(self.repo_path)
        if metadata is not None:
            eapi_var = metadata["EAPI"]
        else:
            eapi_var = None
        eapi_invalid(
            self,
            self.cpv,
            repo_name,
            self.settings,
            eapi_var,
            self._eapi,
            self._eapi_lineno,
        )
