Merge pull request #10043 from dcbaker/submit/type-checking-for-subproject

Add typing for subproject()
pull/10081/head
Jussi Pakkanen 3 years ago committed by GitHub
commit ade6e3a19e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      mesonbuild/build.py
  2. 2
      mesonbuild/coredata.py
  3. 51
      mesonbuild/interpreter/dependencyfallbacks.py
  4. 152
      mesonbuild/interpreter/interpreter.py
  5. 9
      mesonbuild/interpreter/interpreterobjects.py
  6. 15
      mesonbuild/interpreter/kwargs.py
  7. 2
      mesonbuild/mintro.py
  8. 62
      mesonbuild/modules/cmake.py
  9. 4
      mesonbuild/modules/keyval.py
  10. 2
      test cases/failing/120 subproject version conflict/test.json
  11. 2
      test cases/failing/21 subver/test.json

@ -255,7 +255,7 @@ class Build:
self.test_setups: T.Dict[str, TestSetup] = {}
self.test_setup_default_name = None
self.find_overrides: T.Dict[str, T.Union['Executable', programs.ExternalProgram, programs.OverrideProgram]] = {}
self.searched_programs = set() # The list of all programs that have been searched for.
self.searched_programs: T.Set[str] = set() # The list of all programs that have been searched for.
# If we are doing a cross build we need two caches, if we're doing a
# build == host compilation the both caches should point to the same place.
@ -279,7 +279,7 @@ class Build:
custom_targets[name] = t
return custom_targets
def copy(self):
def copy(self) -> Build:
other = Build(self.environment)
for k, v in self.__dict__.items():
if isinstance(v, (list, dict, set, OrderedDict)):
@ -288,11 +288,11 @@ class Build:
other.__dict__[k] = v
return other
def merge(self, other):
def merge(self, other: Build) -> None:
for k, v in other.__dict__.items():
self.__dict__[k] = v
def ensure_static_linker(self, compiler):
def ensure_static_linker(self, compiler: Compiler) -> None:
if self.static_linker[compiler.for_machine] is None and compiler.needs_static_linker():
self.static_linker[compiler.for_machine] = detect_static_linker(self.environment, compiler)

@ -613,7 +613,7 @@ class CoreData:
'Default project to execute in Visual Studio',
'')
def get_option(self, key: OptionKey) -> T.Union[str, int, bool, WrapMode]:
def get_option(self, key: OptionKey) -> T.Union[T.List[str], str, int, bool, WrapMode]:
try:
v = self.options[key].value
if key.name == 'wrap_mode':

@ -4,7 +4,7 @@ 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 ..mesonlib import OptionKey, extract_as_list, stringlistify, version_compare_many, listify
from ..dependencies import Dependency, DependencyException, NotFoundDependency
from ..interpreterbase import (MesonInterpreterObject, FeatureNew,
InterpreterException, InvalidArguments,
@ -26,10 +26,12 @@ class DependencyFallbacksHolder(MesonInterpreterObject):
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_name: T.Optional[str] = None
self.subproject_varname: T.Optional[str] = None
self.subproject_kwargs = {'default_options': default_options or []}
self.names: T.List[str] = []
self.forcefallback: bool = False
self.nofallback: bool = False
for name in names:
if not name:
raise InterpreterException('dependency_fallbacks empty name \'\' is not allowed')
@ -39,6 +41,7 @@ class DependencyFallbacksHolder(MesonInterpreterObject):
if name in self.names:
raise InterpreterException('dependency_fallbacks name {name!r} is duplicated')
self.names.append(name)
self._display_name = self.names[0] if self.names else '(anonymous)'
def set_fallback(self, fbinfo: T.Optional[T.Union[T.List[str], str]]) -> None:
# Legacy: This converts dependency()'s fallback kwargs.
@ -105,14 +108,14 @@ class DependencyFallbacksHolder(MesonInterpreterObject):
def _do_subproject(self, kwargs: TYPE_nkwargs, func_args: TYPE_nvar, func_kwargs: TYPE_nkwargs) -> T.Optional[Dependency]:
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.')
mlog.bold(self._display_name), 'because:\nUse of fallback dependencies is forced.')
elif 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.')
mlog.bold(self._display_name), 'because:\nUse of fallback dependencies is disabled.')
return None
else:
mlog.log('Looking for a fallback subproject for the dependency',
mlog.bold(self.display_name))
mlog.bold(self._display_name))
# dependency('foo', static: true) should implicitly add
# default_options: ['default_library=static']
@ -128,6 +131,9 @@ class DependencyFallbacksHolder(MesonInterpreterObject):
# Configure the subproject
subp_name = self.subproject_name
varname = self.subproject_varname
func_kwargs.setdefault('version', [])
if 'default_options' in kwargs and isinstance(kwargs['default_options'], str):
func_kwargs['default_options'] = listify(kwargs['default_options'])
self.interpreter.do_subproject(subp_name, 'meson', func_kwargs)
return self._get_subproject_dep(subp_name, varname, kwargs)
@ -141,7 +147,7 @@ class DependencyFallbacksHolder(MesonInterpreterObject):
# 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.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
@ -167,27 +173,27 @@ class DependencyFallbacksHolder(MesonInterpreterObject):
# 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.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.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.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.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
@ -209,7 +215,7 @@ class DependencyFallbacksHolder(MesonInterpreterObject):
# have explicitly called meson.override_dependency() with a not-found
# dep.
if not cached_dep.found():
mlog.log('Dependency', mlog.bold(self.display_name),
mlog.log('Dependency', mlog.bold(self._display_name),
'found:', mlog.red('NO'), *info)
return cached_dep
else:
@ -231,7 +237,7 @@ class DependencyFallbacksHolder(MesonInterpreterObject):
return self._notfound_dependency()
if found_vers:
info = [mlog.normal_cyan(found_vers), *info]
mlog.log('Dependency', mlog.bold(self.display_name),
mlog.log('Dependency', mlog.bold(self._display_name),
'found:', mlog.green('YES'), *info)
return cached_dep
return None
@ -247,7 +253,7 @@ class DependencyFallbacksHolder(MesonInterpreterObject):
return None
return var_dep
def _verify_fallback_consistency(self, cached_dep: Dependency):
def _verify_fallback_consistency(self, cached_dep: Dependency) -> None:
subp_name = self.subproject_name
varname = self.subproject_varname
subproject = self._get_subproject(subp_name)
@ -273,12 +279,10 @@ class DependencyFallbacksHolder(MesonInterpreterObject):
return NotFoundDependency(self.names[0] if self.names else '', self.environment)
@staticmethod
def _check_version(wanted: T.Optional[str], found: str) -> bool:
def _check_version(wanted: T.List[str], found: str) -> bool:
if not wanted:
return True
if found == 'undefined' or not version_compare_many(found, wanted)[0]:
return False
return True
return not (found == 'undefined' or not version_compare_many(found, wanted)[0])
def _get_candidates(self) -> T.List[T.Tuple[T.Callable[[TYPE_nkwargs, TYPE_nvar, TYPE_nkwargs], T.Optional[Dependency]], TYPE_nvar, TYPE_nkwargs]]:
candidates = []
@ -298,19 +302,20 @@ class DependencyFallbacksHolder(MesonInterpreterObject):
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))
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')
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'))
assert isinstance(wrap_mode, WrapMode), 'for mypy'
force_fallback_for = self.coredata.get_option(OptionKey('force_fallback_for'))
assert isinstance(force_fallback_for, list), 'for mypy'
self.nofallback = wrap_mode == WrapMode.nofallback
self.forcefallback = (force_fallback or
wrap_mode == WrapMode.forcefallback or
@ -359,7 +364,7 @@ class DependencyFallbacksHolder(MesonInterpreterObject):
# 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.')
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

@ -33,7 +33,7 @@ from ..interpreterbase import InterpreterException, InvalidArguments, InvalidCod
from ..interpreterbase import Disabler, disablerIfNotFound
from ..interpreterbase import FeatureNew, FeatureDeprecated, FeatureNewKwargs, FeatureDeprecatedKwargs
from ..interpreterbase import ObjectHolder
from ..interpreterbase.baseobjects import TYPE_var, TYPE_kwargs
from ..interpreterbase.baseobjects import InterpreterObject, TYPE_var, TYPE_kwargs
from ..modules import ExtensionModule, ModuleObject, MutableModuleObject, NewExtensionModule, NotFoundExtensionModule
from ..cmake import CMakeInterpreter
from ..backend.backends import Backend, ExecutableSerialisation
@ -93,6 +93,8 @@ import importlib
if T.TYPE_CHECKING:
import argparse
from typing_extensions import Literal
from . import kwargs
from ..programs import OverrideProgram
@ -115,7 +117,7 @@ def _project_version_validator(value: T.Union[T.List, str, mesonlib.File, None])
return None
def stringifyUserArguments(args, quote=False):
def stringifyUserArguments(args: T.List[T.Any], quote: bool = False) -> str:
if isinstance(args, list):
return '[%s]' % ', '.join([stringifyUserArguments(x, True) for x in args])
elif isinstance(args, dict):
@ -258,7 +260,7 @@ class Interpreter(InterpreterBase, HoldableObject):
subproject: str = '',
subdir: str = '',
subproject_dir: str = 'subprojects',
default_project_options: T.Optional[T.Dict[str, str]] = None,
default_project_options: T.Optional[T.Dict[OptionKey, str]] = None,
mock: bool = False,
ast: T.Optional[mparser.CodeBlockNode] = None,
is_translated: bool = False,
@ -271,7 +273,7 @@ class Interpreter(InterpreterBase, HoldableObject):
self.coredata = self.environment.get_coredata()
self.backend = backend
self.summary: T.Dict[str, 'Summary'] = {}
self.modules = {}
self.modules: T.Dict[str, NewExtensionModule] = {}
# Subproject directory is usually the name of the subproject, but can
# be different for dependencies provided by wrap files.
self.subproject_directory_name = subdir.split(os.path.sep)[-1]
@ -289,14 +291,14 @@ class Interpreter(InterpreterBase, HoldableObject):
self.project_args_frozen = False
self.global_args_frozen = False # implies self.project_args_frozen
self.subprojects: T.Dict[str, SubprojectHolder] = {}
self.subproject_stack = []
self.configure_file_outputs = {}
self.subproject_stack: T.List[str] = []
self.configure_file_outputs: T.Dict[str, int] = {}
# Passed from the outside, only used in subprojects.
if default_project_options:
self.default_project_options = default_project_options.copy()
else:
self.default_project_options = {}
self.project_default_options = {}
self.project_default_options: T.Dict[OptionKey, str] = {}
self.build_func_dict()
self.build_holder_map()
self.user_defined_options = user_defined_options
@ -306,10 +308,10 @@ class Interpreter(InterpreterBase, HoldableObject):
# For non-meson subprojects, we'll be using the ast. Even if it does
# exist we don't want to add a dependency on it, it's autogenerated
# from the actual build files, and is just for reference.
self.build_def_files = []
self.build_def_files: mesonlib.OrderedSet[str] = mesonlib.OrderedSet()
build_filename = os.path.join(self.subdir, environment.build_filename)
if not is_translated:
self.build_def_files.append(build_filename)
self.build_def_files.add(build_filename)
if not mock:
self.parse_project()
self._redetect_machines()
@ -317,7 +319,7 @@ class Interpreter(InterpreterBase, HoldableObject):
def __getnewargs_ex__(self) -> T.Tuple[T.Tuple[object], T.Dict[str, object]]:
raise MesonBugException('This class is unpicklable')
def _redetect_machines(self):
def _redetect_machines(self) -> None:
# Re-initialize machine descriptions. We can do a better job now because we
# have the compilers needed to gain more knowledge, so wipe out old
# inference and start over.
@ -335,7 +337,7 @@ class Interpreter(InterpreterBase, HoldableObject):
self.builtin['target_machine'] = \
OBJ.MachineHolder(self.build.environment.machines.target, self)
def build_func_dict(self):
def build_func_dict(self) -> None:
self.funcs.update({'add_global_arguments': self.func_add_global_arguments,
'add_global_link_arguments': self.func_add_global_link_arguments,
'add_languages': self.func_add_languages,
@ -501,7 +503,7 @@ class Interpreter(InterpreterBase, HoldableObject):
else:
raise InterpreterException(f'Module returned a value of unknown type {v!r}.')
def get_build_def_files(self) -> T.List[str]:
def get_build_def_files(self) -> mesonlib.OrderedSet[str]:
return self.build_def_files
def add_build_def_file(self, f: mesonlib.FileOrString) -> None:
@ -517,32 +519,32 @@ class Interpreter(InterpreterBase, HoldableObject):
srcdir = Path(self.environment.get_source_dir())
builddir = Path(self.environment.get_build_dir())
try:
f = Path(f).resolve()
f_ = Path(f).resolve()
except OSError:
f = Path(f)
s = f.stat()
f_ = Path(f)
s = f_.stat()
if (hasattr(s, 'st_file_attributes') and
s.st_file_attributes & stat.FILE_ATTRIBUTE_REPARSE_POINT != 0 and
s.st_reparse_tag == stat.IO_REPARSE_TAG_APPEXECLINK):
# This is a Windows Store link which we can't
# resolve, so just do our best otherwise.
f = f.parent.resolve() / f.name
f_ = f_.parent.resolve() / f_.name
else:
raise
if builddir in f.parents:
if builddir in f_.parents:
return
if srcdir in f.parents:
f = f.relative_to(srcdir)
f = str(f)
if srcdir in f_.parents:
f_ = f_.relative_to(srcdir)
f = str(f_)
else:
return
if f not in self.build_def_files:
self.build_def_files.append(f)
self.build_def_files.add(f)
def get_variables(self):
def get_variables(self) -> T.Dict[str, InterpreterObject]:
return self.variables
def check_stdlibs(self):
def check_stdlibs(self) -> None:
machine_choices = [MachineChoice.HOST]
if self.coredata.is_cross_build():
machine_choices.append(MachineChoice.BUILD)
@ -563,7 +565,7 @@ class Interpreter(InterpreterBase, HoldableObject):
dep = df.lookup(kwargs, force_fallback=True)
self.build.stdlibs[for_machine][l] = dep
def _import_module(self, modname: str, required: bool) -> T.Union[ExtensionModule, NewExtensionModule, NotFoundExtensionModule]:
def _import_module(self, modname: str, required: bool) -> NewExtensionModule:
if modname in self.modules:
return self.modules[modname]
try:
@ -797,24 +799,37 @@ external dependencies (including libraries) must go to "dependencies".''')
@FeatureNewKwargs('subproject', '0.38.0', ['default_options'])
@permittedKwargs({'version', 'default_options', 'required'})
@typed_pos_args('subproject', str)
def func_subproject(self, nodes: mparser.BaseNode, args: T.Tuple[str], kwargs: 'TYPE_kwargs') -> SubprojectHolder:
return self.do_subproject(args[0], 'meson', kwargs)
def disabled_subproject(self, subp_name, disabled_feature=None, exception=None):
@typed_kwargs(
'subproject',
REQUIRED_KW,
DEFAULT_OPTIONS.evolve(since='0.38.0'),
KwargInfo('version', ContainerTypeInfo(list, str), default=[], listify=True),
)
def func_subproject(self, nodes: mparser.BaseNode, args: T.Tuple[str], kwargs_: kwargs.Subproject) -> SubprojectHolder:
kw: kwargs.DoSubproject = {
'required': kwargs_['required'],
'default_options': kwargs_['default_options'],
'version': kwargs_['version'],
'options': None,
'cmake_options': [],
}
return self.do_subproject(args[0], 'meson', kw)
def disabled_subproject(self, subp_name: str, disabled_feature: T.Optional[str] = None,
exception: T.Optional[Exception] = None) -> SubprojectHolder:
sub = SubprojectHolder(NullSubprojectInterpreter(), os.path.join(self.subproject_dir, subp_name),
disabled_feature=disabled_feature, exception=exception)
self.subprojects[subp_name] = sub
self.coredata.initialized_subprojects.add(subp_name)
return sub
def do_subproject(self, subp_name: str, method: str, kwargs):
def do_subproject(self, subp_name: str, method: Literal['meson', 'cmake'], kwargs: kwargs.DoSubproject) -> SubprojectHolder:
disabled, required, feature = extract_required_kwarg(kwargs, self.subproject)
if disabled:
mlog.log('Subproject', mlog.bold(subp_name), ':', 'skipped: feature', mlog.bold(feature), 'disabled')
return self.disabled_subproject(subp_name, disabled_feature=feature)
default_options = mesonlib.stringlistify(kwargs.get('default_options', []))
default_options = coredata.create_options_dict(default_options, subp_name)
default_options = coredata.create_options_dict(kwargs['default_options'], subp_name)
if subp_name == '':
raise InterpreterException('Subproject name must not be empty.')
@ -835,7 +850,7 @@ external dependencies (including libraries) must go to "dependencies".''')
subproject = self.subprojects[subp_name]
if required and not subproject.found():
raise InterpreterException(f'Subproject "{subproject.subdir}" required but not found.')
if 'version' in kwargs:
if kwargs['version']:
pv = self.build.subprojects[subp_name]
wanted = kwargs['version']
if pv == 'undefined' or not mesonlib.version_compare_many(pv, wanted)[0]:
@ -882,7 +897,9 @@ external dependencies (including libraries) must go to "dependencies".''')
return self.disabled_subproject(subp_name, exception=e)
raise e
def _do_subproject_meson(self, subp_name: str, subdir: str, default_options, kwargs,
def _do_subproject_meson(self, subp_name: str, subdir: str,
default_options: T.Dict[OptionKey, str],
kwargs: kwargs.DoSubproject,
ast: T.Optional[mparser.CodeBlockNode] = None,
build_def_files: T.Optional[T.List[str]] = None,
is_translated: bool = False) -> SubprojectHolder:
@ -911,7 +928,7 @@ external dependencies (including libraries) must go to "dependencies".''')
mlog.log()
if 'version' in kwargs:
if kwargs['version']:
pv = subi.project_version
wanted = kwargs['version']
if pv == 'undefined' or not mesonlib.version_compare_many(pv, wanted)[0]:
@ -921,27 +938,24 @@ external dependencies (including libraries) must go to "dependencies".''')
self.subprojects[subp_name] = SubprojectHolder(subi, subdir, warnings=subi_warnings)
# Duplicates are possible when subproject uses files from project root
if build_def_files:
self.build_def_files = list(set(self.build_def_files + build_def_files))
self.build_def_files.update(build_def_files)
# We always need the subi.build_def_files, to propgate sub-sub-projects
self.build_def_files = list(set(self.build_def_files + subi.build_def_files))
self.build_def_files.update(subi.build_def_files)
self.build.merge(subi.build)
self.build.subprojects[subp_name] = subi.project_version
self.coredata.initialized_subprojects.add(subp_name)
return self.subprojects[subp_name]
def _do_subproject_cmake(self, subp_name, subdir, subdir_abs, default_options, kwargs):
def _do_subproject_cmake(self, subp_name: str, subdir: str, subdir_abs: str,
default_options: T.Dict[OptionKey, str],
kwargs: kwargs.DoSubproject) -> SubprojectHolder:
with mlog.nested(subp_name):
new_build = self.build.copy()
prefix = self.coredata.options[OptionKey('prefix')].value
from ..modules.cmake import CMakeSubprojectOptions
options = kwargs.get('options', CMakeSubprojectOptions())
if not isinstance(options, CMakeSubprojectOptions):
raise InterpreterException('"options" kwarg must be CMakeSubprojectOptions'
' object (created by cmake.subproject_options())')
cmake_options = mesonlib.stringlistify(kwargs.get('cmake_options', []))
cmake_options += options.cmake_options
options = kwargs['options'] or CMakeSubprojectOptions()
cmake_options = kwargs['cmake_options'] + options.cmake_options
cm_int = CMakeInterpreter(new_build, Path(subdir), Path(subdir_abs), Path(prefix), new_build.environment, self.backend)
cm_int.initialise(cmake_options)
cm_int.analyse()
@ -967,13 +981,13 @@ external dependencies (including libraries) must go to "dependencies".''')
mlog.cmd_ci_include(meson_filename)
mlog.log()
result = self._do_subproject_meson(subp_name, subdir, default_options, kwargs, ast, cm_int.bs_files, is_translated=True)
result = self._do_subproject_meson(subp_name, subdir, default_options, kwargs, ast, [str(f) for f in cm_int.bs_files], is_translated=True)
result.cm_interpreter = cm_int
mlog.log()
return result
def get_option_internal(self, optname: str):
def get_option_internal(self, optname: str) -> coredata.UserOption:
key = OptionKey.from_string(optname).evolve(subproject=self.subproject)
if not key.is_project():
@ -982,6 +996,7 @@ external dependencies (including libraries) must go to "dependencies".''')
if v is None or v.yielding:
v = opts.get(key.as_root())
if v is not None:
assert isinstance(v, coredata.UserOption), 'for mypy'
return v
try:
@ -1037,7 +1052,7 @@ external dependencies (including libraries) must go to "dependencies".''')
f'"configuration_data": initial value dictionary key "{k!r}"" must be "str | int | bool", not "{v!r}"')
return build.ConfigurationData(initial_values)
def set_backend(self):
def set_backend(self) -> None:
# The backend is already set when parsing subprojects
if self.backend is not None:
return
@ -1326,7 +1341,7 @@ external dependencies (including libraries) must go to "dependencies".''')
return False
return should
def add_languages_for(self, args: T.List[str], required: bool, for_machine: MachineChoice) -> None:
def add_languages_for(self, args: T.List[str], required: bool, for_machine: MachineChoice) -> bool:
args = [a.lower() for a in args]
langs = set(self.coredata.compilers[for_machine].keys())
langs.update(args)
@ -1378,7 +1393,8 @@ external dependencies (including libraries) must go to "dependencies".''')
return success
def program_from_file_for(self, for_machine, prognames):
def program_from_file_for(self, for_machine: MachineChoice, prognames: T.List[mesonlib.FileOrString]
) -> T.Optional[ExternalProgram]:
for p in prognames:
if isinstance(p, mesonlib.File):
continue # Always points to a local (i.e. self generated) file.
@ -1389,7 +1405,8 @@ external dependencies (including libraries) must go to "dependencies".''')
return prog
return None
def program_from_system(self, args, search_dirs, extra_info):
def program_from_system(self, args: T.List[mesonlib.FileOrString], search_dirs: T.List[str],
extra_info: T.List[mlog.TV_Loggable]) -> T.Optional[ExternalProgram]:
# Search for scripts relative to current subdir.
# Do not cache found programs because find_program('foobar')
# might give different results when run from different source dirs.
@ -1415,9 +1432,11 @@ external dependencies (including libraries) must go to "dependencies".''')
if extprog.found():
extra_info.append(f"({' '.join(extprog.get_command())})")
return extprog
return None
def program_from_overrides(self, command_names: T.List[str], extra_info: T.List['mlog.TV_Loggable']) -> \
T.Optional[T.Union[ExternalProgram, 'OverrideProgram', 'build.Executable']]:
def program_from_overrides(self, command_names: T.List[mesonlib.FileOrString],
extra_info: T.List['mlog.TV_Loggable']
) -> T.Optional[T.Union[ExternalProgram, OverrideProgram, build.Executable]]:
for name in command_names:
if not isinstance(name, str):
continue
@ -1427,7 +1446,7 @@ external dependencies (including libraries) must go to "dependencies".''')
return exe
return None
def store_name_lookups(self, command_names):
def store_name_lookups(self, command_names: T.List[mesonlib.FileOrString]) -> None:
for name in command_names:
if isinstance(name, str):
self.build.searched_programs.add(name)
@ -1455,7 +1474,7 @@ external dependencies (including libraries) must go to "dependencies".''')
) -> T.Union['ExternalProgram', 'build.Executable', 'OverrideProgram']:
args = mesonlib.listify(args)
extra_info = []
extra_info: T.List[mlog.TV_Loggable] = []
progobj = self.program_lookup(args, for_machine, required, search_dirs, extra_info)
if progobj is None:
progobj = self.notfound_program(args)
@ -1472,14 +1491,15 @@ external dependencies (including libraries) must go to "dependencies".''')
if version_func:
version = version_func(progobj)
elif isinstance(progobj, build.Executable):
interp = self
if progobj.subproject:
interp = self.subprojects[progobj.subproject].held_object
assert isinstance(interp, Interpreter)
else:
interp = self
assert isinstance(interp, Interpreter)
version = interp.project_version
else:
version = progobj.get_version(self)
is_found, not_found, found = mesonlib.version_compare_many(version, wanted)
is_found, not_found, _ = mesonlib.version_compare_many(version, wanted)
if not is_found:
mlog.log('Program', mlog.bold(progobj.name), 'found:', mlog.red('NO'),
'found', mlog.normal_cyan(version), 'but need:',
@ -1498,7 +1518,9 @@ external dependencies (including libraries) must go to "dependencies".''')
progobj.was_returned_by_find_program = True
return progobj
def program_lookup(self, args, for_machine, required, search_dirs, extra_info):
def program_lookup(self, args: T.List[mesonlib.FileOrString], for_machine: MachineChoice,
required: bool, search_dirs: T.List[str], extra_info: T.List[mlog.TV_Loggable]
) -> T.Optional[T.Union[ExternalProgram, build.Executable, OverrideProgram]]:
progobj = self.program_from_overrides(args, extra_info)
if progobj:
return progobj
@ -1521,10 +1543,18 @@ external dependencies (including libraries) must go to "dependencies".''')
return progobj
def find_program_fallback(self, fallback, args, required, extra_info):
def find_program_fallback(self, fallback: str, args: T.List[mesonlib.FileOrString],
required: bool, extra_info: T.List[mlog.TV_Loggable]
) -> T.Optional[T.Union[ExternalProgram, build.Executable, OverrideProgram]]:
mlog.log('Fallback to subproject', mlog.bold(fallback), 'which provides program',
mlog.bold(' '.join(args)))
sp_kwargs = {'required': required}
sp_kwargs: kwargs.DoSubproject = {
'required': required,
'default_options': [],
'version': [],
'cmake_options': [],
'options': None,
}
self.do_subproject(fallback, 'meson', sp_kwargs)
return self.program_from_overrides(args, extra_info)
@ -2115,7 +2145,7 @@ external dependencies (including libraries) must go to "dependencies".''')
self.subdir = subdir
os.makedirs(os.path.join(self.environment.build_dir, subdir), exist_ok=True)
buildfilename = os.path.join(self.subdir, environment.build_filename)
self.build_def_files.append(buildfilename)
self.build_def_files.add(buildfilename)
absname = os.path.join(self.environment.get_source_dir(), buildfilename)
if not os.path.isfile(absname):
self.subdir = prev_subdir

@ -1,3 +1,4 @@
from __future__ import annotations
import os
import shlex
import subprocess
@ -23,15 +24,16 @@ from ..interpreterbase import (
from ..interpreter.type_checking import NoneType, ENV_SEPARATOR_KW
from ..dependencies import Dependency, ExternalLibrary, InternalDependency
from ..programs import ExternalProgram
from ..mesonlib import HoldableObject, MesonException, OptionKey, listify, Popen_safe
from ..mesonlib import HoldableObject, OptionKey, listify, Popen_safe
import typing as T
if T.TYPE_CHECKING:
from . import kwargs
from .interpreter import Interpreter
from ..cmake.interpreter import CMakeInterpreter
from ..envconfig import MachineInfo
from ..interpreterbase import SubProject
from .interpreter import Interpreter
from typing_extensions import TypedDict
@ -683,13 +685,14 @@ class SubprojectHolder(MesonInterpreterObject):
subdir: str,
warnings: int = 0,
disabled_feature: T.Optional[str] = None,
exception: T.Optional[MesonException] = None) -> None:
exception: T.Optional[Exception] = None) -> None:
super().__init__()
self.held_object = subinterpreter
self.warnings = warnings
self.disabled_feature = disabled_feature
self.exception = exception
self.subdir = PurePath(subdir).as_posix()
self.cm_interpreter: T.Optional[CMakeInterpreter] = None
self.methods.update({'get_variable': self.get_variable_method,
'found': self.found_method,
})

@ -12,6 +12,7 @@ from .. import build
from .. import coredata
from ..compilers import Compiler
from ..mesonlib import MachineChoice, File, FileMode, FileOrString, OptionKey
from ..modules.cmake import CMakeSubprojectOptions
from ..programs import ExternalProgram
@ -293,3 +294,17 @@ class ConfigureFile(TypedDict):
command: T.Optional[T.List[T.Union[build.Executable, ExternalProgram, Compiler, File, str]]]
input: T.List[FileOrString]
configuration: T.Optional[T.Union[T.Dict[str, T.Union[str, int, bool]], build.ConfigurationData]]
class Subproject(ExtractRequired):
default_options: T.List[str]
version: T.List[str]
class DoSubproject(ExtractRequired):
default_options: T.List[str]
version: T.List[str]
cmake_options: T.List[str]
options: T.Optional[CMakeSubprojectOptions]

@ -326,7 +326,7 @@ def find_buildsystem_files_list(src_dir: str) -> T.List[str]:
def list_buildsystem_files(builddata: build.Build, interpreter: Interpreter) -> T.List[str]:
src_dir = builddata.environment.get_source_dir()
filelist = interpreter.get_build_def_files() # type: T.List[str]
filelist = list(interpreter.get_build_def_files())
filelist = [PurePath(src_dir, x).as_posix() for x in filelist]
return filelist

@ -11,6 +11,8 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import re
import os, os.path, pathlib
import shutil
@ -21,11 +23,10 @@ from . import ExtensionModule, ModuleReturnValue, ModuleObject
from .. import build, mesonlib, mlog, dependencies
from ..cmake import SingleTargetOptions, TargetOptions, cmake_defines_to_args
from ..interpreter import SubprojectHolder
from ..interpreter.type_checking import NoneType, in_set_validator
from ..interpreter.type_checking import REQUIRED_KW, NoneType, in_set_validator
from ..interpreterbase import (
FeatureNew,
FeatureNewKwargs,
FeatureDeprecatedKwargs,
stringArgs,
permittedKwargs,
@ -35,6 +36,7 @@ from ..interpreterbase import (
InvalidArguments,
InterpreterException,
typed_pos_args,
typed_kwargs,
KwargInfo,
ContainerTypeInfo,
@ -43,6 +45,9 @@ from ..interpreterbase import (
if T.TYPE_CHECKING:
from typing_extensions import TypedDict
from . import ModuleState
from ..interpreter import kwargs
class WriteBasicPackageVersionFile(TypedDict):
arch_independent: bool
@ -58,6 +63,12 @@ if T.TYPE_CHECKING:
install_dir: T.Optional[str]
name: str
class Subproject(kwargs.ExtractRequired):
options: T.Optional[CMakeSubprojectOptions]
cmake_options: T.List[str]
COMPATIBILITIES = ['AnyNewerVersion', 'SameMajorVersion', 'SameMinorVersion', 'ExactVersion']
# Taken from https://github.com/Kitware/CMake/blob/master/Modules/CMakePackageConfigHelpers.cmake
@ -91,11 +102,12 @@ endmacro()
'''
class CMakeSubproject(ModuleObject):
def __init__(self, subp, pv):
def __init__(self, subp: SubprojectHolder):
assert isinstance(subp, SubprojectHolder)
assert hasattr(subp, 'cm_interpreter')
assert subp.cm_interpreter is not None
super().__init__()
self.subp = subp
self.cm_interpreter = subp.cm_interpreter
self.methods.update({'get_variable': self.get_variable,
'dependency': self.dependency,
'include_directories': self.include_directories,
@ -110,7 +122,7 @@ class CMakeSubproject(ModuleObject):
raise InterpreterException('Exactly one argument is required.')
tgt = args[0]
res = self.subp.cm_interpreter.target_info(tgt)
res = self.cm_interpreter.target_info(tgt)
if res is None:
raise InterpreterException(f'The CMake target {tgt} does not exist\n' +
' Use the following command in your meson.build to list all available targets:\n\n' +
@ -161,7 +173,7 @@ class CMakeSubproject(ModuleObject):
@noPosargs
@noKwargs
def target_list(self, state, args, kwargs):
return self.subp.cm_interpreter.target_list()
return self.cm_interpreter.target_list()
@noPosargs
@noKwargs
@ -391,8 +403,7 @@ class CmakeModule(ExtensionModule):
conf.used = True
conffile = os.path.normpath(inputfile.relative_name())
if conffile not in self.interpreter.build_def_files:
self.interpreter.build_def_files.append(conffile)
self.interpreter.build_def_files.add(conffile)
res = build.Data([mesonlib.File(True, ofile_path, ofile_fname)], install_dir, install_dir, None, state.subproject)
self.interpreter.build.data.append(res)
@ -400,20 +411,35 @@ class CmakeModule(ExtensionModule):
return res
@FeatureNew('subproject', '0.51.0')
@FeatureNewKwargs('subproject', '0.55.0', ['options'])
@FeatureDeprecatedKwargs('subproject', '0.55.0', ['cmake_options'])
@permittedKwargs({'cmake_options', 'required', 'options'})
@stringArgs
def subproject(self, state, args, kwargs):
if len(args) != 1:
raise InterpreterException('Subproject takes exactly one argument')
if 'cmake_options' in kwargs and 'options' in kwargs:
@typed_pos_args('cmake.subproject', str)
@typed_kwargs(
'cmake.subproject',
REQUIRED_KW,
KwargInfo('options', (CMakeSubprojectOptions, NoneType), since='0.55.0'),
KwargInfo(
'cmake_options',
ContainerTypeInfo(list, str),
default=[],
listify=True,
deprecated='0.55.0',
deprecated_message='Use options instead',
),
)
def subproject(self, state: ModuleState, args: T.Tuple[str], kwargs_: Subproject) -> T.Union[SubprojectHolder, CMakeSubproject]:
if kwargs_['cmake_options'] and kwargs_['options'] is not None:
raise InterpreterException('"options" cannot be used together with "cmake_options"')
dirname = args[0]
subp = self.interpreter.do_subproject(dirname, 'cmake', kwargs)
kw: kwargs.DoSubproject = {
'required': kwargs_['required'],
'options': kwargs_['options'],
'cmake_options': kwargs_['cmake_options'],
'default_options': [],
'version': [],
}
subp = self.interpreter.do_subproject(dirname, 'cmake', kw)
if not subp.found():
return subp
return CMakeSubproject(subp, dirname)
return CMakeSubproject(subp)
@FeatureNew('subproject_options', '0.55.0')
@noKwargs

@ -63,8 +63,8 @@ class KeyvalModule(ExtensionModule):
else:
s = os.path.join(self.interpreter.environment.source_dir, s)
if s not in self.interpreter.build_def_files and not is_built:
self.interpreter.build_def_files.append(s)
if not is_built:
self.interpreter.build_def_files.add(s)
return self._load_file(s)

@ -1,7 +1,7 @@
{
"stdout": [
{
"line": "test cases/failing/120 subproject version conflict/meson.build:4:0: ERROR: Subproject B version is 100 but 1 required."
"line": "test cases/failing/120 subproject version conflict/meson.build:4:0: ERROR: Subproject B version is 100 but ['1'] required."
}
]
}

@ -1,7 +1,7 @@
{
"stdout": [
{
"line": "test cases/failing/21 subver/meson.build:3:0: ERROR: Subproject foo version is 1.0.0 but >1.0.0 required."
"line": "test cases/failing/21 subver/meson.build:3:0: ERROR: Subproject foo version is 1.0.0 but ['>1.0.0'] required."
}
]
}

Loading…
Cancel
Save