Merge pull request #10039 from eli-schwartz/wayland-protocols-subproject-files

dependencies: allow get_variable to expose files from subprojects
pull/10331/head
Jussi Pakkanen 3 years ago committed by GitHub
commit bba588d8b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 22
      docs/markdown/Dependencies.md
  2. 12
      mesonbuild/dependencies/base.py
  3. 16
      mesonbuild/dependencies/cmake.py
  4. 2
      mesonbuild/dependencies/configtool.py
  5. 2
      mesonbuild/dependencies/pkgconfig.py
  6. 41
      mesonbuild/interpreter/interpreter.py
  7. 2
      mesonbuild/interpreter/interpreterobjects.py
  8. 8
      mesonbuild/interpreter/primitives/__init__.py
  9. 15
      mesonbuild/interpreter/primitives/string.py
  10. 13
      test cases/common/251 subproject dependency variables/meson.build
  11. 26
      test cases/common/251 subproject dependency variables/subprojects/subfiles/meson.build
  12. 1
      test cases/common/251 subproject dependency variables/subprojects/subfiles/subdir/foo.c
  13. 1
      test cases/common/251 subproject dependency variables/subprojects/subfiles/subdir2/foo.c
  14. 7
      test cases/common/251 subproject dependency variables/test.json
  15. 34
      test cases/failing/123 subproject sandbox violation/meson.build
  16. 1
      test cases/failing/123 subproject sandbox violation/meson_options.txt
  17. 0
      test cases/failing/123 subproject sandbox violation/subprojects/subproj1/file.txt
  18. 4
      test cases/failing/123 subproject sandbox violation/subprojects/subproj1/meson.build
  19. 5
      test cases/failing/123 subproject sandbox violation/subprojects/subproj1/nested/meson.build
  20. 0
      test cases/failing/123 subproject sandbox violation/subprojects/subproj2/file.txt
  21. 7
      test cases/failing/123 subproject sandbox violation/subprojects/subproj2/meson.build
  22. 0
      test cases/failing/123 subproject sandbox violation/subprojects/subproj2/nested/meson.build
  23. 0
      test cases/failing/123 subproject sandbox violation/subprojects/subproj3/file.txt
  24. 3
      test cases/failing/123 subproject sandbox violation/subprojects/subproj3/meson.build
  25. 16
      test cases/failing/123 subproject sandbox violation/test.json
  26. 4
      test cases/unit/63 cmake parser/meson.build

@ -90,6 +90,28 @@ following will happen: If 'default_value' was provided that value will
be returned, if 'default_value' was not provided then an error will be be returned, if 'default_value' was not provided then an error will be
raised. raised.
## Dependencies that provide resource files
Sometimes a dependency provides installable files which other projects then
need to use. For example, wayland-protocols XML files.
```meson
foo_dep = dependency('foo')
foo_datadir = foo_dep.get_variable('pkgdatadir')
custom_target(
'foo-generated.c',
input: foo_datadir / 'prototype.xml',
output: 'foo-generated.c',
command: [generator, '@INPUT@', '@OUTPUT@']
)
```
*Since 0.63.0* these actually work as expected, even when they come from a
(well-formed) internal dependency. This only works when treating the files to
be obtained as interchangeable with a system dependency -- e.g. only public
files may be used, and leaving the directory pointed to by the dependency is
not allowed.
# Declaring your own # Declaring your own
You can declare your own dependency objects that can be used You can declare your own dependency objects that can be used

@ -216,7 +216,7 @@ class Dependency(HoldableObject):
def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None, def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None,
configtool: T.Optional[str] = None, internal: T.Optional[str] = None, configtool: T.Optional[str] = None, internal: T.Optional[str] = None,
default_value: T.Optional[str] = None, default_value: T.Optional[str] = None,
pkgconfig_define: T.Optional[T.List[str]] = None) -> T.Union[str, T.List[str]]: pkgconfig_define: T.Optional[T.List[str]] = None) -> str:
if default_value is not None: if default_value is not None:
return default_value return default_value
raise DependencyException(f'No default provided for dependency {self!r}, which is not pkg-config, cmake, or config-tool based.') raise DependencyException(f'No default provided for dependency {self!r}, which is not pkg-config, cmake, or config-tool based.')
@ -232,7 +232,7 @@ class InternalDependency(Dependency):
libraries: T.List[T.Union['BuildTarget', 'CustomTarget']], libraries: T.List[T.Union['BuildTarget', 'CustomTarget']],
whole_libraries: T.List[T.Union['BuildTarget', 'CustomTarget']], whole_libraries: T.List[T.Union['BuildTarget', 'CustomTarget']],
sources: T.Sequence[T.Union['FileOrString', 'CustomTarget', StructuredSources]], sources: T.Sequence[T.Union['FileOrString', 'CustomTarget', StructuredSources]],
ext_deps: T.List[Dependency], variables: T.Dict[str, T.Any], ext_deps: T.List[Dependency], variables: T.Dict[str, str],
d_module_versions: T.List[str], d_import_dirs: T.List['IncludeDirs']): d_module_versions: T.List[str], d_import_dirs: T.List['IncludeDirs']):
super().__init__(DependencyTypeName('internal'), {}) super().__init__(DependencyTypeName('internal'), {})
self.version = version self.version = version
@ -301,15 +301,9 @@ class InternalDependency(Dependency):
def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None, def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None,
configtool: T.Optional[str] = None, internal: T.Optional[str] = None, configtool: T.Optional[str] = None, internal: T.Optional[str] = None,
default_value: T.Optional[str] = None, default_value: T.Optional[str] = None,
pkgconfig_define: T.Optional[T.List[str]] = None) -> T.Union[str, T.List[str]]: pkgconfig_define: T.Optional[T.List[str]] = None) -> str:
val = self.variables.get(internal, default_value) val = self.variables.get(internal, default_value)
if val is not None: if val is not None:
# TODO: Try removing this assert by better typing self.variables
if isinstance(val, str):
return val
if isinstance(val, list):
for i in val:
assert isinstance(i, str)
return val return val
raise DependencyException(f'Could not get an internal variable and no default provided for {self!r}') raise DependencyException(f'Could not get an internal variable and no default provided for {self!r}')

@ -627,17 +627,23 @@ class CMakeDependency(ExternalDependency):
def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None, def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None,
configtool: T.Optional[str] = None, internal: T.Optional[str] = None, configtool: T.Optional[str] = None, internal: T.Optional[str] = None,
default_value: T.Optional[str] = None, default_value: T.Optional[str] = None,
pkgconfig_define: T.Optional[T.List[str]] = None) -> T.Union[str, T.List[str]]: pkgconfig_define: T.Optional[T.List[str]] = None) -> str:
if cmake and self.traceparser is not None: if cmake and self.traceparser is not None:
try: try:
v = self.traceparser.vars[cmake] v = self.traceparser.vars[cmake]
except KeyError: except KeyError:
pass pass
else: else:
if len(v) == 1: # CMake does NOT have a list datatype. We have no idea whether
return v[0] # anything is a string or a string-separated-by-; Internally,
elif v: # we treat them as the latter and represent everything as a
return v # list, because it is convenient when we are mostly handling
# imported targets, which have various properties that are
# actually lists.
#
# As a result we need to convert them back to strings when grabbing
# raw variables the user requested.
return ';'.join(v)
if default_value is not None: if default_value is not None:
return default_value return default_value
raise DependencyException(f'Could not get cmake variable and no default provided for {self!r}') raise DependencyException(f'Could not get cmake variable and no default provided for {self!r}')

@ -155,7 +155,7 @@ class ConfigToolDependency(ExternalDependency):
def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None, def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None,
configtool: T.Optional[str] = None, internal: T.Optional[str] = None, configtool: T.Optional[str] = None, internal: T.Optional[str] = None,
default_value: T.Optional[str] = None, default_value: T.Optional[str] = None,
pkgconfig_define: T.Optional[T.List[str]] = None) -> T.Union[str, T.List[str]]: pkgconfig_define: T.Optional[T.List[str]] = None) -> str:
if configtool: if configtool:
# In the not required case '' (empty string) will be returned if the # In the not required case '' (empty string) will be returned if the
# variable is not found. Since '' is a valid value to return we # variable is not found. Since '' is a valid value to return we

@ -485,7 +485,7 @@ class PkgConfigDependency(ExternalDependency):
def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None, def get_variable(self, *, cmake: T.Optional[str] = None, pkgconfig: T.Optional[str] = None,
configtool: T.Optional[str] = None, internal: T.Optional[str] = None, configtool: T.Optional[str] = None, internal: T.Optional[str] = None,
default_value: T.Optional[str] = None, default_value: T.Optional[str] = None,
pkgconfig_define: T.Optional[T.List[str]] = None) -> T.Union[str, T.List[str]]: pkgconfig_define: T.Optional[T.List[str]] = None) -> str:
if pkgconfig: if pkgconfig:
try: try:
return self.get_pkgconfig_variable(pkgconfig, pkgconfig_define or [], default_value) return self.get_pkgconfig_variable(pkgconfig, pkgconfig_define or [], default_value)

@ -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,
@ -669,6 +670,18 @@ class Interpreter(InterpreterBase, HoldableObject):
d_module_versions = extract_as_list(kwargs, 'd_module_versions') d_module_versions = extract_as_list(kwargs, 'd_module_versions')
d_import_dirs = self.extract_incdirs(kwargs, 'd_import_dirs') d_import_dirs = self.extract_incdirs(kwargs, 'd_import_dirs')
final_deps = [] final_deps = []
srcdir = Path(self.environment.source_dir)
# convert variables which refer to an -uninstalled.pc style datadir
for k, v in variables.items():
try:
p = Path(v)
except ValueError:
continue
else:
if not self.is_subproject() and srcdir / self.subproject_dir in p.parents:
continue
if p.is_absolute() and p.is_dir() and srcdir / self.root_subdir in p.resolve().parents:
variables[k] = P_OBJ.DependencyVariableString(v)
for d in deps: for d in deps:
if not isinstance(d, (dependencies.Dependency, dependencies.ExternalLibrary, dependencies.InternalDependency)): if not isinstance(d, (dependencies.Dependency, dependencies.ExternalLibrary, dependencies.InternalDependency)):
raise InterpreterException('Dependencies must be external deps') raise InterpreterException('Dependencies must be external deps')
@ -2721,7 +2734,13 @@ 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]
other = os.path.join('', *parts[1:]).replace('\\', '/')
ret = os.path.join(*parts).replace('\\', '/')
if isinstance(parts[0], P_OBJ.DependencyVariableString) and '..' not in other:
return P_OBJ.DependencyVariableString(ret)
else:
return ret
def run(self) -> None: def run(self) -> None:
super().run() super().run()
@ -2763,6 +2782,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 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'

@ -479,7 +479,7 @@ class DependencyHolder(ObjectHolder[Dependency]):
KwargInfo('default_value', (str, NoneType)), KwargInfo('default_value', (str, NoneType)),
KwargInfo('pkgconfig_define', ContainerTypeInfo(list, str, pairs=True), default=[], listify=True), KwargInfo('pkgconfig_define', ContainerTypeInfo(list, str, pairs=True), default=[], listify=True),
) )
def variable_method(self, args: T.Tuple[T.Optional[str]], kwargs: 'kwargs.DependencyGetVariable') -> T.Union[str, T.List[str]]: def variable_method(self, args: T.Tuple[T.Optional[str]], kwargs: 'kwargs.DependencyGetVariable') -> str:
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)

@ -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,18 @@ 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) -> T.Union[str, DependencyVariableString]:
ret = super().op_div(other)
if '..' in other:
return ret
return DependencyVariableString(ret)

@ -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,34 @@
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'
main_d = declare_dependency(
variables: [
'dir=@0@'.format(meson.current_source_dir()),
]
)
main_mustfail = main_d.get_variable('dir') / 'subprojects/subproj3/file.txt'
if get_option('failmode') == 'parent-dir'
mustfail = sub1_mustfail
elif get_option('failmode') == 'not-installed'
mustfail = sub2_mustfail
elif get_option('failmode') == 'root-subdir'
mustfail = main_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', 'root-subdir'])

@ -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,16 @@
{
"matrix": {
"options": {
"failmode": [
{ "val": "not-installed" },
{ "val": "parent-dir" },
{ "val": "root-subdir" }
]
}
},
"stdout": [
{
"line": "test cases/failing/123 subproject sandbox violation/meson.build:24:0: ERROR: Sandbox violation: Tried to grab file file.txt from a nested subproject."
}
]
}

@ -12,8 +12,8 @@ assert(dep.get_variable(cmake : 'VAR_WITH_SPACES_PS') == 'With Spaces', 'set(PAR
assert(dep.get_variable(cmake : 'VAR_THAT_IS_UNSET', default_value : 'sentinal') == 'sentinal', 'set() to unset is incorrect') assert(dep.get_variable(cmake : 'VAR_THAT_IS_UNSET', default_value : 'sentinal') == 'sentinal', 'set() to unset is incorrect')
assert(dep.get_variable(cmake : 'CACHED_STRING_NS') == 'foo', 'set(CACHED) without spaces is incorrect') assert(dep.get_variable(cmake : 'CACHED_STRING_NS') == 'foo', 'set(CACHED) without spaces is incorrect')
assert(dep.get_variable(cmake : 'CACHED_STRING_WS') == 'foo bar', 'set(CACHED STRING) with spaces is incorrect') assert(dep.get_variable(cmake : 'CACHED_STRING_WS') == 'foo bar', 'set(CACHED STRING) with spaces is incorrect')
assert(dep.get_variable(cmake : 'CACHED_STRING_ARRAY_NS') == ['foo', 'bar'], 'set(CACHED STRING) without spaces is incorrect') assert(dep.get_variable(cmake : 'CACHED_STRING_ARRAY_NS') == 'foo;bar', 'set(CACHED STRING) without spaces is incorrect')
assert(dep.get_variable(cmake : 'CACHED_STRING_ARRAY_WS') == ['foo', 'foo bar', 'bar'], 'set(CACHED STRING[]) with spaces is incorrect') assert(dep.get_variable(cmake : 'CACHED_STRING_ARRAY_WS') == 'foo;foo bar;bar', 'set(CACHED STRING[]) with spaces is incorrect')
# We don't support this, so it should be unset. # We don't support this, so it should be unset.
assert(dep.get_variable(cmake : 'ENV{var}', default_value : 'sentinal') == 'sentinal', 'set(ENV) should be ignored') assert(dep.get_variable(cmake : 'ENV{var}', default_value : 'sentinal') == 'sentinal', 'set(ENV) should be ignored')
Loading…
Cancel
Save