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