|
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
# Copyright 2013-2021 The Meson development team
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
from .base import ExternalDependency, DependencyException, sort_libpaths, DependencyTypeName
|
|
|
|
from ..mesonlib import EnvironmentVariables, OptionKey, OrderedSet, PerMachine, Popen_safe, Popen_safe_logged, MachineChoice, join_args
|
|
|
|
from ..programs import find_external_program, ExternalProgram
|
|
|
|
from .. import mlog
|
|
|
|
from pathlib import PurePath
|
|
|
|
from functools import lru_cache
|
|
|
|
import re
|
|
|
|
import os
|
|
|
|
import shlex
|
|
|
|
import typing as T
|
|
|
|
|
|
|
|
if T.TYPE_CHECKING:
|
|
|
|
from typing_extensions import Literal
|
|
|
|
from .._typing import ImmutableListProtocol
|
|
|
|
|
|
|
|
from ..environment import Environment
|
|
|
|
from ..utils.core import EnvironOrDict
|
|
|
|
from ..interpreter.type_checking import PkgConfigDefineType
|
|
|
|
|
|
|
|
class PkgConfigInterface:
|
|
|
|
'''Base class wrapping a pkg-config implementation'''
|
|
|
|
|
|
|
|
class_impl: PerMachine[T.Union[Literal[False], T.Optional[PkgConfigInterface]]] = PerMachine(False, False)
|
|
|
|
class_cli_impl: PerMachine[T.Union[Literal[False], T.Optional[PkgConfigCLI]]] = PerMachine(False, False)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def instance(env: Environment, for_machine: MachineChoice, silent: bool) -> T.Optional[PkgConfigInterface]:
|
|
|
|
'''Return a pkg-config implementation singleton'''
|
|
|
|
for_machine = for_machine if env.is_cross_build() else MachineChoice.HOST
|
|
|
|
impl = PkgConfigInterface.class_impl[for_machine]
|
|
|
|
if impl is False:
|
|
|
|
impl = PkgConfigCLI(env, for_machine, silent)
|
|
|
|
if not impl.found():
|
|
|
|
impl = None
|
|
|
|
if not impl and not silent:
|
|
|
|
mlog.log('Found pkg-config:', mlog.red('NO'))
|
|
|
|
PkgConfigInterface.class_impl[for_machine] = impl
|
|
|
|
return impl
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _cli(env: Environment, for_machine: MachineChoice, silent: bool = False) -> T.Optional[PkgConfigCLI]:
|
|
|
|
'''Return the CLI pkg-config implementation singleton
|
|
|
|
Even when we use another implementation internally, external tools might
|
|
|
|
still need the CLI implementation.
|
|
|
|
'''
|
|
|
|
for_machine = for_machine if env.is_cross_build() else MachineChoice.HOST
|
|
|
|
impl: T.Union[Literal[False], T.Optional[PkgConfigInterface]] # Help confused mypy
|
|
|
|
impl = PkgConfigInterface.instance(env, for_machine, silent)
|
|
|
|
if impl and not isinstance(impl, PkgConfigCLI):
|
|
|
|
impl = PkgConfigInterface.class_cli_impl[for_machine]
|
|
|
|
if impl is False:
|
|
|
|
impl = PkgConfigCLI(env, for_machine, silent)
|
|
|
|
if not impl.found():
|
|
|
|
impl = None
|
|
|
|
PkgConfigInterface.class_cli_impl[for_machine] = impl
|
|
|
|
return T.cast('T.Optional[PkgConfigCLI]', impl) # Trust me, mypy
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def get_env(env: Environment, for_machine: MachineChoice, uninstalled: bool = False) -> EnvironmentVariables:
|
|
|
|
cli = PkgConfigInterface._cli(env, for_machine)
|
|
|
|
return cli._get_env(uninstalled) if cli else EnvironmentVariables()
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def setup_env(environ: EnvironOrDict, env: Environment, for_machine: MachineChoice,
|
|
|
|
uninstalled: bool = False) -> EnvironOrDict:
|
|
|
|
cli = PkgConfigInterface._cli(env, for_machine)
|
|
|
|
return cli._setup_env(environ, uninstalled) if cli else environ
|
|
|
|
|
|
|
|
def __init__(self, env: Environment, for_machine: MachineChoice) -> None:
|
|
|
|
self.env = env
|
|
|
|
self.for_machine = for_machine
|
|
|
|
|
|
|
|
def found(self) -> bool:
|
|
|
|
'''Return whether pkg-config is supported'''
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
def version(self, name: str) -> T.Optional[str]:
|
|
|
|
'''Return module version or None if not found'''
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
def cflags(self, name: str, allow_system: bool = False,
|
|
|
|
define_variable: PkgConfigDefineType = None) -> ImmutableListProtocol[str]:
|
|
|
|
'''Return module cflags
|
|
|
|
@allow_system: If False, remove default system include paths
|
|
|
|
'''
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
def libs(self, name: str, static: bool = False, allow_system: bool = False,
|
|
|
|
define_variable: PkgConfigDefineType = None) -> ImmutableListProtocol[str]:
|
|
|
|
'''Return module libs
|
|
|
|
@static: If True, also include private libraries
|
|
|
|
@allow_system: If False, remove default system libraries search paths
|
|
|
|
'''
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
def variable(self, name: str, variable_name: str,
|
|
|
|
define_variable: PkgConfigDefineType) -> T.Optional[str]:
|
|
|
|
'''Return module variable or None if variable is not defined'''
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
def list_all(self) -> ImmutableListProtocol[str]:
|
|
|
|
'''Return all available pkg-config modules'''
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
class PkgConfigCLI(PkgConfigInterface):
|
|
|
|
'''pkg-config CLI implementation'''
|
|
|
|
|
|
|
|
def __init__(self, env: Environment, for_machine: MachineChoice, silent: bool) -> None:
|
|
|
|
super().__init__(env, for_machine)
|
|
|
|
self._detect_pkgbin()
|
|
|
|
if self.pkgbin and not silent:
|
|
|
|
mlog.log('Found pkg-config:', mlog.green('YES'), mlog.bold(f'({self.pkgbin.get_path()})'), mlog.blue(self.pkgbin_version))
|
|
|
|
|
|
|
|
def found(self) -> bool:
|
|
|
|
return bool(self.pkgbin)
|
|
|
|
|
|
|
|
@lru_cache(maxsize=None)
|
|
|
|
def version(self, name: str) -> T.Optional[str]:
|
|
|
|
mlog.debug(f'Determining dependency {name!r} with pkg-config executable {self.pkgbin.get_path()!r}')
|
|
|
|
ret, version, _ = self._call_pkgbin(['--modversion', name])
|
|
|
|
return version if ret == 0 else None
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _define_variable_args(define_variable: PkgConfigDefineType) -> T.List[str]:
|
dependencies: allow get_variable to define multiple pkgconfig defines
It was previously impossible to do this:
```
dep.get_pkgconfig_variable(
'foo',
define_variable: ['prefix', '/usr', 'datadir', '/usr/share'],
)
```
since get_pkgconfig_variable mandated exactly two (if any) arguments.
However, you could do this:
```
dep.get_variable(
'foo',
pkgconfig_define: ['prefix', '/usr', 'datadir', '/usr/share'],
)
```
It would silently do the wrong thing, by defining "prefix" as
`/usr=datadir=/usr/share`, which might not "matter" if only datadir was
used in the "foo" variable as the unmodified value might be adequate.
The actual intention of anyone writing such a meson.build is that they
aren't sure whether the .pc file uses ${prefix} or ${datadir} (or which
one gets used, might have changed between versions of that .pc file,
even).
A recent refactor made this into a hard error, which broke some projects
that were doing this and inadvertently depending on some .pc file that
only used the second variable. (This was "fine" since the result was
essentially meaningful, and even resulted in behavior identical to the
intended behavior if both projects were installed into the same prefix
-- in which case there's nothing to remap.)
Re-allow this. There are two ways we could re-allow this:
- ignore it with a warning
- add a new feature to allow actually doing this
Since the use case which triggered this bug actually has a pretty good
reason to want to do this, it makes sense to add the new feature.
Fixes https://bugs.gentoo.org/916576
Fixes https://github.com/containers/bubblewrap/issues/609
1 year ago
|
|
|
ret = []
|
|
|
|
if define_variable:
|
dependencies: allow get_variable to define multiple pkgconfig defines
It was previously impossible to do this:
```
dep.get_pkgconfig_variable(
'foo',
define_variable: ['prefix', '/usr', 'datadir', '/usr/share'],
)
```
since get_pkgconfig_variable mandated exactly two (if any) arguments.
However, you could do this:
```
dep.get_variable(
'foo',
pkgconfig_define: ['prefix', '/usr', 'datadir', '/usr/share'],
)
```
It would silently do the wrong thing, by defining "prefix" as
`/usr=datadir=/usr/share`, which might not "matter" if only datadir was
used in the "foo" variable as the unmodified value might be adequate.
The actual intention of anyone writing such a meson.build is that they
aren't sure whether the .pc file uses ${prefix} or ${datadir} (or which
one gets used, might have changed between versions of that .pc file,
even).
A recent refactor made this into a hard error, which broke some projects
that were doing this and inadvertently depending on some .pc file that
only used the second variable. (This was "fine" since the result was
essentially meaningful, and even resulted in behavior identical to the
intended behavior if both projects were installed into the same prefix
-- in which case there's nothing to remap.)
Re-allow this. There are two ways we could re-allow this:
- ignore it with a warning
- add a new feature to allow actually doing this
Since the use case which triggered this bug actually has a pretty good
reason to want to do this, it makes sense to add the new feature.
Fixes https://bugs.gentoo.org/916576
Fixes https://github.com/containers/bubblewrap/issues/609
1 year ago
|
|
|
for pair in define_variable:
|
|
|
|
ret.append('--define-variable=' + '='.join(pair))
|
|
|
|
return ret
|
|
|
|
|
|
|
|
@lru_cache(maxsize=None)
|
|
|
|
def cflags(self, name: str, allow_system: bool = False,
|
|
|
|
define_variable: PkgConfigDefineType = None) -> ImmutableListProtocol[str]:
|
|
|
|
env = None
|
|
|
|
if allow_system:
|
|
|
|
env = os.environ.copy()
|
|
|
|
env['PKG_CONFIG_ALLOW_SYSTEM_CFLAGS'] = '1'
|
|
|
|
args: T.List[str] = []
|
|
|
|
args += self._define_variable_args(define_variable)
|
|
|
|
args += ['--cflags', name]
|
|
|
|
ret, out, err = self._call_pkgbin(args, env=env)
|
|
|
|
if ret != 0:
|
|
|
|
raise DependencyException(f'Could not generate cflags for {name}:\n{err}\n')
|
|
|
|
return self._split_args(out)
|
|
|
|
|
|
|
|
@lru_cache(maxsize=None)
|
|
|
|
def libs(self, name: str, static: bool = False, allow_system: bool = False,
|
|
|
|
define_variable: PkgConfigDefineType = None) -> ImmutableListProtocol[str]:
|
|
|
|
env = None
|
|
|
|
if allow_system:
|
|
|
|
env = os.environ.copy()
|
|
|
|
env['PKG_CONFIG_ALLOW_SYSTEM_LIBS'] = '1'
|
|
|
|
args: T.List[str] = []
|
|
|
|
args += self._define_variable_args(define_variable)
|
|
|
|
if static:
|
|
|
|
args.append('--static')
|
|
|
|
args += ['--libs', name]
|
|
|
|
ret, out, err = self._call_pkgbin(args, env=env)
|
|
|
|
if ret != 0:
|
|
|
|
raise DependencyException(f'Could not generate libs for {name}:\n{err}\n')
|
|
|
|
return self._split_args(out)
|
|
|
|
|
|
|
|
@lru_cache(maxsize=None)
|
|
|
|
def variable(self, name: str, variable_name: str,
|
|
|
|
define_variable: PkgConfigDefineType) -> T.Optional[str]:
|
|
|
|
args: T.List[str] = []
|
|
|
|
args += self._define_variable_args(define_variable)
|
|
|
|
args += ['--variable=' + variable_name, name]
|
|
|
|
ret, out, err = self._call_pkgbin(args)
|
|
|
|
if ret != 0:
|
|
|
|
raise DependencyException(f'Could not get variable for {name}:\n{err}\n')
|
|
|
|
variable = out.strip()
|
|
|
|
# pkg-config doesn't distinguish between empty and nonexistent variables
|
|
|
|
# use the variable list to check for variable existence
|
|
|
|
if not variable:
|
|
|
|
ret, out, _ = self._call_pkgbin(['--print-variables', name])
|
|
|
|
if not re.search(rf'^{variable_name}$', out, re.MULTILINE):
|
|
|
|
return None
|
|
|
|
mlog.debug(f'Got pkg-config variable {variable_name} : {variable}')
|
|
|
|
return variable
|
|
|
|
|
|
|
|
@lru_cache(maxsize=None)
|
|
|
|
def list_all(self) -> ImmutableListProtocol[str]:
|
|
|
|
ret, out, err = self._call_pkgbin(['--list-all'])
|
|
|
|
if ret != 0:
|
|
|
|
raise DependencyException(f'could not list modules:\n{err}\n')
|
|
|
|
return [i.split(' ', 1)[0] for i in out.splitlines()]
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _split_args(cmd: str) -> T.List[str]:
|
|
|
|
# pkg-config paths follow Unix conventions, even on Windows; split the
|
|
|
|
# output using shlex.split rather than mesonlib.split_args
|
|
|
|
return shlex.split(cmd)
|
|
|
|
|
|
|
|
def _detect_pkgbin(self) -> None:
|
|
|
|
for potential_pkgbin in find_external_program(
|
|
|
|
self.env, self.for_machine, 'pkg-config', 'Pkg-config',
|
|
|
|
self.env.default_pkgconfig, allow_default_for_cross=False):
|
|
|
|
version_if_ok = self._check_pkgconfig(potential_pkgbin)
|
|
|
|
if version_if_ok:
|
|
|
|
self.pkgbin = potential_pkgbin
|
|
|
|
self.pkgbin_version = version_if_ok
|
|
|
|
return
|
|
|
|
self.pkgbin = None
|
|
|
|
|
|
|
|
def _check_pkgconfig(self, pkgbin: ExternalProgram) -> T.Optional[str]:
|
|
|
|
if not pkgbin.found():
|
|
|
|
mlog.log(f'Did not find pkg-config by name {pkgbin.name!r}')
|
|
|
|
return None
|
|
|
|
command_as_string = ' '.join(pkgbin.get_command())
|
|
|
|
try:
|
|
|
|
helptext = Popen_safe(pkgbin.get_command() + ['--help'])[1]
|
|
|
|
if 'Pure-Perl' in helptext:
|
|
|
|
mlog.log(f'Found pkg-config {command_as_string!r} but it is Strawberry Perl and thus broken. Ignoring...')
|
|
|
|
return None
|
|
|
|
p, out = Popen_safe(pkgbin.get_command() + ['--version'])[0:2]
|
|
|
|
if p.returncode != 0:
|
|
|
|
mlog.warning(f'Found pkg-config {command_as_string!r} but it failed when ran')
|
|
|
|
return None
|
|
|
|
except FileNotFoundError:
|
|
|
|
mlog.warning(f'We thought we found pkg-config {command_as_string!r} but now it\'s not there. How odd!')
|
|
|
|
return None
|
|
|
|
except PermissionError:
|
|
|
|
msg = f'Found pkg-config {command_as_string!r} but didn\'t have permissions to run it.'
|
|
|
|
if not self.env.machines.build.is_windows():
|
|
|
|
msg += '\n\nOn Unix-like systems this is often caused by scripts that are not executable.'
|
|
|
|
mlog.warning(msg)
|
|
|
|
return None
|
|
|
|
return out.strip()
|
|
|
|
|
|
|
|
def _get_env(self, uninstalled: bool = False) -> EnvironmentVariables:
|
|
|
|
env = EnvironmentVariables()
|
|
|
|
key = OptionKey('pkg_config_path', machine=self.for_machine)
|
|
|
|
extra_paths: T.List[str] = self.env.coredata.options[key].value[:]
|
|
|
|
if uninstalled:
|
|
|
|
uninstalled_path = Path(self.env.get_build_dir(), 'meson-uninstalled').as_posix()
|
|
|
|
if uninstalled_path not in extra_paths:
|
|
|
|
extra_paths.append(uninstalled_path)
|
|
|
|
env.set('PKG_CONFIG_PATH', extra_paths)
|
|
|
|
sysroot = self.env.properties[self.for_machine].get_sys_root()
|
|
|
|
if sysroot:
|
|
|
|
env.set('PKG_CONFIG_SYSROOT_DIR', [sysroot])
|
|
|
|
pkg_config_libdir_prop = self.env.properties[self.for_machine].get_pkg_config_libdir()
|
|
|
|
if pkg_config_libdir_prop:
|
|
|
|
env.set('PKG_CONFIG_LIBDIR', pkg_config_libdir_prop)
|
|
|
|
env.set('PKG_CONFIG', [join_args(self.pkgbin.get_command())])
|
|
|
|
return env
|
|
|
|
|
|
|
|
def _setup_env(self, env: EnvironOrDict, uninstalled: bool = False) -> T.Dict[str, str]:
|
|
|
|
envvars = self._get_env(uninstalled)
|
|
|
|
env = envvars.get_env(env)
|
|
|
|
# Dump all PKG_CONFIG environment variables
|
|
|
|
for key, value in env.items():
|
|
|
|
if key.startswith('PKG_'):
|
|
|
|
mlog.debug(f'env[{key}]: {value}')
|
|
|
|
return env
|
|
|
|
|
|
|
|
def _call_pkgbin(self, args: T.List[str], env: T.Optional[EnvironOrDict] = None) -> T.Tuple[int, str, str]:
|
|
|
|
assert isinstance(self.pkgbin, ExternalProgram)
|
|
|
|
env = env or os.environ
|
|
|
|
env = self._setup_env(env)
|
|
|
|
cmd = self.pkgbin.get_command() + args
|
|
|
|
p, out, err = Popen_safe_logged(cmd, env=env)
|
|
|
|
return p.returncode, out.strip(), err.strip()
|
|
|
|
|
|
|
|
|
|
|
|
class PkgConfigDependency(ExternalDependency):
|
|
|
|
|
|
|
|
def __init__(self, name: str, environment: Environment, kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None) -> None:
|
|
|
|
super().__init__(DependencyTypeName('pkgconfig'), environment, kwargs, language=language)
|
|
|
|
self.name = name
|
|
|
|
self.is_libtool = False
|
|
|
|
pkgconfig = PkgConfigInterface.instance(self.env, self.for_machine, self.silent)
|
|
|
|
if not pkgconfig:
|
|
|
|
msg = f'Pkg-config for machine {self.for_machine} not found. Giving up.'
|
|
|
|
if self.required:
|
|
|
|
raise DependencyException(msg)
|
|
|
|
mlog.debug(msg)
|
|
|
|
return
|
|
|
|
self.pkgconfig = pkgconfig
|
|
|
|
|
|
|
|
version = self.pkgconfig.version(name)
|
|
|
|
if version is None:
|
|
|
|
return
|
|
|
|
|
|
|
|
self.version = version
|
|
|
|
self.is_found = True
|
|
|
|
|
|
|
|
try:
|
|
|
|
# Fetch cargs to be used while using this dependency
|
|
|
|
self._set_cargs()
|
|
|
|
# Fetch the libraries and library paths needed for using this
|
|
|
|
self._set_libs()
|
|
|
|
except DependencyException as e:
|
|
|
|
mlog.debug(f"Pkg-config error with '{name}': {e}")
|
|
|
|
if self.required:
|
|
|
|
raise
|
|
|
|
else:
|
|
|
|
self.compile_args = []
|
|
|
|
self.link_args = []
|
|
|
|
self.is_found = False
|
|
|
|
self.reason = e
|
|
|
|
|
|
|
|
def __repr__(self) -> str:
|
|
|
|
s = '<{0} {1}: {2} {3}>'
|
|
|
|
return s.format(self.__class__.__name__, self.name, self.is_found,
|
|
|
|
self.version_reqs)
|
|
|
|
|
|
|
|
def _convert_mingw_paths(self, args: ImmutableListProtocol[str]) -> T.List[str]:
|
|
|
|
'''
|
|
|
|
Both MSVC and native Python on Windows cannot handle MinGW-esque /c/foo
|
|
|
|
paths so convert them to C:/foo. We cannot resolve other paths starting
|
|
|
|
with / like /home/foo so leave them as-is so that the user gets an
|
|
|
|
error/warning from the compiler/linker.
|
|
|
|
'''
|
|
|
|
if not self.env.machines.build.is_windows():
|
|
|
|
return args.copy()
|
|
|
|
converted = []
|
|
|
|
for arg in args:
|
|
|
|
pargs: T.Tuple[str, ...] = tuple()
|
|
|
|
# Library search path
|
|
|
|
if arg.startswith('-L/'):
|
|
|
|
pargs = PurePath(arg[2:]).parts
|
|
|
|
tmpl = '-L{}:/{}'
|
|
|
|
elif arg.startswith('-I/'):
|
|
|
|
pargs = PurePath(arg[2:]).parts
|
|
|
|
tmpl = '-I{}:/{}'
|
|
|
|
# Full path to library or .la file
|
|
|
|
elif arg.startswith('/'):
|
|
|
|
pargs = PurePath(arg).parts
|
|
|
|
tmpl = '{}:/{}'
|
|
|
|
elif arg.startswith(('-L', '-I')) or (len(arg) > 2 and arg[1] == ':'):
|
|
|
|
# clean out improper '\\ ' as comes from some Windows pkg-config files
|
|
|
|
arg = arg.replace('\\ ', ' ')
|
|
|
|
if len(pargs) > 1 and len(pargs[1]) == 1:
|
|
|
|
arg = tmpl.format(pargs[1], '/'.join(pargs[2:]))
|
|
|
|
converted.append(arg)
|
|
|
|
return converted
|
|
|
|
|
|
|
|
def _set_cargs(self) -> None:
|
|
|
|
allow_system = False
|
|
|
|
if self.language == 'fortran':
|
|
|
|
# gfortran doesn't appear to look in system paths for INCLUDE files,
|
|
|
|
# so don't allow pkg-config to suppress -I flags for system paths
|
|
|
|
allow_system = True
|
|
|
|
cflags = self.pkgconfig.cflags(self.name, allow_system)
|
|
|
|
self.compile_args = self._convert_mingw_paths(cflags)
|
|
|
|
|
|
|
|
def _search_libs(self, libs_in: ImmutableListProtocol[str], raw_libs_in: ImmutableListProtocol[str]) -> T.Tuple[T.List[str], T.List[str]]:
|
|
|
|
'''
|
|
|
|
@libs_in: PKG_CONFIG_ALLOW_SYSTEM_LIBS=1 pkg-config --libs
|
|
|
|
@raw_libs_in: pkg-config --libs
|
|
|
|
|
|
|
|
We always look for the file ourselves instead of depending on the
|
|
|
|
compiler to find it with -lfoo or foo.lib (if possible) because:
|
|
|
|
1. We want to be able to select static or shared
|
|
|
|
2. We need the full path of the library to calculate RPATH values
|
|
|
|
3. De-dup of libraries is easier when we have absolute paths
|
|
|
|
|
|
|
|
Libraries that are provided by the toolchain or are not found by
|
|
|
|
find_library() will be added with -L -l pairs.
|
|
|
|
'''
|
|
|
|
# Library paths should be safe to de-dup
|
|
|
|
#
|
|
|
|
# First, figure out what library paths to use. Originally, we were
|
|
|
|
# doing this as part of the loop, but due to differences in the order
|
|
|
|
# of -L values between pkg-config and pkgconf, we need to do that as
|
|
|
|
# a separate step. See:
|
|
|
|
# https://github.com/mesonbuild/meson/issues/3951
|
|
|
|
# https://github.com/mesonbuild/meson/issues/4023
|
|
|
|
#
|
|
|
|
# Separate system and prefix paths, and ensure that prefix paths are
|
|
|
|
# always searched first.
|
|
|
|
prefix_libpaths: OrderedSet[str] = OrderedSet()
|
|
|
|
# We also store this raw_link_args on the object later
|
|
|
|
raw_link_args = self._convert_mingw_paths(raw_libs_in)
|
|
|
|
for arg in raw_link_args:
|
|
|
|
if arg.startswith('-L') and not arg.startswith(('-L-l', '-L-L')):
|
|
|
|
path = arg[2:]
|
|
|
|
if not os.path.isabs(path):
|
|
|
|
# Resolve the path as a compiler in the build directory would
|
|
|
|
path = os.path.join(self.env.get_build_dir(), path)
|
|
|
|
prefix_libpaths.add(path)
|
|
|
|
# Library paths are not always ordered in a meaningful way
|
|
|
|
#
|
|
|
|
# Instead of relying on pkg-config or pkgconf to provide -L flags in a
|
|
|
|
# specific order, we reorder library paths ourselves, according to th
|
|
|
|
# order specified in PKG_CONFIG_PATH. See:
|
|
|
|
# https://github.com/mesonbuild/meson/issues/4271
|
|
|
|
#
|
|
|
|
# Only prefix_libpaths are reordered here because there should not be
|
|
|
|
# too many system_libpaths to cause library version issues.
|
|
|
|
pkg_config_path: T.List[str] = self.env.coredata.options[OptionKey('pkg_config_path', machine=self.for_machine)].value
|
|
|
|
pkg_config_path = self._convert_mingw_paths(pkg_config_path)
|
|
|
|
prefix_libpaths = OrderedSet(sort_libpaths(list(prefix_libpaths), pkg_config_path))
|
|
|
|
system_libpaths: OrderedSet[str] = OrderedSet()
|
|
|
|
full_args = self._convert_mingw_paths(libs_in)
|
|
|
|
for arg in full_args:
|
|
|
|
if arg.startswith(('-L-l', '-L-L')):
|
|
|
|
# These are D language arguments, not library paths
|
|
|
|
continue
|
|
|
|
if arg.startswith('-L') and arg[2:] not in prefix_libpaths:
|
|
|
|
system_libpaths.add(arg[2:])
|
|
|
|
# Use this re-ordered path list for library resolution
|
|
|
|
libpaths = list(prefix_libpaths) + list(system_libpaths)
|
|
|
|
# Track -lfoo libraries to avoid duplicate work
|
|
|
|
libs_found: OrderedSet[str] = OrderedSet()
|
|
|
|
# Track not-found libraries to know whether to add library paths
|
|
|
|
libs_notfound = []
|
|
|
|
# Generate link arguments for this library
|
|
|
|
link_args = []
|
|
|
|
for lib in full_args:
|
|
|
|
if lib.startswith(('-L-l', '-L-L')):
|
|
|
|
# These are D language arguments, add them as-is
|
|
|
|
pass
|
|
|
|
elif lib.startswith('-L'):
|
|
|
|
# We already handled library paths above
|
|
|
|
continue
|
|
|
|
elif lib.startswith('-l:'):
|
|
|
|
# see: https://stackoverflow.com/questions/48532868/gcc-library-option-with-a-colon-llibevent-a
|
|
|
|
# also : See the documentation of -lnamespec | --library=namespec in the linker manual
|
|
|
|
# https://sourceware.org/binutils/docs-2.18/ld/Options.html
|
|
|
|
|
|
|
|
# Don't resolve the same -l:libfoo.a argument again
|
|
|
|
if lib in libs_found:
|
|
|
|
continue
|
|
|
|
libfilename = lib[3:]
|
|
|
|
foundname = None
|
|
|
|
for libdir in libpaths:
|
|
|
|
target = os.path.join(libdir, libfilename)
|
|
|
|
if os.path.exists(target):
|
|
|
|
foundname = target
|
|
|
|
break
|
|
|
|
if foundname is None:
|
|
|
|
if lib in libs_notfound:
|
|
|
|
continue
|
|
|
|
else:
|
|
|
|
mlog.warning('Library {!r} not found for dependency {!r}, may '
|
|
|
|
'not be successfully linked'.format(libfilename, self.name))
|
|
|
|
libs_notfound.append(lib)
|
|
|
|
else:
|
|
|
|
lib = foundname
|
|
|
|
elif lib.startswith('-l'):
|
|
|
|
# Don't resolve the same -lfoo argument again
|
|
|
|
if lib in libs_found:
|
|
|
|
continue
|
|
|
|
if self.clib_compiler:
|
|
|
|
args = self.clib_compiler.find_library(lib[2:], self.env,
|
|
|
|
libpaths, self.libtype,
|
|
|
|
lib_prefix_warning=False)
|
|
|
|
# If the project only uses a non-clib language such as D, Rust,
|
|
|
|
# C#, Python, etc, all we can do is limp along by adding the
|
|
|
|
# arguments as-is and then adding the libpaths at the end.
|
|
|
|
else:
|
|
|
|
args = None
|
|
|
|
if args is not None:
|
|
|
|
libs_found.add(lib)
|
|
|
|
# Replace -l arg with full path to library if available
|
|
|
|
# else, library is either to be ignored, or is provided by
|
|
|
|
# the compiler, can't be resolved, and should be used as-is
|
|
|
|
if args:
|
|
|
|
if not args[0].startswith('-l'):
|
|
|
|
lib = args[0]
|
|
|
|
else:
|
|
|
|
continue
|
|
|
|
else:
|
|
|
|
# Library wasn't found, maybe we're looking in the wrong
|
|
|
|
# places or the library will be provided with LDFLAGS or
|
|
|
|
# LIBRARY_PATH from the environment (on macOS), and many
|
|
|
|
# other edge cases that we can't account for.
|
|
|
|
#
|
|
|
|
# Add all -L paths and use it as -lfoo
|
|
|
|
if lib in libs_notfound:
|
|
|
|
continue
|
|
|
|
if self.static:
|
|
|
|
mlog.warning('Static library {!r} not found for dependency {!r}, may '
|
|
|
|
'not be statically linked'.format(lib[2:], self.name))
|
|
|
|
libs_notfound.append(lib)
|
|
|
|
elif lib.endswith(".la"):
|
|
|
|
shared_libname = self.extract_libtool_shlib(lib)
|
|
|
|
shared_lib = os.path.join(os.path.dirname(lib), shared_libname)
|
|
|
|
if not os.path.exists(shared_lib):
|
|
|
|
shared_lib = os.path.join(os.path.dirname(lib), ".libs", shared_libname)
|
|
|
|
|
|
|
|
if not os.path.exists(shared_lib):
|
|
|
|
raise DependencyException(f'Got a libtools specific "{lib}" dependencies'
|
|
|
|
'but we could not compute the actual shared'
|
|
|
|
'library path')
|
|
|
|
self.is_libtool = True
|
|
|
|
lib = shared_lib
|
|
|
|
if lib in link_args:
|
|
|
|
continue
|
|
|
|
link_args.append(lib)
|
|
|
|
# Add all -Lbar args if we have -lfoo args in link_args
|
|
|
|
if libs_notfound:
|
|
|
|
# Order of -L flags doesn't matter with ld, but it might with other
|
|
|
|
# linkers such as MSVC, so prepend them.
|
|
|
|
link_args = ['-L' + lp for lp in prefix_libpaths] + link_args
|
|
|
|
return link_args, raw_link_args
|
|
|
|
|
|
|
|
def _set_libs(self) -> None:
|
|
|
|
# Force pkg-config to output -L fields even if they are system
|
|
|
|
# paths so we can do manual searching with cc.find_library() later.
|
|
|
|
libs = self.pkgconfig.libs(self.name, self.static, allow_system=True)
|
|
|
|
# Also get the 'raw' output without -Lfoo system paths for adding -L
|
|
|
|
# args with -lfoo when a library can't be found, and also in
|
|
|
|
# gnome.generate_gir + gnome.gtkdoc which need -L -l arguments.
|
|
|
|
raw_libs = self.pkgconfig.libs(self.name, self.static, allow_system=False)
|
|
|
|
self.link_args, self.raw_link_args = self._search_libs(libs, raw_libs)
|
|
|
|
|
|
|
|
def extract_field(self, la_file: str, fieldname: str) -> T.Optional[str]:
|
|
|
|
with open(la_file, encoding='utf-8') as f:
|
|
|
|
for line in f:
|
|
|
|
arr = line.strip().split('=')
|
|
|
|
if arr[0] == fieldname:
|
|
|
|
return arr[1][1:-1]
|
|
|
|
return None
|
|
|
|
|
|
|
|
def extract_dlname_field(self, la_file: str) -> T.Optional[str]:
|
|
|
|
return self.extract_field(la_file, 'dlname')
|
|
|
|
|
|
|
|
def extract_libdir_field(self, la_file: str) -> T.Optional[str]:
|
|
|
|
return self.extract_field(la_file, 'libdir')
|
|
|
|
|
|
|
|
def extract_libtool_shlib(self, la_file: str) -> T.Optional[str]:
|
|
|
|
'''
|
|
|
|
Returns the path to the shared library
|
|
|
|
corresponding to this .la file
|
|
|
|
'''
|
|
|
|
dlname = self.extract_dlname_field(la_file)
|
|
|
|
if dlname is None:
|
|
|
|
return None
|
|
|
|
|
|
|
|
# Darwin uses absolute paths where possible; since the libtool files never
|
|
|
|
# contain absolute paths, use the libdir field
|
|
|
|
if self.env.machines[self.for_machine].is_darwin():
|
|
|
|
dlbasename = os.path.basename(dlname)
|
|
|
|
libdir = self.extract_libdir_field(la_file)
|
|
|
|
if libdir is None:
|
|
|
|
return dlbasename
|
|
|
|
return os.path.join(libdir, dlbasename)
|
|
|
|
# From the comments in extract_libtool(), older libtools had
|
|
|
|
# a path rather than the raw dlname
|
|
|
|
return os.path.basename(dlname)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def log_tried() -> str:
|
|
|
|
return 'pkgconfig'
|
|
|
|
|
|
|
|
def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None,
|
|
|
|
configtool: T.Optional[str] = None, internal: T.Optional[str] = None,
|
|
|
|
default_value: T.Optional[str] = None,
|
|
|
|
pkgconfig_define: PkgConfigDefineType = None) -> str:
|
|
|
|
if pkgconfig:
|
|
|
|
try:
|
|
|
|
variable = self.pkgconfig.variable(self.name, pkgconfig, pkgconfig_define)
|
|
|
|
if variable is not None:
|
|
|
|
return variable
|
|
|
|
except DependencyException:
|
|
|
|
pass
|
|
|
|
if default_value is not None:
|
|
|
|
return default_value
|
|
|
|
raise DependencyException(f'Could not get pkg-config variable and no default provided for {self!r}')
|