# Copyright Spack Project Developers. See COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)

import os
import pathlib
import re
import sys

from spack_repo.builtin.build_systems.generic import Package

from spack.package import *


class Cmake(Package):
    """A cross-platform, open-source build system. CMake is a family of
    tools designed to build, test and package software.
    """

    homepage = "https://www.cmake.org"
    url = "https://github.com/Kitware/CMake/releases/download/v3.19.0/cmake-3.19.0.tar.gz"
    git = "https://gitlab.kitware.com/cmake/cmake.git"
    github = "https://github.com/kitware/cmake"

    maintainers("alalazo", "johnwparent")

    tags = ["build-tools", "windows"]

    executables = ["^cmake[0-9]*$"]

    license("BSD-3-Clause")

    version("master", branch="master")
    version("4.2.0", sha256="4104e94657d247c811cb29985405a360b78130b5d51e7f6daceb2447830bd579")
    version("4.1.2", sha256="643f04182b7ba323ab31f526f785134fb79cba3188a852206ef0473fee282a15")
    version("4.1.1", sha256="b29f6f19733aa224b7763507a108a427ed48c688e1faf22b29c44e1c30549282")
    version("4.0.4", sha256="629be82af0b76e029b675a4a37569e2ddc1769d42a768957c00ec0e98407737e")
    version(
        "3.31.9",
        sha256="5d4fdec04247ca8a8e8f63692f0d0f1e9d6d082a2bdd008dff8ab3ba7215aa83",
        preferred=True,
    )
    version("3.30.9", sha256="65f765bb87c8019316cabe67cbe5e8f45ede334eeb5afd161ca6874d17994e0d")
    version("3.29.6", sha256="1391313003b83d48e2ab115a8b525a557f78d8c1544618b48d1d90184a10f0af")
    version("3.28.6", sha256="c39c733900affc4eb0e9688b4d1a45435a732105d9bf9cc1e75dd2b9b81a36bb")
    version("3.27.9", sha256="609a9b98572a6a5ea477f912cffb973109ed4d0a6a6b3f9e2353d2cdc048708e")
    version("3.26.6", sha256="070b9a2422e666d2c1437e2dab239a236e8a63622d0a8d0ffe9e389613d2b76a")
    version("3.25.3", sha256="cc995701d590ca6debc4245e9989939099ca52827dd46b5d3592f093afe1901c")
    version("3.24.4", sha256="32c9e499510eff7070d3f0adfbabe0afea2058608c5fa93e231beb49fbfa2296")
    version("3.23.5", sha256="f2944cde7a140b992ba5ccea2009a987a92413762250de22ebbace2319a0f47d")
    version("3.22.6", sha256="73933163670ea4ea95c231549007b0c7243282293506a2cf4443714826ad5ec3")
    version("3.21.7", sha256="3523c4a5afc61ac3d7c92835301cdf092129c9b672a6ee17e68c92e928c1375a")
    version("3.20.6", sha256="a0bd485e1a38dd13c0baec89d5f4adbf61c7fd32fddb38eabc69a75bc0b65d72")
    version("3.19.8", sha256="09b4fa4837aae55c75fb170f6a6e2b44818deba48335d1969deddfbb34e30369")
    version("3.18.6", sha256="124f571ab70332da97a173cb794dfa09a5b20ccbb80a08e56570a500f47b6600")
    version("3.17.5", sha256="8c3083d98fd93c1228d5e4e40dbff2dd88f4f7b73b9fa24a2938627b8bc28f1a")
    version("3.16.9", sha256="1708361827a5a0de37d55f5c9698004c035abb1de6120a376d5d59a81630191f")
    version("3.15.7", sha256="71999d8a14c9b51708847371250a61533439a7331eb7702ac105cfb3cb1be54b")
    version("3.14.7", sha256="9221993e0af3e6d10124d840ff24f5b2f3b884416fca04d3312cb0388dec1385")
    version("3.13.5", sha256="526db6a4b47772d1943b2f86de693e712f9dacf3d7c13b19197c9bef133766a5")
    version("3.12.4", sha256="5255584bfd043eb717562cff8942d472f1c0e4679c4941d84baadaa9b28e3194")
    version("3.11.4", sha256="8f864e9f78917de3e1483e256270daabc4a321741592c5b36af028e72bff87f5")
    version("3.10.3", sha256="0c3a1dcf0be03e40cf4f341dda79c96ffb6c35ae35f2f911845b72dab3559cf8")
    version("3.9.6", sha256="7410851a783a41b521214ad987bb534a7e4a65e059651a2514e6ebfc8f46b218")
    version("3.8.2", sha256="da3072794eb4c09f2d782fcee043847b99bb4cf8d4573978d9b2024214d6e92d")
    version("3.7.2", sha256="dc1246c4e6d168ea4d6e042cfba577c1acd65feea27e56f5ff37df920c30cae0")
    version("3.6.1", sha256="28ee98ec40427d41a45673847db7a905b59ce9243bb866eaf59dce0f58aaef11")
    version("3.5.2", sha256="92d8410d3d981bb881dfff2aed466da55a58d34c7390d50449aa59b32bb5e62a")
    version("3.4.3", sha256="b73f8c1029611df7ed81796bf5ca8ba0ef41c6761132340c73ffe42704f980fa")
    version("3.3.1", sha256="cd65022c6a0707f1c7112f99e9c981677fdd5518f7ddfa0f778d4cee7113e3d6")
    version("3.1.0", sha256="8bdc3fa3f2da81bc10c772a6b64cc9052acc2901d42e1e1b2588b40df224aad9")
    version("3.0.2", sha256="6b4ea61eadbbd9bec0ccb383c29d1f4496eacc121ef7acf37c7a24777805693e")
    version("2.8.10.2", sha256="ce524fb39da06ee6d47534bbcec6e0b50422e18b62abc4781a4ba72ea2910eb1")

    with default_args(deprecated=True):
        version("4.1.0", sha256="81ee8170028865581a8e10eaf055afb620fa4baa0beb6387241241a975033508")
        version("4.0.3", sha256="8d3537b7b7732660ea247398f166be892fe6131d63cc291944b45b91279f3ffb")
        version("4.0.0", sha256="ddc54ad63b87e153cf50be450a6580f1b17b4881de8941da963ff56991a4083b")
        version(
            "3.31.8", sha256="e3cde3ca83dc2d3212105326b8f1b565116be808394384007e7ef1c253af6caa"
        )
        version(
            "3.31.6", sha256="653427f0f5014750aafff22727fb2aa60c6c732ca91808cfb78ce22ddd9e55f0"
        )

    depends_on("c", type="build")
    depends_on("cxx", type="build")

    variant(
        "build_type",
        default="Release",
        description="CMake build type",
        values=("Debug", "Release", "RelWithDebInfo", "MinSizeRel"),
    )

    # We default ownlibs to true because it greatly speeds up the CMake
    # build, and CMake is built frequently. Also, CMake is almost always
    # a build dependency, and its libs will not interfere with others in
    # the build.
    variant("ownlibs", default=True, description="Use CMake-provided third-party libraries")
    variant(
        "doc",
        default=False,
        description="Enables the generation of html and man page documentation",
    )
    variant("qtgui", default=False, description="Enables the build of the Qt GUI")

    for spack_platform in ["freebsd", "linux", "darwin"]:
        with when(f"platform={spack_platform}"):
            variant("ncurses", default=True, description="Enables the build of the ncurses gui")

    # Revert the change that introduced a regression when parsing mpi link
    # flags, see: https://gitlab.kitware.com/cmake/cmake/issues/19516
    patch("cmake-revert-findmpi-link-flag-list.patch", when="@3.15.0")

    # Fix linker error when using external libs on darwin.
    # See https://gitlab.kitware.com/cmake/cmake/merge_requests/2873
    patch("cmake-macos-add-coreservices.patch", when="@3.11.0:3.13.3")

    # Fix builds with XLF + Ninja generator
    # https://gitlab.kitware.com/cmake/cmake/merge_requests/4075
    patch(
        "fix-xlf-ninja-mr-4075.patch",
        sha256="42d8b2163a2f37a745800ec13a96c08a3a20d5e67af51031e51f63313d0dedd1",
        when="@3.15.5",
    )

    # Statically linked binaries error on install when CMAKE_INSTALL_RPATH is set
    # https://gitlab.kitware.com/cmake/cmake/-/merge_requests/9623
    patch("mr-9623.patch", when="@3.22.0:3.30")

    patch(
        f"{github}/commit/1b0c92a3a1b782ff3e1c4499b6ab8db614d45bcd.patch?full_index=1",
        sha256="fdea723be9713f3ed4624055bf21ef5876647d63c151b91006608ec44a912ae1",
        when="@3.11:3.31.6",
    )

    depends_on("c", type="build")
    depends_on("cxx", type="build")

    depends_on("ninja", when="platform=windows")
    depends_on("gmake", type=("build", "run"), when="platform=linux")
    depends_on("gmake", type=("build", "run"), when="platform=darwin")
    depends_on("gmake", type=("build", "run"), when="platform=freebsd")

    depends_on("qt", when="+qtgui")
    # Qt depends on libmng, which is a CMake package;
    # ensure we build using a non CMake build system
    # when libmng is build as a transitive dependency of CMake
    for plat in ["linux", "darwin", "freebsd"]:
        with when(f"platform={plat}"):
            depends_on("libmng build_system=autotools", when="+qtgui")

    # See https://gitlab.kitware.com/cmake/cmake/-/issues/21135
    conflicts(
        "platform=darwin %gcc",
        when="@:3.17",
        msg="CMake <3.18 does not compile with GCC on macOS, "
        "please use %apple-clang or a newer CMake release. "
        "See: https://gitlab.kitware.com/cmake/cmake/-/issues/21135",
    )

    # Vendored dependencies do not build with nvhpc; it's also more
    # transparent to patch Spack's versions of CMake's dependencies.
    conflicts("+ownlibs %nvhpc")

    # Use Spack's curl even if +ownlibs, since that allows us to make use of
    # the conflicts on the curl package for TLS libs like OpenSSL.
    # In the past we let CMake build a vendored copy of curl, but had to
    # provide Spack's TLS libs anyways, which is not flexible, and actually
    # leads to issues where we have to keep track of the vendored curl version
    # and its conflicts with OpenSSL.
    depends_on("curl")

    # When using curl, cmake defaults to using system zlib too, probably because
    # curl already depends on zlib. Therefore, also unconditionaly depend on zlib.
    depends_on("zlib-api")

    with when("~ownlibs"):
        depends_on("expat")
        # expat/zlib are used in CMake/CTest, so why not require them in libarchive.
        for plat in ["darwin", "linux", "freebsd"]:
            with when("platform=%s" % plat):
                depends_on("libarchive@3.1.0: xar=expat compression=bz2lib,lzma,zlib,zstd")
                depends_on("libarchive@3.3.3:", when="@3.15.0:")
                depends_on("libuv@1.0.0:1.10", when="@3.7.0:3.10.3")
                depends_on("libuv@1.10.0:1.10", when="@3.11.0:3.11")
                depends_on("libuv@1.10.0:", when="@3.12.0:")
                depends_on("rhash", when="@3.8.0:")
                depends_on("jsoncpp build_system=meson", when="@3.2:")

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

    with when("+doc"):
        depends_on("python@2.7.11:", type="build")
        depends_on("py-sphinx", type="build")

    # Cannot build with Intel, should be fixed in 3.6.2
    # https://gitlab.kitware.com/cmake/cmake/issues/16226
    patch("intel-c-gnu11.patch", when="@3.6.0:3.6.1")

    # Cannot build with Intel again, should be fixed in 3.17.4 and 3.18.1
    # https://gitlab.kitware.com/cmake/cmake/-/issues/21013
    patch("intel-cxx-bootstrap.patch", when="@3.17.0:3.17.3,3.18.0")

    # https://gitlab.kitware.com/cmake/cmake/issues/18232
    patch("nag-response-files.patch", when="@3.7:3.12")

    # Cray libhugetlbfs and icpc warnings failing CXX tests
    # https://gitlab.kitware.com/cmake/cmake/-/merge_requests/4698
    # https://gitlab.kitware.com/cmake/cmake/-/merge_requests/4681
    patch("ignore_crayxc_warnings.patch", when="@3.7:3.17.2")

    # The Fujitsu compiler requires the '--linkfortran' option
    # to combine C++ and Fortran programs.
    patch("fujitsu_add_linker_option.patch", when="%fj")

    # Remove -A from the C++ flags we use when CXX_EXTENSIONS is OFF
    # Should be fixed in 3.19. This patch is needed also for nvhpc.
    # https://gitlab.kitware.com/cmake/cmake/-/merge_requests/5025
    patch("pgi-cxx-ansi.patch", when="@3.15:3.18")

    # Adds CCE v11+ fortran preprocessing definition.
    # requires Cmake 3.19+
    # https://gitlab.kitware.com/cmake/cmake/-/merge_requests/5882
    patch(
        "5882-enable-cce-fortran-preprocessing.patch",
        sha256="b48396c0e4f61756248156b6cebe9bc0d7a22228639b47b5aa77c9330588ce88",
        when="@3.19.0:3.19",
    )

    # https://gitlab.kitware.com/cmake/cmake/issues/18166
    conflicts("%intel", when="@3.11.0:3.11.4")
    conflicts("%intel@:14", when="@3.14:", msg="Intel 14 has immature C++11 support")

    resource(
        name="cmake-bootstrap",
        url="https://cmake.org/files/v3.21/cmake-3.21.2-windows-x86_64.zip",
        checksum="213a4e6485b711cb0a48cbd97b10dfe161a46bfe37b8f3205f47e99ffec434d2",
        placement="cmake-bootstrap",
        when="@3.0.2: platform=windows",
    )

    resource(
        name="cmake-bootstrap",
        url="https://cmake.org/files/v2.8/cmake-2.8.4-win32-x86.zip",
        checksum="8b9b520f3372ce67e33d086421c1cb29a5826d0b9b074f44a8a0304e44cf88f3",
        placement="cmake-bootstrap",
        when="@:2.8.10.2 platform=windows",
    )

    phases = ["bootstrap", "build", "install"]

    @classmethod
    def determine_version(cls, exe):
        output = Executable(exe)("--version", output=str, error=str)
        match = re.search(r"cmake.*version\s+(\S+)", output)
        return match.group(1) if match else None

    def bootstrap_args(self):
        spec = self.spec
        args = []
        self.generator = make

        # The Intel compiler isn't able to deal with noinline member functions of
        # template classes defined in headers.  As such it outputs
        #   warning #2196: routine is both "inline" and "noinline"
        # cmake bootstrap will fail due to the word 'warning'.
        if spec.satisfies("%intel@:2021.6.0"):
            args.append("CXXFLAGS=-diag-disable=2196")

        if self.spec.satisfies("platform=windows"):
            args.append("-GNinja")
            self.generator = ninja

        if not sys.platform == "win32":
            args.append("--prefix={0}".format(self.prefix))

            jobs = get_effective_jobs(
                make_jobs,
                parallel=self.parallel,
                supports_jobserver=self.generator.supports_jobserver,
            )
            if jobs is not None:
                args.append("--parallel={0}".format(jobs))

            if spec.satisfies("+ownlibs"):
                # Build and link to the CMake-provided third-party libraries
                args.append("--no-system-libs")
            else:
                # Build and link to the Spack-installed third-party libraries
                args.append("--system-libs")

                # cppdap is a CMake package, avoid circular dependency
                if spec.satisfies("@3.27:"):
                    args.append("--no-system-cppdap")

            # Whatever +/~ownlibs, use system curl.
            args.append("--system-curl")

            if spec.satisfies("+doc"):
                args.append("--sphinx-html")
                args.append("--sphinx-man")

            if spec.satisfies("+qtgui"):
                args.append("--qt-gui")
            else:
                args.append("--no-qt-gui")

            # Now for CMake arguments to pass after the initial bootstrap
            args.append("--")
        else:
            args.append("-DCMAKE_INSTALL_PREFIX=%s" % self.prefix)

        # Make CMake find its own dependencies.
        prefixes = get_cmake_prefix_path(self)
        rpaths = [
            pathlib.Path(self.prefix, "lib").as_posix(),
            pathlib.Path(self.prefix, "lib64").as_posix(),
        ]

        args.extend(
            [
                f"-DCMAKE_BUILD_TYPE={self.spec.variants['build_type'].value}",
                # Install CMake correctly, even if `spack install` runs
                # inside a ctest environment
                "-DCMake_TEST_INSTALL=OFF",
                f"-DBUILD_CursesDialog={'ON' if '+ncurses' in spec else 'OFF'}",
                f"-DBUILD_QtDialog={'ON' if spec.satisfies('+qtgui') else 'OFF'}",
                "-DCMAKE_INSTALL_RPATH_USE_LINK_PATH=ON",
                f"-DCMAKE_INSTALL_RPATH={';'.join(rpaths)}",
                f"-DCMAKE_PREFIX_PATH={';'.join(str(v) for v in prefixes)}",
            ]
        )

        return args

    def cmake_bootstrap(self):
        exe_prefix = self.stage.source_path
        relative_cmake_exe = os.path.join("cmake-bootstrap", "bin", "cmake.exe")
        return Executable(os.path.join(exe_prefix, relative_cmake_exe))

    def bootstrap(self, spec, prefix):
        bootstrap_args = self.bootstrap_args()
        if sys.platform == "win32":
            bootstrap = self.cmake_bootstrap()
            bootstrap_args.extend(["."])
        else:
            bootstrap = Executable("./bootstrap")
        bootstrap(*bootstrap_args)

    def build(self, spec, prefix):
        self.generator()

    @run_after("build")
    @on_package_attributes(run_tests=True)
    def build_test(self):
        # Some tests fail, takes forever
        self.generator("test")

    def install(self, spec, prefix):
        self.generator("install")

        if spec.satisfies("%fj"):
            for f in find(self.prefix, "FindMPI.cmake", recursive=True):
                filter_file("mpcc_r)", "mpcc_r mpifcc)", f, string=True)
                filter_file("mpc++_r)", "mpc++_r mpiFCC)", f, string=True)
                filter_file("mpifc)", "mpifc mpifrt)", f, string=True)

    def setup_dependent_build_environment(
        self, env: EnvironmentModifications, dependent_spec: Spec
    ) -> None:
        # CMake 4.0.0 breaks compatibility with CMake projects requiring a CMake
        # < 3.5. However, many projects that specify a minimum requirement for
        # versions older than 3.5 are actually compatible with newer CMake
        # and do not use CMake 3.4 or older features. This allows those
        # projects to use a newer CMake
        if self.spec.satisfies("@4:"):
            env.set("CMAKE_POLICY_VERSION_MINIMUM", "3.5")

    def setup_dependent_package(self, module, dependent_spec):
        """Called before cmake packages's install() methods."""

        module.cmake = Executable(self.spec.prefix.bin.cmake)
        module.ctest = Executable(self.spec.prefix.bin.ctest)

    @property
    def libs(self):
        """CMake has no libraries, so if you ask for `spec['cmake'].libs`
        (which happens automatically for packages that depend on CMake as
        a link dependency) the default implementation of ``.libs` will
        search the entire root prefix recursively before failing.

        The longer term solution is for all dependents of CMake to change
        their deptype. For now, this returns an empty set of libraries.
        """
        return LibraryList([])

    @property
    def headers(self):
        return HeaderList([])

    @property
    def archive_files(self):
        return [join_path(self.stage.source_path, "Bootstrap.cmk", "cmake_bootstrap.log")]

    def run_version_check(self, bin):
        """Runs and checks output of the installed binary."""
        exe_path = join_path(self.prefix.bin, bin)
        if not os.path.exists(exe_path):
            raise SkipTest(f"{exe_path} is not installed")

        exe = which(exe_path)
        out = exe("--version", output=str.split, error=str.split)
        assert f"version {self.spec.version}" in out

    def test_ccmake(self):
        """check version from ccmake"""
        self.run_version_check("ccmake")

    def test_cmake(self):
        """check version from cmake"""
        self.run_version_check("cmake")

    def test_cpack(self):
        """check version from cpack"""
        self.run_version_check("cpack")

    def test_ctest(self):
        """check version from ctest"""
        self.run_version_check("ctest")
