Skip to content

[3.7] bpo-38597: Never statically link extension initialization code on Windows (GH-18724) #18759

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 3, 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
55 changes: 7 additions & 48 deletions Lib/distutils/_msvccompiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,23 +90,11 @@ def _find_vc2017():
return None, None

def _find_vcvarsall(plat_spec):
# bpo-38597: Removed vcruntime return value
_, best_dir = _find_vc2017()
vcruntime = None
vcruntime_plat = 'x64' if 'amd64' in plat_spec else 'x86'
if best_dir:
vcredist = os.path.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 = _find_vc2015()
if best_version:
vcruntime = os.path.join(best_dir, 'redist', vcruntime_plat,
"Microsoft.VC140.CRT", "vcruntime140.dll")

if not best_dir:
log.debug("No suitable Visual C++ version found")
Expand All @@ -117,11 +105,7 @@ def _find_vcvarsall(plat_spec):
log.debug("%s cannot be found", vcvarsall)
return None, None

if not vcruntime or not os.path.isfile(vcruntime):
log.debug("%s cannot be found", vcruntime)
vcruntime = None

return vcvarsall, vcruntime
return vcvarsall, None

def _get_vc_env(plat_spec):
if os.getenv("DISTUTILS_USE_SDK"):
Expand All @@ -130,7 +114,7 @@ def _get_vc_env(plat_spec):
for key, value in os.environ.items()
}

vcvarsall, vcruntime = _find_vcvarsall(plat_spec)
vcvarsall, _ = _find_vcvarsall(plat_spec)
if not vcvarsall:
raise DistutilsPlatformError("Unable to find vcvarsall.bat")

Expand All @@ -151,8 +135,6 @@ def _get_vc_env(plat_spec):
if key and value
}

if vcruntime:
env['py_vcruntime_redist'] = vcruntime
return env

def _find_exe(exe, paths=None):
Expand Down Expand Up @@ -180,12 +162,6 @@ def _find_exe(exe, paths=None):
'win-amd64' : 'x86_amd64',
}

# A set containing the DLLs that are guaranteed to be available for
# all micro versions of this Python version. Known extension
# dependencies that are not in this set will be copied to the output
# path.
_BUNDLED_DLLS = frozenset(['vcruntime140.dll'])

class MSVCCompiler(CCompiler) :
"""Concrete class that implements an interface to Microsoft Visual C++,
as defined by the CCompiler abstract class."""
Expand Down Expand Up @@ -249,7 +225,6 @@ def initialize(self, plat_name=None):
self.rc = _find_exe("rc.exe", paths) # resource compiler
self.mc = _find_exe("mc.exe", paths) # message compiler
self.mt = _find_exe("mt.exe", paths) # message compiler
self._vcruntime_redist = vc_env.get('py_vcruntime_redist', '')

for dir in vc_env.get('include', '').split(os.pathsep):
if dir:
Expand All @@ -260,13 +235,12 @@ def initialize(self, plat_name=None):
self.add_library_dir(dir.rstrip(os.sep))

self.preprocess_options = None
# If vcruntime_redist is available, link against it dynamically. Otherwise,
# use /MT[d] to build statically, then switch from libucrt[d].lib to ucrt[d].lib
# later to dynamically link to ucrtbase but not vcruntime.
# bpo-38597: Always compile with dynamic linking
# Future releases of Python 3.x will include all past
# versions of vcruntime*.dll for compatibility.
self.compile_options = [
'/nologo', '/Ox', '/W3', '/GL', '/DNDEBUG'
'/nologo', '/Ox', '/W3', '/GL', '/DNDEBUG', '/MD'
]
self.compile_options.append('/MD' if self._vcruntime_redist else '/MT')

self.compile_options_debug = [
'/nologo', '/Od', '/MDd', '/Zi', '/W3', '/D_DEBUG'
Expand All @@ -275,8 +249,6 @@ def initialize(self, plat_name=None):
ldflags = [
'/nologo', '/INCREMENTAL:NO', '/LTCG'
]
if not self._vcruntime_redist:
ldflags.extend(('/nodefaultlib:libucrt.lib', 'ucrt.lib'))

ldflags_debug = [
'/nologo', '/INCREMENTAL:NO', '/LTCG', '/DEBUG:FULL'
Expand Down Expand Up @@ -518,24 +490,11 @@ def link(self,
try:
log.debug('Executing "%s" %s', self.linker, ' '.join(ld_args))
self.spawn([self.linker] + ld_args)
self._copy_vcruntime(output_dir)
except DistutilsExecError as msg:
raise LinkError(msg)
else:
log.debug("skipping %s (up-to-date)", output_filename)

def _copy_vcruntime(self, output_dir):
vcruntime = self._vcruntime_redist
if not vcruntime or not os.path.isfile(vcruntime):
return

if os.path.basename(vcruntime).lower() in _BUNDLED_DLLS:
return

log.debug('Copying "%s"', vcruntime)
vcruntime = shutil.copy(vcruntime, output_dir)
os.chmod(vcruntime, stat.S_IWRITE)

def spawn(self, cmd):
old_path = os.getenv('path')
try:
Expand Down
51 changes: 0 additions & 51 deletions Lib/distutils/tests/test_msvccompiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,57 +32,6 @@ def _find_vcvarsall(plat_spec):
finally:
_msvccompiler._find_vcvarsall = old_find_vcvarsall

def test_compiler_options(self):
import distutils._msvccompiler as _msvccompiler
# suppress path to vcruntime from _find_vcvarsall to
# check that /MT is added to compile options
old_find_vcvarsall = _msvccompiler._find_vcvarsall
def _find_vcvarsall(plat_spec):
return old_find_vcvarsall(plat_spec)[0], None
_msvccompiler._find_vcvarsall = _find_vcvarsall
try:
compiler = _msvccompiler.MSVCCompiler()
compiler.initialize()

self.assertIn('/MT', compiler.compile_options)
self.assertNotIn('/MD', compiler.compile_options)
finally:
_msvccompiler._find_vcvarsall = old_find_vcvarsall

def test_vcruntime_copy(self):
import distutils._msvccompiler as _msvccompiler
# force path to a known file - it doesn't matter
# what we copy as long as its name is not in
# _msvccompiler._BUNDLED_DLLS
old_find_vcvarsall = _msvccompiler._find_vcvarsall
def _find_vcvarsall(plat_spec):
return old_find_vcvarsall(plat_spec)[0], __file__
_msvccompiler._find_vcvarsall = _find_vcvarsall
try:
tempdir = self.mkdtemp()
compiler = _msvccompiler.MSVCCompiler()
compiler.initialize()
compiler._copy_vcruntime(tempdir)

self.assertTrue(os.path.isfile(os.path.join(
tempdir, os.path.basename(__file__))))
finally:
_msvccompiler._find_vcvarsall = old_find_vcvarsall

def test_vcruntime_skip_copy(self):
import distutils._msvccompiler as _msvccompiler

tempdir = self.mkdtemp()
compiler = _msvccompiler.MSVCCompiler()
compiler.initialize()
dll = compiler._vcruntime_redist
self.assertTrue(os.path.isfile(dll), dll or "<None>")

compiler._copy_vcruntime(tempdir)

self.assertFalse(os.path.isfile(os.path.join(
tempdir, os.path.basename(dll))), dll or "<None>")

def test_get_vc_env_unicode(self):
import distutils._msvccompiler as _msvccompiler

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
:mod:`distutils` will no longer statically link :file:`vcruntime140.dll`
when a redistributable version is unavailable. All future releases of
CPython will include a copy of this DLL to ensure distributed extensions can
continue to load.
2 changes: 2 additions & 0 deletions PCbuild/pythoncore.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,8 @@
<VCRuntimeDLL Include="$(VCRedistDir)\**\vcruntime*.dll" />
</ItemGroup>
<Target Name="_CopyVCRuntime" AfterTargets="Build" Inputs="@(VCRuntimeDLL)" Outputs="$(OutDir)%(Filename)%(Extension)">
<!-- bpo-38597: When we switch to another VCRuntime DLL, include vcruntime140.dll as well -->
<Warning Text="A copy of vcruntime140.dll is also required" Condition="!$(VCToolsRedistVersion.StartsWith(`14.`))" />
<Copy SourceFiles="%(VCRuntimeDLL.FullPath)" DestinationFolder="$(OutDir)" />
</Target>
<Target Name="_CleanVCRuntime" AfterTargets="Clean">
Expand Down