blob: 724b3fbc9c6c2c043897af0f3ed6cd03ac41b2c3 [file]
# SPDX-License-Identifier: Apache-2.0
# Copyright 2019 The meson development team
# Copyright © 2023 Intel Corporation
from __future__ import annotations
"""Abstractions to simplify compilers that implement an MSVC compatible
interface.
"""
import os
import typing as T
from ... import arglist
from ... import mesonlib
from mesonbuild.compilers.compilers import CompileCheckMode
from ...options import OptionKey
from mesonbuild.linkers.linkers import ClangClDynamicLinker, MSVCDynamicLinker
if T.TYPE_CHECKING:
from ...build import BuildTarget
from .clike import CLikeCompiler as Compiler
else:
# This is a bit clever, for mypy we pretend that these mixins descend from
# Compiler, so we get all of the methods and attributes defined for us, but
# for runtime we make them descend from object (which all classes normally
# do). This gives up DRYer type checking, with no runtime impact
Compiler = object
vs32_instruction_set_args: T.Dict[str, T.Optional[T.List[str]]] = {
'mmx': ['/arch:SSE'], # There does not seem to be a flag just for MMX
'sse': ['/arch:SSE'],
'sse2': ['/arch:SSE2'],
'sse3': ['/arch:AVX'], # VS leaped from SSE2 directly to AVX.
'sse41': ['/arch:AVX'],
'sse42': ['/arch:AVX'],
'avx': ['/arch:AVX'],
'avx2': ['/arch:AVX2'],
'neon': None,
}
# The 64 bit compiler defaults to /arch:avx.
vs64_instruction_set_args: T.Dict[str, T.Optional[T.List[str]]] = {
'mmx': ['/arch:AVX'],
'sse': ['/arch:AVX'],
'sse2': ['/arch:AVX'],
'sse3': ['/arch:AVX'],
'ssse3': ['/arch:AVX'],
'sse41': ['/arch:AVX'],
'sse42': ['/arch:AVX'],
'avx': ['/arch:AVX'],
'avx2': ['/arch:AVX2'],
'neon': None,
}
msvc_optimization_args: T.Dict[str, T.List[str]] = {
'plain': [],
'0': ['/Od'],
'g': [], # No specific flag to optimize debugging, /Zi or /ZI will create debug information
'1': ['/O1'],
'2': ['/O2'],
'3': ['/O2', '/Gw'],
's': ['/O1', '/Gw'],
}
class VisualStudioLikeCompiler(Compiler, metaclass=mesonlib.SimpleABC):
"""A common interface for all compilers implementing an MSVC-style
interface.
A number of compilers attempt to mimic MSVC, with varying levels of
success, such as Clang-CL and ICL (the Intel C/C++ Compiler for Windows).
This class implements as much common logic as possible.
"""
std_warn_args = ['/W3']
std_opt_args = ['/O2']
ignore_libs = arglist.UNIXY_COMPILER_INTERNAL_LIBS + ['execinfo']
internal_libs: T.List[str] = []
crt_args: T.Dict[str, T.List[str]] = {
'none': [],
'md': ['/MD'],
'mdd': ['/MDd'],
'mt': ['/MT'],
'mtd': ['/MTd'],
}
# Assume UTF-8 sources by default, but self.unix_args_to_native() removes it
# if `/source-charset` is set too.
# It is also dropped if Visual Studio 2013 or earlier is used, since it would
# not be supported in that case.
always_args = ['/nologo', '/utf-8']
warn_args: T.Dict[str, T.List[str]] = {
'0': [],
'1': ['/W2'],
'2': ['/W3'],
'3': ['/W4'],
'everything': ['/Wall'],
}
USED_FOR_SEPARATE_LINKING_STEP = False
def __init__(self, target: str):
self.base_options = {OptionKey(o) for o in ['b_pch', 'b_ndebug', 'b_vscrt']} # FIXME add lto, pgo and the like
self.target = target
self.is_64 = ('x64' in target) or ('x86_64' in target)
# do some canonicalization of target machine
if 'x86_64' in target:
self.machine = 'x64'
elif '86' in target:
self.machine = 'x86'
elif 'aarch64' in target:
self.machine = 'arm64'
elif 'arm' in target:
self.machine = 'arm'
else:
self.machine = target
if mesonlib.version_compare(self.version, '>=19.28.29910'): # VS 16.9.0 includes cl 19.28.29910
self.base_options.add(OptionKey('b_sanitize'))
assert self.linker is not None
self.linker.machine = self.machine
# Override CCompiler.get_always_args
def get_always_args(self) -> T.List[str]:
# TODO: use ImmutableListProtocol[str] here instead
return self.always_args.copy()
def get_no_stdinc_args(self) -> T.List[str]:
return ['/X']
def get_no_stdlib_link_args(self) -> T.List[str]:
return ['/NODEFAULTLIB']
def get_pch_suffix(self) -> str:
return 'pch'
def get_pch_name(self, name: str) -> str:
chopped = os.path.basename(name).split('.')[:-1]
chopped.append(self.get_pch_suffix())
pchname = '.'.join(chopped)
return pchname
def get_pch_base_name(self, header: str) -> str:
# This needs to be implemented by inheriting classes
raise NotImplementedError
def get_pch_use_args(self, pch_dir: str, header: str) -> T.List[str]:
base = self.get_pch_base_name(header)
pchname = self.get_pch_name(header)
return ['/FI' + base, '/Yu' + base, '/Fp' + os.path.join(pch_dir, pchname)]
def get_preprocess_only_args(self) -> T.List[str]:
return ['/EP']
def get_preprocess_to_file_args(self) -> T.List[str]:
return ['/EP', '/P']
def get_compile_only_args(self) -> T.List[str]:
return ['/c']
def get_no_optimization_args(self) -> T.List[str]:
return ['/Od', '/Oi-']
def sanitizer_compile_args(self, target: T.Optional[BuildTarget], value: T.List[str]) -> T.List[str]:
if not value:
return value
return [f'/fsanitize={",".join(value)}']
def get_output_args(self, outputname: str) -> T.List[str]:
if self.mode == 'PREPROCESSOR':
return ['/Fi' + outputname]
# hack: this list is not exhaustive
if outputname.endswith(('.exe', '.dll')):
return ['/Fe' + outputname]
return ['/Fo' + outputname]
def get_debug_args(self, is_debug: bool) -> T.List[str]:
if is_debug:
return ['/Z7']
else:
return []
def get_optimization_args(self, optimization_level: str) -> T.List[str]:
args = msvc_optimization_args[optimization_level]
if mesonlib.version_compare(self.version, '<18.0'):
args = [arg for arg in args if arg != '/Gw']
return args
def linker_to_compiler_args(self, args: T.List[str]) -> T.List[str]:
return ['/link'] + [arg for arg in args if arg != '/link']
def get_pic_args(self) -> T.List[str]:
return [] # PIC is handled by the loader on Windows
def gen_pch_args(self, header: str, source: str, pchname: str) -> T.Tuple[str, T.List[str]]:
objname = os.path.splitext(source)[0] + '.obj'
return objname, ['/Yc' + header, '/Fp' + pchname, '/Fo' + objname]
def openmp_flags(self) -> T.List[str]:
return ['/openmp']
def openmp_link_flags(self) -> T.List[str]:
return []
# FIXME, no idea what these should be.
def thread_flags(self) -> T.List[str]:
return []
@classmethod
def include_arg_to_native(cls, opt: str, path: str) -> str:
# msvc does not have a concept of system header dirs.
return f'/I{path}'
@classmethod
def unix_args_to_native(cls, args: T.List[str]) -> T.List[str]:
result: T.List[str] = []
prev = None
for i in args:
if prev:
i = cls.include_arg_to_native(prev, i)
prev = None
# -mms-bitfields is specific to MinGW-GCC
# -pthread is only valid for GCC
elif i in {'-mms-bitfields', '-pthread'}:
continue
elif i.startswith('-LIBPATH:'):
i = '/LIBPATH:' + i[9:]
elif i.startswith('-L'):
i = '/LIBPATH:' + i[2:]
# Translate GNU-style -lfoo library name to the import library
elif i.startswith('-l'):
name = i[2:]
if name in cls.ignore_libs:
# With MSVC, these are provided by the C runtime which is
# linked in by default
continue
else:
i = name + '.lib'
elif i.startswith(('-iquote=', '-isystem=', '-idirafter=')):
opt, i = i.split('=', 1)
i = cls.include_arg_to_native(opt, i)
elif i in {'-iquote', '-isystem', '-idirafter'}:
prev = i
continue
elif i.startswith('-iquote'):
i = cls.include_arg_to_native('-iquote', i[7:])
elif i.startswith('-isystem'):
i = cls.include_arg_to_native('-isystem', i[8:])
elif i.startswith('-idirafter'):
i = cls.include_arg_to_native('-idirafter', i[10:])
# cl.exe does not allow specifying both, so remove /utf-8 that we
# added automatically in the case the user overrides it manually.
elif (i.startswith('/source-charset:')
or i.startswith('/execution-charset:')
or i == '/validate-charset-'):
try:
result.remove('/utf-8')
except ValueError:
pass
result.append(i)
return result
@classmethod
def native_args_to_unix(cls, args: T.List[str]) -> T.List[str]:
result: T.List[str] = []
for arg in args:
if arg.startswith(('/LIBPATH:', '-LIBPATH:')):
result.append('-L' + arg[9:])
elif arg.endswith(('.a', '.lib')) and not mesonlib.path_has_root(arg):
result.append('-l' + arg)
else:
result.append(arg)
return result
def get_werror_args(self) -> T.List[str]:
return ['/WX']
def get_include_args(self, path: str, is_system: bool) -> T.List[str]:
if path == '':
path = '.'
if is_system:
# fixed up by unix_args_to_native() for Microsoft cl.exe
return ['-isystem', path]
return ['-I' + path]
def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str], build_dir: str) -> T.List[str]:
for idx, i in enumerate(parameter_list):
if i[:2] == '-I' or i[:2] == '/I':
parameter_list[idx] = i[:2] + os.path.normpath(os.path.join(build_dir, i[2:]))
elif i[:9] == '/LIBPATH:':
parameter_list[idx] = i[:9] + os.path.normpath(os.path.join(build_dir, i[9:]))
return parameter_list
# Visual Studio is special. It ignores some arguments it does not
# understand and you can't tell it to error out on those.
# http://stackoverflow.com/questions/15259720/how-can-i-make-the-microsoft-c-compiler-treat-unknown-flags-as-errors-rather-t
def has_arguments(self, args: T.List[str], code: str, mode: CompileCheckMode) -> T.Tuple[bool, bool]:
warning_text = '4044' if mode == CompileCheckMode.LINK else '9002'
with self._build_wrapper(code, extra_args=args, mode=mode) as p:
if p.returncode != 0:
return False, p.cached
return not (warning_text in p.stderr or warning_text in p.stdout), p.cached
def get_compile_debugfile_args(self, rel_obj: str, pch: bool = False) -> T.List[str]:
pdbarr = rel_obj.split('.')[:-1]
pdbarr += ['pdb']
args = ['/Fd' + '.'.join(pdbarr)]
return args
def get_instruction_set_args(self, instruction_set: str) -> T.Optional[T.List[str]]:
if self.is_64:
return vs64_instruction_set_args.get(instruction_set, None)
return vs32_instruction_set_args.get(instruction_set, None)
def get_default_include_dirs(self) -> T.List[str]:
if 'INCLUDE' not in os.environ:
return []
return os.environ['INCLUDE'].split(os.pathsep)
def get_crt_compile_args(self, crt_val: str) -> T.List[str]:
crt_val = self.get_crt_val(crt_val)
return self.crt_args[crt_val]
def has_func_attribute(self, name: str) -> T.Tuple[bool, bool]:
# MSVC doesn't have __attribute__ like Clang and GCC do, so just return
# false without compiling anything
return name in {'dllimport', 'dllexport'}, False
@staticmethod
def get_argument_syntax() -> str:
return 'msvc'
def symbols_have_underscore_prefix(self) -> bool:
'''
Check if the compiler prefixes an underscore to global C symbols.
This overrides the Clike method, as for MSVC checking the
underscore prefix based on the compiler define never works,
so do not even try.
'''
# Try to consult a hardcoded list of cases we know
# absolutely have an underscore prefix
result = self._symbols_have_underscore_prefix_list()
if result is not None:
return result
# As a last resort, try search in a compiled binary
return self._symbols_have_underscore_prefix_searchbin()
def get_pie_args(self) -> T.List[str]:
return []
def get_show_dep_args(self) -> T.List[str]:
# /showIncludes is needed for build dependency tracking in Ninja
# See: https://ninja-build.org/manual.html#_deps
return ['/showIncludes']
class MSVCCompiler(VisualStudioLikeCompiler):
"""Specific to the Microsoft Compilers."""
id = 'msvc'
def __init__(self, target: str):
super().__init__(target)
self.base_options.update({OptionKey('b_lto'), OptionKey('b_lto_mode'), OptionKey('b_pgo')})
# Visual Studio 2013 and earlier don't support the /utf-8 argument.
# We want to remove it. We also want to make an explicit copy so we
# don't mutate class constant state
if mesonlib.version_compare(self.version, '<19.00') and '/utf-8' in self.always_args:
self.always_args = [r for r in self.always_args if r != '/utf-8']
def get_compile_debugfile_args(self, rel_obj: str, pch: bool = False) -> T.List[str]:
args = super().get_compile_debugfile_args(rel_obj, pch)
# When generating a PDB file with PCH, all compile commands write
# to the same PDB file. Hence, we need to serialize the PDB
# writes using /FS since we do parallel builds. This slows down the
# build obviously, which is why we only do this when PCH is on.
# This was added in Visual Studio 2013 (MSVC 18.0). Before that it was
# always on: https://msdn.microsoft.com/en-us/library/dn502518.aspx
if pch and mesonlib.version_compare(self.version, '>=18.0'):
args = ['/FS'] + args
return args
# Override CCompiler.get_always_args
# We want to drop '/utf-8' for Visual Studio 2013 and earlier
def get_always_args(self) -> T.List[str]:
return self.always_args
def get_instruction_set_args(self, instruction_set: str) -> T.Optional[T.List[str]]:
if self.version.split('.')[0] == '16' and instruction_set == 'avx':
# VS documentation says that this exists and should work, but
# it does not. The headers do not contain AVX intrinsics
# and they cannot be called.
return None
return super().get_instruction_set_args(instruction_set)
def get_pch_base_name(self, header: str) -> str:
return os.path.basename(header)
# MSVC requires linking to the generated object file when linking a build target
# that uses a precompiled header
def should_link_pch_object(self) -> bool:
return True
def get_lto_compile_args(self, *, target: T.Optional[BuildTarget] = None, threads: int = 0,
mode: str = 'default') -> T.List[str]:
args: T.List[str] = ['/GL']
if mode == 'thin':
args.append('/Gy')
return args
def get_lto_link_args(self, *, target: T.Optional[BuildTarget] = None, threads: int = 0,
mode: str = 'default', thinlto_cache_dir: T.Optional[str] = None) -> T.List[str]:
args: T.List[str] = []
# LTO data generated by MSVC is only usable by link
if not isinstance(self.linker, MSVCDynamicLinker):
raise mesonlib.MesonException(f"MSVC's LTCG only works with link, not {self.linker.id}")
if mode == 'default':
args.append('/LTCG')
elif mode == 'thin':
args.append('/LTCG:INCREMENTAL')
return args
def get_profile_generate_args(self) -> T.List[str]:
if not isinstance(self.linker, MSVCDynamicLinker):
raise mesonlib.MesonException(f"MSVC's PGO only works with link, not {self.linker.id}")
return self.linker_to_compiler_args(['/GENPROFILE'])
def get_profile_use_args(self) -> T.List[str]:
if not isinstance(self.linker, MSVCDynamicLinker):
raise mesonlib.MesonException(f"MSVC's PGO only works with link, not {self.linker.id}")
return self.linker_to_compiler_args(['/USEPROFILE'])
class ClangClCompiler(VisualStudioLikeCompiler):
"""Specific to Clang-CL."""
id = 'clang-cl'
@classmethod
def include_arg_to_native(cls, opt: str, path: str) -> str:
# clang-cl does not seem to like a syntax like -iquote=...
# but unix_args_to_native() canonicalizes opt to not have
# a trailing equals sign
return f'/clang:{opt}{path}'
def __init__(self, target: str):
super().__init__(target)
self.base_options.update(
{OptionKey('b_lto_threads'), OptionKey('b_lto'), OptionKey('b_lto_mode'), OptionKey('b_thinlto_cache'),
OptionKey('b_thinlto_cache_dir')})
# Assembly
self.can_compile_suffixes.add('s')
self.can_compile_suffixes.add('sx')
def sanitizer_compile_args(self, target: T.Optional[BuildTarget], value: T.List[str]) -> T.List[str]:
if not value:
return value
args = ['/clang:-fsanitize=' + ','.join(value)]
if 'address' in value:
args.append('/clang:-fno-omit-frame-pointer')
return args
def get_compiler_check_args(self, mode: CompileCheckMode) -> T.List[str]:
myargs: T.List[str] = []
if mode != CompileCheckMode.LINK:
myargs.extend((
'-Werror=unknown-argument',
'-Werror=unknown-warning-option',
'-Werror=unused-command-line-argument',
))
return super().get_compiler_check_args(mode) + myargs
def get_pch_base_name(self, header: str) -> str:
return header
@classmethod
def use_linker_args(cls, linker: str, version: str) -> T.List[str]:
# Clang additionally can use a linker specified as a path, unlike MSVC.
if linker == 'lld-link':
return ['-fuse-ld=lld-link']
return super().use_linker_args(linker, version)
def linker_to_compiler_args(self, args: T.List[str]) -> T.List[str]:
# clang-cl forwards arguments span-wise with the /LINK flag
# therefore -Wl will be received by lld-link or LINK and rejected
return super().use_linker_args(self.linker.id, '') + super().linker_to_compiler_args([flag[4:] if flag.startswith('-Wl,') else flag for flag in args])
def openmp_link_flags(self) -> T.List[str]:
# see https://github.com/mesonbuild/meson/issues/5298
libs = self.find_library('libomp', [])
if libs is None:
raise mesonlib.MesonBugException('Could not find libomp')
return super().openmp_link_flags() + libs
def get_lto_compile_args(self, *, target: T.Optional[BuildTarget] = None, threads: int = 0,
mode: str = 'default') -> T.List[str]:
args: T.List[str] = []
if mode == 'thin':
# LTO data generated by clang-cl is only usable by lld-link
if not isinstance(self.linker, ClangClDynamicLinker):
raise mesonlib.MesonException(f"LLVM's ThinLTO only works with lld-link, not {self.linker.id}")
args.append(f'-flto={mode}')
else:
assert mode == 'default', 'someone forgot to wire something up'
args.extend(super().get_lto_compile_args(target=target, threads=threads))
return args
def get_lto_link_args(self, *, target: T.Optional[BuildTarget] = None, threads: int = 0,
mode: str = 'default', thinlto_cache_dir: T.Optional[str] = None) -> T.List[str]:
args = []
if mode == 'thin' and thinlto_cache_dir is not None:
args.extend(self.linker.get_thinlto_cache_args(thinlto_cache_dir))
# lld-link /threads:N has the same behaviour as -flto-jobs=N in lld
if threads > 0:
# clang-cl was released after clang already had LTO support, so it
# is safe to assume that all versions of clang-cl support LTO
args.append(f'/threads:{threads}')
return args