Source code for skbuild.platform_specifics.windows

"""This module defines object specific to Windows platform."""


from __future__ import annotations

import os
import platform
import re
import subprocess
import sys
import textwrap
from typing import Iterable

from setuptools import monkey

from .._compat.typing import TypedDict
from . import abstract
from .abstract import CMakeGenerator

VS_YEAR_TO_VERSION = {
    "2017": 15,
    "2019": 16,
    "2022": 17,
}
"""Describes the version of `Visual Studio` supported by
:class:`CMakeVisualStudioIDEGenerator` and
:class:`CMakeVisualStudioCommandLineGenerator`.

The different version are identified by their year.
"""

VS_YEAR_TO_MSC_VER = {
    "2017": "1910",  # VS 2017 - can be +9
    "2019": "1920",  # VS 2019 - can be +9
    "2022": "1930",  # VS 2022 - can be +9
}

ARCH_TO_MSVC_ARCH = {
    "Win32": "x86",
    "ARM64": "x86_arm64",
    "x64": "x86_amd64",
}


[docs] class CachedEnv(TypedDict): """Stored environment.""" PATH: str INCLUDE: str LIB: str
[docs] class WindowsPlatform(abstract.CMakePlatform): """Windows implementation of :class:`.abstract.CMakePlatform`.""" def __init__(self) -> None: super().__init__() self._vs_help = "" vs_help_template = ( textwrap.dedent( """ Building windows wheels for Python {pyver} requires Microsoft Visual Studio %s. Get it with "%s": %s """ ) .strip() .format(pyver=".".join(str(v) for v in sys.version_info[:2])) ) # For Python 3.7 and above: VS2022, VS2019, VS2017 supported_vs_years = [("2022", "v143"), ("2019", "v142"), ("2017", "v141")] self._vs_help = vs_help_template % ( supported_vs_years[0][0], "Visual Studio 2017", "https://visualstudio.microsoft.com/vs/", ) self._vs_help += ( "\n\n" + textwrap.dedent( """ Or with "Visual Studio 2019": https://visualstudio.microsoft.com/vs/ Or with "Visual Studio 2022": https://visualstudio.microsoft.com/vs/ """ ).strip() ) try: import ninja # pylint: disable=import-outside-toplevel ninja_executable_path = os.path.join(ninja.BIN_DIR, "ninja") ninja_args = ["-DCMAKE_MAKE_PROGRAM:FILEPATH=" + ninja_executable_path] except ImportError: ninja_args = [] extra = [] for vs_year, vs_toolset in supported_vs_years: vs_version = VS_YEAR_TO_MSC_VER[vs_year] args = [f"-D_SKBUILD_FORCE_MSVC={vs_version}"] self.default_generators.extend( [ CMakeVisualStudioCommandLineGenerator("Ninja", vs_year, vs_toolset, args=ninja_args + args), CMakeVisualStudioIDEGenerator(vs_year, vs_toolset), ] ) extra.append(CMakeVisualStudioCommandLineGenerator("NMake Makefiles", vs_year, vs_toolset, args=args)) self.default_generators.extend(extra) @property def generator_installation_help(self) -> str: """Return message guiding the user for installing a valid toolchain.""" return self._vs_help
def _compute_arch() -> str: """Currently only supports Intel -> ARM cross-compilation.""" if platform.machine() == "ARM64" or "arm64" in os.environ.get("SETUPTOOLS_EXT_SUFFIX", "").lower(): return "ARM64" if platform.architecture()[0] == "64bit": return "x64" return "Win32"
[docs] class CMakeVisualStudioIDEGenerator(CMakeGenerator): """ Represents a Visual Studio CMake generator. .. automethod:: __init__ """
[docs] def __init__(self, year: str, toolset: str | None = None) -> None: """Instantiate a generator object with its name set to the `Visual Studio` generator associated with the given ``year`` (see :data:`VS_YEAR_TO_VERSION`), the current platform (32-bit or 64-bit) and the selected ``toolset`` (if applicable). """ vs_version = VS_YEAR_TO_VERSION[year] vs_base = f"Visual Studio {vs_version} {year}" vs_arch = _compute_arch() super().__init__(vs_base, toolset=toolset, arch=vs_arch)
def _find_visual_studio_2017_or_newer(vs_version: int) -> str: """Adapted from https://github.com/python/cpython/blob/3.7/Lib/distutils/_msvccompiler.py The ``vs_version`` corresponds to the `Visual Studio` version to lookup. See :data:`VS_YEAR_TO_VERSION`. Returns `path` based on the result of invoking ``vswhere.exe``. If no install is found, returns an empty string. ..note: If ``vswhere.exe`` is not available, by definition, VS 2017 or newer is not installed. """ root = os.environ.get("PROGRAMFILES(X86)") or os.environ.get("PROGRAMFILES") if not root: return "" try: path = subprocess.run( [ os.path.join(root, "Microsoft Visual Studio", "Installer", "vswhere.exe"), "-version", f"[{vs_version:.1f}, {vs_version + 1:.1f})", "-prerelease", "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", "-property", "installationPath", "-products", "*", ], encoding="utf-8" if sys.platform.startswith("cygwin") else "mbcs", check=True, stdout=subprocess.PIPE, errors="strict", ).stdout.strip() except (subprocess.CalledProcessError, OSError, UnicodeDecodeError): return "" path = os.path.join(path, "VC", "Auxiliary", "Build") if os.path.isdir(path): return path return ""
[docs] def find_visual_studio(vs_version: int) -> str: """Return Visual Studio installation path associated with ``vs_version`` or an empty string if any. The ``vs_version`` corresponds to the `Visual Studio` version to lookup. See :data:`VS_YEAR_TO_VERSION`. .. note:: - Returns `path` based on the result of invoking ``vswhere.exe``. """ return _find_visual_studio_2017_or_newer(vs_version)
# To avoid multiple slow calls to ``subprocess.run()`` (either directly or # indirectly through ``query_vcvarsall``), results of previous calls are cached. __get_msvc_compiler_env_cache: dict[str, CachedEnv] = {} def _get_msvc_compiler_env(vs_version: int, vs_toolset: str | None = None) -> CachedEnv | dict[str, str]: """ Return a dictionary of environment variables corresponding to ``vs_version`` that can be used with :class:`CMakeVisualStudioCommandLineGenerator`. The ``vs_toolset`` is used only for Visual Studio 2017 or newer (``vs_version >= 15``). If specified, ``vs_toolset`` is used to set the `-vcvars_ver=XX.Y` argument passed to ``vcvarsall.bat`` script. """ # Set architecture vc_arch = ARCH_TO_MSVC_ARCH[_compute_arch()] # If any, return cached version cache_key = ",".join([str(vs_version), vc_arch, str(vs_toolset)]) if cache_key in __get_msvc_compiler_env_cache: return __get_msvc_compiler_env_cache[cache_key] monkey.patch_for_msvc_specialized_compiler() # type: ignore[no-untyped-call] vc_dir = find_visual_studio(vs_version) vcvarsall = os.path.join(vc_dir, "vcvarsall.bat") if not os.path.exists(vcvarsall): return {} # Set vcvars_ver argument based on toolset vcvars_ver = "" if vs_toolset is not None: match = re.findall(r"^v(\d\d)(\d+)$", vs_toolset)[0] if match: match_str = ".".join(match) vcvars_ver = f"-vcvars_ver={match_str}" try: out_bytes = subprocess.run( f'cmd /u /c "{vcvarsall}" {vc_arch} {vcvars_ver} && set', stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=sys.platform.startswith("cygwin"), check=True, ).stdout out = out_bytes.decode("utf-16le", errors="replace") vc_env = { key.lower(): value for key, _, value in (line.partition("=") for line in out.splitlines()) if key and value } cached_env: CachedEnv = { "PATH": vc_env.get("path", ""), "INCLUDE": vc_env.get("include", ""), "LIB": vc_env.get("lib", ""), } __get_msvc_compiler_env_cache[cache_key] = cached_env return cached_env except subprocess.CalledProcessError as exc: print(exc.output.decode("utf-16le", errors="replace"), file=sys.stderr, flush=True) return {}
[docs] class CMakeVisualStudioCommandLineGenerator(CMakeGenerator): """ Represents a command-line CMake generator initialized with a specific `Visual Studio` environment. .. automethod:: __init__ """
[docs] def __init__(self, name: str, year: str, toolset: str | None = None, args: Iterable[str] | None = None): """Instantiate CMake command-line generator. The generator ``name`` can be values like `Ninja`, `NMake Makefiles` or `NMake Makefiles JOM`. The ``year`` defines the `Visual Studio` environment associated with the generator. See :data:`VS_YEAR_TO_VERSION`. If set, the ``toolset`` defines the `Visual Studio Toolset` to select. The platform (32-bit or 64-bit or ARM) is automatically selected. """ arch = _compute_arch() vc_env = _get_msvc_compiler_env(VS_YEAR_TO_VERSION[year], toolset) env = {str(key.upper()): str(value) for key, value in vc_env.items()} super().__init__(name, env, arch=arch, args=args) self._description = f"{self.name} ({CMakeVisualStudioIDEGenerator(year, toolset).description})"