The Meson Build System http://mesonbuild.com/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

570 lines
26 KiB

# 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
self.pkgconfig = PkgConfigInterface.instance(self.env, self.for_machine, self.silent)
if not self.pkgconfig:
msg = f'Pkg-config for machine {self.for_machine} not found. Giving up.'
if self.required:
raise DependencyException(msg)
mlog.debug(msg)
return
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}')