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.
pull/10039/head
Eli Schwartz 3 years ago
parent b55349c2e9
commit 0e3ed2f655
No known key found for this signature in database
GPG Key ID: CEB167EFB5722BD6
  1. 28
      mesonbuild/interpreter/interpreter.py
  2. 5
      mesonbuild/interpreter/interpreterobjects.py
  3. 8
      mesonbuild/interpreter/primitives/__init__.py
  4. 12
      mesonbuild/interpreter/primitives/string.py
  5. 13
      test cases/common/251 subproject dependency variables/meson.build
  6. 26
      test cases/common/251 subproject dependency variables/subprojects/subfiles/meson.build
  7. 1
      test cases/common/251 subproject dependency variables/subprojects/subfiles/subdir/foo.c
  8. 1
      test cases/common/251 subproject dependency variables/subprojects/subfiles/subdir2/foo.c
  9. 7
      test cases/common/251 subproject dependency variables/test.json
  10. 25
      test cases/failing/123 subproject sandbox violation/meson.build
  11. 1
      test cases/failing/123 subproject sandbox violation/meson_options.txt
  12. 0
      test cases/failing/123 subproject sandbox violation/subprojects/subproj1/file.txt
  13. 4
      test cases/failing/123 subproject sandbox violation/subprojects/subproj1/meson.build
  14. 5
      test cases/failing/123 subproject sandbox violation/subprojects/subproj1/nested/meson.build
  15. 0
      test cases/failing/123 subproject sandbox violation/subprojects/subproj2/file.txt
  16. 7
      test cases/failing/123 subproject sandbox violation/subprojects/subproj2/meson.build
  17. 0
      test cases/failing/123 subproject sandbox violation/subprojects/subproj2/nested/meson.build
  18. 15
      test cases/failing/123 subproject sandbox violation/test.json

@ -420,6 +420,7 @@ class Interpreter(InterpreterBase, HoldableObject):
bool: P_OBJ.BooleanHolder, bool: P_OBJ.BooleanHolder,
str: P_OBJ.StringHolder, str: P_OBJ.StringHolder,
P_OBJ.MesonVersionString: P_OBJ.MesonVersionStringHolder, P_OBJ.MesonVersionString: P_OBJ.MesonVersionStringHolder,
P_OBJ.DependencyVariableString: P_OBJ.DependencyVariableStringHolder,
# Meson types # Meson types
mesonlib.File: OBJ.FileHolder, 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) @typed_pos_args('join_paths', varargs=str, min_varargs=1)
@noKwargs @noKwargs
def func_join_paths(self, node: mparser.BaseNode, args: T.Tuple[T.List[str]], kwargs: 'TYPE_kwargs') -> str: 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: def run(self) -> None:
super().run() super().run()
@ -2759,6 +2765,26 @@ Try setting b_lundef to false instead.'''.format(self.coredata.options[OptionKey
# declare_dependency). # declare_dependency).
def validate_within_subproject(self, subdir, fname): def validate_within_subproject(self, subdir, fname):
srcdir = Path(self.environment.source_dir) 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() norm = Path(srcdir, subdir, fname).resolve()
if os.path.isdir(norm): if os.path.isdir(norm):
inputtype = 'directory' inputtype = 'directory'

@ -21,6 +21,7 @@ from ..interpreterbase import (
typed_pos_args, typed_kwargs, typed_operator, typed_pos_args, typed_kwargs, typed_operator,
noArgsFlattening, noPosargs, noKwargs, unholder_return, noArgsFlattening, noPosargs, noKwargs, unholder_return,
flatten, resolve_second_level_holders, InterpreterException, InvalidArguments, InvalidCode) flatten, resolve_second_level_holders, InterpreterException, InvalidArguments, InvalidCode)
from ..interpreter.primitives import DependencyVariableString
from ..interpreter.type_checking import NoneType, ENV_SEPARATOR_KW from ..interpreter.type_checking import NoneType, ENV_SEPARATOR_KW
from ..dependencies import Dependency, ExternalLibrary, InternalDependency from ..dependencies import Dependency, ExternalLibrary, InternalDependency
from ..programs import ExternalProgram from ..programs import ExternalProgram
@ -483,14 +484,14 @@ class DependencyHolder(ObjectHolder[Dependency]):
default_varname = args[0] default_varname = args[0]
if default_varname is not None: if default_varname is not None:
FeatureNew('Positional argument to dependency.get_variable()', '0.58.0').use(self.subproject, self.current_node) 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, cmake=kwargs['cmake'] or default_varname,
pkgconfig=kwargs['pkgconfig'] or default_varname, pkgconfig=kwargs['pkgconfig'] or default_varname,
configtool=kwargs['configtool'] or default_varname, configtool=kwargs['configtool'] or default_varname,
internal=kwargs['internal'] or default_varname, internal=kwargs['internal'] or default_varname,
default_value=kwargs['default_value'], default_value=kwargs['default_value'],
pkgconfig_define=kwargs['pkgconfig_define'], pkgconfig_define=kwargs['pkgconfig_define'],
) ))
@FeatureNew('dependency.include_type', '0.52.0') @FeatureNew('dependency.include_type', '0.52.0')
@noPosargs @noPosargs

@ -10,6 +10,8 @@ __all__ = [
'StringHolder', 'StringHolder',
'MesonVersionString', 'MesonVersionString',
'MesonVersionStringHolder', 'MesonVersionStringHolder',
'DependencyVariableString',
'DependencyVariableStringHolder',
] ]
from .array import ArrayHolder from .array import ArrayHolder
@ -17,4 +19,8 @@ from .boolean import BooleanHolder
from .dict import DictHolder from .dict import DictHolder
from .integer import IntegerHolder from .integer import IntegerHolder
from .range import RangeHolder from .range import RangeHolder
from .string import StringHolder, MesonVersionString, MesonVersionStringHolder from .string import (
StringHolder,
MesonVersionString, MesonVersionStringHolder,
DependencyVariableString, DependencyVariableStringHolder
)

@ -179,3 +179,15 @@ class MesonVersionStringHolder(StringHolder):
def version_compare_method(self, args: T.Tuple[str], kwargs: TYPE_kwargs) -> bool: def version_compare_method(self, args: T.Tuple[str], kwargs: TYPE_kwargs) -> bool:
self.interpreter.tmp_meson_version = args[0] self.interpreter.tmp_meson_version = args[0]
return version_compare(self.held_object, 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))

@ -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'
)

@ -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,
)

@ -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" }
]
}

@ -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@'
],
)

@ -0,0 +1 @@
option('failmode', type: 'combo', choices: ['parent-dir', 'not-installed'])

@ -0,0 +1,4 @@
project('subproj1')
install_data('file.txt')
subdir('nested')

@ -0,0 +1,5 @@
d = declare_dependency(
variables: [
'dir=@0@'.format(meson.current_source_dir()),
]
)

@ -0,0 +1,7 @@
project('subproj1')
d = declare_dependency(
variables: [
'dir=@0@'.format(meson.current_source_dir()),
]
)

@ -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."
}
]
}
Loading…
Cancel
Save