# Copyright 2013-2024 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)

import os.path
import re
import shutil
from textwrap import dedent

import llnl.util.tty as tty

from spack.package import *


class LinuxPerf(Package):
    """The Linux perf tool."""

    homepage = "https://www.kernel.org/"
    url = "https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.5.5.tar.xz"

    executables = ["^perf$"]

    maintainers("Jordan474")

    version("6.9.10", sha256="efd12e335fa67d13a3eae30e4b7b7546e74b8ccc90682e4c3fffab0b22654da1")
    version("6.6.41", sha256="9ec99c578158ab85d99b37791a76643d2ea4c3f72ecbef7b5eb6d60f3de032ef")
    version("5.15.163", sha256="025fc7d8b1560cf456ccae50591fe1ca21c990645df9791aed25820fe78db302")

    variant("libtraceevent", default=True, description="recommended dependency")
    variant("python", default=True, description="Python support")
    variant("perl", default=True, description="perl script extension")
    variant("slang", default=True, description="TUI support")
    variant("libpfm4", default=True, description="libpfm4 events extension")
    variant("babeltrace", default=True, description="libbabeltrace support for CTF data format")
    variant("libcap", default=True, description="process capabilities considered by perf")
    variant("numactl", default=True, description="numa perf benchmark")
    variant(
        "libaudit",
        default=False,
        description=(
            "get perf-trace syscall table from libaudit at runtime,"
            " rather than unistd.h at buildtime"
        ),
    )
    variant("debuginfod", default=False, description="support debuginfod")
    variant("capstone", default=True, description="capstone disassembler")
    variant(
        "zstd",
        default=True,
        description="Zstandard based runtime trace compression in record mode",
    )
    variant("xz", default=True, description="xz kernel module decompression")
    variant(
        "openssl",
        default=True,
        description="support generating build-ids for ELFs generated by jitdump",
    )
    variant("jvmti", default=False, description="build jvmti agent")

    depends_on("c", type="build")
    depends_on("gmake", type="build")
    depends_on("pkgconfig", type="build")
    depends_on("flex", type="build")
    depends_on("bison", type="build")

    depends_on("elfutils")
    depends_on("elfutils +debuginfod", when="+debuginfod")
    depends_on("libunwind components=ptrace")
    depends_on("libiberty")
    depends_on("binutils", type=("build", "link", "run"))
    depends_on("zlib-api")

    depends_on("libtraceevent", when="+libtraceevent")

    # jevents requires python >= 3.6
    depends_on("python@3.6:", type=("build", "link", "run"), when="+python")
    depends_on("py-setuptools", type="build", when="+python")
    depends_on("perl", when="+perl")
    depends_on("slang", when="+slang")
    depends_on("capstone", when="+capstone")
    depends_on("libpfm4", when="+libpfm4")
    depends_on("babeltrace@1.5:", when="+babeltrace")
    depends_on("libcap", when="+libcap")
    depends_on("audit-userspace", when="+libaudit")
    depends_on("numactl", when="+numactl")
    depends_on("zstd", when="+zstd")
    depends_on("xz", when="+xz")
    depends_on("openssl", when="+openssl")
    depends_on("openssl@1.1:", when="@5.19: +openssl")
    depends_on("java", when="+jvmti")

    depends_on("asciidoc", type="build")
    depends_on("xmlto", type="build")

    conflicts(
        "~libtraceevent",
        when="@6.2:",
        msg="linux 6.2 removed internal libtraceevent, and it's highly recommended",
    )

    def url_for_version(self, version):
        return f"https://cdn.kernel.org/pub/linux/kernel/v{version[0]}.x/linux-{version}.tar.xz"

    def setup_build_environment(self, env):
        # This variable is used in the Makefile. If it is defined on the
        # system, it can break the build if there is no build recipe for
        # that specific ARCH
        env.unset("ARCH")

    @property
    def archive_files(self):
        return [join_path(self.stage.source_path, "tools/perf/FEATURE-DUMP")]

    def install(self, spec, prefix):
        # TODO:
        # - GTK2=
        # - NO_LIBBPF=1 ?
        # - d3 flamegraph resources (libexec/perf-core/scripts/python/flamegraph.py)

        version = self.spec.version

        args = [
            "LIBDW_DIR={}".format(spec["elfutils"].prefix),
            "LIBUNWIND_DIR={}".format(spec["libunwind"].prefix),
            "NO_SHELLCHECK=1",
        ]

        # Setup clang if found in the system's or env's PATH:
        clang = shutil.which("clang")
        if clang:
            args.append("CLANG=" + clang)

        # Features to check post-install against `perf version --build-options`
        checks = {"dwarf", "libunwind", "libbfd", "zlib"}

        if version >= Version("6.4"):
            args.append("BUILD_NONDISTRO=1")

        if spec.satisfies("+libaudit"):
            checks.add("libaudit")
            args.append("NO_SYSCALL_TABLE=1")  # will look for libaudit
        else:
            checks.add("syscall_table")
            args.append("NO_LIBAUDIT=1")

        if spec.satisfies("+debuginfod"):
            if version >= Version("5.19"):  # Not in --build-options before that
                checks.add("debuginfod")
        else:
            args.append("NO_LIBDEBUGINFOD=1")

        if spec.satisfies("+python"):
            checks.add("libpython")
            args.extend(
                [
                    "PYTHON={}".format(spec["python"].command),
                    "PYTHON_CONFIG={}".format(spec["python"].prefix.bin.join("python-config")),
                ]
            )
        else:
            args.append("NO_LIBPYTHON=1")

        if spec.satisfies("+perl"):
            checks.add("libperl")
        else:
            args.append("NO_LIBPERL=1")

        if spec.satisfies("+openssl"):
            checks.add("libcrypto")
        else:
            args.append("NO_LIBCRYPTO=1")

        if spec.satisfies("+slang"):
            checks.add("libslang")
        else:
            args.append("NO_SLANG=1")

        if spec.satisfies("+libpfm4"):
            checks.add("libpfm4")
            if version < Version("6.4"):
                args.append("LIBPFM4=1")
        else:
            if version >= Version("6.4"):
                args.append("NO_LIBPFM4=1")

        if spec.satisfies("+babeltrace"):
            # checks.add("babeltrace")  # Not in --build-options ?
            args.append("LIBBABELTRACE_DIR={}".format(spec["babeltrace"].prefix))
        else:
            args.append("NO_LIBBABELTRACE=1")

        if spec.satisfies("+libcap"):
            # checks.add("libcap")  # Not in --build-options ?
            pass
        else:
            args.append("NO_LIBCAP=1")

        if spec.satisfies("+numactl"):
            checks.add("libnuma")
        else:
            args.append("NO_LIBNUMA=1")

        if spec.satisfies("+xz"):
            checks.add("lzma")
        else:
            args.append("NO_LZMA=1")

        if spec.satisfies("+zstd"):
            checks.add("zstd")
            args.append("LIBZSTD_DIR={}".format(spec["zstd"].prefix))
        else:
            args.append("NO_LIBZSTD=1")

        if spec.satisfies("+libtraceevent"):
            if version >= Version("6.2"):  # Not in --build-options before that
                checks.add("libtraceevent")
            if version >= Version("6.10"):
                args.append("LIBTRACEEVENT_DIR={}".format(spec["libtraceevent"].prefix))
            if version < Version("6.2"):
                args.append("LIBTRACEEVENT_DYNAMIC=1")
        else:
            if version >= Version("6.2"):
                args.append("NO_LIBTRACEEVENT=1")

        if spec.satisfies("+jvmti"):
            # checks.add("jvmti")  # Not in --build-options ?
            args.append("JDIR={}".format(spec["java"].prefix))
        else:
            args.append("NO_JVMTI=1")

        with working_dir("tools/perf"):
            make(
                "V=1",
                f"JOBS={make_jobs}",
                f"prefix={prefix}",
                "DESTDIR=",
                *args,
                "all",
                "install",
                parallel=False,
            )

        # Create a perfconfig with binutils paths
        perfconfig = str(prefix.join("etc/perfconfig"))
        assert not os.path.exists(perfconfig)
        mkdirp(os.path.dirname(perfconfig))
        with open(perfconfig, "w") as f:
            f.write(
                dedent(
                    """\
                    [annotate]
                    addr2line = {addr2line}
                    objdump = {objdump}
                    """
                ).format(
                    addr2line=spec["binutils"].prefix.bin.join("addr2line"),
                    objdump=spec["binutils"].prefix.bin.join("objdump"),
                )
            )

        # Post-install dependency check:
        #   $ perf version --build-options
        #   perf version 6.5.7
        #        dwarf: [ on  ]  # HAVE_DWARF_SUPPORT
        #   ...
        perf = Executable(self.prefix.bin.perf)
        output = perf("version", "--build-options", output=str, error=str)
        tty.msg(output)  # keep a trace in build log
        enabled = set(re.findall(r"^\s*(\S+)\s*:\s*\[\s*on\s*\]", output, re.MULTILINE))
        missing = set(checks) - enabled
        tty.msg(f"detected features: {sorted(enabled)!r}")
        tty.msg(f"expected features: {sorted(checks)!r}")
        if missing:
            raise InstallError(f"Perf is missing features {sorted(missing)!r}, see log")
