diff options
author | Steve Dower <steve.dower@microsoft.com> | 2014-09-25 08:46:53 -0700 |
---|---|---|
committer | Steve Dower <steve.dower@microsoft.com> | 2014-09-25 08:46:53 -0700 |
commit | b0f93f3bdd1d67cce1dd03d4a6ecea8256fa4e55 (patch) | |
tree | 039f9235f98626c0a63c2db71a3b62837e052477 | |
parent | d42aea783a347d431eaf293250bab60c05b4c779 (diff) | |
download | external_python_setuptools-b0f93f3bdd1d67cce1dd03d4a6ecea8256fa4e55.tar.gz external_python_setuptools-b0f93f3bdd1d67cce1dd03d4a6ecea8256fa4e55.tar.bz2 external_python_setuptools-b0f93f3bdd1d67cce1dd03d4a6ecea8256fa4e55.zip |
Adds monkeypatching for msvc9compiler.find_vcvarsall() to look for a standalone compiler installation and improves the error message for missing VC installation.
-rw-r--r-- | setuptools/extension.py | 54 | ||||
-rw-r--r-- | setuptools/tests/test_msvc9compiler.py | 135 |
2 files changed, 189 insertions, 0 deletions
diff --git a/setuptools/extension.py b/setuptools/extension.py index ab5908da..41a817b7 100644 --- a/setuptools/extension.py +++ b/setuptools/extension.py @@ -2,12 +2,66 @@ import sys import re import functools import distutils.core +import distutils.errors import distutils.extension +import distutils.msvc9compiler from setuptools.dist import _get_unpatched _Extension = _get_unpatched(distutils.core.Extension) +def _patch_msvc9compiler_find_vcvarsall(): + """ + Looks for the standalone VC for Python before falling back on + distutils's original approach. + """ + VC_BASE = r'Software\%sMicrosoft\DevDiv\VCForPython\%0.1f' + find_vcvarsall = distutils.msvc9compiler.find_vcvarsall + query_vcvarsall = distutils.msvc9compiler.query_vcvarsall + if find_vcvarsall and find_vcvarsall.__module__.startswith('setuptools.'): + # Already patched + return + + def _find_vcvarsall(version): + Reg = distutils.msvc9compiler.Reg + try: + # Per-user installs register the compiler path here + productdir = Reg.get_value(VC_BASE % ('', version), "installdir") + except KeyError: + try: + # All-user installs on a 64-bit system register here + productdir = Reg.get_value(VC_BASE % ('Wow6432Node\\', version), "installdir") + except KeyError: + productdir = None + + if productdir: + import os + vcvarsall = os.path.join(productdir, "vcvarsall.bat") + if os.path.isfile(vcvarsall): + return vcvarsall + + return find_vcvarsall(version) + + def _query_vcvarsall(version, *args, **kwargs): + try: + return query_vcvarsall(version, *args, **kwargs) + except distutils.errors.DistutilsPlatformError: + exc = sys.exc_info()[1] + if exc and "vcvarsall.bat" in exc.args[0]: + message = 'Microsoft Visual C++ %0.1f is required (%s).' % (version, exc.args[0]) + if int(version) == 9: + # This redirection link is maintained by Microsoft. + # Contact vspython@microsoft.com if it needs updating. + raise distutils.errors.DistutilsPlatformError( + message + ' Get it from http://aka.ms/vcpython27' + ) + raise distutils.errors.DistutilsPlatformError(message) + raise + + distutils.msvc9compiler.find_vcvarsall = _find_vcvarsall + distutils.msvc9compiler.query_vcvarsall = _query_vcvarsall +_patch_msvc9compiler_find_vcvarsall() + def have_pyrex(): """ Return True if Cython or Pyrex can be imported. diff --git a/setuptools/tests/test_msvc9compiler.py b/setuptools/tests/test_msvc9compiler.py new file mode 100644 index 00000000..272e99e8 --- /dev/null +++ b/setuptools/tests/test_msvc9compiler.py @@ -0,0 +1,135 @@ +"""msvc9compiler monkey patch test + +This test ensures that importing setuptools is sufficient to replace +the standard find_vcvarsall function with our patched version that +finds the Visual C++ for Python package. +""" + +import os +import shutil +import sys +import tempfile +import unittest + +try: + from winreg import HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE +except ImportError: + from _winreg import HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE + +import distutils.msvc9compiler +from distutils.errors import DistutilsPlatformError + +# importing only setuptools should apply the patch +import setuptools + +class MockReg: + """Mock for distutils.msvc9compiler.Reg. We patch it + with an instance of this class that mocks out the + functions that access the registry. + """ + + def __init__(self, hkey_local_machine={}, hkey_current_user={}): + self.hklm = hkey_local_machine + self.hkcu = hkey_current_user + + def __enter__(self): + self.original_read_keys = distutils.msvc9compiler.Reg.read_keys + self.original_read_values = distutils.msvc9compiler.Reg.read_values + + hives = { + HKEY_CURRENT_USER: self.hkcu, + HKEY_LOCAL_MACHINE: self.hklm, + } + + def read_keys(cls, base, key): + """Return list of registry keys.""" + hive = hives.get(base, {}) + return [k.rpartition('\\')[2] + for k in hive if k.startswith(key.lower())] + + def read_values(cls, base, key): + """Return dict of registry keys and values.""" + hive = hives.get(base, {}) + return dict((k.rpartition('\\')[2], hive[k]) + for k in hive if k.startswith(key.lower())) + + distutils.msvc9compiler.Reg.read_keys = classmethod(read_keys) + distutils.msvc9compiler.Reg.read_values = classmethod(read_values) + + return self + + def __exit__(self, exc_type, exc_value, exc_tb): + distutils.msvc9compiler.Reg.read_keys = self.original_read_keys + distutils.msvc9compiler.Reg.read_values = self.original_read_values + +class TestMSVC9Compiler(unittest.TestCase): + + def test_find_vcvarsall_patch(self): + self.assertEqual( + "setuptools.extension", + distutils.msvc9compiler.find_vcvarsall.__module__, + "find_vcvarsall was not patched" + ) + + find_vcvarsall = distutils.msvc9compiler.find_vcvarsall + query_vcvarsall = distutils.msvc9compiler.query_vcvarsall + + # No registry entries or environment variable means we should + # not find anything + old_value = os.environ.pop("VS90COMNTOOLS", None) + try: + with MockReg(): + self.assertIsNone(find_vcvarsall(9.0)) + + try: + query_vcvarsall(9.0) + self.fail('Expected DistutilsPlatformError from query_vcvarsall()') + except DistutilsPlatformError: + exc_message = str(sys.exc_info()[1]) + self.assertIn('aka.ms/vcpython27', exc_message) + finally: + if old_value: + os.environ["VS90COMNTOOLS"] = old_value + + key_32 = r'software\microsoft\devdiv\vcforpython\9.0\installdir' + key_64 = r'software\wow6432node\microsoft\devdiv\vcforpython\9.0\installdir' + + # Make two mock files so we can tell whether HCKU entries are + # preferred to HKLM entries. + mock_installdir_1 = tempfile.mkdtemp() + mock_vcvarsall_bat_1 = os.path.join(mock_installdir_1, 'vcvarsall.bat') + open(mock_vcvarsall_bat_1, 'w').close() + mock_installdir_2 = tempfile.mkdtemp() + mock_vcvarsall_bat_2 = os.path.join(mock_installdir_2, 'vcvarsall.bat') + open(mock_vcvarsall_bat_2, 'w').close() + try: + # Ensure we get the current user's setting first + with MockReg( + hkey_current_user={ key_32: mock_installdir_1 }, + hkey_local_machine={ + key_32: mock_installdir_2, + key_64: mock_installdir_2, + } + ): + self.assertEqual(mock_vcvarsall_bat_1, find_vcvarsall(9.0)) + + # Ensure we get the local machine value if it's there + with MockReg(hkey_local_machine={ key_32: mock_installdir_2 }): + self.assertEqual(mock_vcvarsall_bat_2, find_vcvarsall(9.0)) + + # Ensure we prefer the 64-bit local machine key + # (*not* the Wow6432Node key) + with MockReg( + hkey_local_machine={ + # This *should* only exist on 32-bit machines + key_32: mock_installdir_1, + # This *should* only exist on 64-bit machines + key_64: mock_installdir_2, + } + ): + self.assertEqual(mock_vcvarsall_bat_1, find_vcvarsall(9.0)) + finally: + shutil.rmtree(mock_installdir_1) + shutil.rmtree(mock_installdir_2) + +
\ No newline at end of file |