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
15from collections import namedtuple
16from .. import mesonlib
17from ..mesonlib import listify
18from . import ExtensionModule
19from ..interpreterbase import (
20    noPosargs, noKwargs, permittedKwargs,
21    InterpreterObject, MutableInterpreterObject, ObjectHolder,
22    InterpreterException, InvalidArguments, InvalidCode, FeatureNew,
23)
24from ..interpreter import (
25    GeneratedListHolder, CustomTargetHolder,
26    CustomTargetIndexHolder
27)
28
29SourceSetRule = namedtuple('SourceSetRule', 'keys sources if_false sourcesets dependencies extra_deps')
30SourceFiles = namedtuple('SourceFiles', 'sources dependencies')
31
32class SourceSetHolder(MutableInterpreterObject, ObjectHolder):
33    def __init__(self, interpreter):
34        MutableInterpreterObject.__init__(self)
35        ObjectHolder.__init__(self, list())
36        self.subproject = interpreter.subproject
37        self.environment = interpreter.environment
38        self.subdir = interpreter.subdir
39        self.frozen = False
40        self.methods.update({
41            'add': self.add_method,
42            'add_all': self.add_all_method,
43            'all_sources': self.all_sources_method,
44            'all_dependencies': self.all_dependencies_method,
45            'apply': self.apply_method,
46        })
47
48    def check_source_files(self, arg, allow_deps):
49        sources = []
50        deps = []
51        for x in arg:
52            if isinstance(x, (str, mesonlib.File,
53                              GeneratedListHolder, CustomTargetHolder,
54                              CustomTargetIndexHolder)):
55                sources.append(x)
56            elif hasattr(x, 'found'):
57                if not allow_deps:
58                    msg = 'Dependencies are not allowed in the if_false argument.'
59                    raise InvalidArguments(msg)
60                deps.append(x)
61            else:
62                msg = 'Sources must be strings or file-like objects.'
63                raise InvalidArguments(msg)
64        mesonlib.check_direntry_issues(sources)
65        return sources, deps
66
67    def check_conditions(self, arg):
68        keys = []
69        deps = []
70        for x in listify(arg):
71            if isinstance(x, str):
72                keys.append(x)
73            elif hasattr(x, 'found'):
74                deps.append(x)
75            else:
76                raise InvalidArguments('Conditions must be strings or dependency object')
77        return keys, deps
78
79    @permittedKwargs(['when', 'if_false', 'if_true'])
80    def add_method(self, args, kwargs):
81        if self.frozen:
82            raise InvalidCode('Tried to use \'add\' after querying the source set')
83        when = listify(kwargs.get('when', []))
84        if_true = listify(kwargs.get('if_true', []))
85        if_false = listify(kwargs.get('if_false', []))
86        if not when and not if_true and not if_false:
87            if_true = args
88        elif args:
89            raise InterpreterException('add called with both positional and keyword arguments')
90        keys, dependencies = self.check_conditions(when)
91        sources, extra_deps = self.check_source_files(if_true, True)
92        if_false, _ = self.check_source_files(if_false, False)
93        self.held_object.append(SourceSetRule(keys, sources, if_false, [], dependencies, extra_deps))
94
95    @permittedKwargs(['when', 'if_true'])
96    def add_all_method(self, args, kwargs):
97        if self.frozen:
98            raise InvalidCode('Tried to use \'add_all\' after querying the source set')
99        when = listify(kwargs.get('when', []))
100        if_true = listify(kwargs.get('if_true', []))
101        if not when and not if_true:
102            if_true = args
103        elif args:
104            raise InterpreterException('add_all called with both positional and keyword arguments')
105        keys, dependencies = self.check_conditions(when)
106        for s in if_true:
107            if not isinstance(s, SourceSetHolder):
108                raise InvalidCode('Arguments to \'add_all\' after the first must be source sets')
109            s.frozen = True
110        self.held_object.append(SourceSetRule(keys, [], [], if_true, dependencies, []))
111
112    def collect(self, enabled_fn, all_sources, into=None):
113        if not into:
114            into = SourceFiles(set(), set())
115        for entry in self.held_object:
116            if all(x.found() for x in entry.dependencies) and \
117               all(enabled_fn(key) for key in entry.keys):
118                into.sources.update(entry.sources)
119                into.dependencies.update(entry.dependencies)
120                into.dependencies.update(entry.extra_deps)
121                for ss in entry.sourcesets:
122                    ss.collect(enabled_fn, all_sources, into)
123                if not all_sources:
124                    continue
125            into.sources.update(entry.if_false)
126        return into
127
128    @noKwargs
129    @noPosargs
130    def all_sources_method(self, args, kwargs):
131        self.frozen = True
132        files = self.collect(lambda x: True, True)
133        return list(files.sources)
134
135    @noKwargs
136    @noPosargs
137    @FeatureNew('source_set.all_dependencies() method', '0.52.0')
138    def all_dependencies_method(self, args, kwargs):
139        self.frozen = True
140        files = self.collect(lambda x: True, True)
141        return list(files.dependencies)
142
143    @permittedKwargs(['strict'])
144    def apply_method(self, args, kwargs):
145        if len(args) != 1:
146            raise InterpreterException('Apply takes exactly one argument')
147        config_data = args[0]
148        self.frozen = True
149        strict = kwargs.get('strict', True)
150        if isinstance(config_data, dict):
151            def _get_from_config_data(key):
152                if strict and key not in config_data:
153                    raise InterpreterException('Entry {} not in configuration dictionary.'.format(key))
154                return config_data.get(key, False)
155        else:
156            config_cache = dict()
157
158            def _get_from_config_data(key):
159                nonlocal config_cache
160                if key not in config_cache:
161                    args = [key] if strict else [key, False]
162                    config_cache[key] = config_data.get_method(args, {})
163                return config_cache[key]
164
165        files = self.collect(_get_from_config_data, False)
166        res = SourceFilesHolder(files)
167        return res
168
169class SourceFilesHolder(InterpreterObject, ObjectHolder):
170    def __init__(self, files):
171        InterpreterObject.__init__(self)
172        ObjectHolder.__init__(self, files)
173        self.methods.update({
174            'sources': self.sources_method,
175            'dependencies': self.dependencies_method,
176        })
177
178    @noPosargs
179    @noKwargs
180    def sources_method(self, args, kwargs):
181        return list(self.held_object.sources)
182
183    @noPosargs
184    @noKwargs
185    def dependencies_method(self, args, kwargs):
186        return list(self.held_object.dependencies)
187
188class SourceSetModule(ExtensionModule):
189    @FeatureNew('SourceSet module', '0.51.0')
190    def __init__(self, *args, **kwargs):
191        super().__init__(*args, **kwargs)
192        self.snippets.add('source_set')
193
194    @noKwargs
195    @noPosargs
196    def source_set(self, interpreter, state, args, kwargs):
197        return SourceSetHolder(interpreter)
198
199def initialize(*args, **kwargs):
200    return SourceSetModule(*args, **kwargs)
201