Skip to content

Commit 42eb813

Browse files
authored
Merge pull request #1904 from mayeut/msvc-python3.5
Use CPython 3.8.0 mechanism to find msvc 14+
2 parents ccbe2da + 65fa920 commit 42eb813

File tree

5 files changed

+246
-9
lines changed

5 files changed

+246
-9
lines changed

appveyor.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ environment:
77
CODECOV_ENV: APPVEYOR_JOB_NAME
88

99
matrix:
10+
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2015
11+
APPVEYOR_JOB_NAME: "python35-x64-vs2015"
12+
PYTHON: "C:\\Python35-x64"
13+
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
14+
APPVEYOR_JOB_NAME: "python35-x64-vs2017"
15+
PYTHON: "C:\\Python35-x64"
16+
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
17+
APPVEYOR_JOB_NAME: "python35-x64-vs2019"
18+
PYTHON: "C:\\Python35-x64"
1019
- APPVEYOR_JOB_NAME: "python36-x64"
1120
PYTHON: "C:\\Python36-x64"
1221
- APPVEYOR_JOB_NAME: "python37-x64"

changelog.d/1904.change.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Update msvc.py to use CPython 3.8.0 mechanism to find msvc 14+

setuptools/msvc.py

Lines changed: 151 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import sys
2727
import platform
2828
import itertools
29+
import subprocess
2930
import distutils.errors
3031
from setuptools.extern.packaging.version import LegacyVersion
3132

@@ -142,6 +143,154 @@ def msvc9_query_vcvarsall(ver, arch='x86', *args, **kwargs):
142143
raise
143144

144145

146+
def _msvc14_find_vc2015():
147+
"""Python 3.8 "distutils/_msvccompiler.py" backport"""
148+
try:
149+
key = winreg.OpenKey(
150+
winreg.HKEY_LOCAL_MACHINE,
151+
r"Software\Microsoft\VisualStudio\SxS\VC7",
152+
0,
153+
winreg.KEY_READ | winreg.KEY_WOW64_32KEY
154+
)
155+
except OSError:
156+
return None, None
157+
158+
best_version = 0
159+
best_dir = None
160+
with key:
161+
for i in itertools.count():
162+
try:
163+
v, vc_dir, vt = winreg.EnumValue(key, i)
164+
except OSError:
165+
break
166+
if v and vt == winreg.REG_SZ and isdir(vc_dir):
167+
try:
168+
version = int(float(v))
169+
except (ValueError, TypeError):
170+
continue
171+
if version >= 14 and version > best_version:
172+
best_version, best_dir = version, vc_dir
173+
return best_version, best_dir
174+
175+
176+
def _msvc14_find_vc2017():
177+
"""Python 3.8 "distutils/_msvccompiler.py" backport
178+
179+
Returns "15, path" based on the result of invoking vswhere.exe
180+
If no install is found, returns "None, None"
181+
182+
The version is returned to avoid unnecessarily changing the function
183+
result. It may be ignored when the path is not None.
184+
185+
If vswhere.exe is not available, by definition, VS 2017 is not
186+
installed.
187+
"""
188+
root = environ.get("ProgramFiles(x86)") or environ.get("ProgramFiles")
189+
if not root:
190+
return None, None
191+
192+
try:
193+
path = subprocess.check_output([
194+
join(root, "Microsoft Visual Studio", "Installer", "vswhere.exe"),
195+
"-latest",
196+
"-prerelease",
197+
"-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
198+
"-property", "installationPath",
199+
"-products", "*",
200+
]).decode(encoding="mbcs", errors="strict").strip()
201+
except (subprocess.CalledProcessError, OSError, UnicodeDecodeError):
202+
return None, None
203+
204+
path = join(path, "VC", "Auxiliary", "Build")
205+
if isdir(path):
206+
return 15, path
207+
208+
return None, None
209+
210+
211+
PLAT_SPEC_TO_RUNTIME = {
212+
'x86': 'x86',
213+
'x86_amd64': 'x64',
214+
'x86_arm': 'arm',
215+
'x86_arm64': 'arm64'
216+
}
217+
218+
219+
def _msvc14_find_vcvarsall(plat_spec):
220+
"""Python 3.8 "distutils/_msvccompiler.py" backport"""
221+
_, best_dir = _msvc14_find_vc2017()
222+
vcruntime = None
223+
224+
if plat_spec in PLAT_SPEC_TO_RUNTIME:
225+
vcruntime_plat = PLAT_SPEC_TO_RUNTIME[plat_spec]
226+
else:
227+
vcruntime_plat = 'x64' if 'amd64' in plat_spec else 'x86'
228+
229+
if best_dir:
230+
vcredist = join(best_dir, "..", "..", "redist", "MSVC", "**",
231+
vcruntime_plat, "Microsoft.VC14*.CRT",
232+
"vcruntime140.dll")
233+
try:
234+
import glob
235+
vcruntime = glob.glob(vcredist, recursive=True)[-1]
236+
except (ImportError, OSError, LookupError):
237+
vcruntime = None
238+
239+
if not best_dir:
240+
best_version, best_dir = _msvc14_find_vc2015()
241+
if best_version:
242+
vcruntime = join(best_dir, 'redist', vcruntime_plat,
243+
"Microsoft.VC140.CRT", "vcruntime140.dll")
244+
245+
if not best_dir:
246+
return None, None
247+
248+
vcvarsall = join(best_dir, "vcvarsall.bat")
249+
if not isfile(vcvarsall):
250+
return None, None
251+
252+
if not vcruntime or not isfile(vcruntime):
253+
vcruntime = None
254+
255+
return vcvarsall, vcruntime
256+
257+
258+
def _msvc14_get_vc_env(plat_spec):
259+
"""Python 3.8 "distutils/_msvccompiler.py" backport"""
260+
if "DISTUTILS_USE_SDK" in environ:
261+
return {
262+
key.lower(): value
263+
for key, value in environ.items()
264+
}
265+
266+
vcvarsall, vcruntime = _msvc14_find_vcvarsall(plat_spec)
267+
if not vcvarsall:
268+
raise distutils.errors.DistutilsPlatformError(
269+
"Unable to find vcvarsall.bat"
270+
)
271+
272+
try:
273+
out = subprocess.check_output(
274+
'cmd /u /c "{}" {} && set'.format(vcvarsall, plat_spec),
275+
stderr=subprocess.STDOUT,
276+
).decode('utf-16le', errors='replace')
277+
except subprocess.CalledProcessError as exc:
278+
raise distutils.errors.DistutilsPlatformError(
279+
"Error executing {}".format(exc.cmd)
280+
)
281+
282+
env = {
283+
key.lower(): value
284+
for key, _, value in
285+
(line.partition('=') for line in out.splitlines())
286+
if key and value
287+
}
288+
289+
if vcruntime:
290+
env['py_vcruntime_redist'] = vcruntime
291+
return env
292+
293+
145294
def msvc14_get_vc_env(plat_spec):
146295
"""
147296
Patched "distutils._msvccompiler._get_vc_env" for support extra
@@ -159,16 +308,10 @@ def msvc14_get_vc_env(plat_spec):
159308
dict
160309
environment
161310
"""
162-
# Try to get environment from vcvarsall.bat (Classical way)
163-
try:
164-
return get_unpatched(msvc14_get_vc_env)(plat_spec)
165-
except distutils.errors.DistutilsPlatformError:
166-
# Pass error Vcvarsall.bat is missing
167-
pass
168311

169-
# If error, try to set environment directly
312+
# Always use backport from CPython 3.8
170313
try:
171-
return EnvironmentInfo(plat_spec, vc_min_ver=14.0).return_env()
314+
return _msvc14_get_vc_env(plat_spec)
172315
except distutils.errors.DistutilsPlatformError as exc:
173316
_augment_exception(exc, 14.0)
174317
raise

setuptools/tests/test_msvc14.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Tests for msvc support module (msvc14 unit tests).
4+
"""
5+
6+
import os
7+
from distutils.errors import DistutilsPlatformError
8+
import pytest
9+
import sys
10+
11+
12+
@pytest.mark.skipif(sys.platform != "win32",
13+
reason="These tests are only for win32")
14+
class TestMSVC14:
15+
"""Python 3.8 "distutils/tests/test_msvccompiler.py" backport"""
16+
def test_no_compiler(self):
17+
import setuptools.msvc as _msvccompiler
18+
# makes sure query_vcvarsall raises
19+
# a DistutilsPlatformError if the compiler
20+
# is not found
21+
22+
def _find_vcvarsall(plat_spec):
23+
return None, None
24+
25+
old_find_vcvarsall = _msvccompiler._msvc14_find_vcvarsall
26+
_msvccompiler._msvc14_find_vcvarsall = _find_vcvarsall
27+
try:
28+
pytest.raises(DistutilsPlatformError,
29+
_msvccompiler._msvc14_get_vc_env,
30+
'wont find this version')
31+
finally:
32+
_msvccompiler._msvc14_find_vcvarsall = old_find_vcvarsall
33+
34+
@pytest.mark.skipif(sys.version_info[0] < 3,
35+
reason="Unicode requires encode/decode on Python 2")
36+
def test_get_vc_env_unicode(self):
37+
import setuptools.msvc as _msvccompiler
38+
39+
test_var = 'ṰḖṤṪ┅ṼẨṜ'
40+
test_value = '₃⁴₅'
41+
42+
# Ensure we don't early exit from _get_vc_env
43+
old_distutils_use_sdk = os.environ.pop('DISTUTILS_USE_SDK', None)
44+
os.environ[test_var] = test_value
45+
try:
46+
env = _msvccompiler._msvc14_get_vc_env('x86')
47+
assert test_var.lower() in env
48+
assert test_value == env[test_var.lower()]
49+
finally:
50+
os.environ.pop(test_var)
51+
if old_distutils_use_sdk:
52+
os.environ['DISTUTILS_USE_SDK'] = old_distutils_use_sdk
53+
54+
def test_get_vc2017(self):
55+
import setuptools.msvc as _msvccompiler
56+
57+
# This function cannot be mocked, so pass it if we find VS 2017
58+
# and mark it skipped if we do not.
59+
version, path = _msvccompiler._msvc14_find_vc2017()
60+
if os.environ.get('APPVEYOR_BUILD_WORKER_IMAGE', '') in [
61+
'Visual Studio 2017'
62+
]:
63+
assert version
64+
if version:
65+
assert version >= 15
66+
assert os.path.isdir(path)
67+
else:
68+
pytest.skip("VS 2017 is not installed")
69+
70+
def test_get_vc2015(self):
71+
import setuptools.msvc as _msvccompiler
72+
73+
# This function cannot be mocked, so pass it if we find VS 2015
74+
# and mark it skipped if we do not.
75+
version, path = _msvccompiler._msvc14_find_vc2015()
76+
if os.environ.get('APPVEYOR_BUILD_WORKER_IMAGE', '') in [
77+
'Visual Studio 2015', 'Visual Studio 2017'
78+
]:
79+
assert version
80+
if version:
81+
assert version >= 14
82+
assert os.path.isdir(path)
83+
else:
84+
pytest.skip("VS 2015 is not installed")

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ setenv =
2222
COVERAGE_FILE={toxworkdir}/.coverage.{envname}
2323
# TODO: The passed environment variables came from copying other tox.ini files
2424
# These should probably be individually annotated to explain what needs them.
25-
passenv=APPDATA HOMEDRIVE HOMEPATH windir APPVEYOR APPVEYOR_* CI CODECOV_* TRAVIS TRAVIS_* NETWORK_REQUIRED
25+
passenv=APPDATA HOMEDRIVE HOMEPATH windir Program* CommonProgram* VS* APPVEYOR APPVEYOR_* CI CODECOV_* TRAVIS TRAVIS_* NETWORK_REQUIRED
2626
commands=pytest --cov-config={toxinidir}/tox.ini --cov-report= {posargs}
2727
usedevelop=True
2828
extras =

0 commit comments

Comments
 (0)