1# Copyright © 2020 Intel Corporation
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
15import os
16import typing as T
17
18from . import ExtensionModule, ModuleReturnValue
19from .. import mlog
20from ..build import BothLibraries, BuildTarget, CustomTargetIndex, Executable, ExtractedObjects, GeneratedList, IncludeDirs, CustomTarget
21from ..dependencies import Dependency, ExternalLibrary
22from ..interpreter.interpreter import TEST_KWARGS
23from ..interpreterbase import ContainerTypeInfo, InterpreterException, KwargInfo, FeatureNew, typed_kwargs, typed_pos_args, noPosargs
24from ..mesonlib import File
25
26if T.TYPE_CHECKING:
27    from . import ModuleState
28    from ..interpreter import Interpreter
29    from ..interpreter import kwargs as _kwargs
30    from ..interpreter.interpreter import SourceInputs, SourceOutputs
31    from ..programs import ExternalProgram
32
33    from typing_extensions import TypedDict
34
35    class FuncTest(_kwargs.BaseTest):
36
37        dependencies: T.List[T.Union[Dependency, ExternalLibrary]]
38        is_parallel: bool
39
40    class FuncBindgen(TypedDict):
41
42        args: T.List[str]
43        c_args: T.List[str]
44        include_directories: T.List[IncludeDirs]
45        input: T.List[SourceInputs]
46        output: str
47
48
49class RustModule(ExtensionModule):
50
51    """A module that holds helper functions for rust."""
52
53    @FeatureNew('rust module', '0.57.0')
54    def __init__(self, interpreter: 'Interpreter') -> None:
55        super().__init__(interpreter)
56        self._bindgen_bin: T.Optional['ExternalProgram'] = None
57        self.methods.update({
58            'test': self.test,
59            'bindgen': self.bindgen,
60        })
61
62    @typed_pos_args('rust.test', str, BuildTarget)
63    @typed_kwargs(
64        'rust.test',
65        *TEST_KWARGS,
66        KwargInfo('is_parallel', bool, default=False),
67        KwargInfo(
68            'dependencies',
69            ContainerTypeInfo(list, (Dependency, ExternalLibrary)),
70            listify=True,
71            default=[]),
72    )
73    def test(self, state: 'ModuleState', args: T.Tuple[str, BuildTarget], kwargs: 'FuncTest') -> ModuleReturnValue:
74        """Generate a rust test target from a given rust target.
75
76        Rust puts it's unitests inside it's main source files, unlike most
77        languages that put them in external files. This means that normally
78        you have to define two separate targets with basically the same
79        arguments to get tests:
80
81        ```meson
82        rust_lib_sources = [...]
83        rust_lib = static_library(
84            'rust_lib',
85            rust_lib_sources,
86        )
87
88        rust_lib_test = executable(
89            'rust_lib_test',
90            rust_lib_sources,
91            rust_args : ['--test'],
92        )
93
94        test(
95            'rust_lib_test',
96            rust_lib_test,
97            protocol : 'rust',
98        )
99        ```
100
101        This is all fine, but not very DRY. This method makes it much easier
102        to define rust tests:
103
104        ```meson
105        rust = import('unstable-rust')
106
107        rust_lib = static_library(
108            'rust_lib',
109            [sources],
110        )
111
112        rust.test('rust_lib_test', rust_lib)
113        ```
114        """
115        name = args[0]
116        base_target: BuildTarget = args[1]
117        if not base_target.uses_rust():
118            raise InterpreterException('Second positional argument to rustmod.test() must be a rust based target')
119        extra_args = kwargs['args']
120
121        # Delete any arguments we don't want passed
122        if '--test' in extra_args:
123            mlog.warning('Do not add --test to rustmod.test arguments')
124            extra_args.remove('--test')
125        if '--format' in extra_args:
126            mlog.warning('Do not add --format to rustmod.test arguments')
127            i = extra_args.index('--format')
128            # Also delete the argument to --format
129            del extra_args[i + 1]
130            del extra_args[i]
131        for i, a in enumerate(extra_args):
132            if isinstance(a, str) and a.startswith('--format='):
133                del extra_args[i]
134                break
135
136        dependencies = [d for d in kwargs['dependencies']]
137
138        # We need to cast here, as currently these don't have protocol in them, but test itself does.
139        tkwargs = T.cast('_kwargs.FuncTest', kwargs.copy())
140
141        tkwargs['args'] = extra_args + ['--test', '--format', 'pretty']
142        tkwargs['protocol'] = 'rust'
143
144        new_target_kwargs = base_target.kwargs.copy()
145        # Don't mutate the shallow copied list, instead replace it with a new
146        # one
147        new_target_kwargs['rust_args'] = new_target_kwargs.get('rust_args', []) + ['--test']
148        new_target_kwargs['install'] = False
149        new_target_kwargs['dependencies'] = new_target_kwargs.get('dependencies', []) + dependencies
150
151        new_target = Executable(
152            name, base_target.subdir, state.subproject,
153            base_target.for_machine, base_target.sources,
154            base_target.objects, base_target.environment,
155            new_target_kwargs
156        )
157
158        test = self.interpreter.make_test(
159            self.interpreter.current_node, (name, new_target), tkwargs)
160
161        return ModuleReturnValue(None, [new_target, test])
162
163    @noPosargs
164    @typed_kwargs(
165        'rust.bindgen',
166        KwargInfo('c_args', ContainerTypeInfo(list, str), default=[], listify=True),
167        KwargInfo('args', ContainerTypeInfo(list, str), default=[], listify=True),
168        KwargInfo('include_directories', ContainerTypeInfo(list, IncludeDirs), default=[], listify=True),
169        KwargInfo(
170            'input',
171            ContainerTypeInfo(list, (File, GeneratedList, BuildTarget, BothLibraries, ExtractedObjects, CustomTargetIndex, CustomTarget, str), allow_empty=False),
172            default=[],
173            listify=True,
174            required=True,
175        ),
176        KwargInfo('output', str, required=True),
177    )
178    def bindgen(self, state: 'ModuleState', args: T.List, kwargs: 'FuncBindgen') -> ModuleReturnValue:
179        """Wrapper around bindgen to simplify it's use.
180
181        The main thing this simplifies is the use of `include_directory`
182        objects, instead of having to pass a plethora of `-I` arguments.
183        """
184        header, *_deps = self.interpreter.source_strings_to_files(kwargs['input'])
185
186        # Split File and Target dependencies to add pass to CustomTarget
187        depends: T.List['SourceOutputs'] = []
188        depend_files: T.List[File] = []
189        for d in _deps:
190            if isinstance(d, File):
191                depend_files.append(d)
192            else:
193                depends.append(d)
194
195        inc_strs: T.List[str] = []
196        for i in kwargs['include_directories']:
197            # bindgen always uses clang, so it's safe to hardcode -I here
198            inc_strs.extend([f'-I{x}' for x in i.to_string_list(state.environment.get_source_dir())])
199
200        if self._bindgen_bin is None:
201            self._bindgen_bin = state.find_program('bindgen')
202
203        name: str
204        if isinstance(header, File):
205            name = header.fname
206        elif isinstance(header, (BuildTarget, BothLibraries, ExtractedObjects)):
207            raise InterpreterException('bindgen source file must be a C header, not an object or build target')
208        else:
209            name = header.get_outputs()[0]
210
211        target = CustomTarget(
212            f'rustmod-bindgen-{name}'.replace('/', '_'),
213            state.subdir,
214            state.subproject,
215            {
216                'input': header,
217                'output': kwargs['output'],
218                'command': self._bindgen_bin.get_command() + [
219                    '@INPUT@', '--output',
220                    os.path.join(state.environment.build_dir, '@OUTPUT@')] +
221                    kwargs['args'] + ['--'] + kwargs['c_args'] + inc_strs +
222                    ['-MD', '-MQ', '@INPUT@', '-MF', '@DEPFILE@'],
223                'depfile': '@PLAINNAME@.d',
224                'depends': depends,
225                'depend_files': depend_files,
226            },
227            backend=state.backend,
228        )
229
230        return ModuleReturnValue([target], [target])
231
232
233def initialize(interp: 'Interpreter') -> RustModule:
234    return RustModule(interp)
235