1# Copyright 2013-2021 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 .base import ExternalDependency, DependencyException, sort_libpaths, DependencyTypeName 16from ..mesonlib import MachineChoice, OptionKey, OrderedSet, PerMachine, Popen_safe 17from ..programs import find_external_program, ExternalProgram 18from .. import mlog 19from pathlib import PurePath 20import re 21import os 22import shlex 23import typing as T 24 25if T.TYPE_CHECKING: 26 from ..environment import Environment 27 28class PkgConfigDependency(ExternalDependency): 29 # The class's copy of the pkg-config path. Avoids having to search for it 30 # multiple times in the same Meson invocation. 31 class_pkgbin: PerMachine[T.Union[None, bool, ExternalProgram]] = PerMachine(None, None) 32 # We cache all pkg-config subprocess invocations to avoid redundant calls 33 pkgbin_cache: T.Dict[ 34 T.Tuple[ExternalProgram, T.Tuple[str, ...], T.FrozenSet[T.Tuple[str, str]]], 35 T.Tuple[int, str, str] 36 ] = {} 37 38 def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None) -> None: 39 super().__init__(DependencyTypeName('pkgconfig'), environment, kwargs, language=language) 40 self.name = name 41 self.is_libtool = False 42 # Store a copy of the pkg-config path on the object itself so it is 43 # stored in the pickled coredata and recovered. 44 self.pkgbin: T.Union[None, bool, ExternalProgram] = None 45 46 # Only search for pkg-config for each machine the first time and store 47 # the result in the class definition 48 if PkgConfigDependency.class_pkgbin[self.for_machine] is False: 49 mlog.debug('Pkg-config binary for %s is cached as not found.' % self.for_machine) 50 elif PkgConfigDependency.class_pkgbin[self.for_machine] is not None: 51 mlog.debug('Pkg-config binary for %s is cached.' % self.for_machine) 52 else: 53 assert PkgConfigDependency.class_pkgbin[self.for_machine] is None 54 mlog.debug('Pkg-config binary for %s is not cached.' % self.for_machine) 55 for potential_pkgbin in find_external_program( 56 self.env, self.for_machine, 'pkgconfig', 'Pkg-config', 57 environment.default_pkgconfig, allow_default_for_cross=False): 58 version_if_ok = self.check_pkgconfig(potential_pkgbin) 59 if not version_if_ok: 60 continue 61 if not self.silent: 62 mlog.log('Found pkg-config:', mlog.bold(potential_pkgbin.get_path()), 63 '(%s)' % version_if_ok) 64 PkgConfigDependency.class_pkgbin[self.for_machine] = potential_pkgbin 65 break 66 else: 67 if not self.silent: 68 mlog.log('Found Pkg-config:', mlog.red('NO')) 69 # Set to False instead of None to signify that we've already 70 # searched for it and not found it 71 PkgConfigDependency.class_pkgbin[self.for_machine] = False 72 73 self.pkgbin = PkgConfigDependency.class_pkgbin[self.for_machine] 74 if self.pkgbin is False: 75 self.pkgbin = None 76 msg = 'Pkg-config binary for machine %s not found. Giving up.' % self.for_machine 77 if self.required: 78 raise DependencyException(msg) 79 else: 80 mlog.debug(msg) 81 return 82 83 assert isinstance(self.pkgbin, ExternalProgram) 84 mlog.debug('Determining dependency {!r} with pkg-config executable ' 85 '{!r}'.format(name, self.pkgbin.get_path())) 86 ret, self.version, _ = self._call_pkgbin(['--modversion', name]) 87 if ret != 0: 88 return 89 90 self.is_found = True 91 92 try: 93 # Fetch cargs to be used while using this dependency 94 self._set_cargs() 95 # Fetch the libraries and library paths needed for using this 96 self._set_libs() 97 except DependencyException as e: 98 mlog.debug(f"pkg-config error with '{name}': {e}") 99 if self.required: 100 raise 101 else: 102 self.compile_args = [] 103 self.link_args = [] 104 self.is_found = False 105 self.reason = e 106 107 def __repr__(self) -> str: 108 s = '<{0} {1}: {2} {3}>' 109 return s.format(self.__class__.__name__, self.name, self.is_found, 110 self.version_reqs) 111 112 def _call_pkgbin_real(self, args: T.List[str], env: T.Dict[str, str]) -> T.Tuple[int, str, str]: 113 assert isinstance(self.pkgbin, ExternalProgram) 114 cmd = self.pkgbin.get_command() + args 115 p, out, err = Popen_safe(cmd, env=env) 116 rc, out, err = p.returncode, out.strip(), err.strip() 117 call = ' '.join(cmd) 118 mlog.debug(f"Called `{call}` -> {rc}\n{out}") 119 return rc, out, err 120 121 @staticmethod 122 def setup_env(env: T.MutableMapping[str, str], environment: 'Environment', for_machine: MachineChoice, 123 extra_path: T.Optional[str] = None) -> None: 124 extra_paths: T.List[str] = environment.coredata.options[OptionKey('pkg_config_path', machine=for_machine)].value[:] 125 if extra_path and extra_path not in extra_paths: 126 extra_paths.append(extra_path) 127 sysroot = environment.properties[for_machine].get_sys_root() 128 if sysroot: 129 env['PKG_CONFIG_SYSROOT_DIR'] = sysroot 130 new_pkg_config_path = ':'.join([p for p in extra_paths]) 131 env['PKG_CONFIG_PATH'] = new_pkg_config_path 132 133 pkg_config_libdir_prop = environment.properties[for_machine].get_pkg_config_libdir() 134 if pkg_config_libdir_prop: 135 new_pkg_config_libdir = ':'.join([p for p in pkg_config_libdir_prop]) 136 env['PKG_CONFIG_LIBDIR'] = new_pkg_config_libdir 137 # Dump all PKG_CONFIG environment variables 138 for key, value in env.items(): 139 if key.startswith('PKG_'): 140 mlog.debug(f'env[{key}]: {value}') 141 142 def _call_pkgbin(self, args: T.List[str], env: T.Optional[T.Dict[str, str]] = None) -> T.Tuple[int, str, str]: 143 # Always copy the environment since we're going to modify it 144 # with pkg-config variables 145 if env is None: 146 env = os.environ.copy() 147 else: 148 env = env.copy() 149 150 assert isinstance(self.pkgbin, ExternalProgram) 151 PkgConfigDependency.setup_env(env, self.env, self.for_machine) 152 153 fenv = frozenset(env.items()) 154 targs = tuple(args) 155 cache = PkgConfigDependency.pkgbin_cache 156 if (self.pkgbin, targs, fenv) not in cache: 157 cache[(self.pkgbin, targs, fenv)] = self._call_pkgbin_real(args, env) 158 return cache[(self.pkgbin, targs, fenv)] 159 160 def _convert_mingw_paths(self, args: T.List[str]) -> T.List[str]: 161 ''' 162 Both MSVC and native Python on Windows cannot handle MinGW-esque /c/foo 163 paths so convert them to C:/foo. We cannot resolve other paths starting 164 with / like /home/foo so leave them as-is so that the user gets an 165 error/warning from the compiler/linker. 166 ''' 167 if not self.env.machines.build.is_windows(): 168 return args 169 converted = [] 170 for arg in args: 171 pargs: T.Tuple[str, ...] = tuple() 172 # Library search path 173 if arg.startswith('-L/'): 174 pargs = PurePath(arg[2:]).parts 175 tmpl = '-L{}:/{}' 176 elif arg.startswith('-I/'): 177 pargs = PurePath(arg[2:]).parts 178 tmpl = '-I{}:/{}' 179 # Full path to library or .la file 180 elif arg.startswith('/'): 181 pargs = PurePath(arg).parts 182 tmpl = '{}:/{}' 183 elif arg.startswith(('-L', '-I')) or (len(arg) > 2 and arg[1] == ':'): 184 # clean out improper '\\ ' as comes from some Windows pkg-config files 185 arg = arg.replace('\\ ', ' ') 186 if len(pargs) > 1 and len(pargs[1]) == 1: 187 arg = tmpl.format(pargs[1], '/'.join(pargs[2:])) 188 converted.append(arg) 189 return converted 190 191 def _split_args(self, cmd: str) -> T.List[str]: 192 # pkg-config paths follow Unix conventions, even on Windows; split the 193 # output using shlex.split rather than mesonlib.split_args 194 return shlex.split(cmd) 195 196 def _set_cargs(self) -> None: 197 env = None 198 if self.language == 'fortran': 199 # gfortran doesn't appear to look in system paths for INCLUDE files, 200 # so don't allow pkg-config to suppress -I flags for system paths 201 env = os.environ.copy() 202 env['PKG_CONFIG_ALLOW_SYSTEM_CFLAGS'] = '1' 203 ret, out, err = self._call_pkgbin(['--cflags', self.name], env=env) 204 if ret != 0: 205 raise DependencyException('Could not generate cargs for %s:\n%s\n' % 206 (self.name, err)) 207 self.compile_args = self._convert_mingw_paths(self._split_args(out)) 208 209 def _search_libs(self, out: str, out_raw: str) -> T.Tuple[T.List[str], T.List[str]]: 210 ''' 211 @out: PKG_CONFIG_ALLOW_SYSTEM_LIBS=1 pkg-config --libs 212 @out_raw: pkg-config --libs 213 214 We always look for the file ourselves instead of depending on the 215 compiler to find it with -lfoo or foo.lib (if possible) because: 216 1. We want to be able to select static or shared 217 2. We need the full path of the library to calculate RPATH values 218 3. De-dup of libraries is easier when we have absolute paths 219 220 Libraries that are provided by the toolchain or are not found by 221 find_library() will be added with -L -l pairs. 222 ''' 223 # Library paths should be safe to de-dup 224 # 225 # First, figure out what library paths to use. Originally, we were 226 # doing this as part of the loop, but due to differences in the order 227 # of -L values between pkg-config and pkgconf, we need to do that as 228 # a separate step. See: 229 # https://github.com/mesonbuild/meson/issues/3951 230 # https://github.com/mesonbuild/meson/issues/4023 231 # 232 # Separate system and prefix paths, and ensure that prefix paths are 233 # always searched first. 234 prefix_libpaths: OrderedSet[str] = OrderedSet() 235 # We also store this raw_link_args on the object later 236 raw_link_args = self._convert_mingw_paths(self._split_args(out_raw)) 237 for arg in raw_link_args: 238 if arg.startswith('-L') and not arg.startswith(('-L-l', '-L-L')): 239 path = arg[2:] 240 if not os.path.isabs(path): 241 # Resolve the path as a compiler in the build directory would 242 path = os.path.join(self.env.get_build_dir(), path) 243 prefix_libpaths.add(path) 244 # Library paths are not always ordered in a meaningful way 245 # 246 # Instead of relying on pkg-config or pkgconf to provide -L flags in a 247 # specific order, we reorder library paths ourselves, according to th 248 # order specified in PKG_CONFIG_PATH. See: 249 # https://github.com/mesonbuild/meson/issues/4271 250 # 251 # Only prefix_libpaths are reordered here because there should not be 252 # too many system_libpaths to cause library version issues. 253 pkg_config_path: T.List[str] = self.env.coredata.options[OptionKey('pkg_config_path', machine=self.for_machine)].value 254 pkg_config_path = self._convert_mingw_paths(pkg_config_path) 255 prefix_libpaths = OrderedSet(sort_libpaths(list(prefix_libpaths), pkg_config_path)) 256 system_libpaths: OrderedSet[str] = OrderedSet() 257 full_args = self._convert_mingw_paths(self._split_args(out)) 258 for arg in full_args: 259 if arg.startswith(('-L-l', '-L-L')): 260 # These are D language arguments, not library paths 261 continue 262 if arg.startswith('-L') and arg[2:] not in prefix_libpaths: 263 system_libpaths.add(arg[2:]) 264 # Use this re-ordered path list for library resolution 265 libpaths = list(prefix_libpaths) + list(system_libpaths) 266 # Track -lfoo libraries to avoid duplicate work 267 libs_found: OrderedSet[str] = OrderedSet() 268 # Track not-found libraries to know whether to add library paths 269 libs_notfound = [] 270 # Generate link arguments for this library 271 link_args = [] 272 for lib in full_args: 273 if lib.startswith(('-L-l', '-L-L')): 274 # These are D language arguments, add them as-is 275 pass 276 elif lib.startswith('-L'): 277 # We already handled library paths above 278 continue 279 elif lib.startswith('-l:'): 280 # see: https://stackoverflow.com/questions/48532868/gcc-library-option-with-a-colon-llibevent-a 281 # also : See the documentation of -lnamespec | --library=namespec in the linker manual 282 # https://sourceware.org/binutils/docs-2.18/ld/Options.html 283 284 # Don't resolve the same -l:libfoo.a argument again 285 if lib in libs_found: 286 continue 287 libfilename = lib[3:] 288 foundname = None 289 for libdir in libpaths: 290 target = os.path.join(libdir, libfilename) 291 if os.path.exists(target): 292 foundname = target 293 break 294 if foundname is None: 295 if lib in libs_notfound: 296 continue 297 else: 298 mlog.warning('Library {!r} not found for dependency {!r}, may ' 299 'not be successfully linked'.format(libfilename, self.name)) 300 libs_notfound.append(lib) 301 else: 302 lib = foundname 303 elif lib.startswith('-l'): 304 # Don't resolve the same -lfoo argument again 305 if lib in libs_found: 306 continue 307 if self.clib_compiler: 308 args = self.clib_compiler.find_library(lib[2:], self.env, 309 libpaths, self.libtype) 310 # If the project only uses a non-clib language such as D, Rust, 311 # C#, Python, etc, all we can do is limp along by adding the 312 # arguments as-is and then adding the libpaths at the end. 313 else: 314 args = None 315 if args is not None: 316 libs_found.add(lib) 317 # Replace -l arg with full path to library if available 318 # else, library is either to be ignored, or is provided by 319 # the compiler, can't be resolved, and should be used as-is 320 if args: 321 if not args[0].startswith('-l'): 322 lib = args[0] 323 else: 324 continue 325 else: 326 # Library wasn't found, maybe we're looking in the wrong 327 # places or the library will be provided with LDFLAGS or 328 # LIBRARY_PATH from the environment (on macOS), and many 329 # other edge cases that we can't account for. 330 # 331 # Add all -L paths and use it as -lfoo 332 if lib in libs_notfound: 333 continue 334 if self.static: 335 mlog.warning('Static library {!r} not found for dependency {!r}, may ' 336 'not be statically linked'.format(lib[2:], self.name)) 337 libs_notfound.append(lib) 338 elif lib.endswith(".la"): 339 shared_libname = self.extract_libtool_shlib(lib) 340 shared_lib = os.path.join(os.path.dirname(lib), shared_libname) 341 if not os.path.exists(shared_lib): 342 shared_lib = os.path.join(os.path.dirname(lib), ".libs", shared_libname) 343 344 if not os.path.exists(shared_lib): 345 raise DependencyException('Got a libtools specific "%s" dependencies' 346 'but we could not compute the actual shared' 347 'library path' % lib) 348 self.is_libtool = True 349 lib = shared_lib 350 if lib in link_args: 351 continue 352 link_args.append(lib) 353 # Add all -Lbar args if we have -lfoo args in link_args 354 if libs_notfound: 355 # Order of -L flags doesn't matter with ld, but it might with other 356 # linkers such as MSVC, so prepend them. 357 link_args = ['-L' + lp for lp in prefix_libpaths] + link_args 358 return link_args, raw_link_args 359 360 def _set_libs(self) -> None: 361 env = None 362 libcmd = ['--libs'] 363 364 if self.static: 365 libcmd.append('--static') 366 367 libcmd.append(self.name) 368 369 # Force pkg-config to output -L fields even if they are system 370 # paths so we can do manual searching with cc.find_library() later. 371 env = os.environ.copy() 372 env['PKG_CONFIG_ALLOW_SYSTEM_LIBS'] = '1' 373 ret, out, err = self._call_pkgbin(libcmd, env=env) 374 if ret != 0: 375 raise DependencyException('Could not generate libs for %s:\n%s\n' % 376 (self.name, err)) 377 # Also get the 'raw' output without -Lfoo system paths for adding -L 378 # args with -lfoo when a library can't be found, and also in 379 # gnome.generate_gir + gnome.gtkdoc which need -L -l arguments. 380 ret, out_raw, err_raw = self._call_pkgbin(libcmd) 381 if ret != 0: 382 raise DependencyException('Could not generate libs for %s:\n\n%s' % 383 (self.name, out_raw)) 384 self.link_args, self.raw_link_args = self._search_libs(out, out_raw) 385 386 def get_pkgconfig_variable(self, variable_name: str, kwargs: T.Dict[str, T.Union[str, T.List[str]]]) -> str: 387 options = ['--variable=' + variable_name, self.name] 388 389 if 'define_variable' in kwargs: 390 definition = kwargs.get('define_variable', []) 391 if not isinstance(definition, list): 392 raise DependencyException('define_variable takes a list') 393 394 if len(definition) != 2 or not all(isinstance(i, str) for i in definition): 395 raise DependencyException('define_variable must be made up of 2 strings for VARIABLENAME and VARIABLEVALUE') 396 397 options = ['--define-variable=' + '='.join(definition)] + options 398 399 ret, out, err = self._call_pkgbin(options) 400 variable = '' 401 if ret != 0: 402 if self.required: 403 raise DependencyException('dependency %s not found:\n%s\n' % 404 (self.name, err)) 405 else: 406 variable = out.strip() 407 408 # pkg-config doesn't distinguish between empty and non-existent variables 409 # use the variable list to check for variable existence 410 if not variable: 411 ret, out, _ = self._call_pkgbin(['--print-variables', self.name]) 412 if not re.search(r'^' + variable_name + r'$', out, re.MULTILINE): 413 if 'default' in kwargs: 414 assert isinstance(kwargs['default'], str) 415 variable = kwargs['default'] 416 else: 417 mlog.warning(f"pkgconfig variable '{variable_name}' not defined for dependency {self.name}.") 418 419 mlog.debug(f'Got pkgconfig variable {variable_name} : {variable}') 420 return variable 421 422 def check_pkgconfig(self, pkgbin: ExternalProgram) -> T.Optional[str]: 423 if not pkgbin.found(): 424 mlog.log(f'Did not find pkg-config by name {pkgbin.name!r}') 425 return None 426 command_as_string = ' '.join(pkgbin.get_command()) 427 try: 428 helptext = Popen_safe(pkgbin.get_command() + ['--help'])[1] 429 if 'Pure-Perl' in helptext: 430 mlog.log(f'found pkg-config {command_as_string!r} but it is Strawberry Perl and thus broken. Ignoring...') 431 return None 432 p, out = Popen_safe(pkgbin.get_command() + ['--version'])[0:2] 433 if p.returncode != 0: 434 mlog.warning(f'Found pkg-config {command_as_string!r} but it failed when run') 435 return None 436 except FileNotFoundError: 437 mlog.warning(f'We thought we found pkg-config {command_as_string!r} but now it\'s not there. How odd!') 438 return None 439 except PermissionError: 440 msg = f'Found pkg-config {command_as_string!r} but didn\'t have permissions to run it.' 441 if not self.env.machines.build.is_windows(): 442 msg += '\n\nOn Unix-like systems this is often caused by scripts that are not executable.' 443 mlog.warning(msg) 444 return None 445 return out.strip() 446 447 def extract_field(self, la_file: str, fieldname: str) -> T.Optional[str]: 448 with open(la_file, encoding='utf-8') as f: 449 for line in f: 450 arr = line.strip().split('=') 451 if arr[0] == fieldname: 452 return arr[1][1:-1] 453 return None 454 455 def extract_dlname_field(self, la_file: str) -> T.Optional[str]: 456 return self.extract_field(la_file, 'dlname') 457 458 def extract_libdir_field(self, la_file: str) -> T.Optional[str]: 459 return self.extract_field(la_file, 'libdir') 460 461 def extract_libtool_shlib(self, la_file: str) -> T.Optional[str]: 462 ''' 463 Returns the path to the shared library 464 corresponding to this .la file 465 ''' 466 dlname = self.extract_dlname_field(la_file) 467 if dlname is None: 468 return None 469 470 # Darwin uses absolute paths where possible; since the libtool files never 471 # contain absolute paths, use the libdir field 472 if self.env.machines[self.for_machine].is_darwin(): 473 dlbasename = os.path.basename(dlname) 474 libdir = self.extract_libdir_field(la_file) 475 if libdir is None: 476 return dlbasename 477 return os.path.join(libdir, dlbasename) 478 # From the comments in extract_libtool(), older libtools had 479 # a path rather than the raw dlname 480 return os.path.basename(dlname) 481 482 def log_tried(self) -> str: 483 return self.type_name 484 485 def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None, 486 configtool: T.Optional[str] = None, internal: T.Optional[str] = None, 487 default_value: T.Optional[str] = None, 488 pkgconfig_define: T.Optional[T.List[str]] = None) -> T.Union[str, T.List[str]]: 489 if pkgconfig: 490 kwargs: T.Dict[str, T.Union[str, T.List[str]]] = {} 491 if default_value is not None: 492 kwargs['default'] = default_value 493 if pkgconfig_define is not None: 494 kwargs['define_variable'] = pkgconfig_define 495 try: 496 return self.get_pkgconfig_variable(pkgconfig, kwargs) 497 except DependencyException: 498 pass 499 if default_value is not None: 500 return default_value 501 raise DependencyException(f'Could not get pkg-config variable and no default provided for {self!r}') 502