From 2fdedc4d0fc73c509669bf9f89863017e0f0989b Mon Sep 17 00:00:00 2001 From: Xavier Claessens Date: Wed, 19 Dec 2018 20:56:07 -0500 Subject: [PATCH 1/6] Add meson.override_dependency() Similar to meson.override_find_program() but overrides the result of the dependency() function. Also ensure that dependency() always returns the same result when looking for the same dependency, this fixes cases where parts of the project could be using a system library and other parts use the library provided by a subproject. --- docs/markdown/Reference-manual.md | 8 ++- docs/markdown/snippets/override_dependency.md | 59 +++++++++++++++ mesonbuild/build.py | 1 + mesonbuild/interpreter.py | 71 ++++++++++++++----- run_unittests.py | 4 +- .../common/102 subproject subdir/meson.build | 13 ++++ .../subprojects/sub/lib/meson.build | 1 + .../subprojects/sub/meson.build | 2 +- .../5 dependency versions/meson.build | 19 +++-- 9 files changed, 151 insertions(+), 27 deletions(-) create mode 100644 docs/markdown/snippets/override_dependency.md diff --git a/docs/markdown/Reference-manual.md b/docs/markdown/Reference-manual.md index e43ef5731..09d54bead 100644 --- a/docs/markdown/Reference-manual.md +++ b/docs/markdown/Reference-manual.md @@ -1825,12 +1825,18 @@ the following methods. - `override_find_program(progname, program)` [*(Added 0.46.0)*](Release-notes-for-0.46.0.md#can-override-find_program) specifies that whenever `find_program` is used to find a program - named `progname`, Meson should not not look it up on the system but + named `progname`, Meson should not look it up on the system but instead return `program`, which may either be the result of `find_program`, `configure_file` or `executable`. If `program` is an `executable`, it cannot be used during configure. +- `override_dependency(name, dep_object)` [*(Added + 0.54.0)*](Release-notes-for-0.54.0.md#override-dependency) + specifies that whenever `dependency(name, ...)` is used, Meson should not + look it up on the system but instead return `dep_object`, which may either be + the result of `dependency()` or `declare_dependency()`. + - `project_version()` returns the version string specified in `project` function call. diff --git a/docs/markdown/snippets/override_dependency.md b/docs/markdown/snippets/override_dependency.md new file mode 100644 index 000000000..875eef8c2 --- /dev/null +++ b/docs/markdown/snippets/override_dependency.md @@ -0,0 +1,59 @@ +## `dependency()` consistency + +The first time a dependency is found, using `dependency('foo', ...)`, the return +value is now cached. Any subsequent call will return the same value as long as +version requested match, otherwise not-found dependency is returned. This means +that if a system dependency is first found, it won't fallback to a subproject +in a subsequent call any more and will rather return not-found instead if the +system version does not match. Similarly, if the first call returns the subproject +fallback dependency, it will also return the subproject dependency in a subsequent +call even if no fallback is provided. + +For example, if the system has `foo` version 1.0: +```meson +# d2 is set to foo_dep and not the system dependency, even without fallback argument. +d1 = dependency('foo', version : '>=2.0', required : false, + fallback : ['foo', 'foo_dep']) +d2 = dependency('foo', version : '>=1.0', required : false) +``` +```meson +# d2 is not-found because the first call returned the system dependency, but its version is too old for 2nd call. +d1 = dependency('foo', version : '>=1.0', required : false) +d2 = dependency('foo', version : '>=2.0', required : false, + fallback : ['foo', 'foo_dep']) +``` + +## Override `dependency()` + +It is now possible to override the result of `dependency()` to point +to any dependency object you want. The overriding is global and applies to +every subproject from there on. + +For example, this subproject provides 2 libraries with version 2.0: + +```meson +project(..., version : '2.0') + +libfoo = library('foo', ...) +foo_dep = declare_dependency(link_with : libfoo) +meson.override_dependency('foo', foo_dep) + +libbar = library('bar', ...) +bar_dep = declare_dependency(link_with : libbar) +meson.override_dependency('bar', bar_dep) +``` + +Assuming the system has `foo` and `bar` 1.0 installed, and master project does this: +```meson +foo_dep = dependency('foo', version : '>=2.0', fallback : ['foo', 'foo_dep']) +bar_dep = dependency('bar') +``` + +This used to mix system 1.0 version and subproject 2.0 dependencies, but thanks +to the override `bar_dep` is now set to the subproject's version instead. + +Another case this can be useful is to force a subproject to use a specific dependency. +If the subproject does `dependency('foo')` but the main project wants to provide +its own implementation of `foo`, it can for example call +`meson.override_dependency('foo', declare_dependency(...))` before configuring the +subproject. diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 48d21b1a4..33820b4b2 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -141,6 +141,7 @@ class Build: self.test_setup_default_name = None self.find_overrides = {} self.searched_programs = set() # The list of all programs that have been searched for. + self.dependency_overrides = PerMachine({}, {}) def copy(self): other = Build(self.environment) diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py index 9e8897562..4b978c47a 100644 --- a/mesonbuild/interpreter.py +++ b/mesonbuild/interpreter.py @@ -1857,6 +1857,7 @@ class MesonMain(InterpreterObject): 'add_postconf_script': self.add_postconf_script_method, 'add_dist_script': self.add_dist_script_method, 'install_dependency_manifest': self.install_dependency_manifest_method, + 'override_dependency': self.override_dependency_method, 'override_find_program': self.override_find_program_method, 'project_version': self.project_version_method, 'project_license': self.project_license_method, @@ -2013,6 +2014,26 @@ class MesonMain(InterpreterObject): raise InterpreterException('Second argument must be an external program or executable.') self.interpreter.add_find_program_override(name, exe) + @FeatureNew('meson.override_dependency', '0.53.0') + @permittedKwargs({'native'}) + def override_dependency_method(self, args, kwargs): + if len(args) != 2: + raise InterpreterException('Override needs two arguments') + name = args[0] + dep = args[1] + if not isinstance(name, str) or not name: + raise InterpreterException('First argument must be not empty string') + if hasattr(dep, 'held_object'): + dep = dep.held_object + if not isinstance(dep, dependencies.Dependency): + raise InterpreterException('Second argument must be a dependency object') + identifier = dependencies.get_dep_identifier(name, kwargs) + for_machine = self.interpreter.machine_from_native_kwarg(kwargs) + if identifier in self.build.dependency_overrides[for_machine]: + raise InterpreterException('Tried to override dependency "%s" which has already been overridden.' + % name) + self.build.dependency_overrides[for_machine][identifier] = dep + @noPosargs @permittedKwargs({}) def project_version_method(self, args, kwargs): @@ -3218,30 +3239,38 @@ external dependencies (including libraries) must go to "dependencies".''') # Check if we want this as a build-time / build machine or runt-time / # host machine dep. for_machine = self.machine_from_native_kwarg(kwargs) - identifier = dependencies.get_dep_identifier(name, kwargs) - cached_dep = self.coredata.deps[for_machine].get(identifier) - if cached_dep: - if not cached_dep.found(): - mlog.log('Dependency', mlog.bold(name), - 'found:', mlog.red('NO'), mlog.blue('(cached)')) - return identifier, cached_dep + wanted_vers = mesonlib.stringlistify(kwargs.get('version', [])) - # Verify the cached dep version match - wanted_vers = mesonlib.stringlistify(kwargs.get('version', [])) + cached_dep = self.build.dependency_overrides[for_machine].get(identifier) + if cached_dep: found_vers = cached_dep.get_version() - if not wanted_vers or mesonlib.version_compare_many(found_vers, wanted_vers)[0]: - info = [mlog.blue('(cached)')] - if found_vers: - info = [mlog.normal_cyan(found_vers), *info] + if not self.check_version(wanted_vers, found_vers): mlog.log('Dependency', mlog.bold(name), - 'found:', mlog.green('YES'), *info) - return identifier, cached_dep + 'found:', mlog.red('NO'), + 'found', mlog.normal_cyan(found_vers), 'but need:', + mlog.bold(', '.join(["'{}'".format(e) for e in wanted_vers])), + mlog.blue('(cached)')) + return identifier, NotFoundDependency(self.environment) + else: + cached_dep = self.coredata.deps[for_machine].get(identifier) + if cached_dep: + found_vers = cached_dep.get_version() + if not self.check_version(wanted_vers, found_vers): + return identifier, None + + if cached_dep: + info = [mlog.blue('(cached)')] + if found_vers: + info = [mlog.normal_cyan(found_vers), *info] + mlog.log('Dependency', mlog.bold(name), + 'found:', mlog.green('YES'), *info) + return identifier, cached_dep return identifier, None @staticmethod - def check_subproject_version(wanted, found): + def check_version(wanted, found): if not wanted: return True if found == 'undefined' or not mesonlib.version_compare_many(found, wanted)[0]: @@ -3278,7 +3307,7 @@ external dependencies (including libraries) must go to "dependencies".''') return dep found = dep.held_object.get_version() - if not self.check_subproject_version(wanted, found): + if not self.check_version(wanted, found): if required: raise DependencyException('Version {} of subproject dependency {} already ' 'cached, requested incompatible version {} for ' @@ -3330,6 +3359,14 @@ external dependencies (including libraries) must go to "dependencies".''') raise if not d.found() and not_found_message: self.message_impl([not_found_message]) + self.message_impl([not_found_message]) + # Override this dependency to have consistent results in subsequent + # dependency lookups. + if name and d.found(): + for_machine = self.machine_from_native_kwarg(kwargs) + identifier = dependencies.get_dep_identifier(name, kwargs) + if identifier not in self.build.dependency_overrides[for_machine]: + self.build.dependency_overrides[for_machine][identifier] = d.held_object return d def dependency_impl(self, name, display_name, kwargs): diff --git a/run_unittests.py b/run_unittests.py index 658da3703..ee5a7131d 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -3980,7 +3980,7 @@ recommended as it is not supported on some platforms''') { 'descriptive_name': 'sub', 'name': 'sub', - 'version': 'undefined' + 'version': '1.0' } ] } @@ -4555,7 +4555,7 @@ class FailureTests(BasePlatformTests): raise unittest.SkipTest('zlib not found with pkg-config') a = (("dependency('zlib', method : 'fail')", "'fail' is invalid"), ("dependency('zlib', static : '1')", "[Ss]tatic.*boolean"), - ("dependency('zlib', version : 1)", "[Vv]ersion.*string or list"), + ("dependency('zlib', version : 1)", "Item must be a list or one of "), ("dependency('zlib', required : 1)", "[Rr]equired.*boolean"), ("dependency('zlib', method : 1)", "[Mm]ethod.*string"), ("dependency('zlibfail')", self.dnf),) diff --git a/test cases/common/102 subproject subdir/meson.build b/test cases/common/102 subproject subdir/meson.build index ec9fad123..fc54db18f 100644 --- a/test cases/common/102 subproject subdir/meson.build +++ b/test cases/common/102 subproject subdir/meson.build @@ -4,3 +4,16 @@ libSub = dependency('sub', fallback: ['sub', 'libSub']) exe = executable('prog', 'prog.c', dependencies: libSub) test('subproject subdir', exe) + +# Verify the subproject has placed dependency override. +dependency('sub-1.0') + +# Verify we can now take 'sub' dependency without fallback, but only version 1.0. +dependency('sub') +d = dependency('sub', version : '>=2.0', required : false) +assert(not d.found(), 'version should not match') + +# Verify that not-found does not get cached, we can still fallback afterward. +dependency('sub2', required : false) +d = dependency('sub2', fallback: ['sub', 'libSub']) +assert(d.found(), 'Should fallback even if a previous call returned not-found') diff --git a/test cases/common/102 subproject subdir/subprojects/sub/lib/meson.build b/test cases/common/102 subproject subdir/subprojects/sub/lib/meson.build index 731d22bfb..53233ab4e 100644 --- a/test cases/common/102 subproject subdir/subprojects/sub/lib/meson.build +++ b/test cases/common/102 subproject subdir/subprojects/sub/lib/meson.build @@ -1,2 +1,3 @@ lib = static_library('sub', 'sub.c') libSub = declare_dependency(include_directories: include_directories('.'), link_with: lib) +meson.override_dependency('sub-1.0', libSub) diff --git a/test cases/common/102 subproject subdir/subprojects/sub/meson.build b/test cases/common/102 subproject subdir/subprojects/sub/meson.build index bf69c25b7..d8c4dce70 100644 --- a/test cases/common/102 subproject subdir/subprojects/sub/meson.build +++ b/test cases/common/102 subproject subdir/subprojects/sub/meson.build @@ -1,2 +1,2 @@ -project('sub', 'c') +project('sub', 'c', version : '1.0') subdir('lib') diff --git a/test cases/linuxlike/5 dependency versions/meson.build b/test cases/linuxlike/5 dependency versions/meson.build index 087db5ff7..cb58a64d7 100644 --- a/test cases/linuxlike/5 dependency versions/meson.build +++ b/test cases/linuxlike/5 dependency versions/meson.build @@ -38,32 +38,32 @@ somelibver = dependency('somelib', fallback : ['somelibnover', 'some_dep']) assert(somelibver.type_name() == 'internal', 'somelibver should be of type "internal", not ' + somelibver.type_name()) # Find an internal dependency again with the same name and a specific version -somelib = dependency('somelib', +somelib = dependency('somelib2', version : '== 0.1', fallback : ['somelib', 'some_dep']) # Find an internal dependency again even if required = false -somelib_reqfalse = dependency('somelib', +somelib_reqfalse = dependency('somelib3', required: false, fallback : ['somelib', 'some_dep']) assert(somelib_reqfalse.found(), 'somelib should have been found') # Find an internal dependency again with the same name and incompatible version -somelibver = dependency('somelib', +somelibver = dependency('somelib4', version : '>= 0.3', fallback : ['somelibver', 'some_dep']) # Find an internal dependency again with impossible multi-version -somelibver = dependency('somelib', +somelibver = dependency('somelib5', version : ['>= 0.3', '<0.3'], required : false, fallback : ['somelibver', 'some_dep']) assert(not somelibver.found(), 'Dependency should not be found') # Find somelib again, but with a fallback that will fail because subproject does not exist -somelibfail = dependency('somelib', +somelibfail = dependency('somelib6', version : '>= 0.2', required : false, fallback : ['somelibfail', 'some_dep']) assert(somelibfail.found() == false, 'somelibfail found via wrong fallback') # Find somelib again, but with a fallback that will fail because dependency does not exist -somefail_dep = dependency('somelib', +somefail_dep = dependency('somelib7', version : '>= 0.2', required : false, fallback : ['somelib', 'somefail_dep']) @@ -79,6 +79,13 @@ fakezlib_dep = dependency('fakezlib', fallback : ['somelib', 'fakezlib_dep']) assert(fakezlib_dep.type_name() == 'internal', 'fakezlib_dep should be of type "internal", not ' + fakezlib_dep.type_name()) +# Verify that once we got a system dependency, we won't fallback if a newer +# version is requested. +d = dependency('zlib', version: '>= 999', + fallback : ['somelib', 'some_dep'], + required: false) +assert(not d.found(), 'version should not match and it should not fallback') + # Check that you can find a dependency by not specifying a version after not # finding it by specifying a version. We add `static: true` here so that the # previously cached zlib dependencies don't get checked. From 943e9368f7198b6c2b069ad024ee798037f3c35e Mon Sep 17 00:00:00 2001 From: Xavier Claessens Date: Mon, 25 Nov 2019 14:29:05 -0500 Subject: [PATCH 2/6] Simplify dependency() fallback Now that subprojects can override the dependency name, there is no need to provide a variable name for the fallback any more. --- docs/markdown/Reference-manual.md | 4 ++ docs/markdown/snippets/override_dependency.md | 6 +++ mesonbuild/interpreter.py | 38 ++++++++++++------- run_unittests.py | 10 ++++- .../common/102 subproject subdir/meson.build | 4 ++ .../subprojects/sub_novar/meson.build | 3 ++ 6 files changed, 50 insertions(+), 15 deletions(-) create mode 100644 test cases/common/102 subproject subdir/subprojects/sub_novar/meson.build diff --git a/docs/markdown/Reference-manual.md b/docs/markdown/Reference-manual.md index 09d54bead..b4d233342 100644 --- a/docs/markdown/Reference-manual.md +++ b/docs/markdown/Reference-manual.md @@ -445,6 +445,10 @@ arguments: [`dependency()`](#dependency), etc. Note that this means the fallback dependency may be a not-found dependency, in which case the value of the `required:` kwarg will be obeyed. + *Since 0.54.0* `'subproj_dep'` argument can be omitted in the case the + subproject used `meson.override_dependency('dependency_name', subproj_dep)`. + In that case, the `fallback` keyword argument can be a single string instead + of a list of 2 strings. - `language` *(added 0.42.0)* defines what language-specific dependency to find if it's available for multiple languages. - `method` defines the way the dependency is detected, the default is diff --git a/docs/markdown/snippets/override_dependency.md b/docs/markdown/snippets/override_dependency.md index 875eef8c2..ca420bc84 100644 --- a/docs/markdown/snippets/override_dependency.md +++ b/docs/markdown/snippets/override_dependency.md @@ -57,3 +57,9 @@ If the subproject does `dependency('foo')` but the main project wants to provide its own implementation of `foo`, it can for example call `meson.override_dependency('foo', declare_dependency(...))` before configuring the subproject. + +## Simplified `dependency()` fallback + +In the case a subproject `foo` calls `meson.override_dependency('foo-2.0', foo_dep)`, +the parent project can omit the dependency variable name in fallback keyword +argument: `dependency('foo-2.0', fallback : 'foo')`. diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py index 4b978c47a..420114149 100644 --- a/mesonbuild/interpreter.py +++ b/mesonbuild/interpreter.py @@ -3280,10 +3280,24 @@ external dependencies (including libraries) must go to "dependencies".''') def notfound_dependency(self): return DependencyHolder(NotFoundDependency(self.environment), self.subproject) - def get_subproject_dep(self, display_name, dirname, varname, kwargs): + def get_subproject_dep(self, name, display_name, dirname, varname, kwargs): + required = kwargs.get('required', True) + wanted = mesonlib.stringlistify(kwargs.get('version', [])) + subproj_path = os.path.join(self.subproject_dir, dirname) dep = self.notfound_dependency() try: subproject = self.subprojects[dirname] + if varname is None: + # Assuming the subproject overriden the dependency we want + _, cached_dep = self._find_cached_dep(name, kwargs) + if cached_dep: + if required and not cached_dep.found(): + m = 'Dependency {!r} is not satisfied' + raise DependencyException(m.format(display_name)) + return DependencyHolder(cached_dep, self.subproject) + else: + m = 'Subproject {} did not override dependency {}' + raise DependencyException(m.format(subproj_path, display_name)) if subproject.found(): dep = self.subprojects[dirname].get_variable_method([varname], {}) except InvalidArguments: @@ -3293,10 +3307,6 @@ external dependencies (including libraries) must go to "dependencies".''') raise InvalidCode('Fetched variable {!r} in the subproject {!r} is ' 'not a dependency object.'.format(varname, dirname)) - required = kwargs.get('required', True) - wanted = mesonlib.stringlistify(kwargs.get('version', [])) - subproj_path = os.path.join(self.subproject_dir, dirname) - if not dep.found(): if required: raise DependencyException('Could not find dependency {} in subproject {}' @@ -3400,7 +3410,7 @@ external dependencies (including libraries) must go to "dependencies".''') if has_fallback: dirname, varname = self.get_subproject_infos(kwargs) if dirname in self.subprojects: - return self.get_subproject_dep(name, dirname, varname, kwargs) + return self.get_subproject_dep(name, display_name, dirname, varname, kwargs) wrap_mode = self.coredata.get_builtin_option('wrap_mode') forcefallback = wrap_mode == WrapMode.forcefallback and has_fallback @@ -3420,7 +3430,7 @@ external dependencies (including libraries) must go to "dependencies".''') return DependencyHolder(dep, self.subproject) if has_fallback: - return self.dependency_fallback(display_name, kwargs) + return self.dependency_fallback(name, display_name, kwargs) return self.notfound_dependency() @@ -3448,13 +3458,15 @@ external dependencies (including libraries) must go to "dependencies".''') mlog.warning(*message, location=self.current_node) def get_subproject_infos(self, kwargs): - fbinfo = kwargs['fallback'] - check_stringlist(fbinfo) - if len(fbinfo) != 2: - raise InterpreterException('Fallback info must have exactly two items.') + fbinfo = mesonlib.stringlistify(kwargs['fallback']) + if len(fbinfo) == 1: + FeatureNew('Fallback without variable name', '0.53.0').use(self.subproject) + return fbinfo[0], None + elif len(fbinfo) != 2: + raise InterpreterException('Fallback info must have one or two items.') return fbinfo - def dependency_fallback(self, display_name, kwargs): + def dependency_fallback(self, name, display_name, kwargs): required = kwargs.get('required', True) if self.coredata.get_builtin_option('wrap_mode') == WrapMode.nofallback: mlog.log('Not looking for a fallback subproject for the dependency', @@ -3476,7 +3488,7 @@ external dependencies (including libraries) must go to "dependencies".''') 'required': required, } self.do_subproject(dirname, 'meson', sp_kwargs) - return self.get_subproject_dep(display_name, dirname, varname, kwargs) + return self.get_subproject_dep(name, display_name, dirname, varname, kwargs) @FeatureNewKwargs('executable', '0.42.0', ['implib']) @permittedKwargs(permitted_kwargs['executable']) diff --git a/run_unittests.py b/run_unittests.py index ee5a7131d..8479e3491 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -3981,10 +3981,16 @@ recommended as it is not supported on some platforms''') 'descriptive_name': 'sub', 'name': 'sub', 'version': '1.0' - } + }, + { + 'descriptive_name': 'sub-novar', + 'name': 'sub_novar', + 'version': '1.0', + }, ] } - self.assertDictEqual(res, expected) + res['subprojects'] = sorted(res['subprojects'], key=lambda i: i['name']) + self.assertDictEqual(expected, res) def test_introspection_target_subproject(self): testdir = os.path.join(self.common_test_dir, '45 subproject') diff --git a/test cases/common/102 subproject subdir/meson.build b/test cases/common/102 subproject subdir/meson.build index fc54db18f..3004f570b 100644 --- a/test cases/common/102 subproject subdir/meson.build +++ b/test cases/common/102 subproject subdir/meson.build @@ -17,3 +17,7 @@ assert(not d.found(), 'version should not match') dependency('sub2', required : false) d = dependency('sub2', fallback: ['sub', 'libSub']) assert(d.found(), 'Should fallback even if a previous call returned not-found') + +# Verify we can get a fallback dependency without specifying the variable name, +# because the subproject overridden 'sub-novar'. +dependency('sub-novar', fallback : 'sub_novar') diff --git a/test cases/common/102 subproject subdir/subprojects/sub_novar/meson.build b/test cases/common/102 subproject subdir/subprojects/sub_novar/meson.build new file mode 100644 index 000000000..0126b5fba --- /dev/null +++ b/test cases/common/102 subproject subdir/subprojects/sub_novar/meson.build @@ -0,0 +1,3 @@ +project('sub-novar', 'c', version : '1.0') + +meson.override_dependency('sub-novar', declare_dependency()) From 141401c11d8900775a15a91ee6ca4dbeac2dfe87 Mon Sep 17 00:00:00 2001 From: Xavier Claessens Date: Mon, 25 Nov 2019 14:44:40 -0500 Subject: [PATCH 3/6] Allow override_dependency() with a not-found dep --- mesonbuild/interpreter.py | 7 +++++++ test cases/common/102 subproject subdir/meson.build | 4 ++++ .../subprojects/sub_novar/meson.build | 1 + 3 files changed, 12 insertions(+) diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py index 420114149..b3789c3b1 100644 --- a/mesonbuild/interpreter.py +++ b/mesonbuild/interpreter.py @@ -3244,6 +3244,13 @@ external dependencies (including libraries) must go to "dependencies".''') cached_dep = self.build.dependency_overrides[for_machine].get(identifier) if cached_dep: + # We don't implicitly override not-found dependencies, but user could + # have explicitly called meson.override_dependency() with a not-found + # dep. + if not cached_dep.found(): + mlog.log('Dependency', mlog.bold(name), + 'found:', mlog.red('NO'), mlog.blue('(cached)')) + return identifier, cached_dep found_vers = cached_dep.get_version() if not self.check_version(wanted_vers, found_vers): mlog.log('Dependency', mlog.bold(name), diff --git a/test cases/common/102 subproject subdir/meson.build b/test cases/common/102 subproject subdir/meson.build index 3004f570b..8299a371f 100644 --- a/test cases/common/102 subproject subdir/meson.build +++ b/test cases/common/102 subproject subdir/meson.build @@ -21,3 +21,7 @@ assert(d.found(), 'Should fallback even if a previous call returned not-found') # Verify we can get a fallback dependency without specifying the variable name, # because the subproject overridden 'sub-novar'. dependency('sub-novar', fallback : 'sub_novar') + +# Verify a subproject can force a dependency to be not-found +d = dependency('sub-notfound', fallback : 'sub_novar', required : false) +assert(not d.found(), 'Dependency should be not-found') diff --git a/test cases/common/102 subproject subdir/subprojects/sub_novar/meson.build b/test cases/common/102 subproject subdir/subprojects/sub_novar/meson.build index 0126b5fba..6450a1032 100644 --- a/test cases/common/102 subproject subdir/subprojects/sub_novar/meson.build +++ b/test cases/common/102 subproject subdir/subprojects/sub_novar/meson.build @@ -1,3 +1,4 @@ project('sub-novar', 'c', version : '1.0') meson.override_dependency('sub-novar', declare_dependency()) +meson.override_dependency('sub-notfound', dependency('', required : false)) From 8edc6d655d6069dd5e6e7b531701086d774d7529 Mon Sep 17 00:00:00 2001 From: Xavier Claessens Date: Mon, 25 Nov 2019 20:16:54 -0500 Subject: [PATCH 4/6] Improve logged messages for overriden dependencies --- docs/markdown/Reference-manual.md | 5 ++++- mesonbuild/build.py | 6 ++++++ mesonbuild/interpreter.py | 28 +++++++++++++++++----------- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/docs/markdown/Reference-manual.md b/docs/markdown/Reference-manual.md index b4d233342..9bb791117 100644 --- a/docs/markdown/Reference-manual.md +++ b/docs/markdown/Reference-manual.md @@ -1839,7 +1839,10 @@ the following methods. 0.54.0)*](Release-notes-for-0.54.0.md#override-dependency) specifies that whenever `dependency(name, ...)` is used, Meson should not look it up on the system but instead return `dep_object`, which may either be - the result of `dependency()` or `declare_dependency()`. + the result of `dependency()` or `declare_dependency()`. It takes optional + `native` keyword arguments. Doing this in a subproject allows the parent + project to retrieve the dependency without having to know the dependency + variable name: `dependency(name, fallback : subproject_name)`. - `project_version()` returns the version string specified in `project` function call. diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 33820b4b2..2b5c0ea59 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -106,6 +106,12 @@ def get_target_macos_dylib_install_name(ld) -> str: class InvalidArguments(MesonException): pass +class DependencyOverride: + def __init__(self, dep, node, explicit=True): + self.dep = dep + self.node = node + self.explicit = explicit + class Build: """A class that holds the status of one build including all dependencies and so on. diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py index b3789c3b1..c247e6303 100644 --- a/mesonbuild/interpreter.py +++ b/mesonbuild/interpreter.py @@ -2022,17 +2022,20 @@ class MesonMain(InterpreterObject): name = args[0] dep = args[1] if not isinstance(name, str) or not name: - raise InterpreterException('First argument must be not empty string') + raise InterpreterException('First argument must be a string and cannot be empty') if hasattr(dep, 'held_object'): dep = dep.held_object if not isinstance(dep, dependencies.Dependency): raise InterpreterException('Second argument must be a dependency object') identifier = dependencies.get_dep_identifier(name, kwargs) for_machine = self.interpreter.machine_from_native_kwarg(kwargs) - if identifier in self.build.dependency_overrides[for_machine]: - raise InterpreterException('Tried to override dependency "%s" which has already been overridden.' - % name) - self.build.dependency_overrides[for_machine][identifier] = dep + override = self.build.dependency_overrides[for_machine].get(identifier) + if override: + m = 'Tried to override dependency {!r} which has already been resolved or overridden at {}' + location = mlog.get_error_location_string(override.node.filename, override.node.lineno) + raise InterpreterException(m.format(name, location)) + self.build.dependency_overrides[for_machine][identifier] = \ + build.DependencyOverride(dep, self.interpreter.current_node) @noPosargs @permittedKwargs({}) @@ -3242,14 +3245,16 @@ external dependencies (including libraries) must go to "dependencies".''') identifier = dependencies.get_dep_identifier(name, kwargs) wanted_vers = mesonlib.stringlistify(kwargs.get('version', [])) - cached_dep = self.build.dependency_overrides[for_machine].get(identifier) - if cached_dep: + override = self.build.dependency_overrides[for_machine].get(identifier) + if override: + info = [mlog.blue('(overridden)' if override.explicit else '(cached)')] + cached_dep = override.dep # We don't implicitly override not-found dependencies, but user could # have explicitly called meson.override_dependency() with a not-found # dep. if not cached_dep.found(): mlog.log('Dependency', mlog.bold(name), - 'found:', mlog.red('NO'), mlog.blue('(cached)')) + 'found:', mlog.red('NO'), *info) return identifier, cached_dep found_vers = cached_dep.get_version() if not self.check_version(wanted_vers, found_vers): @@ -3257,9 +3262,10 @@ external dependencies (including libraries) must go to "dependencies".''') 'found:', mlog.red('NO'), 'found', mlog.normal_cyan(found_vers), 'but need:', mlog.bold(', '.join(["'{}'".format(e) for e in wanted_vers])), - mlog.blue('(cached)')) + *info) return identifier, NotFoundDependency(self.environment) else: + info = [mlog.blue('(cached)')] cached_dep = self.coredata.deps[for_machine].get(identifier) if cached_dep: found_vers = cached_dep.get_version() @@ -3267,7 +3273,6 @@ external dependencies (including libraries) must go to "dependencies".''') return identifier, None if cached_dep: - info = [mlog.blue('(cached)')] if found_vers: info = [mlog.normal_cyan(found_vers), *info] mlog.log('Dependency', mlog.bold(name), @@ -3383,7 +3388,8 @@ external dependencies (including libraries) must go to "dependencies".''') for_machine = self.machine_from_native_kwarg(kwargs) identifier = dependencies.get_dep_identifier(name, kwargs) if identifier not in self.build.dependency_overrides[for_machine]: - self.build.dependency_overrides[for_machine][identifier] = d.held_object + self.build.dependency_overrides[for_machine][identifier] = \ + build.DependencyOverride(d.held_object, node, explicit=False) return d def dependency_impl(self, name, display_name, kwargs): From d40c26e5fd642724155da813a4f2de0c05b52449 Mon Sep 17 00:00:00 2001 From: Xavier Claessens Date: Sun, 8 Dec 2019 17:24:49 -0500 Subject: [PATCH 5/6] override_dependency: Add failure tests --- run_unittests.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/run_unittests.py b/run_unittests.py index 8479e3491..9ebece6a5 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -4778,6 +4778,17 @@ class FailureTests(BasePlatformTests): self.assertMesonOutputs("warning('Array:', ['a', 'b'])", r"WARNING:.* Array: \['a', 'b'\]") + def test_override_dependency_twice(self): + self.assertMesonRaises("meson.override_dependency('foo', declare_dependency())\n" + + "meson.override_dependency('foo', declare_dependency())", + """Tried to override dependency 'foo' which has already been resolved or overridden""") + + @unittest.skipIf(is_windows(), 'zlib is not available on Windows') + def test_override_resolved_dependency(self): + self.assertMesonRaises("dependency('zlib')\n" + + "meson.override_dependency('zlib', declare_dependency())", + """Tried to override dependency 'zlib' which has already been resolved or overridden""") + @unittest.skipUnless(is_windows() or is_cygwin(), "requires Windows (or Windows via Cygwin)") class WindowsTests(BasePlatformTests): ''' From 823c83b2696b517289dbfa0f524fd938bc4001e2 Mon Sep 17 00:00:00 2001 From: Xavier Claessens Date: Sun, 15 Dec 2019 11:22:52 -0500 Subject: [PATCH 6/6] dependency: Verify fallback variable consistency This change made `5 dependency versions` unit test fail because now once a subproject has been configured, the fallback variable is checked to be consistent. So it has to use new subproject because 'somesub' was already configured by previous tests. --- mesonbuild/interpreter.py | 16 ++++++++++++++-- .../failing/100 fallback consistency/meson.build | 7 +++++++ .../subprojects/sub/meson.build | 5 +++++ .../failing/99 fallback consistency/meson.build | 4 ++++ .../subprojects/sub/meson.build | 5 +++++ .../linuxlike/5 dependency versions/meson.build | 6 +++--- .../subprojects/fakezlib/meson.build | 3 +++ 7 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 test cases/failing/100 fallback consistency/meson.build create mode 100644 test cases/failing/100 fallback consistency/subprojects/sub/meson.build create mode 100644 test cases/failing/99 fallback consistency/meson.build create mode 100644 test cases/failing/99 fallback consistency/subprojects/sub/meson.build create mode 100644 test cases/linuxlike/5 dependency versions/subprojects/fakezlib/meson.build diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py index c247e6303..77d64c4bc 100644 --- a/mesonbuild/interpreter.py +++ b/mesonbuild/interpreter.py @@ -3292,6 +3292,15 @@ external dependencies (including libraries) must go to "dependencies".''') def notfound_dependency(self): return DependencyHolder(NotFoundDependency(self.environment), self.subproject) + def verify_fallback_consistency(self, dirname, varname, cached_dep): + subi = self.subprojects.get(dirname) + if not cached_dep or not varname or not subi or not cached_dep.found(): + return + dep = subi.get_variable_method([varname], {}) + if dep.held_object != cached_dep: + m = 'Inconsistency: Subproject has overriden the dependency with another variable than {!r}' + raise DependencyException(m.format(varname)) + def get_subproject_dep(self, name, display_name, dirname, varname, kwargs): required = kwargs.get('required', True) wanted = mesonlib.stringlistify(kwargs.get('version', [])) @@ -3299,9 +3308,9 @@ external dependencies (including libraries) must go to "dependencies".''') dep = self.notfound_dependency() try: subproject = self.subprojects[dirname] + _, cached_dep = self._find_cached_dep(name, kwargs) if varname is None: # Assuming the subproject overriden the dependency we want - _, cached_dep = self._find_cached_dep(name, kwargs) if cached_dep: if required and not cached_dep.found(): m = 'Dependency {!r} is not satisfied' @@ -3311,6 +3320,7 @@ external dependencies (including libraries) must go to "dependencies".''') m = 'Subproject {} did not override dependency {}' raise DependencyException(m.format(subproj_path, display_name)) if subproject.found(): + self.verify_fallback_consistency(dirname, varname, cached_dep) dep = self.subprojects[dirname].get_variable_method([varname], {}) except InvalidArguments: pass @@ -3413,6 +3423,9 @@ external dependencies (including libraries) must go to "dependencies".''') identifier, cached_dep = self._find_cached_dep(name, kwargs) if cached_dep: + if has_fallback: + dirname, varname = self.get_subproject_infos(kwargs) + self.verify_fallback_consistency(dirname, varname, cached_dep) if required and not cached_dep.found(): m = 'Dependency {!r} was already checked and was not found' raise DependencyException(m.format(display_name)) @@ -3431,7 +3444,6 @@ external dependencies (including libraries) must go to "dependencies".''') self._handle_featurenew_dependencies(name) kwargs['required'] = required and not has_fallback dep = dependencies.find_external_dependency(name, self.environment, kwargs) - kwargs['required'] = required # Only store found-deps in the cache # Never add fallback deps to self.coredata.deps since we diff --git a/test cases/failing/100 fallback consistency/meson.build b/test cases/failing/100 fallback consistency/meson.build new file mode 100644 index 000000000..ad2b22604 --- /dev/null +++ b/test cases/failing/100 fallback consistency/meson.build @@ -0,0 +1,7 @@ +project('proj', 'c') + +# The first call succeed and cache the value of 'sub' dependency. The 2nd call +# should return the cached value, but still verify the fallback variable is +# consistent. +dependency('sub', fallback : ['sub', 'dep1']) +dependency('sub', fallback : ['sub', 'dep2']) diff --git a/test cases/failing/100 fallback consistency/subprojects/sub/meson.build b/test cases/failing/100 fallback consistency/subprojects/sub/meson.build new file mode 100644 index 000000000..12a6570df --- /dev/null +++ b/test cases/failing/100 fallback consistency/subprojects/sub/meson.build @@ -0,0 +1,5 @@ +project('proj', 'c') + +dep1 = declare_dependency() +dep2 = declare_dependency() +meson.override_dependency('sub', dep1) diff --git a/test cases/failing/99 fallback consistency/meson.build b/test cases/failing/99 fallback consistency/meson.build new file mode 100644 index 000000000..362459c37 --- /dev/null +++ b/test cases/failing/99 fallback consistency/meson.build @@ -0,0 +1,4 @@ +project('proj', 'c') + +# Subproject overrides 'sub' with another variable than dep2. This should fail. +dependency('sub', fallback : ['sub', 'dep2']) diff --git a/test cases/failing/99 fallback consistency/subprojects/sub/meson.build b/test cases/failing/99 fallback consistency/subprojects/sub/meson.build new file mode 100644 index 000000000..12a6570df --- /dev/null +++ b/test cases/failing/99 fallback consistency/subprojects/sub/meson.build @@ -0,0 +1,5 @@ +project('proj', 'c') + +dep1 = declare_dependency() +dep2 = declare_dependency() +meson.override_dependency('sub', dep1) diff --git a/test cases/linuxlike/5 dependency versions/meson.build b/test cases/linuxlike/5 dependency versions/meson.build index cb58a64d7..94f424deb 100644 --- a/test cases/linuxlike/5 dependency versions/meson.build +++ b/test cases/linuxlike/5 dependency versions/meson.build @@ -71,18 +71,18 @@ assert(somefail_dep.found() == false, 'somefail_dep found via wrong fallback') # Fallback should only be used if the primary was not found fallbackzlib_dep = dependency('zlib', - fallback : ['somelib', 'fakezlib_dep']) + fallback : ['fakezlib', 'fakezlib_dep']) assert(fallbackzlib_dep.type_name() == 'pkgconfig', 'fallbackzlib_dep should be of type "pkgconfig", not ' + fallbackzlib_dep.type_name()) # Check that the above dependency was pkgconfig because the fallback wasn't # checked, not because the fallback didn't work fakezlib_dep = dependency('fakezlib', - fallback : ['somelib', 'fakezlib_dep']) + fallback : ['fakezlib', 'fakezlib_dep']) assert(fakezlib_dep.type_name() == 'internal', 'fakezlib_dep should be of type "internal", not ' + fakezlib_dep.type_name()) # Verify that once we got a system dependency, we won't fallback if a newer # version is requested. d = dependency('zlib', version: '>= 999', - fallback : ['somelib', 'some_dep'], + fallback : ['donotexist', 'fakezlib_dep'], required: false) assert(not d.found(), 'version should not match and it should not fallback') diff --git a/test cases/linuxlike/5 dependency versions/subprojects/fakezlib/meson.build b/test cases/linuxlike/5 dependency versions/subprojects/fakezlib/meson.build new file mode 100644 index 000000000..5a077638e --- /dev/null +++ b/test cases/linuxlike/5 dependency versions/subprojects/fakezlib/meson.build @@ -0,0 +1,3 @@ +project('some', 'c', version : '0.1') + +fakezlib_dep = declare_dependency()