From f2ad5e377e0c70f4c7292f6abb33cbaa283d84b2 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Mon, 14 Jun 2021 15:36:16 -0700 Subject: [PATCH 01/17] backend: Headers.install_subdir is allowed to be None But we don't properly handle that. --- mesonbuild/backend/backends.py | 7 ++++++- mesonbuild/build.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py index 2652ae6b0..4e1ac1e15 100644 --- a/mesonbuild/backend/backends.py +++ b/mesonbuild/backend/backends.py @@ -1445,7 +1445,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') diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 414a4f814..83dd3f0cf 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -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]: From 5d3a60ae65f44ea300d61dd0f08ad01618a105a6 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Mon, 14 Jun 2021 15:36:16 -0700 Subject: [PATCH 02/17] minstall: fix type annotation for add_arguments This takes an `argparse.ArgumentParser` instance, not a namespace --- mesonbuild/minstall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mesonbuild/minstall.py b/mesonbuild/minstall.py index e14a04704..d91f61b6f 100644 --- a/mesonbuild/minstall.py +++ b/mesonbuild/minstall.py @@ -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', From 86da1311299be28c178b16c3b83844c1374c392d Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Mon, 14 Jun 2021 15:36:16 -0700 Subject: [PATCH 03/17] mesonlib: Fix FileMode type annotations --- mesonbuild/mesonlib/universal.py | 4 ++-- mesonbuild/minstall.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mesonbuild/mesonlib/universal.py b/mesonbuild/mesonlib/universal.py index 3714ecdea..94dc0a98c 100644 --- a/mesonbuild/mesonlib/universal.py +++ b/mesonbuild/mesonlib/universal.py @@ -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 diff --git a/mesonbuild/minstall.py b/mesonbuild/minstall.py index d91f61b6f..9bb5ba775 100644 --- a/mesonbuild/minstall.py +++ b/mesonbuild/minstall.py @@ -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 From f276b2349a604ee84ea06ada55ae69e9ac4db4b2 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Mon, 14 Jun 2021 15:36:16 -0700 Subject: [PATCH 04/17] mesonlib: add rsplit and and maxsplit Since string has a maxsplit as well, we should implement that for polymorphism --- mesonbuild/mesonlib/universal.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mesonbuild/mesonlib/universal.py b/mesonbuild/mesonlib/universal.py index 94dc0a98c..160cc377d 100644 --- a/mesonbuild/mesonlib/universal.py +++ b/mesonbuild/mesonlib/universal.py @@ -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): From a024f432dd1cd406fa80334f63e91939536d3f79 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Mon, 14 Jun 2021 15:36:16 -0700 Subject: [PATCH 05/17] interpreter: add overload for source_strings_to_files when only passing strings or Files we only get back Files. This is useful for the install_* methods --- mesonbuild/interpreter/interpreter.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index be17c9a9c..75f55fd3e 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -2480,6 +2480,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. From dd296f321ba102052dcf3d0a67e4321752d560a2 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Mon, 14 Jun 2021 15:36:16 -0700 Subject: [PATCH 06/17] interpreterbase: Add evolve to KwargInfo This works just like OptionKey.evolve, pass new keyword arguments to override old ones, otherwise the original versions are copied to the new one. --- mesonbuild/interpreterbase/decorators.py | 41 ++++++++++++++++++++++++ run_unittests.py | 12 +++++++ 2 files changed, 53 insertions(+) diff --git a/mesonbuild/interpreterbase/decorators.py b/mesonbuild/interpreterbase/decorators.py index a011b6681..b05df5bcd 100644 --- a/mesonbuild/interpreterbase/decorators.py +++ b/mesonbuild/interpreterbase/decorators.py @@ -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. diff --git a/run_unittests.py b/run_unittests.py index b55ba96f6..6ca1a81da 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -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): From 3d940fec980f61c8b5a6cb74a6af61c59c307db9 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Mon, 14 Jun 2021 15:36:16 -0700 Subject: [PATCH 07/17] interpreter: use typed_pos_args for install_subdir --- mesonbuild/interpreter/interpreter.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 75f55fd3e..82bb71dd5 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -1965,13 +1965,9 @@ This will become a hard error in the future.''' % kwargs['input'], location=self @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.') + @typed_pos_args('install_subdir', str) + def func_install_subdir(self, node: mparser.BaseNode, args: T.Tuple[str], kwargs): + subdir = args[0] if 'install_dir' not in kwargs: raise InvalidArguments('Missing keyword argument install_dir') install_dir: str = kwargs['install_dir'] From 596c8d4af50d0e5a25ee0ee1e177e46b6c7ad22e Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Mon, 14 Jun 2021 15:36:16 -0700 Subject: [PATCH 08/17] interpreter: use typed_kwargs for install_subdir --- mesonbuild/interpreter/interpreter.py | 128 ++++++++++++++++++-------- mesonbuild/interpreter/kwargs.py | 12 ++- 2 files changed, 99 insertions(+), 41 deletions(-) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 82bb71dd5..287572630 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -85,6 +85,63 @@ 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]]]) -> T.Optional[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. + """ + if mode is None: + return None + # 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 +154,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): @@ -1962,46 +2028,30 @@ This will become a hard error in the future.''' % kwargs['input'], location=self 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'}) @typed_pos_args('install_subdir', str) - def func_install_subdir(self, node: mparser.BaseNode, args: T.Tuple[str], kwargs): - subdir = args[0] - 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_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 diff --git a/mesonbuild/interpreter/kwargs.py b/mesonbuild/interpreter/kwargs.py index 1cc208213..e9042af82 100644 --- a/mesonbuild/interpreter/kwargs.py +++ b/mesonbuild/interpreter/kwargs.py @@ -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 from .interpreterobjects import EnvironmentVariablesObject @@ -102,3 +102,11 @@ 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 From a551e7613728051a398f296cda40a4c8cf25dcbb Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Mon, 14 Jun 2021 15:36:16 -0700 Subject: [PATCH 09/17] interpreter: use typed_pos_args for install_data --- mesonbuild/interpreter/interpreter.py | 6 ++++-- test cases/failing/95 custom target install data/test.json | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 287572630..d5bfdb903 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -1999,9 +1999,11 @@ This will become a hard error in the future.''' % kwargs['input'], location=self @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]): + @typed_pos_args('install_data', varargs=(str, mesonlib.File)) + def func_install_data(self, node: mparser.BaseNode, args: T.Tuple[T.List['mesonlib.FileOrString']], + kwargs: T.Dict[str, T.Any]): kwsource = mesonlib.stringlistify(kwargs.get('sources', [])) - raw_sources = args + kwsource + raw_sources = args[0] + kwsource sources: T.List[mesonlib.File] = [] source_strings: T.List[str] = [] for s in raw_sources: diff --git a/test cases/failing/95 custom target install data/test.json b/test cases/failing/95 custom target install data/test.json index 64ef53070..c8004d660 100644 --- a/test cases/failing/95 custom target install data/test.json +++ b/test cases/failing/95 custom target install data/test.json @@ -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\"" } ] } From ea3d85a1c0933f7b5b0588e3137c1dd8a50ce5f3 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Mon, 14 Jun 2021 15:36:16 -0700 Subject: [PATCH 10/17] interpreter: use typed_kwargs for install_data --- mesonbuild/interpreter/interpreter.py | 41 +++++++++++---------------- mesonbuild/interpreter/kwargs.py | 10 ++++++- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index d5bfdb903..78a1420af 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -1996,37 +1996,28 @@ 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'}) @typed_pos_args('install_data', varargs=(str, mesonlib.File)) - def func_install_data(self, node: mparser.BaseNode, args: T.Tuple[T.List['mesonlib.FileOrString']], - kwargs: T.Dict[str, T.Any]): - kwsource = mesonlib.stringlistify(kwargs.get('sources', [])) - raw_sources = args[0] + 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_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 diff --git a/mesonbuild/interpreter/kwargs.py b/mesonbuild/interpreter/kwargs.py index e9042af82..300bb1c26 100644 --- a/mesonbuild/interpreter/kwargs.py +++ b/mesonbuild/interpreter/kwargs.py @@ -10,7 +10,7 @@ from typing_extensions import TypedDict, Literal from .. import build from .. import coredata -from ..mesonlib import MachineChoice, File, FileMode +from ..mesonlib import MachineChoice, File, FileMode, FileOrString from .interpreterobjects import EnvironmentVariablesObject @@ -110,3 +110,11 @@ class FuncInstallSubdir(TypedDict): 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 From 251dff56fb074cd718da81c21e104cd26d897869 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Mon, 14 Jun 2021 15:36:16 -0700 Subject: [PATCH 11/17] interpreter: use typed_pos_args for install_headers --- mesonbuild/interpreter/interpreter.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 78a1420af..3b881e8eb 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -1884,10 +1884,11 @@ 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) + @typed_pos_args('install_headers', varargs=(str, mesonlib.File), min_varargs=1) + def func_install_headers(self, node: mparser.BaseNode, + args: T.Tuple[T.List['mesonlib.FileOrString']], + kwargs) -> build.Headers: + source_files = self.source_strings_to_files(args[0]) install_mode = self._get_kwarg_install_mode(kwargs) install_subdir = kwargs.get('subdir', '') From f4d2efbed3a589bc7dbb129778eb2debb1f5fb2e Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Mon, 14 Jun 2021 15:36:16 -0700 Subject: [PATCH 12/17] interpreter: use typed_kwargs for install_headers --- mesonbuild/interpreter/interpreter.py | 23 +++++++++++------------ mesonbuild/interpreter/kwargs.py | 7 +++++++ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 3b881e8eb..851305411 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -1885,23 +1885,22 @@ This will become a hard error in the future.''' % kwargs['input'], location=self mlog.debug('Adding benchmark', mlog.bold(t.name, True)) @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) -> build.Headers: + kwargs: 'kwargs.FuncInstallHeaders') -> build.Headers: source_files = self.source_strings_to_files(args[0]) - 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): + 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 diff --git a/mesonbuild/interpreter/kwargs.py b/mesonbuild/interpreter/kwargs.py index 300bb1c26..1906d8556 100644 --- a/mesonbuild/interpreter/kwargs.py +++ b/mesonbuild/interpreter/kwargs.py @@ -118,3 +118,10 @@ class FuncInstallData(TypedDict): 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] From 9611bd82443230a17f711a25688f05f4f816a426 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Mon, 14 Jun 2021 15:36:16 -0700 Subject: [PATCH 13/17] interpreter: use typed_pos_args for install_man --- mesonbuild/interpreter/interpreter.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 851305411..0b496b38b 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -1908,8 +1908,11 @@ This will become a hard error in the future.''' % kwargs['input'], location=self @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) + def func_install_man(self, node: mparser.BaseNode, + args: T.Tuple[T.List['SourceInputs']], + kwargs) -> build.Man: + sources = self.source_strings_to_files(args[0]) for s in sources: try: num = int(s.split('.')[-1]) From 7619f31f716ef24fcd3ddee7275f4aa48c2e4b5c Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Mon, 14 Jun 2021 15:36:17 -0700 Subject: [PATCH 14/17] interpreter: man sections can be up to 9 on many platforms Linux and FreeBSD use section 9 for kernel man pages, so we should allow that. --- mesonbuild/interpreter/interpreter.py | 7 ++++--- test cases/failing/30 invalid man extension/test.json | 2 +- test cases/failing/31 no man extension/test.json | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 0b496b38b..d7e468266 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -1915,11 +1915,12 @@ This will become a hard error in the future.''' % kwargs['input'], location=self 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') + if not 1 <= num <= 9: + raise InvalidArguments('Man file must have a file extension of a number between 1 and 9') + custom_install_mode = self._get_kwarg_install_mode(kwargs) custom_install_dir = kwargs.get('install_dir', None) locale = kwargs.get('locale') diff --git a/test cases/failing/30 invalid man extension/test.json b/test cases/failing/30 invalid man extension/test.json index 3f77a04f4..3e5f45de5 100644 --- a/test cases/failing/30 invalid man extension/test.json +++ b/test cases/failing/30 invalid man extension/test.json @@ -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" } ] } diff --git a/test cases/failing/31 no man extension/test.json b/test cases/failing/31 no man extension/test.json index 6e1f542c4..0972da1f3 100644 --- a/test cases/failing/31 no man extension/test.json +++ b/test cases/failing/31 no man extension/test.json @@ -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" } ] } From 7213b7d81fb2cc53d7fdca35409281045bae0fdc Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Mon, 14 Jun 2021 15:36:17 -0700 Subject: [PATCH 15/17] interpreter: use typed_kwargs for install_man --- mesonbuild/interpreter/interpreter.py | 24 ++++++++++++------------ mesonbuild/interpreter/kwargs.py | 7 +++++++ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index d7e468266..d522d2528 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -1905,13 +1905,18 @@ This will become a hard error in the future.''' % kwargs['input'], location=self return h - @FeatureNewKwargs('install_man', '0.47.0', ['install_mode']) - @FeatureNewKwargs('install_man', '0.58.0', ['locale']) - @permittedKwargs({'install_dir', 'install_mode', 'locale'}) @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['SourceInputs']], - kwargs) -> build.Man: + 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: @@ -1921,13 +1926,8 @@ This will become a hard error in the future.''' % kwargs['input'], location=self if not 1 <= num <= 9: raise InvalidArguments('Man file must have a file extension of a number between 1 and 9') - 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) + m = build.Man(sources, kwargs['install_dir'], kwargs['install_mode'], + self.subproject, kwargs['locale']) self.build.man.append(m) return m diff --git a/mesonbuild/interpreter/kwargs.py b/mesonbuild/interpreter/kwargs.py index 1906d8556..3c3ecf641 100644 --- a/mesonbuild/interpreter/kwargs.py +++ b/mesonbuild/interpreter/kwargs.py @@ -125,3 +125,10 @@ 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] From d636b92c1adc1588ff11b6ee4972c4bdd686f433 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Mon, 14 Jun 2021 15:36:17 -0700 Subject: [PATCH 16/17] install_*: FileMode doesn't need to be None There's no reason to allow None into the backend, it already has code to check that all of the values of the FileMode object are None, so let's use that, which is much simpler all the way down. --- mesonbuild/backend/backends.py | 3 --- mesonbuild/build.py | 12 ++++++------ mesonbuild/interpreter/interpreter.py | 4 +--- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py index 4e1ac1e15..8df9705c9 100644 --- a/mesonbuild/backend/backends.py +++ b/mesonbuild/backend/backends.py @@ -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 diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 83dd3f0cf..ee3b9c9ea 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -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 @@ -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): @@ -2611,7 +2611,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 diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index d522d2528..8d1b693b8 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -128,7 +128,7 @@ def _install_mode_validator(mode: T.List[T.Union[str, bool, int]]) -> T.Optional return None -def _install_mode_convertor(mode: T.Optional[T.List[T.Union[str, bool, int]]]) -> T.Optional[FileMode]: +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 @@ -136,8 +136,6 @@ def _install_mode_convertor(mode: T.Optional[T.List[T.Union[str, bool, int]]]) - TODO: It's not clear to me why this needs to be None and not just return an emtpy FileMode. """ - if mode is None: - return None # this has already been validated by the validator return FileMode(*[m if isinstance(m, str) else None for m in mode]) From 132420a05901deb8be5524c17fde7031d5d9b8a1 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Mon, 14 Jun 2021 15:36:17 -0700 Subject: [PATCH 17/17] minstall: make intentions clearer The existing code works, but it probably doesn't do what the author thought it would do. `(x or y or z) is not None` works by checking that each of those things are *truthy* in turn, and returning the first truthy value, which is compared against None. Using `all()` makes it very clear that what you want to do is make sure that each value is not None. --- mesonbuild/minstall.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mesonbuild/minstall.py b/mesonbuild/minstall.py index 9bb5ba775..7011c42f1 100644 --- a/mesonbuild/minstall.py +++ b/mesonbuild/minstall.py @@ -183,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: