# Copyright 2019 The meson development team # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Provides mixins for GNU compilers and GNU-like compilers.""" import abc import functools import os import multiprocessing import pathlib import re import subprocess import typing as T from ... import mesonlib from ... import mlog from ...mesonlib import OptionKey if T.TYPE_CHECKING: from ..._typing import ImmutableListProtocol from ...environment import Environment from ..compilers import 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 # XXX: prevent circular references. # FIXME: this really is a posix interface not a c-like interface clike_debug_args = { False: [], True: ['-g'], } # type: T.Dict[bool, T.List[str]] gnulike_buildtype_args = { 'plain': [], 'debug': [], 'debugoptimized': [], 'release': [], 'minsize': [], 'custom': [], } # type: T.Dict[str, T.List[str]] gnu_optimization_args = { '0': [], 'g': ['-Og'], '1': ['-O1'], '2': ['-O2'], '3': ['-O3'], 's': ['-Os'], } # type: T.Dict[str, T.List[str]] gnulike_instruction_set_args = { 'mmx': ['-mmmx'], 'sse': ['-msse'], 'sse2': ['-msse2'], 'sse3': ['-msse3'], 'ssse3': ['-mssse3'], 'sse41': ['-msse4.1'], 'sse42': ['-msse4.2'], 'avx': ['-mavx'], 'avx2': ['-mavx2'], 'neon': ['-mfpu=neon'], } # type: T.Dict[str, T.List[str]] gnu_symbol_visibility_args = { '': [], 'default': ['-fvisibility=default'], 'internal': ['-fvisibility=internal'], 'hidden': ['-fvisibility=hidden'], 'protected': ['-fvisibility=protected'], 'inlineshidden': ['-fvisibility=hidden', '-fvisibility-inlines-hidden'], } # type: T.Dict[str, T.List[str]] gnu_color_args = { 'auto': ['-fdiagnostics-color=auto'], 'always': ['-fdiagnostics-color=always'], 'never': ['-fdiagnostics-color=never'], } # type: T.Dict[str, T.List[str]] @functools.lru_cache(maxsize=None) def gnulike_default_include_dirs(compiler: T.Tuple[str, ...], lang: str) -> 'ImmutableListProtocol[str]': lang_map = { 'c': 'c', 'cpp': 'c++', 'objc': 'objective-c', 'objcpp': 'objective-c++' } if lang not in lang_map: return [] lang = lang_map[lang] env = os.environ.copy() env["LC_ALL"] = 'C' cmd = list(compiler) + [f'-x{lang}', '-E', '-v', '-'] p = subprocess.Popen( cmd, stdin=subprocess.DEVNULL, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, env=env ) stdout = p.stdout.read().decode('utf-8', errors='replace') parse_state = 0 paths = [] # type: T.List[str] for line in stdout.split('\n'): line = line.strip(' \n\r\t') if parse_state == 0: if line == '#include "..." search starts here:': parse_state = 1 elif parse_state == 1: if line == '#include <...> search starts here:': parse_state = 2 else: paths.append(line) elif parse_state == 2: if line == 'End of search list.': break else: paths.append(line) if not paths: mlog.warning('No include directory found parsing "{cmd}" output'.format(cmd=" ".join(cmd))) # Append a normalized copy of paths to make path lookup easier paths += [os.path.normpath(x) for x in paths] return paths class GnuLikeCompiler(Compiler, metaclass=abc.ABCMeta): """ GnuLikeCompiler is a common interface to all compilers implementing the GNU-style commandline interface. This includes GCC, Clang and ICC. Certain functionality between them is different and requires that the actual concrete subclass define their own implementation. """ LINKER_PREFIX = '-Wl,' def __init__(self) -> None: self.base_options = { OptionKey(o) for o in ['b_pch', 'b_lto', 'b_pgo', 'b_coverage', 'b_ndebug', 'b_staticpic', 'b_pie']} if not (self.info.is_windows() or self.info.is_cygwin() or self.info.is_openbsd()): self.base_options.add(OptionKey('b_lundef')) if not self.info.is_windows() or self.info.is_cygwin(): self.base_options.add(OptionKey('b_asneeded')) if not self.info.is_hurd(): self.base_options.add(OptionKey('b_sanitize')) # All GCC-like backends can do assembly self.can_compile_suffixes.add('s') def get_pic_args(self) -> T.List[str]: if self.info.is_windows() or self.info.is_cygwin() or self.info.is_darwin(): return [] # On Window and OS X, pic is always on. return ['-fPIC'] def get_pie_args(self) -> T.List[str]: return ['-fPIE'] def get_buildtype_args(self, buildtype: str) -> T.List[str]: return gnulike_buildtype_args[buildtype] @abc.abstractmethod def get_optimization_args(self, optimization_level: str) -> T.List[str]: pass def get_debug_args(self, is_debug: bool) -> T.List[str]: return clike_debug_args[is_debug] @abc.abstractmethod def get_pch_suffix(self) -> str: pass def split_shlib_to_parts(self, fname: str) -> T.Tuple[str, str]: return os.path.dirname(fname), fname def get_instruction_set_args(self, instruction_set: str) -> T.Optional[T.List[str]]: return gnulike_instruction_set_args.get(instruction_set, None) def get_default_include_dirs(self) -> T.List[str]: return gnulike_default_include_dirs(tuple(self.exelist), self.language).copy() @abc.abstractmethod def openmp_flags(self) -> T.List[str]: pass def gnu_symbol_visibility_args(self, vistype: str) -> T.List[str]: return gnu_symbol_visibility_args[vistype] def gen_vs_module_defs_args(self, defsfile: str) -> T.List[str]: if not isinstance(defsfile, str): raise RuntimeError('Module definitions file should be str') # On Windows targets, .def files may be specified on the linker command # line like an object file. if self.info.is_windows() or self.info.is_cygwin(): return [defsfile] # For other targets, discard the .def file. return [] def get_argument_syntax(self) -> str: return 'gcc' def get_profile_generate_args(self) -> T.List[str]: return ['-fprofile-generate'] def get_profile_use_args(self) -> T.List[str]: return ['-fprofile-use', '-fprofile-correction'] def get_gui_app_args(self, value: bool) -> T.List[str]: return ['-mwindows' if value else '-mconsole'] 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] == '-L': parameter_list[idx] = i[:2] + os.path.normpath(os.path.join(build_dir, i[2:])) return parameter_list @functools.lru_cache() def _get_search_dirs(self, env: 'Environment') -> str: extra_args = ['--print-search-dirs'] with self._build_wrapper('', env, extra_args=extra_args, dependencies=None, mode='compile', want_output=True) as p: return p.stdout def _split_fetch_real_dirs(self, pathstr: str) -> T.List[str]: # We need to use the path separator used by the compiler for printing # lists of paths ("gcc --print-search-dirs"). By default # we assume it uses the platform native separator. pathsep = os.pathsep # clang uses ':' instead of ';' on Windows https://reviews.llvm.org/D61121 # so we need to repair things like 'C:\foo:C:\bar' if pathsep == ';': pathstr = re.sub(r':([^/\\])', r';\1', pathstr) # pathlib treats empty paths as '.', so filter those out paths = [p for p in pathstr.split(pathsep) if p] result = [] for p in paths: # GCC returns paths like this: # /usr/lib/gcc/x86_64-linux-gnu/8/../../../../x86_64-linux-gnu/lib # It would make sense to normalize them to get rid of the .. parts # Sadly when you are on a merged /usr fs it also kills these: # /lib/x86_64-linux-gnu # since /lib is a symlink to /usr/lib. This would mean # paths under /lib would be considered not a "system path", # which is wrong and breaks things. Store everything, just to be sure. pobj = pathlib.Path(p) unresolved = pobj.as_posix() if pobj.exists(): if unresolved not in result: result.append(unresolved) try: resolved = pathlib.Path(p).resolve().as_posix() if resolved not in result: result.append(resolved) except FileNotFoundError: pass return result def get_compiler_dirs(self, env: 'Environment', name: str) -> T.List[str]: ''' Get dirs from the compiler, either `libraries:` or `programs:` ''' stdo = self._get_search_dirs(env) for line in stdo.split('\n'): if line.startswith(name + ':'): return self._split_fetch_real_dirs(line.split('=', 1)[1]) return [] def get_lto_compile_args(self, *, threads: int = 0, mode: str = 'default') -> T.List[str]: # This provides a base for many compilers, GCC and Clang override this # for their specific arguments return ['-flto'] def sanitizer_compile_args(self, value: str) -> T.List[str]: if value == 'none': return [] args = ['-fsanitize=' + value] if 'address' in value: # for -fsanitize=address,undefined args.append('-fno-omit-frame-pointer') return args def get_output_args(self, target: str) -> T.List[str]: return ['-o', target] def get_dependency_gen_args(self, outtarget: str, outfile: str) -> T.List[str]: return ['-MD', '-MQ', outtarget, '-MF', outfile] def get_compile_only_args(self) -> T.List[str]: return ['-c'] def get_include_args(self, path: str, is_system: bool) -> T.List[str]: if not path: path = '.' if is_system: return ['-isystem' + path] return ['-I' + path] @classmethod def use_linker_args(cls, linker: str) -> T.List[str]: if linker not in {'gold', 'bfd', 'lld'}: raise mesonlib.MesonException( f'Unsupported linker, only bfd, gold, and lld are supported, not {linker}.') return [f'-fuse-ld={linker}'] def get_coverage_args(self) -> T.List[str]: return ['--coverage'] class GnuCompiler(GnuLikeCompiler): """ GnuCompiler represents an actual GCC in its many incarnations. Compilers imitating GCC (Clang/Intel) should use the GnuLikeCompiler ABC. """ id = 'gcc' def __init__(self, defines: T.Optional[T.Dict[str, str]]): super().__init__() self.defines = defines or {} self.base_options.update({OptionKey('b_colorout'), OptionKey('b_lto_threads')}) def get_colorout_args(self, colortype: str) -> T.List[str]: if mesonlib.version_compare(self.version, '>=4.9.0'): return gnu_color_args[colortype][:] return [] def get_warn_args(self, level: str) -> T.List[str]: # Mypy doesn't understand cooperative inheritance args = super().get_warn_args(level) if mesonlib.version_compare(self.version, '<4.8.0') and '-Wpedantic' in args: # -Wpedantic was added in 4.8.0 # https://gcc.gnu.org/gcc-4.8/changes.html args[args.index('-Wpedantic')] = '-pedantic' return args def has_builtin_define(self, define: str) -> bool: return define in self.defines def get_builtin_define(self, define: str) -> T.Optional[str]: if define in self.defines: return self.defines[define] return None def get_optimization_args(self, optimization_level: str) -> T.List[str]: return gnu_optimization_args[optimization_level] def get_pch_suffix(self) -> str: return 'gch' def openmp_flags(self) -> T.List[str]: return ['-fopenmp'] def has_arguments(self, args: T.List[str], env: 'Environment', code: str, mode: str) -> T.Tuple[bool, bool]: # For some compiler command line arguments, the GNU compilers will # emit a warning on stderr indicating that an option is valid for a # another language, but still complete with exit_success with self._build_wrapper(code, env, args, None, mode) as p: result = p.returncode == 0 if self.language in {'cpp', 'objcpp'} and 'is valid for C/ObjC' in p.stderr: result = False if self.language in {'c', 'objc'} and 'is valid for C++/ObjC++' in p.stderr: result = False return result, p.cached def get_has_func_attribute_extra_args(self, name: str) -> T.List[str]: # GCC only warns about unknown or ignored attributes, so force an # error. return ['-Werror=attributes'] def get_prelink_args(self, prelink_name: str, obj_list: T.List[str]) -> T.List[str]: return ['-r', '-o', prelink_name] + obj_list def get_lto_compile_args(self, *, threads: int = 0, mode: str = 'default') -> T.List[str]: if threads == 0: if mesonlib.version_compare(self.version, '>= 10.0'): return ['-flto=auto'] # This matches clang's behavior of using the number of cpus return [f'-flto={multiprocessing.cpu_count()}'] elif threads > 0: return [f'-flto={threads}'] return super().get_lto_compile_args(threads=threads)