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.

354 lines
18 KiB

from .interpreterobjects import SubprojectHolder, extract_required_kwarg
from .. import mlog
from .. import dependencies
from .. import build
from ..wrap import WrapMode
from ..mesonlib import OptionKey, extract_as_list, stringlistify, version_compare_many
from ..dependencies import Dependency, DependencyException, NotFoundDependency
from ..interpreterbase import (MesonInterpreterObject, FeatureNew,
InterpreterException, InvalidArguments,
TYPE_nkwargs, TYPE_nvar)
import typing as T
if T.TYPE_CHECKING:
from .interpreter import Interpreter
class DependencyFallbacksHolder(MesonInterpreterObject):
def __init__(self, interpreter: 'Interpreter', names: T.List[str], allow_fallback: T.Optional[bool] = None) -> None:
super().__init__(subproject=interpreter.subproject)
self.interpreter = interpreter
self.subproject = interpreter.subproject
self.coredata = interpreter.coredata
self.build = interpreter.build
self.environment = interpreter.environment
self.wrap_resolver = interpreter.environment.wrap_resolver
self.allow_fallback = allow_fallback
self.subproject_name = None
self.subproject_varname = None
self.subproject_kwargs = None
self.names: T.List[str] = []
for name in names:
if not name:
raise InterpreterException('dependency_fallbacks empty name \'\' is not allowed')
if '<' in name or '>' in name or '=' in name:
raise InvalidArguments('Characters <, > and = are forbidden in dependency names. To specify'
'version\n requirements use the \'version\' keyword argument instead.')
if name in self.names:
raise InterpreterException('dependency_fallbacks name {name!r} is duplicated')
self.names.append(name)
def set_fallback(self, fbinfo: T.Optional[T.Union[T.List[str], str]], default_options: T.Optional[T.List[str]] = None) -> None:
# Legacy: This converts dependency()'s fallback and default_options kwargs.
if fbinfo is None:
if default_options is not None:
mlog.warning('The "default_options" keyword argument does nothing without a fallback subproject.',
location=self.interpreter.current_node)
return
fbinfo = stringlistify(fbinfo)
if len(fbinfo) == 0:
# dependency('foo', fallback: []) is the same as dependency('foo', allow_fallback: false)
self.allow_fallback = False
return
if len(fbinfo) == 1:
FeatureNew.single_use('Fallback without variable name', '0.53.0', self.subproject)
subp_name, varname = fbinfo[0], None
elif len(fbinfo) == 2:
subp_name, varname = fbinfo
else:
raise InterpreterException('Fallback info must have one or two items.')
kwargs = {'default_options': default_options or []}
self._subproject_impl(subp_name, varname, kwargs)
def _subproject_impl(self, subp_name: str, varname: str, kwargs: TYPE_nkwargs) -> None:
if not varname:
# If no variable name is specified, check if the wrap file has one.
# If the wrap file has a variable name, better use it because the
# subproject most probably is not using meson.override_dependency().
for name in self.names:
varname = self.wrap_resolver.get_varname(subp_name, name)
if varname:
break
assert self.subproject_name is None
self.subproject_name = subp_name
self.subproject_varname = varname
self.subproject_kwargs = kwargs
def _do_dependency_cache(self, kwargs: TYPE_nkwargs, func_args: TYPE_nvar, func_kwargs: TYPE_nkwargs) -> T.Optional[Dependency]:
name = func_args[0]
cached_dep = self._get_cached_dep(name, kwargs)
if cached_dep:
self._verify_fallback_consistency(cached_dep)
return cached_dep
def _do_dependency(self, kwargs: TYPE_nkwargs, func_args: TYPE_nvar, func_kwargs: TYPE_nkwargs) -> T.Optional[Dependency]:
# Note that there is no df.dependency() method, this is called for names
# given as positional arguments to dependency_fallbacks(name1, ...).
# We use kwargs from the dependency() function, for things like version,
# module, etc.
name = func_args[0]
self._handle_featurenew_dependencies(name)
dep = dependencies.find_external_dependency(name, self.environment, kwargs)
if dep.found():
for_machine = self.interpreter.machine_from_native_kwarg(kwargs)
identifier = dependencies.get_dep_identifier(name, kwargs)
self.coredata.deps[for_machine].put(identifier, dep)
return dep
return None
def _do_existing_subproject(self, kwargs: TYPE_nkwargs, func_args: TYPE_nvar, func_kwargs: TYPE_nkwargs) -> T.Optional[Dependency]:
subp_name = func_args[0]
varname = self.subproject_varname
if subp_name and self._get_subproject(subp_name):
return self._get_subproject_dep(subp_name, varname, kwargs)
return None
def _do_subproject(self, kwargs: TYPE_nkwargs, func_args: TYPE_nvar, func_kwargs: TYPE_nkwargs) -> T.Optional[Dependency]:
if self.nofallback:
mlog.log('Not looking for a fallback subproject for the dependency',
mlog.bold(self.display_name), 'because:\nUse of fallback dependencies is disabled.')
return None
if self.forcefallback:
mlog.log('Looking for a fallback subproject for the dependency',
mlog.bold(self.display_name), 'because:\nUse of fallback dependencies is forced.')
else:
mlog.log('Looking for a fallback subproject for the dependency',
mlog.bold(self.display_name))
# Configure the subproject
subp_name = self.subproject_name
varname = self.subproject_varname
self.interpreter.do_subproject(subp_name, 'meson', func_kwargs)
return self._get_subproject_dep(subp_name, varname, kwargs)
def _get_subproject(self, subp_name: str) -> T.Optional[SubprojectHolder]:
sub = self.interpreter.subprojects.get(subp_name)
if sub and sub.found():
return sub
return None
def _get_subproject_dep(self, subp_name: str, varname: str, kwargs: TYPE_nkwargs) -> T.Optional[Dependency]:
# Verify the subproject is found
subproject = self._get_subproject(subp_name)
if not subproject:
mlog.log('Dependency', mlog.bold(self.display_name), 'from subproject',
mlog.bold(subp_name), 'found:', mlog.red('NO'),
mlog.blue('(subproject failed to configure)'))
return None
# The subproject has been configured. If for any reason the dependency
# cannot be found in this subproject we have to return not-found object
# instead of None, because we don't want to continue the lookup on the
# system.
# Check if the subproject overridden at least one of the names we got.
cached_dep = None
for name in self.names:
cached_dep = self._get_cached_dep(name, kwargs)
if cached_dep:
break
# If we have cached_dep we did all the checks and logging already in
# self._get_cached_dep().
if cached_dep:
self._verify_fallback_consistency(cached_dep)
return cached_dep
# Legacy: Use the variable name if provided instead of relying on the
# subproject to override one of our dependency names
if not varname:
mlog.warning(f'Subproject {subp_name!r} did not override {self.display_name!r} dependency and no variable name specified')
mlog.log('Dependency', mlog.bold(self.display_name), 'from subproject',
mlog.bold(subproject.subdir), 'found:', mlog.red('NO'))
return self._notfound_dependency()
var_dep = self._get_subproject_variable(subproject, varname) or self._notfound_dependency()
if not var_dep.found():
mlog.log('Dependency', mlog.bold(self.display_name), 'from subproject',
mlog.bold(subproject.subdir), 'found:', mlog.red('NO'))
return var_dep
wanted = stringlistify(kwargs.get('version', []))
found = var_dep.get_version()
if not self._check_version(wanted, found):
mlog.log('Dependency', mlog.bold(self.display_name), 'from subproject',
mlog.bold(subproject.subdir), 'found:', mlog.red('NO'),
'found', mlog.normal_cyan(found), 'but need:',
mlog.bold(', '.join([f"'{e}'" for e in wanted])))
return self._notfound_dependency()
mlog.log('Dependency', mlog.bold(self.display_name), 'from subproject',
mlog.bold(subproject.subdir), 'found:', mlog.green('YES'),
mlog.normal_cyan(found) if found else None)
return var_dep
def _get_cached_dep(self, name: str, kwargs: TYPE_nkwargs) -> T.Optional[Dependency]:
# Unlike other methods, this one returns not-found dependency instead
# of None in the case the dependency is cached as not-found, or if cached
# version does not match. In that case we don't want to continue with
# other candidates.
for_machine = self.interpreter.machine_from_native_kwarg(kwargs)
identifier = dependencies.get_dep_identifier(name, kwargs)
wanted_vers = stringlistify(kwargs.get('version', []))
override = self.build.dependency_overrides[for_machine].get(identifier)
if override:
info = [mlog.blue('(overridden)' if override.explicit else '(cached)')]
cached_dep = override.dep
# We don't implicitly override not-found dependencies, but user could
# have explicitly called meson.override_dependency() with a not-found
# dep.
if not cached_dep.found():
mlog.log('Dependency', mlog.bold(self.display_name),
'found:', mlog.red('NO'), *info)
return cached_dep
else:
info = [mlog.blue('(cached)')]
cached_dep = self.coredata.deps[for_machine].get(identifier)
if cached_dep:
found_vers = cached_dep.get_version()
if not self._check_version(wanted_vers, found_vers):
mlog.log('Dependency', mlog.bold(name),
'found:', mlog.red('NO'),
'found', mlog.normal_cyan(found_vers), 'but need:',
mlog.bold(', '.join([f"'{e}'" for e in wanted_vers])),
*info)
return self._notfound_dependency()
if found_vers:
info = [mlog.normal_cyan(found_vers), *info]
mlog.log('Dependency', mlog.bold(self.display_name),
'found:', mlog.green('YES'), *info)
return cached_dep
return None
def _get_subproject_variable(self, subproject: SubprojectHolder, varname: str) -> T.Optional[Dependency]:
try:
var_dep = subproject.get_variable_method([varname], {})
except InvalidArguments:
var_dep = None
if not isinstance(var_dep, Dependency):
mlog.warning(f'Variable {varname!r} in the subproject {subproject.subdir!r} is',
'not found' if var_dep is None else 'not a dependency object')
return None
return var_dep
def _verify_fallback_consistency(self, cached_dep: Dependency):
subp_name = self.subproject_name
varname = self.subproject_varname
subproject = self._get_subproject(subp_name)
if subproject and varname:
var_dep = self._get_subproject_variable(subproject, varname)
if var_dep and cached_dep.found() and var_dep != cached_dep:
mlog.warning(f'Inconsistency: Subproject has overridden the dependency with another variable than {varname!r}')
def _handle_featurenew_dependencies(self, name: str) -> None:
'Do a feature check on dependencies used by this subproject'
if name == 'mpi':
FeatureNew.single_use('MPI Dependency', '0.42.0', self.subproject)
elif name == 'pcap':
FeatureNew.single_use('Pcap Dependency', '0.42.0', self.subproject)
elif name == 'vulkan':
FeatureNew.single_use('Vulkan Dependency', '0.42.0', self.subproject)
elif name == 'libwmf':
FeatureNew.single_use('LibWMF Dependency', '0.44.0', self.subproject)
elif name == 'openmp':
FeatureNew.single_use('OpenMP Dependency', '0.46.0', self.subproject)
def _notfound_dependency(self) -> NotFoundDependency:
return NotFoundDependency(self.environment)
@staticmethod
def _check_version(wanted: T.Optional[str], found: str) -> bool:
if not wanted:
return True
if found == 'undefined' or not version_compare_many(found, wanted)[0]:
return False
return True
def _get_candidates(self) -> T.List[T.Tuple[T.Callable[[TYPE_nkwargs, TYPE_nvar, TYPE_nkwargs], T.Optional[Dependency]], TYPE_nvar, TYPE_nkwargs]]:
candidates = []
# 1. check if any of the names is cached already.
for name in self.names:
candidates.append((self._do_dependency_cache, [name], {}))
# 2. check if the subproject fallback has already been configured.
if self.subproject_name:
candidates.append((self._do_existing_subproject, [self.subproject_name], self.subproject_kwargs))
# 3. check external dependency if we are not forced to use subproject
if not self.forcefallback or not self.subproject_name:
for name in self.names:
candidates.append((self._do_dependency, [name], {}))
# 4. configure the subproject
if self.subproject_name:
candidates.append((self._do_subproject, [self.subproject_name], self.subproject_kwargs))
return candidates
def lookup(self, kwargs: TYPE_nkwargs, force_fallback: bool = False) -> Dependency:
self.display_name = self.names[0] if self.names else '(anonymous)'
mods = extract_as_list(kwargs, 'modules')
if mods:
self.display_name += ' (modules: {})'.format(', '.join(str(i) for i in mods))
disabled, required, feature = extract_required_kwarg(kwargs, self.subproject)
if disabled:
mlog.log('Dependency', mlog.bold(self.display_name), 'skipped: feature', mlog.bold(feature), 'disabled')
return self._notfound_dependency()
# Check if usage of the subproject fallback is forced
wrap_mode = self.coredata.get_option(OptionKey('wrap_mode'))
force_fallback_for = self.coredata.get_option(OptionKey('force_fallback_for'))
self.nofallback = wrap_mode == WrapMode.nofallback
self.forcefallback = (force_fallback or
wrap_mode == WrapMode.forcefallback or
any(name in force_fallback_for for name in self.names) or
self.subproject_name in force_fallback_for)
# Add an implicit subproject fallback if none has been set explicitly,
# unless implicit fallback is not allowed.
# Legacy: self.allow_fallback can be None when that kwarg is not defined
# in dependency('name'). In that case we don't want to use implicit
# fallback when required is false because user will typically fallback
# manually using cc.find_library() for example.
if not self.subproject_name and self.allow_fallback is not False:
for name in self.names:
subp_name, varname = self.wrap_resolver.find_dep_provider(name)
if subp_name:
self.forcefallback |= subp_name in force_fallback_for
if self.forcefallback or self.allow_fallback is True or required or self._get_subproject(subp_name):
self._subproject_impl(subp_name, varname, {})
break
candidates = self._get_candidates()
# writing just "dependency('')" is an error, because it can only fail
if not candidates and required:
raise InvalidArguments('Dependency is required but has no candidates.')
# Try all candidates, only the last one is really required.
last = len(candidates) - 1
for i, item in enumerate(candidates):
func, func_args, func_kwargs = item
func_kwargs['required'] = required and (i == last)
kwargs['required'] = required and (i == last)
dep = func(kwargs, func_args, func_kwargs)
if dep and dep.found():
# Override this dependency to have consistent results in subsequent
# dependency lookups.
for name in self.names:
for_machine = self.interpreter.machine_from_native_kwarg(kwargs)
identifier = dependencies.get_dep_identifier(name, kwargs)
if identifier not in self.build.dependency_overrides[for_machine]:
self.build.dependency_overrides[for_machine][identifier] = \
build.DependencyOverride(dep, self.interpreter.current_node, explicit=False)
return dep
elif required and (dep or i == last):
# This was the last candidate or the dependency has been cached
# as not-found, or cached dependency version does not match,
# otherwise func() would have returned None instead.
raise DependencyException(f'Dependency {self.display_name!r} is required but not found.')
elif dep:
# Same as above, but the dependency is not required.
return dep
return self._notfound_dependency()