interpreter: Extract dependency() logic into its own helper class

The dependency lookup is a lot of complex code. This refactor it all
into a single file/class outside of interpreter main class. This new
design allows adding more fallbacks candidates in the future (e.g. using
cc.find_library()) but does not yet add any extra API.
pull/8892/head
Xavier Claessens 4 years ago committed by Xavier Claessens
parent 3970f269fd
commit b6d754a40c
  1. 346
      mesonbuild/interpreter/dependencyfallbacks.py
  2. 318
      mesonbuild/interpreter/interpreter.py
  3. 14
      mesonbuild/wrap/wrap.py
  4. 2
      run_unittests.py
  5. 13
      test cases/common/88 dep fallback/meson.build
  6. 2
      test cases/common/88 dep fallback/subprojects/boblib/meson.build
  7. 7
      test cases/failing/100 fallback consistency/test.json
  8. 3
      test cases/failing/106 fallback consistency/meson.build
  9. 7
      test cases/failing/106 fallback consistency/test.json
  10. 2
      test cases/failing/36 pkgconfig dependency impossible conditions/test.json
  11. 2
      test cases/failing/66 dependency not-found and required/test.json
  12. 2
      test cases/failing/67 subproj different versions/test.json
  13. 2
      test cases/failing/84 gtest dependency with version/test.json
  14. 2
      test cases/failing/88 subproj not-found dep/test.json
  15. 7
      test cases/failing/98 fallback consistency/test.json
  16. 0
      test cases/warning/3 fallback consistency/meson.build
  17. 0
      test cases/warning/3 fallback consistency/subprojects/sub/meson.build
  18. 7
      test cases/warning/3 fallback consistency/test.json
  19. 2
      test cases/warning/4 fallback consistency/meson.build
  20. 0
      test cases/warning/4 fallback consistency/subprojects/sub/meson.build
  21. 7
      test cases/warning/4 fallback consistency/test.json
  22. 4
      test cases/warning/5 fallback consistency/meson.build
  23. 0
      test cases/warning/5 fallback consistency/subprojects/foo.wrap
  24. 0
      test cases/warning/5 fallback consistency/subprojects/foo/meson.build
  25. 7
      test cases/warning/5 fallback consistency/test.json

@ -0,0 +1,346 @@
from .interpreterobjects import DependencyHolder, 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 DependencyException, NotFoundDependency
from ..interpreterbase import (InterpreterObject, FeatureNew,
InterpreterException, InvalidArguments,
TYPE_nkwargs, TYPE_nvar)
import typing as T
if T.TYPE_CHECKING:
from .interpreter import Interpreter
class DependencyFallbacksHolder(InterpreterObject):
def __init__(self, interpreter: 'Interpreter', names: T.List[str], allow_fallback: T.Optional[bool] = None) -> None:
super().__init__()
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) == 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[DependencyHolder]:
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[DependencyHolder]:
# 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 DependencyHolder(dep, self.subproject)
return None
def _do_existing_subproject(self, kwargs: TYPE_nkwargs, func_args: TYPE_nvar, func_kwargs: TYPE_nkwargs) -> T.Optional[DependencyHolder]:
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[DependencyHolder]:
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[DependencyHolder]:
# 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.held_object.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[DependencyHolder]:
# 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 DependencyHolder(cached_dep, self.subproject)
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 DependencyHolder(cached_dep, self.subproject)
return None
def _get_subproject_variable(self, subproject: SubprojectHolder, varname: str) -> T.Optional[DependencyHolder]:
var_dep = subproject.held_object.variables.get(varname)
if not isinstance(var_dep, DependencyHolder):
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: DependencyHolder):
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.held_object != cached_dep.held_object:
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) -> DependencyHolder:
return DependencyHolder(NotFoundDependency(self.environment), self.subproject)
@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[DependencyHolder]], 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) -> DependencyHolder:
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.held_object.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.held_object, 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()

@ -23,7 +23,7 @@ from ..wrap import wrap, WrapMode
from .. import mesonlib from .. import mesonlib
from ..mesonlib import FileMode, MachineChoice, OptionKey, listify, extract_as_list, has_path_sep, unholder from ..mesonlib import FileMode, MachineChoice, OptionKey, listify, extract_as_list, has_path_sep, unholder
from ..programs import ExternalProgram, NonExistingExternalProgram from ..programs import ExternalProgram, NonExistingExternalProgram
from ..dependencies import Dependency, NotFoundDependency, DependencyException from ..dependencies import Dependency
from ..depfile import DepFile from ..depfile import DepFile
from ..interpreterbase import ContainerTypeInfo, InterpreterBase, KwargInfo, typed_kwargs, typed_pos_args from ..interpreterbase import ContainerTypeInfo, InterpreterBase, KwargInfo, typed_kwargs, typed_pos_args
from ..interpreterbase import noPosargs, noKwargs, stringArgs, permittedKwargs, noArgsFlattening from ..interpreterbase import noPosargs, noKwargs, stringArgs, permittedKwargs, noArgsFlattening
@ -47,6 +47,7 @@ from .interpreterobjects import (SubprojectHolder, MachineHolder, EnvironmentVar
BuildTargetHolder, DataHolder, JarHolder, Test, RunProcess, BuildTargetHolder, DataHolder, JarHolder, Test, RunProcess,
ManHolder, GeneratorHolder, InstallDirHolder, extract_required_kwarg, ManHolder, GeneratorHolder, InstallDirHolder, extract_required_kwarg,
extract_search_dirs, MutableModuleObjectHolder) extract_search_dirs, MutableModuleObjectHolder)
from .dependencyfallbacks import DependencyFallbacksHolder
from pathlib import Path from pathlib import Path
import os import os
@ -501,11 +502,12 @@ class Interpreter(InterpreterBase):
continue continue
if len(di) == 1: if len(di) == 1:
FeatureNew.single_use('stdlib without variable name', '0.56.0', self.subproject) FeatureNew.single_use('stdlib without variable name', '0.56.0', self.subproject)
kwargs = {'fallback': di, kwargs = {'native': for_machine is MachineChoice.BUILD,
'native': for_machine is MachineChoice.BUILD,
} }
name = display_name = l + '_stdlib' name = l + '_stdlib'
dep = self.dependency_impl(name, display_name, kwargs, force_fallback=True) df = DependencyFallbacksHolder(self, [name])
df.set_fallback(di)
dep = df.lookup(kwargs, force_fallback=True)
self.build.stdlibs[for_machine][l] = dep self.build.stdlibs[for_machine][l] = dep
def import_module(self, modname): def import_module(self, modname):
@ -734,12 +736,6 @@ external dependencies (including libraries) must go to "dependencies".''')
self.coredata.initialized_subprojects.add(subp_name) self.coredata.initialized_subprojects.add(subp_name)
return sub return sub
def get_subproject(self, subp_name):
sub = self.subprojects.get(subp_name)
if sub and sub.found():
return sub
return None
def do_subproject(self, subp_name: str, method: str, kwargs): def do_subproject(self, subp_name: str, method: str, kwargs):
disabled, required, feature = extract_required_kwarg(kwargs, self.subproject) disabled, required, feature = extract_required_kwarg(kwargs, self.subproject)
if disabled: if disabled:
@ -1430,155 +1426,6 @@ external dependencies (including libraries) must go to "dependencies".''')
'Look here for example: http://mesonbuild.com/howtox.html#add-math-library-lm-portably\n' 'Look here for example: http://mesonbuild.com/howtox.html#add-math-library-lm-portably\n'
) )
def _find_cached_dep(self, name, display_name, kwargs):
# Check if we want this as a build-time / build machine or runt-time /
# host machine dep.
for_machine = self.machine_from_native_kwarg(kwargs)
identifier = dependencies.get_dep_identifier(name, kwargs)
wanted_vers = mesonlib.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(display_name),
'found:', mlog.red('NO'), *info)
return identifier, 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 identifier, NotFoundDependency(self.environment)
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):
return identifier, None
if cached_dep:
if found_vers:
info = [mlog.normal_cyan(found_vers), *info]
mlog.log('Dependency', mlog.bold(display_name),
'found:', mlog.green('YES'), *info)
return identifier, cached_dep
return identifier, None
@staticmethod
def check_version(wanted, found):
if not wanted:
return True
if found == 'undefined' or not mesonlib.version_compare_many(found, wanted)[0]:
return False
return True
def notfound_dependency(self):
return DependencyHolder(NotFoundDependency(self.environment), self.subproject)
def verify_fallback_consistency(self, subp_name, varname, cached_dep):
subi = self.get_subproject(subp_name)
if not cached_dep or not varname or not subi or not cached_dep.found():
return
dep = subi.get_variable_method([varname], {})
if dep.held_object != cached_dep:
m = 'Inconsistency: Subproject has overridden the dependency with another variable than {!r}'
raise DependencyException(m.format(varname))
def get_subproject_dep(self, name, display_name, subp_name, varname, kwargs):
required = kwargs.get('required', True)
wanted = mesonlib.stringlistify(kwargs.get('version', []))
dep = self.notfound_dependency()
# Verify the subproject is found
subproject = self.subprojects.get(subp_name)
if not subproject or not subproject.found():
mlog.log('Dependency', mlog.bold(display_name), 'from subproject',
mlog.bold(subproject.subdir), 'found:', mlog.red('NO'),
mlog.blue('(subproject failed to configure)'))
if required:
m = 'Subproject {} failed to configure for dependency {}'
raise DependencyException(m.format(subproject.subdir, display_name))
return dep
extra_info = []
try:
# Check if the subproject overridden the dependency
_, cached_dep = self._find_cached_dep(name, display_name, kwargs)
if cached_dep:
if varname:
self.verify_fallback_consistency(subp_name, varname, cached_dep)
if required and not cached_dep.found():
m = 'Dependency {!r} is not satisfied'
raise DependencyException(m.format(display_name))
return DependencyHolder(cached_dep, self.subproject)
elif varname is None:
mlog.log('Dependency', mlog.bold(display_name), 'from subproject',
mlog.bold(subproject.subdir), 'found:', mlog.red('NO'))
if required:
m = 'Subproject {} did not override dependency {}'
raise DependencyException(m.format(subproject.subdir, display_name))
return self.notfound_dependency()
else:
# The subproject did not override the dependency, but we know the
# variable name to take.
dep = subproject.get_variable_method([varname], {})
except InvalidArguments:
# This is raised by get_variable_method() if varname does no exist
# in the subproject. Just add the reason in the not-found message
# that will be printed later.
extra_info.append(mlog.blue(f'(Variable {varname!r} not found)'))
if not isinstance(dep, DependencyHolder):
raise InvalidCode('Fetched variable {!r} in the subproject {!r} is '
'not a dependency object.'.format(varname, subp_name))
if not dep.found():
mlog.log('Dependency', mlog.bold(display_name), 'from subproject',
mlog.bold(subproject.subdir), 'found:', mlog.red('NO'), *extra_info)
if required:
raise DependencyException('Could not find dependency {} in subproject {}'
''.format(varname, subp_name))
return dep
found = dep.held_object.get_version()
if not self.check_version(wanted, found):
mlog.log('Dependency', mlog.bold(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])))
if required:
raise DependencyException('Version {} of subproject dependency {} already '
'cached, requested incompatible version {} for '
'dep {}'.format(found, subp_name, wanted, display_name))
return self.notfound_dependency()
found = mlog.normal_cyan(found) if found else None
mlog.log('Dependency', mlog.bold(display_name), 'from subproject',
mlog.bold(subproject.subdir), 'found:', mlog.green('YES'), found)
return dep
def _handle_featurenew_dependencies(self, name):
'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)
# When adding kwargs, please check if they make sense in dependencies.get_dep_identifier() # When adding kwargs, please check if they make sense in dependencies.get_dep_identifier()
@FeatureNewKwargs('dependency', '0.57.0', ['cmake_package_version']) @FeatureNewKwargs('dependency', '0.57.0', ['cmake_package_version'])
@FeatureNewKwargs('dependency', '0.56.0', ['allow_fallback']) @FeatureNewKwargs('dependency', '0.56.0', ['allow_fallback'])
@ -1590,18 +1437,22 @@ external dependencies (including libraries) must go to "dependencies".''')
@FeatureNewKwargs('dependency', '0.38.0', ['default_options']) @FeatureNewKwargs('dependency', '0.38.0', ['default_options'])
@disablerIfNotFound @disablerIfNotFound
@permittedKwargs(permitted_dependency_kwargs) @permittedKwargs(permitted_dependency_kwargs)
@typed_pos_args('dependency', str)
def func_dependency(self, node, args, kwargs): def func_dependency(self, node, args, kwargs):
self.validate_arguments(args, 1, [str]) # Replace '' by empty list of names
name = args[0] names = [args[0]] if args[0] else []
display_name = name if name else '(anonymous)' allow_fallback = kwargs.get('allow_fallback')
mods = extract_as_list(kwargs, 'modules') if allow_fallback is not None and not isinstance(allow_fallback, bool):
if mods: raise InvalidArguments('"allow_fallback" argument must be boolean')
display_name += ' (modules: {})'.format(', '.join(str(i) for i in mods)) fallback = kwargs.get('fallback')
default_options = kwargs.get('default_options')
df = DependencyFallbacksHolder(self, names, allow_fallback)
df.set_fallback(fallback, default_options)
not_found_message = kwargs.get('not_found_message', '') not_found_message = kwargs.get('not_found_message', '')
if not isinstance(not_found_message, str): if not isinstance(not_found_message, str):
raise InvalidArguments('The not_found_message must be a string.') raise InvalidArguments('The not_found_message must be a string.')
try: try:
d = self.dependency_impl(name, display_name, kwargs) d = df.lookup(kwargs)
except Exception: except Exception:
if not_found_message: if not_found_message:
self.message_impl([not_found_message]) self.message_impl([not_found_message])
@ -1610,152 +1461,21 @@ external dependencies (including libraries) must go to "dependencies".''')
if not d.found() and not_found_message: if not d.found() and not_found_message:
self.message_impl([not_found_message]) self.message_impl([not_found_message])
self.message_impl([not_found_message]) self.message_impl([not_found_message])
# Override this dependency to have consistent results in subsequent
# dependency lookups.
if name and d.found():
for_machine = self.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(d.held_object, node, explicit=False)
# Ensure the correct include type # Ensure the correct include type
if 'include_type' in kwargs: if 'include_type' in kwargs:
wanted = kwargs['include_type'] wanted = kwargs['include_type']
actual = d.include_type_method([], {}) actual = d.include_type_method([], {})
if wanted != actual: if wanted != actual:
mlog.debug(f'Current include type of {name} is {actual}. Converting to requested {wanted}') mlog.debug(f'Current include type of {names[0]} is {actual}. Converting to requested {wanted}')
d = d.as_system_method([wanted], {}) d = d.as_system_method([wanted], {})
return d return d
def dependency_impl(self, name, display_name, kwargs, force_fallback=False):
disabled, required, feature = extract_required_kwarg(kwargs, self.subproject)
if disabled:
mlog.log('Dependency', mlog.bold(display_name), 'skipped: feature', mlog.bold(feature), 'disabled')
return self.notfound_dependency()
fallback = kwargs.get('fallback', None)
allow_fallback = kwargs.get('allow_fallback', None)
if allow_fallback is not None:
if fallback is not None:
raise InvalidArguments('"fallback" and "allow_fallback" arguments are mutually exclusive')
if not isinstance(allow_fallback, bool):
raise InvalidArguments('"allow_fallback" argument must be boolean')
wrap_mode = self.coredata.get_option(OptionKey('wrap_mode'))
force_fallback_for = self.coredata.get_option(OptionKey('force_fallback_for'))
force_fallback |= (wrap_mode == WrapMode.forcefallback or
name in force_fallback_for)
# If "fallback" is absent, look for an implicit fallback.
if name and fallback is None and allow_fallback is not False:
# Add an implicit fallback if we have a wrap file or a directory with the same name,
# but only if this dependency is required. It is common to first check for a pkg-config,
# then fallback to use find_library() and only afterward check again the dependency
# with a fallback. If the fallback has already been configured then we have to use it
# even if the dependency is not required.
provider = self.environment.wrap_resolver.find_dep_provider(name)
if not provider and allow_fallback is True:
raise InvalidArguments('Fallback wrap or subproject not found for dependency \'%s\'' % name)
subp_name = mesonlib.listify(provider)[0]
force_fallback |= subp_name in force_fallback_for
if provider and (allow_fallback is True or required or self.get_subproject(subp_name) or force_fallback):
fallback = provider
if 'default_options' in kwargs and not fallback:
mlog.warning('The "default_options" keyword argument does nothing without a fallback subproject.',
location=self.current_node)
# writing just "dependency('')" is an error, because it can only fail
if name == '' and required and not fallback:
raise InvalidArguments('Dependency is both required and not-found')
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.')
identifier, cached_dep = self._find_cached_dep(name, display_name, kwargs)
if cached_dep:
if fallback:
subp_name, varname = self.get_subproject_infos(fallback)
self.verify_fallback_consistency(subp_name, varname, cached_dep)
if required and not cached_dep.found():
m = 'Dependency {!r} was already checked and was not found'
raise DependencyException(m.format(display_name))
return DependencyHolder(cached_dep, self.subproject)
if fallback:
# If the dependency has already been configured, possibly by
# a higher level project, try to use it first.
subp_name, varname = self.get_subproject_infos(fallback)
if self.get_subproject(subp_name):
return self.get_subproject_dep(name, display_name, subp_name, varname, kwargs)
force_fallback |= subp_name in force_fallback_for
if name != '' and (not fallback or not force_fallback):
self._handle_featurenew_dependencies(name)
kwargs['required'] = required and not fallback
dep = dependencies.find_external_dependency(name, self.environment, kwargs)
kwargs['required'] = required
# Only store found-deps in the cache
# Never add fallback deps to self.coredata.deps since we
# cannot cache them. They must always be evaluated else
# we won't actually read all the build files.
if dep.found():
for_machine = self.machine_from_native_kwarg(kwargs)
self.coredata.deps[for_machine].put(identifier, dep)
return DependencyHolder(dep, self.subproject)
if fallback:
return self.dependency_fallback(name, display_name, fallback, kwargs)
return self.notfound_dependency()
@FeatureNew('disabler', '0.44.0') @FeatureNew('disabler', '0.44.0')
@noKwargs @noKwargs
@noPosargs @noPosargs
def func_disabler(self, node, args, kwargs): def func_disabler(self, node, args, kwargs):
return Disabler() return Disabler()
def get_subproject_infos(self, fbinfo):
fbinfo = mesonlib.stringlistify(fbinfo)
if len(fbinfo) == 1:
FeatureNew.single_use('Fallback without variable name', '0.53.0', self.subproject)
return fbinfo[0], None
elif len(fbinfo) != 2:
raise InterpreterException('Fallback info must have one or two items.')
return fbinfo
def dependency_fallback(self, name, display_name, fallback, kwargs):
subp_name, varname = self.get_subproject_infos(fallback)
required = kwargs.get('required', True)
# Explicitly listed fallback preferences for specific subprojects
# take precedence over wrap-mode
force_fallback_for = self.coredata.get_option(OptionKey('force_fallback_for'))
if name in force_fallback_for or subp_name in force_fallback_for:
mlog.log('Looking for a fallback subproject for the dependency',
mlog.bold(display_name), 'because:\nUse of fallback was forced for that specific subproject')
elif self.coredata.get_option(OptionKey('wrap_mode')) == WrapMode.nofallback:
mlog.log('Not looking for a fallback subproject for the dependency',
mlog.bold(display_name), 'because:\nUse of fallback '
'dependencies is disabled.')
if required:
m = 'Dependency {!r} not found and fallback is disabled'
raise DependencyException(m.format(display_name))
return self.notfound_dependency()
elif self.coredata.get_option(OptionKey('wrap_mode')) == WrapMode.forcefallback:
mlog.log('Looking for a fallback subproject for the dependency',
mlog.bold(display_name), 'because:\nUse of fallback dependencies is forced.')
else:
mlog.log('Looking for a fallback subproject for the dependency',
mlog.bold(display_name))
sp_kwargs = {
'default_options': kwargs.get('default_options', []),
'required': required,
}
self.do_subproject(subp_name, 'meson', sp_kwargs)
return self.get_subproject_dep(name, display_name, subp_name, varname, kwargs)
@FeatureNewKwargs('executable', '0.42.0', ['implib']) @FeatureNewKwargs('executable', '0.42.0', ['implib'])
@FeatureNewKwargs('executable', '0.56.0', ['win_subsystem']) @FeatureNewKwargs('executable', '0.56.0', ['win_subsystem'])
@FeatureDeprecatedKwargs('executable', '0.56.0', ['gui_app'], extra_message="Use 'win_subsystem' instead.") @FeatureDeprecatedKwargs('executable', '0.56.0', ['gui_app'], extra_message="Use 'win_subsystem' instead.")

@ -248,19 +248,19 @@ class Resolver:
for k, v in other_resolver.provided_programs.items(): for k, v in other_resolver.provided_programs.items():
self.provided_programs.setdefault(k, v) self.provided_programs.setdefault(k, v)
def find_dep_provider(self, packagename: str) -> T.Optional[T.Union[str, T.List[str]]]: def find_dep_provider(self, packagename: str) -> T.Tuple[T.Optional[str], T.Optional[str]]:
# Python's ini parser converts all key values to lowercase. # Python's ini parser converts all key values to lowercase.
# Thus the query name must also be in lower case. # Thus the query name must also be in lower case.
packagename = packagename.lower() packagename = packagename.lower()
# Return value is in the same format as fallback kwarg:
# ['subproject_name', 'variable_name'], or 'subproject_name'.
wrap = self.provided_deps.get(packagename) wrap = self.provided_deps.get(packagename)
if wrap: if wrap:
dep_var = wrap.provided_deps.get(packagename) dep_var = wrap.provided_deps.get(packagename)
if dep_var: return wrap.name, dep_var
return [wrap.name, dep_var] return None, None
return wrap.name
return None def get_varname(self, subp_name: str, depname: str) -> T.Optional[str]:
wrap = self.wraps.get(subp_name)
return wrap.provided_deps.get(depname) if wrap else None
def find_program_provider(self, names: T.List[str]) -> T.Optional[str]: def find_program_provider(self, names: T.List[str]) -> T.Optional[str]:
for name in names: for name in names:

@ -6221,7 +6221,7 @@ class FailureTests(BasePlatformTests):
def test_wrap_nofallback(self): def test_wrap_nofallback(self):
self.assertMesonRaises("dependency('notfound', fallback : ['foo', 'foo_dep'])", self.assertMesonRaises("dependency('notfound', fallback : ['foo', 'foo_dep'])",
r"Dependency \'notfound\' not found and fallback is disabled", r"Dependency 'notfound' is required but not found.",
extra_args=['--wrap-mode=nofallback']) extra_args=['--wrap-mode=nofallback'])
def test_message(self): def test_message(self):

@ -5,8 +5,21 @@ bob = dependency('boblib', fallback : ['boblib', 'bob_dep'], required: false,
if not bob.found() if not bob.found()
error('Bob is actually needed') error('Bob is actually needed')
endif endif
# boblib subproject exists, but bobinc is not a dependency variable
sita = dependency('sitalib', fallback : ['boblib', 'bobinc'], required: false)
assert(not sita.found())
# boblib subproject exists, but sita_dep doesn't exist # boblib subproject exists, but sita_dep doesn't exist
sita = dependency('sitalib', fallback : ['boblib', 'sita_dep'], required: false) sita = dependency('sitalib', fallback : ['boblib', 'sita_dep'], required: false)
assert(not sita.found())
# boblib has been configured so zlib cannot be searched on the system
zlib = dependency('zlib', fallback : ['boblib', 'notfound_dep'], required: false)
assert(not zlib.found())
# boblib has been configured so zlib cannot be searched on the system.
# Not variable name provided and the subproject does not override zlib.
zlib = dependency('zlib', fallback : 'boblib', required: false)
assert(not zlib.found())
# jimmylib subproject doesn't exist # jimmylib subproject doesn't exist
jimmy = dependency('jimmylib', fallback : ['jimmylib', 'jimmy_dep'], required: false) jimmy = dependency('jimmylib', fallback : ['jimmylib', 'jimmy_dep'], required: false)
# dummylib subproject fails to configure # dummylib subproject fails to configure

@ -14,3 +14,5 @@ bobinc = include_directories('.')
bob_dep = declare_dependency(link_with : boblib, bob_dep = declare_dependency(link_with : boblib,
sources : [genbob_h], sources : [genbob_h],
include_directories : bobinc) include_directories : bobinc)
notfound_dep = dependency('', required: false)

@ -1,7 +0,0 @@
{
"stdout": [
{
"line": "test cases/failing/100 fallback consistency/meson.build:7:0: ERROR: Inconsistency: Subproject has overridden the dependency with another variable than 'dep2'"
}
]
}

@ -1,3 +0,0 @@
project('fallback consistency')
dependency('foo')

@ -1,7 +0,0 @@
{
"stdout": [
{
"line": "test cases/failing/106 fallback consistency/meson.build:3:0: ERROR: Inconsistency: Subproject has overridden the dependency with another variable than 'bar_dep'"
}
]
}

@ -1,7 +1,7 @@
{ {
"stdout": [ "stdout": [
{ {
"line": "test cases/failing/36 pkgconfig dependency impossible conditions/meson.build:7:0: ERROR: Dependency 'zlib' was already checked and was not found" "line": "test cases/failing/36 pkgconfig dependency impossible conditions/meson.build:7:0: ERROR: Dependency 'zlib' is required but not found."
} }
] ]
} }

@ -1,7 +1,7 @@
{ {
"stdout": [ "stdout": [
{ {
"line": "test cases/failing/66 dependency not-found and required/meson.build:2:0: ERROR: Dependency is both required and not-found" "line": "test cases/failing/66 dependency not-found and required/meson.build:2:0: ERROR: Dependency is required but has no candidates."
} }
] ]
} }

@ -1,7 +1,7 @@
{ {
"stdout": [ "stdout": [
{ {
"line": "test cases/failing/67 subproj different versions/subprojects/b/meson.build:3:0: ERROR: Dependency 'c' was already checked and was not found" "line": "test cases/failing/67 subproj different versions/subprojects/b/meson.build:3:0: ERROR: Dependency 'c' is required but not found."
} }
] ]
} }

@ -1,7 +1,7 @@
{ {
"stdout": [ "stdout": [
{ {
"line": "test cases/failing/84 gtest dependency with version/meson.build:8:0: ERROR: Dependency 'gtest' was already checked and was not found" "line": "test cases/failing/84 gtest dependency with version/meson.build:8:0: ERROR: Dependency 'gtest' is required but not found."
} }
] ]
} }

@ -1,7 +1,7 @@
{ {
"stdout": [ "stdout": [
{ {
"line": "test cases/failing/88 subproj not-found dep/meson.build:2:0: ERROR: Could not find dependency notfound_dep in subproject somesubproj" "line": "test cases/failing/88 subproj not-found dep/meson.build:2:0: ERROR: Dependency '(anonymous)' is required but not found."
} }
] ]
} }

@ -1,7 +0,0 @@
{
"stdout": [
{
"line": "test cases/failing/98 fallback consistency/meson.build:4:0: ERROR: Inconsistency: Subproject has overridden the dependency with another variable than 'dep2'"
}
]
}

@ -0,0 +1,7 @@
{
"stdout": [
{
"line": "WARNING: Inconsistency: Subproject has overridden the dependency with another variable than 'dep2'"
}
]
}

@ -1,4 +1,4 @@
project('proj', 'c') project('proj', 'c')
# Subproject overrides 'sub' with another variable than dep2. This should fail. # Subproject overrides 'sub' with another variable than dep2. This should warn.
dependency('sub', fallback : ['sub', 'dep2']) dependency('sub', fallback : ['sub', 'dep2'])

@ -0,0 +1,7 @@
{
"stdout": [
{
"line": "WARNING: Inconsistency: Subproject has overridden the dependency with another variable than 'dep2'"
}
]
}

@ -0,0 +1,4 @@
project('fallback consistency')
# Subproject overrides foo with foo_dep but wrap file says it's bar_dep. This should warn.
dependency('foo')

@ -0,0 +1,7 @@
{
"stdout": [
{
"line": "WARNING: Inconsistency: Subproject has overridden the dependency with another variable than 'bar_dep'"
}
]
}
Loading…
Cancel
Save