From c3c30d4b060239654c9b848092692ab346ebed9d Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Tue, 10 Aug 2021 20:07:39 -0700 Subject: [PATCH] interpreter: Use typed_kwargs for func_custom_target This does not convert the build side, or remove any of the checking it does. We still need that for other callers of custom target. What we'll do for those is add an internal interface that defaults things, then we'll be able to have those callers do their own validation, and the CustomTarget validation machinary can be removed. Fixes #9096 --- mesonbuild/build.py | 41 ++++++--- mesonbuild/interpreter/interpreter.py | 90 ++++++++++++++++--- mesonbuild/interpreter/kwargs.py | 26 +++++- mesonbuild/interpreter/type_checking.py | 46 +++++++--- mesonbuild/modules/pkgconfig.py | 9 +- mesonbuild/modules/qt.py | 16 ++-- .../modules/unstable_external_project.py | 2 +- 7 files changed, 181 insertions(+), 49 deletions(-) diff --git a/mesonbuild/build.py b/mesonbuild/build.py index a7051ade8..c2649ada6 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -535,18 +535,27 @@ class Target(HoldableObject): def get_default_install_dir(self, env: environment.Environment) -> T.Tuple[str, str]: raise NotImplementedError + def get_custom_install_dir(self) -> T.List[T.Union[str, bool]]: + raise NotImplementedError + def get_install_dir(self, environment: environment.Environment) -> T.Tuple[T.Any, str, bool]: # Find the installation directory. default_install_dir, install_dir_name = self.get_default_install_dir(environment) outdirs = self.get_custom_install_dir() - if outdirs[0] is not None and outdirs[0] != default_install_dir and outdirs[0] is not True: + if outdirs and outdirs[0] != default_install_dir and outdirs[0] is not True: # Either the value is set to a non-default value, or is set to # False (which means we want this specific output out of many # outputs to not be installed). custom_install_dir = True else: custom_install_dir = False - outdirs[0] = default_install_dir + # if outdirs is empty we need to set to something, otherwise we set + # only the first value to the default + if outdirs: + outdirs[0] = default_install_dir + else: + outdirs = [default_install_dir] + return outdirs, install_dir_name, custom_install_dir def get_basename(self) -> str: @@ -641,6 +650,8 @@ class Target(HoldableObject): class BuildTarget(Target): known_kwargs = known_build_target_kwargs + install_dir: T.List[T.Union[str, bool]] + def __init__(self, name: str, subdir: str, subproject: str, for_machine: MachineChoice, sources: T.List['SourceOutputs'], objects, environment: environment.Environment, kwargs): super().__init__(name, subdir, subproject, True, for_machine) @@ -997,7 +1008,7 @@ class BuildTarget(Target): def get_default_install_dir(self, environment: environment.Environment) -> T.Tuple[str, str]: return environment.get_libdir(), '{libdir}' - def get_custom_install_dir(self): + def get_custom_install_dir(self) -> T.List[T.Union[str, bool]]: return self.install_dir def get_custom_install_mode(self) -> T.Optional['FileMode']: @@ -1081,7 +1092,7 @@ class BuildTarget(Target): self.add_deps(deplist) # If an item in this list is False, the output corresponding to # the list index of that item will not be installed - self.install_dir = typeslistify(kwargs.get('install_dir', [None]), + self.install_dir = typeslistify(kwargs.get('install_dir', []), (str, bool)) self.install_mode = kwargs.get('install_mode', None) self.install_tag = stringlistify(kwargs.get('install_tag', [None])) @@ -2292,6 +2303,8 @@ class CustomTarget(Target, CommandBase): 'env', } + install_dir: T.List[T.Union[str, bool]] + def __init__(self, name: str, subdir: str, subproject: str, kwargs: T.Dict[str, T.Any], absolute_paths: bool = False, backend: T.Optional['Backend'] = None): self.typename = 'custom' @@ -2418,15 +2431,19 @@ class CustomTarget(Target, CommandBase): self.install_mode = kwargs.get('install_mode', None) # If only one tag is provided, assume all outputs have the same tag. # Otherwise, we must have as much tags as outputs. - self.install_tag = typeslistify(kwargs.get('install_tag', [None]), (str, bool)) - if len(self.install_tag) == 1: - self.install_tag = self.install_tag * len(self.outputs) - elif len(self.install_tag) != len(self.outputs): - m = f'Target {self.name!r} has {len(self.outputs)} outputs but {len(self.install_tag)} "install_tag"s were found.' + install_tag: T.List[T.Union[str, bool, None]] = typeslistify(kwargs.get('install_tag', []), (str, bool, type(None))) + if not install_tag: + self.install_tag = [None] * len(self.outputs) + elif len(install_tag) == 1: + self.install_tag = install_tag * len(self.outputs) + elif install_tag and len(install_tag) != len(self.outputs): + m = f'Target {self.name!r} has {len(self.outputs)} outputs but {len(install_tag)} "install_tag"s were found.' raise InvalidArguments(m) + else: + self.install_tag = install_tag else: self.install = False - self.install_dir = [None] + self.install_dir = [] self.install_mode = None self.install_tag = [] if kwargs.get('build_always') is not None and kwargs.get('build_always_stale') is not None: @@ -2459,7 +2476,7 @@ class CustomTarget(Target, CommandBase): def should_install(self) -> bool: return self.install - def get_custom_install_dir(self): + def get_custom_install_dir(self) -> T.List[T.Union[str, bool]]: return self.install_dir def get_custom_install_mode(self) -> T.Optional['FileMode']: @@ -2693,7 +2710,7 @@ class CustomTargetIndex(HoldableObject): def extract_all_objects_recurse(self) -> T.List[T.Union[str, 'ExtractedObjects']]: return self.target.extract_all_objects_recurse() - def get_custom_install_dir(self): + def get_custom_install_dir(self) -> T.List[T.Union[str, bool]]: return self.target.get_custom_install_dir() class ConfigurationData(HoldableObject): diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 78c8f95b7..c5997f2ab 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -52,11 +52,12 @@ from .interpreterobjects import ( from .type_checking import ( COMMAND_KW, DEPENDS_KW, + DEPEND_FILES_KW, DEPFILE_KW, ENV_KW, INSTALL_MODE_KW, LANGUAGE_KW, - NATIVE_KW, + NATIVE_KW, OVERRIDE_OPTIONS_KW, REQUIRED_KW, NoneType, in_set_validator, @@ -89,6 +90,18 @@ if T.TYPE_CHECKING: build.GeneratedList] +def _output_validator(outputs: T.List[str]) -> T.Optional[str]: + for i in outputs: + if i == '': + return 'Output must not be empty.' + elif i.strip() == '': + return 'Output must not consist only of whitespace.' + elif has_path_sep(i): + return f'Output {i!r} must not contain a path segment.' + + return None + + def stringifyUserArguments(args, quote=False): if isinstance(args, list): return '[%s]' % ', '.join([stringifyUserArguments(x, True) for x in args]) @@ -1626,20 +1639,71 @@ external dependencies (including libraries) must go to "dependencies".''') def func_subdir_done(self, node, args, kwargs): raise SubdirDoneRequest() - @FeatureNewKwargs('custom_target', '0.60.0', ['install_tag']) - @FeatureNewKwargs('custom_target', '0.57.0', ['env']) - @FeatureNewKwargs('custom_target', '0.48.0', ['console']) - @FeatureNewKwargs('custom_target', '0.47.0', ['install_mode', 'build_always_stale']) - @FeatureNewKwargs('custom_target', '0.40.0', ['build_by_default']) - @FeatureNewKwargs('custom_target', '0.59.0', ['feed']) - @permittedKwargs({'input', 'output', 'command', 'install', 'install_dir', 'install_mode', - 'build_always', 'capture', 'depends', 'depend_files', 'depfile', - 'build_by_default', 'build_always_stale', 'console', 'env', - 'feed', 'install_tag'}) @typed_pos_args('custom_target', optargs=[str]) - def func_custom_target(self, node: mparser.FunctionNode, args: T.Tuple[T.Optional[str]], kwargs: 'TYPE_kwargs') -> build.CustomTarget: - if 'depfile' in kwargs and ('@BASENAME@' in kwargs['depfile'] or '@PLAINNAME@' in kwargs['depfile']): + @typed_kwargs( + 'custom_target', + COMMAND_KW, + DEPEND_FILES_KW, + DEPENDS_KW, + DEPFILE_KW, + ENV_KW.evolve(since='0.57.0'), + INSTALL_MODE_KW.evolve(since='0.47.0'), + OVERRIDE_OPTIONS_KW, + KwargInfo('build_by_default', (bool, type(None)), since='0.40.0'), + KwargInfo('build_always', (bool, type(None)), deprecated='0.47.0'), + KwargInfo('build_always_stale', (bool, type(None)), since='0.47.0'), + KwargInfo('feed', bool, default=False, since='0.59.0'), + KwargInfo('capture', bool, default=False), + KwargInfo('console', bool, default=False, since='0.48.0'), + KwargInfo('install', bool, default=False), + KwargInfo('install_dir', ContainerTypeInfo(list, (str, bool)), listify=True, default=[]), + KwargInfo( + 'output', + ContainerTypeInfo(list, str, allow_empty=False), + listify=True, + required=True, + default=[], + validator=_output_validator, + ), + KwargInfo( + 'input', + ContainerTypeInfo(list, (str, mesonlib.File, ExternalProgram, build.BuildTarget, build.CustomTarget, build.CustomTargetIndex, build.ExtractedObjects, build.GeneratedList)), + listify=True, + default=[], + ), + KwargInfo('install_tag', ContainerTypeInfo(list, (str, bool)), listify=True, default=[], since='0.60.0'), + ) + def func_custom_target(self, node: mparser.FunctionNode, args: T.Tuple[str], + kwargs: 'kwargs.CustomTarget') -> build.CustomTarget: + if kwargs['depfile'] and ('@BASENAME@' in kwargs['depfile'] or '@PLAINNAME@' in kwargs['depfile']): FeatureNew.single_use('substitutions in custom_target depfile', '0.47.0', self.subproject) + + # Don't mutate the kwargs + kwargs = kwargs.copy() + + # Remap build_always to build_by_default and build_always_stale + if kwargs['build_always'] is not None and kwargs['build_always_stale'] is not None: + raise InterpreterException('CustomTarget: "build_always" and "build_always_stale" are mutually exclusive') + + if kwargs['build_by_default'] is None and kwargs['install']: + kwargs['build_by_default'] = True + + elif kwargs['build_always'] is not None: + if kwargs['build_by_default'] is None: + kwargs['build_by_default'] = kwargs['build_always'] + kwargs['build_always_stale'] = kwargs['build_by_default'] + + # Set this to None to satisfy process_kwargs + kwargs['build_always'] = None + + # These are are nullaable so that we can konw whether they're explicitly + # set or not. If they haven't been overwritten, set them to their true + # default + if kwargs['build_by_default'] is None: + kwargs['build_by_default'] = False + if kwargs['build_always_stale'] is None: + kwargs['build_always_stale'] = False + return self._func_custom_target_impl(node, args, kwargs) def _func_custom_target_impl(self, node, args, kwargs): diff --git a/mesonbuild/interpreter/kwargs.py b/mesonbuild/interpreter/kwargs.py index 4281ee420..2229984c9 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, FileOrString +from ..mesonlib import MachineChoice, File, FileMode, FileOrString, OptionKey from ..programs import ExternalProgram @@ -162,3 +162,27 @@ class RunTarget(TypedDict): command: T.List[T.Union[str, build.BuildTarget, build.CustomTarget, ExternalProgram, File]] depends: T.List[T.Union[build.BuildTarget, build.CustomTarget]] env: build.EnvironmentVariables + + +class CustomTarget(TypedDict): + + build_always: bool + build_always_stale: bool + build_by_default: bool + capture: bool + command: T.List[T.Union[str, build.BuildTarget, build.CustomTarget, + build.CustomTargetIndex, ExternalProgram, File]] + consonle: bool + depend_files: T.List[FileOrString] + depends: T.List[T.Union[build.BuildTarget, build.CustomTarget]] + depfile: T.Optional[str] + env: build.EnvironmentVariables + feed: bool + input: T.List[T.Union[str, build.BuildTarget, build.CustomTarget, build.CustomTargetIndex, + build.ExtractedObjects, build.GeneratedList, ExternalProgram, File]] + install: bool + install_dir: T.List[T.Union[str, bool]] + install_mode: FileMode + install_tag: T.List[T.Union[str, bool]] + output: T.List[str] + override_options: T.Dict[OptionKey, str] diff --git a/mesonbuild/interpreter/type_checking.py b/mesonbuild/interpreter/type_checking.py index fb63fca95..d8f1030e8 100644 --- a/mesonbuild/interpreter/type_checking.py +++ b/mesonbuild/interpreter/type_checking.py @@ -6,11 +6,11 @@ import typing as T from .. import compilers -from ..build import EnvironmentVariables, CustomTarget, BuildTarget +from ..build import EnvironmentVariables, CustomTarget, BuildTarget, CustomTargetIndex from ..coredata import UserFeatureOption from ..interpreterbase import TYPE_var from ..interpreterbase.decorators import KwargInfo, ContainerTypeInfo -from ..mesonlib import File, FileMode, MachineChoice, listify, has_path_sep +from ..mesonlib import File, FileMode, MachineChoice, listify, has_path_sep, OptionKey from ..programs import ExternalProgram # Helper definition for type checks that are `Optional[T]` @@ -158,13 +158,21 @@ def _env_validator(value: T.Union[EnvironmentVariables, T.List['TYPE_var'], T.Di return None -def _env_convertor(value: T.Union[EnvironmentVariables, T.List[str], T.Dict[str, str], str, None]) -> EnvironmentVariables: - def splitter(input: str) -> T.Tuple[str, str]: - a, b = input.split('=', 1) - return (a.strip(), b.strip()) - if isinstance(value, (str, list)): - return EnvironmentVariables(dict(splitter(v) for v in listify(value))) +def split_equal_string(input: str) -> T.Tuple[str, str]: + """Split a string in the form `x=y` + + This assumes that the string has already been validated to split properly. + """ + a, b = input.split('=', 1) + return (a, b) + + +def _env_convertor(value: T.Union[EnvironmentVariables, T.List[str], T.List[T.List[str]], T.Dict[str, str], str, None]) -> EnvironmentVariables: + if isinstance(value, str): + return EnvironmentVariables(dict([split_equal_string(value)])) + elif isinstance(value, list): + return EnvironmentVariables(dict(split_equal_string(v) for v in listify(value))) elif isinstance(value, dict): return EnvironmentVariables(value) elif value is None: @@ -199,11 +207,29 @@ DEPEND_FILES_KW: KwargInfo[T.List[T.Union[str, File]]] = KwargInfo( default=[], ) -COMMAND_KW: KwargInfo[T.List[T.Union[BuildTarget, CustomTarget, ExternalProgram, File]]] = KwargInfo( +COMMAND_KW: KwargInfo[T.List[T.Union[str, BuildTarget, CustomTarget, CustomTargetIndex, ExternalProgram, File]]] = KwargInfo( 'command', # TODO: should accept CustomTargetIndex as well? - ContainerTypeInfo(list, (str, BuildTarget, CustomTarget, ExternalProgram, File), allow_empty=False), + ContainerTypeInfo(list, (str, BuildTarget, CustomTarget, CustomTargetIndex, ExternalProgram, File), allow_empty=False), required=True, listify=True, default=[], ) + +def _override_options_convertor(raw: T.List[str]) -> T.Dict[OptionKey, str]: + output: T.Dict[OptionKey, str] = {} + for each in raw: + k, v = split_equal_string(each) + output[OptionKey.from_string(k)] = v + return output + + +OVERRIDE_OPTIONS_KW: KwargInfo[T.List[str]] = KwargInfo( + 'override_options', + ContainerTypeInfo(list, str), + listify=True, + default=[], + # Reusing the env validator is a littl overkill, but nicer than duplicating the code + validator=_env_validator, + convertor=_override_options_convertor, +) diff --git a/mesonbuild/modules/pkgconfig.py b/mesonbuild/modules/pkgconfig.py index 7be479667..c9bec4ab1 100644 --- a/mesonbuild/modules/pkgconfig.py +++ b/mesonbuild/modules/pkgconfig.py @@ -381,7 +381,8 @@ class PkgConfigModule(ExtensionModule): if uninstalled: install_dir = os.path.dirname(state.backend.get_target_filename_abs(l)) else: - install_dir = l.get_custom_install_dir()[0] + _i = l.get_custom_install_dir() + install_dir = _i[0] if _i else None if install_dir is False: continue is_custom_target = isinstance(l, (build.CustomTarget, build.CustomTargetIndex)) @@ -471,9 +472,9 @@ class PkgConfigModule(ExtensionModule): raise mesonlib.MesonException('Pkgconfig_gen first positional argument must be a library object') default_name = mainlib.name default_description = state.project_name + ': ' + mainlib.name - install_dir = mainlib.get_custom_install_dir()[0] - if isinstance(install_dir, str): - default_install_dir = os.path.join(install_dir, 'pkgconfig') + install_dir = mainlib.get_custom_install_dir() + if install_dir and isinstance(install_dir[0], str): + default_install_dir = os.path.join(install_dir[0], 'pkgconfig') elif len(args) > 1: raise mesonlib.MesonException('Too many positional arguments passed to Pkgconfig_gen.') diff --git a/mesonbuild/modules/qt.py b/mesonbuild/modules/qt.py index 7dafad735..874bcb1cd 100644 --- a/mesonbuild/modules/qt.py +++ b/mesonbuild/modules/qt.py @@ -556,14 +556,14 @@ class QtBaseModule(ExtensionModule): else: outdir = state.subdir cmd = [self.tools['lrelease'], '@INPUT@', '-qm', '@OUTPUT@'] - lrelease_kwargs = {'output': '@BASENAME@.qm', - 'input': ts, - 'install': kwargs['install'], - 'install_tag': 'i18n', - 'build_by_default': kwargs['build_by_default'], - 'command': cmd} - if install_dir is not None: - lrelease_kwargs['install_dir'] = install_dir + lrelease_kwargs: T.Dict[str, T.Any] = { + 'output': '@BASENAME@.qm', + 'input': ts, + 'install': kwargs['install'], + 'install_dir': install_dir or [], + 'install_tag': 'i18n', + 'build_by_default': kwargs['build_by_default'], + 'command': cmd} lrelease_target = build.CustomTarget(f'qt{self.qt_version}-compile-{ts}', outdir, state.subproject, lrelease_kwargs) translations.append(lrelease_target) if qresource: diff --git a/mesonbuild/modules/unstable_external_project.py b/mesonbuild/modules/unstable_external_project.py index b8e98504b..80cf41b65 100644 --- a/mesonbuild/modules/unstable_external_project.py +++ b/mesonbuild/modules/unstable_external_project.py @@ -71,7 +71,7 @@ class ExternalProject(NewExtensionModule): self.targets = self._create_targets() - def _configure(self, state: ModuleState): + def _configure(self, state: ModuleState) -> None: if self.configure_command == 'waf': FeatureNew('Waf external project', '0.60.0').use(self.subproject) waf = state.find_program('waf')