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