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
pull/9329/head
Dylan Baker 3 years ago committed by Daniel Mensinger
parent bb706231bd
commit c3c30d4b06
  1. 41
      mesonbuild/build.py
  2. 90
      mesonbuild/interpreter/interpreter.py
  3. 26
      mesonbuild/interpreter/kwargs.py
  4. 46
      mesonbuild/interpreter/type_checking.py
  5. 9
      mesonbuild/modules/pkgconfig.py
  6. 16
      mesonbuild/modules/qt.py
  7. 2
      mesonbuild/modules/unstable_external_project.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):

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

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

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

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

@ -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:

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

Loading…
Cancel
Save