codekingpro/portable-devtools
114k
1"""distutils._msvccompiler2 3Contains MSVCCompiler, an implementation of the abstract CCompiler class4for Microsoft Visual Studio 2015.5 6The module is compatible with VS 2015 and later. You can find legacy support7for older versions in distutils.msvc9compiler and distutils.msvccompiler.8"""9 10# Written by Perry Stoll11# hacked by Robin Becker and Thomas Heller to do a better job of12# finding DevStudio (through the registry)13# ported to VS 2005 and VS 2008 by Christian Heimes14# ported to VS 2015 by Steve Dower15 16import contextlib17import os18import subprocess19import unittest.mock as mock20import warnings21 22with contextlib.suppress(ImportError):23 import winreg24 25from itertools import count26 27from ._log import log28from .ccompiler import CCompiler, gen_lib_options29from .errors import (30 CompileError,31 DistutilsExecError,32 DistutilsPlatformError,33 LibError,34 LinkError,35)36from .util import get_platform37 38 39def _find_vc2015():40 try:41 key = winreg.OpenKeyEx(42 winreg.HKEY_LOCAL_MACHINE,43 r"Software\Microsoft\VisualStudio\SxS\VC7",44 access=winreg.KEY_READ | winreg.KEY_WOW64_32KEY,45 )46 except OSError:47 log.debug("Visual C++ is not registered")48 return None, None49 50 best_version = 051 best_dir = None52 with key:53 for i in count():54 try:55 v, vc_dir, vt = winreg.EnumValue(key, i)56 except OSError:57 break58 if v and vt == winreg.REG_SZ and os.path.isdir(vc_dir):59 try:60 version = int(float(v))61 except (ValueError, TypeError):62 continue63 if version >= 14 and version > best_version:64 best_version, best_dir = version, vc_dir65 return best_version, best_dir66 67 68def _find_vc2017():69 """Returns "15, path" based on the result of invoking vswhere.exe70 If no install is found, returns "None, None"71 72 The version is returned to avoid unnecessarily changing the function73 result. It may be ignored when the path is not None.74 75 If vswhere.exe is not available, by definition, VS 2017 is not76 installed.77 """78 root = os.environ.get("ProgramFiles(x86)") or os.environ.get("ProgramFiles")79 if not root:80 return None, None81 82 try:83 path = subprocess.check_output(84 [85 os.path.join(86 root, "Microsoft Visual Studio", "Installer", "vswhere.exe"87 ),88 "-latest",89 "-prerelease",90 "-requires",91 "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",92 "-property",93 "installationPath",94 "-products",95 "*",96 ],97 encoding="mbcs",98 errors="strict",99 ).strip()100 except (subprocess.CalledProcessError, OSError, UnicodeDecodeError):101 return None, None102 103 path = os.path.join(path, "VC", "Auxiliary", "Build")104 if os.path.isdir(path):105 return 15, path106 107 return None, None108 109 110PLAT_SPEC_TO_RUNTIME = {111 'x86': 'x86',112 'x86_amd64': 'x64',113 'x86_arm': 'arm',114 'x86_arm64': 'arm64',115}116 117 118def _find_vcvarsall(plat_spec):119 # bpo-38597: Removed vcruntime return value120 _, best_dir = _find_vc2017()121 122 if not best_dir:123 best_version, best_dir = _find_vc2015()124 125 if not best_dir:126 log.debug("No suitable Visual C++ version found")127 return None, None128 129 vcvarsall = os.path.join(best_dir, "vcvarsall.bat")130 if not os.path.isfile(vcvarsall):131 log.debug("%s cannot be found", vcvarsall)132 return None, None133 134 return vcvarsall, None135 136 137def _get_vc_env(plat_spec):138 if os.getenv("DISTUTILS_USE_SDK"):139 return {key.lower(): value for key, value in os.environ.items()}140 141 vcvarsall, _ = _find_vcvarsall(plat_spec)142 if not vcvarsall:143 raise DistutilsPlatformError("Unable to find vcvarsall.bat")144 145 try:146 out = subprocess.check_output(147 f'cmd /u /c "{vcvarsall}" {plat_spec} && set',148 stderr=subprocess.STDOUT,149 ).decode('utf-16le', errors='replace')150 except subprocess.CalledProcessError as exc:151 log.error(exc.output)152 raise DistutilsPlatformError(f"Error executing {exc.cmd}")153 154 env = {155 key.lower(): value156 for key, _, value in (line.partition('=') for line in out.splitlines())157 if key and value158 }159 160 return env161 162 163def _find_exe(exe, paths=None):164 """Return path to an MSVC executable program.165 166 Tries to find the program in several places: first, one of the167 MSVC program search paths from the registry; next, the directories168 in the PATH environment variable. If any of those work, return an169 absolute path that is known to exist. If none of them work, just170 return the original program name, 'exe'.171 """172 if not paths:173 paths = os.getenv('path').split(os.pathsep)174 for p in paths:175 fn = os.path.join(os.path.abspath(p), exe)176 if os.path.isfile(fn):177 return fn178 return exe179 180 181# A map keyed by get_platform() return values to values accepted by182# 'vcvarsall.bat'. Always cross-compile from x86 to work with the183# lighter-weight MSVC installs that do not include native 64-bit tools.184PLAT_TO_VCVARS = {185 'win32': 'x86',186 'win-amd64': 'x86_amd64',187 'win-arm32': 'x86_arm',188 'win-arm64': 'x86_arm64',189}190 191 192class MSVCCompiler(CCompiler):193 """Concrete class that implements an interface to Microsoft Visual C++,194 as defined by the CCompiler abstract class."""195 196 compiler_type = 'msvc'197 198 # Just set this so CCompiler's constructor doesn't barf. We currently199 # don't use the 'set_executables()' bureaucracy provided by CCompiler,200 # as it really isn't necessary for this sort of single-compiler class.201 # Would be nice to have a consistent interface with UnixCCompiler,202 # though, so it's worth thinking about.203 executables = {}204 205 # Private class data (need to distinguish C from C++ source for compiler)206 _c_extensions = ['.c']207 _cpp_extensions = ['.cc', '.cpp', '.cxx']208 _rc_extensions = ['.rc']209 _mc_extensions = ['.mc']210 211 # Needed for the filename generation methods provided by the212 # base class, CCompiler.213 src_extensions = _c_extensions + _cpp_extensions + _rc_extensions + _mc_extensions214 res_extension = '.res'215 obj_extension = '.obj'216 static_lib_extension = '.lib'217 shared_lib_extension = '.dll'218 static_lib_format = shared_lib_format = '%s%s'219 exe_extension = '.exe'220 221 def __init__(self, verbose=0, dry_run=0, force=0):222 super().__init__(verbose, dry_run, force)223 # target platform (.plat_name is consistent with 'bdist')224 self.plat_name = None225 self.initialized = False226 227 @classmethod228 def _configure(cls, vc_env):229 """230 Set class-level include/lib dirs.231 """232 cls.include_dirs = cls._parse_path(vc_env.get('include', ''))233 cls.library_dirs = cls._parse_path(vc_env.get('lib', ''))234 235 @staticmethod236 def _parse_path(val):237 return [dir.rstrip(os.sep) for dir in val.split(os.pathsep) if dir]238 239 def initialize(self, plat_name=None):240 # multi-init means we would need to check platform same each time...241 assert not self.initialized, "don't init multiple times"242 if plat_name is None:243 plat_name = get_platform()244 # sanity check for platforms to prevent obscure errors later.245 if plat_name not in PLAT_TO_VCVARS:246 raise DistutilsPlatformError(247 f"--plat-name must be one of {tuple(PLAT_TO_VCVARS)}"248 )249 250 # Get the vcvarsall.bat spec for the requested platform.251 plat_spec = PLAT_TO_VCVARS[plat_name]252 253 vc_env = _get_vc_env(plat_spec)254 if not vc_env:255 raise DistutilsPlatformError(256 "Unable to find a compatible Visual Studio installation."257 )258 self._configure(vc_env)259 260 self._paths = vc_env.get('path', '')261 paths = self._paths.split(os.pathsep)262 self.cc = _find_exe("cl.exe", paths)263 self.linker = _find_exe("link.exe", paths)264 self.lib = _find_exe("lib.exe", paths)265 self.rc = _find_exe("rc.exe", paths) # resource compiler266 self.mc = _find_exe("mc.exe", paths) # message compiler267 self.mt = _find_exe("mt.exe", paths) # message compiler268 269 self.preprocess_options = None270 # bpo-38597: Always compile with dynamic linking271 # Future releases of Python 3.x will include all past272 # versions of vcruntime*.dll for compatibility.273 self.compile_options = ['/nologo', '/O2', '/W3', '/GL', '/DNDEBUG', '/MD']274 275 self.compile_options_debug = [276 '/nologo',277 '/Od',278 '/MDd',279 '/Zi',280 '/W3',281 '/D_DEBUG',282 ]283 284 ldflags = ['/nologo', '/INCREMENTAL:NO', '/LTCG']285 286 ldflags_debug = ['/nologo', '/INCREMENTAL:NO', '/LTCG', '/DEBUG:FULL']287 288 self.ldflags_exe = [*ldflags, '/MANIFEST:EMBED,ID=1']289 self.ldflags_exe_debug = [*ldflags_debug, '/MANIFEST:EMBED,ID=1']290 self.ldflags_shared = [291 *ldflags,292 '/DLL',293 '/MANIFEST:EMBED,ID=2',294 '/MANIFESTUAC:NO',295 ]296 self.ldflags_shared_debug = [297 *ldflags_debug,298 '/DLL',299 '/MANIFEST:EMBED,ID=2',300 '/MANIFESTUAC:NO',301 ]302 self.ldflags_static = [*ldflags]303 self.ldflags_static_debug = [*ldflags_debug]304 305 self._ldflags = {306 (CCompiler.EXECUTABLE, None): self.ldflags_exe,307 (CCompiler.EXECUTABLE, False): self.ldflags_exe,308 (CCompiler.EXECUTABLE, True): self.ldflags_exe_debug,309 (CCompiler.SHARED_OBJECT, None): self.ldflags_shared,310 (CCompiler.SHARED_OBJECT, False): self.ldflags_shared,311 (CCompiler.SHARED_OBJECT, True): self.ldflags_shared_debug,312 (CCompiler.SHARED_LIBRARY, None): self.ldflags_static,313 (CCompiler.SHARED_LIBRARY, False): self.ldflags_static,314 (CCompiler.SHARED_LIBRARY, True): self.ldflags_static_debug,315 }316 317 self.initialized = True318 319 # -- Worker methods ------------------------------------------------320 321 @property322 def out_extensions(self):323 return {324 **super().out_extensions,325 **{326 ext: self.res_extension327 for ext in self._rc_extensions + self._mc_extensions328 },329 }330 331 def compile( # noqa: C901332 self,333 sources,334 output_dir=None,335 macros=None,336 include_dirs=None,337 debug=0,338 extra_preargs=None,339 extra_postargs=None,340 depends=None,341 ):342 if not self.initialized:343 self.initialize()344 compile_info = self._setup_compile(345 output_dir, macros, include_dirs, sources, depends, extra_postargs346 )347 macros, objects, extra_postargs, pp_opts, build = compile_info348 349 compile_opts = extra_preargs or []350 compile_opts.append('/c')351 if debug:352 compile_opts.extend(self.compile_options_debug)353 else:354 compile_opts.extend(self.compile_options)355 356 add_cpp_opts = False357 358 for obj in objects:359 try:360 src, ext = build[obj]361 except KeyError:362 continue363 if debug:364 # pass the full pathname to MSVC in debug mode,365 # this allows the debugger to find the source file366 # without asking the user to browse for it367 src = os.path.abspath(src)368 369 if ext in self._c_extensions:370 input_opt = "/Tc" + src371 elif ext in self._cpp_extensions:372 input_opt = "/Tp" + src373 add_cpp_opts = True374 elif ext in self._rc_extensions:375 # compile .RC to .RES file376 input_opt = src377 output_opt = "/fo" + obj378 try:379 self.spawn([self.rc] + pp_opts + [output_opt, input_opt])380 except DistutilsExecError as msg:381 raise CompileError(msg)382 continue383 elif ext in self._mc_extensions:384 # Compile .MC to .RC file to .RES file.385 # * '-h dir' specifies the directory for the386 # generated include file387 # * '-r dir' specifies the target directory of the388 # generated RC file and the binary message resource389 # it includes390 #391 # For now (since there are no options to change this),392 # we use the source-directory for the include file and393 # the build directory for the RC file and message394 # resources. This works at least for win32all.395 h_dir = os.path.dirname(src)396 rc_dir = os.path.dirname(obj)397 try:398 # first compile .MC to .RC and .H file399 self.spawn([self.mc, '-h', h_dir, '-r', rc_dir, src])400 base, _ = os.path.splitext(os.path.basename(src))401 rc_file = os.path.join(rc_dir, base + '.rc')402 # then compile .RC to .RES file403 self.spawn([self.rc, "/fo" + obj, rc_file])404 405 except DistutilsExecError as msg:406 raise CompileError(msg)407 continue408 else:409 # how to handle this file?410 raise CompileError(f"Don't know how to compile {src} to {obj}")411 412 args = [self.cc] + compile_opts + pp_opts413 if add_cpp_opts:414 args.append('/EHsc')415 args.extend((input_opt, "/Fo" + obj))416 args.extend(extra_postargs)417 418 try:419 self.spawn(args)420 except DistutilsExecError as msg:421 raise CompileError(msg)422 423 return objects424 425 def create_static_lib(426 self, objects, output_libname, output_dir=None, debug=0, target_lang=None427 ):428 if not self.initialized:429 self.initialize()430 objects, output_dir = self._fix_object_args(objects, output_dir)431 output_filename = self.library_filename(output_libname, output_dir=output_dir)432 433 if self._need_link(objects, output_filename):434 lib_args = objects + ['/OUT:' + output_filename]435 if debug:436 pass # XXX what goes here?437 try:438 log.debug('Executing "%s" %s', self.lib, ' '.join(lib_args))439 self.spawn([self.lib] + lib_args)440 except DistutilsExecError as msg:441 raise LibError(msg)442 else:443 log.debug("skipping %s (up-to-date)", output_filename)444 445 def link(446 self,447 target_desc,448 objects,449 output_filename,450 output_dir=None,451 libraries=None,452 library_dirs=None,453 runtime_library_dirs=None,454 export_symbols=None,455 debug=0,456 extra_preargs=None,457 extra_postargs=None,458 build_temp=None,459 target_lang=None,460 ):461 if not self.initialized:462 self.initialize()463 objects, output_dir = self._fix_object_args(objects, output_dir)464 fixed_args = self._fix_lib_args(libraries, library_dirs, runtime_library_dirs)465 libraries, library_dirs, runtime_library_dirs = fixed_args466 467 if runtime_library_dirs:468 self.warn(469 "I don't know what to do with 'runtime_library_dirs': "470 + str(runtime_library_dirs)471 )472 473 lib_opts = gen_lib_options(self, library_dirs, runtime_library_dirs, libraries)474 if output_dir is not None:475 output_filename = os.path.join(output_dir, output_filename)476 477 if self._need_link(objects, output_filename):478 ldflags = self._ldflags[target_desc, debug]479 480 export_opts = ["/EXPORT:" + sym for sym in (export_symbols or [])]481 482 ld_args = (483 ldflags + lib_opts + export_opts + objects + ['/OUT:' + output_filename]484 )485 486 # The MSVC linker generates .lib and .exp files, which cannot be487 # suppressed by any linker switches. The .lib files may even be488 # needed! Make sure they are generated in the temporary build489 # directory. Since they have different names for debug and release490 # builds, they can go into the same directory.491 build_temp = os.path.dirname(objects[0])492 if export_symbols is not None:493 (dll_name, dll_ext) = os.path.splitext(494 os.path.basename(output_filename)495 )496 implib_file = os.path.join(build_temp, self.library_filename(dll_name))497 ld_args.append('/IMPLIB:' + implib_file)498 499 if extra_preargs:500 ld_args[:0] = extra_preargs501 if extra_postargs:502 ld_args.extend(extra_postargs)503 504 output_dir = os.path.dirname(os.path.abspath(output_filename))505 self.mkpath(output_dir)506 try:507 log.debug('Executing "%s" %s', self.linker, ' '.join(ld_args))508 self.spawn([self.linker] + ld_args)509 except DistutilsExecError as msg:510 raise LinkError(msg)511 else:512 log.debug("skipping %s (up-to-date)", output_filename)513 514 def spawn(self, cmd):515 env = dict(os.environ, PATH=self._paths)516 with self._fallback_spawn(cmd, env) as fallback:517 return super().spawn(cmd, env=env)518 return fallback.value519 520 @contextlib.contextmanager521 def _fallback_spawn(self, cmd, env):522 """523 Discovered in pypa/distutils#15, some tools monkeypatch the compiler,524 so the 'env' kwarg causes a TypeError. Detect this condition and525 restore the legacy, unsafe behavior.526 """527 bag = type('Bag', (), {})()528 try:529 yield bag530 except TypeError as exc:531 if "unexpected keyword argument 'env'" not in str(exc):532 raise533 else:534 return535 warnings.warn("Fallback spawn triggered. Please update distutils monkeypatch.")536 with mock.patch.dict('os.environ', env):537 bag.value = super().spawn(cmd)538 539 # -- Miscellaneous methods -----------------------------------------540 # These are all used by the 'gen_lib_options() function, in541 # ccompiler.py.542 543 def library_dir_option(self, dir):544 return "/LIBPATH:" + dir545 546 def runtime_library_dir_option(self, dir):547 raise DistutilsPlatformError(548 "don't know how to set runtime library search path for MSVC"549 )550 551 def library_option(self, lib):552 return self.library_filename(lib)553 554 def find_library_file(self, dirs, lib, debug=0):555 # Prefer a debugging library if found (and requested), but deal556 # with it if we don't have one.557 if debug:558 try_names = [lib + "_d", lib]559 else:560 try_names = [lib]561 for dir in dirs:562 for name in try_names:563 libfile = os.path.join(dir, self.library_filename(name))564 if os.path.isfile(libfile):565 return libfile566 else:567 # Oops, didn't find it in *any* of 'dirs'568 return None569 