"""This module defines object specific to Windows platform."""
import os
import platform
import re
import subprocess
import sys
import textwrap
from typing import Dict, Iterable, Optional, Union
from setuptools import monkey
from ..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):
PATH: str
INCLUDE: str
LIB: str
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: Optional[str] = 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}"
if platform.machine() == "ARM64":
vs_arch = "ARM64"
elif platform.architecture()[0] == "64bit":
vs_arch = "x64"
else:
vs_arch = "Win32"
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.check_output(
[
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",
errors="strict",
).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.check_output()`` (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: Optional[str] = None) -> Union[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.check_output(
f'cmd /u /c "{vcvarsall}" {vc_arch} {vcvars_ver} && set',
stderr=subprocess.STDOUT,
shell=sys.platform.startswith("cygwin"),
)
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, 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: Optional[str] = None, args: Optional[Iterable[str]] = 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})"