1# Copyright 2013-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 file contains the detection logic for external dependencies. 16# Custom logic for several other packages are in separate files. 17import copy 18import os 19import itertools 20import typing as T 21from enum import Enum 22 23from .. import mlog 24from ..compilers import clib_langs 25from ..mesonlib import MachineChoice, MesonException, HoldableObject 26from ..mesonlib import version_compare_many 27from ..interpreterbase import FeatureDeprecated 28 29if T.TYPE_CHECKING: 30 from ..compilers.compilers import Compiler 31 from ..environment import Environment 32 from ..build import BuildTarget 33 from ..mesonlib import FileOrString 34 35 36class DependencyException(MesonException): 37 '''Exceptions raised while trying to find dependencies''' 38 39 40class DependencyMethods(Enum): 41 # Auto means to use whatever dependency checking mechanisms in whatever order meson thinks is best. 42 AUTO = 'auto' 43 PKGCONFIG = 'pkg-config' 44 CMAKE = 'cmake' 45 # The dependency is provided by the standard library and does not need to be linked 46 BUILTIN = 'builtin' 47 # Just specify the standard link arguments, assuming the operating system provides the library. 48 SYSTEM = 'system' 49 # This is only supported on OSX - search the frameworks directory by name. 50 EXTRAFRAMEWORK = 'extraframework' 51 # Detect using the sysconfig module. 52 SYSCONFIG = 'sysconfig' 53 # Specify using a "program"-config style tool 54 CONFIG_TOOL = 'config-tool' 55 # For backwards compatibility 56 SDLCONFIG = 'sdlconfig' 57 CUPSCONFIG = 'cups-config' 58 PCAPCONFIG = 'pcap-config' 59 LIBWMFCONFIG = 'libwmf-config' 60 QMAKE = 'qmake' 61 # Misc 62 DUB = 'dub' 63 64 65DependencyTypeName = T.NewType('DependencyTypeName', str) 66 67 68class Dependency(HoldableObject): 69 70 @classmethod 71 def _process_include_type_kw(cls, kwargs: T.Dict[str, T.Any]) -> str: 72 if 'include_type' not in kwargs: 73 return 'preserve' 74 if not isinstance(kwargs['include_type'], str): 75 raise DependencyException('The include_type kwarg must be a string type') 76 if kwargs['include_type'] not in ['preserve', 'system', 'non-system']: 77 raise DependencyException("include_type may only be one of ['preserve', 'system', 'non-system']") 78 return kwargs['include_type'] 79 80 def __init__(self, type_name: DependencyTypeName, kwargs: T.Dict[str, T.Any]) -> None: 81 self.name = "null" 82 self.version: T.Optional[str] = None 83 self.language: T.Optional[str] = None # None means C-like 84 self.is_found = False 85 self.type_name = type_name 86 self.compile_args: T.List[str] = [] 87 self.link_args: T.List[str] = [] 88 # Raw -L and -l arguments without manual library searching 89 # If None, self.link_args will be used 90 self.raw_link_args: T.Optional[T.List[str]] = None 91 self.sources: T.List['FileOrString'] = [] 92 self.methods = process_method_kw(self.get_methods(), kwargs) 93 self.include_type = self._process_include_type_kw(kwargs) 94 self.ext_deps: T.List[Dependency] = [] 95 96 def __repr__(self) -> str: 97 return f'<{self.__class__.__name__} {self.name}: {self.is_found}>' 98 99 def is_built(self) -> bool: 100 return False 101 102 def summary_value(self) -> T.Union[str, mlog.AnsiDecorator, mlog.AnsiText]: 103 if not self.found(): 104 return mlog.red('NO') 105 if not self.version: 106 return mlog.green('YES') 107 return mlog.AnsiText(mlog.green('YES'), ' ', mlog.cyan(self.version)) 108 109 def get_compile_args(self) -> T.List[str]: 110 if self.include_type == 'system': 111 converted = [] 112 for i in self.compile_args: 113 if i.startswith('-I') or i.startswith('/I'): 114 converted += ['-isystem' + i[2:]] 115 else: 116 converted += [i] 117 return converted 118 if self.include_type == 'non-system': 119 converted = [] 120 for i in self.compile_args: 121 if i.startswith('-isystem'): 122 converted += ['-I' + i[8:]] 123 else: 124 converted += [i] 125 return converted 126 return self.compile_args 127 128 def get_all_compile_args(self) -> T.List[str]: 129 """Get the compile arguments from this dependency and it's sub dependencies.""" 130 return list(itertools.chain(self.get_compile_args(), 131 *[d.get_all_compile_args() for d in self.ext_deps])) 132 133 def get_link_args(self, language: T.Optional[str] = None, raw: bool = False) -> T.List[str]: 134 if raw and self.raw_link_args is not None: 135 return self.raw_link_args 136 return self.link_args 137 138 def get_all_link_args(self) -> T.List[str]: 139 """Get the link arguments from this dependency and it's sub dependencies.""" 140 return list(itertools.chain(self.get_link_args(), 141 *[d.get_all_link_args() for d in self.ext_deps])) 142 143 def found(self) -> bool: 144 return self.is_found 145 146 def get_sources(self) -> T.List['FileOrString']: 147 """Source files that need to be added to the target. 148 As an example, gtest-all.cc when using GTest.""" 149 return self.sources 150 151 @staticmethod 152 def get_methods() -> T.List[DependencyMethods]: 153 return [DependencyMethods.AUTO] 154 155 def get_name(self) -> str: 156 return self.name 157 158 def get_version(self) -> str: 159 if self.version: 160 return self.version 161 else: 162 return 'unknown' 163 164 def get_include_type(self) -> str: 165 return self.include_type 166 167 def get_exe_args(self, compiler: 'Compiler') -> T.List[str]: 168 return [] 169 170 def get_pkgconfig_variable(self, variable_name: str, kwargs: T.Dict[str, T.Any]) -> str: 171 raise DependencyException(f'{self.name!r} is not a pkgconfig dependency') 172 173 def get_configtool_variable(self, variable_name: str) -> str: 174 raise DependencyException(f'{self.name!r} is not a config-tool dependency') 175 176 def get_partial_dependency(self, *, compile_args: bool = False, 177 link_args: bool = False, links: bool = False, 178 includes: bool = False, sources: bool = False) -> 'Dependency': 179 """Create a new dependency that contains part of the parent dependency. 180 181 The following options can be inherited: 182 links -- all link_with arguments 183 includes -- all include_directory and -I/-isystem calls 184 sources -- any source, header, or generated sources 185 compile_args -- any compile args 186 link_args -- any link args 187 188 Additionally the new dependency will have the version parameter of it's 189 parent (if any) and the requested values of any dependencies will be 190 added as well. 191 """ 192 raise RuntimeError('Unreachable code in partial_dependency called') 193 194 def _add_sub_dependency(self, deplist: T.Iterable[T.Callable[[], 'Dependency']]) -> bool: 195 """Add an internal depdency from a list of possible dependencies. 196 197 This method is intended to make it easier to add additional 198 dependencies to another dependency internally. 199 200 Returns true if the dependency was successfully added, false 201 otherwise. 202 """ 203 for d in deplist: 204 dep = d() 205 if dep.is_found: 206 self.ext_deps.append(dep) 207 return True 208 return False 209 210 def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None, 211 configtool: T.Optional[str] = None, internal: T.Optional[str] = None, 212 default_value: T.Optional[str] = None, 213 pkgconfig_define: T.Optional[T.List[str]] = None) -> T.Union[str, T.List[str]]: 214 if default_value is not None: 215 return default_value 216 raise DependencyException(f'No default provided for dependency {self!r}, which is not pkg-config, cmake, or config-tool based.') 217 218 def generate_system_dependency(self, include_type: str) -> 'Dependency': 219 new_dep = copy.deepcopy(self) 220 new_dep.include_type = self._process_include_type_kw({'include_type': include_type}) 221 return new_dep 222 223class InternalDependency(Dependency): 224 def __init__(self, version: str, incdirs: T.List[str], compile_args: T.List[str], 225 link_args: T.List[str], libraries: T.List['BuildTarget'], 226 whole_libraries: T.List['BuildTarget'], sources: T.List['FileOrString'], 227 ext_deps: T.List[Dependency], variables: T.Dict[str, T.Any]): 228 super().__init__(DependencyTypeName('internal'), {}) 229 self.version = version 230 self.is_found = True 231 self.include_directories = incdirs 232 self.compile_args = compile_args 233 self.link_args = link_args 234 self.libraries = libraries 235 self.whole_libraries = whole_libraries 236 self.sources = sources 237 self.ext_deps = ext_deps 238 self.variables = variables 239 240 def __deepcopy__(self, memo: T.Dict[int, 'InternalDependency']) -> 'InternalDependency': 241 result = self.__class__.__new__(self.__class__) 242 assert isinstance(result, InternalDependency) 243 memo[id(self)] = result 244 for k, v in self.__dict__.items(): 245 if k in ['libraries', 'whole_libraries']: 246 setattr(result, k, copy.copy(v)) 247 else: 248 setattr(result, k, copy.deepcopy(v, memo)) 249 return result 250 251 def summary_value(self) -> mlog.AnsiDecorator: 252 # Omit the version. Most of the time it will be just the project 253 # version, which is uninteresting in the summary. 254 return mlog.green('YES') 255 256 def is_built(self) -> bool: 257 if self.sources or self.libraries or self.whole_libraries: 258 return True 259 return any(d.is_built() for d in self.ext_deps) 260 261 def get_pkgconfig_variable(self, variable_name: str, kwargs: T.Dict[str, T.Any]) -> str: 262 raise DependencyException('Method "get_pkgconfig_variable()" is ' 263 'invalid for an internal dependency') 264 265 def get_configtool_variable(self, variable_name: str) -> str: 266 raise DependencyException('Method "get_configtool_variable()" is ' 267 'invalid for an internal dependency') 268 269 def get_partial_dependency(self, *, compile_args: bool = False, 270 link_args: bool = False, links: bool = False, 271 includes: bool = False, sources: bool = False) -> 'InternalDependency': 272 final_compile_args = self.compile_args.copy() if compile_args else [] 273 final_link_args = self.link_args.copy() if link_args else [] 274 final_libraries = self.libraries.copy() if links else [] 275 final_whole_libraries = self.whole_libraries.copy() if links else [] 276 final_sources = self.sources.copy() if sources else [] 277 final_includes = self.include_directories.copy() if includes else [] 278 final_deps = [d.get_partial_dependency( 279 compile_args=compile_args, link_args=link_args, links=links, 280 includes=includes, sources=sources) for d in self.ext_deps] 281 return InternalDependency( 282 self.version, final_includes, final_compile_args, 283 final_link_args, final_libraries, final_whole_libraries, 284 final_sources, final_deps, self.variables) 285 286 def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None, 287 configtool: T.Optional[str] = None, internal: T.Optional[str] = None, 288 default_value: T.Optional[str] = None, 289 pkgconfig_define: T.Optional[T.List[str]] = None) -> T.Union[str, T.List[str]]: 290 val = self.variables.get(internal, default_value) 291 if val is not None: 292 # TODO: Try removing this assert by better typing self.variables 293 if isinstance(val, str): 294 return val 295 if isinstance(val, list): 296 for i in val: 297 assert isinstance(i, str) 298 return val 299 raise DependencyException(f'Could not get an internal variable and no default provided for {self!r}') 300 301 def generate_link_whole_dependency(self) -> Dependency: 302 new_dep = copy.deepcopy(self) 303 new_dep.whole_libraries += new_dep.libraries 304 new_dep.libraries = [] 305 return new_dep 306 307class HasNativeKwarg: 308 def __init__(self, kwargs: T.Dict[str, T.Any]): 309 self.for_machine = self.get_for_machine_from_kwargs(kwargs) 310 311 def get_for_machine_from_kwargs(self, kwargs: T.Dict[str, T.Any]) -> MachineChoice: 312 return MachineChoice.BUILD if kwargs.get('native', False) else MachineChoice.HOST 313 314class ExternalDependency(Dependency, HasNativeKwarg): 315 def __init__(self, type_name: DependencyTypeName, environment: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None): 316 Dependency.__init__(self, type_name, kwargs) 317 self.env = environment 318 self.name = type_name # default 319 self.is_found = False 320 self.language = language 321 self.version_reqs = kwargs.get('version', None) 322 if isinstance(self.version_reqs, str): 323 self.version_reqs = [self.version_reqs] 324 self.required = kwargs.get('required', True) 325 self.silent = kwargs.get('silent', False) 326 self.static = kwargs.get('static', False) 327 if not isinstance(self.static, bool): 328 raise DependencyException('Static keyword must be boolean') 329 # Is this dependency to be run on the build platform? 330 HasNativeKwarg.__init__(self, kwargs) 331 self.clib_compiler = detect_compiler(self.name, environment, self.for_machine, self.language) 332 333 def get_compiler(self) -> 'Compiler': 334 return self.clib_compiler 335 336 def get_partial_dependency(self, *, compile_args: bool = False, 337 link_args: bool = False, links: bool = False, 338 includes: bool = False, sources: bool = False) -> Dependency: 339 new = copy.copy(self) 340 if not compile_args: 341 new.compile_args = [] 342 if not link_args: 343 new.link_args = [] 344 if not sources: 345 new.sources = [] 346 if not includes: 347 pass # TODO maybe filter compile_args? 348 if not sources: 349 new.sources = [] 350 351 return new 352 353 def log_details(self) -> str: 354 return '' 355 356 def log_info(self) -> str: 357 return '' 358 359 def log_tried(self) -> str: 360 return '' 361 362 # Check if dependency version meets the requirements 363 def _check_version(self) -> None: 364 if not self.is_found: 365 return 366 367 if self.version_reqs: 368 # an unknown version can never satisfy any requirement 369 if not self.version: 370 self.is_found = False 371 found_msg: mlog.TV_LoggableList = [] 372 found_msg += ['Dependency', mlog.bold(self.name), 'found:'] 373 found_msg += [mlog.red('NO'), 'unknown version, but need:', self.version_reqs] 374 mlog.log(*found_msg) 375 376 if self.required: 377 m = f'Unknown version of dependency {self.name!r}, but need {self.version_reqs!r}.' 378 raise DependencyException(m) 379 380 else: 381 (self.is_found, not_found, found) = \ 382 version_compare_many(self.version, self.version_reqs) 383 if not self.is_found: 384 found_msg = ['Dependency', mlog.bold(self.name), 'found:'] 385 found_msg += [mlog.red('NO'), 386 'found', mlog.normal_cyan(self.version), 'but need:', 387 mlog.bold(', '.join([f"'{e}'" for e in not_found]))] 388 if found: 389 found_msg += ['; matched:', 390 ', '.join([f"'{e}'" for e in found])] 391 mlog.log(*found_msg) 392 393 if self.required: 394 m = 'Invalid version of dependency, need {!r} {!r} found {!r}.' 395 raise DependencyException(m.format(self.name, not_found, self.version)) 396 return 397 398 399class NotFoundDependency(Dependency): 400 def __init__(self, environment: 'Environment') -> None: 401 super().__init__(DependencyTypeName('not-found'), {}) 402 self.env = environment 403 self.name = 'not-found' 404 self.is_found = False 405 406 def get_partial_dependency(self, *, compile_args: bool = False, 407 link_args: bool = False, links: bool = False, 408 includes: bool = False, sources: bool = False) -> 'NotFoundDependency': 409 return copy.copy(self) 410 411 412class ExternalLibrary(ExternalDependency): 413 def __init__(self, name: str, link_args: T.List[str], environment: 'Environment', 414 language: str, silent: bool = False) -> None: 415 super().__init__(DependencyTypeName('library'), environment, {}, language=language) 416 self.name = name 417 self.language = language 418 self.is_found = False 419 if link_args: 420 self.is_found = True 421 self.link_args = link_args 422 if not silent: 423 if self.is_found: 424 mlog.log('Library', mlog.bold(name), 'found:', mlog.green('YES')) 425 else: 426 mlog.log('Library', mlog.bold(name), 'found:', mlog.red('NO')) 427 428 def get_link_args(self, language: T.Optional[str] = None, raw: bool = False) -> T.List[str]: 429 ''' 430 External libraries detected using a compiler must only be used with 431 compatible code. For instance, Vala libraries (.vapi files) cannot be 432 used with C code, and not all Rust library types can be linked with 433 C-like code. Note that C++ libraries *can* be linked with C code with 434 a C++ linker (and vice-versa). 435 ''' 436 # Using a vala library in a non-vala target, or a non-vala library in a vala target 437 # XXX: This should be extended to other non-C linkers such as Rust 438 if (self.language == 'vala' and language != 'vala') or \ 439 (language == 'vala' and self.language != 'vala'): 440 return [] 441 return super().get_link_args(language=language, raw=raw) 442 443 def get_partial_dependency(self, *, compile_args: bool = False, 444 link_args: bool = False, links: bool = False, 445 includes: bool = False, sources: bool = False) -> 'ExternalLibrary': 446 # External library only has link_args, so ignore the rest of the 447 # interface. 448 new = copy.copy(self) 449 if not link_args: 450 new.link_args = [] 451 return new 452 453 454def sort_libpaths(libpaths: T.List[str], refpaths: T.List[str]) -> T.List[str]: 455 """Sort <libpaths> according to <refpaths> 456 457 It is intended to be used to sort -L flags returned by pkg-config. 458 Pkg-config returns flags in random order which cannot be relied on. 459 """ 460 if len(refpaths) == 0: 461 return list(libpaths) 462 463 def key_func(libpath: str) -> T.Tuple[int, int]: 464 common_lengths: T.List[int] = [] 465 for refpath in refpaths: 466 try: 467 common_path: str = os.path.commonpath([libpath, refpath]) 468 except ValueError: 469 common_path = '' 470 common_lengths.append(len(common_path)) 471 max_length = max(common_lengths) 472 max_index = common_lengths.index(max_length) 473 reversed_max_length = len(refpaths[max_index]) - max_length 474 return (max_index, reversed_max_length) 475 return sorted(libpaths, key=key_func) 476 477def strip_system_libdirs(environment: 'Environment', for_machine: MachineChoice, link_args: T.List[str]) -> T.List[str]: 478 """Remove -L<system path> arguments. 479 480 leaving these in will break builds where a user has a version of a library 481 in the system path, and a different version not in the system path if they 482 want to link against the non-system path version. 483 """ 484 exclude = {f'-L{p}' for p in environment.get_compiler_system_dirs(for_machine)} 485 return [l for l in link_args if l not in exclude] 486 487def process_method_kw(possible: T.Iterable[DependencyMethods], kwargs: T.Dict[str, T.Any]) -> T.List[DependencyMethods]: 488 method = kwargs.get('method', 'auto') # type: T.Union[DependencyMethods, str] 489 if isinstance(method, DependencyMethods): 490 return [method] 491 # TODO: try/except? 492 if method not in [e.value for e in DependencyMethods]: 493 raise DependencyException(f'method {method!r} is invalid') 494 method = DependencyMethods(method) 495 496 # This sets per-tool config methods which are deprecated to to the new 497 # generic CONFIG_TOOL value. 498 if method in [DependencyMethods.SDLCONFIG, DependencyMethods.CUPSCONFIG, 499 DependencyMethods.PCAPCONFIG, DependencyMethods.LIBWMFCONFIG]: 500 FeatureDeprecated.single_use(f'Configuration method {method.value}', '0.44', 'Use "config-tool" instead.') 501 method = DependencyMethods.CONFIG_TOOL 502 if method is DependencyMethods.QMAKE: 503 FeatureDeprecated.single_use(f'Configuration method "qmake"', '0.58', 'Use "config-tool" instead.') 504 method = DependencyMethods.CONFIG_TOOL 505 506 # Set the detection method. If the method is set to auto, use any available method. 507 # If method is set to a specific string, allow only that detection method. 508 if method == DependencyMethods.AUTO: 509 methods = list(possible) 510 elif method in possible: 511 methods = [method] 512 else: 513 raise DependencyException( 514 'Unsupported detection method: {}, allowed methods are {}'.format( 515 method.value, 516 mlog.format_list([x.value for x in [DependencyMethods.AUTO] + list(possible)]))) 517 518 return methods 519 520def detect_compiler(name: str, env: 'Environment', for_machine: MachineChoice, 521 language: T.Optional[str]) -> T.Optional['Compiler']: 522 """Given a language and environment find the compiler used.""" 523 compilers = env.coredata.compilers[for_machine] 524 525 # Set the compiler for this dependency if a language is specified, 526 # else try to pick something that looks usable. 527 if language: 528 if language not in compilers: 529 m = name.capitalize() + ' requires a {0} compiler, but ' \ 530 '{0} is not in the list of project languages' 531 raise DependencyException(m.format(language.capitalize())) 532 return compilers[language] 533 else: 534 for lang in clib_langs: 535 try: 536 return compilers[lang] 537 except KeyError: 538 continue 539 return None 540 541 542class SystemDependency(ExternalDependency): 543 544 """Dependency base for System type dependencies.""" 545 546 def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any], 547 language: T.Optional[str] = None) -> None: 548 super().__init__(DependencyTypeName('system'), env, kwargs, language=language) 549 self.name = name 550 551 @staticmethod 552 def get_methods() -> T.List[DependencyMethods]: 553 return [DependencyMethods.SYSTEM] 554 555 def log_tried(self) -> str: 556 return 'system' 557 558 559class BuiltinDependency(ExternalDependency): 560 561 """Dependency base for Builtin type dependencies.""" 562 563 def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any], 564 language: T.Optional[str] = None) -> None: 565 super().__init__(DependencyTypeName('builtin'), env, kwargs, language=language) 566 self.name = name 567 568 @staticmethod 569 def get_methods() -> T.List[DependencyMethods]: 570 return [DependencyMethods.BUILTIN] 571 572 def log_tried(self) -> str: 573 return 'builtin' 574