1# Copyright 2018 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# This class contains the basic functionality needed to run any interpreter
16# or an interpreter-based tool
17
18from .interpreter import AstInterpreter
19from .visitor import AstVisitor
20from .. import compilers, environment, mesonlib, optinterpreter
21from .. import coredata as cdata
22from ..mesonlib import MachineChoice, OptionKey
23from ..interpreterbase import InvalidArguments, TYPE_nvar
24from ..build import BuildTarget, Executable, Jar, SharedLibrary, SharedModule, StaticLibrary
25from ..mparser import BaseNode, ArithmeticNode, ArrayNode, ElementaryNode, IdNode, FunctionNode, StringNode
26from ..compilers import detect_compiler_for
27import typing as T
28import os
29import argparse
30
31build_target_functions = ['executable', 'jar', 'library', 'shared_library', 'shared_module', 'static_library', 'both_libraries']
32
33class IntrospectionHelper(argparse.Namespace):
34    # mimic an argparse namespace
35    def __init__(self, cross_file: str):
36        super().__init__()
37        self.cross_file = cross_file  # type: str
38        self.native_file = None       # type: str
39        self.cmd_line_options = {}    # type: T.Dict[str, str]
40
41    def __eq__(self, other: object) -> bool:
42        return NotImplemented
43
44class IntrospectionInterpreter(AstInterpreter):
45    # Interpreter to detect the options without a build directory
46    # Most of the code is stolen from interpreter.Interpreter
47    def __init__(self,
48                 source_root: str,
49                 subdir: str,
50                 backend: str,
51                 visitors: T.Optional[T.List[AstVisitor]] = None,
52                 cross_file: T.Optional[str] = None,
53                 subproject: str = '',
54                 subproject_dir: str = 'subprojects',
55                 env: T.Optional[environment.Environment] = None):
56        visitors = visitors if visitors is not None else []
57        super().__init__(source_root, subdir, subproject, visitors=visitors)
58
59        options = IntrospectionHelper(cross_file)
60        self.cross_file = cross_file
61        if env is None:
62            self.environment = environment.Environment(source_root, None, options)
63        else:
64            self.environment = env
65        self.subproject_dir = subproject_dir
66        self.coredata = self.environment.get_coredata()
67        self.option_file = os.path.join(self.source_root, self.subdir, 'meson_options.txt')
68        self.backend = backend
69        self.default_options = {OptionKey('backend'): self.backend}
70        self.project_data = {}    # type: T.Dict[str, T.Any]
71        self.targets = []         # type: T.List[T.Dict[str, T.Any]]
72        self.dependencies = []    # type: T.List[T.Dict[str, T.Any]]
73        self.project_node = None  # type: BaseNode
74
75        self.funcs.update({
76            'add_languages': self.func_add_languages,
77            'dependency': self.func_dependency,
78            'executable': self.func_executable,
79            'jar': self.func_jar,
80            'library': self.func_library,
81            'project': self.func_project,
82            'shared_library': self.func_shared_lib,
83            'shared_module': self.func_shared_module,
84            'static_library': self.func_static_lib,
85            'both_libraries': self.func_both_lib,
86        })
87
88    def func_project(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> None:
89        if self.project_node:
90            raise InvalidArguments('Second call to project()')
91        self.project_node = node
92        if len(args) < 1:
93            raise InvalidArguments('Not enough arguments to project(). Needs at least the project name.')
94
95        proj_name = args[0]
96        proj_vers = kwargs.get('version', 'undefined')
97        proj_langs = self.flatten_args(args[1:])
98        if isinstance(proj_vers, ElementaryNode):
99            proj_vers = proj_vers.value
100        if not isinstance(proj_vers, str):
101            proj_vers = 'undefined'
102        self.project_data = {'descriptive_name': proj_name, 'version': proj_vers}
103
104        if os.path.exists(self.option_file):
105            oi = optinterpreter.OptionInterpreter(self.subproject)
106            oi.process(self.option_file)
107            self.coredata.update_project_options(oi.options)
108
109        def_opts = self.flatten_args(kwargs.get('default_options', []))
110        _project_default_options = mesonlib.stringlistify(def_opts)
111        self.project_default_options = cdata.create_options_dict(_project_default_options, self.subproject)
112        self.default_options.update(self.project_default_options)
113        self.coredata.set_default_options(self.default_options, self.subproject, self.environment)
114
115        if not self.is_subproject() and 'subproject_dir' in kwargs:
116            spdirname = kwargs['subproject_dir']
117            if isinstance(spdirname, StringNode):
118                assert isinstance(spdirname.value, str)
119                self.subproject_dir = spdirname.value
120        if not self.is_subproject():
121            self.project_data['subprojects'] = []
122            subprojects_dir = os.path.join(self.source_root, self.subproject_dir)
123            if os.path.isdir(subprojects_dir):
124                for i in os.listdir(subprojects_dir):
125                    if os.path.isdir(os.path.join(subprojects_dir, i)):
126                        self.do_subproject(i)
127
128        self.coredata.init_backend_options(self.backend)
129        options = {k: v for k, v in self.environment.options.items() if k.is_backend()}
130
131        self.coredata.set_options(options)
132        self._add_languages(proj_langs, MachineChoice.HOST)
133        self._add_languages(proj_langs, MachineChoice.BUILD)
134
135    def do_subproject(self, dirname: str) -> None:
136        subproject_dir_abs = os.path.join(self.environment.get_source_dir(), self.subproject_dir)
137        subpr = os.path.join(subproject_dir_abs, dirname)
138        try:
139            subi = IntrospectionInterpreter(subpr, '', self.backend, cross_file=self.cross_file, subproject=dirname, subproject_dir=self.subproject_dir, env=self.environment, visitors=self.visitors)
140            subi.analyze()
141            subi.project_data['name'] = dirname
142            self.project_data['subprojects'] += [subi.project_data]
143        except (mesonlib.MesonException, RuntimeError):
144            return
145
146    def func_add_languages(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> None:
147        kwargs = self.flatten_kwargs(kwargs)
148        if 'native' in kwargs:
149            native = kwargs.get('native', False)
150            self._add_languages(args, MachineChoice.BUILD if native else MachineChoice.HOST)
151        else:
152            for for_machine in [MachineChoice.BUILD, MachineChoice.HOST]:
153                self._add_languages(args, for_machine)
154
155    def _add_languages(self, raw_langs: T.List[TYPE_nvar], for_machine: MachineChoice) -> None:
156        langs = []  # type: T.List[str]
157        for l in self.flatten_args(raw_langs):
158            if isinstance(l, str):
159                langs.append(l)
160            elif isinstance(l, StringNode):
161                langs.append(l.value)
162
163        for lang in sorted(langs, key=compilers.sort_clink):
164            lang = lang.lower()
165            if lang not in self.coredata.compilers[for_machine]:
166                detect_compiler_for(self.environment, lang, for_machine)
167
168    def func_dependency(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> None:
169        args = self.flatten_args(args)
170        kwargs = self.flatten_kwargs(kwargs)
171        if not args:
172            return
173        name = args[0]
174        has_fallback = 'fallback' in kwargs
175        required = kwargs.get('required', True)
176        version = kwargs.get('version', [])
177        if not isinstance(version, list):
178            version = [version]
179        if isinstance(required, ElementaryNode):
180            required = required.value
181        if not isinstance(required, bool):
182            required = False
183        self.dependencies += [{
184            'name': name,
185            'required': required,
186            'version': version,
187            'has_fallback': has_fallback,
188            'conditional': node.condition_level > 0,
189            'node': node
190        }]
191
192    def build_target(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs_raw: T.Dict[str, TYPE_nvar], targetclass: T.Type[BuildTarget]) -> T.Optional[T.Dict[str, T.Any]]:
193        args = self.flatten_args(args)
194        if not args or not isinstance(args[0], str):
195            return None
196        name = args[0]
197        srcqueue = [node]
198        extra_queue = []
199
200        # Process the sources BEFORE flattening the kwargs, to preserve the original nodes
201        if 'sources' in kwargs_raw:
202            srcqueue += mesonlib.listify(kwargs_raw['sources'])
203
204        if 'extra_files' in kwargs_raw:
205            extra_queue += mesonlib.listify(kwargs_raw['extra_files'])
206
207        kwargs = self.flatten_kwargs(kwargs_raw, True)
208
209        def traverse_nodes(inqueue: T.List[BaseNode]) -> T.List[BaseNode]:
210            res = []  # type: T.List[BaseNode]
211            while inqueue:
212                curr = inqueue.pop(0)
213                arg_node = None
214                assert(isinstance(curr, BaseNode))
215                if isinstance(curr, FunctionNode):
216                    arg_node = curr.args
217                elif isinstance(curr, ArrayNode):
218                    arg_node = curr.args
219                elif isinstance(curr, IdNode):
220                    # Try to resolve the ID and append the node to the queue
221                    assert isinstance(curr.value, str)
222                    var_name = curr.value
223                    if var_name in self.assignments:
224                        tmp_node = self.assignments[var_name]
225                        if isinstance(tmp_node, (ArrayNode, IdNode, FunctionNode)):
226                            inqueue += [tmp_node]
227                elif isinstance(curr, ArithmeticNode):
228                    inqueue += [curr.left, curr.right]
229                if arg_node is None:
230                    continue
231                arg_nodes = arg_node.arguments.copy()
232                # Pop the first element if the function is a build target function
233                if isinstance(curr, FunctionNode) and curr.func_name in build_target_functions:
234                    arg_nodes.pop(0)
235                elemetary_nodes = [x for x in arg_nodes if isinstance(x, (str, StringNode))]
236                inqueue += [x for x in arg_nodes if isinstance(x, (FunctionNode, ArrayNode, IdNode, ArithmeticNode))]
237                if elemetary_nodes:
238                    res += [curr]
239            return res
240
241        source_nodes = traverse_nodes(srcqueue)
242        extraf_nodes = traverse_nodes(extra_queue)
243
244        # Make sure nothing can crash when creating the build class
245        kwargs_reduced = {k: v for k, v in kwargs.items() if k in targetclass.known_kwargs and k in ['install', 'build_by_default', 'build_always']}
246        kwargs_reduced = {k: v.value if isinstance(v, ElementaryNode) else v for k, v in kwargs_reduced.items()}
247        kwargs_reduced = {k: v for k, v in kwargs_reduced.items() if not isinstance(v, BaseNode)}
248        for_machine = MachineChoice.HOST
249        objects = []        # type: T.List[T.Any]
250        empty_sources = []  # type: T.List[T.Any]
251        # Passing the unresolved sources list causes errors
252        target = targetclass(name, self.subdir, self.subproject, for_machine, empty_sources, objects, self.environment, kwargs_reduced)
253
254        new_target = {
255            'name': target.get_basename(),
256            'id': target.get_id(),
257            'type': target.get_typename(),
258            'defined_in': os.path.normpath(os.path.join(self.source_root, self.subdir, environment.build_filename)),
259            'subdir': self.subdir,
260            'build_by_default': target.build_by_default,
261            'installed': target.should_install(),
262            'outputs': target.get_outputs(),
263            'sources': source_nodes,
264            'extra_files': extraf_nodes,
265            'kwargs': kwargs,
266            'node': node,
267        }
268
269        self.targets += [new_target]
270        return new_target
271
272    def build_library(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> T.Optional[T.Dict[str, T.Any]]:
273        default_library = self.coredata.get_option(OptionKey('default_library'))
274        if default_library == 'shared':
275            return self.build_target(node, args, kwargs, SharedLibrary)
276        elif default_library == 'static':
277            return self.build_target(node, args, kwargs, StaticLibrary)
278        elif default_library == 'both':
279            return self.build_target(node, args, kwargs, SharedLibrary)
280        return None
281
282    def func_executable(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> T.Optional[T.Dict[str, T.Any]]:
283        return self.build_target(node, args, kwargs, Executable)
284
285    def func_static_lib(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> T.Optional[T.Dict[str, T.Any]]:
286        return self.build_target(node, args, kwargs, StaticLibrary)
287
288    def func_shared_lib(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> T.Optional[T.Dict[str, T.Any]]:
289        return self.build_target(node, args, kwargs, SharedLibrary)
290
291    def func_both_lib(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> T.Optional[T.Dict[str, T.Any]]:
292        return self.build_target(node, args, kwargs, SharedLibrary)
293
294    def func_shared_module(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> T.Optional[T.Dict[str, T.Any]]:
295        return self.build_target(node, args, kwargs, SharedModule)
296
297    def func_library(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> T.Optional[T.Dict[str, T.Any]]:
298        return self.build_library(node, args, kwargs)
299
300    def func_jar(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> T.Optional[T.Dict[str, T.Any]]:
301        return self.build_target(node, args, kwargs, Jar)
302
303    def func_build_target(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> T.Optional[T.Dict[str, T.Any]]:
304        if 'target_type' not in kwargs:
305            return None
306        target_type = kwargs.pop('target_type')
307        if isinstance(target_type, ElementaryNode):
308            target_type = target_type.value
309        if target_type == 'executable':
310            return self.build_target(node, args, kwargs, Executable)
311        elif target_type == 'shared_library':
312            return self.build_target(node, args, kwargs, SharedLibrary)
313        elif target_type == 'static_library':
314            return self.build_target(node, args, kwargs, StaticLibrary)
315        elif target_type == 'both_libraries':
316            return self.build_target(node, args, kwargs, SharedLibrary)
317        elif target_type == 'library':
318            return self.build_library(node, args, kwargs)
319        elif target_type == 'jar':
320            return self.build_target(node, args, kwargs, Jar)
321        return None
322
323    def is_subproject(self) -> bool:
324        return self.subproject != ''
325
326    def analyze(self) -> None:
327        self.load_root_meson_file()
328        self.sanity_check_ast()
329        self.parse_project()
330        self.run()
331