""" Improved support for Microsoft Visual C++ compilers. Known supported compilers: -------------------------- Microsoft Visual C++ 9.0: Microsoft Visual C++ Compiler for Python 2.7 (x86, amd64) Microsoft Windows SDK 6.1 (x86, x64, ia64) Microsoft Windows SDK 7.0 (x86, x64, ia64) Microsoft Visual C++ 10.0: Microsoft Windows SDK 7.1 (x86, x64, ia64) Microsoft Visual C++ 14.X: Microsoft Visual C++ Build Tools 2015 (x86, x64, arm) Microsoft Visual Studio Build Tools 2017 (x86, x64, arm, arm64) Microsoft Visual Studio Build Tools 2019 (x86, x64, arm, arm64) This may also support compilers shipped with compatible Visual Studio versions. """ import json from io import open from os import listdir, pathsep from os.path import join, isfile, isdir, dirname import sys import platform import itertools import subprocess import distutils.errors from setuptools.extern.packaging.version import LegacyVersion from .monkey import get_unpatched if platform.system() == 'Windows': import winreg from os import environ else: # Mock winreg and environ so the module can be imported on this platform. class winreg: HKEY_USERS = None HKEY_CURRENT_USER = None HKEY_LOCAL_MACHINE = None HKEY_CLASSES_ROOT = None environ = dict() _msvc9_suppress_errors = ( # msvc9compiler isn't available on some platforms ImportError, # msvc9compiler raises DistutilsPlatformError in some # environments. See #1118. distutils.errors.DistutilsPlatformError, ) try: from distutils.msvc9compiler import Reg except _msvc9_suppress_errors: pass def msvc9_find_vcvarsall(version): """ Patched "distutils.msvc9compiler.find_vcvarsall" to use the standalone compiler build for Python (VCForPython / Microsoft Visual C++ Compiler for Python 2.7). Fall back to original behavior when the standalone compiler is not available. Redirect the path of "vcvarsall.bat". Parameters ---------- version: float Required Microsoft Visual C++ version. Return ------ str vcvarsall.bat path """ vc_base = r'Software\%sMicrosoft\DevDiv\VCForPython\%0.1f' key = vc_base % ('', version) try: # Per-user installs register the compiler path here productdir = Reg.get_value(key, "installdir") except KeyError: try: # All-user installs on a 64-bit system register here key = vc_base % ('Wow6432Node\\', version) productdir = Reg.get_value(key, "installdir") except KeyError: productdir = None if productdir: vcvarsall = join(productdir, "vcvarsall.bat") if isfile(vcvarsall): return vcvarsall return get_unpatched(msvc9_find_vcvarsall)(version) def msvc9_query_vcvarsall(ver, arch='x86', *args, **kwargs): """ Patched "distutils.msvc9compiler.query_vcvarsall" for support extra Microsoft Visual C++ 9.0 and 10.0 compilers. Set environment without use of "vcvarsall.bat". Parameters ---------- ver: float Required Microsoft Visual C++ version. arch: str Target architecture. Return ------ dict environment """ # Try to get environment from vcvarsall.bat (Classical way) try: orig = get_unpatched(msvc9_query_vcvarsall) return orig(ver, arch, *args, **kwargs) except distutils.errors.DistutilsPlatformError: # Pass error if Vcvarsall.bat is missing pass except ValueError: # Pass error if environment not set after executing vcvarsall.bat pass # If error, try to set environment directly try: return EnvironmentInfo(arch, ver).return_env() except distutils.errors.DistutilsPlatformError as exc: _augment_exception(exc, ver, arch) raise def _msvc14_find_vc2015(): """Python 3.8 "distutils/_msvccompiler.py" backport""" try: key = winreg.OpenKey( winreg.HKEY_LOCAL_MACHINE, r"Software\Microsoft\VisualStudio\SxS\VC7", 0, winreg.KEY_READ | winreg.KEY_WOW64_32KEY ) except OSError: return None, None best_version = 0 best_dir = None with key: for i in itertools.count(): try: v, vc_dir, vt = winreg.EnumValue(key, i) except OSError: break if v and vt == winreg.REG_SZ and isdir(vc_dir): try: version = int(float(v)) except (ValueError, TypeError): continue if version >= 14 and version > best_version: best_version, best_dir = version, vc_dir return best_version, best_dir def _msvc14_find_vc2017(): """Python 3.8 "distutils/_msvccompiler.py" backport Returns "15, path" based on the result of invoking vswhere.exe If no install is found, returns "None, None" The version is returned to avoid unnecessarily changing the function result. It may be ignored when the path is not None. If vswhere.exe is not available, by definition, VS 2017 is not installed. """ root = environ.get("ProgramFiles(x86)") or environ.get("ProgramFiles") if not root: return None, None try: path = subprocess.check_output([ join(root, "Microsoft Visual Studio", "Installer", "vswhere.exe"), "-latest", "-prerelease", "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", "-property", "installationPath", "-products", "*", ]).decode(encoding="mbcs", errors="strict").strip() except (subprocess.CalledProcessError, OSError, UnicodeDecodeError): return None, None path = join(path, "VC", "Auxiliary", "Build") if isdir(path): return 15, path return None, None PLAT_SPEC_TO_RUNTIME = { 'x86': 'x86', 'x86_amd64': 'x64', 'x86_arm': 'arm', 'x86_arm64': 'arm64' } def _msvc14_find_vcvarsall(plat_spec): """Python 3.8 "distutils/_msvccompiler.py" backport""" _, best_dir = _msvc14_find_vc2017() vcruntime = None if plat_spec in PLAT_SPEC_TO_RUNTIME: vcruntime_plat = PLAT_SPEC_TO_RUNTIME[plat_spec] else: vcruntime_plat = 'x64' if 'amd64' in plat_spec else 'x86' if best_dir: vcredist = join(best_dir, "..", "..", "redist", "MSVC", "**", vcruntime_plat, "Microsoft.VC14*.CRT", "vcruntime140.dll") try: import glob vcruntime = glob.glob(vcredist, recursive=True)[-1] except (ImportError, OSError, LookupError): vcruntime = None if not best_dir: best_version, best_dir = _msvc14_find_vc2015() if best_version: vcruntime = join(best_dir, 'redist', vcruntime_plat, "Microsoft.VC140.CRT", "vcruntime140.dll") if not best_dir: return None, None vcvarsall = join(best_dir, "vcvarsall.bat") if not isfile(vcvarsall): return None, None if not vcruntime or not isfile(vcruntime): vcruntime = None return vcvarsall, vcruntime def _msvc14_get_vc_env(plat_spec): """Python 3.8 "distutils/_msvccompiler.py" backport""" if "DISTUTILS_USE_SDK" in environ: return { key.lower(): value for key, value in environ.items() } vcvarsall, vcruntime = _msvc14_find_vcvarsall(plat_spec) if not vcvarsall: raise distutils.errors.DistutilsPlatformError( "Unable to find vcvarsall.bat" ) try: out = subprocess.check_output( 'cmd /u /c "{}" {} && set'.format(vcvarsall, plat_spec), stderr=subprocess.STDOUT, ).decode('utf-16le', errors='replace') except subprocess.CalledProcessError as exc: raise distutils.errors.DistutilsPlatformError( "Error executing {}".format(exc.cmd) ) from exc env = { key.lower(): value for key, _, value in (line.partition('=') for line in out.splitlines()) if key and value } if vcruntime: env['py_vcruntime_redist'] = vcruntime return env def msvc14_get_vc_env(plat_spec): """ Patched "distutils._msvccompiler._get_vc_env" for support extra Microsoft Visual C++ 14.X compilers. Set environment without use of "vcvarsall.bat". Parameters ---------- plat_spec: str Target architecture. Return ------ dict environment """ # Always use backport from CPython 3.8 try: return _msvc14_get_vc_env(plat_spec) except distutils.errors.DistutilsPlatformError as exc: _augment_exception(exc, 14.0) raise def msvc14_gen_lib_options(*args, **kwargs): """ Patched "distutils._msvccompiler.gen_lib_options" for fix compatibility between "numpy.distutils" and "distutils._msvccompiler" (for Numpy < 1.11.2) """ if "numpy.distutils" in sys.modules: import numpy as np if LegacyVersion(np.__version__) < LegacyVersion('1.11.2'): return np.distutils.ccompiler.gen_lib_options(*args, **kwargs) return get_unpatched(msvc14_gen_lib_options)(*args, **kwargs) def _augment_exception(exc, version, arch=''): """ Add details to the exception message to help guide the user as to what action will resolve it. """ # Error if MSVC++ directory not found or environment not set message = exc.args[0] if "vcvarsall" in message.lower() or "visual c" in message.lower(): # Special error message if MSVC++ not installed tmpl = 'Microsoft Visual C++ {version:0.1f} or greater is required.' message = tmpl.format(**locals()) msdownload = 'www.microsoft.com/download/details.aspx?id=%d' if version == 9.0: if arch.lower().find('ia64') > -1: # For VC++ 9.0, if IA64 support is needed, redirect user # to Windows SDK 7.0. # Note: No download link available from Microsoft. message += ' Get it with "Microsoft Windows SDK 7.0"' else: # For VC++ 9.0 redirect user to Vc++ for Python 2.7 : # This redirection link is maintained by Microsoft. # Contact vspython@microsoft.com if it needs updating. message += ' Get it from http://aka.ms/vcpython27' elif version == 10.0: # For VC++ 10.0 Redirect user to Windows SDK 7.1 message += ' Get it with "Microsoft Windows SDK 7.1": ' message += msdownload % 8279 elif version >= 14.0: # For VC++ 14.X Redirect user to latest Visual C++ Build Tools message += (' Get it with "Microsoft C++ Build Tools": ' r'https://visualstudio.microsoft.com' r'/visual-cpp-build-tools/') exc.args = (message, ) class PlatformInfo: """ Current and Target Architectures information. Parameters ---------- arch: str Target architecture. """ current_cpu = environ.get('processor_architecture', '').lower() def __init__(self, arch): self.arch = arch.lower().replace('x64', 'amd64') @property def target_cpu(self): """ Return Target CPU architecture. Return ------ str Target CPU """ return self.arch[self.arch.find('_') + 1:] def target_is_x86(self): """ Return True if target CPU is x86 32 bits.. Return ------ bool CPU is x86 32 bits """ return self.target_cpu == 'x86' def current_is_x86(self): """ Return True if current CPU is x86 32 bits.. Return ------ bool CPU is x86 32 bits """ return self.current_cpu == 'x86' def current_dir(self, hidex86=False, x64=False): """ Current platform specific subfolder. Parameters ---------- hidex86: bool return '' and not '\x86' if architecture is x86. x64: bool return '\x64' and not '\amd64' if architecture is amd64. Return ------ str subfolder: '\target', or '' (see hidex86 parameter) """ return ( '' if (self.current_cpu == 'x86' and hidex86) else r'\x64' if (self.current_cpu == 'amd64' and x64) else r'\%s' % self.current_cpu ) def target_dir(self, hidex86=False, x64=False): r""" Target platform specific subfolder. Parameters ---------- hidex86: bool return '' and not '\x86' if architecture is x86. x64: bool return '\x64' and not '\amd64' if architecture is amd64. Return ------ str subfolder: '\current', or '' (see hidex86 parameter) """ return ( '' if (self.target_cpu == 'x86' and hidex86) else r'\x64' if (self.target_cpu == 'amd64' and x64) else r'\%s' % self.target_cpu ) def cross_dir(self, forcex86=False): r""" Cross platform specific subfolder. Parameters ---------- forcex86: bool Use 'x86' as current architecture even if current architecture is not x86. Return ------ str subfolder: '' if target architecture is current architecture, '\current_target' if not. """ current = 'x86' if forcex86 else self.current_cpu return ( '' if self.target_cpu == current else self.target_dir().replace('\\', '\\%s_' % current) ) class RegistryInfo: """ Microsoft Visual Studio related registry information. Parameters ---------- platform_info: PlatformInfo "PlatformInfo" instance. """ HKEYS = (winreg.HKEY_USERS, winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CLASSES_ROOT) def __init__(self, platform_info): self.pi = platform_info @property def visualstudio(self): """ Microsoft Visual Studio root registry key. Return ------ str Registry key """ return 'VisualStudio' @property def sxs(self): """ Microsoft Visual Studio SxS registry key. Return ------ str Registry key """ return join(self.visualstudio, 'SxS') @property def vc(self): """ Microsoft Visual C++ VC7 registry key. Return ------ str Registry key """ return join(self.sxs, 'VC7') @property def vs(self): """ Microsoft Visual Studio VS7 registry key. Return ------ str Registry key """ return join(self.sxs, 'VS7') @property def vc_for_python(self): """ Microsoft Visual C++ for Python registry key. Return ------ str Registry key """ return r'DevDiv\VCForPython' @property def microsoft_sdk(self): """ Microsoft SDK registry key. Return ------ str Registry key """ return 'Microsoft SDKs' @property def windows_sdk(self): """ Microsoft Windows/Platform SDK registry key. Return ------ str Registry key """ return join(self.microsoft_sdk, 'Windows') @property def netfx_sdk(self): """ Microsoft .NET Framework SDK registry key. Return ------ str Registry key """ return join(self.microsoft_sdk, 'NETFXSDK') @property def windows_kits_roots(self): """ Microsoft Windows Kits Roots registry key. Return ------ str Registry key """ return r'Windows Kits\Installed Roots' def microsoft(self, key, x86=False): """ Return key in Microsoft software registry. Parameters ---------- key: str Registry key path where look. x86: str Force x86 software registry. Return ------ str Registry key """ node64 = '' if self.pi.current_is_x86() or x86 else 'Wow6432Node' return join('Software', node64, 'Microsoft', key) def lookup(self, key, name): """ Look for values in registry in Microsoft software registry. Parameters ---------- key: str Registry key path where look. name: str Value name to find. Return ------ str value """ key_read = winreg.KEY_READ openkey = winreg.OpenKey closekey = winreg.CloseKey ms = self.microsoft for hkey in self.HKEYS: bkey = None try: bkey = openkey(hkey, ms(key), 0, key_read) except (OSError, IOError): if not self.pi.current_is_x86(): try: bkey = openkey(hkey, ms(key, True), 0, key_read) except (OSError, IOError): continue else: continue try: return winreg.QueryValueEx(bkey, name)[0] except (OSError, IOError): pass finally: if bkey: closekey(bkey) class SystemInfo: """ Microsoft Windows and Visual Studio related system information. Parameters ---------- registry_info: RegistryInfo "RegistryInfo" instance. vc_ver: float Required Microsoft Visual C++ version. """ # Variables and properties in this class use originals CamelCase variables # names from Microsoft source files for more easy comparison. WinDir = environ.get('WinDir', '') ProgramFiles = environ.get('ProgramFiles', '') ProgramFilesx86 = environ.get('ProgramFiles(x86)', ProgramFiles) def __init__(self, registry_info, vc_ver=None): self.ri = registry_info self.pi = self.ri.pi self.known_vs_paths = self.find_programdata_vs_vers() # Except for VS15+, VC version is aligned with VS version self.vs_ver = self.vc_ver = ( vc_ver or self._find_latest_available_vs_ver()) def _find_latest_available_vs_ver(self): """ Find the latest VC version Return ------ float version """ reg_vc_vers = self.find_reg_vs_vers() if not (reg_vc_vers or self.known_vs_paths): raise distutils.errors.DistutilsPlatformError( 'No Microsoft Visual C++ version found') vc_vers = set(reg_vc_vers) vc_vers.update(self.known_vs_paths) return sorted(vc_vers)[-1] def find_reg_vs_vers(self): """ Find Microsoft Visual Studio versions available in registry. Return ------ list of float Versions """ ms = self.ri.microsoft vckeys = (self.ri.vc, self.ri.vc_for_python, self.ri.vs) vs_vers = [] for hkey in self.ri.HKEYS: for key in vckeys: try: bkey = winreg.OpenKey(hkey, ms(key), 0, winreg.KEY_READ) except (OSError, IOError): continue with bkey: subkeys, values, _ = winreg.QueryInfoKey(bkey) for i in range(values): try: ver = float(winreg.EnumValue(bkey, i)[0]) if ver not in vs_vers: vs_vers.append(ver) except ValueError: pass for i in range(subkeys): try: ver = float(winreg.EnumKey(bkey, i)) if ver not in vs_vers: vs_vers.append(ver) except ValueError: pass return sorted(vs_vers) def find_programdata_vs_vers(self): r""" Find Visual studio 2017+ versions from information in "C:\ProgramData\Microsoft\VisualStudio\Packages\_Instances". Return ------ dict float version as key, path as value. """ vs_versions = {} instances_dir = \ r'C:\ProgramData\Microsoft\VisualStudio\Packages\_Instances' try: hashed_names = listdir(instances_dir) except (OSError, IOError): # Directory not exists with all Visual Studio versions return vs_versions for name in hashed_names: try: # Get VS installation path from "state.json" file state_path = join(instances_dir, name, 'state.json') with open(state_path, 'rt', encoding='utf-8') as state_file: state = json.load(state_file) vs_path = state['installationPath'] # Raises OSError if this VS installation does not contain VC listdir(join(vs_path, r'VC\Tools\MSVC')) # Store version and path vs_versions[self._as_float_version( state['installationVersion'])] = vs_path except (OSError, IOError, KeyError): # Skip if "state.json" file is missing or bad format continue return vs_versions @staticmethod def _as_float_version(version): """ Return a string version as a simplified float version (major.minor) Parameters ---------- version: str Version. Return ------ float version """ return float('.'.join(version.split('.')[:2])) @property def VSInstallDir(self): """ Microsoft Visual Studio directory. Return ------ str path """ # Default path default = join(self.ProgramFilesx86, 'Microsoft Visual Studio %0.1f' % self.vs_ver) # Try to get path from registry, if fail use default path return self.ri.lookup(self.ri.vs, '%0.1f' % self.vs_ver) or default @property def VCInstallDir(self): """ Microsoft Visual C++ directory. Return ------ str path """ path = self._guess_vc() or self._guess_vc_legacy() if not isdir(path): msg = 'Microsoft Visual C++ directory not found' raise distutils.errors.DistutilsPlatformError(msg) return path def _guess_vc(self): """ Locate Visual C++ for VS2017+. Return ------ str path """ if self.vs_ver <= 14.0: return '' try: # First search in known VS paths vs_dir = self.known_vs_paths[self.vs_ver] except KeyError: # Else, search with path from registry vs_dir = self.VSInstallDir guess_vc = join(vs_dir, r'VC\Tools\MSVC') # Subdir with VC exact version as name try: # Update the VC version with real one instead of VS version vc_ver = listdir(guess_vc)[-1] self.vc_ver = self._as_float_version(vc_ver) return join(guess_vc, vc_ver) except (OSError, IOError, IndexError): return '' def _guess_vc_legacy(self): """ Locate Visual C++ for versions prior to 2017. Return ------ str path """ default = join(self.ProgramFilesx86, r'Microsoft Visual Studio %0.1f\VC' % self.vs_ver) # Try to get "VC++ for Python" path from registry as default path reg_path = join(self.ri.vc_for_python, '%0.1f' % self.vs_ver) python_vc = self.ri.lookup(reg_path, 'installdir') default_vc = join(python_vc, 'VC') if python_vc else default # Try to get path from registry, if fail use default path return self.ri.lookup(self.ri.vc, '%0.1f' % self.vs_ver) or default_vc @property def WindowsSdkVersion(self): """ Microsoft Windows SDK versions for specified MSVC++ version. Return ------ tuple of str versions """ if self.vs_ver <= 9.0: return '7.0', '6.1', '6.0a' elif self.vs_ver == 10.0: return '7.1', '7.0a' elif self.vs_ver == 11.0: return '8.0', '8.0a' elif self.vs_ver == 12.0: return '8.1', '8.1a' elif self.vs_ver >= 14.0: return '10.0', '8.1' @property def WindowsSdkLastVersion(self): """ Microsoft Windows SDK last version. Return ------ str version """ return self._use_last_dir_name(join(self.WindowsSdkDir, 'lib')) @property def WindowsSdkDir(self): """ Microsoft Windows SDK directory. Return ------ str path """ sdkdir = '' for ver in self.WindowsSdkVersion: # Try to get it from registry loc = join(self.ri.windows_sdk, 'v%s' % ver) sdkdir = self.ri.lookup(loc, 'installationfolder') if sdkdir: break if not sdkdir or not isdir(sdkdir): # Try to get "VC++ for Python" version from registry path = join(self.ri.vc_for_python, '%0.1f' % self.vc_ver) install_base = self.ri.lookup(path, 'installdir') if install_base: sdkdir = join(install_base, 'WinSDK') if not sdkdir or not isdir(sdkdir): # If fail, use default new path for ver in self.WindowsSdkVersion: intver = ver[:ver.rfind('.')] path = r'Microsoft SDKs\Windows Kits\%s' % intver d = join(self.ProgramFiles, path) if isdir(d): sdkdir = d if not sdkdir or not isdir(sdkdir): # If fail, use default old path for ver in self.WindowsSdkVersion: path = r'Microsoft SDKs\Windows\v%s' % ver d = join(self.ProgramFiles, path) if isdir(d): sdkdir = d if not sdkdir: # If fail, use Platform SDK sdkdir = join(self.VCInstallDir, 'PlatformSDK') return sdkdir @property def WindowsSDKExecutablePath(self): """ Microsoft Windows SDK executable directory. Return ------ str path """ # Find WinSDK NetFx Tools registry dir name if self.vs_ver <= 11.0: netfxver = 35 arch = '' else: netfxver = 40 hidex86 = True if self.vs_ver <= 12.0 else False arch = self.pi.current_dir(x64=True, hidex86=hidex86) fx = 'WinSDK-NetFx%dTools%s' % (netfxver, arch.replace('\\', '-')) # list all possibles registry paths regpaths = [] if self.vs_ver >= 14.0: for ver in self.NetFxSdkVersion: regpaths += [join(self.ri.netfx_sdk, ver, fx)] for ver in self.WindowsSdkVersion: regpaths += [join(self.ri.windows_sdk, 'v%sA' % ver, fx)] # Return installation folder from the more recent path for path in regpaths: execpath = self.ri.lookup(path, 'installationfolder') if execpath: return execpath @property def FSharpInstallDir(self): """ Microsoft Visual F# directory. Return ------ str path """ path = join(self.ri.visualstudio, r'%0.1f\Setup\F#' % self.vs_ver) return self.ri.lookup(path, 'productdir') or '' @property def UniversalCRTSdkDir(self): """ Microsoft Universal CRT SDK directory. Return ------ str path """ # Set Kit Roots versions for specified MSVC++ version vers = ('10', '81') if self.vs_ver >= 14.0 else () # Find path of the more recent Kit for ver in vers: sdkdir = self.ri.lookup(self.ri.windows_kits_roots, 'kitsroot%s' % ver) if sdkdir: return sdkdir or '' @property def UniversalCRTSdkLastVersion(self): """ Microsoft Universal C Runtime SDK last version. Return ------ str version """ return self._use_last_dir_name(join(self.UniversalCRTSdkDir, 'lib')) @property def NetFxSdkVersion(self): """ Microsoft .NET Framework SDK versions. Return ------ tuple of str versions """ # Set FxSdk versions for specified VS version return (('4.7.2', '4.7.1', '4.7', '4.6.2', '4.6.1', '4.6', '4.5.2', '4.5.1', '4.5') if self.vs_ver >= 14.0 else ()) @property def NetFxSdkDir(self): """ Microsoft .NET Framework SDK directory. Return ------ str path """ sdkdir = '' for ver in self.NetFxSdkVersion: loc = join(self.ri.netfx_sdk, ver) sdkdir = self.ri.lookup(loc, 'kitsinstallationfolder') if sdkdir: break return sdkdir @property def FrameworkDir32(self): """ Microsoft .NET Framework 32bit directory. Return ------ str path """ # Default path guess_fw = join(self.WinDir, r'Microsoft.NET\Framework') # Try to get path from registry, if fail use default path return self.ri.lookup(self.ri.vc, 'frameworkdir32') or guess_fw @property def FrameworkDir64(self): """ Microsoft .NET Framework 64bit directory. Return ------ str path """ # Default path guess_fw = join(self.WinDir, r'Microsoft.NET\Framework64') # Try to get path from registry, if fail use default path return self.ri.lookup(self.ri.vc, 'frameworkdir64') or guess_fw @property def FrameworkVersion32(self): """ Microsoft .NET Framework 32bit versions. Return ------ tuple of str versions """ return self._find_dot_net_versions(32) @property def FrameworkVersion64(self): """ Microsoft .NET Framework 64bit versions. Return ------ tuple of str versions """ return self._find_dot_net_versions(64) def _find_dot_net_versions(self, bits): """ Find Microsoft .NET Framework versions. Parameters ---------- bits: int Platform number of bits: 32 or 64. Return ------ tuple of str versions """ # Find actual .NET version in registry reg_ver = self.ri.lookup(self.ri.vc, 'frameworkver%d' % bits) dot_net_dir = getattr(self, 'FrameworkDir%d' % bits) ver = reg_ver or self._use_last_dir_name(dot_net_dir, 'v') or '' # Set .NET versions for specified MSVC++ version if self.vs_ver >= 12.0: return ver, 'v4.0' elif self.vs_ver >= 10.0: return 'v4.0.30319' if ver.lower()[:2] != 'v4' else ver, 'v3.5' elif self.vs_ver == 9.0: return 'v3.5', 'v2.0.50727' elif self.vs_ver == 8.0: return 'v3.0', 'v2.0.50727' @staticmethod def _use_last_dir_name(path, prefix=''): """ Return name of the last dir in path or '' if no dir found. Parameters ---------- path: str Use dirs in this path prefix: str Use only dirs starting by this prefix Return ------ str name """ matching_dirs = ( dir_name for dir_name in reversed(listdir(path)) if isdir(join(path, dir_name)) and dir_name.startswith(prefix) ) return next(matching_dirs, None) or '' class EnvironmentInfo: """ Return environment variables for specified Microsoft Visual C++ version and platform : Lib, Include, Path and libpath. This function is compatible with Microsoft Visual C++ 9.0 to 14.X. Script created by analysing Microsoft environment configuration files like "vcvars[...].bat", "SetEnv.Cmd", "vcbuildtools.bat", ... Parameters ---------- arch: str Target architecture. vc_ver: float Required Microsoft Visual C++ version. If not set, autodetect the last version. vc_min_ver: float Minimum Microsoft Visual C++ version. """ # Variables and properties in this class use originals CamelCase variables # names from Microsoft source files for more easy comparison. def __init__(self, arch, vc_ver=None, vc_min_ver=0): self.pi = PlatformInfo(arch) self.ri = RegistryInfo(self.pi) self.si = SystemInfo(self.ri, vc_ver) if self.vc_ver < vc_min_ver: err = 'No suitable Microsoft Visual C++ version found' raise distutils.errors.DistutilsPlatformError(err) @property def vs_ver(self): """ Microsoft Visual Studio. Return ------ float version """ return self.si.vs_ver @property def vc_ver(self): """ Microsoft Visual C++ version. Return ------ float version """ return self.si.vc_ver @property def VSTools(self): """ Microsoft Visual Studio Tools. Return ------ list of str paths """ paths = [r'Common7\IDE', r'Common7\Tools'] if self.vs_ver >= 14.0: arch_subdir = self.pi.current_dir(hidex86=True, x64=True) paths += [r'Common7\IDE\CommonExtensions\Microsoft\TestWindow'] paths += [r'Team Tools\Performance Tools'] paths += [r'Team Tools\Performance Tools%s' % arch_subdir] return [join(self.si.VSInstallDir, path) for path in paths] @property def VCIncludes(self): """ Microsoft Visual C++ & Microsoft Foundation Class Includes. Return ------ list of str paths """ return [join(self.si.VCInstallDir, 'Include'), join(self.si.VCInstallDir, r'ATLMFC\Include')] @property def VCLibraries(self): """ Microsoft Visual C++ & Microsoft Foundation Class Libraries. Return ------ list of str paths """ if self.vs_ver >= 15.0: arch_subdir = self.pi.target_dir(x64=True) else: arch_subdir = self.pi.target_dir(hidex86=True) paths = ['Lib%s' % arch_subdir, r'ATLMFC\Lib%s' % arch_subdir] if self.vs_ver >= 14.0: paths += [r'Lib\store%s' % arch_subdir] return [join(self.si.VCInstallDir, path) for path in paths] @property def VCStoreRefs(self): """ Microsoft Visual C++ store references Libraries. Return ------ list of str paths """ if self.vs_ver < 14.0: return [] return [join(self.si.VCInstallDir, r'Lib\store\references')] @property def VCTools(self): """ Microsoft Visual C++ Tools. Return ------ list of str paths """ si = self.si tools = [join(si.VCInstallDir, 'VCPackages')] forcex86 = True if self.vs_ver <= 10.0 else False arch_subdir = self.pi.cross_dir(forcex86) if arch_subdir: tools += [join(si.VCInstallDir, 'Bin%s' % arch_subdir)] if self.vs_ver == 14.0: path = 'Bin%s' % self.pi.current_dir(hidex86=True) tools += [join(si.VCInstallDir, path)] elif self.vs_ver >= 15.0: host_dir = (r'bin\HostX86%s' if self.pi.current_is_x86() else r'bin\HostX64%s') tools += [join( si.VCInstallDir, host_dir % self.pi.target_dir(x64=True))] if self.pi.current_cpu != self.pi.target_cpu: tools += [join( si.VCInstallDir, host_dir % self.pi.current_dir(x64=True))] else: tools += [join(si.VCInstallDir, 'Bin')] return tools @property def OSLibraries(self): """ Microsoft Windows SDK Libraries. Return ------ list of str paths """ if self.vs_ver <= 10.0: arch_subdir = self.pi.target_dir(hidex86=True, x64=True) return [join(self.si.WindowsSdkDir, 'Lib%s' % arch_subdir)] else: arch_subdir = self.pi.target_dir(x64=True) lib = join(self.si.WindowsSdkDir, 'lib') libver = self._sdk_subdir return [join(lib, '%sum%s' % (libver, arch_subdir))] @property def OSIncludes(self): """ Microsoft Windows SDK Include. Return ------ list of str paths """ include = join(self.si.WindowsSdkDir, 'include') if self.vs_ver <= 10.0: return [include, join(include, 'gl')] else: if self.vs_ver >= 14.0: sdkver = self._sdk_subdir else: sdkver = '' return [join(include, '%sshared' % sdkver), join(include, '%sum' % sdkver), join(include, '%swinrt' % sdkver)] @property def OSLibpath(self): """ Microsoft Windows SDK Libraries Paths. Return ------ list of str paths """ ref = join(self.si.WindowsSdkDir, 'References') libpath = [] if self.vs_ver <= 9.0: libpath += self.OSLibraries if self.vs_ver >= 11.0: libpath += [join(ref, r'CommonConfiguration\Neutral')] if self.vs_ver >= 14.0: libpath += [ ref, join(self.si.WindowsSdkDir, 'UnionMetadata'), join( ref, 'Windows.Foundation.UniversalApiContract', '1.0.0.0'), join(ref, 'Windows.Foundation.FoundationContract', '1.0.0.0'), join( ref, 'Windows.Networking.Connectivity.WwanContract', '1.0.0.0'), join( self.si.WindowsSdkDir, 'ExtensionSDKs', 'Microsoft.VCLibs', '%0.1f' % self.vs_ver, 'References', 'CommonConfiguration', 'neutral'), ] return libpath @property def SdkTools(self): """ Microsoft Windows SDK Tools. Return ------ list of str paths """ return list(self._sdk_tools()) def _sdk_tools(self): """ Microsoft Windows SDK Tools paths generator. Return ------ generator of str paths """ if self.vs_ver < 15.0: bin_dir = 'Bin' if self.vs_ver <= 11.0 else r'Bin\x86' yield join(self.si.WindowsSdkDir, bin_dir) if not self.pi.current_is_x86(): arch_subdir = self.pi.current_dir(x64=True) path = 'Bin%s' % arch_subdir yield join(self.si.WindowsSdkDir, path) if self.vs_ver in (10.0, 11.0): if self.pi.target_is_x86(): arch_subdir = '' else: arch_subdir = self.pi.current_dir(hidex86=True, x64=True) path = r'Bin\NETFX 4.0 Tools%s' % arch_subdir yield join(self.si.WindowsSdkDir, path) elif self.vs_ver >= 15.0: path = join(self.si.WindowsSdkDir, 'Bin') arch_subdir = self.pi.current_dir(x64=True) sdkver = self.si.WindowsSdkLastVersion yield join(path, '%s%s' % (sdkver, arch_subdir)) if self.si.WindowsSDKExecutablePath: yield self.si.WindowsSDKExecutablePath @property def _sdk_subdir(self): """ Microsoft Windows SDK version subdir. Return ------ str subdir """ ucrtver = self.si.WindowsSdkLastVersion return ('%s\\' % ucrtver) if ucrtver else '' @property def SdkSetup(self): """ Microsoft Windows SDK Setup. Return ------ list of str paths """ if self.vs_ver > 9.0: return [] return [join(self.si.WindowsSdkDir, 'Setup')] @property def FxTools(self): """ Microsoft .NET Framework Tools. Return ------ list of str paths """ pi = self.pi si = self.si if self.vs_ver <= 10.0: include32 = True include64 = not pi.target_is_x86() and not pi.current_is_x86() else: include32 = pi.target_is_x86() or pi.current_is_x86() include64 = pi.current_cpu == 'amd64' or pi.target_cpu == 'amd64' tools = [] if include32: tools += [join(si.FrameworkDir32, ver) for ver in si.FrameworkVersion32] if include64: tools += [join(si.FrameworkDir64, ver) for ver in si.FrameworkVersion64] return tools @property def NetFxSDKLibraries(self): """ Microsoft .Net Framework SDK Libraries. Return ------ list of str paths """ if self.vs_ver < 14.0 or not self.si.NetFxSdkDir: return [] arch_subdir = self.pi.target_dir(x64=True) return [join(self.si.NetFxSdkDir, r'lib\um%s' % arch_subdir)] @property def NetFxSDKIncludes(self): """ Microsoft .Net Framework SDK Includes. Return ------ list of str paths """ if self.vs_ver < 14.0 or not self.si.NetFxSdkDir: return [] return [join(self.si.NetFxSdkDir, r'include\um')] @property def VsTDb(self): """ Microsoft Visual Studio Team System Database. Return ------ list of str paths """ return [join(self.si.VSInstallDir, r'VSTSDB\Deploy')] @property def MSBuild(self): """ Microsoft Build Engine. Return ------ list of str paths """ if self.vs_ver < 12.0: return [] elif self.vs_ver < 15.0: base_path = self.si.ProgramFilesx86 arch_subdir = self.pi.current_dir(hidex86=True) else: base_path = self.si.VSInstallDir arch_subdir = '' path = r'MSBuild\%0.1f\bin%s' % (self.vs_ver, arch_subdir) build = [join(base_path, path)] if self.vs_ver >= 15.0: # Add Roslyn C# & Visual Basic Compiler build += [join(base_path, path, 'Roslyn')] return build @property def HTMLHelpWorkshop(self): """ Microsoft HTML Help Workshop. Return ------ list of str paths """ if self.vs_ver < 11.0: return [] return [join(self.si.ProgramFilesx86, 'HTML Help Workshop')] @property def UCRTLibraries(self): """ Microsoft Universal C Runtime SDK Libraries. Return ------ list of str paths """ if self.vs_ver < 14.0: return [] arch_subdir = self.pi.target_dir(x64=True) lib = join(self.si.UniversalCRTSdkDir, 'lib') ucrtver = self._ucrt_subdir return [join(lib, '%sucrt%s' % (ucrtver, arch_subdir))] @property def UCRTIncludes(self): """ Microsoft Universal C Runtime SDK Include. Return ------ list of str paths """ if self.vs_ver < 14.0: return [] include = join(self.si.UniversalCRTSdkDir, 'include') return [join(include, '%sucrt' % self._ucrt_subdir)] @property def _ucrt_subdir(self): """ Microsoft Universal C Runtime SDK version subdir. Return ------ str subdir """ ucrtver = self.si.UniversalCRTSdkLastVersion return ('%s\\' % ucrtver) if ucrtver else '' @property def FSharp(self): """ Microsoft Visual F#. Return ------ list of str paths """ if 11.0 > self.vs_ver > 12.0: return [] return [self.si.FSharpInstallDir] @property def VCRuntimeRedist(self): """ Microsoft Visual C++ runtime redistributable dll. Return ------ str path """ vcruntime = 'vcruntime%d0.dll' % self.vc_ver arch_subdir = self.pi.target_dir(x64=True).strip('\\') # Installation prefixes candidates prefixes = [] tools_path = self.si.VCInstallDir redist_path = dirname(tools_path.replace(r'\Tools', r'\Redist')) if isdir(redist_path): # Redist version may not be exactly the same as tools redist_path = join(redist_path, listdir(redist_path)[-1]) prefixes += [redist_path, join(redist_path, 'onecore')] prefixes += [join(tools_path, 'redist')] # VS14 legacy path # CRT directory crt_dirs = ('Microsoft.VC%d.CRT' % (self.vc_ver * 10), # Sometime store in directory with VS version instead of VC 'Microsoft.VC%d.CRT' % (int(self.vs_ver) * 10)) # vcruntime path for prefix, crt_dir in itertools.product(prefixes, crt_dirs): path = join(prefix, arch_subdir, crt_dir, vcruntime) if isfile(path): return path def return_env(self, exists=True): """ Return environment dict. Parameters ---------- exists: bool It True, only return existing paths. Return ------ dict environment """ env = dict( include=self._build_paths('include', [self.VCIncludes, self.OSIncludes, self.UCRTIncludes, self.NetFxSDKIncludes], exists), lib=self._build_paths('lib', [self.VCLibraries, self.OSLibraries, self.FxTools, self.UCRTLibraries, self.NetFxSDKLibraries], exists), libpath=self._build_paths('libpath', [self.VCLibraries, self.FxTools, self.VCStoreRefs, self.OSLibpath], exists), path=self._build_paths('path', [self.VCTools, self.VSTools, self.VsTDb, self.SdkTools, self.SdkSetup, self.FxTools, self.MSBuild, self.HTMLHelpWorkshop, self.FSharp], exists), ) if self.vs_ver >= 14 and isfile(self.VCRuntimeRedist): env['py_vcruntime_redist'] = self.VCRuntimeRedist return env def _build_paths(self, name, spec_path_lists, exists): """ Given an environment variable name and specified paths, return a pathsep-separated string of paths containing unique, extant, directories from those paths and from the environment variable. Raise an error if no paths are resolved. Parameters ---------- name: str Environment variable name spec_path_lists: list of str Paths exists: bool It True, only return existing paths. Return ------ str Pathsep-separated paths """ # flatten spec_path_lists spec_paths = itertools.chain.from_iterable(spec_path_lists) env_paths = environ.get(name, '').split(pathsep) paths = itertools.chain(spec_paths, env_paths) extant_paths = list(filter(isdir, paths)) if exists else paths if not extant_paths: msg = "%s environment variable is empty" % name.upper() raise distutils.errors.DistutilsPlatformError(msg) unique_paths = self._unique_everseen(extant_paths) return pathsep.join(unique_paths) # from Python docs @staticmethod def _unique_everseen(iterable, key=None): """ List unique elements, preserving order. Remember all elements ever seen. _unique_everseen('AAAABBBCCDAABBB') --> A B C D _unique_everseen('ABBCcAD', str.lower) --> A B C D """ seen = set() seen_add = seen.add if key is None: for element in itertools.filterfalse(seen.__contains__, iterable): seen_add(element) yield element else: for element in iterable: k = key(element) if k not in seen: seen_add(k) yield element