Merge pull request #6968 from xclaesse/auto-promote

Merge wraps from subprojects into wraps from main project
pull/7852/head
Jussi Pakkanen 4 years ago committed by GitHub
commit 30d78f3981
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      docs/markdown/Builtin-options.md
  2. 7
      docs/markdown/Subprojects.md
  3. 21
      docs/markdown/Using-wraptool.md
  4. 13
      docs/markdown/snippets/subsubproject.md
  5. 277
      mesonbuild/interpreter.py
  6. 2
      mesonbuild/msubprojects.py
  7. 2
      mesonbuild/wrap/__init__.py
  8. 86
      mesonbuild/wrap/wrap.py
  9. 66
      run_unittests.py
  10. 11
      test cases/common/102 subproject subdir/meson.build
  11. 3
      test cases/common/102 subproject subdir/subprojects/sub_implicit/subprojects/subsub/meson.build
  12. BIN
      test cases/common/102 subproject subdir/subprojects/sub_implicit/subprojects/subsub/subprojects/packagefiles/subsubsub-1.0.zip
  13. 4
      test cases/common/102 subproject subdir/subprojects/sub_implicit/subprojects/subsub/subprojects/subsubsub.wrap
  14. 2
      test cases/failing/16 extract from subproject/test.json

@ -79,7 +79,7 @@ for details.
| unity_size {>=2} | 4 | Unity file block size | no | no |
| warning_level {0, 1, 2, 3} | 1 | Set the warning level. From 0 = none to 3 = highest | no | yes |
| werror | false | Treat warnings as errors | no | yes |
| wrap_mode {default, nofallback,<br>nodownload, forcefallback} | default | Wrap mode to use | no | no |
| wrap_mode {default, nofallback,<br>nodownload, forcefallback, nopromote} | default | Wrap mode to use | no | no |
| force_fallback_for | [] | Force fallback for those dependencies | no | no |
<a name="build-type-options"></a>

@ -258,6 +258,13 @@ the following command-line options:
`glib-2.0` must also be forced to fallback, in this case with
`--force-fallback-for=glib,gsteamer`.
* **--wrap-mode=nopromote**
*Since 0.56.0* Meson will automatically use wrap files found in subprojects
and copy them into the main project. That new behavior can be disabled by
passing `--wrap-mode=nopromote`. In that case only wraps found in the main
project will be used.
## `meson subprojects` command
*Since 0.49.0*

@ -76,24 +76,3 @@ straightforward:
Wraptool can do other things besides these. Documentation for these
can be found in the command line help, which can be accessed by
`meson wrap --help`.
## Promoting dependencies
Meson will only search for subprojects from the top level
`subprojects` directory. If you have subprojects that themselves have
subprojects, you must transfer them to the top level. This can be done
by going to your source root and issuing a promotion command.
meson wrap promote projname
This will cause Meson to go through your entire project tree, find an
embedded subproject and copy it to the top level.
If there are multiple embedded copies of a subproject, Meson will not
try to guess which one you want. Instead it will print all the
possibilities. You can then manually select which one to promote by
writing it out fully.
meson wrap promote subprojects/s1/subprojects/projname
This functionality was added in Meson release 0.45.0.

@ -0,0 +1,13 @@
## Wraps from subprojects are automatically promoted
It is not required to promote wrap files for subprojects into the main project
any more. When configuring a subproject, meson will look for any wrap file or
directory in the subproject's `subprojects/` directory and add them into the
global list of available subprojects, to be used by any future `subproject()`
call or `dependency()` fallback. If a subproject with the same name already exists,
the new wrap file or directory is ignored. That means that the main project can
always override any subproject's wrap files by providing their own, it also means
the ordering in which subprojects are configured matters, if 2 subprojects provide
foo.wrap only the one from the first subproject to be configured will be used.
This new behavior can be disabled by passing `--wrap-mode=nopromote`.

@ -1010,15 +1010,14 @@ class Test(InterpreterObject):
class SubprojectHolder(InterpreterObject, ObjectHolder):
def __init__(self, subinterpreter, subproject_dir, name, warnings=0, disabled_feature=None,
def __init__(self, subinterpreter, subdir, warnings=0, disabled_feature=None,
exception=None):
InterpreterObject.__init__(self)
ObjectHolder.__init__(self, subinterpreter)
self.name = name
self.warnings = warnings
self.disabled_feature = disabled_feature
self.exception = exception
self.subproject_dir = subproject_dir
self.subdir = PurePath(subdir).as_posix()
self.methods.update({'get_variable': self.get_variable_method,
'found': self.found_method,
})
@ -1037,8 +1036,7 @@ class SubprojectHolder(InterpreterObject, ObjectHolder):
if len(args) < 1 or len(args) > 2:
raise InterpreterException('Get_variable takes one or two arguments.')
if not self.found():
raise InterpreterException('Subproject "%s/%s" disabled can\'t get_variable on it.' % (
self.subproject_dir, self.name))
raise InterpreterException('Subproject "%s" disabled can\'t get_variable on it.' % (self.subdir))
varname = args[0]
if not isinstance(varname, str):
raise InterpreterException('Get_variable first argument must be a string.')
@ -2171,7 +2169,7 @@ class MesonMain(InterpreterObject):
self.interpreter.environment.build_dir)
if not os.path.exists(abspath):
raise InterpreterException('Tried to override %s with a file that does not exist.' % name)
exe = OverrideProgram(abspath)
exe = OverrideProgram(name, abspath)
if not isinstance(exe, (dependencies.ExternalProgram, build.Executable)):
raise InterpreterException('Second argument must be an external program or executable.')
self.interpreter.add_find_program_override(name, exe)
@ -2840,84 +2838,75 @@ external dependencies (including libraries) must go to "dependencies".''')
def func_subproject(self, nodes, args, kwargs):
if len(args) != 1:
raise InterpreterException('Subproject takes exactly one argument')
dirname = args[0]
return self.do_subproject(dirname, 'meson', kwargs)
subp_name = args[0]
return self.do_subproject(subp_name, 'meson', kwargs)
def disabled_subproject(self, dirname, disabled_feature=None, exception=None):
sub = SubprojectHolder(None, self.subproject_dir, dirname,
def disabled_subproject(self, subp_name, disabled_feature=None, exception=None):
sub = SubprojectHolder(None, os.path.join(self.subproject_dir, subp_name),
disabled_feature=disabled_feature, exception=exception)
self.subprojects[dirname] = sub
self.subprojects[subp_name] = sub
return sub
def get_subproject(self, dirname):
sub = self.subprojects.get(dirname)
def get_subproject(self, subp_name):
sub = self.subprojects.get(subp_name)
if sub and sub.found():
return sub
return None
def do_subproject(self, dirname: str, method: str, kwargs):
def do_subproject(self, subp_name: str, method: str, kwargs):
disabled, required, feature = extract_required_kwarg(kwargs, self.subproject)
if disabled:
mlog.log('Subproject', mlog.bold(dirname), ':', 'skipped: feature', mlog.bold(feature), 'disabled')
return self.disabled_subproject(dirname, disabled_feature=feature)
mlog.log('Subproject', mlog.bold(subp_name), ':', 'skipped: feature', mlog.bold(feature), 'disabled')
return self.disabled_subproject(subp_name, disabled_feature=feature)
default_options = mesonlib.stringlistify(kwargs.get('default_options', []))
default_options = coredata.create_options_dict(default_options)
if dirname == '':
raise InterpreterException('Subproject dir name must not be empty.')
if dirname[0] == '.':
raise InterpreterException('Subproject dir name must not start with a period.')
if '..' in dirname:
if subp_name == '':
raise InterpreterException('Subproject name must not be empty.')
if subp_name[0] == '.':
raise InterpreterException('Subproject name must not start with a period.')
if '..' in subp_name:
raise InterpreterException('Subproject name must not contain a ".." path segment.')
if os.path.isabs(dirname):
if os.path.isabs(subp_name):
raise InterpreterException('Subproject name must not be an absolute path.')
if has_path_sep(dirname):
if has_path_sep(subp_name):
mlog.warning('Subproject name has a path separator. This may cause unexpected behaviour.',
location=self.current_node)
if dirname in self.subproject_stack:
fullstack = self.subproject_stack + [dirname]
if subp_name in self.subproject_stack:
fullstack = self.subproject_stack + [subp_name]
incpath = ' => '.join(fullstack)
raise InvalidCode('Recursive include of subprojects: %s.' % incpath)
if dirname in self.subprojects:
subproject = self.subprojects[dirname]
if subp_name in self.subprojects:
subproject = self.subprojects[subp_name]
if required and not subproject.found():
raise InterpreterException('Subproject "%s/%s" required but not found.' % (
self.subproject_dir, dirname))
raise InterpreterException('Subproject "%s" required but not found.' % (subproject.subdir))
return subproject
r = self.environment.wrap_resolver
try:
resolved = r.resolve(dirname, method, self.subproject)
subdir = r.resolve(subp_name, method, self.subproject)
except wrap.WrapException as e:
subprojdir = os.path.join(self.subproject_dir, r.directory)
if isinstance(e, wrap.WrapNotFoundException):
# if the reason subproject execution failed was because
# the directory doesn't exist, try to give some helpful
# advice if it's a nested subproject that needs
# promotion...
self.print_nested_info(dirname)
if not required:
mlog.log(e)
mlog.log('Subproject ', mlog.bold(subprojdir), 'is buildable:', mlog.red('NO'), '(disabling)')
return self.disabled_subproject(dirname, exception=e)
mlog.log('Subproject ', mlog.bold(subp_name), 'is buildable:', mlog.red('NO'), '(disabling)')
return self.disabled_subproject(subp_name, exception=e)
raise e
subdir = os.path.join(self.subproject_dir, resolved)
subdir_abs = os.path.join(self.environment.get_source_dir(), subdir)
os.makedirs(os.path.join(self.build.environment.get_build_dir(), subdir), exist_ok=True)
self.global_args_frozen = True
mlog.log()
with mlog.nested():
mlog.log('Executing subproject', mlog.bold(dirname), 'method', mlog.bold(method), '\n')
mlog.log('Executing subproject', mlog.bold(subp_name), 'method', mlog.bold(method), '\n')
try:
if method == 'meson':
return self._do_subproject_meson(dirname, subdir, default_options, kwargs)
return self._do_subproject_meson(subp_name, subdir, default_options, kwargs)
elif method == 'cmake':
return self._do_subproject_cmake(dirname, subdir, subdir_abs, default_options, kwargs)
return self._do_subproject_cmake(subp_name, subdir, subdir_abs, default_options, kwargs)
else:
raise InterpreterException('The method {} is invalid for the subproject {}'.format(method, dirname))
raise InterpreterException('The method {} is invalid for the subproject {}'.format(method, subp_name))
# Invalid code is always an error
except InvalidCode:
raise
@ -2927,18 +2916,18 @@ external dependencies (including libraries) must go to "dependencies".''')
# Suppress the 'ERROR:' prefix because this exception is not
# fatal and VS CI treat any logs with "ERROR:" as fatal.
mlog.exception(e, prefix=mlog.yellow('Exception:'))
mlog.log('\nSubproject', mlog.bold(dirname), 'is buildable:', mlog.red('NO'), '(disabling)')
return self.disabled_subproject(dirname, exception=e)
mlog.log('\nSubproject', mlog.bold(subdir), 'is buildable:', mlog.red('NO'), '(disabling)')
return self.disabled_subproject(subp_name, exception=e)
raise e
def _do_subproject_meson(self, dirname, subdir, default_options, kwargs, ast=None, build_def_files=None):
def _do_subproject_meson(self, subp_name, subdir, default_options, kwargs, ast=None, build_def_files=None):
with mlog.nested():
new_build = self.build.copy()
subi = Interpreter(new_build, self.backend, dirname, subdir, self.subproject_dir,
subi = Interpreter(new_build, self.backend, subp_name, subdir, self.subproject_dir,
self.modules, default_options, ast=ast)
subi.subprojects = self.subprojects
subi.subproject_stack = self.subproject_stack + [dirname]
subi.subproject_stack = self.subproject_stack + [subp_name]
current_active = self.active_projectname
current_warnings_counter = mlog.log_warnings_counter
mlog.log_warnings_counter = 0
@ -2946,7 +2935,7 @@ external dependencies (including libraries) must go to "dependencies".''')
subi_warnings = mlog.log_warnings_counter
mlog.log_warnings_counter = current_warnings_counter
mlog.log('Subproject', mlog.bold(dirname), 'finished.')
mlog.log('Subproject', mlog.bold(subp_name), 'finished.')
mlog.log()
@ -2954,22 +2943,21 @@ external dependencies (including libraries) must go to "dependencies".''')
pv = subi.project_version
wanted = kwargs['version']
if pv == 'undefined' or not mesonlib.version_compare_many(pv, wanted)[0]:
raise InterpreterException('Subproject %s version is %s but %s required.' % (dirname, pv, wanted))
raise InterpreterException('Subproject %s version is %s but %s required.' % (subp_name, pv, wanted))
self.active_projectname = current_active
self.subprojects.update(subi.subprojects)
self.subprojects[dirname] = SubprojectHolder(subi, self.subproject_dir, dirname,
warnings=subi_warnings)
self.subprojects[subp_name] = SubprojectHolder(subi, subdir, warnings=subi_warnings)
# Duplicates are possible when subproject uses files from project root
if build_def_files:
self.build_def_files = list(set(self.build_def_files + build_def_files))
else:
self.build_def_files = list(set(self.build_def_files + subi.build_def_files))
self.build.merge(subi.build)
self.build.subprojects[dirname] = subi.project_version
self.build.subprojects[subp_name] = subi.project_version
self.summary.update(subi.summary)
return self.subprojects[dirname]
return self.subprojects[subp_name]
def _do_subproject_cmake(self, dirname, subdir, subdir_abs, default_options, kwargs):
def _do_subproject_cmake(self, subp_name, subdir, subdir_abs, default_options, kwargs):
with mlog.nested():
new_build = self.build.copy()
prefix = self.coredata.builtins['prefix'].value
@ -3007,7 +2995,7 @@ external dependencies (including libraries) must go to "dependencies".''')
mlog.cmd_ci_include(meson_filename)
mlog.log()
result = self._do_subproject_meson(dirname, subdir, default_options, kwargs, ast, cm_int.bs_files)
result = self._do_subproject_meson(subp_name, subdir, default_options, kwargs, ast, cm_int.bs_files)
result.cm_interpreter = cm_int
mlog.log()
@ -3157,8 +3145,11 @@ external dependencies (including libraries) must go to "dependencies".''')
'license': proj_license}
if self.subproject in self.build.projects:
raise InvalidCode('Second call to project().')
if not self.is_subproject() and 'subproject_dir' in kwargs:
spdirname = kwargs['subproject_dir']
# spdirname is the subproject_dir for this project, relative to self.subdir.
# self.subproject_dir is the subproject_dir for the main project, relative to top source dir.
spdirname = kwargs.get('subproject_dir')
if spdirname:
if not isinstance(spdirname, str):
raise InterpreterException('Subproject_dir must be a string')
if os.path.isabs(spdirname):
@ -3167,13 +3158,21 @@ external dependencies (including libraries) must go to "dependencies".''')
raise InterpreterException('Subproject_dir must not begin with a period.')
if '..' in spdirname:
raise InterpreterException('Subproject_dir must not contain a ".." segment.')
self.subproject_dir = spdirname
if not self.is_subproject():
self.subproject_dir = spdirname
else:
spdirname = 'subprojects'
self.build.subproject_dir = self.subproject_dir
if not self.is_subproject():
wrap_mode = self.coredata.get_builtin_option('wrap_mode')
subproject_dir_abs = os.path.join(self.environment.get_source_dir(), self.subproject_dir)
self.environment.wrap_resolver = wrap.Resolver(subproject_dir_abs, wrap_mode)
# Load wrap files from this (sub)project.
wrap_mode = self.coredata.get_builtin_option('wrap_mode')
if not self.is_subproject() or wrap_mode != WrapMode.nopromote:
subdir = os.path.join(self.subdir, spdirname)
r = wrap.Resolver(self.environment.get_source_dir(), subdir, wrap_mode)
if self.is_subproject():
self.environment.wrap_resolver.merge_wraps(r)
else:
self.environment.wrap_resolver = r
self.build.projects[self.subproject] = proj_name
mlog.log('Project name:', mlog.bold(proj_name))
@ -3376,7 +3375,7 @@ external dependencies (including libraries) must go to "dependencies".''')
return ExternalProgramHolder(prog, self.subproject)
return None
def program_from_system(self, args, search_dirs, silent=False):
def program_from_system(self, args, search_dirs, extra_info):
# Search for scripts relative to current subdir.
# Do not cache found programs because find_program('foobar')
# might give different results when run from different source dirs.
@ -3399,9 +3398,10 @@ external dependencies (including libraries) must go to "dependencies".''')
'files, not {!r}'.format(exename))
extprog = dependencies.ExternalProgram(exename, search_dir=search_dir,
extra_search_dirs=extra_search_dirs,
silent=silent)
silent=True)
progobj = ExternalProgramHolder(extprog, self.subproject)
if progobj.found():
extra_info.append('({})'.format(' '.join(progobj.get_command())))
return progobj
def program_from_overrides(self, command_names, extra_info):
@ -3459,7 +3459,7 @@ external dependencies (including libraries) must go to "dependencies".''')
if not is_found:
mlog.log('Program', mlog.bold(progobj.get_name()), 'found:', mlog.red('NO'),
'found', mlog.normal_cyan(version), 'but need:',
mlog.bold(', '.join(["'{}'".format(e) for e in not_found])))
mlog.bold(', '.join(["'{}'".format(e) for e in not_found])), *extra_info)
if required:
m = 'Invalid version of program, need {!r} {!r} found {!r}.'
raise InterpreterException(m.format(progobj.get_name(), not_found, version))
@ -3485,7 +3485,7 @@ external dependencies (including libraries) must go to "dependencies".''')
progobj = self.program_from_file_for(for_machine, args)
if progobj is None:
progobj = self.program_from_system(args, search_dirs, silent=True)
progobj = self.program_from_system(args, search_dirs, extra_info)
if progobj is None and args[0].endswith('python3'):
prog = dependencies.ExternalProgram('python3', mesonlib.python_command, silent=True)
progobj = ExternalProgramHolder(prog, self.subproject) if prog.found() else None
@ -3582,8 +3582,8 @@ 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.get_subproject(dirname)
def verify_fallback_consistency(self, subp_name, varname, cached_dep):
subi = self.get_subproject(subp_name)
if not cached_dep or not varname or not subi or not cached_dep.found():
return
dep = subi.get_variable_method([varname], {})
@ -3591,63 +3591,77 @@ external dependencies (including libraries) must go to "dependencies".''')
m = 'Inconsistency: Subproject has overridden the dependency with another variable than {!r}'
raise DependencyException(m.format(varname))
def get_subproject_dep(self, name, display_name, dirname, varname, kwargs):
def get_subproject_dep(self, name, display_name, subp_name, 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()
# Verify the subproject is found
subproject = self.subprojects.get(subp_name)
if not subproject or not subproject.found():
mlog.log('Dependency', mlog.bold(display_name), 'from subproject',
mlog.bold(subproject.subdir), 'found:', mlog.red('NO'),
mlog.blue('(subproject failed to configure)'))
if required:
m = 'Subproject {} failed to configure for dependency {}'
raise DependencyException(m.format(subproject.subdir, display_name))
return dep
extra_info = []
try:
subproject = self.subprojects[dirname]
# Check if the subproject overridden the dependency
_, cached_dep = self._find_cached_dep(name, display_name, kwargs)
if varname is None:
# Assuming the subproject overridden the dependency we want
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:
if required:
m = 'Subproject {} did not override dependency {}'
raise DependencyException(m.format(subproj_path, display_name))
mlog.log('Dependency', mlog.bold(display_name), 'from subproject',
mlog.bold(subproj_path), 'found:', mlog.red('NO'))
return self.notfound_dependency()
if subproject.found():
self.verify_fallback_consistency(dirname, varname, cached_dep)
dep = self.subprojects[dirname].get_variable_method([varname], {})
if cached_dep:
if varname:
self.verify_fallback_consistency(subp_name, varname, cached_dep)
if required and not cached_dep.found():
m = 'Dependency {!r} is not satisfied'
raise DependencyException(m.format(display_name))
return DependencyHolder(cached_dep, self.subproject)
elif varname is None:
mlog.log('Dependency', mlog.bold(display_name), 'from subproject',
mlog.bold(subproject.subdir), 'found:', mlog.red('NO'))
if required:
m = 'Subproject {} did not override dependency {}'
raise DependencyException(m.format(subproject.subdir, display_name))
return self.notfound_dependency()
else:
# The subproject did not override the dependency, but we know the
# variable name to take.
dep = subproject.get_variable_method([varname], {})
except InvalidArguments:
pass
# This is raised by get_variable_method() if varname does no exist
# in the subproject. Just add the reason in the not-found message
# that will be printed later.
extra_info.append(mlog.blue('(Variable {!r} not found)'.format(varname)))
if not isinstance(dep, DependencyHolder):
raise InvalidCode('Fetched variable {!r} in the subproject {!r} is '
'not a dependency object.'.format(varname, dirname))
'not a dependency object.'.format(varname, subp_name))
if not dep.found():
mlog.log('Dependency', mlog.bold(display_name), 'from subproject',
mlog.bold(subproject.subdir), 'found:', mlog.red('NO'), *extra_info)
if required:
raise DependencyException('Could not find dependency {} in subproject {}'
''.format(varname, dirname))
# If the dependency is not required, don't raise an exception
mlog.log('Dependency', mlog.bold(display_name), 'from subproject',
mlog.bold(subproj_path), 'found:', mlog.red('NO'))
''.format(varname, subp_name))
return dep
found = dep.held_object.get_version()
if not self.check_version(wanted, found):
if required:
raise DependencyException('Version {} of subproject dependency {} already '
'cached, requested incompatible version {} for '
'dep {}'.format(found, dirname, wanted, display_name))
mlog.log('Dependency', mlog.bold(display_name), 'from subproject',
mlog.bold(subproj_path), 'found:', mlog.red('NO'),
mlog.bold(subproject.subdir), 'found:', mlog.red('NO'),
'found', mlog.normal_cyan(found), 'but need:',
mlog.bold(', '.join(["'{}'".format(e) for e in wanted])))
if required:
raise DependencyException('Version {} of subproject dependency {} already '
'cached, requested incompatible version {} for '
'dep {}'.format(found, subp_name, wanted, display_name))
return self.notfound_dependency()
found = mlog.normal_cyan(found) if found else None
mlog.log('Dependency', mlog.bold(display_name), 'from subproject',
mlog.bold(subproj_path), 'found:', mlog.green('YES'), found)
mlog.bold(subproject.subdir), 'found:', mlog.green('YES'), found)
return dep
def _handle_featurenew_dependencies(self, name):
@ -3733,8 +3747,8 @@ external dependencies (including libraries) must go to "dependencies".''')
provider = self.environment.wrap_resolver.find_dep_provider(name)
if not provider and allow_fallback is True:
raise InvalidArguments('Fallback wrap or subproject not found for dependency \'%s\'' % name)
dirname = mesonlib.listify(provider)[0]
if provider and (allow_fallback is True or required or self.get_subproject(dirname)):
subp_name = mesonlib.listify(provider)[0]
if provider and (allow_fallback is True or required or self.get_subproject(subp_name)):
fallback = provider
if 'default_options' in kwargs and not fallback:
@ -3752,8 +3766,8 @@ external dependencies (including libraries) must go to "dependencies".''')
identifier, cached_dep = self._find_cached_dep(name, display_name, kwargs)
if cached_dep:
if fallback:
dirname, varname = self.get_subproject_infos(fallback)
self.verify_fallback_consistency(dirname, varname, cached_dep)
subp_name, varname = self.get_subproject_infos(fallback)
self.verify_fallback_consistency(subp_name, varname, cached_dep)
if required and not cached_dep.found():
m = 'Dependency {!r} was already checked and was not found'
raise DependencyException(m.format(display_name))
@ -3762,16 +3776,16 @@ external dependencies (including libraries) must go to "dependencies".''')
if fallback:
# If the dependency has already been configured, possibly by
# a higher level project, try to use it first.
dirname, varname = self.get_subproject_infos(fallback)
if self.get_subproject(dirname):
return self.get_subproject_dep(name, display_name, dirname, varname, kwargs)
subp_name, varname = self.get_subproject_infos(fallback)
if self.get_subproject(subp_name):
return self.get_subproject_dep(name, display_name, subp_name, varname, kwargs)
wrap_mode = self.coredata.get_builtin_option('wrap_mode')
force_fallback_for = self.coredata.get_builtin_option('force_fallback_for')
force_fallback = (force_fallback or
wrap_mode == WrapMode.forcefallback or
name in force_fallback_for or
dirname in force_fallback_for)
subp_name in force_fallback_for)
if name != '' and (not fallback or not force_fallback):
self._handle_featurenew_dependencies(name)
@ -3798,23 +3812,6 @@ external dependencies (including libraries) must go to "dependencies".''')
def func_disabler(self, node, args, kwargs):
return Disabler()
def print_nested_info(self, dependency_name):
message = ['Dependency', mlog.bold(dependency_name), 'not found but it is available in a sub-subproject.\n' +
'To use it in the current project, promote it by going in the project source\n'
'root and issuing']
sprojs = mesonlib.detect_subprojects('subprojects', self.source_root)
if dependency_name not in sprojs:
return
found = sprojs[dependency_name]
if len(found) > 1:
message.append('one of the following commands:')
else:
message.append('the following command:')
command_templ = '\nmeson wrap promote {}'
for l in found:
message.append(mlog.bold(command_templ.format(l[len(self.source_root) + 1:])))
mlog.warning(*message, location=self.current_node)
def get_subproject_infos(self, fbinfo):
fbinfo = mesonlib.stringlistify(fbinfo)
if len(fbinfo) == 1:
@ -3825,13 +3822,13 @@ external dependencies (including libraries) must go to "dependencies".''')
return fbinfo
def dependency_fallback(self, name, display_name, fallback, kwargs):
dirname, varname = self.get_subproject_infos(fallback)
subp_name, varname = self.get_subproject_infos(fallback)
required = kwargs.get('required', True)
# Explicitly listed fallback preferences for specific subprojects
# take precedence over wrap-mode
force_fallback_for = self.coredata.get_builtin_option('force_fallback_for')
if name in force_fallback_for or dirname in force_fallback_for:
if name in force_fallback_for or subp_name in force_fallback_for:
mlog.log('Looking for a fallback subproject for the dependency',
mlog.bold(display_name), 'because:\nUse of fallback was forced for that specific subproject')
elif self.coredata.get_builtin_option('wrap_mode') == WrapMode.nofallback:
@ -3852,8 +3849,8 @@ external dependencies (including libraries) must go to "dependencies".''')
'default_options': kwargs.get('default_options', []),
'required': required,
}
self.do_subproject(dirname, 'meson', sp_kwargs)
return self.get_subproject_dep(name, display_name, dirname, varname, kwargs)
self.do_subproject(subp_name, 'meson', sp_kwargs)
return self.get_subproject_dep(name, display_name, subp_name, varname, kwargs)
@FeatureNewKwargs('executable', '0.42.0', ['implib'])
@permittedKwargs(permitted_kwargs['executable'])
@ -4709,11 +4706,11 @@ This will probably not work.
Try setting b_lundef to false instead.'''.format(self.coredata.base_options['b_sanitize'].value),
location=self.current_node)
def evaluate_subproject_info(self, path_from_source_root, subproject_dirname):
def evaluate_subproject_info(self, path_from_source_root, subproject_dir):
depth = 0
subproj_name = ''
segs = PurePath(path_from_source_root).parts
segs_spd = PurePath(subproject_dirname).parts
segs_spd = PurePath(subproject_dir).parts
while segs and segs[0] == segs_spd[0]:
if len(segs_spd) == 1:
subproj_name = segs[1]
@ -4926,14 +4923,8 @@ This will become a hard error in the future.''', location=self.current_node)
# Only permit object extraction from the same subproject
def validate_extraction(self, buildtarget: InterpreterObject) -> None:
if not self.subdir.startswith(self.subproject_dir):
if buildtarget.subdir.startswith(self.subproject_dir):
raise InterpreterException('Tried to extract objects from a subproject target.')
else:
if not buildtarget.subdir.startswith(self.subproject_dir):
raise InterpreterException('Tried to extract objects from the main project from a subproject.')
if self.subdir.split('/')[1] != buildtarget.subdir.split('/')[1]:
raise InterpreterException('Tried to extract objects from a different subproject.')
if self.subproject != buildtarget.subproject:
raise InterpreterException('Tried to extract objects from a different subproject.')
def is_subproject(self):
return self.subproject != ''

@ -364,7 +364,7 @@ def run(options):
if not os.path.isdir(subprojects_dir):
mlog.log('Directory', mlog.bold(src_dir), 'does not seem to have subprojects.')
return 0
r = Resolver(subprojects_dir)
r = Resolver(src_dir, 'subprojects')
if options.subprojects:
wraps = [wrap for name, wrap in r.wraps.items() if name in options.subprojects]
else:

@ -40,6 +40,7 @@ string_to_value = {'default': 1,
'nofallback': 2,
'nodownload': 3,
'forcefallback': 4,
'nopromote': 5,
}
class WrapMode(Enum):
@ -47,6 +48,7 @@ class WrapMode(Enum):
nofallback = 2
nodownload = 3
forcefallback = 4
nopromote = 5
def __str__(self) -> str:
return self.name

@ -26,6 +26,7 @@ import subprocess
import sys
import configparser
import typing as T
import textwrap
from .._pathlib import Path
from . import WrapMode
@ -97,25 +98,47 @@ class PackageDefinition:
self.provided_deps = {} # type: T.Dict[str, T.Optional[str]]
self.provided_programs = [] # type: T.List[str]
self.basename = os.path.basename(fname)
self.name = self.basename
if self.name.endswith('.wrap'):
self.name = self.name[:-5]
self.has_wrap = self.basename.endswith('.wrap')
self.name = self.basename[:-5] if self.has_wrap else self.basename
self.directory = self.name
self.provided_deps[self.name] = None
if fname.endswith('.wrap'):
self.parse_wrap(fname)
if self.has_wrap:
self.parse_wrap()
self.directory = self.values.get('directory', self.name)
if os.path.dirname(self.directory):
raise WrapException('Directory key must be a name and not a path')
if self.type and self.type not in ALL_TYPES:
raise WrapException('Unknown wrap type {!r}'.format(self.type))
self.filesdir = os.path.join(os.path.dirname(self.filename), 'packagefiles')
def parse_wrap(self, fname: str) -> None:
def parse_wrap(self) -> None:
try:
self.config = configparser.ConfigParser(interpolation=None)
self.config.read(fname)
self.config.read(self.filename)
except configparser.Error:
raise WrapException('Failed to parse {}'.format(self.basename))
self.parse_wrap_section()
if self.type == 'redirect':
# [wrap-redirect] have a `filename` value pointing to the real wrap
# file we should parse instead. It must be relative to the current
# wrap file location and must be in the form foo/subprojects/bar.wrap.
dirname = Path(self.filename).parent
fname = Path(self.values['filename'])
for i, p in enumerate(fname.parts):
if i % 2 == 0:
if p == '..':
raise WrapException('wrap-redirect filename cannot contain ".."')
else:
if p != 'subprojects':
raise WrapException('wrap-redirect filename must be in the form foo/subprojects/bar.wrap')
if fname.suffix != '.wrap':
raise WrapException('wrap-redirect filename must be a .wrap file')
fname = dirname / fname
if not fname.is_file():
raise WrapException('wrap-redirect filename does not exist')
self.filename = str(fname)
self.parse_wrap()
return
self.parse_provide_section()
def parse_wrap_section(self) -> None:
@ -164,11 +187,12 @@ def get_directory(subdir_root: str, packagename: str) -> str:
return packagename
class Resolver:
def __init__(self, subdir_root: str, wrap_mode: WrapMode = WrapMode.default) -> None:
def __init__(self, source_dir: str, subdir: str, wrap_mode: WrapMode = WrapMode.default) -> None:
self.source_dir = source_dir
self.subdir = subdir
self.wrap_mode = wrap_mode
self.subdir_root = subdir_root
self.subdir_root = os.path.join(source_dir, subdir)
self.cachedir = os.path.join(self.subdir_root, 'packagecache')
self.filesdir = os.path.join(self.subdir_root, 'packagefiles')
self.wraps = {} # type: T.Dict[str, PackageDefinition]
self.provided_deps = {} # type: T.Dict[str, PackageDefinition]
self.provided_programs = {} # type: T.Dict[str, PackageDefinition]
@ -208,6 +232,14 @@ class Resolver:
raise WrapException(m.format(k, wrap.basename, prev_wrap.basename))
self.provided_programs[k] = wrap
def merge_wraps(self, other_resolver: 'Resolver') -> None:
for k, v in other_resolver.wraps.items():
self.wraps.setdefault(k, v)
for k, v in other_resolver.provided_deps.items():
self.provided_deps.setdefault(k, v)
for k, v in other_resolver.provided_programs.items():
self.provided_programs.setdefault(k, v)
def find_dep_provider(self, packagename: str) -> T.Optional[T.Union[str, T.List[str]]]:
# Return value is in the same format as fallback kwarg:
# ['subproject_name', 'variable_name'], or 'subproject_name'.
@ -235,7 +267,29 @@ class Resolver:
m = 'Subproject directory not found and {}.wrap file not found'
raise WrapNotFoundException(m.format(self.packagename))
self.directory = self.wrap.directory
self.dirname = os.path.join(self.subdir_root, self.directory)
if self.wrap.has_wrap:
# We have a .wrap file, source code will be placed into main
# project's subproject_dir even if the wrap file comes from another
# subproject.
self.dirname = os.path.join(self.subdir_root, self.directory)
# Check if the wrap comes from the main project.
main_fname = os.path.join(self.subdir_root, self.wrap.basename)
if self.wrap.filename != main_fname:
rel = os.path.relpath(self.wrap.filename, self.source_dir)
mlog.log('Using', mlog.bold(rel))
# Write a dummy wrap file in main project that redirect to the
# wrap we picked.
with open(main_fname, 'w') as f:
f.write(textwrap.dedent('''\
[wrap-redirect]
filename = {}
'''.format(os.path.relpath(self.wrap.filename, self.subdir_root))))
else:
# No wrap file, it's a dummy package definition for an existing
# directory. Use the source code in place.
self.dirname = self.wrap.filename
rel_path = os.path.relpath(self.dirname, self.source_dir)
meson_file = os.path.join(self.dirname, 'meson.build')
cmake_file = os.path.join(self.dirname, 'CMakeLists.txt')
@ -245,9 +299,9 @@ class Resolver:
# The directory is there and has meson.build? Great, use it.
if method == 'meson' and os.path.exists(meson_file):
return self.directory
return rel_path
if method == 'cmake' and os.path.exists(cmake_file):
return self.directory
return rel_path
# Check if the subproject is a git submodule
self.resolve_git_submodule()
@ -276,7 +330,7 @@ class Resolver:
if method == 'cmake' and not os.path.exists(cmake_file):
raise WrapException('Subproject exists but has no CMakeLists.txt file')
return self.directory
return rel_path
def check_can_download(self) -> None:
# Don't download subproject data based on wrap file if requested.
@ -487,7 +541,7 @@ class Resolver:
else:
from ..interpreterbase import FeatureNew
FeatureNew('Local wrap patch files without {}_url'.format(what), '0.55.0').use(self.current_subproject)
path = Path(self.filesdir) / filename
path = Path(self.wrap.filesdir) / filename
if not path.exists():
raise WrapException('File "{}" does not exist'.format(path))
@ -511,7 +565,7 @@ class Resolver:
from ..interpreterbase import FeatureNew
FeatureNew('patch_directory', '0.55.0').use(self.current_subproject)
patch_dir = self.wrap.values['patch_directory']
src_dir = os.path.join(self.filesdir, patch_dir)
src_dir = os.path.join(self.wrap.filesdir, patch_dir)
if not os.path.isdir(src_dir):
raise WrapException('patch directory does not exists: {}'.format(patch_dir))
self.copy_tree(src_dir, self.dirname)

@ -69,6 +69,8 @@ import mesonbuild.modules.pkgconfig
from mesonbuild.mtest import TAPParser, TestResult
from mesonbuild.wrap.wrap import PackageDefinition, WrapException
from run_tests import (
Backend, FakeBuild, FakeCompilerOptions,
ensure_backend_detects_changes, exe_suffix, get_backend_commands,
@ -4183,6 +4185,16 @@ recommended as it is not supported on some platforms''')
'name': 'sub_novar',
'version': '1.0',
},
{
'descriptive_name': 'subsub',
'name': 'subsub',
'version': 'undefined'
},
{
'descriptive_name': 'subsubsub',
'name': 'subsubsub',
'version': 'undefined'
},
]
}
res['subprojects'] = sorted(res['subprojects'], key=lambda i: i['name'])
@ -5156,6 +5168,52 @@ recommended as it is not supported on some platforms''')
out = self.init(testdir)
self.assertNotRegex(out, r'WARNING')
def test_wrap_redirect(self):
redirect_wrap = os.path.join(self.builddir, 'redirect.wrap')
real_wrap = os.path.join(self.builddir, 'foo/subprojects/real.wrap')
os.makedirs(os.path.dirname(real_wrap))
# Invalid redirect, filename must have .wrap extension
with open(redirect_wrap, 'w') as f:
f.write(textwrap.dedent('''
[wrap-redirect]
filename = foo/subprojects/real.wrapper
'''))
with self.assertRaisesRegex(WrapException, 'wrap-redirect filename must be a .wrap file'):
PackageDefinition(redirect_wrap)
# Invalid redirect, filename cannot be in parent directory
with open(redirect_wrap, 'w') as f:
f.write(textwrap.dedent('''
[wrap-redirect]
filename = ../real.wrap
'''))
with self.assertRaisesRegex(WrapException, 'wrap-redirect filename cannot contain ".."'):
PackageDefinition(redirect_wrap)
# Invalid redirect, filename must be in foo/subprojects/real.wrap
with open(redirect_wrap, 'w') as f:
f.write(textwrap.dedent('''
[wrap-redirect]
filename = foo/real.wrap
'''))
with self.assertRaisesRegex(WrapException, 'wrap-redirect filename must be in the form foo/subprojects/bar.wrap'):
wrap = PackageDefinition(redirect_wrap)
# Correct redirect
with open(redirect_wrap, 'w') as f:
f.write(textwrap.dedent('''
[wrap-redirect]
filename = foo/subprojects/real.wrap
'''))
with open(real_wrap, 'w') as f:
f.write(textwrap.dedent('''
[wrap-git]
url = http://invalid
'''))
wrap = PackageDefinition(redirect_wrap)
self.assertEqual(wrap.get('url'), 'http://invalid')
class FailureTests(BasePlatformTests):
'''
Tests that test failure conditions. Build files here should be dynamically
@ -5365,16 +5423,16 @@ class FailureTests(BasePlatformTests):
correct message when the fallback subproject is found but the
variable inside it is not.
4. A fallback dependency is found from the subproject parsed in (3)
5. The correct message is outputted when the .wrap file is missing for
a sub-subproject.
5. A wrap file from a subproject is used but fails because it does not
contain required keys.
'''
tdir = os.path.join(self.unit_test_dir, '20 subproj dep variables')
out = self.init(tdir, inprocess=True)
self.assertRegex(out, r"Subproject directory not found and .*nosubproj.wrap.* file not found")
self.assertRegex(out, r'Function does not take positional arguments.')
self.assertRegex(out, r'WARNING:.* Dependency .*subsubproject.* not found but it is available in a sub-subproject.')
self.assertRegex(out, r'Subproject directory not found and .*subsubproject.wrap.* file not found')
self.assertRegex(out, r'Dependency .*somenotfounddep.* from subproject .*subprojects/somesubproj.* found: .*NO.*')
self.assertRegex(out, r'Dependency .*zlibproxy.* from subproject .*subprojects.*somesubproj.* found: .*YES.*')
self.assertRegex(out, r'Missing key .*source_filename.* in subsubproject.wrap')
def test_exception_exit_status(self):
'''

@ -54,3 +54,14 @@ assert(d.type_name() == 'internal')
# Using gobject-2.0 here because some CI runners have it installed.
d = dependency('gobject-2.0', required : false)
assert(not d.found())
# Verify that implicit fallback works because subprojects/sub_implicit/subprojects/subsub
# directory exists.
d = dependency('subsub')
assert(d.found(), 'Should be able to fallback to sub-subproject')
# Verify that implicit fallback works because
# subprojects/sub_implicit/subprojects/subsub/subprojects/subsubsub.wrap
# file exists.
d = dependency('subsubsub')
assert(d.found(), 'Should be able to fallback to sub-sub-subproject')

@ -0,0 +1,3 @@
project('subsub')
meson.override_dependency('subsub', declare_dependency())

@ -0,0 +1,4 @@
[wrap-file]
directory = subsubsub-1.0
source_filename = subsubsub-1.0.zip
source_hash = c073a96b7251937e53216578f6f03d91b84816618a0f1ce3ecfb867beddf1498

@ -1,7 +1,7 @@
{
"stdout": [
{
"line": "test cases/failing/16 extract from subproject/meson.build:6:0: ERROR: Tried to extract objects from a subproject target."
"line": "test cases/failing/16 extract from subproject/meson.build:6:0: ERROR: Tried to extract objects from a different subproject."
}
]
}

Loading…
Cancel
Save