# Source code for skbuild.platform_specifics.abstract

    """This module defines objects useful to discover which CMake generator is
    supported on the current platform."""

    from __future__ import annotations

    import os
    import shutil
    import subprocess
    import textwrap
    from typing import Iterable, Mapping

    from ..constants import CMAKE_DEFAULT_EXECUTABLE
    from ..exceptions import SKBuildGeneratorNotFoundError
    from ..utils import push_dir

    test_folder = "_cmake_test_compile"

    class CMakePlatform:
        """This class encapsulates the logic allowing to get the identifier of a
        working CMake generator.

        Derived class should at least set :attr:`default_generators`.
        """

        def __init__(self) -> None:
            # default_generators is a property for mocking in tests
            self._default_generators: list[CMakeGenerator] = []
            self.architecture: str | None = None

        @property
        def default_generators(self) -> list[CMakeGenerator]:
            """List of generators considered by :func:`get_best_generator()`."""
            return self._default_generators

        @default_generators.setter
        def default_generators(self, generators: list[CMakeGenerator]) -> None:
            self._default_generators = generators

        @property
        def generator_installation_help(self) -> str:
            """Return message guiding the user for installing a valid toolchain."""
            raise NotImplementedError()  # pragma: no cover

        @staticmethod
        def write_test_cmakelist(languages: Iterable[str]) -> None:
            """Write a minimal ``CMakeLists.txt`` useful to check if the
            requested ``languages`` are supported."""
            if not os.path.exists(test_folder):
                os.makedirs(test_folder)
            with open(f"{test_folder}/CMakeLists.txt", "w", encoding="utf-8") as f:
                f.write("cmake_minimum_required(VERSION 3.5)\n")
                f.write("PROJECT(compiler_test NONE)\n")
                for language in languages:
                    f.write(f"ENABLE_LANGUAGE({language:s})\n")
                f.write(
                    'if("${_SKBUILD_FORCE_MSVC}")\n'
                    '  if("${_SKBUILD_FORCE_MSVC}" STREQUAL "1930")\n'
                    '    math(EXPR FORCE_MAX "${_SKBUILD_FORCE_MSVC}+19")\n'
                    "  else()\n"
                    '    math(EXPR FORCE_MAX "${_SKBUILD_FORCE_MSVC}+9")\n'
                    "  endif()\n"
                    '  math(EXPR FORCE_MIN "${_SKBUILD_FORCE_MSVC}")\n'
                    "  if(NOT MSVC)\n"
                    '    message(FATAL_ERROR "MSVC is required to pass this check.")\n'
                    "  elseif(MSVC_VERSION LESS FORCE_MIN OR MSVC_VERSION GREATER FORCE_MAX)\n"
                    '    message(FATAL_ERROR "MSVC ${MSVC_VERSION} does pass this check.")\n'
                    "  endif()\n"
                    "endif()\n"
                )

        @staticmethod
        def cleanup_test() -> None:
            """Delete test project directory."""
            if os.path.exists(test_folder):
                shutil.rmtree(test_folder)

        def get_generator(self, generator_name: str) -> CMakeGenerator:
            """Loop over generators and return the first that matches the given
            name.
            """
            for default_generator in self.default_generators:
                if default_generator.name == generator_name:
                    return default_generator

            return CMakeGenerator(generator_name)

        def get_generators(self, generator_name: str) -> list[CMakeGenerator]:
            """Loop over generators and return all that match the given name."""
            return [
                default_generator
                for default_generator in self.default_generators
                if default_generator.name == generator_name
            ]

        # TODO: this method name is not great.  Does anyone have a better idea for
        # renaming it?

        def get_best_generator(
            self,
            generator_name: str | None = None,
            skip_generator_test: bool = False,
            languages: Iterable[str] = ("CXX", "C"),
            cleanup: bool = True,
            cmake_executable: str = CMAKE_DEFAULT_EXECUTABLE,
            cmake_args: Iterable[str] = (),
            architecture: str | None = None,
        ) -> CMakeGenerator:
            """Loop over generators to find one that works by configuring
            and compiling a test project.

            :param generator_name: If provided, uses only provided generator, \
            instead of trying :attr:`default_generators`.
            :type generator_name: str | None

            :param skip_generator_test: If set to True and if a generator name is \
            specified, the generator test is skipped. If no generator_name is specified \
            and the option is set to True, the first available generator is used.
            :type skip_generator_test: bool

            :param languages: The languages you'll need for your project, in terms \
            that CMake recognizes.
            :type languages: tuple

            :param cleanup: If True, cleans up temporary folder used to test \
            generators. Set to False for debugging to see CMake's output files.
            :type cleanup: bool

            :param cmake_executable: Path to CMake executable used to configure \
            and build the test project used to evaluate if a generator is working.
            :type cmake_executable: str

            :param cmake_args: List of CMake arguments to use when configuring \
            the test project. Only arguments starting with ``-DCMAKE_`` are \
            used.
            :type cmake_args: tuple

            :return: CMake Generator object
            :rtype: :class:`CMakeGenerator` or None

            :raises skbuild.exceptions.SKBuildGeneratorNotFoundError:
            """

            candidate_generators: list[CMakeGenerator] = []

            if generator_name is None:
                candidate_generators = self.default_generators
            else:
                # Lookup CMakeGenerator by name. Doing this allow to get a
                # generator object with its ``env`` property appropriately
                # initialized.

                # MSVC should be used in "-A arch" form
                if architecture is not None:
                    self.architecture = architecture

                # Support classic names for generators
                generator_name, self.architecture = _parse_legacy_generator_name(generator_name, self.architecture)

                candidate_generators = []
                for default_generator in self.default_generators:
                    if default_generator.name == generator_name:
                        candidate_generators.append(default_generator)
                if not candidate_generators:
                    candidate_generators = [CMakeGenerator(generator_name)]

            self.write_test_cmakelist(languages)

            working_generator: CMakeGenerator | None
            if skip_generator_test:
                working_generator = candidate_generators[0]
            else:
                working_generator = self.compile_test_cmakelist(cmake_executable, candidate_generators, cmake_args)

            if working_generator is None:
                line = "*" * 80
                installation_help = self.generator_installation_help
                msg = textwrap.dedent(
                    f"""\
                    {line}
                    scikit-build could not get a working generator for your system. Aborting build.

                    {installation_help}

                    {line}"""
                )
                raise SKBuildGeneratorNotFoundError(msg)

            if cleanup:
                CMakePlatform.cleanup_test()

            return working_generator

        @staticmethod
        @push_dir(directory=test_folder)
        def compile_test_cmakelist(
            cmake_exe_path: str, candidate_generators: Iterable[CMakeGenerator], cmake_args: Iterable[str] = ()
        ) -> CMakeGenerator | None:
            """Attempt to configure the test project with
            each :class:`CMakeGenerator` from ``candidate_generators``.

            Only cmake arguments starting with ``-DCMAKE_`` are used to configure
            the test project.

            The function returns the first generator allowing to successfully
            configure the test project using ``cmake_exe_path``."""
            # working generator is the first generator we find that works.
            working_generator = None

            # Include only -DCMAKE_* arguments
            cmake_args = [arg for arg in cmake_args if arg.startswith("-DCMAKE_")]

            # Do not complain about unused CMake arguments
            cmake_args.insert(0, "--no-warn-unused-cli")

            def _generator_discovery_status_msg(_generator: CMakeGenerator, suffix: str = "") -> None:
                outer = "-" * 80
                inner = ["-" * ((idx * 5) - 3) for idx in range(1, 8)]
                print("\n".join(inner) if suffix else outer)
                print(f"-- Trying {_generator.description!r} generator{suffix}")
                print(outer if suffix else "\n".join(inner[::-1]), flush=True)

            for generator in candidate_generators:
                print("\n", flush=True)
                _generator_discovery_status_msg(generator)

                # clear the cache for each attempted generator type
                if os.path.isdir("build"):
                    shutil.rmtree("build")

                with push_dir("build", make_directory=True):
                    # call cmake to see if the compiler specified by this
                    # generator works for the specified languages
                    cmd = [cmake_exe_path, "../", "-G", generator.name]
                    if generator.toolset:
                        cmd.extend(["-T", generator.toolset])
                    if generator.architecture and "Visual Studio" in generator.name:
                        cmd.extend(["-A", generator.architecture])
                    cmd.extend(cmake_args)
                    cmd.extend(generator.args)

                    status = subprocess.run(cmd, env=generator.env, check=False).returncode

                msg = "success" if status == 0 else "failure"
                _generator_discovery_status_msg(generator, f" - {msg}")
                print(flush=True)

                # cmake succeeded, this generator should work
                if status == 0:
                    # we have a working generator, don't bother looking for more
                    working_generator = generator
                    break

            return working_generator

    class CMakeGenerator:
        """Represents a CMake generator.

        .. automethod:: __init__
        """

        def __init__(
            self,
            name: str,
            env: Mapping[str, str] | None = None,
            toolset: str | None = None,
            arch: str | None = None,
            args: Iterable[str] | None = None,
        ) -> None:
            """Instantiate a generator object with the given ``name``.

            By default, ``os.environ`` is associated with the generator. Dictionary
            passed as ``env`` parameter will be merged with ``os.environ``. If an
            environment variable is set in both ``os.environ`` and ``env``, the
            variable in ``env`` is used.

            Some CMake generators support a ``toolset`` specification to tell the native
            build system how to choose a compiler. You can also include CMake arguments.
            """
            self._generator_name = name
            self.args = list(args or [])
            self.env = dict(list(os.environ.items()) + list(env.items() if env else []))
            self._generator_toolset = toolset
            self._generator_architecture = arch
            description_arch = name if arch is None else f"{name} {arch}"
            if toolset is None:
                self._description = description_arch
            else:
                self._description = f"{description_arch} {toolset}"

        @property
        def name(self) -> str:
            """Name of CMake generator."""
            return self._generator_name

        @property
        def toolset(self) -> str | None:
            """Toolset specification associated with the CMake generator."""
            return self._generator_toolset

        @property
        def architecture(self) -> str | None:
            """Architecture associated with the CMake generator."""
            return self._generator_architecture

        @property
        def description(self) -> str:
            """Name of CMake generator with properties describing the environment (e.g toolset)"""
            return self._description

    def _parse_legacy_generator_name(generator_name: str, arch: str | None) -> tuple[str, str | None]:
        """
        Support classic names for MSVC generators. Architecture is stripped from
        the name and "arch" is replaced with the arch string if a legacy name is
        given.
        """

        if generator_name.startswith("Visual Studio"):
            if generator_name.endswith(" Win64"):
                arch = "x64"
                generator_name = generator_name[:-6]
            elif generator_name.endswith(" ARM"):
                arch = "ARM"
                generator_name = generator_name[:-4]

        return generator_name, arch