Allow setting method/separator in environment() and meson.add_devenv()

pull/10047/head
Xavier Claessens 3 years ago committed by Xavier Claessens
parent ac4f8d0088
commit 6acfe48f32
  1. 20
      docs/markdown/snippets/devenv.md
  2. 24
      docs/yaml/builtins/meson.yaml
  3. 23
      docs/yaml/functions/environment.yaml
  4. 9
      mesonbuild/build.py
  5. 9
      mesonbuild/interpreter/interpreter.py
  6. 12
      mesonbuild/interpreter/interpreterobjects.py
  7. 14
      mesonbuild/interpreter/mesonmain.py
  8. 43
      mesonbuild/interpreter/type_checking.py
  9. 5
      test cases/unit/91 devenv/meson.build
  10. 2
      test cases/unit/91 devenv/test-devenv.py

@ -21,3 +21,23 @@ directory, that file is loaded by gdb automatically.
With `--dump` option, all envorinment variables that have been modified are With `--dump` option, all envorinment variables that have been modified are
printed instead of starting an interactive shell. It can be used by shell printed instead of starting an interactive shell. It can be used by shell
scripts that wish to setup their environment themself. scripts that wish to setup their environment themself.
## New `method` and `separator` kwargs on `environment()` and `meson.add_devenv()`
It simplifies this common pattern:
```meson
env = environment()
env.prepend('FOO', ['a', 'b'], separator: ',')
meson.add_devenv(env)
```
becomes one line:
```meson
meson.add_devenv({'FOO': ['a', 'b']}, method: 'prepend', separator: ',')
```
or two lines:
```meson
env = environment({'FOO': ['a', 'b']}, method: 'prepend', separator: ',')
meson.add_devenv(env)
```

@ -444,5 +444,25 @@ methods:
posargs: posargs:
env: env:
type: env type: env | str | list[str] | dict[str] | dict[list[str]]
description: The [[@env]] object to add. description: |
The [[@env]] object to add.
Since *0.62.0* list of strings is allowed in dictionnary values. In that
case values are joined using the separator.
kwargs:
separator:
type: str
since: 0.62.0
description: |
The separator to use for the initial values defined in
the first positional argument. If not explicitly specified, the default
path separator for the host operating system will be used, i.e. ';' for
Windows and ':' for UNIX/POSIX systems.
method:
type: str
since: 0.62.0
description: |
Must be one of 'set', 'prepend', or 'append'
(defaults to 'set'). Controls if initial values defined in the first
positional argument are prepended, appended or repace the current value
of the environment variable.

@ -5,8 +5,29 @@ description: Returns an empty [[@env]] object.
optargs: optargs:
env: env:
type: dict[str] type: str | list[str] | dict[str] | dict[list[str]]
since: 0.52.0 since: 0.52.0
description: | description: |
If provided, each key/value pair is added into the [[@env]] object If provided, each key/value pair is added into the [[@env]] object
as if [[env.set]] method was called for each of them. as if [[env.set]] method was called for each of them.
Since *0.62.0* list of strings is allowed in dictionnary values. In that
case values are joined using the separator.
kwargs:
separator:
type: str
since: 0.62.0
description: |
The separator to use for the initial values defined in
the first positional argument. If not explicitly specified, the default
path separator for the host operating system will be used, i.e. ';' for
Windows and ':' for UNIX/POSIX systems.
method:
type: str
since: 0.62.0
description: |
Must be one of 'set', 'prepend', or 'append'
(defaults to 'set'). Controls if initial values defined in the first
positional argument are prepended, appended or repace the current value
of the environment variable.

@ -46,6 +46,7 @@ from .linkers import StaticLinker
from .interpreterbase import FeatureNew, FeatureDeprecated from .interpreterbase import FeatureNew, FeatureDeprecated
if T.TYPE_CHECKING: if T.TYPE_CHECKING:
from typing_extensions import Literal
from ._typing import ImmutableListProtocol, ImmutableSetProtocol from ._typing import ImmutableListProtocol, ImmutableSetProtocol
from .backend.backends import Backend, ExecutableSerialisation from .backend.backends import Backend, ExecutableSerialisation
from .interpreter.interpreter import Test, SourceOutputs, Interpreter from .interpreter.interpreter import Test, SourceOutputs, Interpreter
@ -451,15 +452,19 @@ class ExtractedObjects(HoldableObject):
for source in self.get_sources(self.srclist, self.genlist) for source in self.get_sources(self.srclist, self.genlist)
] ]
EnvInitValueType = T.Dict[str, T.Union[str, T.List[str]]]
class EnvironmentVariables(HoldableObject): class EnvironmentVariables(HoldableObject):
def __init__(self, values: T.Optional[T.Dict[str, str]] = None) -> None: def __init__(self, values: T.Optional[EnvValueType] = None,
init_method: Literal['set', 'prepend', 'append'] = 'set', separator: str = os.pathsep) -> None:
self.envvars: T.List[T.Tuple[T.Callable[[T.Dict[str, str], str, T.List[str], str], str], str, T.List[str], str]] = [] self.envvars: T.List[T.Tuple[T.Callable[[T.Dict[str, str], str, T.List[str], str], str], str, T.List[str], str]] = []
# The set of all env vars we have operations for. Only used for self.has_name() # The set of all env vars we have operations for. Only used for self.has_name()
self.varnames: T.Set[str] = set() self.varnames: T.Set[str] = set()
if values: if values:
init_func = getattr(self, init_method)
for name, value in values.items(): for name, value in values.items():
self.set(name, [value]) init_func(name, listify(value), separator)
def __repr__(self) -> str: def __repr__(self) -> str:
repr_str = "<{0}: {1}>" repr_str = "<{0}: {1}>"

@ -63,6 +63,8 @@ from .type_checking import (
DEPFILE_KW, DEPFILE_KW,
DISABLER_KW, DISABLER_KW,
ENV_KW, ENV_KW,
ENV_METHOD_KW,
ENV_SEPARATOR_KW,
INSTALL_KW, INSTALL_KW,
INSTALL_MODE_KW, INSTALL_MODE_KW,
CT_INSTALL_TAG_KW, CT_INSTALL_TAG_KW,
@ -71,6 +73,7 @@ from .type_checking import (
REQUIRED_KW, REQUIRED_KW,
NoneType, NoneType,
in_set_validator, in_set_validator,
env_convertor_with_method
) )
from . import primitives as P_OBJ from . import primitives as P_OBJ
@ -2610,9 +2613,9 @@ external dependencies (including libraries) must go to "dependencies".''')
for lang in kwargs['language']: for lang in kwargs['language']:
argsdict[lang] = argsdict.get(lang, []) + args argsdict[lang] = argsdict.get(lang, []) + args
@noKwargs
@noArgsFlattening @noArgsFlattening
@typed_pos_args('environment', optargs=[(str, list, dict)]) @typed_pos_args('environment', optargs=[(str, list, dict)])
@typed_kwargs('environment', ENV_METHOD_KW, ENV_SEPARATOR_KW.evolve(since='0.62.0'))
def func_environment(self, node: mparser.FunctionNode, args: T.Tuple[T.Union[None, str, T.List['TYPE_var'], T.Dict[str, 'TYPE_var']]], def func_environment(self, node: mparser.FunctionNode, args: T.Tuple[T.Union[None, str, T.List['TYPE_var'], T.Dict[str, 'TYPE_var']]],
kwargs: 'TYPE_kwargs') -> build.EnvironmentVariables: kwargs: 'TYPE_kwargs') -> build.EnvironmentVariables:
init = args[0] init = args[0]
@ -2621,7 +2624,9 @@ external dependencies (including libraries) must go to "dependencies".''')
msg = ENV_KW.validator(init) msg = ENV_KW.validator(init)
if msg: if msg:
raise InvalidArguments(f'"environment": {msg}') raise InvalidArguments(f'"environment": {msg}')
return ENV_KW.convertor(init) if isinstance(init, dict) and any(i for i in init.values() if isinstance(i, list)):
FeatureNew.single_use('List of string in dictionary value', '0.62.0', self.subproject, location=node)
return env_convertor_with_method(init, kwargs['method'], kwargs['separator'])
return build.EnvironmentVariables() return build.EnvironmentVariables()
@typed_pos_args('join_paths', varargs=str, min_varargs=1) @typed_pos_args('join_paths', varargs=str, min_varargs=1)

@ -20,7 +20,7 @@ from ..interpreterbase import (
typed_pos_args, typed_kwargs, typed_operator, typed_pos_args, typed_kwargs, typed_operator,
noArgsFlattening, noPosargs, noKwargs, unholder_return, TYPE_var, TYPE_kwargs, TYPE_nvar, TYPE_nkwargs, noArgsFlattening, noPosargs, noKwargs, unholder_return, TYPE_var, TYPE_kwargs, TYPE_nvar, TYPE_nkwargs,
flatten, resolve_second_level_holders, InterpreterException, InvalidArguments, InvalidCode) flatten, resolve_second_level_holders, InterpreterException, InvalidArguments, InvalidCode)
from ..interpreter.type_checking import NoneType from ..interpreter.type_checking import NoneType, ENV_SEPARATOR_KW
from ..dependencies import Dependency, ExternalLibrary, InternalDependency from ..dependencies import Dependency, ExternalLibrary, InternalDependency
from ..programs import ExternalProgram from ..programs import ExternalProgram
from ..mesonlib import HoldableObject, MesonException, OptionKey, listify, Popen_safe from ..mesonlib import HoldableObject, MesonException, OptionKey, listify, Popen_safe
@ -232,10 +232,6 @@ class RunProcess(MesonInterpreterObject):
def stderr_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str: def stderr_method(self, args: T.List[TYPE_var], kwargs: TYPE_kwargs) -> str:
return self.stderr return self.stderr
_ENV_SEPARATOR_KW = KwargInfo('separator', str, default=os.pathsep)
class EnvironmentVariablesHolder(ObjectHolder[build.EnvironmentVariables], MutableInterpreterObject): class EnvironmentVariablesHolder(ObjectHolder[build.EnvironmentVariables], MutableInterpreterObject):
def __init__(self, obj: build.EnvironmentVariables, interpreter: 'Interpreter'): def __init__(self, obj: build.EnvironmentVariables, interpreter: 'Interpreter'):
@ -260,20 +256,20 @@ class EnvironmentVariablesHolder(ObjectHolder[build.EnvironmentVariables], Mutab
FeatureNew(m, '0.58.0', location=self.current_node).use(self.subproject) FeatureNew(m, '0.58.0', location=self.current_node).use(self.subproject)
@typed_pos_args('environment.set', str, varargs=str, min_varargs=1) @typed_pos_args('environment.set', str, varargs=str, min_varargs=1)
@typed_kwargs('environment.set', _ENV_SEPARATOR_KW) @typed_kwargs('environment.set', ENV_SEPARATOR_KW)
def set_method(self, args: T.Tuple[str, T.List[str]], kwargs: 'EnvironmentSeparatorKW') -> None: def set_method(self, args: T.Tuple[str, T.List[str]], kwargs: 'EnvironmentSeparatorKW') -> None:
name, values = args name, values = args
self.held_object.set(name, values, kwargs['separator']) self.held_object.set(name, values, kwargs['separator'])
@typed_pos_args('environment.append', str, varargs=str, min_varargs=1) @typed_pos_args('environment.append', str, varargs=str, min_varargs=1)
@typed_kwargs('environment.append', _ENV_SEPARATOR_KW) @typed_kwargs('environment.append', ENV_SEPARATOR_KW)
def append_method(self, args: T.Tuple[str, T.List[str]], kwargs: 'EnvironmentSeparatorKW') -> None: def append_method(self, args: T.Tuple[str, T.List[str]], kwargs: 'EnvironmentSeparatorKW') -> None:
name, values = args name, values = args
self.warn_if_has_name(name) self.warn_if_has_name(name)
self.held_object.append(name, values, kwargs['separator']) self.held_object.append(name, values, kwargs['separator'])
@typed_pos_args('environment.prepend', str, varargs=str, min_varargs=1) @typed_pos_args('environment.prepend', str, varargs=str, min_varargs=1)
@typed_kwargs('environment.prepend', _ENV_SEPARATOR_KW) @typed_kwargs('environment.prepend', ENV_SEPARATOR_KW)
def prepend_method(self, args: T.Tuple[str, T.List[str]], kwargs: 'EnvironmentSeparatorKW') -> None: def prepend_method(self, args: T.Tuple[str, T.List[str]], kwargs: 'EnvironmentSeparatorKW') -> None:
name, values = args name, values = args
self.warn_if_has_name(name) self.warn_if_has_name(name)

@ -12,7 +12,7 @@ from .. import mlog
from ..mesonlib import MachineChoice, OptionKey from ..mesonlib import MachineChoice, OptionKey
from ..programs import OverrideProgram, ExternalProgram from ..programs import OverrideProgram, ExternalProgram
from ..interpreter.type_checking import ENV_KW from ..interpreter.type_checking import ENV_KW, ENV_METHOD_KW, ENV_SEPARATOR_KW, env_convertor_with_method
from ..interpreterbase import (MesonInterpreterObject, FeatureNew, FeatureDeprecated, from ..interpreterbase import (MesonInterpreterObject, FeatureNew, FeatureDeprecated,
typed_pos_args, noArgsFlattening, noPosargs, noKwargs, typed_pos_args, noArgsFlattening, noPosargs, noKwargs,
typed_kwargs, KwargInfo, InterpreterException) typed_kwargs, KwargInfo, InterpreterException)
@ -20,6 +20,7 @@ from .primitives import MesonVersionString
from .type_checking import NATIVE_KW, NoneType from .type_checking import NATIVE_KW, NoneType
if T.TYPE_CHECKING: if T.TYPE_CHECKING:
from typing_extensions import Literal
from ..backend.backends import ExecutableSerialisation from ..backend.backends import ExecutableSerialisation
from ..compilers import Compiler from ..compilers import Compiler
from ..interpreterbase import TYPE_kwargs, TYPE_var from ..interpreterbase import TYPE_kwargs, TYPE_var
@ -41,6 +42,10 @@ if T.TYPE_CHECKING:
native: mesonlib.MachineChoice native: mesonlib.MachineChoice
class AddDevenvKW(TypedDict):
method: Literal['set', 'prepend', 'append']
separator: str
class MesonMain(MesonInterpreterObject): class MesonMain(MesonInterpreterObject):
def __init__(self, build: 'build.Build', interpreter: 'Interpreter'): def __init__(self, build: 'build.Build', interpreter: 'Interpreter'):
@ -438,13 +443,14 @@ class MesonMain(MesonInterpreterObject):
return prop_name in self.interpreter.environment.properties[kwargs['native']] return prop_name in self.interpreter.environment.properties[kwargs['native']]
@FeatureNew('add_devenv', '0.58.0') @FeatureNew('add_devenv', '0.58.0')
@noKwargs @typed_kwargs('environment', ENV_METHOD_KW, ENV_SEPARATOR_KW.evolve(since='0.62.0'))
@typed_pos_args('add_devenv', (str, list, dict, build.EnvironmentVariables)) @typed_pos_args('add_devenv', (str, list, dict, build.EnvironmentVariables))
def add_devenv_method(self, args: T.Tuple[T.Union[str, list, dict, build.EnvironmentVariables]], kwargs: 'TYPE_kwargs') -> None: def add_devenv_method(self, args: T.Tuple[T.Union[str, list, dict, build.EnvironmentVariables]],
kwargs: 'AddDevenvKW') -> None:
env = args[0] env = args[0]
msg = ENV_KW.validator(env) msg = ENV_KW.validator(env)
if msg: if msg:
raise build.InvalidArguments(f'"add_devenv": {msg}') raise build.InvalidArguments(f'"add_devenv": {msg}')
converted = ENV_KW.convertor(env) converted = env_convertor_with_method(env, kwargs['method'], kwargs['separator'])
assert isinstance(converted, build.EnvironmentVariables) assert isinstance(converted, build.EnvironmentVariables)
self.build.devenv.append(converted) self.build.devenv.append(converted)

@ -3,10 +3,13 @@
"""Helpers for strict type checking.""" """Helpers for strict type checking."""
from __future__ import annotations
import os
import typing as T import typing as T
from .. import compilers from .. import compilers
from ..build import EnvironmentVariables, CustomTarget, BuildTarget, CustomTargetIndex, ExtractedObjects, GeneratedList, IncludeDirs from ..build import (EnvironmentVariables, EnvInitValueType, CustomTarget, BuildTarget,
CustomTargetIndex, ExtractedObjects, GeneratedList, IncludeDirs)
from ..coredata import UserFeatureOption from ..coredata import UserFeatureOption
from ..interpreterbase import TYPE_var from ..interpreterbase import TYPE_var
from ..interpreterbase.decorators import KwargInfo, ContainerTypeInfo from ..interpreterbase.decorators import KwargInfo, ContainerTypeInfo
@ -16,6 +19,8 @@ from ..programs import ExternalProgram
# Helper definition for type checks that are `Optional[T]` # Helper definition for type checks that are `Optional[T]`
NoneType: T.Type[None] = type(None) NoneType: T.Type[None] = type(None)
if T.TYPE_CHECKING:
from typing_extensions import Literal
def in_set_validator(choices: T.Set[str]) -> T.Callable[[str], T.Optional[str]]: def in_set_validator(choices: T.Set[str]) -> T.Callable[[str], T.Optional[str]]:
"""Check that the choice given was one of the given set.""" """Check that the choice given was one of the given set."""
@ -131,7 +136,8 @@ REQUIRED_KW: KwargInfo[T.Union[bool, UserFeatureOption]] = KwargInfo(
DISABLER_KW: KwargInfo[bool] = KwargInfo('disabler', bool, default=False) DISABLER_KW: KwargInfo[bool] = KwargInfo('disabler', bool, default=False)
def _env_validator(value: T.Union[EnvironmentVariables, T.List['TYPE_var'], T.Dict[str, 'TYPE_var'], str, None]) -> T.Optional[str]: def _env_validator(value: T.Union[EnvironmentVariables, T.List['TYPE_var'], T.Dict[str, 'TYPE_var'], str, None],
allow_dict_list: bool = True) -> T.Optional[str]:
def _splitter(v: str) -> T.Optional[str]: def _splitter(v: str) -> T.Optional[str]:
split = v.split('=', 1) split = v.split('=', 1)
if len(split) == 1: if len(split) == 1:
@ -152,12 +158,18 @@ def _env_validator(value: T.Union[EnvironmentVariables, T.List['TYPE_var'], T.Di
elif isinstance(value, dict): elif isinstance(value, dict):
# We don't need to spilt here, just do the type checking # We don't need to spilt here, just do the type checking
for k, dv in value.items(): for k, dv in value.items():
if not isinstance(dv, str): if allow_dict_list:
if any(i for i in listify(dv) if not isinstance(i, str)):
return f"Dictionary element {k} must be a string or list of strings not {dv!r}"
elif not isinstance(dv, str):
return f"Dictionary element {k} must be a string not {dv!r}" return f"Dictionary element {k} must be a string not {dv!r}"
# We know that otherwise we have an EnvironmentVariables object or None, and # We know that otherwise we have an EnvironmentVariables object or None, and
# we're okay at this point # we're okay at this point
return None return None
def _options_validator(value: T.Union[EnvironmentVariables, T.List['TYPE_var'], T.Dict[str, 'TYPE_var'], str, None]) -> T.Optional[str]:
# Reusing the env validator is a littl overkill, but nicer than duplicating the code
return _env_validator(value, allow_dict_list=False)
def split_equal_string(input: str) -> T.Tuple[str, str]: def split_equal_string(input: str) -> T.Tuple[str, str]:
"""Split a string in the form `x=y` """Split a string in the form `x=y`
@ -167,18 +179,25 @@ def split_equal_string(input: str) -> T.Tuple[str, str]:
a, b = input.split('=', 1) a, b = input.split('=', 1)
return (a, b) return (a, b)
_FullEnvInitValueType = T.Union[EnvironmentVariables, T.List[str], T.List[T.List[str]], EnvInitValueType, str, None]
def _env_convertor(value: T.Union[EnvironmentVariables, T.List[str], T.List[T.List[str]], T.Dict[str, str], str, None]) -> EnvironmentVariables: # Split _env_convertor() and env_convertor_with_method() to make mypy happy.
# It does not want extra arguments in KwargInfo convertor callable.
def env_convertor_with_method(value: _FullEnvInitValueType,
init_method: Literal['set', 'prepend', 'append'] = 'set',
separator: str = os.pathsep) -> EnvironmentVariables:
if isinstance(value, str): if isinstance(value, str):
return EnvironmentVariables(dict([split_equal_string(value)])) return EnvironmentVariables(dict([split_equal_string(value)]), init_method, separator)
elif isinstance(value, list): elif isinstance(value, list):
return EnvironmentVariables(dict(split_equal_string(v) for v in listify(value))) return EnvironmentVariables(dict(split_equal_string(v) for v in listify(value)), init_method, separator)
elif isinstance(value, dict): elif isinstance(value, dict):
return EnvironmentVariables(value) return EnvironmentVariables(value, init_method, separator)
elif value is None: elif value is None:
return EnvironmentVariables() return EnvironmentVariables()
return value return value
def _env_convertor(value: _FullEnvInitValueType) -> EnvironmentVariables:
return env_convertor_with_method(value)
ENV_KW: KwargInfo[T.Union[EnvironmentVariables, T.List, T.Dict, str, None]] = KwargInfo( ENV_KW: KwargInfo[T.Union[EnvironmentVariables, T.List, T.Dict, str, None]] = KwargInfo(
'env', 'env',
@ -230,8 +249,7 @@ OVERRIDE_OPTIONS_KW: KwargInfo[T.List[str]] = KwargInfo(
ContainerTypeInfo(list, str), ContainerTypeInfo(list, str),
listify=True, listify=True,
default=[], default=[],
# Reusing the env validator is a littl overkill, but nicer than duplicating the code validator=_options_validator,
validator=_env_validator,
convertor=_override_options_convertor, convertor=_override_options_convertor,
) )
@ -309,5 +327,10 @@ DEFAULT_OPTIONS: KwargInfo[T.List[str]] = KwargInfo(
ContainerTypeInfo(list, (str, IncludeDirs)), ContainerTypeInfo(list, (str, IncludeDirs)),
listify=True, listify=True,
default=[], default=[],
validator=_env_validator, validator=_options_validator,
) )
ENV_METHOD_KW = KwargInfo('method', str, default='set', since='0.62.0',
validator=in_set_validator({'set', 'prepend', 'append'}))
ENV_SEPARATOR_KW = KwargInfo('separator', str, default=os.pathsep, since='0.62.0')

@ -11,6 +11,11 @@ env = environment()
env.append('TEST_B', ['2', '3'], separator: '+') env.append('TEST_B', ['2', '3'], separator: '+')
meson.add_devenv(env) meson.add_devenv(env)
meson.add_devenv({'TEST_B': '0'}, separator: '+', method: 'prepend')
env = environment({'TEST_B': ['4']}, separator: '+', method: 'append')
meson.add_devenv(env)
# This exe links on a library built in another directory. On Windows this means # This exe links on a library built in another directory. On Windows this means
# PATH must contain builddir/subprojects/sub to be able to run it. # PATH must contain builddir/subprojects/sub to be able to run it.
executable('app', 'main.c', dependencies: foo_dep, install: true) executable('app', 'main.c', dependencies: foo_dep, install: true)

@ -6,7 +6,7 @@ from pathlib import Path
assert os.environ['MESON_DEVENV'] == '1' assert os.environ['MESON_DEVENV'] == '1'
assert os.environ['MESON_PROJECT_NAME'] == 'devenv' assert os.environ['MESON_PROJECT_NAME'] == 'devenv'
assert os.environ['TEST_A'] == '1' assert os.environ['TEST_A'] == '1'
assert os.environ['TEST_B'] == '1+2+3' assert os.environ['TEST_B'] == '0+1+2+3+4'
from mymod.mod import hello from mymod.mod import hello
assert hello == 'world' assert hello == 'world'

Loading…
Cancel
Save