codekingpro/portable-devtools
114k
1"""distutils._msvccompiler2 3Contains MSVCCompiler, an implementation of the abstract CCompiler class4for Microsoft Visual Studio 2015.5 6This module requires VS 2015 or later.7"""8 9# Written by Perry Stoll10# hacked by Robin Becker and Thomas Heller to do a better job of11# finding DevStudio (through the registry)12# ported to VS 2005 and VS 2008 by Christian Heimes13# ported to VS 2015 by Steve Dower14from __future__ import annotations15 16import contextlib17import os18import subprocess19import unittest.mock as mock20import warnings21from collections.abc import Iterable22 23with contextlib.suppress(ImportError):24 import winreg25 26from itertools import count27 28from ..._log import log29from ...errors import (30 DistutilsExecError,31 DistutilsPlatformError,32)33from ...util import get_host_platform, get_platform34from . import base35from .base import gen_lib_options36from .errors import (37 CompileError,38 LibError,39 LinkError,40)41 42 43def _find_vc2015():44 try:45 key = winreg.OpenKeyEx(46 winreg.HKEY_LOCAL_MACHINE,47 r"Software\Microsoft\VisualStudio\SxS\VC7",48 access=winreg.KEY_READ | winreg.KEY_WOW64_32KEY,49 )50 except OSError:51 log.debug("Visual C++ is not registered")52 return None, None53 54 best_version = 055 best_dir = None56 with key:57 for i in count():58 try:59 v, vc_dir, vt = winreg.EnumValue(key, i)60 except OSError:61 break62 if v and vt == winreg.REG_SZ and os.path.isdir(vc_dir):63 try:64 version = int(float(v))65 except (ValueError, TypeError):66 continue67 if version >= 14 and version > best_version:68 best_version, best_dir = version, vc_dir69 return best_version, best_dir70 71 72def _find_vc2017():73 """Returns "15, path" based on the result of invoking vswhere.exe74 If no install is found, returns "None, None"75 76 The version is returned to avoid unnecessarily changing the function77 result. It may be ignored when the path is not None.78 79 If vswhere.exe is not available, by definition, VS 2017 is not80 installed.81 """82 root = os.environ.get("ProgramFiles(x86)") or os.environ.get("ProgramFiles")83 if not root:84 return None, None85 86 variant = 'arm64' if get_platform() == 'win-arm64' else 'x86.x64'87 suitable_components = (88 f"Microsoft.VisualStudio.Component.VC.Tools.{variant}",89 "Microsoft.VisualStudio.Workload.WDExpress",90 )91 92 for component in suitable_components:93 # Workaround for `-requiresAny` (only available on VS 2017 > 15.6)94 with contextlib.suppress(95 subprocess.CalledProcessError, OSError, UnicodeDecodeError96 ):97 path = (98 subprocess.check_output([99 os.path.join(100 root, "Microsoft Visual Studio", "Installer", "vswhere.exe"101 ),102 "-latest",103 "-prerelease",104 "-requires",105 component,106 "-property",107 "installationPath",108 "-products",109 "*",110 ])111 .decode(encoding="mbcs", errors="strict")112 .strip()113 )114 115 path = os.path.join(path, "VC", "Auxiliary", "Build")116 if os.path.isdir(path):117 return 15, path118 119 return None, None # no suitable component found120 121 122PLAT_SPEC_TO_RUNTIME = {123 'x86': 'x86',124 'x86_amd64': 'x64',125 'x86_arm': 'arm',126 'x86_arm64': 'arm64',127}128 129 130def _find_vcvarsall(plat_spec):131 # bpo-38597: Removed vcruntime return value132 _, best_dir = _find_vc2017()133 134 if not best_dir:135 best_version, best_dir = _find_vc2015()136 137 if not best_dir:138 log.debug("No suitable Visual C++ version found")139 return None, None140 141 vcvarsall = os.path.join(best_dir, "vcvarsall.bat")142 if not os.path.isfile(vcvarsall):143 log.debug("%s cannot be found", vcvarsall)144 return None, None145 146 return vcvarsall, None147 148 149def _get_vc_env(plat_spec):150 if os.getenv("DISTUTILS_USE_SDK"):151 return {key.lower(): value for key, value in os.environ.items()}152 153 vcvarsall, _ = _find_vcvarsall(plat_spec)154 if not vcvarsall:155 raise DistutilsPlatformError(156 'Microsoft Visual C++ 14.0 or greater is required. '157 'Get it with "Microsoft C++ Build Tools": '158 'https://visualstudio.microsoft.com/visual-cpp-build-tools/'159 )160 161 try:162 out = subprocess.check_output(163 f'cmd /u /c "{vcvarsall}" {plat_spec} && set',164 stderr=subprocess.STDOUT,165 ).decode('utf-16le', errors='replace')166 except subprocess.CalledProcessError as exc:167 log.error(exc.output)168 raise DistutilsPlatformError(f"Error executing {exc.cmd}")169 170 env = {171 key.lower(): value172 for key, _, value in (line.partition('=') for line in out.splitlines())173 if key and value174 }175 176 return env177 178 179def _find_exe(exe, paths=None):180 """Return path to an MSVC executable program.181 182 Tries to find the program in several places: first, one of the183 MSVC program search paths from the registry; next, the directories184 in the PATH environment variable. If any of those work, return an185 absolute path that is known to exist. If none of them work, just186 return the original program name, 'exe'.187 """188 if not paths:189 paths = os.getenv('path').split(os.pathsep)190 for p in paths:191 fn = os.path.join(os.path.abspath(p), exe)192 if os.path.isfile(fn):193 return fn194 return exe195 196 197_vcvars_names = {198 'win32': 'x86',199 'win-amd64': 'amd64',200 'win-arm32': 'arm',201 'win-arm64': 'arm64',202}203 204 205def _get_vcvars_spec(host_platform, platform):206 """207 Given a host platform and platform, determine the spec for vcvarsall.208 209 Uses the native MSVC host if the host platform would need expensive210 emulation for x86.211 212 >>> _get_vcvars_spec('win-arm64', 'win32')213 'arm64_x86'214 >>> _get_vcvars_spec('win-arm64', 'win-amd64')215 'arm64_amd64'216 217 Otherwise, always cross-compile from x86 to work with the218 lighter-weight MSVC installs that do not include native 64-bit tools.219 220 >>> _get_vcvars_spec('win32', 'win32')221 'x86'222 >>> _get_vcvars_spec('win-arm32', 'win-arm32')223 'x86_arm'224 >>> _get_vcvars_spec('win-amd64', 'win-arm64')225 'x86_arm64'226 """227 if host_platform != 'win-arm64':228 host_platform = 'win32'229 vc_hp = _vcvars_names[host_platform]230 vc_plat = _vcvars_names[platform]231 return vc_hp if vc_hp == vc_plat else f'{vc_hp}_{vc_plat}'232 233 234class Compiler(base.Compiler):235 """Concrete class that implements an interface to Microsoft Visual C++,236 as defined by the CCompiler abstract class."""237 238 compiler_type = 'msvc'239 240 # Just set this so CCompiler's constructor doesn't barf. We currently241 # don't use the 'set_executables()' bureaucracy provided by CCompiler,242 # as it really isn't necessary for this sort of single-compiler class.243 # Would be nice to have a consistent interface with UnixCCompiler,244 # though, so it's worth thinking about.245 executables = {}246 247 # Private class data (need to distinguish C from C++ source for compiler)248 _c_extensions = ['.c']249 _cpp_extensions = ['.cc', '.cpp', '.cxx']250 _rc_extensions = ['.rc']251 _mc_extensions = ['.mc']252 253 # Needed for the filename generation methods provided by the254 # base class, CCompiler.255 src_extensions = _c_extensions + _cpp_extensions + _rc_extensions + _mc_extensions256 res_extension = '.res'257 obj_extension = '.obj'258 static_lib_extension = '.lib'259 shared_lib_extension = '.dll'260 static_lib_format = shared_lib_format = '%s%s'261 exe_extension = '.exe'262 263 def __init__(self, verbose=False, force=False) -> None:264 super().__init__(verbose, force=force)265 # target platform (.plat_name is consistent with 'bdist')266 self.plat_name = None267 self.initialized = False268 269 @classmethod270 def _configure(cls, vc_env):271 """272 Set class-level include/lib dirs.273 """274 cls.include_dirs = cls._parse_path(vc_env.get('include', ''))275 cls.library_dirs = cls._parse_path(vc_env.get('lib', ''))276 277 @staticmethod278 def _parse_path(val):279 return [dir.rstrip(os.sep) for dir in val.split(os.pathsep) if dir]280 281 def initialize(self, plat_name: str | None = None) -> None:282 # multi-init means we would need to check platform same each time...283 assert not self.initialized, "don't init multiple times"284 if plat_name is None:285 plat_name = get_platform()286 # sanity check for platforms to prevent obscure errors later.287 if plat_name not in _vcvars_names:288 raise DistutilsPlatformError(289 f"--plat-name must be one of {tuple(_vcvars_names)}"290 )291 292 plat_spec = _get_vcvars_spec(get_host_platform(), plat_name)293 294 vc_env = _get_vc_env(plat_spec)295 if not vc_env:296 raise DistutilsPlatformError(297 "Unable to find a compatible Visual Studio installation."298 )299 self._configure(vc_env)300 301 self._paths = vc_env.get('path', '')302 paths = self._paths.split(os.pathsep)303 self.cc = _find_exe("cl.exe", paths)304 self.linker = _find_exe("link.exe", paths)305 self.lib = _find_exe("lib.exe", paths)306 self.rc = _find_exe("rc.exe", paths) # resource compiler307 self.mc = _find_exe("mc.exe", paths) # message compiler308 self.mt = _find_exe("mt.exe", paths) # message compiler309 310 self.preprocess_options = None311 # bpo-38597: Always compile with dynamic linking312 # Future releases of Python 3.x will include all past313 # versions of vcruntime*.dll for compatibility.314 self.compile_options = ['/nologo', '/O2', '/W3', '/GL', '/DNDEBUG', '/MD']315 316 self.compile_options_debug = [317 '/nologo',318 '/Od',319 '/MDd',320 '/Zi',321 '/W3',322 '/D_DEBUG',323 ]324 325 ldflags = ['/nologo', '/INCREMENTAL:NO', '/LTCG']326 327 ldflags_debug = ['/nologo', '/INCREMENTAL:NO', '/LTCG', '/DEBUG:FULL']328 329 self.ldflags_exe = [*ldflags, '/MANIFEST:EMBED,ID=1']330 self.ldflags_exe_debug = [*ldflags_debug, '/MANIFEST:EMBED,ID=1']331 self.ldflags_shared = [332 *ldflags,333 '/DLL',334 '/MANIFEST:EMBED,ID=2',335 '/MANIFESTUAC:NO',336 ]337 self.ldflags_shared_debug = [338 *ldflags_debug,339 '/DLL',340 '/MANIFEST:EMBED,ID=2',341 '/MANIFESTUAC:NO',342 ]343 self.ldflags_static = [*ldflags]344 self.ldflags_static_debug = [*ldflags_debug]345 346 self._ldflags = {347 (base.Compiler.EXECUTABLE, None): self.ldflags_exe,348 (base.Compiler.EXECUTABLE, False): self.ldflags_exe,349 (base.Compiler.EXECUTABLE, True): self.ldflags_exe_debug,350 (base.Compiler.SHARED_OBJECT, None): self.ldflags_shared,351 (base.Compiler.SHARED_OBJECT, False): self.ldflags_shared,352 (base.Compiler.SHARED_OBJECT, True): self.ldflags_shared_debug,353 (base.Compiler.SHARED_LIBRARY, None): self.ldflags_static,354 (base.Compiler.SHARED_LIBRARY, False): self.ldflags_static,355 (base.Compiler.SHARED_LIBRARY, True): self.ldflags_static_debug,356 }357 358 self.initialized = True359 360 # -- Worker methods ------------------------------------------------361 362 @property363 def out_extensions(self) -> dict[str, str]:364 return {365 **super().out_extensions,366 **{367 ext: self.res_extension368 for ext in self._rc_extensions + self._mc_extensions369 },370 }371 372 def compile( # noqa: C901373 self,374 sources,375 output_dir=None,376 macros=None,377 include_dirs=None,378 debug=False,379 extra_preargs=None,380 extra_postargs=None,381 depends=None,382 ):383 if not self.initialized:384 self.initialize()385 compile_info = self._setup_compile(386 output_dir, macros, include_dirs, sources, depends, extra_postargs387 )388 macros, objects, extra_postargs, pp_opts, build = compile_info389 390 compile_opts = extra_preargs or []391 compile_opts.append('/c')392 if debug:393 compile_opts.extend(self.compile_options_debug)394 else:395 compile_opts.extend(self.compile_options)396 397 add_cpp_opts = False398 399 for obj in objects:400 try:401 src, ext = build[obj]402 except KeyError:403 continue404 if debug:405 # pass the full pathname to MSVC in debug mode,406 # this allows the debugger to find the source file407 # without asking the user to browse for it408 src = os.path.abspath(src)409 410 if ext in self._c_extensions:411 input_opt = f"/Tc{src}"412 elif ext in self._cpp_extensions:413 input_opt = f"/Tp{src}"414 add_cpp_opts = True415 elif ext in self._rc_extensions:416 # compile .RC to .RES file417 input_opt = src418 output_opt = "/fo" + obj419 try:420 self.spawn([self.rc] + pp_opts + [output_opt, input_opt])421 except DistutilsExecError as msg:422 raise CompileError(msg)423 continue424 elif ext in self._mc_extensions:425 # Compile .MC to .RC file to .RES file.426 # * '-h dir' specifies the directory for the427 # generated include file428 # * '-r dir' specifies the target directory of the429 # generated RC file and the binary message resource430 # it includes431 #432 # For now (since there are no options to change this),433 # we use the source-directory for the include file and434 # the build directory for the RC file and message435 # resources. This works at least for win32all.436 h_dir = os.path.dirname(src)437 rc_dir = os.path.dirname(obj)438 try:439 # first compile .MC to .RC and .H file440 self.spawn([self.mc, '-h', h_dir, '-r', rc_dir, src])441 base, _ = os.path.splitext(os.path.basename(src))442 rc_file = os.path.join(rc_dir, base + '.rc')443 # then compile .RC to .RES file444 self.spawn([self.rc, "/fo" + obj, rc_file])445 446 except DistutilsExecError as msg:447 raise CompileError(msg)448 continue449 else:450 # how to handle this file?451 raise CompileError(f"Don't know how to compile {src} to {obj}")452 453 args = [self.cc] + compile_opts + pp_opts454 if add_cpp_opts:455 args.append('/EHsc')456 args.extend((input_opt, "/Fo" + obj))457 args.extend(extra_postargs)458 459 try:460 self.spawn(args)461 except DistutilsExecError as msg:462 raise CompileError(msg)463 464 return objects465 466 def create_static_lib(467 self,468 objects: list[str] | tuple[str, ...],469 output_libname: str,470 output_dir: str | None = None,471 debug: bool = False,472 target_lang: str | None = None,473 ) -> None:474 if not self.initialized:475 self.initialize()476 objects, output_dir = self._fix_object_args(objects, output_dir)477 output_filename = self.library_filename(output_libname, output_dir=output_dir)478 479 if self._need_link(objects, output_filename):480 lib_args = objects + ['/OUT:' + output_filename]481 if debug:482 pass # XXX what goes here?483 try:484 log.debug('Executing "%s" %s', self.lib, ' '.join(lib_args))485 self.spawn([self.lib] + lib_args)486 except DistutilsExecError as msg:487 raise LibError(msg)488 else:489 log.debug("skipping %s (up-to-date)", output_filename)490 491 def link(492 self,493 target_desc: str,494 objects: list[str] | tuple[str, ...],495 output_filename: str,496 output_dir: str | None = None,497 libraries: list[str] | tuple[str, ...] | None = None,498 library_dirs: list[str] | tuple[str, ...] | None = None,499 runtime_library_dirs: list[str] | tuple[str, ...] | None = None,500 export_symbols: Iterable[str] | None = None,501 debug: bool = False,502 extra_preargs: list[str] | None = None,503 extra_postargs: Iterable[str] | None = None,504 build_temp: str | os.PathLike[str] | None = None,505 target_lang: str | None = None,506 ) -> None:507 if not self.initialized:508 self.initialize()509 objects, output_dir = self._fix_object_args(objects, output_dir)510 fixed_args = self._fix_lib_args(libraries, library_dirs, runtime_library_dirs)511 libraries, library_dirs, runtime_library_dirs = fixed_args512 513 if runtime_library_dirs:514 self.warn(515 "I don't know what to do with 'runtime_library_dirs': "516 + str(runtime_library_dirs)517 )518 519 lib_opts = gen_lib_options(self, library_dirs, runtime_library_dirs, libraries)520 if output_dir is not None:521 output_filename = os.path.join(output_dir, output_filename)522 523 if self._need_link(objects, output_filename):524 ldflags = self._ldflags[target_desc, debug]525 526 export_opts = ["/EXPORT:" + sym for sym in (export_symbols or [])]527 528 ld_args = (529 ldflags + lib_opts + export_opts + objects + ['/OUT:' + output_filename]530 )531 532 # The MSVC linker generates .lib and .exp files, which cannot be533 # suppressed by any linker switches. The .lib files may even be534 # needed! Make sure they are generated in the temporary build535 # directory. Since they have different names for debug and release536 # builds, they can go into the same directory.537 build_temp = os.path.dirname(objects[0])538 if export_symbols is not None:539 (dll_name, dll_ext) = os.path.splitext(540 os.path.basename(output_filename)541 )542 implib_file = os.path.join(build_temp, self.library_filename(dll_name))543 ld_args.append('/IMPLIB:' + implib_file)544 545 if extra_preargs:546 ld_args[:0] = extra_preargs547 if extra_postargs:548 ld_args.extend(extra_postargs)549 550 output_dir = os.path.dirname(os.path.abspath(output_filename))551 self.mkpath(output_dir)552 try:553 log.debug('Executing "%s" %s', self.linker, ' '.join(ld_args))554 self.spawn([self.linker] + ld_args)555 except DistutilsExecError as msg:556 raise LinkError(msg)557 else:558 log.debug("skipping %s (up-to-date)", output_filename)559 560 def spawn(self, cmd):561 env = dict(os.environ, PATH=self._paths)562 with self._fallback_spawn(cmd, env) as fallback:563 return super().spawn(cmd, env=env)564 return fallback.value565 566 @contextlib.contextmanager567 def _fallback_spawn(self, cmd, env):568 """569 Discovered in pypa/distutils#15, some tools monkeypatch the compiler,570 so the 'env' kwarg causes a TypeError. Detect this condition and571 restore the legacy, unsafe behavior.572 """573 bag = type('Bag', (), {})()574 try:575 yield bag576 except TypeError as exc:577 if "unexpected keyword argument 'env'" not in str(exc):578 raise579 else:580 return581 warnings.warn("Fallback spawn triggered. Please update distutils monkeypatch.")582 with mock.patch.dict('os.environ', env):583 bag.value = super().spawn(cmd)584 585 # -- Miscellaneous methods -----------------------------------------586 # These are all used by the 'gen_lib_options() function, in587 # ccompiler.py.588 589 def library_dir_option(self, dir):590 return "/LIBPATH:" + dir591 592 def runtime_library_dir_option(self, dir):593 raise DistutilsPlatformError(594 "don't know how to set runtime library search path for MSVC"595 )596 597 def library_option(self, lib):598 return self.library_filename(lib)599 600 def find_library_file(self, dirs, lib, debug=False):601 # Prefer a debugging library if found (and requested), but deal602 # with it if we don't have one.603 if debug:604 try_names = [lib + "_d", lib]605 else:606 try_names = [lib]607 for dir in dirs:608 for name in try_names:609 libfile = os.path.join(dir, self.library_filename(name))610 if os.path.isfile(libfile):611 return libfile612 else:613 # Oops, didn't find it in *any* of 'dirs'614 return None615 