From 0e3ed2f6559ff97e4ba85a4d723597017630d150 Mon Sep 17 00:00:00 2001 From: Eli Schwartz Date: Sun, 27 Feb 2022 21:38:04 -0500 Subject: [PATCH] dependencies: allow get_variable to expose files from subprojects There are somewhat common, reasonable and legitimate use cases for a dependency to provide data files installed to /usr which are used as command inputs. When getting a dependency from a subproject, however, the attempt to directly construct an input file from a subproject results in a sandbox violation. This means not all dependencies can be wrapped as a subproject. One example is wayland-protocols XML files which get scanned and used to produce C source files. Teach Meson to recognize when a string path is the result of fetching a dep.get_variable(), and special case this to be exempt from subproject violations. A requirement of this is that the file must be installed by install_data() or install_subdir() because otherwise it is not actually representative of what a pkg-config dependency would provide. --- mesonbuild/interpreter/interpreter.py | 28 ++++++++++++++++++- mesonbuild/interpreter/interpreterobjects.py | 5 ++-- mesonbuild/interpreter/primitives/__init__.py | 8 +++++- mesonbuild/interpreter/primitives/string.py | 12 ++++++++ .../meson.build | 13 +++++++++ .../subprojects/subfiles/meson.build | 26 +++++++++++++++++ .../subprojects/subfiles/subdir/foo.c | 1 + .../subprojects/subfiles/subdir2/foo.c | 1 + .../test.json | 7 +++++ .../meson.build | 25 +++++++++++++++++ .../meson_options.txt | 1 + .../subprojects/subproj1/file.txt | 0 .../subprojects/subproj1/meson.build | 4 +++ .../subprojects/subproj1/nested/meson.build | 5 ++++ .../subprojects/subproj2/file.txt | 0 .../subprojects/subproj2/meson.build | 7 +++++ .../subprojects/subproj2/nested/meson.build | 0 .../test.json | 15 ++++++++++ 18 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 test cases/common/251 subproject dependency variables/meson.build create mode 100644 test cases/common/251 subproject dependency variables/subprojects/subfiles/meson.build create mode 100644 test cases/common/251 subproject dependency variables/subprojects/subfiles/subdir/foo.c create mode 100644 test cases/common/251 subproject dependency variables/subprojects/subfiles/subdir2/foo.c create mode 100644 test cases/common/251 subproject dependency variables/test.json create mode 100644 test cases/failing/123 subproject sandbox violation/meson.build create mode 100644 test cases/failing/123 subproject sandbox violation/meson_options.txt create mode 100644 test cases/failing/123 subproject sandbox violation/subprojects/subproj1/file.txt create mode 100644 test cases/failing/123 subproject sandbox violation/subprojects/subproj1/meson.build create mode 100644 test cases/failing/123 subproject sandbox violation/subprojects/subproj1/nested/meson.build create mode 100644 test cases/failing/123 subproject sandbox violation/subprojects/subproj2/file.txt create mode 100644 test cases/failing/123 subproject sandbox violation/subprojects/subproj2/meson.build create mode 100644 test cases/failing/123 subproject sandbox violation/subprojects/subproj2/nested/meson.build create mode 100644 test cases/failing/123 subproject sandbox violation/test.json diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index b31b7a866..5bac8bae5 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -420,6 +420,7 @@ class Interpreter(InterpreterBase, HoldableObject): bool: P_OBJ.BooleanHolder, str: P_OBJ.StringHolder, P_OBJ.MesonVersionString: P_OBJ.MesonVersionStringHolder, + P_OBJ.DependencyVariableString: P_OBJ.DependencyVariableStringHolder, # Meson types mesonlib.File: OBJ.FileHolder, @@ -2716,7 +2717,12 @@ external dependencies (including libraries) must go to "dependencies".''') @typed_pos_args('join_paths', varargs=str, min_varargs=1) @noKwargs def func_join_paths(self, node: mparser.BaseNode, args: T.Tuple[T.List[str]], kwargs: 'TYPE_kwargs') -> str: - return os.path.join(*args[0]).replace('\\', '/') + parts = args[0] + ret = os.path.join(*parts).replace('\\', '/') + if isinstance(parts[0], P_OBJ.DependencyVariableString): + return P_OBJ.DependencyVariableString(ret) + else: + return ret def run(self) -> None: super().run() @@ -2759,6 +2765,26 @@ Try setting b_lundef to false instead.'''.format(self.coredata.options[OptionKey # declare_dependency). def validate_within_subproject(self, subdir, fname): srcdir = Path(self.environment.source_dir) + builddir = Path(self.environment.build_dir) + if isinstance(fname, P_OBJ.DependencyVariableString): + def validate_installable_file(fpath: Path) -> bool: + installablefiles: T.Set[Path] = set() + for d in self.build.data: + for s in d.sources: + installablefiles.add(Path(s.absolute_path(srcdir, builddir))) + installabledirs = [str(Path(srcdir, s.source_subdir)) for s in self.build.install_dirs] + if fpath in installablefiles: + return True + for d in installabledirs: + if str(fpath).startswith(d): + return True + return False + + norm = Path(fname) + # variables built from a dep.get_variable are allowed to refer to + # subproject files, as long as they are scheduled to be installed. + if norm.is_absolute() and '..' not in norm.parts and validate_installable_file(norm): + return norm = Path(srcdir, subdir, fname).resolve() if os.path.isdir(norm): inputtype = 'directory' diff --git a/mesonbuild/interpreter/interpreterobjects.py b/mesonbuild/interpreter/interpreterobjects.py index 39085e5f9..4aa5548b6 100644 --- a/mesonbuild/interpreter/interpreterobjects.py +++ b/mesonbuild/interpreter/interpreterobjects.py @@ -21,6 +21,7 @@ from ..interpreterbase import ( typed_pos_args, typed_kwargs, typed_operator, noArgsFlattening, noPosargs, noKwargs, unholder_return, flatten, resolve_second_level_holders, InterpreterException, InvalidArguments, InvalidCode) +from ..interpreter.primitives import DependencyVariableString from ..interpreter.type_checking import NoneType, ENV_SEPARATOR_KW from ..dependencies import Dependency, ExternalLibrary, InternalDependency from ..programs import ExternalProgram @@ -483,14 +484,14 @@ class DependencyHolder(ObjectHolder[Dependency]): default_varname = args[0] if default_varname is not None: FeatureNew('Positional argument to dependency.get_variable()', '0.58.0').use(self.subproject, self.current_node) - return self.held_object.get_variable( + return DependencyVariableString(self.held_object.get_variable( cmake=kwargs['cmake'] or default_varname, pkgconfig=kwargs['pkgconfig'] or default_varname, configtool=kwargs['configtool'] or default_varname, internal=kwargs['internal'] or default_varname, default_value=kwargs['default_value'], pkgconfig_define=kwargs['pkgconfig_define'], - ) + )) @FeatureNew('dependency.include_type', '0.52.0') @noPosargs diff --git a/mesonbuild/interpreter/primitives/__init__.py b/mesonbuild/interpreter/primitives/__init__.py index b4fe621ba..1874d0d81 100644 --- a/mesonbuild/interpreter/primitives/__init__.py +++ b/mesonbuild/interpreter/primitives/__init__.py @@ -10,6 +10,8 @@ __all__ = [ 'StringHolder', 'MesonVersionString', 'MesonVersionStringHolder', + 'DependencyVariableString', + 'DependencyVariableStringHolder', ] from .array import ArrayHolder @@ -17,4 +19,8 @@ from .boolean import BooleanHolder from .dict import DictHolder from .integer import IntegerHolder from .range import RangeHolder -from .string import StringHolder, MesonVersionString, MesonVersionStringHolder +from .string import ( + StringHolder, + MesonVersionString, MesonVersionStringHolder, + DependencyVariableString, DependencyVariableStringHolder +) diff --git a/mesonbuild/interpreter/primitives/string.py b/mesonbuild/interpreter/primitives/string.py index 912930306..bf213ef8e 100644 --- a/mesonbuild/interpreter/primitives/string.py +++ b/mesonbuild/interpreter/primitives/string.py @@ -179,3 +179,15 @@ class MesonVersionStringHolder(StringHolder): def version_compare_method(self, args: T.Tuple[str], kwargs: TYPE_kwargs) -> bool: self.interpreter.tmp_meson_version = args[0] return version_compare(self.held_object, args[0]) + +# These special subclasses of string exist to cover the case where a dependency +# exports a string variable interchangeable with a system dependency. This +# matters because a dependency can only have string-type get_variable() return +# values. If at any time dependencies start supporting additional variable +# types, this class could be deprecated. +class DependencyVariableString(str): + pass + +class DependencyVariableStringHolder(StringHolder): + def op_div(self, other: str) -> DependencyVariableString: + return DependencyVariableString(super().op_div(other)) diff --git a/test cases/common/251 subproject dependency variables/meson.build b/test cases/common/251 subproject dependency variables/meson.build new file mode 100644 index 000000000..6abcc165e --- /dev/null +++ b/test cases/common/251 subproject dependency variables/meson.build @@ -0,0 +1,13 @@ +project('subproject dependency variables', 'c') + +subfiles_dep = subproject('subfiles').get_variable('files_dep') + +executable( + 'foo', + join_paths(subfiles_dep.get_variable('pkgdatadir'), 'foo.c') +) + +executable( + 'foo2', + subfiles_dep.get_variable('pkgdatadir2') / 'foo.c' +) diff --git a/test cases/common/251 subproject dependency variables/subprojects/subfiles/meson.build b/test cases/common/251 subproject dependency variables/subprojects/subfiles/meson.build new file mode 100644 index 000000000..0c63bacef --- /dev/null +++ b/test cases/common/251 subproject dependency variables/subprojects/subfiles/meson.build @@ -0,0 +1,26 @@ +project('dependency variable resource') + +files_dep = declare_dependency( + variables: [ + 'pkgdatadir=@0@/subdir'.format(meson.current_source_dir()), + 'pkgdatadir2=@0@/subdir2'.format(meson.current_source_dir()), + ] +) + +install_data('subdir/foo.c', install_dir: get_option('datadir') / 'subdir') +install_subdir('subdir2', install_dir: get_option('datadir')) + +import('pkgconfig').generate( + name: 'depvar_resource', + description: 'Get a resource file from pkgconfig or a subproject', + version: '0.1', + variables: [ + 'pkgdatadir=${datadir}/subdir', + 'pkgdatadir2=${datadir}/subdir2', + ], + uninstalled_variables: [ + 'pkgdatadir=@0@/subdir'.format(meson.current_source_dir()), + 'pkgdatadir2=@0@/subdir2'.format(meson.current_source_dir()), + ], + dataonly: true, +) diff --git a/test cases/common/251 subproject dependency variables/subprojects/subfiles/subdir/foo.c b/test cases/common/251 subproject dependency variables/subprojects/subfiles/subdir/foo.c new file mode 100644 index 000000000..78f2de106 --- /dev/null +++ b/test cases/common/251 subproject dependency variables/subprojects/subfiles/subdir/foo.c @@ -0,0 +1 @@ +int main(void) { return 0; } diff --git a/test cases/common/251 subproject dependency variables/subprojects/subfiles/subdir2/foo.c b/test cases/common/251 subproject dependency variables/subprojects/subfiles/subdir2/foo.c new file mode 100644 index 000000000..78f2de106 --- /dev/null +++ b/test cases/common/251 subproject dependency variables/subprojects/subfiles/subdir2/foo.c @@ -0,0 +1 @@ +int main(void) { return 0; } diff --git a/test cases/common/251 subproject dependency variables/test.json b/test cases/common/251 subproject dependency variables/test.json new file mode 100644 index 000000000..dfd348c64 --- /dev/null +++ b/test cases/common/251 subproject dependency variables/test.json @@ -0,0 +1,7 @@ +{ + "installed": [ + { "type": "file", "file": "usr/share/pkgconfig/depvar_resource.pc" }, + { "type": "file", "file": "usr/share/subdir/foo.c" }, + { "type": "file", "file": "usr/share/subdir2/foo.c" } + ] +} diff --git a/test cases/failing/123 subproject sandbox violation/meson.build b/test cases/failing/123 subproject sandbox violation/meson.build new file mode 100644 index 000000000..0070eb652 --- /dev/null +++ b/test cases/failing/123 subproject sandbox violation/meson.build @@ -0,0 +1,25 @@ +project('subproject-sandbox-violation') + +sub1_d = subproject('subproj1').get_variable('d') +sub1_mustfail = sub1_d.get_variable('dir') / '..' / 'file.txt' + +sub2_d = subproject('subproj2').get_variable('d') +sub2_mustfail = sub2_d.get_variable('dir') / 'file.txt' + +if get_option('failmode') == 'parent-dir' + mustfail = sub1_mustfail +elif get_option('failmode') == 'not-installed' + mustfail = sub2_mustfail +endif + +custom_target( + 'mustfail', + input: mustfail, + output: 'file.txt', + command: [ + 'python3', '-c', + 'import os; shutil.copy(sys.argv[1], sys.argv[2])', + '@INPUT@', + '@OUTPUT@' + ], +) diff --git a/test cases/failing/123 subproject sandbox violation/meson_options.txt b/test cases/failing/123 subproject sandbox violation/meson_options.txt new file mode 100644 index 000000000..cee2bdc0f --- /dev/null +++ b/test cases/failing/123 subproject sandbox violation/meson_options.txt @@ -0,0 +1 @@ +option('failmode', type: 'combo', choices: ['parent-dir', 'not-installed']) diff --git a/test cases/failing/123 subproject sandbox violation/subprojects/subproj1/file.txt b/test cases/failing/123 subproject sandbox violation/subprojects/subproj1/file.txt new file mode 100644 index 000000000..e69de29bb diff --git a/test cases/failing/123 subproject sandbox violation/subprojects/subproj1/meson.build b/test cases/failing/123 subproject sandbox violation/subprojects/subproj1/meson.build new file mode 100644 index 000000000..bd33bf38d --- /dev/null +++ b/test cases/failing/123 subproject sandbox violation/subprojects/subproj1/meson.build @@ -0,0 +1,4 @@ +project('subproj1') + +install_data('file.txt') +subdir('nested') diff --git a/test cases/failing/123 subproject sandbox violation/subprojects/subproj1/nested/meson.build b/test cases/failing/123 subproject sandbox violation/subprojects/subproj1/nested/meson.build new file mode 100644 index 000000000..038c13906 --- /dev/null +++ b/test cases/failing/123 subproject sandbox violation/subprojects/subproj1/nested/meson.build @@ -0,0 +1,5 @@ +d = declare_dependency( + variables: [ + 'dir=@0@'.format(meson.current_source_dir()), + ] +) diff --git a/test cases/failing/123 subproject sandbox violation/subprojects/subproj2/file.txt b/test cases/failing/123 subproject sandbox violation/subprojects/subproj2/file.txt new file mode 100644 index 000000000..e69de29bb diff --git a/test cases/failing/123 subproject sandbox violation/subprojects/subproj2/meson.build b/test cases/failing/123 subproject sandbox violation/subprojects/subproj2/meson.build new file mode 100644 index 000000000..a6032aa74 --- /dev/null +++ b/test cases/failing/123 subproject sandbox violation/subprojects/subproj2/meson.build @@ -0,0 +1,7 @@ +project('subproj1') + +d = declare_dependency( + variables: [ + 'dir=@0@'.format(meson.current_source_dir()), + ] +) diff --git a/test cases/failing/123 subproject sandbox violation/subprojects/subproj2/nested/meson.build b/test cases/failing/123 subproject sandbox violation/subprojects/subproj2/nested/meson.build new file mode 100644 index 000000000..e69de29bb diff --git a/test cases/failing/123 subproject sandbox violation/test.json b/test cases/failing/123 subproject sandbox violation/test.json new file mode 100644 index 000000000..f31497732 --- /dev/null +++ b/test cases/failing/123 subproject sandbox violation/test.json @@ -0,0 +1,15 @@ +{ + "matrix": { + "options": { + "failmode": [ + { "val": "not-installed" }, + { "val": "parent-dir" } + ] + } + }, + "stdout": [ + { + "line": "test cases/failing/123 subproject sandbox violation/meson.build:19:0: ERROR: Sandbox violation: Tried to grab file file.txt from a nested subproject." + } + ] +}