#!/usr/bin/env python3
#
# This work is licensed under the GNU GPLv2 or later.
# See the COPYING file in the top-level directory.


import sys
if sys.version_info.major < 3:
    print("virt-manager is python3 only. Run this as ./setup.py")
    sys.exit(1)

import glob
import importlib.util
import os
from pathlib import Path
import shutil
import sysconfig
import subprocess

import setuptools
import setuptools.command.install
import setuptools.command.install_egg_info


# distutils will be deprecated in python 3.12 in favor of setuptools,
# but as of this writing there's standard no setuptools way to extend the
# 'build' commands which are the only standard commands we trigger.
# https://github.com/pypa/setuptools/issues/2591
#
# Newer setuptools will transparently support 'import distutils' though.
# That can be overridden with SETUPTOOLS_USE_DISTUTILS env variable
import distutils.command.build  # pylint: disable=wrong-import-order


SYSPREFIX = sysconfig.get_config_var("prefix")


def _import_buildconfig():
    # A bit of crazyness to import the buildconfig file without importing
    # the rest of virtinst, so the build process doesn't require all the
    # runtime deps to be installed
    spec = importlib.util.spec_from_file_location(
            'buildconfig', 'virtinst/buildconfig.py')
    buildconfig = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(buildconfig)
    if "libvirt" in sys.modules:
        raise RuntimeError("Found libvirt in sys.modules. setup.py should "
                "not import virtinst.")
    return buildconfig.BuildConfig


BuildConfig = _import_buildconfig()


# pylint: disable=attribute-defined-outside-init

_desktop_files = [
    ("share/applications", ["data/virt-manager.desktop.in"]),
]
_appdata_files = [
    ("share/metainfo", ["data/virt-manager.appdata.xml.in"]),
]


class my_build_i18n(setuptools.Command):
    """
    Add our desktop files to the list, saves us having to track setup.cfg
    """
    user_options = [
        ('merge-po', 'm', 'merge po files against template'),
    ]

    def initialize_options(self):
        self.merge_po = False
    def finalize_options(self):
        pass

    def run(self):
        po_dir = "po"
        if self.merge_po:
            pot_file = os.path.join("po", "virt-manager.pot")
            for po_file in glob.glob("%s/*.po" % po_dir):
                cmd = ["msgmerge", "--previous", "-o", po_file, po_file, pot_file]
                self.spawn(cmd)

        max_po_mtime = 0
        for po_file in glob.glob("%s/*.po" % po_dir):
            lang = os.path.basename(po_file[:-3])
            mo_dir = os.path.join("build", "mo", lang, "LC_MESSAGES")
            mo_file = os.path.join(mo_dir, "virt-manager.mo")
            if not os.path.exists(mo_dir):
                os.makedirs(mo_dir)

            cmd = ["msgfmt", po_file, "-o", mo_file]
            po_mtime = os.path.getmtime(po_file)
            mo_mtime = (os.path.exists(mo_file) and
                        os.path.getmtime(mo_file)) or 0
            if po_mtime > max_po_mtime:
                max_po_mtime = po_mtime
            if po_mtime > mo_mtime:
                self.spawn(cmd)

            targetpath = os.path.join("share/locale", lang, "LC_MESSAGES")
            self.distribution.data_files.append((targetpath, (mo_file,)))

        # Merge .in with translations using gettext
        for (file_set, switch) in [(_appdata_files, "--xml"),
                                   (_desktop_files, "--desktop")]:
            for (target, files) in file_set:
                build_target = os.path.join("build", target)
                if not os.path.exists(build_target):
                    os.makedirs(build_target)

                files_merged = []
                for f in files:
                    if f.endswith(".in"):
                        file_merged = os.path.basename(f[:-3])
                    else:
                        file_merged = os.path.basename(f)

                    file_merged = os.path.join(build_target, file_merged)
                    cmd = ["msgfmt", switch, "--template", f, "-d", po_dir,
                           "-o", file_merged]
                    mtime_merged = (os.path.exists(file_merged) and
                                    os.path.getmtime(file_merged)) or 0
                    mtime_file = os.path.getmtime(f)
                    if (mtime_merged < max_po_mtime or
                        mtime_merged < mtime_file):
                        # Only build if output is older than input (.po,.in)
                        self.spawn(cmd)
                    files_merged.append(file_merged)
                self.distribution.data_files.append((target, files_merged))


class my_build(distutils.command.build.build):
    def _make_bin_wrappers(self):
        template = """#!/usr/bin/env python3

import os
import sys
sys.path.insert(0, "%(sharepath)s")
from %(pkgname)s import %(filename)s

%(filename)s.runcli()
"""
        if not os.path.exists("build"):
            os.mkdir("build")
        sharepath = os.path.join(BuildConfig.prefix, "share", "virt-manager")

        def make_script(pkgname, filename, toolname):
            assert os.path.exists(pkgname + "/" + filename + ".py")
            content = template % {
                "sharepath": sharepath,
                "pkgname": pkgname,
                "filename": filename}

            newpath = os.path.abspath(os.path.join("build", toolname))
            print("Generating %s" % newpath)
            open(newpath, "w").write(content)

        make_script("virtinst", "virtinstall", "virt-install")
        make_script("virtinst", "virtclone", "virt-clone")
        make_script("virtinst", "virtxml", "virt-xml")
        make_script("virtManager", "virtmanager", "virt-manager")


    def _make_man_pages(self):
        rstbin = shutil.which("rst2man")
        if not rstbin:
            rstbin = shutil.which("rst2man.py")
        if not rstbin:
            sys.exit("Didn't find rst2man or rst2man.py")

        for path in glob.glob("man/*.rst"):
            base = os.path.basename(path)
            appname = os.path.splitext(base)[0]
            newpath = os.path.join(os.path.dirname(path),
                                   appname + ".1")

            print("Generating %s" % newpath)
            out = subprocess.check_output([rstbin, "--strict", path])
            open(newpath, "wb").write(out)

            self.distribution.data_files.append(
                ('share/man/man1', (newpath,)))

    def _build_icons(self):
        for size in glob.glob(os.path.join("data/icons", "*")):
            for category in glob.glob(os.path.join(size, "*")):
                icons = []
                for icon in glob.glob(os.path.join(category, "*")):
                    icons.append(icon)
                if not icons:
                    continue

                category = os.path.basename(category)
                dest = ("share/icons/hicolor/%s/%s" %
                        (os.path.basename(size), category))
                if category != "apps":
                    dest = dest.replace("share/", "share/virt-manager/")

                self.distribution.data_files.append((dest, icons))


    def _make_bash_completion_files(self):
        scripts = ["virt-install", "virt-clone", "virt-xml"]
        srcfile = "data/bash-completion.sh.in"
        builddir = "build/bash-completion/"
        if not os.path.exists(builddir):
            os.makedirs(builddir)

        instpaths = []
        for script in scripts:
            genfile = os.path.join(builddir, script)
            print("Generating %s" % genfile)
            src = open(srcfile, "r")
            dst = open(genfile, "w")
            dst.write(src.read().replace("::SCRIPTNAME::", script))
            dst.close()
            instpaths.append(genfile)

        bashdir = "share/bash-completion/completions/"
        self.distribution.data_files.append((bashdir, instpaths))


    def run(self):
        self._make_bin_wrappers()
        self._make_man_pages()
        self._build_icons()
        self._make_bash_completion_files()

        self.run_command("build_i18n")
        distutils.command.build.build.run(self)


class my_egg_info(setuptools.command.install_egg_info.install_egg_info):
    """
    Disable egg_info installation, seems pointless for a non-library
    """
    def run(self):
        pass


class my_install(setuptools.command.install.install):
    """
    Error if we weren't 'configure'd with the correct install prefix
    """
    def finalize_options(self):
        if self.prefix is None:
            if BuildConfig.prefix != SYSPREFIX:
                print("Using configured prefix=%s instead of SYSPREFIX=%s" % (
                    BuildConfig.prefix, SYSPREFIX))
                self.prefix = BuildConfig.prefix
            else:
                print("Using SYSPREFIX=%s" % SYSPREFIX)
                self.prefix = SYSPREFIX

        elif self.prefix != BuildConfig.prefix:
            print("Install prefix=%s doesn't match configure prefix=%s\n"
                  "Pass matching --prefix to 'setup.py configure'" %
                  (self.prefix, BuildConfig.prefix))
            sys.exit(1)

        setuptools.command.install.install.finalize_options(self)

    def run(self):
        if not self.distribution.no_update_icon_cache:
            print("running gtk-update-icon-cache")
            icon_path = os.path.join(self.install_data, "share/icons/hicolor")
            self.spawn(["gtk-update-icon-cache", "-q", "-t", icon_path])

        if not self.distribution.no_compile_schemas:
            print("compiling gsettings schemas")
            gschema_install = os.path.join(self.install_data,
                "share/glib-2.0/schemas")
            self.spawn(["glib-compile-schemas", gschema_install])

        setuptools.command.install.install.run(self)


###################
# Custom commands #
###################

class my_rpm(setuptools.Command):
    user_options = []
    description = "Build RPMs and output to the source directory."

    def initialize_options(self):
        pass
    def finalize_options(self):
        pass

    def run(self):
        self.run_command('sdist')
        srcdir = os.path.dirname(__file__)
        cmd = [
            "rpmbuild", "-ta",
            "--define", "_rpmdir %s" % srcdir,
            "--define", "_srcrpmdir %s" % srcdir,
            "--define", "_specdir /tmp",
            "dist/virt-manager-%s.tar.gz" % BuildConfig.version,
        ]
        subprocess.check_call(cmd)


class configure(setuptools.Command):
    user_options = [
        ("prefix=", None, "installation prefix"),
        ("default-graphics=", None,
         "Default graphics type (spice or vnc) (default=spice)"),
        ("default-hvs=", None,
         "Comma separated list of hypervisors shown in 'Open Connection' "
         "wizard. (default=all hvs)"),

    ]
    description = "Configure the build, similar to ./configure"

    def finalize_options(self):
        pass

    def initialize_options(self):
        self.prefix = SYSPREFIX
        self.default_graphics = None
        self.default_hvs = None


    def run(self):
        template = ""
        template += "[config]\n"
        template += "prefix = %s\n" % self.prefix
        if self.default_graphics is not None:
            template += "default_graphics = %s\n" % self.default_graphics
        if self.default_hvs is not None:
            template += "default_hvs = %s\n" % self.default_hvs

        open(BuildConfig.cfgpath, "w").write(template)
        print("Generated %s" % BuildConfig.cfgpath)


class TestCommand(setuptools.Command):
    user_options = []
    description = "DEPRECATED: Use `pytest`. See CONTRIBUTING.md"
    def finalize_options(self):
        pass
    def initialize_options(self):
        pass
    def run(self):
        sys.exit("ERROR: `test` is deprecated. Call `pytest` instead. "
                 "See CONTRIBUTING.md for more info.")


class CheckPylint(setuptools.Command):
    user_options = [
        ("jobs=", "j", "use multiple processes to speed up Pylint"),
    ]
    description = "Check code using pylint and pycodestyle"

    def initialize_options(self):
        self.jobs = None

    def finalize_options(self):
        if self.jobs:
            self.jobs = int(self.jobs)

    def run(self):
        import pylint.lint
        import pycodestyle

        lintfiles = [
            # Put this first so pylint learns what Gtk version we
            # want to lint against
            "virtManager/virtmanager.py",
            "setup.py",
            "tests",
            "virtinst",
            "virtManager"]

        spellfiles = lintfiles[:]
        spellfiles += list(glob.glob("*.md"))
        spellfiles += list(glob.glob("man/*.rst"))
        spellfiles += ["data/virt-manager.appdata.xml.in",
                       "data/virt-manager.desktop.in",
                       "data/org.virt-manager.virt-manager.gschema.xml",
                       "virt-manager.spec"]
        spellfiles.remove("NEWS.md")

        try:
            import codespell_lib
            # pylint: disable=protected-access
            print("running codespell")
            codespell_lib._codespell.main(
                '-I', 'tests/data/codespell_dict.txt',
                '--skip', '*.pyc,*.iso,*.xml', *spellfiles)
        except ImportError:
            print("codespell is not installed. skipping...")
        except Exception as e:
            print("Error running codespell: %s" % e)

        output_format = sys.stdout.isatty() and "colorized" or "text"

        print("running pycodestyle")
        style_guide = pycodestyle.StyleGuide(
            config_file='setup.cfg',
            format="pylint",
            paths=lintfiles,
        )
        report = style_guide.check_files()
        if style_guide.options.count:
            sys.stderr.write(str(report.total_errors) + '\n')

        print("running pylint")
        pylint_opts = [
            "--rcfile", ".pylintrc",
            "--output-format=%s" % output_format,
        ]
        if self.jobs:
            pylint_opts += ["--jobs=%d" % self.jobs]

        pylint.lint.Run(lintfiles + pylint_opts)


class VMMDistribution(setuptools.dist.Distribution):
    global_options = setuptools.dist.Distribution.global_options + [
        ("no-update-icon-cache", None, "Don't run gtk-update-icon-cache"),
        ("no-compile-schemas", None, "Don't compile gsettings schemas"),
    ]

    def __init__(self, *args, **kwargs):
        self.no_update_icon_cache = False
        self.no_compile_schemas = False
        setuptools.dist.Distribution.__init__(self, *args, **kwargs)


class ExtractMessages(setuptools.Command):
    user_options = [
    ]
    description = "Extract the translation messages"

    def initialize_options(self):
        pass

    def finalize_options(self):
        pass

    def run(self):
        bug_address = "https://github.com/virt-manager/virt-manager/issues"
        potfile = "po/virt-manager.pot"
        xgettext_args = [
            "xgettext",
            "--add-comments=translators",
            "--msgid-bugs-address=" + bug_address,
            "--package-name=virt-manager",
            "--output=" + potfile,
            "--sort-by-file",
            "--join-existing",
        ]

        # Truncate .pot file to ensure it exists
        open(potfile, "w").write("")

        # First extract the messages from the AppStream sources,
        # creating the template
        appdata_files = [f for sublist in _appdata_files for f in sublist[1]]
        cmd = xgettext_args + appdata_files
        self.spawn(cmd)

        # Extract the messages from the desktop files
        desktop_files = [f for sublist in _desktop_files for f in sublist[1]]
        cmd = xgettext_args + ["--language=Desktop"] + desktop_files
        self.spawn(cmd)

        # Extract the messages from the Python sources
        py_sources = list(Path("virtManager").rglob("*.py"))
        py_sources += list(Path("virtinst").rglob("*.py"))
        py_sources = [str(src) for src in py_sources]
        cmd = xgettext_args + ["--language=Python"] + py_sources
        self.spawn(cmd)

        # Extract the messages from the Glade UI files
        ui_files = list(Path(".").rglob("*.ui"))
        ui_files = [str(src) for src in ui_files]
        cmd = xgettext_args + ["--language=Glade"] + ui_files
        self.spawn(cmd)


setuptools.setup(
    name="virt-manager",
    version=BuildConfig.version,
    author="Cole Robinson",
    author_email="virt-tools-list@redhat.com",
    url="http://virt-manager.org",
    license="GPLv2+",

    # These wrappers are generated in our custom build command
    scripts=([
        "build/virt-manager",
        "build/virt-clone",
        "build/virt-install",
        "build/virt-xml"]),

    data_files=[
        ("share/glib-2.0/schemas",
         ["data/org.virt-manager.virt-manager.gschema.xml"]),
        ("share/virt-manager/ui", glob.glob("ui/*.ui")),

        ("share/man/man1", [
            "man/virt-manager.1",
            "man/virt-install.1",
            "man/virt-clone.1",
            "man/virt-xml.1"
        ]),

        ("share/virt-manager/virtManager", glob.glob("virtManager/*.py")),
        ("share/virt-manager/virtManager/details",
            glob.glob("virtManager/details/*.py")),
        ("share/virt-manager/virtManager/device",
            glob.glob("virtManager/device/*.py")),
        ("share/virt-manager/virtManager/lib",
            glob.glob("virtManager/lib/*.py")),
        ("share/virt-manager/virtManager/object",
            glob.glob("virtManager/object/*.py")),
        ("share/virt-manager/virtinst",
            glob.glob("virtinst/*.py") + glob.glob("virtinst/build.cfg")),
        ("share/virt-manager/virtinst/devices",
            glob.glob("virtinst/devices/*.py")),
        ("share/virt-manager/virtinst/domain",
            glob.glob("virtinst/domain/*.py")),
        ("share/virt-manager/virtinst/install",
            glob.glob("virtinst/install/*.py")),
    ],

    cmdclass={
        'build': my_build,
        'build_i18n': my_build_i18n,

        'install': my_install,
        'install_egg_info': my_egg_info,

        'configure': configure,

        'pylint': CheckPylint,
        'rpm': my_rpm,
        'test': TestCommand,

        'extract_messages': ExtractMessages,
    },

    distclass=VMMDistribution,
)
