1# Copyright 2019 The meson development team
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Abstractions to simplify compilers that implement an MSVC compatible
16interface.
17"""
18
19import abc
20import os
21import typing as T
22
23from ... import arglist
24from ... import mesonlib
25from ... import mlog
26
27if T.TYPE_CHECKING:
28    from ...environment import Environment
29    from .clike import CLikeCompiler as Compiler
30else:
31    # This is a bit clever, for mypy we pretend that these mixins descend from
32    # Compiler, so we get all of the methods and attributes defined for us, but
33    # for runtime we make them descend from object (which all classes normally
34    # do). This gives up DRYer type checking, with no runtime impact
35    Compiler = object
36
37vs32_instruction_set_args = {
38    'mmx': ['/arch:SSE'], # There does not seem to be a flag just for MMX
39    'sse': ['/arch:SSE'],
40    'sse2': ['/arch:SSE2'],
41    'sse3': ['/arch:AVX'], # VS leaped from SSE2 directly to AVX.
42    'sse41': ['/arch:AVX'],
43    'sse42': ['/arch:AVX'],
44    'avx': ['/arch:AVX'],
45    'avx2': ['/arch:AVX2'],
46    'neon': None,
47}  # T.Dicst[str, T.Optional[T.List[str]]]
48
49# The 64 bit compiler defaults to /arch:avx.
50vs64_instruction_set_args = {
51    'mmx': ['/arch:AVX'],
52    'sse': ['/arch:AVX'],
53    'sse2': ['/arch:AVX'],
54    'sse3': ['/arch:AVX'],
55    'ssse3': ['/arch:AVX'],
56    'sse41': ['/arch:AVX'],
57    'sse42': ['/arch:AVX'],
58    'avx': ['/arch:AVX'],
59    'avx2': ['/arch:AVX2'],
60    'neon': None,
61}  # T.Dicst[str, T.Optional[T.List[str]]]
62
63msvc_optimization_args = {
64    '0': ['/Od'],
65    'g': [], # No specific flag to optimize debugging, /Zi or /ZI will create debug information
66    '1': ['/O1'],
67    '2': ['/O2'],
68    '3': ['/O2', '/Gw'],
69    's': ['/O1', '/Gw'],
70}  # type: T.Dict[str, T.List[str]]
71
72msvc_debug_args = {
73    False: [],
74    True: ['/Zi']
75}  # type: T.Dict[bool, T.List[str]]
76
77
78class VisualStudioLikeCompiler(Compiler, metaclass=abc.ABCMeta):
79
80    """A common interface for all compilers implementing an MSVC-style
81    interface.
82
83    A number of compilers attempt to mimic MSVC, with varying levels of
84    success, such as Clang-CL and ICL (the Intel C/C++ Compiler for Windows).
85    This class implements as much common logic as possible.
86    """
87
88    std_warn_args = ['/W3']
89    std_opt_args = ['/O2']
90    ignore_libs = arglist.UNIXY_COMPILER_INTERNAL_LIBS + ['execinfo']
91    internal_libs = []  # type: T.List[str]
92
93    crt_args = {
94        'none': [],
95        'md': ['/MD'],
96        'mdd': ['/MDd'],
97        'mt': ['/MT'],
98        'mtd': ['/MTd'],
99    }  # type: T.Dict[str, T.List[str]]
100
101    # /showIncludes is needed for build dependency tracking in Ninja
102    # See: https://ninja-build.org/manual.html#_deps
103    # Assume UTF-8 sources by default, but self.unix_args_to_native() removes it
104    # if `/source-charset` is set too.
105    always_args = ['/nologo', '/showIncludes', '/utf-8']
106    warn_args = {
107        '0': [],
108        '1': ['/W2'],
109        '2': ['/W3'],
110        '3': ['/W4'],
111    }  # type: T.Dict[str, T.List[str]]
112
113    INVOKES_LINKER = False
114
115    def __init__(self, target: str):
116        self.base_options = {mesonlib.OptionKey(o) for o in ['b_pch', 'b_ndebug', 'b_vscrt']} # FIXME add lto, pgo and the like
117        self.target = target
118        self.is_64 = ('x64' in target) or ('x86_64' in target)
119        # do some canonicalization of target machine
120        if 'x86_64' in target:
121            self.machine = 'x64'
122        elif '86' in target:
123            self.machine = 'x86'
124        elif 'aarch64' in target:
125            self.machine = 'arm64'
126        elif 'arm' in target:
127            self.machine = 'arm'
128        else:
129            self.machine = target
130        if mesonlib.version_compare(self.version, '>=19.28.29910'): # VS 16.9.0 includes cl 19.28.29910
131            self.base_options.add(mesonlib.OptionKey('b_sanitize'))
132        assert self.linker is not None
133        self.linker.machine = self.machine
134
135    # Override CCompiler.get_always_args
136    def get_always_args(self) -> T.List[str]:
137        return self.always_args
138
139    def get_pch_suffix(self) -> str:
140        return 'pch'
141
142    def get_pch_name(self, header: str) -> str:
143        chopped = os.path.basename(header).split('.')[:-1]
144        chopped.append(self.get_pch_suffix())
145        pchname = '.'.join(chopped)
146        return pchname
147
148    def get_pch_base_name(self, header: str) -> str:
149        # This needs to be implemented by inheriting classes
150        raise NotImplementedError
151
152    def get_pch_use_args(self, pch_dir: str, header: str) -> T.List[str]:
153        base = self.get_pch_base_name(header)
154        pchname = self.get_pch_name(header)
155        return ['/FI' + base, '/Yu' + base, '/Fp' + os.path.join(pch_dir, pchname)]
156
157    def get_preprocess_only_args(self) -> T.List[str]:
158        return ['/EP']
159
160    def get_compile_only_args(self) -> T.List[str]:
161        return ['/c']
162
163    def get_no_optimization_args(self) -> T.List[str]:
164        return ['/Od', '/Oi-']
165
166    def sanitizer_compile_args(self, value: str) -> T.List[str]:
167        if value == 'none':
168            return []
169        if value != 'address':
170            raise mesonlib.MesonException('VS only supports address sanitizer at the moment.')
171        return ['/fsanitize=address']
172
173    def get_output_args(self, target: str) -> T.List[str]:
174        if target.endswith('.exe'):
175            return ['/Fe' + target]
176        return ['/Fo' + target]
177
178    def get_buildtype_args(self, buildtype: str) -> T.List[str]:
179        return []
180
181    def get_debug_args(self, is_debug: bool) -> T.List[str]:
182        return msvc_debug_args[is_debug]
183
184    def get_optimization_args(self, optimization_level: str) -> T.List[str]:
185        args = msvc_optimization_args[optimization_level]
186        if mesonlib.version_compare(self.version, '<18.0'):
187            args = [arg for arg in args if arg != '/Gw']
188        return args
189
190    def linker_to_compiler_args(self, args: T.List[str]) -> T.List[str]:
191        return ['/link'] + args
192
193    def get_pic_args(self) -> T.List[str]:
194        return [] # PIC is handled by the loader on Windows
195
196    def gen_vs_module_defs_args(self, defsfile: str) -> T.List[str]:
197        if not isinstance(defsfile, str):
198            raise RuntimeError('Module definitions file should be str')
199        # With MSVC, DLLs only export symbols that are explicitly exported,
200        # so if a module defs file is specified, we use that to export symbols
201        return ['/DEF:' + defsfile]
202
203    def gen_pch_args(self, header: str, source: str, pchname: str) -> T.Tuple[str, T.List[str]]:
204        objname = os.path.splitext(pchname)[0] + '.obj'
205        return objname, ['/Yc' + header, '/Fp' + pchname, '/Fo' + objname]
206
207    def openmp_flags(self) -> T.List[str]:
208        return ['/openmp']
209
210    def openmp_link_flags(self) -> T.List[str]:
211        return []
212
213    # FIXME, no idea what these should be.
214    def thread_flags(self, env: 'Environment') -> T.List[str]:
215        return []
216
217    @classmethod
218    def unix_args_to_native(cls, args: T.List[str]) -> T.List[str]:
219        result: T.List[str] = []
220        for i in args:
221            # -mms-bitfields is specific to MinGW-GCC
222            # -pthread is only valid for GCC
223            if i in ('-mms-bitfields', '-pthread'):
224                continue
225            if i.startswith('-LIBPATH:'):
226                i = '/LIBPATH:' + i[9:]
227            elif i.startswith('-L'):
228                i = '/LIBPATH:' + i[2:]
229            # Translate GNU-style -lfoo library name to the import library
230            elif i.startswith('-l'):
231                name = i[2:]
232                if name in cls.ignore_libs:
233                    # With MSVC, these are provided by the C runtime which is
234                    # linked in by default
235                    continue
236                else:
237                    i = name + '.lib'
238            elif i.startswith('-isystem'):
239                # just use /I for -isystem system include path s
240                if i.startswith('-isystem='):
241                    i = '/I' + i[9:]
242                else:
243                    i = '/I' + i[8:]
244            elif i.startswith('-idirafter'):
245                # same as -isystem, but appends the path instead
246                if i.startswith('-idirafter='):
247                    i = '/I' + i[11:]
248                else:
249                    i = '/I' + i[10:]
250            # -pthread in link flags is only used on Linux
251            elif i == '-pthread':
252                continue
253            # cl.exe does not allow specifying both, so remove /utf-8 that we
254            # added automatically in the case the user overrides it manually.
255            elif i.startswith('/source-charset:') or i.startswith('/execution-charset:'):
256                try:
257                    result.remove('/utf-8')
258                except ValueError:
259                    pass
260            result.append(i)
261        return result
262
263    @classmethod
264    def native_args_to_unix(cls, args: T.List[str]) -> T.List[str]:
265        result = []
266        for arg in args:
267            if arg.startswith(('/LIBPATH:', '-LIBPATH:')):
268                result.append('-L' + arg[9:])
269            elif arg.endswith(('.a', '.lib')) and not os.path.isabs(arg):
270                result.append('-l' + arg)
271            else:
272                result.append(arg)
273        return result
274
275    def get_werror_args(self) -> T.List[str]:
276        return ['/WX']
277
278    def get_include_args(self, path: str, is_system: bool) -> T.List[str]:
279        if path == '':
280            path = '.'
281        # msvc does not have a concept of system header dirs.
282        return ['-I' + path]
283
284    def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str], build_dir: str) -> T.List[str]:
285        for idx, i in enumerate(parameter_list):
286            if i[:2] == '-I' or i[:2] == '/I':
287                parameter_list[idx] = i[:2] + os.path.normpath(os.path.join(build_dir, i[2:]))
288            elif i[:9] == '/LIBPATH:':
289                parameter_list[idx] = i[:9] + os.path.normpath(os.path.join(build_dir, i[9:]))
290
291        return parameter_list
292
293    # Visual Studio is special. It ignores some arguments it does not
294    # understand and you can't tell it to error out on those.
295    # http://stackoverflow.com/questions/15259720/how-can-i-make-the-microsoft-c-compiler-treat-unknown-flags-as-errors-rather-t
296    def has_arguments(self, args: T.List[str], env: 'Environment', code: str, mode: str) -> T.Tuple[bool, bool]:
297        warning_text = '4044' if mode == 'link' else '9002'
298        with self._build_wrapper(code, env, extra_args=args, mode=mode) as p:
299            if p.returncode != 0:
300                return False, p.cached
301            return not(warning_text in p.stderr or warning_text in p.stdout), p.cached
302
303    def get_compile_debugfile_args(self, rel_obj: str, pch: bool = False) -> T.List[str]:
304        pdbarr = rel_obj.split('.')[:-1]
305        pdbarr += ['pdb']
306        args = ['/Fd' + '.'.join(pdbarr)]
307        return args
308
309    def get_instruction_set_args(self, instruction_set: str) -> T.Optional[T.List[str]]:
310        if self.is_64:
311            return vs64_instruction_set_args.get(instruction_set, None)
312        return vs32_instruction_set_args.get(instruction_set, None)
313
314    def _calculate_toolset_version(self, version: int) -> T.Optional[str]:
315        if version < 1310:
316            return '7.0'
317        elif version < 1400:
318            return '7.1' # (Visual Studio 2003)
319        elif version < 1500:
320            return '8.0' # (Visual Studio 2005)
321        elif version < 1600:
322            return '9.0' # (Visual Studio 2008)
323        elif version < 1700:
324            return '10.0' # (Visual Studio 2010)
325        elif version < 1800:
326            return '11.0' # (Visual Studio 2012)
327        elif version < 1900:
328            return '12.0' # (Visual Studio 2013)
329        elif version < 1910:
330            return '14.0' # (Visual Studio 2015)
331        elif version < 1920:
332            return '14.1' # (Visual Studio 2017)
333        elif version < 1930:
334            return '14.2' # (Visual Studio 2019)
335        mlog.warning(f'Could not find toolset for version {self.version!r}')
336        return None
337
338    def get_toolset_version(self) -> T.Optional[str]:
339        # See boost/config/compiler/visualc.cpp for up to date mapping
340        try:
341            version = int(''.join(self.version.split('.')[0:2]))
342        except ValueError:
343            return None
344        return self._calculate_toolset_version(version)
345
346    def get_default_include_dirs(self) -> T.List[str]:
347        if 'INCLUDE' not in os.environ:
348            return []
349        return os.environ['INCLUDE'].split(os.pathsep)
350
351    def get_crt_compile_args(self, crt_val: str, buildtype: str) -> T.List[str]:
352        if crt_val in self.crt_args:
353            return self.crt_args[crt_val]
354        assert crt_val in ['from_buildtype', 'static_from_buildtype']
355        dbg = 'mdd'
356        rel = 'md'
357        if crt_val == 'static_from_buildtype':
358            dbg = 'mtd'
359            rel = 'mt'
360        # Match what build type flags used to do.
361        if buildtype == 'plain':
362            return []
363        elif buildtype == 'debug':
364            return self.crt_args[dbg]
365        elif buildtype == 'debugoptimized':
366            return self.crt_args[rel]
367        elif buildtype == 'release':
368            return self.crt_args[rel]
369        elif buildtype == 'minsize':
370            return self.crt_args[rel]
371        else:
372            assert buildtype == 'custom'
373            raise mesonlib.EnvironmentException('Requested C runtime based on buildtype, but buildtype is "custom".')
374
375    def has_func_attribute(self, name: str, env: 'Environment') -> T.Tuple[bool, bool]:
376        # MSVC doesn't have __attribute__ like Clang and GCC do, so just return
377        # false without compiling anything
378        return name in ['dllimport', 'dllexport'], False
379
380    def get_argument_syntax(self) -> str:
381        return 'msvc'
382
383
384class MSVCCompiler(VisualStudioLikeCompiler):
385
386    """Specific to the Microsoft Compilers."""
387
388    def __init__(self, target: str):
389        super().__init__(target)
390        self.id = 'msvc'
391
392    def get_compile_debugfile_args(self, rel_obj: str, pch: bool = False) -> T.List[str]:
393        args = super().get_compile_debugfile_args(rel_obj, pch)
394        # When generating a PDB file with PCH, all compile commands write
395        # to the same PDB file. Hence, we need to serialize the PDB
396        # writes using /FS since we do parallel builds. This slows down the
397        # build obviously, which is why we only do this when PCH is on.
398        # This was added in Visual Studio 2013 (MSVC 18.0). Before that it was
399        # always on: https://msdn.microsoft.com/en-us/library/dn502518.aspx
400        if pch and mesonlib.version_compare(self.version, '>=18.0'):
401            args = ['/FS'] + args
402        return args
403
404    def get_instruction_set_args(self, instruction_set: str) -> T.Optional[T.List[str]]:
405        if self.version.split('.')[0] == '16' and instruction_set == 'avx':
406            # VS documentation says that this exists and should work, but
407            # it does not. The headers do not contain AVX intrinsics
408            # and they can not be called.
409            return None
410        return super().get_instruction_set_args(instruction_set)
411
412    def get_pch_base_name(self, header: str) -> str:
413        return os.path.basename(header)
414
415
416class ClangClCompiler(VisualStudioLikeCompiler):
417
418    """Specific to Clang-CL."""
419
420    def __init__(self, target: str):
421        super().__init__(target)
422        self.id = 'clang-cl'
423
424        # Assembly
425        self.can_compile_suffixes.add('s')
426
427    def has_arguments(self, args: T.List[str], env: 'Environment', code: str, mode: str) -> T.Tuple[bool, bool]:
428        if mode != 'link':
429            args = args + ['-Werror=unknown-argument']
430        return super().has_arguments(args, env, code, mode)
431
432    def get_toolset_version(self) -> T.Optional[str]:
433        # XXX: what is the right thing to do here?
434        return '14.1'
435
436    def get_pch_base_name(self, header: str) -> str:
437        return header
438