Merge pull request #8884 from dcbaker/submit/type-and-annotate-install-functions

Add annotations for the various install_* functions
pull/8925/head
Jussi Pakkanen 3 years ago committed by GitHub
commit 6e0a0fd1da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      mesonbuild/backend/backends.py
  2. 14
      mesonbuild/build.py
  3. 241
      mesonbuild/interpreter/interpreter.py
  4. 34
      mesonbuild/interpreter/kwargs.py
  5. 41
      mesonbuild/interpreterbase/decorators.py
  6. 11
      mesonbuild/mesonlib/universal.py
  7. 9
      mesonbuild/minstall.py
  8. 12
      run_unittests.py
  9. 2
      test cases/failing/30 invalid man extension/test.json
  10. 2
      test cases/failing/31 no man extension/test.json
  11. 2
      test cases/failing/95 custom target install data/test.json

@ -42,9 +42,6 @@ if T.TYPE_CHECKING:
from ..interpreter import Interpreter, Test
from ..mesonlib import FileMode
InstallType = T.List[T.Tuple[str, str, T.Optional['FileMode']]]
InstallSubdirsType = T.List[T.Tuple[str, str, T.Optional['FileMode'], T.Tuple[T.Set[str], T.Set[str]]]]
# Languages that can mix with C or C++ but don't support unity builds yet
# because the syntax we use for unity builds is specific to C/++/ObjC/++.
# Assembly files cannot be unitified and neither can LLVM IR files
@ -1451,7 +1448,12 @@ class Backend:
for h in headers:
outdir = h.get_custom_install_dir()
if outdir is None:
outdir = os.path.join(incroot, h.get_install_subdir())
subdir = h.get_install_subdir()
if subdir is None:
outdir = incroot
else:
outdir = os.path.join(incroot, subdir)
for f in h.get_sources():
if not isinstance(f, File):
raise MesonException(f'Invalid header type {f!r} can\'t be installed')

@ -135,7 +135,7 @@ class DependencyOverride(HoldableObject):
class Headers(HoldableObject):
def __init__(self, sources: T.List[File], install_subdir: T.Optional[str],
install_dir: T.Optional[str], install_mode: T.Optional['FileMode'],
install_dir: T.Optional[str], install_mode: 'FileMode',
subproject: str):
self.sources = sources
self.install_subdir = install_subdir
@ -149,7 +149,7 @@ class Headers(HoldableObject):
def set_install_subdir(self, subdir: str) -> None:
self.install_subdir = subdir
def get_install_subdir(self) -> str:
def get_install_subdir(self) -> T.Optional[str]:
return self.install_subdir
def get_sources(self) -> T.List[File]:
@ -158,14 +158,14 @@ class Headers(HoldableObject):
def get_custom_install_dir(self) -> T.Optional[str]:
return self.custom_install_dir
def get_custom_install_mode(self) -> T.Optional['FileMode']:
def get_custom_install_mode(self) -> 'FileMode':
return self.custom_install_mode
class Man(HoldableObject):
def __init__(self, sources: T.List[File], install_dir: T.Optional[str],
install_mode: T.Optional['FileMode'], subproject: str,
install_mode: 'FileMode', subproject: str,
locale: T.Optional[str]):
self.sources = sources
self.custom_install_dir = install_dir
@ -176,7 +176,7 @@ class Man(HoldableObject):
def get_custom_install_dir(self) -> T.Optional[str]:
return self.custom_install_dir
def get_custom_install_mode(self) -> T.Optional['FileMode']:
def get_custom_install_mode(self) -> 'FileMode':
return self.custom_install_mode
def get_sources(self) -> T.List['File']:
@ -186,7 +186,7 @@ class Man(HoldableObject):
class InstallDir(HoldableObject):
def __init__(self, src_subdir: str, inst_subdir: str, install_dir: str,
install_mode: T.Optional['FileMode'],
install_mode: 'FileMode',
exclude: T.Tuple[T.Set[str], T.Set[str]],
strip_directory: bool, subproject: str,
from_source_dir: bool = True):
@ -2623,7 +2623,7 @@ class ConfigurationData(HoldableObject):
# during install.
class Data(HoldableObject):
def __init__(self, sources: T.List[File], install_dir: str,
install_mode: T.Optional['FileMode'], subproject: str,
install_mode: 'FileMode', subproject: str,
rename: T.List[str] = None):
self.sources = sources
self.install_dir = install_dir

@ -85,6 +85,61 @@ def _language_validator(l: T.List[str]) -> T.Optional[str]:
return None
def _install_mode_validator(mode: T.List[T.Union[str, bool, int]]) -> T.Optional[str]:
"""Validate the `install_mode` keyword argument.
This is a rather odd thing, it's a scalar, or an array of 3 values in the form:
[(str | False), (str | int | False) = False, (str | int | False) = False]
Where the second and third arguments are not required, and are considered to
default to False.
"""
if not mode:
return None
if True in mode:
return 'can only be a string or false, not true'
if len(mode) > 3:
return 'may have at most 3 elements'
perms = mode[0]
if not isinstance(perms, (str, bool)):
return 'permissions part must be a string or false'
if isinstance(perms, str):
if not len(perms) == 9:
return (f'permissions string must be exactly 9 characters, got "{len(perms)}" '
'in the form rwxr-xr-x')
for i in [0, 3, 6]:
if perms[i] not in {'-', 'r'}:
return f'bit {i} must be "-" or "r", not {perms[i]}'
for i in [1, 4, 7]:
if perms[i] not in {'-', 'w'}:
return f'bit {i} must be "-" or "w", not {perms[i]}'
for i in [2, 5]:
if perms[i] not in {'-', 'x', 's', 'S'}:
return f'bit {i} must be "-", "s", "S", or "x", not {perms[i]}'
if perms[8] not in {'-', 'x', 't', 'T'}:
return f'bit 8 must be "-", "t", "T", or "x", not {perms[8]}'
if len(mode) >= 2 and not isinstance(mode[1], (int, str, bool)):
return 'second componenent must be a string, number, or False if provided'
if len(mode) >= 3 and not isinstance(mode[2], (int, str, bool)):
return 'third componenent must be a string, number, or False if provided'
return None
def _install_mode_convertor(mode: T.Optional[T.List[T.Union[str, bool, int]]]) -> FileMode:
"""Convert the DSL form of the `install_mode` keyword arugment to `FileMode`
This is not required, and if not required returns None
TODO: It's not clear to me why this needs to be None and not just return an
emtpy FileMode.
"""
# this has already been validated by the validator
return FileMode(*[m if isinstance(m, str) else None for m in mode])
_NATIVE_KW = KwargInfo(
'native', bool,
default=False,
@ -97,6 +152,15 @@ _LANGUAGE_KW = KwargInfo(
validator=_language_validator,
convertor=lambda x: [i.lower() for i in x])
_INSTALL_MODE_KW = KwargInfo(
'install_mode',
ContainerTypeInfo(list, (str, bool, int)),
listify=True,
default=[],
validator=_install_mode_validator,
convertor=_install_mode_convertor,
)
def stringifyUserArguments(args, quote=False):
if isinstance(args, list):
@ -1817,46 +1881,50 @@ This will become a hard error in the future.''' % kwargs['input'], location=self
self.build.benchmarks.append(t)
mlog.debug('Adding benchmark', mlog.bold(t.name, True))
@FeatureNewKwargs('install_headers', '0.47.0', ['install_mode'])
@permittedKwargs({'install_dir', 'install_mode', 'subdir'})
def func_install_headers(self, node, args, kwargs):
source_files = self.source_strings_to_files(args)
install_mode = self._get_kwarg_install_mode(kwargs)
install_subdir = kwargs.get('subdir', '')
if not isinstance(install_subdir, str):
raise InterpreterException('subdir keyword argument must be a string')
elif os.path.isabs(install_subdir):
@typed_pos_args('install_headers', varargs=(str, mesonlib.File), min_varargs=1)
@typed_kwargs(
'install_headers',
KwargInfo('install_dir', (str, None)),
KwargInfo('subdir', (str, None)),
_INSTALL_MODE_KW.evolve(since='0.47.0'),
)
def func_install_headers(self, node: mparser.BaseNode,
args: T.Tuple[T.List['mesonlib.FileOrString']],
kwargs: 'kwargs.FuncInstallHeaders') -> build.Headers:
source_files = self.source_strings_to_files(args[0])
install_subdir = kwargs['subdir']
if install_subdir is not None and os.path.isabs(install_subdir):
mlog.deprecation('Subdir keyword must not be an absolute path. This will be a hard error in the next release.')
install_dir = kwargs.get('install_dir', None)
if install_dir is not None and not isinstance(install_dir, str):
raise InterpreterException('install_dir keyword argument must be a string if provided')
h = build.Headers(source_files, install_subdir, install_dir, install_mode, self.subproject)
h = build.Headers(source_files, install_subdir, kwargs['install_dir'],
kwargs['install_mode'], self.subproject)
self.build.headers.append(h)
return h
@FeatureNewKwargs('install_man', '0.47.0', ['install_mode'])
@FeatureNewKwargs('install_man', '0.58.0', ['locale'])
@permittedKwargs({'install_dir', 'install_mode', 'locale'})
def func_install_man(self, node, args, kwargs):
sources = self.source_strings_to_files(args)
@typed_pos_args('install_man', varargs=(str, mesonlib.File), min_varargs=1)
@typed_kwargs(
'install_man',
KwargInfo('install_dir', (str, None)),
KwargInfo('locale', (str, None), since='0.58.0'),
_INSTALL_MODE_KW.evolve(since='0.47.0')
)
def func_install_man(self, node: mparser.BaseNode,
args: T.Tuple[T.List['mesonlib.FileOrString']],
kwargs: 'kwargs.FuncInstallMan') -> build.Man:
# We just need to narrow this, because the input is limited to files and
# Strings as inputs, so only Files will be returned
sources = self.source_strings_to_files(args[0])
for s in sources:
try:
num = int(s.split('.')[-1])
num = int(s.rsplit('.', 1)[-1])
except (IndexError, ValueError):
num = 0
if num < 1 or num > 8:
raise InvalidArguments('Man file must have a file extension of a number between 1 and 8')
custom_install_mode = self._get_kwarg_install_mode(kwargs)
custom_install_dir = kwargs.get('install_dir', None)
locale = kwargs.get('locale')
if custom_install_dir is not None and not isinstance(custom_install_dir, str):
raise InterpreterException('install_dir must be a string.')
m = build.Man(sources, custom_install_dir, custom_install_mode, self.subproject, locale)
if not 1 <= num <= 9:
raise InvalidArguments('Man file must have a file extension of a number between 1 and 9')
m = build.Man(sources, kwargs['install_dir'], kwargs['install_mode'],
self.subproject, kwargs['locale'])
self.build.man.append(m)
return m
@ -1929,82 +1997,55 @@ This will become a hard error in the future.''' % kwargs['input'], location=self
'permissions arg to be a string or false')
return FileMode(*install_mode)
@FeatureNewKwargs('install_data', '0.46.0', ['rename'])
@FeatureNewKwargs('install_data', '0.38.0', ['install_mode'])
@permittedKwargs({'install_dir', 'install_mode', 'rename', 'sources'})
def func_install_data(self, node, args: T.List, kwargs: T.Dict[str, T.Any]):
kwsource = mesonlib.stringlistify(kwargs.get('sources', []))
raw_sources = args + kwsource
sources: T.List[mesonlib.File] = []
source_strings: T.List[str] = []
for s in raw_sources:
if isinstance(s, mesonlib.File):
sources.append(s)
elif isinstance(s, str):
source_strings.append(s)
else:
raise InvalidArguments('Argument must be string or file.')
sources += self.source_strings_to_files(source_strings)
install_dir: T.Optional[str] = kwargs.get('install_dir', None)
if install_dir is not None and not isinstance(install_dir, str):
raise InvalidArguments('Keyword argument install_dir not a string.')
install_mode = self._get_kwarg_install_mode(kwargs)
rename: T.Optional[T.List[str]] = kwargs.get('rename', None)
if rename is not None:
rename = mesonlib.stringlistify(rename)
@typed_pos_args('install_data', varargs=(str, mesonlib.File))
@typed_kwargs(
'install_data',
KwargInfo('install_dir', str),
KwargInfo('sources', ContainerTypeInfo(list, (str, mesonlib.File)), listify=True, default=[]),
KwargInfo('rename', ContainerTypeInfo(list, str), default=[], listify=True, since='0.46.0'),
_INSTALL_MODE_KW.evolve(since='0.38.0'),
)
def func_install_data(self, node: mparser.BaseNode,
args: T.Tuple[T.List['mesonlib.FileOrString']],
kwargs: 'kwargs.FuncInstallData') -> build.Data:
sources = self.source_strings_to_files(args[0] + kwargs['sources'])
rename = kwargs['rename'] or None
if rename:
if len(rename) != len(sources):
raise InvalidArguments(
'"rename" and "sources" argument lists must be the same length if "rename" is given. '
f'Rename has {len(rename)} elements and sources has {len(sources)}.')
data = build.Data(sources, install_dir, install_mode, self.subproject, rename)
data = build.Data(
sources, kwargs['install_dir'], kwargs['install_mode'],
self.subproject, rename)
self.build.data.append(data)
return data
@FeatureNewKwargs('install_subdir', '0.42.0', ['exclude_files', 'exclude_directories'])
@FeatureNewKwargs('install_subdir', '0.38.0', ['install_mode'])
@permittedKwargs({'exclude_files', 'exclude_directories', 'install_dir', 'install_mode', 'strip_directory'})
@stringArgs
def func_install_subdir(self, node, args, kwargs):
if len(args) != 1:
raise InvalidArguments('Install_subdir requires exactly one argument.')
subdir: str = args[0]
if not isinstance(subdir, str):
raise InvalidArguments('install_subdir positional argument 1 must be a string.')
if 'install_dir' not in kwargs:
raise InvalidArguments('Missing keyword argument install_dir')
install_dir: str = kwargs['install_dir']
if not isinstance(install_dir, str):
raise InvalidArguments('Keyword argument install_dir not a string.')
if 'strip_directory' in kwargs:
strip_directory: bool = kwargs['strip_directory']
if not isinstance(strip_directory, bool):
raise InterpreterException('"strip_directory" keyword must be a boolean.')
else:
strip_directory = False
if 'exclude_files' in kwargs:
exclude: T.List[str] = extract_as_list(kwargs, 'exclude_files')
for f in exclude:
if not isinstance(f, str):
raise InvalidArguments('Exclude argument not a string.')
elif os.path.isabs(f):
raise InvalidArguments('Exclude argument cannot be absolute.')
exclude_files: T.Set[str] = set(exclude)
else:
exclude_files = set()
if 'exclude_directories' in kwargs:
exclude: T.List[str] = extract_as_list(kwargs, 'exclude_directories')
for d in exclude:
if not isinstance(d, str):
raise InvalidArguments('Exclude argument not a string.')
elif os.path.isabs(d):
raise InvalidArguments('Exclude argument cannot be absolute.')
exclude_directories: T.Set[str] = set(exclude)
else:
exclude_directories = set()
exclude = (exclude_files, exclude_directories)
install_mode = self._get_kwarg_install_mode(kwargs)
idir = build.InstallDir(self.subdir, subdir, install_dir, install_mode, exclude, strip_directory, self.subproject)
@typed_pos_args('install_subdir', str)
@typed_kwargs(
'install_subdir',
KwargInfo('install_dir', str, required=True),
KwargInfo('strip_directory', bool, default=False),
KwargInfo('exclude_files', ContainerTypeInfo(list, str),
default=[], listify=True, since='0.42.0',
validator=lambda x: 'cannot be absolute' if any(os.path.isabs(d) for d in x) else None),
KwargInfo('exclude_directories', ContainerTypeInfo(list, str),
default=[], listify=True, since='0.42.0',
validator=lambda x: 'cannot be absolute' if any(os.path.isabs(d) for d in x) else None),
_INSTALL_MODE_KW.evolve(since='0.38.0'),
)
def func_install_subdir(self, node: mparser.BaseNode, args: T.Tuple[str],
kwargs: 'kwargs.FuncInstallSubdir') -> build.InstallDir:
exclude = (set(kwargs['exclude_files']), set(kwargs['exclude_directories']))
idir = build.InstallDir(
self.subdir,
args[0],
kwargs['install_dir'],
kwargs['install_mode'],
exclude,
kwargs['strip_directory'],
self.subproject)
self.build.install_dirs.append(idir)
return idir
@ -2479,6 +2520,10 @@ Try setting b_lundef to false instead.'''.format(self.coredata.options[OptionKey
if project_root / self.subproject_dir in norm.parents:
raise InterpreterException(f'Sandbox violation: Tried to grab {inputtype} {norm.name} from a nested subproject.')
@T.overload
def source_strings_to_files(self, sources: T.List['mesonlib.FileOrString']) -> T.List['mesonlib.File']: ...
def source_strings_to_files(self, sources: T.List['SourceInputs']) -> T.List['SourceOutputs']:
"""Lower inputs to a list of Targets and Files, replacing any strings.

@ -4,13 +4,13 @@
"""Keyword Argument type annotations."""
from mesonbuild import coredata
import typing as T
from typing_extensions import TypedDict, Literal
from .. import build
from ..mesonlib import MachineChoice, File
from .. import coredata
from ..mesonlib import MachineChoice, File, FileMode, FileOrString
from .interpreterobjects import EnvironmentVariablesObject
@ -102,3 +102,33 @@ class DependencyMethodPartialDependency(TypedDict):
class BuildTargeMethodExtractAllObjects(TypedDict):
recursive: bool
class FuncInstallSubdir(TypedDict):
install_dir: str
strip_directory: bool
exclude_files: T.List[str]
exclude_directories: T.List[str]
install_mode: FileMode
class FuncInstallData(TypedDict):
install_dir: str
sources: T.List[FileOrString]
rename: T.List[str]
install_mode: FileMode
class FuncInstallHeaders(TypedDict):
install_dir: T.Optional[str]
install_mode: FileMode
subdir: T.Optional[str]
class FuncInstallMan(TypedDict):
install_dir: T.Optional[str]
install_mode: FileMode
locale: T.Optional[str]

@ -272,6 +272,11 @@ class ContainerTypeInfo:
_T = T.TypeVar('_T')
class _NULL_T:
"""Special null type for evolution, this is an implementation detail."""
_NULL = _NULL_T()
class KwargInfo(T.Generic[_T]):
@ -329,6 +334,42 @@ class KwargInfo(T.Generic[_T]):
self.convertor = convertor
self.not_set_warning = not_set_warning
def evolve(self, *,
required: T.Union[bool, _NULL_T] = _NULL,
listify: T.Union[bool, _NULL_T] = _NULL,
default: T.Union[_T, None, _NULL_T] = _NULL,
since: T.Union[str, None, _NULL_T] = _NULL,
since_values: T.Union[T.Dict[str, str], None, _NULL_T] = _NULL,
deprecated: T.Union[str, None, _NULL_T] = _NULL,
deprecated_values: T.Union[T.Dict[str, str], None, _NULL_T] = _NULL,
validator: T.Union[T.Callable[[_T], T.Optional[str]], None, _NULL_T] = _NULL,
convertor: T.Union[T.Callable[[_T], TYPE_var], None, _NULL_T] = _NULL) -> 'KwargInfo':
"""Create a shallow copy of this KwargInfo, with modifications.
This allows us to create a new copy of a KwargInfo with modifications.
This allows us to use a shared kwarg that implements complex logic, but
has slight differences in usage, such as being added to different
functions in different versions of Meson.
The use the _NULL special value here allows us to pass None, which has
meaning in many of these cases. _NULL itself is never stored, always
being replaced by either the copy in self, or the provided new version.
"""
return type(self)(
self.name,
self.types,
listify=listify if not isinstance(listify, _NULL_T) else self.listify,
required=required if not isinstance(required, _NULL_T) else self.required,
default=default if not isinstance(default, _NULL_T) else self.default,
since=since if not isinstance(since, _NULL_T) else self.since,
since_values=since_values if not isinstance(since_values, _NULL_T) else self.since_values,
deprecated=deprecated if not isinstance(deprecated, _NULL_T) else self.deprecated,
deprecated_values=deprecated_values if not isinstance(deprecated_values, _NULL_T) else self.deprecated_values,
validator=validator if not isinstance(validator, _NULL_T) else self.validator,
convertor=convertor if not isinstance(convertor, _NULL_T) else self.convertor,
)
def typed_kwargs(name: str, *types: KwargInfo) -> T.Callable[..., T.Any]:
"""Decorator for type checking keyword arguments.

@ -298,8 +298,8 @@ class FileMode:
'[r-][w-][xsS-]' # Group perms
'[r-][w-][xtT-]') # Others perms
def __init__(self, perms: T.Optional[str] = None, owner: T.Optional[str] = None,
group: T.Optional[str] = None):
def __init__(self, perms: T.Optional[str] = None, owner: T.Union[str, int, None] = None,
group: T.Union[str, int, None] = None):
self.perms_s = perms
self.perms = self.perms_s_to_bits(perms)
self.owner = owner
@ -422,8 +422,11 @@ class File(HoldableObject):
def endswith(self, ending: str) -> bool:
return self.fname.endswith(ending)
def split(self, s: str) -> T.List[str]:
return self.fname.split(s)
def split(self, s: str, maxsplit: int = -1) -> T.List[str]:
return self.fname.split(s, maxsplit=maxsplit)
def rsplit(self, s: str, maxsplit: int = -1) -> T.List[str]:
return self.fname.rsplit(s, maxsplit=maxsplit)
def __eq__(self, other: object) -> bool:
if not isinstance(other, File):

@ -64,7 +64,7 @@ build definitions so that it will not break when the change happens.'''
selinux_updates: T.List[str] = []
def add_arguments(parser: argparse.Namespace) -> None:
def add_arguments(parser: argparse.ArgumentParser) -> None:
parser.add_argument('-C', default='.', dest='wd',
help='directory to cd into before running')
parser.add_argument('--profile-self', action='store_true', dest='profile',
@ -130,7 +130,8 @@ def append_to_log(lf: T.TextIO, line: str) -> None:
lf.flush()
def set_chown(path: str, user: T.Optional[str] = None, group: T.Optional[str] = None,
def set_chown(path: str, user: T.Union[str, int, None] = None,
group: T.Union[str, int, None] = None,
dir_fd: T.Optional[int] = None, follow_symlinks: bool = True) -> None:
# shutil.chown will call os.chown without passing all the parameters
# and particularly follow_symlinks, thus we replace it temporary
@ -182,12 +183,12 @@ def sanitize_permissions(path: str, umask: T.Union[str, int]) -> None:
def set_mode(path: str, mode: T.Optional['FileMode'], default_umask: T.Union[str, int]) -> None:
if mode is None or (mode.perms_s or mode.owner or mode.group) is None:
if mode is None or all(m is None for m in [mode.perms_s, mode.owner, mode.group]):
# Just sanitize permissions with the default umask
sanitize_permissions(path, default_umask)
return
# No chown() on Windows, and must set one of owner/group
if not is_windows() and (mode.owner or mode.group) is not None:
if not is_windows() and (mode.owner is not None or mode.group is not None):
try:
set_chown(path, mode.owner, mode.group, follow_symlinks=False)
except PermissionError as e:

@ -1698,6 +1698,18 @@ class InternalTests(unittest.TestCase):
_(None, mock.Mock(subproject=''), [], {'mode': 'since'})
self.assertRegex(out.getvalue(), r"""WARNING:.Project targeting '1.0'.*introduced in '1.1': "testfunc" keyword argument "mode" value "since".*""")
def test_typed_kwarg_evolve(self) -> None:
k = KwargInfo('foo', str, required=True, default='foo')
v = k.evolve(default='bar')
self.assertEqual(k.name, 'foo')
self.assertEqual(k.name, v.name)
self.assertEqual(k.types, str)
self.assertEqual(k.types, v.types)
self.assertEqual(k.required, True)
self.assertEqual(k.required, v.required)
self.assertEqual(k.default, 'foo')
self.assertEqual(v.default, 'bar')
@unittest.skipIf(is_tarball(), 'Skipping because this is a tarball release')
class DataTests(unittest.TestCase):

@ -1,7 +1,7 @@
{
"stdout": [
{
"line": "test cases/failing/30 invalid man extension/meson.build:2:0: ERROR: Man file must have a file extension of a number between 1 and 8"
"line": "test cases/failing/30 invalid man extension/meson.build:2:0: ERROR: Man file must have a file extension of a number between 1 and 9"
}
]
}

@ -1,7 +1,7 @@
{
"stdout": [
{
"line": "test cases/failing/31 no man extension/meson.build:2:0: ERROR: Man file must have a file extension of a number between 1 and 8"
"line": "test cases/failing/31 no man extension/meson.build:2:0: ERROR: Man file must have a file extension of a number between 1 and 9"
}
]
}

@ -1,7 +1,7 @@
{
"stdout": [
{
"line": "test cases/failing/95 custom target install data/meson.build:11:0: ERROR: Argument must be string or file."
"line": "test cases/failing/95 custom target install data/meson.build:11:0: ERROR: install_data argument 1 was of type \"CustomTarget\" but should have been one of: \"str\", \"File\""
}
]
}

Loading…
Cancel
Save