# SPDX-License-Identifier: Apache-2.0 # Copyright 2019-2022 The meson development team from __future__ import annotations """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 from mesonbuild.compilers.compilers import CompileCheckMode 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: T.Dict[bool, T.List[str]] = { False: [], True: ['-g'], } gnu_optimization_args: T.Dict[str, T.List[str]] = { 'plain': [], '0': ['-O0'], 'g': ['-Og'], '1': ['-O1'], '2': ['-O2'], '3': ['-O3'], 's': ['-Os'], } gnulike_instruction_set_args: T.Dict[str, T.List[str]] = { 'mmx': ['-mmmx'], 'sse': ['-msse'], 'sse2': ['-msse2'], 'sse3': ['-msse3'], 'ssse3': ['-mssse3'], 'sse41': ['-msse4.1'], 'sse42': ['-msse4.2'], 'avx': ['-mavx'], 'avx2': ['-mavx2'], 'neon': ['-mfpu=neon'], } gnu_symbol_visibility_args: T.Dict[str, T.List[str]] = { '': [], 'default': ['-fvisibility=default'], 'internal': ['-fvisibility=internal'], 'hidden': ['-fvisibility=hidden'], 'protected': ['-fvisibility=protected'], 'inlineshidden': ['-fvisibility=hidden', '-fvisibility-inlines-hidden'], } gnu_color_args: T.Dict[str, T.List[str]] = { 'auto': ['-fdiagnostics-color=auto'], 'always': ['-fdiagnostics-color=always'], 'never': ['-fdiagnostics-color=never'], } # Warnings collected from the GCC source and documentation. This is an # objective set of all the warnings flags that apply to general projects: the # only ones omitted are those that require a project-specific value, or are # related to non-standard or legacy language support. This behaves roughly # like -Weverything in clang. Warnings implied by -Wall, -Wextra, or # higher-level warnings already enabled here are not included in these lists to # keep them as short as possible. History goes back to GCC 3.0.0, everything # earlier is considered historical and listed under version 0.0.0. # GCC warnings for all C-family languages # Omitted non-general warnings: # -Wabi= # -Waggregate-return # -Walloc-size-larger-than=BYTES # -Walloca-larger-than=BYTES # -Wframe-larger-than=BYTES # -Wlarger-than=BYTES # -Wstack-usage=BYTES # -Wsystem-headers # -Wtrampolines # -Wvla-larger-than=BYTES # # Omitted warnings enabled elsewhere in meson: # -Winvalid-pch (GCC 3.4.0) gnu_common_warning_args: T.Dict[str, T.List[str]] = { "0.0.0": [ "-Wcast-qual", "-Wconversion", "-Wfloat-equal", "-Wformat=2", "-Winline", "-Wmissing-declarations", "-Wredundant-decls", "-Wshadow", "-Wundef", "-Wuninitialized", "-Wwrite-strings", ], "3.0.0": [ "-Wdisabled-optimization", "-Wpacked", "-Wpadded", ], "3.3.0": [ "-Wmultichar", "-Wswitch-default", "-Wswitch-enum", "-Wunused-macros", ], "4.0.0": [ "-Wmissing-include-dirs", ], "4.1.0": [ "-Wunsafe-loop-optimizations", "-Wstack-protector", ], "4.2.0": [ "-Wstrict-overflow=5", ], "4.3.0": [ "-Warray-bounds=2", "-Wlogical-op", "-Wstrict-aliasing=3", "-Wvla", ], "4.6.0": [ "-Wdouble-promotion", "-Wsuggest-attribute=const", "-Wsuggest-attribute=noreturn", "-Wsuggest-attribute=pure", "-Wtrampolines", ], "4.7.0": [ "-Wvector-operation-performance", ], "4.8.0": [ "-Wsuggest-attribute=format", ], "4.9.0": [ "-Wdate-time", ], "5.1.0": [ "-Wformat-signedness", "-Wnormalized=nfc", ], "6.1.0": [ "-Wduplicated-cond", "-Wnull-dereference", "-Wshift-negative-value", "-Wshift-overflow=2", "-Wunused-const-variable=2", ], "7.1.0": [ "-Walloca", "-Walloc-zero", "-Wformat-overflow=2", "-Wformat-truncation=2", "-Wstringop-overflow=3", ], "7.2.0": [ "-Wduplicated-branches", ], "8.1.0": [ "-Wcast-align=strict", "-Wsuggest-attribute=cold", "-Wsuggest-attribute=malloc", ], "9.1.0": [ "-Wattribute-alias=2", ], "10.1.0": [ "-Wanalyzer-too-complex", "-Warith-conversion", ], "12.1.0": [ "-Wbidi-chars=ucn", "-Wopenacc-parallelism", "-Wtrivial-auto-var-init", ], } # GCC warnings for C # Omitted non-general or legacy warnings: # -Wc11-c2x-compat # -Wc90-c99-compat # -Wc99-c11-compat # -Wdeclaration-after-statement # -Wtraditional # -Wtraditional-conversion gnu_c_warning_args: T.Dict[str, T.List[str]] = { "0.0.0": [ "-Wbad-function-cast", "-Wmissing-prototypes", "-Wnested-externs", "-Wstrict-prototypes", ], "3.4.0": [ "-Wold-style-definition", "-Winit-self", ], "4.1.0": [ "-Wc++-compat", ], "4.5.0": [ "-Wunsuffixed-float-constants", ], } # GCC warnings for C++ # Omitted non-general or legacy warnings: # -Wc++0x-compat # -Wc++1z-compat # -Wc++2a-compat # -Wctad-maybe-unsupported # -Wnamespaces # -Wtemplates gnu_cpp_warning_args: T.Dict[str, T.List[str]] = { "0.0.0": [ "-Wctor-dtor-privacy", "-Weffc++", "-Wnon-virtual-dtor", "-Wold-style-cast", "-Woverloaded-virtual", "-Wsign-promo", ], "4.0.1": [ "-Wstrict-null-sentinel", ], "4.6.0": [ "-Wnoexcept", ], "4.7.0": [ "-Wzero-as-null-pointer-constant", ], "4.8.0": [ "-Wabi-tag", "-Wuseless-cast", ], "4.9.0": [ "-Wconditionally-supported", ], "5.1.0": [ "-Wsuggest-final-methods", "-Wsuggest-final-types", "-Wsuggest-override", ], "6.1.0": [ "-Wmultiple-inheritance", "-Wplacement-new=2", "-Wvirtual-inheritance", ], "7.1.0": [ "-Waligned-new=all", "-Wnoexcept-type", "-Wregister", ], "8.1.0": [ "-Wcatch-value=3", "-Wextra-semi", ], "9.1.0": [ "-Wdeprecated-copy-dtor", "-Wredundant-move", ], "10.1.0": [ "-Wcomma-subscript", "-Wmismatched-tags", "-Wredundant-tags", "-Wvolatile", ], "11.1.0": [ "-Wdeprecated-enum-enum-conversion", "-Wdeprecated-enum-float-conversion", "-Winvalid-imported-macros", ], } # GCC warnings for Objective C and Objective C++ # Omitted non-general or legacy warnings: # -Wtraditional # -Wtraditional-conversion gnu_objc_warning_args: T.Dict[str, T.List[str]] = { "0.0.0": [ "-Wselector", ], "3.3": [ "-Wundeclared-selector", ], "4.1.0": [ "-Wassign-intercept", "-Wstrict-selector-match", ], } gnu_lang_map = { 'c': 'c', 'cpp': 'c++', 'objc': 'objective-c', 'objcpp': 'objective-c++' } @functools.lru_cache(maxsize=None) def gnulike_default_include_dirs(compiler: T.Tuple[str, ...], lang: str) -> 'ImmutableListProtocol[str]': if lang not in gnu_lang_map: return [] lang = gnu_lang_map[lang] env = os.environ.copy() env["LC_ALL"] = 'C' cmd = list(compiler) + [f'-x{lang}', '-E', '-v', '-'] _, stdout, _ = mesonlib.Popen_safe(cmd, stderr=subprocess.STDOUT, env=env) parse_state = 0 paths: 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') self.can_compile_suffixes.add('sx') 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'] @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.get_exelist(ccache=False)), self.language).copy() @abc.abstractmethod def openmp_flags(self, env: Environment) -> T.List[str]: pass def gnu_symbol_visibility_args(self, vistype: str) -> T.List[str]: if vistype == 'inlineshidden' and self.language not in {'cpp', 'objcpp'}: vistype = 'hidden' 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'] 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=CompileCheckMode.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: T.List[str] = [] 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) if pobj.exists(): try: resolved = pobj.resolve(True).as_posix() if resolved not in result: result.append(resolved) except FileNotFoundError: pass unresolved = pobj.as_posix() if unresolved not in result: result.append(unresolved) 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, outputname: str) -> T.List[str]: return ['-o', outputname] 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, version: 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'] def get_preprocess_to_file_args(self) -> T.List[str]: # We want to allow preprocessing files with any extension, such as # foo.c.in. In that case we need to tell GCC/CLANG to treat them as # assembly file. lang = gnu_lang_map.get(self.language, 'assembler-with-cpp') return self.get_preprocess_only_args() + [f'-x{lang}'] 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 supported_warn_args(self, warn_args_by_version: T.Dict[str, T.List[str]]) -> T.List[str]: result: T.List[str] = [] for version, warn_args in warn_args_by_version.items(): if mesonlib.version_compare(self.version, '>=' + version): result += warn_args return result 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, env: Environment) -> T.List[str]: return ['-fopenmp'] def has_arguments(self, args: T.List[str], env: 'Environment', code: str, mode: CompileCheckMode) -> 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) @classmethod def use_linker_args(cls, linker: str, version: str) -> T.List[str]: if linker == 'mold' and mesonlib.version_compare(version, '>=12.0.1'): return ['-fuse-ld=mold'] return super().use_linker_args(linker, version) def get_profile_use_args(self) -> T.List[str]: return super().get_profile_use_args() + ['-fprofile-correction']