1"""distutils._msvccompiler
2
3Contains MSVCCompiler, an implementation of the abstract CCompiler class
4for Microsoft Visual Studio 2015.
5
6The module is compatible with VS 2015 and later. You can find legacy support
7for older versions in distutils.msvc9compiler and distutils.msvccompiler.
8"""
9
10# Written by Perry Stoll
11# hacked by Robin Becker and Thomas Heller to do a better job of
12#   finding DevStudio (through the registry)
13# ported to VS 2005 and VS 2008 by Christian Heimes
14# ported to VS 2015 by Steve Dower
15
16import os
17import subprocess
18import contextlib
19import warnings
20import unittest.mock
21with contextlib.suppress(ImportError):
22    import winreg
23
24from distutils.errors import DistutilsExecError, DistutilsPlatformError, \
25                             CompileError, LibError, LinkError
26from distutils.ccompiler import CCompiler, gen_lib_options
27from distutils import log
28from distutils.util import get_platform
29
30from itertools import count
31
32def _find_vc2015():
33    try:
34        key = winreg.OpenKeyEx(
35            winreg.HKEY_LOCAL_MACHINE,
36            r"Software\Microsoft\VisualStudio\SxS\VC7",
37            access=winreg.KEY_READ | winreg.KEY_WOW64_32KEY
38        )
39    except OSError:
40        log.debug("Visual C++ is not registered")
41        return None, None
42
43    best_version = 0
44    best_dir = None
45    with key:
46        for i in count():
47            try:
48                v, vc_dir, vt = winreg.EnumValue(key, i)
49            except OSError:
50                break
51            if v and vt == winreg.REG_SZ and os.path.isdir(vc_dir):
52                try:
53                    version = int(float(v))
54                except (ValueError, TypeError):
55                    continue
56                if version >= 14 and version > best_version:
57                    best_version, best_dir = version, vc_dir
58    return best_version, best_dir
59
60def _find_vc2017():
61    """Returns "15, path" based on the result of invoking vswhere.exe
62    If no install is found, returns "None, None"
63
64    The version is returned to avoid unnecessarily changing the function
65    result. It may be ignored when the path is not None.
66
67    If vswhere.exe is not available, by definition, VS 2017 is not
68    installed.
69    """
70    root = os.environ.get("ProgramFiles(x86)") or os.environ.get("ProgramFiles")
71    if not root:
72        return None, None
73
74    try:
75        path = subprocess.check_output([
76            os.path.join(root, "Microsoft Visual Studio", "Installer", "vswhere.exe"),
77            "-latest",
78            "-prerelease",
79            "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
80            "-property", "installationPath",
81            "-products", "*",
82        ], encoding="mbcs", errors="strict").strip()
83    except (subprocess.CalledProcessError, OSError, UnicodeDecodeError):
84        return None, None
85
86    path = os.path.join(path, "VC", "Auxiliary", "Build")
87    if os.path.isdir(path):
88        return 15, path
89
90    return None, None
91
92PLAT_SPEC_TO_RUNTIME = {
93    'x86' : 'x86',
94    'x86_amd64' : 'x64',
95    'x86_arm' : 'arm',
96    'x86_arm64' : 'arm64'
97}
98
99def _find_vcvarsall(plat_spec):
100    # bpo-38597: Removed vcruntime return value
101    _, best_dir = _find_vc2017()
102
103    if not best_dir:
104        best_version, best_dir = _find_vc2015()
105
106    if not best_dir:
107        log.debug("No suitable Visual C++ version found")
108        return None, None
109
110    vcvarsall = os.path.join(best_dir, "vcvarsall.bat")
111    if not os.path.isfile(vcvarsall):
112        log.debug("%s cannot be found", vcvarsall)
113        return None, None
114
115    return vcvarsall, None
116
117def _get_vc_env(plat_spec):
118    if os.getenv("DISTUTILS_USE_SDK"):
119        return {
120            key.lower(): value
121            for key, value in os.environ.items()
122        }
123
124    vcvarsall, _ = _find_vcvarsall(plat_spec)
125    if not vcvarsall:
126        raise DistutilsPlatformError("Unable to find vcvarsall.bat")
127
128    try:
129        out = subprocess.check_output(
130            'cmd /u /c "{}" {} && set'.format(vcvarsall, plat_spec),
131            stderr=subprocess.STDOUT,
132        ).decode('utf-16le', errors='replace')
133    except subprocess.CalledProcessError as exc:
134        log.error(exc.output)
135        raise DistutilsPlatformError("Error executing {}"
136                .format(exc.cmd))
137
138    env = {
139        key.lower(): value
140        for key, _, value in
141        (line.partition('=') for line in out.splitlines())
142        if key and value
143    }
144
145    return env
146
147def _find_exe(exe, paths=None):
148    """Return path to an MSVC executable program.
149
150    Tries to find the program in several places: first, one of the
151    MSVC program search paths from the registry; next, the directories
152    in the PATH environment variable.  If any of those work, return an
153    absolute path that is known to exist.  If none of them work, just
154    return the original program name, 'exe'.
155    """
156    if not paths:
157        paths = os.getenv('path').split(os.pathsep)
158    for p in paths:
159        fn = os.path.join(os.path.abspath(p), exe)
160        if os.path.isfile(fn):
161            return fn
162    return exe
163
164# A map keyed by get_platform() return values to values accepted by
165# 'vcvarsall.bat'. Always cross-compile from x86 to work with the
166# lighter-weight MSVC installs that do not include native 64-bit tools.
167PLAT_TO_VCVARS = {
168    'win32' : 'x86',
169    'win-amd64' : 'x86_amd64',
170    'win-arm32' : 'x86_arm',
171    'win-arm64' : 'x86_arm64'
172}
173
174class MSVCCompiler(CCompiler) :
175    """Concrete class that implements an interface to Microsoft Visual C++,
176       as defined by the CCompiler abstract class."""
177
178    compiler_type = 'msvc'
179
180    # Just set this so CCompiler's constructor doesn't barf.  We currently
181    # don't use the 'set_executables()' bureaucracy provided by CCompiler,
182    # as it really isn't necessary for this sort of single-compiler class.
183    # Would be nice to have a consistent interface with UnixCCompiler,
184    # though, so it's worth thinking about.
185    executables = {}
186
187    # Private class data (need to distinguish C from C++ source for compiler)
188    _c_extensions = ['.c']
189    _cpp_extensions = ['.cc', '.cpp', '.cxx']
190    _rc_extensions = ['.rc']
191    _mc_extensions = ['.mc']
192
193    # Needed for the filename generation methods provided by the
194    # base class, CCompiler.
195    src_extensions = (_c_extensions + _cpp_extensions +
196                      _rc_extensions + _mc_extensions)
197    res_extension = '.res'
198    obj_extension = '.obj'
199    static_lib_extension = '.lib'
200    shared_lib_extension = '.dll'
201    static_lib_format = shared_lib_format = '%s%s'
202    exe_extension = '.exe'
203
204
205    def __init__(self, verbose=0, dry_run=0, force=0):
206        CCompiler.__init__ (self, verbose, dry_run, force)
207        # target platform (.plat_name is consistent with 'bdist')
208        self.plat_name = None
209        self.initialized = False
210
211    def initialize(self, plat_name=None):
212        # multi-init means we would need to check platform same each time...
213        assert not self.initialized, "don't init multiple times"
214        if plat_name is None:
215            plat_name = get_platform()
216        # sanity check for platforms to prevent obscure errors later.
217        if plat_name not in PLAT_TO_VCVARS:
218            raise DistutilsPlatformError("--plat-name must be one of {}"
219                                         .format(tuple(PLAT_TO_VCVARS)))
220
221        # Get the vcvarsall.bat spec for the requested platform.
222        plat_spec = PLAT_TO_VCVARS[plat_name]
223
224        vc_env = _get_vc_env(plat_spec)
225        if not vc_env:
226            raise DistutilsPlatformError("Unable to find a compatible "
227                "Visual Studio installation.")
228
229        self._paths = vc_env.get('path', '')
230        paths = self._paths.split(os.pathsep)
231        self.cc = _find_exe("cl.exe", paths)
232        self.linker = _find_exe("link.exe", paths)
233        self.lib = _find_exe("lib.exe", paths)
234        self.rc = _find_exe("rc.exe", paths)   # resource compiler
235        self.mc = _find_exe("mc.exe", paths)   # message compiler
236        self.mt = _find_exe("mt.exe", paths)   # message compiler
237
238        for dir in vc_env.get('include', '').split(os.pathsep):
239            if dir:
240                self.add_include_dir(dir.rstrip(os.sep))
241
242        for dir in vc_env.get('lib', '').split(os.pathsep):
243            if dir:
244                self.add_library_dir(dir.rstrip(os.sep))
245
246        self.preprocess_options = None
247        # bpo-38597: Always compile with dynamic linking
248        # Future releases of Python 3.x will include all past
249        # versions of vcruntime*.dll for compatibility.
250        self.compile_options = [
251            '/nologo', '/O2', '/W3', '/GL', '/DNDEBUG', '/MD'
252        ]
253
254        self.compile_options_debug = [
255            '/nologo', '/Od', '/MDd', '/Zi', '/W3', '/D_DEBUG'
256        ]
257
258        ldflags = [
259            '/nologo', '/INCREMENTAL:NO', '/LTCG'
260        ]
261
262        ldflags_debug = [
263            '/nologo', '/INCREMENTAL:NO', '/LTCG', '/DEBUG:FULL'
264        ]
265
266        self.ldflags_exe = [*ldflags, '/MANIFEST:EMBED,ID=1']
267        self.ldflags_exe_debug = [*ldflags_debug, '/MANIFEST:EMBED,ID=1']
268        self.ldflags_shared = [*ldflags, '/DLL', '/MANIFEST:EMBED,ID=2', '/MANIFESTUAC:NO']
269        self.ldflags_shared_debug = [*ldflags_debug, '/DLL', '/MANIFEST:EMBED,ID=2', '/MANIFESTUAC:NO']
270        self.ldflags_static = [*ldflags]
271        self.ldflags_static_debug = [*ldflags_debug]
272
273        self._ldflags = {
274            (CCompiler.EXECUTABLE, None): self.ldflags_exe,
275            (CCompiler.EXECUTABLE, False): self.ldflags_exe,
276            (CCompiler.EXECUTABLE, True): self.ldflags_exe_debug,
277            (CCompiler.SHARED_OBJECT, None): self.ldflags_shared,
278            (CCompiler.SHARED_OBJECT, False): self.ldflags_shared,
279            (CCompiler.SHARED_OBJECT, True): self.ldflags_shared_debug,
280            (CCompiler.SHARED_LIBRARY, None): self.ldflags_static,
281            (CCompiler.SHARED_LIBRARY, False): self.ldflags_static,
282            (CCompiler.SHARED_LIBRARY, True): self.ldflags_static_debug,
283        }
284
285        self.initialized = True
286
287    # -- Worker methods ------------------------------------------------
288
289    def object_filenames(self,
290                         source_filenames,
291                         strip_dir=0,
292                         output_dir=''):
293        ext_map = {
294            **{ext: self.obj_extension for ext in self.src_extensions},
295            **{ext: self.res_extension for ext in self._rc_extensions + self._mc_extensions},
296        }
297
298        output_dir = output_dir or ''
299
300        def make_out_path(p):
301            base, ext = os.path.splitext(p)
302            if strip_dir:
303                base = os.path.basename(base)
304            else:
305                _, base = os.path.splitdrive(base)
306                if base.startswith((os.path.sep, os.path.altsep)):
307                    base = base[1:]
308            try:
309                # XXX: This may produce absurdly long paths. We should check
310                # the length of the result and trim base until we fit within
311                # 260 characters.
312                return os.path.join(output_dir, base + ext_map[ext])
313            except LookupError:
314                # Better to raise an exception instead of silently continuing
315                # and later complain about sources and targets having
316                # different lengths
317                raise CompileError("Don't know how to compile {}".format(p))
318
319        return list(map(make_out_path, source_filenames))
320
321
322    def compile(self, sources,
323                output_dir=None, macros=None, include_dirs=None, debug=0,
324                extra_preargs=None, extra_postargs=None, depends=None):
325
326        if not self.initialized:
327            self.initialize()
328        compile_info = self._setup_compile(output_dir, macros, include_dirs,
329                                           sources, depends, extra_postargs)
330        macros, objects, extra_postargs, pp_opts, build = compile_info
331
332        compile_opts = extra_preargs or []
333        compile_opts.append('/c')
334        if debug:
335            compile_opts.extend(self.compile_options_debug)
336        else:
337            compile_opts.extend(self.compile_options)
338
339
340        add_cpp_opts = False
341
342        for obj in objects:
343            try:
344                src, ext = build[obj]
345            except KeyError:
346                continue
347            if debug:
348                # pass the full pathname to MSVC in debug mode,
349                # this allows the debugger to find the source file
350                # without asking the user to browse for it
351                src = os.path.abspath(src)
352
353            if ext in self._c_extensions:
354                input_opt = "/Tc" + src
355            elif ext in self._cpp_extensions:
356                input_opt = "/Tp" + src
357                add_cpp_opts = True
358            elif ext in self._rc_extensions:
359                # compile .RC to .RES file
360                input_opt = src
361                output_opt = "/fo" + obj
362                try:
363                    self.spawn([self.rc] + pp_opts + [output_opt, input_opt])
364                except DistutilsExecError as msg:
365                    raise CompileError(msg)
366                continue
367            elif ext in self._mc_extensions:
368                # Compile .MC to .RC file to .RES file.
369                #   * '-h dir' specifies the directory for the
370                #     generated include file
371                #   * '-r dir' specifies the target directory of the
372                #     generated RC file and the binary message resource
373                #     it includes
374                #
375                # For now (since there are no options to change this),
376                # we use the source-directory for the include file and
377                # the build directory for the RC file and message
378                # resources. This works at least for win32all.
379                h_dir = os.path.dirname(src)
380                rc_dir = os.path.dirname(obj)
381                try:
382                    # first compile .MC to .RC and .H file
383                    self.spawn([self.mc, '-h', h_dir, '-r', rc_dir, src])
384                    base, _ = os.path.splitext(os.path.basename (src))
385                    rc_file = os.path.join(rc_dir, base + '.rc')
386                    # then compile .RC to .RES file
387                    self.spawn([self.rc, "/fo" + obj, rc_file])
388
389                except DistutilsExecError as msg:
390                    raise CompileError(msg)
391                continue
392            else:
393                # how to handle this file?
394                raise CompileError("Don't know how to compile {} to {}"
395                                   .format(src, obj))
396
397            args = [self.cc] + compile_opts + pp_opts
398            if add_cpp_opts:
399                args.append('/EHsc')
400            args.append(input_opt)
401            args.append("/Fo" + obj)
402            args.extend(extra_postargs)
403
404            try:
405                self.spawn(args)
406            except DistutilsExecError as msg:
407                raise CompileError(msg)
408
409        return objects
410
411
412    def create_static_lib(self,
413                          objects,
414                          output_libname,
415                          output_dir=None,
416                          debug=0,
417                          target_lang=None):
418
419        if not self.initialized:
420            self.initialize()
421        objects, output_dir = self._fix_object_args(objects, output_dir)
422        output_filename = self.library_filename(output_libname,
423                                                output_dir=output_dir)
424
425        if self._need_link(objects, output_filename):
426            lib_args = objects + ['/OUT:' + output_filename]
427            if debug:
428                pass # XXX what goes here?
429            try:
430                log.debug('Executing "%s" %s', self.lib, ' '.join(lib_args))
431                self.spawn([self.lib] + lib_args)
432            except DistutilsExecError as msg:
433                raise LibError(msg)
434        else:
435            log.debug("skipping %s (up-to-date)", output_filename)
436
437
438    def link(self,
439             target_desc,
440             objects,
441             output_filename,
442             output_dir=None,
443             libraries=None,
444             library_dirs=None,
445             runtime_library_dirs=None,
446             export_symbols=None,
447             debug=0,
448             extra_preargs=None,
449             extra_postargs=None,
450             build_temp=None,
451             target_lang=None):
452
453        if not self.initialized:
454            self.initialize()
455        objects, output_dir = self._fix_object_args(objects, output_dir)
456        fixed_args = self._fix_lib_args(libraries, library_dirs,
457                                        runtime_library_dirs)
458        libraries, library_dirs, runtime_library_dirs = fixed_args
459
460        if runtime_library_dirs:
461            self.warn("I don't know what to do with 'runtime_library_dirs': "
462                       + str(runtime_library_dirs))
463
464        lib_opts = gen_lib_options(self,
465                                   library_dirs, runtime_library_dirs,
466                                   libraries)
467        if output_dir is not None:
468            output_filename = os.path.join(output_dir, output_filename)
469
470        if self._need_link(objects, output_filename):
471            ldflags = self._ldflags[target_desc, debug]
472
473            export_opts = ["/EXPORT:" + sym for sym in (export_symbols or [])]
474
475            ld_args = (ldflags + lib_opts + export_opts +
476                       objects + ['/OUT:' + output_filename])
477
478            # The MSVC linker generates .lib and .exp files, which cannot be
479            # suppressed by any linker switches. The .lib files may even be
480            # needed! Make sure they are generated in the temporary build
481            # directory. Since they have different names for debug and release
482            # builds, they can go into the same directory.
483            build_temp = os.path.dirname(objects[0])
484            if export_symbols is not None:
485                (dll_name, dll_ext) = os.path.splitext(
486                    os.path.basename(output_filename))
487                implib_file = os.path.join(
488                    build_temp,
489                    self.library_filename(dll_name))
490                ld_args.append ('/IMPLIB:' + implib_file)
491
492            if extra_preargs:
493                ld_args[:0] = extra_preargs
494            if extra_postargs:
495                ld_args.extend(extra_postargs)
496
497            output_dir = os.path.dirname(os.path.abspath(output_filename))
498            self.mkpath(output_dir)
499            try:
500                log.debug('Executing "%s" %s', self.linker, ' '.join(ld_args))
501                self.spawn([self.linker] + ld_args)
502            except DistutilsExecError as msg:
503                raise LinkError(msg)
504        else:
505            log.debug("skipping %s (up-to-date)", output_filename)
506
507    def spawn(self, cmd):
508        env = dict(os.environ, PATH=self._paths)
509        with self._fallback_spawn(cmd, env) as fallback:
510            return super().spawn(cmd, env=env)
511        return fallback.value
512
513    @contextlib.contextmanager
514    def _fallback_spawn(self, cmd, env):
515        """
516        Discovered in pypa/distutils#15, some tools monkeypatch the compiler,
517        so the 'env' kwarg causes a TypeError. Detect this condition and
518        restore the legacy, unsafe behavior.
519        """
520        bag = type('Bag', (), {})()
521        try:
522            yield bag
523        except TypeError as exc:
524            if "unexpected keyword argument 'env'" not in str(exc):
525                raise
526        else:
527            return
528        warnings.warn(
529            "Fallback spawn triggered. Please update distutils monkeypatch.")
530        with unittest.mock.patch('os.environ', env):
531            bag.value = super().spawn(cmd)
532
533    # -- Miscellaneous methods -----------------------------------------
534    # These are all used by the 'gen_lib_options() function, in
535    # ccompiler.py.
536
537    def library_dir_option(self, dir):
538        return "/LIBPATH:" + dir
539
540    def runtime_library_dir_option(self, dir):
541        raise DistutilsPlatformError(
542              "don't know how to set runtime library search path for MSVC")
543
544    def library_option(self, lib):
545        return self.library_filename(lib)
546
547    def find_library_file(self, dirs, lib, debug=0):
548        # Prefer a debugging library if found (and requested), but deal
549        # with it if we don't have one.
550        if debug:
551            try_names = [lib + "_d", lib]
552        else:
553            try_names = [lib]
554        for dir in dirs:
555            for name in try_names:
556                libfile = os.path.join(dir, self.library_filename(name))
557                if os.path.isfile(libfile):
558                    return libfile
559        else:
560            # Oops, didn't find it in *any* of 'dirs'
561            return None
562