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"""Provides mixins for GNU compilers and GNU-like compilers."""
16
17import abc
18import functools
19import os
20import multiprocessing
21import pathlib
22import re
23import subprocess
24import typing as T
25
26from ... import mesonlib
27from ... import mlog
28from ...mesonlib import OptionKey
29
30if T.TYPE_CHECKING:
31    from ..._typing import ImmutableListProtocol
32    from ...environment import Environment
33    from ..compilers import Compiler
34else:
35    # This is a bit clever, for mypy we pretend that these mixins descend from
36    # Compiler, so we get all of the methods and attributes defined for us, but
37    # for runtime we make them descend from object (which all classes normally
38    # do). This gives up DRYer type checking, with no runtime impact
39    Compiler = object
40
41# XXX: prevent circular references.
42# FIXME: this really is a posix interface not a c-like interface
43clike_debug_args = {
44    False: [],
45    True: ['-g'],
46}  # type: T.Dict[bool, T.List[str]]
47
48gnulike_buildtype_args = {
49    'plain': [],
50    'debug': [],
51    'debugoptimized': [],
52    'release': [],
53    'minsize': [],
54    'custom': [],
55}  # type: T.Dict[str, T.List[str]]
56
57gnu_optimization_args = {
58    '0': [],
59    'g': ['-Og'],
60    '1': ['-O1'],
61    '2': ['-O2'],
62    '3': ['-O3'],
63    's': ['-Os'],
64}  # type: T.Dict[str, T.List[str]]
65
66gnulike_instruction_set_args = {
67    'mmx': ['-mmmx'],
68    'sse': ['-msse'],
69    'sse2': ['-msse2'],
70    'sse3': ['-msse3'],
71    'ssse3': ['-mssse3'],
72    'sse41': ['-msse4.1'],
73    'sse42': ['-msse4.2'],
74    'avx': ['-mavx'],
75    'avx2': ['-mavx2'],
76    'neon': ['-mfpu=neon'],
77}  # type: T.Dict[str, T.List[str]]
78
79gnu_symbol_visibility_args = {
80    '': [],
81    'default': ['-fvisibility=default'],
82    'internal': ['-fvisibility=internal'],
83    'hidden': ['-fvisibility=hidden'],
84    'protected': ['-fvisibility=protected'],
85    'inlineshidden': ['-fvisibility=hidden', '-fvisibility-inlines-hidden'],
86}  # type: T.Dict[str, T.List[str]]
87
88gnu_color_args = {
89    'auto': ['-fdiagnostics-color=auto'],
90    'always': ['-fdiagnostics-color=always'],
91    'never': ['-fdiagnostics-color=never'],
92}  # type: T.Dict[str, T.List[str]]
93
94
95@functools.lru_cache(maxsize=None)
96def gnulike_default_include_dirs(compiler: T.Tuple[str, ...], lang: str) -> 'ImmutableListProtocol[str]':
97    lang_map = {
98        'c': 'c',
99        'cpp': 'c++',
100        'objc': 'objective-c',
101        'objcpp': 'objective-c++'
102    }
103    if lang not in lang_map:
104        return []
105    lang = lang_map[lang]
106    env = os.environ.copy()
107    env["LC_ALL"] = 'C'
108    cmd = list(compiler) + [f'-x{lang}', '-E', '-v', '-']
109    p = subprocess.Popen(
110        cmd,
111        stdin=subprocess.DEVNULL,
112        stderr=subprocess.STDOUT,
113        stdout=subprocess.PIPE,
114        env=env
115    )
116    stdout = p.stdout.read().decode('utf-8', errors='replace')
117    parse_state = 0
118    paths = []  # type: T.List[str]
119    for line in stdout.split('\n'):
120        line = line.strip(' \n\r\t')
121        if parse_state == 0:
122            if line == '#include "..." search starts here:':
123                parse_state = 1
124        elif parse_state == 1:
125            if line == '#include <...> search starts here:':
126                parse_state = 2
127            else:
128                paths.append(line)
129        elif parse_state == 2:
130            if line == 'End of search list.':
131                break
132            else:
133                paths.append(line)
134    if not paths:
135        mlog.warning('No include directory found parsing "{cmd}" output'.format(cmd=" ".join(cmd)))
136    # Append a normalized copy of paths to make path lookup easier
137    paths += [os.path.normpath(x) for x in paths]
138    return paths
139
140
141class GnuLikeCompiler(Compiler, metaclass=abc.ABCMeta):
142    """
143    GnuLikeCompiler is a common interface to all compilers implementing
144    the GNU-style commandline interface. This includes GCC, Clang
145    and ICC. Certain functionality between them is different and requires
146    that the actual concrete subclass define their own implementation.
147    """
148
149    LINKER_PREFIX = '-Wl,'
150
151    def __init__(self) -> None:
152        self.base_options = {
153            OptionKey(o) for o in ['b_pch', 'b_lto', 'b_pgo', 'b_coverage',
154                                   'b_ndebug', 'b_staticpic', 'b_pie']}
155        if not (self.info.is_windows() or self.info.is_cygwin() or self.info.is_openbsd()):
156            self.base_options.add(OptionKey('b_lundef'))
157        if not self.info.is_windows() or self.info.is_cygwin():
158            self.base_options.add(OptionKey('b_asneeded'))
159        if not self.info.is_hurd():
160            self.base_options.add(OptionKey('b_sanitize'))
161        # All GCC-like backends can do assembly
162        self.can_compile_suffixes.add('s')
163
164    def get_pic_args(self) -> T.List[str]:
165        if self.info.is_windows() or self.info.is_cygwin() or self.info.is_darwin():
166            return [] # On Window and OS X, pic is always on.
167        return ['-fPIC']
168
169    def get_pie_args(self) -> T.List[str]:
170        return ['-fPIE']
171
172    def get_buildtype_args(self, buildtype: str) -> T.List[str]:
173        return gnulike_buildtype_args[buildtype]
174
175    @abc.abstractmethod
176    def get_optimization_args(self, optimization_level: str) -> T.List[str]:
177        pass
178
179    def get_debug_args(self, is_debug: bool) -> T.List[str]:
180        return clike_debug_args[is_debug]
181
182    @abc.abstractmethod
183    def get_pch_suffix(self) -> str:
184        pass
185
186    def split_shlib_to_parts(self, fname: str) -> T.Tuple[str, str]:
187        return os.path.dirname(fname), fname
188
189    def get_instruction_set_args(self, instruction_set: str) -> T.Optional[T.List[str]]:
190        return gnulike_instruction_set_args.get(instruction_set, None)
191
192    def get_default_include_dirs(self) -> T.List[str]:
193        return gnulike_default_include_dirs(tuple(self.exelist), self.language).copy()
194
195    @abc.abstractmethod
196    def openmp_flags(self) -> T.List[str]:
197        pass
198
199    def gnu_symbol_visibility_args(self, vistype: str) -> T.List[str]:
200        return gnu_symbol_visibility_args[vistype]
201
202    def gen_vs_module_defs_args(self, defsfile: str) -> T.List[str]:
203        if not isinstance(defsfile, str):
204            raise RuntimeError('Module definitions file should be str')
205        # On Windows targets, .def files may be specified on the linker command
206        # line like an object file.
207        if self.info.is_windows() or self.info.is_cygwin():
208            return [defsfile]
209        # For other targets, discard the .def file.
210        return []
211
212    def get_argument_syntax(self) -> str:
213        return 'gcc'
214
215    def get_profile_generate_args(self) -> T.List[str]:
216        return ['-fprofile-generate']
217
218    def get_profile_use_args(self) -> T.List[str]:
219        return ['-fprofile-use', '-fprofile-correction']
220
221    def get_gui_app_args(self, value: bool) -> T.List[str]:
222        return ['-mwindows' if value else '-mconsole']
223
224    def compute_parameters_with_absolute_paths(self, parameter_list: T.List[str], build_dir: str) -> T.List[str]:
225        for idx, i in enumerate(parameter_list):
226            if i[:2] == '-I' or i[:2] == '-L':
227                parameter_list[idx] = i[:2] + os.path.normpath(os.path.join(build_dir, i[2:]))
228
229        return parameter_list
230
231    @functools.lru_cache()
232    def _get_search_dirs(self, env: 'Environment') -> str:
233        extra_args = ['--print-search-dirs']
234        with self._build_wrapper('', env, extra_args=extra_args,
235                                 dependencies=None, mode='compile',
236                                 want_output=True) as p:
237            return p.stdout
238
239    def _split_fetch_real_dirs(self, pathstr: str) -> T.List[str]:
240        # We need to use the path separator used by the compiler for printing
241        # lists of paths ("gcc --print-search-dirs"). By default
242        # we assume it uses the platform native separator.
243        pathsep = os.pathsep
244
245        # clang uses ':' instead of ';' on Windows https://reviews.llvm.org/D61121
246        # so we need to repair things like 'C:\foo:C:\bar'
247        if pathsep == ';':
248            pathstr = re.sub(r':([^/\\])', r';\1', pathstr)
249
250        # pathlib treats empty paths as '.', so filter those out
251        paths = [p for p in pathstr.split(pathsep) if p]
252
253        result = []
254        for p in paths:
255            # GCC returns paths like this:
256            # /usr/lib/gcc/x86_64-linux-gnu/8/../../../../x86_64-linux-gnu/lib
257            # It would make sense to normalize them to get rid of the .. parts
258            # Sadly when you are on a merged /usr fs it also kills these:
259            # /lib/x86_64-linux-gnu
260            # since /lib is a symlink to /usr/lib. This would mean
261            # paths under /lib would be considered not a "system path",
262            # which is wrong and breaks things. Store everything, just to be sure.
263            pobj = pathlib.Path(p)
264            unresolved = pobj.as_posix()
265            if pobj.exists():
266                if unresolved not in result:
267                    result.append(unresolved)
268                try:
269                    resolved = pathlib.Path(p).resolve().as_posix()
270                    if resolved not in result:
271                        result.append(resolved)
272                except FileNotFoundError:
273                    pass
274        return result
275
276    def get_compiler_dirs(self, env: 'Environment', name: str) -> T.List[str]:
277        '''
278        Get dirs from the compiler, either `libraries:` or `programs:`
279        '''
280        stdo = self._get_search_dirs(env)
281        for line in stdo.split('\n'):
282            if line.startswith(name + ':'):
283                return self._split_fetch_real_dirs(line.split('=', 1)[1])
284        return []
285
286    def get_lto_compile_args(self, *, threads: int = 0, mode: str = 'default') -> T.List[str]:
287        # This provides a base for many compilers, GCC and Clang override this
288        # for their specific arguments
289        return ['-flto']
290
291    def sanitizer_compile_args(self, value: str) -> T.List[str]:
292        if value == 'none':
293            return []
294        args = ['-fsanitize=' + value]
295        if 'address' in value:  # for -fsanitize=address,undefined
296            args.append('-fno-omit-frame-pointer')
297        return args
298
299    def get_output_args(self, target: str) -> T.List[str]:
300        return ['-o', target]
301
302    def get_dependency_gen_args(self, outtarget: str, outfile: str) -> T.List[str]:
303        return ['-MD', '-MQ', outtarget, '-MF', outfile]
304
305    def get_compile_only_args(self) -> T.List[str]:
306        return ['-c']
307
308    def get_include_args(self, path: str, is_system: bool) -> T.List[str]:
309        if not path:
310            path = '.'
311        if is_system:
312            return ['-isystem' + path]
313        return ['-I' + path]
314
315    @classmethod
316    def use_linker_args(cls, linker: str) -> T.List[str]:
317        if linker not in {'gold', 'bfd', 'lld'}:
318            raise mesonlib.MesonException(
319                f'Unsupported linker, only bfd, gold, and lld are supported, not {linker}.')
320        return [f'-fuse-ld={linker}']
321
322    def get_coverage_args(self) -> T.List[str]:
323        return ['--coverage']
324
325
326class GnuCompiler(GnuLikeCompiler):
327    """
328    GnuCompiler represents an actual GCC in its many incarnations.
329    Compilers imitating GCC (Clang/Intel) should use the GnuLikeCompiler ABC.
330    """
331
332    def __init__(self, defines: T.Optional[T.Dict[str, str]]):
333        super().__init__()
334        self.id = 'gcc'
335        self.defines = defines or {}
336        self.base_options.update({OptionKey('b_colorout'), OptionKey('b_lto_threads')})
337
338    def get_colorout_args(self, colortype: str) -> T.List[str]:
339        if mesonlib.version_compare(self.version, '>=4.9.0'):
340            return gnu_color_args[colortype][:]
341        return []
342
343    def get_warn_args(self, level: str) -> T.List[str]:
344        # Mypy doesn't understand cooperative inheritance
345        args = super().get_warn_args(level)
346        if mesonlib.version_compare(self.version, '<4.8.0') and '-Wpedantic' in args:
347            # -Wpedantic was added in 4.8.0
348            # https://gcc.gnu.org/gcc-4.8/changes.html
349            args[args.index('-Wpedantic')] = '-pedantic'
350        return args
351
352    def has_builtin_define(self, define: str) -> bool:
353        return define in self.defines
354
355    def get_builtin_define(self, define: str) -> T.Optional[str]:
356        if define in self.defines:
357            return self.defines[define]
358        return None
359
360    def get_optimization_args(self, optimization_level: str) -> T.List[str]:
361        return gnu_optimization_args[optimization_level]
362
363    def get_pch_suffix(self) -> str:
364        return 'gch'
365
366    def openmp_flags(self) -> T.List[str]:
367        return ['-fopenmp']
368
369    def has_arguments(self, args: T.List[str], env: 'Environment', code: str,
370                      mode: str) -> T.Tuple[bool, bool]:
371        # For some compiler command line arguments, the GNU compilers will
372        # emit a warning on stderr indicating that an option is valid for a
373        # another language, but still complete with exit_success
374        with self._build_wrapper(code, env, args, None, mode) as p:
375            result = p.returncode == 0
376            if self.language in {'cpp', 'objcpp'} and 'is valid for C/ObjC' in p.stderr:
377                result = False
378            if self.language in {'c', 'objc'} and 'is valid for C++/ObjC++' in p.stderr:
379                result = False
380        return result, p.cached
381
382    def get_has_func_attribute_extra_args(self, name: str) -> T.List[str]:
383        # GCC only warns about unknown or ignored attributes, so force an
384        # error.
385        return ['-Werror=attributes']
386
387    def get_prelink_args(self, prelink_name: str, obj_list: T.List[str]) -> T.List[str]:
388        return ['-r', '-o', prelink_name] + obj_list
389
390    def get_lto_compile_args(self, *, threads: int = 0, mode: str = 'default') -> T.List[str]:
391        if threads == 0:
392            if mesonlib.version_compare(self.version, '>= 10.0'):
393                return ['-flto=auto']
394            # This matches clang's behavior of using the number of cpus
395            return [f'-flto={multiprocessing.cpu_count()}']
396        elif threads > 0:
397            return [f'-flto={threads}']
398        return super().get_lto_compile_args(threads=threads)
399