1# SPDX-License-Identifier: Apache-2.0
2# Copyright 2012-2021 The Meson development team
3# Copyright © 2021 Intel Corporation
4
5import os
6import typing as T
7
8from .. import mesonlib
9from .. import dependencies
10from .. import build
11from .. import mlog
12
13from ..mesonlib import MachineChoice, OptionKey
14from ..programs import OverrideProgram, ExternalProgram
15from ..interpreter.type_checking import ENV_KW
16from ..interpreterbase import (MesonInterpreterObject, FeatureNew, FeatureDeprecated,
17                               typed_pos_args,  noArgsFlattening, noPosargs, noKwargs,
18                               typed_kwargs, KwargInfo, InterpreterException)
19from .primitives import MesonVersionString
20from .type_checking import NATIVE_KW, NoneType
21
22if T.TYPE_CHECKING:
23    from ..backend.backends import ExecutableSerialisation
24    from ..compilers import Compiler
25    from ..interpreterbase import TYPE_kwargs, TYPE_var
26    from .interpreter import Interpreter
27
28    from typing_extensions import TypedDict
29
30    class FuncOverrideDependency(TypedDict):
31
32        native: mesonlib.MachineChoice
33        static: T.Optional[bool]
34
35    class AddInstallScriptKW(TypedDict):
36
37        skip_if_destdir: bool
38        install_tag: str
39
40    class NativeKW(TypedDict):
41
42        native: mesonlib.MachineChoice
43
44
45class MesonMain(MesonInterpreterObject):
46    def __init__(self, build: 'build.Build', interpreter: 'Interpreter'):
47        super().__init__(subproject=interpreter.subproject)
48        self.build = build
49        self.interpreter = interpreter
50        self.methods.update({'get_compiler': self.get_compiler_method,
51                             'is_cross_build': self.is_cross_build_method,
52                             'has_exe_wrapper': self.has_exe_wrapper_method,
53                             'can_run_host_binaries': self.can_run_host_binaries_method,
54                             'is_unity': self.is_unity_method,
55                             'is_subproject': self.is_subproject_method,
56                             'current_source_dir': self.current_source_dir_method,
57                             'current_build_dir': self.current_build_dir_method,
58                             'source_root': self.source_root_method,
59                             'build_root': self.build_root_method,
60                             'project_source_root': self.project_source_root_method,
61                             'project_build_root': self.project_build_root_method,
62                             'global_source_root': self.global_source_root_method,
63                             'global_build_root': self.global_build_root_method,
64                             'add_install_script': self.add_install_script_method,
65                             'add_postconf_script': self.add_postconf_script_method,
66                             'add_dist_script': self.add_dist_script_method,
67                             'install_dependency_manifest': self.install_dependency_manifest_method,
68                             'override_dependency': self.override_dependency_method,
69                             'override_find_program': self.override_find_program_method,
70                             'project_version': self.project_version_method,
71                             'project_license': self.project_license_method,
72                             'version': self.version_method,
73                             'project_name': self.project_name_method,
74                             'get_cross_property': self.get_cross_property_method,
75                             'get_external_property': self.get_external_property_method,
76                             'has_external_property': self.has_external_property_method,
77                             'backend': self.backend_method,
78                             'add_devenv': self.add_devenv_method,
79                             })
80
81    def _find_source_script(
82            self, prog: T.Union[str, mesonlib.File, build.Executable, ExternalProgram],
83            args: T.List[str]) -> 'ExecutableSerialisation':
84        largs: T.List[T.Union[str, build.Executable, ExternalProgram]] = []
85        if isinstance(prog, (build.Executable, ExternalProgram)):
86            largs.append(prog)
87            largs.extend(args)
88            return self.interpreter.backend.get_executable_serialisation(largs)
89        found = self.interpreter.func_find_program({}, prog, {})
90        largs.append(found)
91        largs.extend(args)
92        es = self.interpreter.backend.get_executable_serialisation(largs)
93        es.subproject = self.interpreter.subproject
94        return es
95
96    def _process_script_args(
97            self, name: str, args: T.Sequence[T.Union[
98                str, mesonlib.File, build.BuildTarget, build.CustomTarget,
99                build.CustomTargetIndex,
100                ExternalProgram,
101            ]], allow_built: bool = False) -> T.List[str]:
102        script_args = []  # T.List[str]
103        new = False
104        for a in args:
105            if isinstance(a, str):
106                script_args.append(a)
107            elif isinstance(a, mesonlib.File):
108                new = True
109                script_args.append(a.rel_to_builddir(self.interpreter.environment.source_dir))
110            elif isinstance(a, (build.BuildTarget, build.CustomTarget, build.CustomTargetIndex)):
111                if not allow_built:
112                    raise InterpreterException(f'Arguments to {name} cannot be built')
113                new = True
114                script_args.extend([os.path.join(a.get_subdir(), o) for o in a.get_outputs()])
115
116                # This feels really hacky, but I'm not sure how else to fix
117                # this without completely rewriting install script handling.
118                # This is complicated by the fact that the install target
119                # depends on all.
120                if isinstance(a, build.CustomTargetIndex):
121                    a.target.build_by_default = True
122                else:
123                    a.build_by_default = True
124            else:
125                script_args.extend(a.command)
126                new = True
127
128        if new:
129            FeatureNew.single_use(
130                f'Calling "{name}" with File, CustomTaget, Index of CustomTarget, '
131                'Executable, or ExternalProgram',
132                '0.55.0', self.interpreter.subproject)
133        return script_args
134
135    @typed_pos_args(
136        'meson.add_install_script',
137        (str, mesonlib.File, build.Executable, ExternalProgram),
138        varargs=(str, mesonlib.File, build.BuildTarget, build.CustomTarget, build.CustomTargetIndex, ExternalProgram)
139    )
140    @typed_kwargs(
141        'meson.add_install_script',
142        KwargInfo('skip_if_destdir', bool, default=False, since='0.57.0'),
143        KwargInfo('install_tag', (str, NoneType), since='0.60.0'),
144    )
145    def add_install_script_method(
146            self,
147            args: T.Tuple[T.Union[str, mesonlib.File, build.Executable, ExternalProgram],
148                          T.List[T.Union[str, mesonlib.File, build.BuildTarget, build.CustomTarget, build.CustomTargetIndex, ExternalProgram]]],
149            kwargs: 'AddInstallScriptKW') -> None:
150        if isinstance(args[0], mesonlib.File):
151            FeatureNew.single_use('Passing file object to script parameter of add_install_script',
152                                  '0.57.0', self.interpreter.subproject)
153
154        script_args = self._process_script_args('add_install_script', args[1], allow_built=True)
155        script = self._find_source_script(args[0], script_args)
156        script.skip_if_destdir = kwargs['skip_if_destdir']
157        script.tag = kwargs['install_tag']
158        self.build.install_scripts.append(script)
159
160    @typed_pos_args(
161        'meson.add_postconf_script',
162        (str, mesonlib.File, ExternalProgram),
163        varargs=(str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex)
164    )
165    @noKwargs
166    def add_postconf_script_method(
167            self,
168            args: T.Tuple[T.Union[str, mesonlib.File, ExternalProgram],
169                          T.List[T.Union[str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex]]],
170            kwargs: 'TYPE_kwargs') -> None:
171        if isinstance(args[0], mesonlib.File):
172            FeatureNew.single_use('Passing file object to script parameter of add_postconf_script',
173                                  '0.57.0', self.interpreter.subproject)
174        script_args = self._process_script_args('add_postconf_script', args[1], allow_built=True)
175        script = self._find_source_script(args[0], script_args)
176        self.build.postconf_scripts.append(script)
177
178    @typed_pos_args(
179        'meson.add_dist_script',
180        (str, mesonlib.File, build.Executable, ExternalProgram),
181        varargs=(str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex)
182    )
183    @noKwargs
184    def add_dist_script_method(
185            self,
186            args: T.Tuple[T.Union[str, mesonlib.File, build.Executable, ExternalProgram],
187                          T.List[T.Union[str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex]]],
188            kwargs: 'TYPE_kwargs') -> None:
189        if args[1]:
190            FeatureNew.single_use('Calling "add_dist_script" with multiple arguments',
191                                  '0.49.0', self.interpreter.subproject)
192        if isinstance(args[0], mesonlib.File):
193            FeatureNew.single_use('Passing file object to script parameter of add_dist_script',
194                                  '0.57.0', self.interpreter.subproject)
195        if self.interpreter.subproject != '':
196            FeatureNew.single_use('Calling "add_dist_script" in a subproject',
197                                  '0.58.0', self.interpreter.subproject)
198        script_args = self._process_script_args('add_dist_script', args[1], allow_built=True)
199        script = self._find_source_script(args[0], script_args)
200        self.build.dist_scripts.append(script)
201
202    @noPosargs
203    @noKwargs
204    def current_source_dir_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
205        src = self.interpreter.environment.source_dir
206        sub = self.interpreter.subdir
207        if sub == '':
208            return src
209        return os.path.join(src, sub)
210
211    @noPosargs
212    @noKwargs
213    def current_build_dir_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
214        src = self.interpreter.environment.build_dir
215        sub = self.interpreter.subdir
216        if sub == '':
217            return src
218        return os.path.join(src, sub)
219
220    @noPosargs
221    @noKwargs
222    def backend_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
223        return self.interpreter.backend.name
224
225    @noPosargs
226    @noKwargs
227    @FeatureDeprecated('meson.source_root', '0.56.0', 'use meson.project_source_root() or meson.global_source_root() instead.')
228    def source_root_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
229        return self.interpreter.environment.source_dir
230
231    @noPosargs
232    @noKwargs
233    @FeatureDeprecated('meson.build_root', '0.56.0', 'use meson.project_build_root() or meson.global_build_root() instead.')
234    def build_root_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
235        return self.interpreter.environment.build_dir
236
237    @noPosargs
238    @noKwargs
239    @FeatureNew('meson.project_source_root', '0.56.0')
240    def project_source_root_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
241        src = self.interpreter.environment.source_dir
242        sub = self.interpreter.root_subdir
243        if sub == '':
244            return src
245        return os.path.join(src, sub)
246
247    @noPosargs
248    @noKwargs
249    @FeatureNew('meson.project_build_root', '0.56.0')
250    def project_build_root_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
251        src = self.interpreter.environment.build_dir
252        sub = self.interpreter.root_subdir
253        if sub == '':
254            return src
255        return os.path.join(src, sub)
256
257    @noPosargs
258    @noKwargs
259    @FeatureNew('meson.global_source_root', '0.58.0')
260    def global_source_root_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
261        return self.interpreter.environment.source_dir
262
263    @noPosargs
264    @noKwargs
265    @FeatureNew('meson.global_build_root', '0.58.0')
266    def global_build_root_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
267        return self.interpreter.environment.build_dir
268
269    @noPosargs
270    @noKwargs
271    @FeatureDeprecated('meson.has_exe_wrapper', '0.55.0', 'use meson.can_run_host_binaries instead.')
272    def has_exe_wrapper_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> bool:
273        return self._can_run_host_binaries_impl()
274
275    @noPosargs
276    @noKwargs
277    @FeatureNew('meson.can_run_host_binaries', '0.55.0')
278    def can_run_host_binaries_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> bool:
279        return self._can_run_host_binaries_impl()
280
281    def _can_run_host_binaries_impl(self) -> bool:
282        return not (
283            self.build.environment.is_cross_build() and
284            self.build.environment.need_exe_wrapper() and
285            self.build.environment.exe_wrapper is None
286        )
287
288    @noPosargs
289    @noKwargs
290    def is_cross_build_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> bool:
291        return self.build.environment.is_cross_build()
292
293    @typed_pos_args('meson.get_compiler', str)
294    @typed_kwargs('meson.get_compiler', NATIVE_KW)
295    def get_compiler_method(self, args: T.Tuple[str], kwargs: 'NativeKW') -> 'Compiler':
296        cname = args[0]
297        for_machine = kwargs['native']
298        clist = self.interpreter.coredata.compilers[for_machine]
299        try:
300            return clist[cname]
301        except KeyError:
302            raise InterpreterException(f'Tried to access compiler for language "{cname}", not specified for {for_machine.get_lower_case_name()} machine.')
303
304    @noPosargs
305    @noKwargs
306    def is_unity_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> bool:
307        optval = self.interpreter.environment.coredata.get_option(OptionKey('unity'))
308        return optval == 'on' or (optval == 'subprojects' and self.interpreter.is_subproject())
309
310    @noPosargs
311    @noKwargs
312    def is_subproject_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> bool:
313        return self.interpreter.is_subproject()
314
315    @typed_pos_args('meson.install_dependency_manifest', str)
316    @noKwargs
317    def install_dependency_manifest_method(self, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> None:
318        self.build.dep_manifest_name = args[0]
319
320    @FeatureNew('meson.override_find_program', '0.46.0')
321    @typed_pos_args('meson.override_find_program', str, (mesonlib.File, ExternalProgram, build.Executable))
322    @noKwargs
323    def override_find_program_method(self, args: T.Tuple[str, T.Union[mesonlib.File, ExternalProgram, build.Executable]], kwargs: 'TYPE_kwargs') -> None:
324        name, exe = args
325        if isinstance(exe, mesonlib.File):
326            abspath = exe.absolute_path(self.interpreter.environment.source_dir,
327                                        self.interpreter.environment.build_dir)
328            if not os.path.exists(abspath):
329                raise InterpreterException(f'Tried to override {name} with a file that does not exist.')
330            exe = OverrideProgram(name, [abspath])
331        self.interpreter.add_find_program_override(name, exe)
332
333    @typed_kwargs(
334        'meson.override_dependency',
335        NATIVE_KW,
336        KwargInfo('static', (bool, NoneType), since='0.60.0'),
337    )
338    @typed_pos_args('meson.override_dependency', str, dependencies.Dependency)
339    @FeatureNew('meson.override_dependency', '0.54.0')
340    def override_dependency_method(self, args: T.Tuple[str, dependencies.Dependency], kwargs: 'FuncOverrideDependency') -> None:
341        name, dep = args
342        if not name:
343            raise InterpreterException('First argument must be a string and cannot be empty')
344
345        optkey = OptionKey('default_library', subproject=self.interpreter.subproject)
346        default_library = self.interpreter.coredata.get_option(optkey)
347        assert isinstance(default_library, str), 'for mypy'
348        static = kwargs['static']
349        if static is None:
350            # We don't know if dep represents a static or shared library, could
351            # be a mix of both. We assume it is following default_library
352            # value.
353            self._override_dependency_impl(name, dep, kwargs, static=None)
354            if default_library == 'static':
355                self._override_dependency_impl(name, dep, kwargs, static=True)
356            elif default_library == 'shared':
357                self._override_dependency_impl(name, dep, kwargs, static=False)
358            else:
359                self._override_dependency_impl(name, dep, kwargs, static=True)
360                self._override_dependency_impl(name, dep, kwargs, static=False)
361        else:
362            # dependency('foo') without specifying static kwarg should find this
363            # override regardless of the static value here. But do not raise error
364            # if it has already been overridden, which would happen when overriding
365            # static and shared separately:
366            # meson.override_dependency('foo', shared_dep, static: false)
367            # meson.override_dependency('foo', static_dep, static: true)
368            # In that case dependency('foo') would return the first override.
369            self._override_dependency_impl(name, dep, kwargs, static=None, permissive=True)
370            self._override_dependency_impl(name, dep, kwargs, static=static)
371
372    def _override_dependency_impl(self, name: str, dep: dependencies.Dependency, kwargs: 'FuncOverrideDependency',
373                                  static: T.Optional[bool], permissive: bool = False) -> None:
374        # We need the cast here as get_dep_identifier works on such a dict,
375        # which FuncOverrideDependency is, but mypy can't fgure that out
376        nkwargs = T.cast(T.Dict[str, T.Any], kwargs.copy())
377        if static is None:
378            del nkwargs['static']
379        else:
380            nkwargs['static'] = static
381        identifier = dependencies.get_dep_identifier(name, nkwargs)
382        for_machine = kwargs['native']
383        override = self.build.dependency_overrides[for_machine].get(identifier)
384        if override:
385            if permissive:
386                return
387            m = 'Tried to override dependency {!r} which has already been resolved or overridden at {}'
388            location = mlog.get_error_location_string(override.node.filename, override.node.lineno)
389            raise InterpreterException(m.format(name, location))
390        self.build.dependency_overrides[for_machine][identifier] = \
391            build.DependencyOverride(dep, self.interpreter.current_node)
392
393    @noPosargs
394    @noKwargs
395    def project_version_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
396        return self.build.dep_manifest[self.interpreter.active_projectname].version
397
398    @FeatureNew('meson.project_license()', '0.45.0')
399    @noPosargs
400    @noKwargs
401    def project_license_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> T.List[str]:
402        return self.build.dep_manifest[self.interpreter.active_projectname].license
403
404    @noPosargs
405    @noKwargs
406    def version_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> MesonVersionString:
407        return MesonVersionString(self.interpreter.coredata.version)
408
409    @noPosargs
410    @noKwargs
411    def project_name_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str:
412        return self.interpreter.active_projectname
413
414    def __get_external_property_impl(self, propname: str, fallback: T.Optional[object], machine: MachineChoice) -> object:
415        """Shared implementation for get_cross_property and get_external_property."""
416        try:
417            return self.interpreter.environment.properties[machine][propname]
418        except KeyError:
419            if fallback is not None:
420                return fallback
421            raise InterpreterException(f'Unknown property for {machine.get_lower_case_name()} machine: {propname}')
422
423    @noArgsFlattening
424    @FeatureDeprecated('meson.get_cross_property', '0.58.0', 'Use meson.get_external_property() instead')
425    @typed_pos_args('meson.get_cross_property', str, optargs=[object])
426    @noKwargs
427    def get_cross_property_method(self, args: T.Tuple[str, T.Optional[object]], kwargs: 'TYPE_kwargs') -> object:
428        propname, fallback = args
429        return self.__get_external_property_impl(propname, fallback, MachineChoice.HOST)
430
431    @noArgsFlattening
432    @FeatureNew('meson.get_external_property', '0.54.0')
433    @typed_pos_args('meson.get_external_property', str, optargs=[object])
434    @typed_kwargs('meson.get_external_property', NATIVE_KW)
435    def get_external_property_method(self, args: T.Tuple[str, T.Optional[object]], kwargs: 'NativeKW') -> object:
436        propname, fallback = args
437        return self.__get_external_property_impl(propname, fallback, kwargs['native'])
438
439    @FeatureNew('meson.has_external_property', '0.58.0')
440    @typed_pos_args('meson.has_external_property', str)
441    @typed_kwargs('meson.has_external_property', NATIVE_KW)
442    def has_external_property_method(self, args: T.Tuple[str], kwargs: 'NativeKW') -> bool:
443        prop_name = args[0]
444        return prop_name in self.interpreter.environment.properties[kwargs['native']]
445
446    @FeatureNew('add_devenv', '0.58.0')
447    @noKwargs
448    @typed_pos_args('add_devenv', (str, list, dict, build.EnvironmentVariables))
449    def add_devenv_method(self, args: T.Tuple[T.Union[str, list, dict, build.EnvironmentVariables]], kwargs: 'TYPE_kwargs') -> None:
450        env = args[0]
451        msg = ENV_KW.validator(env)
452        if msg:
453            raise build.InvalidArguments(f'"add_devenv": {msg}')
454        converted = ENV_KW.convertor(env)
455        assert isinstance(converted, build.EnvironmentVariables)
456        self.build.devenv.append(converted)
457