diff options
-rw-r--r-- | appveyor.yml | 9 | ||||
-rw-r--r-- | changelog.d/1904.change.rst | 1 | ||||
-rw-r--r-- | setuptools/msvc.py | 159 | ||||
-rw-r--r-- | setuptools/tests/test_msvc14.py | 84 | ||||
-rw-r--r-- | tox.ini | 2 |
5 files changed, 246 insertions, 9 deletions
diff --git a/appveyor.yml b/appveyor.yml index f7ab22f6..de4e6c66 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,6 +7,15 @@ environment: CODECOV_ENV: APPVEYOR_JOB_NAME matrix: + - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015 + APPVEYOR_JOB_NAME: "python35-x64-vs2015" + PYTHON: "C:\\Python35-x64" + - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 + APPVEYOR_JOB_NAME: "python35-x64-vs2017" + PYTHON: "C:\\Python35-x64" + - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 + APPVEYOR_JOB_NAME: "python35-x64-vs2019" + PYTHON: "C:\\Python35-x64" - APPVEYOR_JOB_NAME: "python36-x64" PYTHON: "C:\\Python36-x64" - APPVEYOR_JOB_NAME: "python37-x64" diff --git a/changelog.d/1904.change.rst b/changelog.d/1904.change.rst new file mode 100644 index 00000000..fa4bbfd9 --- /dev/null +++ b/changelog.d/1904.change.rst @@ -0,0 +1 @@ +Update msvc.py to use CPython 3.8.0 mechanism to find msvc 14+ diff --git a/setuptools/msvc.py b/setuptools/msvc.py index c2cbd1e5..213e39c9 100644 --- a/setuptools/msvc.py +++ b/setuptools/msvc.py @@ -26,6 +26,7 @@ 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 @@ -142,6 +143,154 @@ def msvc9_query_vcvarsall(ver, arch='x86', *args, **kwargs): 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) + ) + + 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 @@ -159,16 +308,10 @@ def msvc14_get_vc_env(plat_spec): dict environment """ - # Try to get environment from vcvarsall.bat (Classical way) - try: - return get_unpatched(msvc14_get_vc_env)(plat_spec) - except distutils.errors.DistutilsPlatformError: - # Pass error Vcvarsall.bat is missing - pass - # If error, try to set environment directly + # Always use backport from CPython 3.8 try: - return EnvironmentInfo(plat_spec, vc_min_ver=14.0).return_env() + return _msvc14_get_vc_env(plat_spec) except distutils.errors.DistutilsPlatformError as exc: _augment_exception(exc, 14.0) raise diff --git a/setuptools/tests/test_msvc14.py b/setuptools/tests/test_msvc14.py new file mode 100644 index 00000000..7833aab4 --- /dev/null +++ b/setuptools/tests/test_msvc14.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +""" +Tests for msvc support module (msvc14 unit tests). +""" + +import os +from distutils.errors import DistutilsPlatformError +import pytest +import sys + + +@pytest.mark.skipif(sys.platform != "win32", + reason="These tests are only for win32") +class TestMSVC14: + """Python 3.8 "distutils/tests/test_msvccompiler.py" backport""" + def test_no_compiler(self): + import setuptools.msvc as _msvccompiler + # makes sure query_vcvarsall raises + # a DistutilsPlatformError if the compiler + # is not found + + def _find_vcvarsall(plat_spec): + return None, None + + old_find_vcvarsall = _msvccompiler._msvc14_find_vcvarsall + _msvccompiler._msvc14_find_vcvarsall = _find_vcvarsall + try: + pytest.raises(DistutilsPlatformError, + _msvccompiler._msvc14_get_vc_env, + 'wont find this version') + finally: + _msvccompiler._msvc14_find_vcvarsall = old_find_vcvarsall + + @pytest.mark.skipif(sys.version_info[0] < 3, + reason="Unicode requires encode/decode on Python 2") + def test_get_vc_env_unicode(self): + import setuptools.msvc as _msvccompiler + + test_var = 'ṰḖṤṪ┅ṼẨṜ' + test_value = '₃⁴₅' + + # Ensure we don't early exit from _get_vc_env + old_distutils_use_sdk = os.environ.pop('DISTUTILS_USE_SDK', None) + os.environ[test_var] = test_value + try: + env = _msvccompiler._msvc14_get_vc_env('x86') + assert test_var.lower() in env + assert test_value == env[test_var.lower()] + finally: + os.environ.pop(test_var) + if old_distutils_use_sdk: + os.environ['DISTUTILS_USE_SDK'] = old_distutils_use_sdk + + def test_get_vc2017(self): + import setuptools.msvc as _msvccompiler + + # This function cannot be mocked, so pass it if we find VS 2017 + # and mark it skipped if we do not. + version, path = _msvccompiler._msvc14_find_vc2017() + if os.environ.get('APPVEYOR_BUILD_WORKER_IMAGE', '') in [ + 'Visual Studio 2017' + ]: + assert version + if version: + assert version >= 15 + assert os.path.isdir(path) + else: + pytest.skip("VS 2017 is not installed") + + def test_get_vc2015(self): + import setuptools.msvc as _msvccompiler + + # This function cannot be mocked, so pass it if we find VS 2015 + # and mark it skipped if we do not. + version, path = _msvccompiler._msvc14_find_vc2015() + if os.environ.get('APPVEYOR_BUILD_WORKER_IMAGE', '') in [ + 'Visual Studio 2015', 'Visual Studio 2017' + ]: + assert version + if version: + assert version >= 14 + assert os.path.isdir(path) + else: + pytest.skip("VS 2015 is not installed") @@ -22,7 +22,7 @@ setenv = COVERAGE_FILE={toxworkdir}/.coverage.{envname} # TODO: The passed environment variables came from copying other tox.ini files # These should probably be individually annotated to explain what needs them. -passenv=APPDATA HOMEDRIVE HOMEPATH windir APPVEYOR APPVEYOR_* CI CODECOV_* TRAVIS TRAVIS_* NETWORK_REQUIRED +passenv=APPDATA HOMEDRIVE HOMEPATH windir Program* CommonProgram* VS* APPVEYOR APPVEYOR_* CI CODECOV_* TRAVIS TRAVIS_* NETWORK_REQUIRED commands=pytest --cov-config={toxinidir}/tox.ini --cov-report= {posargs} usedevelop=True extras = |