From a1d9adba096589122aefc50fb9429ec0ce0432b9 Mon Sep 17 00:00:00 2001 From: Nirbheek Chauhan Date: Mon, 2 Jul 2018 18:44:36 +0530 Subject: [PATCH] FeatureNew: Make all checks subproject-specific We now pass the current subproject to every FeatureNew and FeatureDeprecated call. This requires a bunch of rework to: 1. Ensure that we have access to the subproject in the list of arguments when used as a decorator (see _get_callee_args). 2. Pass the subproject to .use() when it's called manually. 3. We also can't do feature checks for new features in meson_options.txt because that's parsed before we know the meson_version from project() --- mesonbuild/build.py | 2 +- mesonbuild/dependencies/misc.py | 5 - mesonbuild/dependencies/ui.py | 2 - mesonbuild/interpreter.py | 88 +++++--- mesonbuild/interpreterbase.py | 209 ++++++++++-------- mesonbuild/mesonlib.py | 4 +- mesonbuild/modules/pkgconfig.py | 4 +- mesonbuild/optinterpreter.py | 12 +- run_unittests.py | 20 ++ .../common/174 dependency factory/meson.build | 2 +- .../34 featurenew subprojects/meson.build | 6 + .../subprojects/bar/meson.build | 3 + .../subprojects/foo/meson.build | 3 + 13 files changed, 211 insertions(+), 149 deletions(-) create mode 100644 test cases/unit/34 featurenew subprojects/meson.build create mode 100644 test cases/unit/34 featurenew subprojects/subprojects/bar/meson.build create mode 100644 test cases/unit/34 featurenew subprojects/subprojects/foo/meson.build diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 2b225212d..7d071e0b9 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -1780,7 +1780,7 @@ class CustomTarget(Target): 'when installing a target') if isinstance(kwargs['install_dir'], list): - FeatureNew('multiple install_dir for custom_target', '0.40.0').use() + FeatureNew('multiple install_dir for custom_target', '0.40.0').use(self.subproject) # If an item in this list is False, the output corresponding to # the list index of that item will not be installed self.install_dir = typeslistify(kwargs['install_dir'], (str, bool)) diff --git a/mesonbuild/dependencies/misc.py b/mesonbuild/dependencies/misc.py index 95fe1d8de..745dff011 100644 --- a/mesonbuild/dependencies/misc.py +++ b/mesonbuild/dependencies/misc.py @@ -31,11 +31,9 @@ from .base import ( ConfigToolDependency, ) -from ..interpreterbase import FeatureNew class MPIDependency(ExternalDependency): - @FeatureNew('MPI Dependency', '0.42.0') def __init__(self, environment, kwargs): language = kwargs.get('language', 'c') super().__init__('mpi', environment, language, kwargs) @@ -252,7 +250,6 @@ class OpenMPDependency(ExternalDependency): '199810': '1.0', } - @FeatureNew('OpenMP Dependency', '0.46.0') def __init__(self, environment, kwargs): language = kwargs.get('language') super().__init__('openmp', environment, language, kwargs) @@ -433,7 +430,6 @@ class Python3Dependency(ExternalDependency): class PcapDependency(ExternalDependency): - @FeatureNew('Pcap Dependency', '0.42.0') def __init__(self, environment, kwargs): super().__init__('pcap', environment, None, kwargs) @@ -517,7 +513,6 @@ class CupsDependency(ExternalDependency): class LibWmfDependency(ExternalDependency): - @FeatureNew('LibWMF Dependency', '0.44.0') def __init__(self, environment, kwargs): super().__init__('libwmf', environment, None, kwargs) diff --git a/mesonbuild/dependencies/ui.py b/mesonbuild/dependencies/ui.py index 324f9fa97..197d22cb1 100644 --- a/mesonbuild/dependencies/ui.py +++ b/mesonbuild/dependencies/ui.py @@ -33,7 +33,6 @@ from .base import ExternalDependency, ExternalProgram from .base import ExtraFrameworkDependency, PkgConfigDependency from .base import ConfigToolDependency -from ..interpreterbase import FeatureNew class GLDependency(ExternalDependency): def __init__(self, environment, kwargs): @@ -516,7 +515,6 @@ class WxDependency(ConfigToolDependency): class VulkanDependency(ExternalDependency): - @FeatureNew('Vulkan Dependency', '0.42.0') def __init__(self, environment, kwargs): super().__init__('vulkan', environment, None, kwargs) diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py index fc6588b58..e39abd8a8 100644 --- a/mesonbuild/interpreter.py +++ b/mesonbuild/interpreter.py @@ -57,8 +57,9 @@ def stringifyUserArguments(args): class ObjectHolder: - def __init__(self, obj): + def __init__(self, obj, subproject=None): self.held_object = obj + self.subproject = subproject def __repr__(self): return ''.format(self.held_object) @@ -210,8 +211,8 @@ class ConfigureFileHolder(InterpreterObject, ObjectHolder): def __init__(self, subdir, sourcename, targetname, configuration_data): InterpreterObject.__init__(self) - ObjectHolder.__init__(self, build.ConfigureFile(subdir, sourcename, - targetname, configuration_data)) + obj = build.ConfigureFile(subdir, sourcename, targetname, configuration_data) + ObjectHolder.__init__(self, obj) class EnvironmentVariablesHolder(MutableInterpreterObject, ObjectHolder): @@ -254,10 +255,10 @@ class EnvironmentVariablesHolder(MutableInterpreterObject, ObjectHolder): class ConfigurationDataHolder(MutableInterpreterObject, ObjectHolder): - def __init__(self): + def __init__(self, pv): MutableInterpreterObject.__init__(self) self.used = False # These objects become immutable after use in configure_file. - ObjectHolder.__init__(self, build.ConfigurationData()) + ObjectHolder.__init__(self, build.ConfigurationData(), pv) self.methods.update({'set': self.set_method, 'set10': self.set10_method, 'set_quoted': self.set_quoted_method, @@ -363,9 +364,9 @@ This will become a hard error in the future''') # these wrappers. class DependencyHolder(InterpreterObject, ObjectHolder): - def __init__(self, dep): + def __init__(self, dep, pv): InterpreterObject.__init__(self) - ObjectHolder.__init__(self, dep) + ObjectHolder.__init__(self, dep, pv) self.methods.update({'found': self.found_method, 'type_name': self.type_name_method, 'version': self.version_method, @@ -416,12 +417,13 @@ class DependencyHolder(InterpreterObject, ObjectHolder): @noPosargs @permittedKwargs(permitted_method_kwargs['partial_dependency']) def partial_dependency_method(self, args, kwargs): - return DependencyHolder(self.held_object.get_partial_dependency(**kwargs)) + pdep = self.held_object.get_partial_dependency(**kwargs) + return DependencyHolder(pdep, self.subproject) class InternalDependencyHolder(InterpreterObject, ObjectHolder): - def __init__(self, dep): + def __init__(self, dep, pv): InterpreterObject.__init__(self) - ObjectHolder.__init__(self, dep) + ObjectHolder.__init__(self, dep, pv) self.methods.update({'found': self.found_method, 'version': self.version_method, 'partial_dependency': self.partial_dependency_method, @@ -441,7 +443,8 @@ class InternalDependencyHolder(InterpreterObject, ObjectHolder): @noPosargs @permittedKwargs(permitted_method_kwargs['partial_dependency']) def partial_dependency_method(self, args, kwargs): - return DependencyHolder(self.held_object.get_partial_dependency(**kwargs)) + pdep = self.held_object.get_partial_dependency(**kwargs) + return DependencyHolder(pdep, self.subproject) class ExternalProgramHolder(InterpreterObject, ObjectHolder): def __init__(self, ep): @@ -470,9 +473,9 @@ class ExternalProgramHolder(InterpreterObject, ObjectHolder): return self.held_object.get_name() class ExternalLibraryHolder(InterpreterObject, ObjectHolder): - def __init__(self, el): + def __init__(self, el, pv): InterpreterObject.__init__(self) - ObjectHolder.__init__(self, el) + ObjectHolder.__init__(self, el, pv) self.methods.update({'found': self.found_method, 'partial_dependency': self.partial_dependency_method, }) @@ -501,14 +504,15 @@ class ExternalLibraryHolder(InterpreterObject, ObjectHolder): @noPosargs @permittedKwargs(permitted_method_kwargs['partial_dependency']) def partial_dependency_method(self, args, kwargs): - return DependencyHolder(self.held_object.get_partial_dependency(**kwargs)) + pdep = self.held_object.get_partial_dependency(**kwargs) + return DependencyHolder(pdep, self.subproject) class GeneratorHolder(InterpreterObject, ObjectHolder): @FeatureNewKwargs('generator', '0.43.0', ['capture']) - def __init__(self, interpreter, args, kwargs): + def __init__(self, interp, args, kwargs): + self.interpreter = interp InterpreterObject.__init__(self) - self.interpreter = interpreter - ObjectHolder.__init__(self, build.Generator(args, kwargs)) + ObjectHolder.__init__(self, build.Generator(args, kwargs), interp.subproject) self.methods.update({'process': self.process_method}) @FeatureNewKwargs('generator.process', '0.45.0', ['preserve_path_from']) @@ -715,7 +719,7 @@ class GeneratedObjectsHolder(InterpreterObject, ObjectHolder): class TargetHolder(InterpreterObject, ObjectHolder): def __init__(self, target, interp): InterpreterObject.__init__(self) - ObjectHolder.__init__(self, target) + ObjectHolder.__init__(self, target, interp.subproject) self.interpreter = interp class BuildTargetHolder(TargetHolder): @@ -911,10 +915,11 @@ class SubprojectHolder(InterpreterObject, ObjectHolder): return self.held_object.variables[varname] class CompilerHolder(InterpreterObject): - def __init__(self, compiler, env): + def __init__(self, compiler, env, subproject): InterpreterObject.__init__(self) self.compiler = compiler self.environment = env + self.subproject = subproject self.methods.update({'compiles': self.compiles_method, 'links': self.links_method, 'get_id': self.get_id_method, @@ -1408,7 +1413,7 @@ class CompilerHolder(InterpreterObject): self.environment, self.compiler.language, silent=True) - return ExternalLibraryHolder(lib) + return ExternalLibraryHolder(lib, self.subproject) search_dirs = mesonlib.stringlistify(kwargs.get('dirs', [])) for i in search_dirs: @@ -1419,7 +1424,7 @@ class CompilerHolder(InterpreterObject): raise InterpreterException('{} library {!r} not found'.format(self.compiler.get_display_language(), libname)) lib = dependencies.ExternalLibrary(libname, linkargs, self.environment, self.compiler.language) - return ExternalLibraryHolder(lib) + return ExternalLibraryHolder(lib, self.subproject) @permittedKwargs({}) def has_argument_method(self, args, kwargs): @@ -1690,7 +1695,7 @@ class MesonMain(InterpreterObject): else: clist = self.build.cross_compilers if cname in clist: - return CompilerHolder(clist[cname], self.build.environment) + return CompilerHolder(clist[cname], self.build.environment, self.interpreter.subproject) raise InterpreterException('Tried to access compiler for unspecified language "%s".' % cname) @noPosargs @@ -1962,9 +1967,9 @@ class Interpreter(InterpreterBase): elif isinstance(item, build.Data): return DataHolder(item) elif isinstance(item, dependencies.InternalDependency): - return InternalDependencyHolder(item) + return InternalDependencyHolder(item, self.subproject) elif isinstance(item, dependencies.ExternalDependency): - return DependencyHolder(item) + return DependencyHolder(item, self.subproject) elif isinstance(item, dependencies.ExternalProgram): return ExternalProgramHolder(item) elif hasattr(item, 'held_object'): @@ -2079,7 +2084,7 @@ class Interpreter(InterpreterBase): external dependencies (including libraries) must go to "dependencies".''') dep = dependencies.InternalDependency(version, incs, compile_args, link_args, libs, libs_whole, sources, final_deps) - return DependencyHolder(dep) + return DependencyHolder(dep, self.subproject) @noKwargs def func_assert(self, node, args, kwargs): @@ -2305,7 +2310,7 @@ external dependencies (including libraries) must go to "dependencies".''') def func_configuration_data(self, node, args, kwargs): if args: raise InterpreterException('configuration_data takes no arguments') - return ConfigurationDataHolder() + return ConfigurationDataHolder(self.subproject) def set_options(self, default_options): # Set default options as if they were passed to the command line. @@ -2426,10 +2431,11 @@ external dependencies (including libraries) must go to "dependencies".''') self.build.subproject_dir = self.subproject_dir + mesonlib.project_meson_versions[self.subproject] = '' if 'meson_version' in kwargs: cv = coredata.version pv = kwargs['meson_version'] - mesonlib.target_version = pv + mesonlib.project_meson_versions[self.subproject] = pv if not mesonlib.version_compare(cv, pv): raise InterpreterException('Meson version is %s but project requires %s.' % (cv, pv)) self.build.projects[self.subproject] = proj_name @@ -2799,6 +2805,19 @@ external dependencies (including libraries) must go to "dependencies".''') 'dep {}'.format(found, dirname, wanted, name)) return None + def _handle_featurenew_dependencies(self, name): + 'Do a feature check on dependencies used by this subproject' + if name == 'mpi': + FeatureNew('MPI Dependency', '0.42.0').use(self.subproject) + elif name == 'pcap': + FeatureNew('Pcap Dependency', '0.42.0').use(self.subproject) + elif name == 'vulkan': + FeatureNew('Vulkan Dependency', '0.42.0').use(self.subproject) + elif name == 'libwmf': + FeatureNew('LibWMF Dependency', '0.44.0').use(self.subproject) + elif name == 'openmp': + FeatureNew('OpenMP Dependency', '0.46.0').use(self.subproject) + @FeatureNewKwargs('dependency', '0.40.0', ['method']) @FeatureNewKwargs('dependency', '0.38.0', ['default_options']) @permittedKwargs(permitted_kwargs['dependency']) @@ -2810,7 +2829,7 @@ external dependencies (including libraries) must go to "dependencies".''') disabled, required, feature = extract_required_kwarg(kwargs) if disabled: mlog.log('Dependency', mlog.bold(display_name), 'skipped: feature', mlog.bold(feature), 'disabled') - return DependencyHolder(NotFoundDependency(self.environment)) + return DependencyHolder(NotFoundDependency(self.environment), self.subproject) # writing just "dependency('')" is an error, because it can only fail if name == '' and required and 'fallback' not in kwargs: @@ -2845,6 +2864,7 @@ external dependencies (including libraries) must go to "dependencies".''') pass # ... search for it outside the project elif name != '': + self._handle_featurenew_dependencies(name) try: dep = dependencies.find_external_dependency(name, self.environment, kwargs) except DependencyException as e: @@ -2868,7 +2888,7 @@ external dependencies (including libraries) must go to "dependencies".''') # Only store found-deps in the cache if dep.found(): self.coredata.deps[identifier] = dep - return DependencyHolder(dep) + return DependencyHolder(dep, self.subproject) @FeatureNew('disabler', '0.44.0') @noKwargs @@ -3012,7 +3032,7 @@ root and issuing %s. if 'input' not in kwargs or 'output' not in kwargs: raise InterpreterException('Keyword arguments input and output must exist') if 'fallback' not in kwargs: - FeatureNew('Optional fallback in vcs_tag', '0.41.0').use() + FeatureNew('Optional fallback in vcs_tag', '0.41.0').use(self.subproject) fallback = kwargs.pop('fallback', self.project_version) if not isinstance(fallback, str): raise InterpreterException('Keyword argument fallback must be a string.') @@ -3064,7 +3084,7 @@ root and issuing %s. if len(args) != 1: raise InterpreterException('custom_target: Only one positional argument is allowed, and it must be a string name') if 'depfile' in kwargs and ('@BASENAME@' in kwargs['depfile'] or '@PLAINNAME@' in kwargs['depfile']): - FeatureNew('substitutions in custom_target depfile', '0.47.0').use() + FeatureNew('substitutions in custom_target depfile', '0.47.0').use(self.subproject) name = args[0] kwargs['install_mode'] = self._get_kwarg_install_mode(kwargs) tg = CustomTargetHolder(build.CustomTarget(name, self.subdir, self.subproject, kwargs), self) @@ -3649,9 +3669,9 @@ different subdirectory. def run(self): super().run() mlog.log('Build targets in project:', mlog.bold(str(len(self.build.targets)))) - FeatureNew.called_features_report() - FeatureDeprecated.called_features_report() - if self.subproject == '': + FeatureNew.report(self.subproject) + FeatureDeprecated.report(self.subproject) + if not self.is_subproject(): self.print_extra_warnings() def print_extra_warnings(self): diff --git a/mesonbuild/interpreterbase.py b/mesonbuild/interpreterbase.py index 0d0e4afd0..f61ff472e 100644 --- a/mesonbuild/interpreterbase.py +++ b/mesonbuild/interpreterbase.py @@ -31,33 +31,64 @@ def check_stringlist(a, msg='Arguments must be strings.'): mlog.debug('Element not a string:', str(a)) raise InvalidArguments(msg) -def _get_callee_args(wrapped_args): +def _get_callee_args(wrapped_args, want_subproject=False): s = wrapped_args[0] n = len(wrapped_args) - if n == 3: - # Methods on objects (Holder, MesonMain, etc) have 3 args: self, args, kwargs - node_or_state = None + # Raise an error if the codepaths are not there + subproject = None + if want_subproject and n == 2: + if hasattr(s, 'subproject'): + # Interpreter base types have 2 args: self, node + node_or_state = wrapped_args[1] + # args and kwargs are inside the node + args = None + kwargs = None + subproject = s.subproject + elif hasattr(wrapped_args[1], 'subproject'): + # Module objects have 2 args: self, interpreter + node_or_state = wrapped_args[1] + # args and kwargs are inside the node + args = None + kwargs = None + subproject = wrapped_args[1].subproject + else: + raise AssertionError('Unknown args: {!r}'.format(wrapped_args)) + elif n == 3: + # Methods on objects (*Holder, MesonMain, etc) have 3 args: self, args, kwargs + node_or_state = None # FIXME args = wrapped_args[1] kwargs = wrapped_args[2] + if want_subproject: + if hasattr(s, 'subproject'): + subproject = s.subproject + elif hasattr(s, 'interpreter'): + subproject = s.interpreter.subproject elif n == 4: # Meson functions have 4 args: self, node, args, kwargs - # Module functions have 4 args: self, state, args, kwargs + # Module functions have 4 args: self, state, args, kwargs; except, + # PythonInstallation methods have self, interpreter, args, kwargs node_or_state = wrapped_args[1] args = wrapped_args[2] kwargs = wrapped_args[3] + if want_subproject: + if isinstance(s, InterpreterBase): + subproject = s.subproject + else: + subproject = node_or_state.subproject elif n == 5: # Module snippets have 5 args: self, interpreter, state, args, kwargs node_or_state = wrapped_args[2] args = wrapped_args[3] kwargs = wrapped_args[4] + if want_subproject: + subproject = node_or_state.subproject else: - raise AssertionError('Expecting 3, 4, or 5 args, got: {!r}'.format(wrapped_args)) - + raise AssertionError('Unknown args: {!r}'.format(wrapped_args)) # Sometimes interpreter methods are called internally with None instead of # empty list/dict args = args if args is not None else [] kwargs = kwargs if kwargs is not None else {} - return s, node_or_state, args, kwargs + return s, node_or_state, args, kwargs, subproject def flatten(args): if isinstance(args, mparser.StringNode): @@ -114,7 +145,7 @@ class permittedKwargs: def __call__(self, f): @wraps(f) def wrapped(*wrapped_args, **wrapped_kwargs): - s, node_or_state, args, kwargs = _get_callee_args(wrapped_args) + s, node_or_state, args, kwargs, _ = _get_callee_args(wrapped_args) loc = types.SimpleNamespace() if hasattr(s, 'subdir'): loc.subdir = s.subdir @@ -131,104 +162,92 @@ class permittedKwargs: return f(*wrapped_args, **wrapped_kwargs) return wrapped -# TODO: Share code between FeatureNew, FeatureDeprecated, FeatureNewKwargs, -# and FeatureDeprecatedKwargs -class FeatureNew: - """Checks for new features""" - # Shared across all instances - feature_versions = dict() - feature_warnings = False + +class FeatureCheckBase: + "Base class for feature version checks" def __init__(self, feature_name, version): self.feature_name = feature_name self.feature_version = version - def add_called_feature(self): - if self.feature_version not in self.feature_versions: - self.feature_versions[self.feature_version] = set() - if self.feature_name in self.feature_versions[self.feature_version]: - return False - self.feature_versions[self.feature_version].add(self.feature_name) - return True - - @classmethod - def called_features_report(cls): - if not cls.feature_warnings: - return - warning_str = 'Invalid minimum meson_version \'{}\' conflicts with:'\ - .format(mesonlib.target_version) - fv = cls.feature_versions - for version in sorted(fv.keys()): - warning_str += '\n * {}: {}'.format(version, fv[version]) - mlog.warning(warning_str) + @staticmethod + def get_target_version(subproject): + return mesonlib.project_meson_versions[subproject] - def use(self): - tv = mesonlib.target_version + def use(self, subproject): + tv = self.get_target_version(subproject) + # No target version if tv == '': return + # Target version is new enough if mesonlib.version_compare_condition_with_min(tv, self.feature_version): return - FeatureNew.feature_warnings = True - if not self.add_called_feature(): + # Feature is too new for target version, register it + if subproject not in self.feature_registry: + self.feature_registry[subproject] = {self.feature_version: set()} + register = self.feature_registry[subproject] + if self.feature_version not in register: + register[self.feature_version] = set() + if self.feature_name in register[self.feature_version]: + # Don't warn about the same feature multiple times + # FIXME: This is needed to prevent duplicate warnings, but also + # means we won't warn about a feature used in multiple places. return - mlog.warning('Project targetting \'{}\' but tried to use feature introduced ' - 'in \'{}\': {}'.format(tv, self.feature_version, self.feature_name)) + register[self.feature_version].add(self.feature_name) + self.log_usage_warning(tv) + + @classmethod + def report(cls, subproject): + if subproject not in cls.feature_registry: + return + warning_str = cls.get_warning_str_prefix(cls.get_target_version(subproject)) + fv = cls.feature_registry[subproject] + for version in sorted(fv.keys()): + warning_str += '\n * {}: {}'.format(version, fv[version]) + mlog.warning(warning_str) def __call__(self, f): @wraps(f) def wrapped(*wrapped_args, **wrapped_kwargs): - self.use() + subproject = _get_callee_args(wrapped_args, want_subproject=True)[4] + if subproject is None: + raise AssertionError('{!r}'.format(wrapped_args)) + self.use(subproject) return f(*wrapped_args, **wrapped_kwargs) return wrapped -class FeatureDeprecated: - """Checks for deprecated features""" - # Shared across all instances - feature_versions = dict() - feature_warnings = False +class FeatureNew(FeatureCheckBase): + """Checks for new features""" + # Class variable, shared across all instances + # + # Format: {subproject: {feature_version: set(feature_names)}} + feature_registry = {} - def __init__(self, feature_name, version): - self.feature_name = feature_name - self.feature_version = version + @staticmethod + def get_warning_str_prefix(tv): + return 'Project specifies a minimum meson_version \'{}\' which conflicts with:'.format(tv) - def add_called_feature(self): - if self.feature_version not in self.feature_versions: - self.feature_versions[self.feature_version] = set() - if self.feature_name in self.feature_versions[self.feature_version]: - return False - self.feature_versions[self.feature_version].add(self.feature_name) - return True + def log_usage_warning(self, tv): + mlog.warning('Project targetting \'{}\' but tried to use feature introduced ' + 'in \'{}\': {}'.format(tv, self.feature_version, self.feature_name)) - @classmethod - def called_features_report(cls): - if not cls.feature_warnings: - return - warning_str = 'Deprecated features used:'.format(mesonlib.target_version) - fv = cls.feature_versions - for version in sorted(fv.keys()): - warning_str += '\n * {}: {}'.format(version, fv[version]) - mlog.warning(warning_str) +class FeatureDeprecated(FeatureCheckBase): + """Checks for deprecated features""" + # Class variable, shared across all instances + # + # Format: {subproject: {feature_version: set(feature_names)}} + feature_registry = {} - def use(self): - tv = mesonlib.target_version - if tv == '': - return - if mesonlib.version_compare_condition_with_max(tv, self.feature_version): - return - FeatureDeprecated.feature_warnings = True - if not self.add_called_feature(): - return + @staticmethod + def get_warning_str_prefix(tv): + return 'Deprecated features used:' + + def log_usage_warning(self, tv): mlog.warning('Project targetting \'{}\' but tried to use feature deprecated ' 'since \'{}\': {}'.format(tv, self.feature_version, self.feature_name)) - def __call__(self, f): - @wraps(f) - def wrapped(*wrapped_args, **wrapped_kwargs): - self.use() - return f(*wrapped_args, **wrapped_kwargs) - return wrapped -class FeatureNewKwargs: +class FeatureCheckKwargsBase: def __init__(self, feature_name, feature_version, kwargs): self.feature_name = feature_name self.feature_version = feature_version @@ -237,30 +256,24 @@ class FeatureNewKwargs: def __call__(self, f): @wraps(f) def wrapped(*wrapped_args, **wrapped_kwargs): - s, node_or_state, args, kwargs = _get_callee_args(wrapped_args) + # Which FeatureCheck class to invoke + FeatureCheckClass = self.feature_check_class + kwargs, subproject = _get_callee_args(wrapped_args, want_subproject=True)[3:5] + if subproject is None: + raise AssertionError('{!r}'.format(wrapped_args)) for arg in self.kwargs: if arg not in kwargs: continue - FeatureNew(arg + ' arg in ' + self.feature_name, self.feature_version).use() + name = arg + ' arg in ' + self.feature_name + FeatureCheckClass(name, self.feature_version).use(subproject) return f(*wrapped_args, **wrapped_kwargs) return wrapped -class FeatureDeprecatedKwargs: - def __init__(self, feature_name, feature_version, kwargs): - self.feature_name = feature_name - self.feature_version = feature_version - self.kwargs = kwargs +class FeatureNewKwargs(FeatureCheckKwargsBase): + feature_check_class = FeatureNew - def __call__(self, f): - @wraps(f) - def wrapped(*wrapped_args, **wrapped_kwargs): - s, node_or_state, args, kwargs = _get_callee_args(wrapped_args) - for arg in self.kwargs: - if arg not in kwargs: - continue - FeatureDeprecated(arg + ' arg in ' + self.feature_name, self.feature_version).use() - return f(*wrapped_args, **wrapped_kwargs) - return wrapped +class FeatureDeprecatedKwargs(FeatureCheckKwargsBase): + feature_check_class = FeatureDeprecated class InterpreterException(mesonlib.MesonException): diff --git a/mesonbuild/mesonlib.py b/mesonbuild/mesonlib.py index c1225893e..5f9b98a53 100644 --- a/mesonbuild/mesonlib.py +++ b/mesonbuild/mesonlib.py @@ -23,8 +23,8 @@ from mesonbuild import mlog have_fcntl = False have_msvcrt = False -# Used to report conflicts between meson_version and new features used -target_version = '' +# {subproject: project_meson_version} +project_meson_versions = {} try: import fcntl diff --git a/mesonbuild/modules/pkgconfig.py b/mesonbuild/modules/pkgconfig.py index 63d1109f3..8684864d2 100644 --- a/mesonbuild/modules/pkgconfig.py +++ b/mesonbuild/modules/pkgconfig.py @@ -313,14 +313,14 @@ class PkgConfigModule(ExtensionModule): 'install_dir', 'extra_cflags', 'variables', 'url', 'd_module_versions'}) def generate(self, state, args, kwargs): if 'variables' in kwargs: - FeatureNew('custom pkgconfig variables', '0.41.0').use() + FeatureNew('custom pkgconfig variables', '0.41.0').use(state.subproject) default_version = state.project_version['version'] default_install_dir = None default_description = None default_name = None mainlib = None if len(args) == 1: - FeatureNew('pkgconfig.generate optional positional argument', '0.46.0').use() + FeatureNew('pkgconfig.generate optional positional argument', '0.46.0').use(state.subproject) mainlib = getattr(args[0], 'held_object', args[0]) if not isinstance(mainlib, (build.StaticLibrary, build.SharedLibrary)): raise mesonlib.MesonException('Pkgconfig_gen first positional argument must be a library object') diff --git a/mesonbuild/optinterpreter.py b/mesonbuild/optinterpreter.py index cd3139be2..94efbcf96 100644 --- a/mesonbuild/optinterpreter.py +++ b/mesonbuild/optinterpreter.py @@ -18,7 +18,6 @@ import functools from . import mparser from . import coredata from . import mesonlib -from .interpreterbase import FeatureNew from . import compilers forbidden_option_names = coredata.get_builtin_options() @@ -94,7 +93,9 @@ def IntegerParser(name, description, kwargs): kwargs['value'], kwargs.get('yield', coredata.default_yielding)) -@FeatureNew('array type option()', '0.44.0') +# FIXME: Cannot use FeatureNew while parsing options because we parse it before +# reading options in project(). See func_project() in interpreter.py +#@FeatureNew('array type option()', '0.44.0') @permitted_kwargs({'value', 'yield', 'choices'}) def string_array_parser(name, description, kwargs): if 'choices' in kwargs: @@ -188,8 +189,11 @@ class OptionInterpreter: raise OptionException('Only calls to option() are allowed in option files.') (posargs, kwargs) = self.reduce_arguments(node.args) - if 'yield' in kwargs: - FeatureNew('option yield', '0.45.0').use() + # FIXME: Cannot use FeatureNew while parsing options because we parse + # it before reading options in project(). See func_project() in + # interpreter.py + #if 'yield' in kwargs: + # FeatureNew('option yield', '0.45.0').use(self.subproject) if 'type' not in kwargs: raise OptionException('Option call missing mandatory "type" keyword argument') diff --git a/run_unittests.py b/run_unittests.py index 480ae2ec0..b6ad20d76 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -2271,6 +2271,21 @@ recommended as it is not supported on some platforms''') arches = set(arches[1:]) self.assertEqual(arches, set(mesonbuild.environment.known_cpu_families)) + def test_feature_check_usage_subprojects(self): + testdir = os.path.join(self.unit_test_dir, '34 featurenew subprojects') + out = self.init(testdir) + # Parent project warns correctly + self.assertRegex(out, "WARNING: Project targetting '>=0.45'.*'0.47.0': dict") + # Subproject warns correctly + self.assertRegex(out, "|WARNING: Project targetting '>=0.40'.*'0.44.0': disabler") + # Subproject has a new-enough meson_version, no warning + self.assertNotRegex(out, "WARNING: Project targetting.*Python") + # Ensure a summary is printed in the subproject and the outer project + self.assertRegex(out, "|WARNING: Project specifies a minimum meson_version '>=0.40'") + self.assertRegex(out, "| * 0.44.0: {'disabler'}") + self.assertRegex(out, "WARNING: Project specifies a minimum meson_version '>=0.45'") + self.assertRegex(out, " * 0.47.0: {'dict'}") + class FailureTests(BasePlatformTests): ''' @@ -2493,6 +2508,11 @@ class FailureTests(BasePlatformTests): ".*WARNING.*Project targetting.*but.*", meson_version='>= 0.47.0') + def test_using_too_recent_feature_dependency(self): + self.assertMesonOutputs("dependency('pcap', required: false)", + ".*WARNING.*Project targetting.*but.*", + meson_version='>= 0.41.0') + class WindowsTests(BasePlatformTests): ''' diff --git a/test cases/common/174 dependency factory/meson.build b/test cases/common/174 dependency factory/meson.build index 54f7d26b1..1b8ed176b 100644 --- a/test cases/common/174 dependency factory/meson.build +++ b/test cases/common/174 dependency factory/meson.build @@ -1,4 +1,4 @@ -project('dependency factory') +project('dependency factory', meson_version : '>=0.40') dep = dependency('gl', method: 'pkg-config', required: false) if dep.found() and dep.type_name() == 'pkgconfig' diff --git a/test cases/unit/34 featurenew subprojects/meson.build b/test cases/unit/34 featurenew subprojects/meson.build new file mode 100644 index 000000000..27898cd79 --- /dev/null +++ b/test cases/unit/34 featurenew subprojects/meson.build @@ -0,0 +1,6 @@ +project('featurenew subproject', meson_version: '>=0.45') + +foo = {} + +subproject('foo') +subproject('bar') diff --git a/test cases/unit/34 featurenew subprojects/subprojects/bar/meson.build b/test cases/unit/34 featurenew subprojects/subprojects/bar/meson.build new file mode 100644 index 000000000..712a125f7 --- /dev/null +++ b/test cases/unit/34 featurenew subprojects/subprojects/bar/meson.build @@ -0,0 +1,3 @@ +project('foo subproject', meson_version: '>=0.46') + +import('python') diff --git a/test cases/unit/34 featurenew subprojects/subprojects/foo/meson.build b/test cases/unit/34 featurenew subprojects/subprojects/foo/meson.build new file mode 100644 index 000000000..0ef4472bd --- /dev/null +++ b/test cases/unit/34 featurenew subprojects/subprojects/foo/meson.build @@ -0,0 +1,3 @@ +project('foo subproject', meson_version: '>=0.40') + +disabler()