Skip to content

Commit

Permalink
Allow setting method/separator in environment() and meson.add_devenv()
Browse files Browse the repository at this point in the history
  • Loading branch information
xclaesse committed Feb 28, 2022
1 parent ac4f8d0 commit 6acfe48
Show file tree
Hide file tree
Showing 10 changed files with 131 additions and 30 deletions.
20 changes: 20 additions & 0 deletions docs/markdown/snippets/devenv.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,23 @@ directory, that file is loaded by gdb automatically.
With `--dump` option, all envorinment variables that have been modified are
printed instead of starting an interactive shell. It can be used by shell
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)
```
24 changes: 22 additions & 2 deletions docs/yaml/builtins/meson.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -444,5 +444,25 @@ methods:
posargs:
env:
type: env
description: The [[@env]] object to add.
type: env | str | list[str] | dict[str] | dict[list[str]]
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.
23 changes: 22 additions & 1 deletion docs/yaml/functions/environment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,29 @@ description: Returns an empty [[@env]] object.

optargs:
env:
type: dict[str]
type: str | list[str] | dict[str] | dict[list[str]]
since: 0.52.0
description: |
If provided, each key/value pair is added into the [[@env]] object
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.
9 changes: 7 additions & 2 deletions mesonbuild/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
from .interpreterbase import FeatureNew, FeatureDeprecated

if T.TYPE_CHECKING:
from typing_extensions import Literal
from ._typing import ImmutableListProtocol, ImmutableSetProtocol
from .backend.backends import Backend, ExecutableSerialisation
from .interpreter.interpreter import Test, SourceOutputs, Interpreter
Expand Down Expand Up @@ -451,15 +452,19 @@ def get_outputs(self, backend: 'Backend') -> T.List[str]:
for source in self.get_sources(self.srclist, self.genlist)
]

EnvInitValueType = T.Dict[str, T.Union[str, T.List[str]]]

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]] = []
# The set of all env vars we have operations for. Only used for self.has_name()
self.varnames: T.Set[str] = set()

if values:
init_func = getattr(self, init_method)
for name, value in values.items():
self.set(name, [value])
init_func(name, listify(value), separator)

def __repr__(self) -> str:
repr_str = "<{0}: {1}>"
Expand Down
9 changes: 7 additions & 2 deletions mesonbuild/interpreter/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@
DEPFILE_KW,
DISABLER_KW,
ENV_KW,
ENV_METHOD_KW,
ENV_SEPARATOR_KW,
INSTALL_KW,
INSTALL_MODE_KW,
CT_INSTALL_TAG_KW,
Expand All @@ -71,6 +73,7 @@
REQUIRED_KW,
NoneType,
in_set_validator,
env_convertor_with_method
)
from . import primitives as P_OBJ

Expand Down Expand Up @@ -2610,9 +2613,9 @@ def _add_arguments(self, node: mparser.FunctionNode, argsdict: T.Dict[str, T.Lis
for lang in kwargs['language']:
argsdict[lang] = argsdict.get(lang, []) + args

@noKwargs
@noArgsFlattening
@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']]],
kwargs: 'TYPE_kwargs') -> build.EnvironmentVariables:
init = args[0]
Expand All @@ -2621,7 +2624,9 @@ def func_environment(self, node: mparser.FunctionNode, args: T.Tuple[T.Union[Non
msg = ENV_KW.validator(init)
if 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()

@typed_pos_args('join_paths', varargs=str, min_varargs=1)
Expand Down
12 changes: 4 additions & 8 deletions mesonbuild/interpreter/interpreterobjects.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
typed_pos_args, typed_kwargs, typed_operator,
noArgsFlattening, noPosargs, noKwargs, unholder_return, TYPE_var, TYPE_kwargs, TYPE_nvar, TYPE_nkwargs,
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 ..programs import ExternalProgram
from ..mesonlib import HoldableObject, MesonException, OptionKey, listify, Popen_safe
Expand Down Expand Up @@ -232,10 +232,6 @@ def stdout_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


_ENV_SEPARATOR_KW = KwargInfo('separator', str, default=os.pathsep)


class EnvironmentVariablesHolder(ObjectHolder[build.EnvironmentVariables], MutableInterpreterObject):

def __init__(self, obj: build.EnvironmentVariables, interpreter: 'Interpreter'):
Expand All @@ -260,20 +256,20 @@ def warn_if_has_name(self, name: str) -> None:
FeatureNew(m, '0.58.0', location=self.current_node).use(self.subproject)

@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:
name, values = args
self.held_object.set(name, values, kwargs['separator'])

@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:
name, values = args
self.warn_if_has_name(name)
self.held_object.append(name, values, kwargs['separator'])

@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:
name, values = args
self.warn_if_has_name(name)
Expand Down
14 changes: 10 additions & 4 deletions mesonbuild/interpreter/mesonmain.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@

from ..mesonlib import MachineChoice, OptionKey
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,
typed_pos_args, noArgsFlattening, noPosargs, noKwargs,
typed_kwargs, KwargInfo, InterpreterException)
from .primitives import MesonVersionString
from .type_checking import NATIVE_KW, NoneType

if T.TYPE_CHECKING:
from typing_extensions import Literal
from ..backend.backends import ExecutableSerialisation
from ..compilers import Compiler
from ..interpreterbase import TYPE_kwargs, TYPE_var
Expand All @@ -41,6 +42,10 @@ class NativeKW(TypedDict):

native: mesonlib.MachineChoice

class AddDevenvKW(TypedDict):
method: Literal['set', 'prepend', 'append']
separator: str


class MesonMain(MesonInterpreterObject):
def __init__(self, build: 'build.Build', interpreter: 'Interpreter'):
Expand Down Expand Up @@ -438,13 +443,14 @@ def has_external_property_method(self, args: T.Tuple[str], kwargs: 'NativeKW') -
return prop_name in self.interpreter.environment.properties[kwargs['native']]

@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))
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]
msg = ENV_KW.validator(env)
if 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)
self.build.devenv.append(converted)
43 changes: 33 additions & 10 deletions mesonbuild/interpreter/type_checking.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@

"""Helpers for strict type checking."""

from __future__ import annotations
import os
import typing as T

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 ..interpreterbase import TYPE_var
from ..interpreterbase.decorators import KwargInfo, ContainerTypeInfo
Expand All @@ -16,6 +19,8 @@
# Helper definition for type checks that are `Optional[T]`
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]]:
"""Check that the choice given was one of the given set."""
Expand Down Expand Up @@ -131,7 +136,8 @@ def _lower_strlist(input: T.List[str]) -> T.List[str]:

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]:
split = v.split('=', 1)
if len(split) == 1:
Expand All @@ -152,12 +158,18 @@ def _splitter(v: str) -> T.Optional[str]:
elif isinstance(value, dict):
# We don't need to spilt here, just do the type checking
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}"
# We know that otherwise we have an EnvironmentVariables object or None, and
# we're okay at this point
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]:
"""Split a string in the form `x=y`
Expand All @@ -167,18 +179,25 @@ def split_equal_string(input: str) -> T.Tuple[str, str]:
a, b = input.split('=', 1)
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):
return EnvironmentVariables(dict([split_equal_string(value)]))
return EnvironmentVariables(dict([split_equal_string(value)]), init_method, separator)
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):
return EnvironmentVariables(value)
return EnvironmentVariables(value, init_method, separator)
elif value is None:
return EnvironmentVariables()
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',
Expand Down Expand Up @@ -230,8 +249,7 @@ def _override_options_convertor(raw: T.List[str]) -> T.Dict[OptionKey, str]:
ContainerTypeInfo(list, str),
listify=True,
default=[],
# Reusing the env validator is a littl overkill, but nicer than duplicating the code
validator=_env_validator,
validator=_options_validator,
convertor=_override_options_convertor,
)

Expand Down Expand Up @@ -309,5 +327,10 @@ def _output_validator(outputs: T.List[str]) -> T.Optional[str]:
ContainerTypeInfo(list, (str, IncludeDirs)),
listify=True,
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')
5 changes: 5 additions & 0 deletions test cases/unit/91 devenv/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ env = environment()
env.append('TEST_B', ['2', '3'], separator: '+')
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
# PATH must contain builddir/subprojects/sub to be able to run it.
executable('app', 'main.c', dependencies: foo_dep, install: true)
Expand Down
2 changes: 1 addition & 1 deletion test cases/unit/91 devenv/test-devenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
assert os.environ['MESON_DEVENV'] == '1'
assert os.environ['MESON_PROJECT_NAME'] == 'devenv'
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
assert hello == 'world'
Expand Down

0 comments on commit 6acfe48

Please sign in to comment.