aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/syntax-highlighting/vim/syntax/meson.vim1
-rw-r--r--docs/markdown/Contributing.md14
-rw-r--r--docs/markdown/snippets/feature_new.md6
-rw-r--r--mesonbuild/build.py2
-rw-r--r--mesonbuild/dependencies/misc.py5
-rw-r--r--mesonbuild/dependencies/ui.py2
-rw-r--r--mesonbuild/interpreter.py90
-rw-r--r--mesonbuild/interpreterbase.py209
-rw-r--r--mesonbuild/mesonlib.py4
-rw-r--r--mesonbuild/modules/pkgconfig.py4
-rw-r--r--mesonbuild/optinterpreter.py12
-rwxr-xr-xrun_unittests.py20
-rw-r--r--setup.cfg2
-rw-r--r--test cases/common/174 dependency factory/meson.build2
-rw-r--r--test cases/unit/34 featurenew subprojects/meson.build6
-rw-r--r--test cases/unit/34 featurenew subprojects/subprojects/bar/meson.build3
-rw-r--r--test cases/unit/34 featurenew subprojects/subprojects/foo/meson.build3
17 files changed, 232 insertions, 153 deletions
diff --git a/data/syntax-highlighting/vim/syntax/meson.vim b/data/syntax-highlighting/vim/syntax/meson.vim
index d58903e..c83302c 100644
--- a/data/syntax-highlighting/vim/syntax/meson.vim
+++ b/data/syntax-highlighting/vim/syntax/meson.vim
@@ -78,6 +78,7 @@ syn keyword mesonBuiltin
\ custom_target
\ declare_dependency
\ dependency
+ \ disabler
\ environment
\ error
\ executable
diff --git a/docs/markdown/Contributing.md b/docs/markdown/Contributing.md
index 7559c00..e385be2 100644
--- a/docs/markdown/Contributing.md
+++ b/docs/markdown/Contributing.md
@@ -29,6 +29,20 @@ Do not merge head back to your branch. Any merge commits in your pull
request make it not acceptable for merging into master and you must
remove them.
+## Special procedure for new features
+
+Every new feature requires some extra steps, namely:
+
+ - Must include a project test under `test cases/`, or if that's not
+ possible or if the test requires a special environment, it must go
+ into `run_unittests.py`.
+ - Must be registered with the [FeatureChecks framework](Release-notes-for-0.47.0.md#Feature_detection_based_on_meson_version_in_project)
+ that will warn the user if they try to use a new feature while
+ targetting an older meson version.
+ - Needs a release note snippet inside `docs/markdown/snippets/` with
+ a heading and a brief paragraph explaining what the feature does
+ with an example.
+
## Acceptance and merging
The kind of review and acceptance any merge proposal gets depends on
diff --git a/docs/markdown/snippets/feature_new.md b/docs/markdown/snippets/feature_new.md
index 7480634..94fb880 100644
--- a/docs/markdown/snippets/feature_new.md
+++ b/docs/markdown/snippets/feature_new.md
@@ -24,9 +24,9 @@ Project name: featurenew
Project version: undefined
Build machine cpu family: x86_64
Build machine cpu: x86_64
-WARNING: Project targetting '>=0.43' but tried to use feature introduced in '0.44.0': get_unquoted
+WARNING: Project targetting '>=0.43' but tried to use feature introduced in '0.44.0': configuration_data.get_unquoted()
Message: bar
Build targets in project: 0
-Minimum version of features used:
-0.44.0: {'get_unquoted'}
+WARNING: Project specifies a minimum meson_version '>=0.43' which conflicts with:
+ * 0.44.0: {'configuration_data.get_unquoted()'}
```
diff --git a/mesonbuild/build.py b/mesonbuild/build.py
index 2b22521..7d071e0 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 95fe1d8..745dff0 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 324f9fa..197d22c 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 fc6588b..06d80ab 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 '<Holder: {!r}>'.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,
@@ -328,7 +329,7 @@ This will become a hard error in the future''')
return args[1]
raise InterpreterException('Entry %s not in configuration data.' % name)
- @FeatureNew('configuration_data.get_unquoted', '0.44.0')
+ @FeatureNew('configuration_data.get_unquoted()', '0.44.0')
def get_unquoted_method(self, args, kwargs):
if len(args) < 1 or len(args) > 2:
raise InterpreterException('Get method takes one or two arguments.')
@@ -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 0d0e4af..f61ff47 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 c122589..5f9b98a 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 63d1109..8684864 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 cd3139b..94efbcf 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 480ae2e..b6ad20d 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/setup.cfg b/setup.cfg
index 669c932..65a3c64 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -6,6 +6,8 @@ ignore =
E251,
# E261: at least two spaces before inline comment
E261,
+ # E265: block comment should start with '# '
+ E265,
# E501: line too long
E501,
# E302: expected 2 blank lines, found 1
diff --git a/test cases/common/174 dependency factory/meson.build b/test cases/common/174 dependency factory/meson.build
index 54f7d26..1b8ed17 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 0000000..27898cd
--- /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 0000000..712a125
--- /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 0000000..0ef4472
--- /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()