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