Skip to content

Use CPython 3.8.0 mechanism to find msvc 14+ #1904

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions changelog.d/1904.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Update msvc.py to use CPython 3.8.0 mechanism to find msvc 14+
159 changes: 151 additions & 8 deletions setuptools/msvc.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import sys
import platform
import itertools
import subprocess
import distutils.errors
from setuptools.extern.packaging.version import LegacyVersion

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
84 changes: 84 additions & 0 deletions setuptools/tests/test_msvc14.py
Original file line number Diff line number Diff line change
@@ -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")
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down