From 311a07c39a34e2aa4c193b4188d47f5e50ca1eda Mon Sep 17 00:00:00 2001 From: Xavier Claessens Date: Mon, 13 Apr 2020 10:17:49 -0400 Subject: [PATCH 1/6] interpreter: Rename dirname to subp_name dirname is confusing because the name of a subproject does not always match its directory name, the wrap file can define another directory. For example foo.wrap will often extract the subproject into foo-1.2 directory, in that case the subproject name is 'foo' and the subproject directory is 'foo-1.2'. --- mesonbuild/interpreter.py | 130 +++++++++++++++++++------------------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py index f25a6f354..713372d10 100644 --- a/mesonbuild/interpreter.py +++ b/mesonbuild/interpreter.py @@ -2840,55 +2840,55 @@ 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, 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)) + self.subproject_dir, subp_name)) return subproject r = self.environment.wrap_resolver try: - resolved = r.resolve(dirname, method, self.subproject) + resolved = 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): @@ -2896,11 +2896,11 @@ external dependencies (including libraries) must go to "dependencies".''') # 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) + self.print_nested_info(subp_name) 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) + return self.disabled_subproject(subp_name, exception=e) raise e subdir = os.path.join(self.subproject_dir, resolved) @@ -2910,14 +2910,14 @@ external dependencies (including libraries) must go to "dependencies".''') 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 +2927,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(subp_name), '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 +2946,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,10 +2954,10 @@ 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, + self.subprojects[subp_name] = SubprojectHolder(subi, self.subproject_dir, subp_name, warnings=subi_warnings) # Duplicates are possible when subproject uses files from project root if build_def_files: @@ -2965,11 +2965,11 @@ external dependencies (including libraries) must go to "dependencies".''') 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 +3007,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() @@ -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,13 +3591,13 @@ 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) + subproj_path = os.path.join(self.subproject_dir, subp_name) dep = self.notfound_dependency() try: - subproject = self.subprojects[dirname] + subproject = self.subprojects[subp_name] _, cached_dep = self._find_cached_dep(name, display_name, kwargs) if varname is None: # Assuming the subproject overridden the dependency we want @@ -3614,19 +3614,19 @@ external dependencies (including libraries) must go to "dependencies".''') 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], {}) + self.verify_fallback_consistency(subp_name, varname, cached_dep) + dep = self.subprojects[subp_name].get_variable_method([varname], {}) except InvalidArguments: pass 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(): if required: raise DependencyException('Could not find dependency {} in subproject {}' - ''.format(varname, dirname)) + ''.format(varname, subp_name)) # 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')) @@ -3637,7 +3637,7 @@ external dependencies (including libraries) must go to "dependencies".''') if required: raise DependencyException('Version {} of subproject dependency {} already ' 'cached, requested incompatible version {} for ' - 'dep {}'.format(found, dirname, wanted, display_name)) + 'dep {}'.format(found, subp_name, wanted, display_name)) mlog.log('Dependency', mlog.bold(display_name), 'from subproject', mlog.bold(subproj_path), 'found:', mlog.red('NO'), @@ -3733,8 +3733,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 +3752,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 +3762,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) @@ -3825,13 +3825,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 +3852,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 +4709,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] From 6333ee88c1a243f28b3a7a9bce2dd003b541280a Mon Sep 17 00:00:00 2001 From: Xavier Claessens Date: Mon, 13 Apr 2020 14:35:06 -0400 Subject: [PATCH 2/6] Merge wraps from subprojects into wraps from main project wraps from subprojects are now merged into the list of wraps from main project, so they can be used to download dependencies of dependencies instead of having to promote wraps manually. If multiple projects provides the same wrap file, the first one to be configured wins. This also fix usage of sub-subproject that don't have wrap files. We can now configure B when its source tree is at `subprojects/A/subprojects/B/`. This has the implication that we cannot assume that subproject "foo" is at `self.subproject_dir / 'foo'` any more. --- docs/markdown/Using-wraptool.md | 21 ---- docs/markdown/snippets/subsubproject.md | 11 ++ mesonbuild/interpreter.py | 94 +++++++----------- mesonbuild/msubprojects.py | 2 +- mesonbuild/wrap/wrap.py | 47 +++++++-- run_unittests.py | 18 +++- .../common/102 subproject subdir/meson.build | 11 ++ .../packagefiles/subsubsub-1.0.zip | Bin 0 -> 455 bytes .../subprojects/subsub/meson.build | 3 + .../subsub/subprojects/subsubsub.wrap | 4 + .../16 extract from subproject/test.json | 2 +- 11 files changed, 116 insertions(+), 97 deletions(-) create mode 100644 docs/markdown/snippets/subsubproject.md create mode 100644 test cases/common/102 subproject subdir/subprojects/packagefiles/subsubsub-1.0.zip create mode 100644 test cases/common/102 subproject subdir/subprojects/sub_implicit/subprojects/subsub/meson.build create mode 100644 test cases/common/102 subproject subdir/subprojects/sub_implicit/subprojects/subsub/subprojects/subsubsub.wrap diff --git a/docs/markdown/Using-wraptool.md b/docs/markdown/Using-wraptool.md index f6023e89d..ffa8309cf 100644 --- a/docs/markdown/Using-wraptool.md +++ b/docs/markdown/Using-wraptool.md @@ -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. diff --git a/docs/markdown/snippets/subsubproject.md b/docs/markdown/snippets/subsubproject.md new file mode 100644 index 000000000..fdba3e014 --- /dev/null +++ b/docs/markdown/snippets/subsubproject.md @@ -0,0 +1,11 @@ +## Sub-subprojects + +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. diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py index 713372d10..5104af672 100644 --- a/mesonbuild/interpreter.py +++ b/mesonbuild/interpreter.py @@ -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.') @@ -2844,7 +2842,7 @@ external dependencies (including libraries) must go to "dependencies".''') return self.do_subproject(subp_name, 'meson', kwargs) def disabled_subproject(self, subp_name, disabled_feature=None, exception=None): - sub = SubprojectHolder(None, self.subproject_dir, subp_name, + sub = SubprojectHolder(None, os.path.join(self.subproject_dir, subp_name), disabled_feature=disabled_feature, exception=exception) self.subprojects[subp_name] = sub return sub @@ -2882,28 +2880,19 @@ external dependencies (including libraries) must go to "dependencies".''') 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, subp_name)) + raise InterpreterException('Subproject "%s" required but not found.' % (subproject.subdir)) return subproject r = self.environment.wrap_resolver try: - resolved = r.resolve(subp_name, 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(subp_name) if not required: mlog.log(e) - mlog.log('Subproject ', mlog.bold(subprojdir), 'is buildable:', mlog.red('NO'), '(disabling)') + 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 @@ -2927,7 +2916,7 @@ 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(subp_name), 'is buildable:', mlog.red('NO'), '(disabling)') + mlog.log('\nSubproject', mlog.bold(subdir), 'is buildable:', mlog.red('NO'), '(disabling)') return self.disabled_subproject(subp_name, exception=e) raise e @@ -2957,8 +2946,7 @@ external dependencies (including libraries) must go to "dependencies".''') 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[subp_name] = SubprojectHolder(subi, self.subproject_dir, subp_name, - 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)) @@ -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,20 @@ 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') + 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)) @@ -3594,7 +3592,6 @@ external dependencies (including libraries) must go to "dependencies".''') 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, subp_name) dep = self.notfound_dependency() try: subproject = self.subprojects[subp_name] @@ -3609,9 +3606,9 @@ external dependencies (including libraries) must go to "dependencies".''') else: if required: m = 'Subproject {} did not override dependency {}' - raise DependencyException(m.format(subproj_path, display_name)) + raise DependencyException(m.format(subproject.subdir, 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')) return self.notfound_dependency() if subproject.found(): self.verify_fallback_consistency(subp_name, varname, cached_dep) @@ -3629,7 +3626,7 @@ external dependencies (including libraries) must go to "dependencies".''') ''.format(varname, subp_name)) # 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')) + mlog.bold(subproject.subdir), 'found:', mlog.red('NO')) return dep found = dep.held_object.get_version() @@ -3640,14 +3637,14 @@ external dependencies (including libraries) must go to "dependencies".''') 'dep {}'.format(found, subp_name, 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]))) 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): @@ -3798,23 +3795,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: @@ -4926,14 +4906,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 != '' diff --git a/mesonbuild/msubprojects.py b/mesonbuild/msubprojects.py index b628a47ca..20639cb7f 100755 --- a/mesonbuild/msubprojects.py +++ b/mesonbuild/msubprojects.py @@ -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: diff --git a/mesonbuild/wrap/wrap.py b/mesonbuild/wrap/wrap.py index a0a4801c9..838066156 100644 --- a/mesonbuild/wrap/wrap.py +++ b/mesonbuild/wrap/wrap.py @@ -97,11 +97,11 @@ 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'): + if self.has_wrap: self.parse_wrap(fname) self.directory = self.values.get('directory', self.name) if os.path.dirname(self.directory): @@ -164,9 +164,11 @@ 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] @@ -208,6 +210,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 +245,24 @@ 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) + # Copy .wrap file into main project's subproject_dir + wrap_dir = os.path.normpath(os.path.dirname(self.wrap.filename)) + main_dir = os.path.normpath(self.subdir_root) + if wrap_dir != main_dir: + rel = os.path.relpath(self.wrap.filename, self.source_dir) + mlog.log('Using', mlog.bold(rel)) + shutil.copy2(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 +272,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 +303,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. diff --git a/run_unittests.py b/run_unittests.py index 22e7cdc48..cfab11bcf 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -4183,6 +4183,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']) @@ -5365,16 +5375,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): ''' diff --git a/test cases/common/102 subproject subdir/meson.build b/test cases/common/102 subproject subdir/meson.build index a891ca992..36e48a7fa 100644 --- a/test cases/common/102 subproject subdir/meson.build +++ b/test cases/common/102 subproject subdir/meson.build @@ -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') diff --git a/test cases/common/102 subproject subdir/subprojects/packagefiles/subsubsub-1.0.zip b/test cases/common/102 subproject subdir/subprojects/packagefiles/subsubsub-1.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..dfb7576f3847730bae99129b429bcaf8a0daabcc GIT binary patch literal 455 zcmWIWW@Zs#0D*Vay#Zhbl;C4fU??t4f*@T(Jp=vF5MBm$!F^NW`diD6c5YH@y^UQ%ghP72&ukdau7)z&z5-pg0-Vt~G$Z<9i) zuim;lS04Sb+52bHl|_rrOu6!7%9W-od!}63^TaEq)QU@U Date: Wed, 22 Apr 2020 12:20:10 -0400 Subject: [PATCH 3/6] interpreter: Improve message when fallback dependency is not found - Log the message before raising the exception. - Add a reason when the dependency is not found because the subproject failed to configure, because it was not obvious in the case the subproject failed to configure earlier while looking for an optional dependency. - Avoid double message when the subproject has overriden the dependency and we provided the fallback variable as well. --- mesonbuild/interpreter.py | 80 +++++++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 32 deletions(-) diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py index 5104af672..56bc72d75 100644 --- a/mesonbuild/interpreter.py +++ b/mesonbuild/interpreter.py @@ -2169,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) @@ -3374,7 +3374,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. @@ -3397,9 +3397,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): @@ -3457,7 +3458,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)) @@ -3483,7 +3484,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 @@ -3593,53 +3594,68 @@ external dependencies (including libraries) must go to "dependencies".''') required = kwargs.get('required', True) wanted = mesonlib.stringlistify(kwargs.get('version', [])) 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[subp_name] + # 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(subproject.subdir, display_name)) - mlog.log('Dependency', mlog.bold(display_name), 'from subproject', - mlog.bold(subproject.subdir), 'found:', mlog.red('NO')) - return self.notfound_dependency() - if subproject.found(): - self.verify_fallback_consistency(subp_name, varname, cached_dep) - dep = self.subprojects[subp_name].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, 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, subp_name)) - # If the dependency is not required, don't raise an exception - mlog.log('Dependency', mlog.bold(display_name), 'from subproject', - mlog.bold(subproject.subdir), 'found:', mlog.red('NO')) 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, subp_name, wanted, display_name)) - mlog.log('Dependency', mlog.bold(display_name), 'from subproject', 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 From a20d7ad67d3c876785901349cfc990e4bf31778b Mon Sep 17 00:00:00 2001 From: Xavier Claessens Date: Sat, 29 Aug 2020 18:02:24 -0400 Subject: [PATCH 4/6] wrap: Use sub-subproject packagefiles --- mesonbuild/wrap/wrap.py | 11 ++++++----- .../subprojects}/packagefiles/subsubsub-1.0.zip | Bin 2 files changed, 6 insertions(+), 5 deletions(-) rename test cases/common/102 subproject subdir/subprojects/{ => sub_implicit/subprojects/subsub/subprojects}/packagefiles/subsubsub-1.0.zip (100%) diff --git a/mesonbuild/wrap/wrap.py b/mesonbuild/wrap/wrap.py index 838066156..ef35c6dbb 100644 --- a/mesonbuild/wrap/wrap.py +++ b/mesonbuild/wrap/wrap.py @@ -90,8 +90,9 @@ class WrapNotFoundException(WrapException): pass class PackageDefinition: - def __init__(self, fname: str): + def __init__(self, fname: str, filesdir: str = None): self.filename = fname + self.filesdir = filesdir self.type = None # type: T.Optional[str] self.values = {} # type: T.Dict[str, str] self.provided_deps = {} # type: T.Dict[str, T.Optional[str]] @@ -184,7 +185,7 @@ class Resolver: if not i.endswith('.wrap'): continue fname = os.path.join(self.subdir_root, i) - wrap = PackageDefinition(fname) + wrap = PackageDefinition(fname, self.filesdir) self.wraps[wrap.name] = wrap if wrap.directory in dirs: dirs.remove(wrap.directory) @@ -193,7 +194,7 @@ class Resolver: if i in ['packagecache', 'packagefiles']: continue fname = os.path.join(self.subdir_root, i) - wrap = PackageDefinition(fname) + wrap = PackageDefinition(fname, self.filesdir) self.wraps[wrap.name] = wrap for wrap in self.wraps.values(): @@ -514,7 +515,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)) @@ -538,7 +539,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) diff --git a/test cases/common/102 subproject subdir/subprojects/packagefiles/subsubsub-1.0.zip b/test cases/common/102 subproject subdir/subprojects/sub_implicit/subprojects/subsub/subprojects/packagefiles/subsubsub-1.0.zip similarity index 100% rename from test cases/common/102 subproject subdir/subprojects/packagefiles/subsubsub-1.0.zip rename to test cases/common/102 subproject subdir/subprojects/sub_implicit/subprojects/subsub/subprojects/packagefiles/subsubsub-1.0.zip From 173c115834e37acf2a66fd00fa4010e5765c39a9 Mon Sep 17 00:00:00 2001 From: Xavier Claessens Date: Thu, 17 Sep 2020 11:46:42 -0400 Subject: [PATCH 5/6] Add wrap mode to disable auto promote --- docs/markdown/Builtin-options.md | 2 +- docs/markdown/Subprojects.md | 7 +++++++ docs/markdown/snippets/subsubproject.md | 4 +++- mesonbuild/interpreter.py | 13 +++++++------ mesonbuild/wrap/__init__.py | 2 ++ 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/docs/markdown/Builtin-options.md b/docs/markdown/Builtin-options.md index 05ff4ff67..b7dd3de42 100644 --- a/docs/markdown/Builtin-options.md +++ b/docs/markdown/Builtin-options.md @@ -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,
nodownload, forcefallback} | default | Wrap mode to use | no | no | +| wrap_mode {default, nofallback,
nodownload, forcefallback, nopromote} | default | Wrap mode to use | no | no | | force_fallback_for | [] | Force fallback for those dependencies | no | no | diff --git a/docs/markdown/Subprojects.md b/docs/markdown/Subprojects.md index 7e17afa9f..e8adc965e 100644 --- a/docs/markdown/Subprojects.md +++ b/docs/markdown/Subprojects.md @@ -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* diff --git a/docs/markdown/snippets/subsubproject.md b/docs/markdown/snippets/subsubproject.md index fdba3e014..77f4a0d4e 100644 --- a/docs/markdown/snippets/subsubproject.md +++ b/docs/markdown/snippets/subsubproject.md @@ -1,4 +1,4 @@ -## Sub-subprojects +## 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 @@ -9,3 +9,5 @@ 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`. diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py index 56bc72d75..ad6f04eb7 100644 --- a/mesonbuild/interpreter.py +++ b/mesonbuild/interpreter.py @@ -3166,12 +3166,13 @@ external dependencies (including libraries) must go to "dependencies".''') # Load wrap files from this (sub)project. wrap_mode = self.coredata.get_builtin_option('wrap_mode') - 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 + 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)) diff --git a/mesonbuild/wrap/__init__.py b/mesonbuild/wrap/__init__.py index 17711461e..653f42ab9 100644 --- a/mesonbuild/wrap/__init__.py +++ b/mesonbuild/wrap/__init__.py @@ -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 From 3a0182378612bc764667328805b11bc92db696c2 Mon Sep 17 00:00:00 2001 From: Xavier Claessens Date: Tue, 29 Sep 2020 22:24:56 -0400 Subject: [PATCH 6/6] wrap: Add 'redirect' type and use it when auto promote --- mesonbuild/wrap/wrap.py | 52 ++++++++++++++++++++++++++++++----------- run_unittests.py | 48 +++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 13 deletions(-) diff --git a/mesonbuild/wrap/wrap.py b/mesonbuild/wrap/wrap.py index ef35c6dbb..a4e0b25c0 100644 --- a/mesonbuild/wrap/wrap.py +++ b/mesonbuild/wrap/wrap.py @@ -26,6 +26,7 @@ import subprocess import sys import configparser import typing as T +import textwrap from .._pathlib import Path from . import WrapMode @@ -90,9 +91,8 @@ class WrapNotFoundException(WrapException): pass class PackageDefinition: - def __init__(self, fname: str, filesdir: str = None): + def __init__(self, fname: str): self.filename = fname - self.filesdir = filesdir self.type = None # type: T.Optional[str] self.values = {} # type: T.Dict[str, str] self.provided_deps = {} # type: T.Dict[str, T.Optional[str]] @@ -103,20 +103,42 @@ class PackageDefinition: self.directory = self.name self.provided_deps[self.name] = None if self.has_wrap: - self.parse_wrap(fname) + 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: @@ -171,7 +193,6 @@ class Resolver: self.wrap_mode = wrap_mode 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] @@ -185,7 +206,7 @@ class Resolver: if not i.endswith('.wrap'): continue fname = os.path.join(self.subdir_root, i) - wrap = PackageDefinition(fname, self.filesdir) + wrap = PackageDefinition(fname) self.wraps[wrap.name] = wrap if wrap.directory in dirs: dirs.remove(wrap.directory) @@ -194,7 +215,7 @@ class Resolver: if i in ['packagecache', 'packagefiles']: continue fname = os.path.join(self.subdir_root, i) - wrap = PackageDefinition(fname, self.filesdir) + wrap = PackageDefinition(fname) self.wraps[wrap.name] = wrap for wrap in self.wraps.values(): @@ -252,13 +273,18 @@ class Resolver: # project's subproject_dir even if the wrap file comes from another # subproject. self.dirname = os.path.join(self.subdir_root, self.directory) - # Copy .wrap file into main project's subproject_dir - wrap_dir = os.path.normpath(os.path.dirname(self.wrap.filename)) - main_dir = os.path.normpath(self.subdir_root) - if wrap_dir != main_dir: + # 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)) - shutil.copy2(self.wrap.filename, self.subdir_root) + # 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. diff --git a/run_unittests.py b/run_unittests.py index cfab11bcf..f6adcee14 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -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, @@ -5166,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