diff --git a/docs/markdown/snippets/devenv.md b/docs/markdown/snippets/devenv.md index 505f9710b4df..104cfd957727 100644 --- a/docs/markdown/snippets/devenv.md +++ b/docs/markdown/snippets/devenv.md @@ -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) +``` diff --git a/docs/yaml/builtins/meson.yaml b/docs/yaml/builtins/meson.yaml index 92fc90ee5fc0..09e5dbb0b8d1 100644 --- a/docs/yaml/builtins/meson.yaml +++ b/docs/yaml/builtins/meson.yaml @@ -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. diff --git a/docs/yaml/functions/environment.yaml b/docs/yaml/functions/environment.yaml index 99c8a45906b8..5fb81e34e32e 100644 --- a/docs/yaml/functions/environment.yaml +++ b/docs/yaml/functions/environment.yaml @@ -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. diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 35e5e66eca26..6bb487b58e7d 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -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 @@ -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}>" diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 30d0e4cc2436..380cc1dc9403 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -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, @@ -71,6 +73,7 @@ REQUIRED_KW, NoneType, in_set_validator, + env_convertor_with_method ) from . import primitives as P_OBJ @@ -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] @@ -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) diff --git a/mesonbuild/interpreter/interpreterobjects.py b/mesonbuild/interpreter/interpreterobjects.py index 989877e679e4..4520f096f406 100644 --- a/mesonbuild/interpreter/interpreterobjects.py +++ b/mesonbuild/interpreter/interpreterobjects.py @@ -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 @@ -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'): @@ -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) diff --git a/mesonbuild/interpreter/mesonmain.py b/mesonbuild/interpreter/mesonmain.py index 5781e0375476..4533c4a901e1 100644 --- a/mesonbuild/interpreter/mesonmain.py +++ b/mesonbuild/interpreter/mesonmain.py @@ -12,7 +12,7 @@ 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) @@ -20,6 +20,7 @@ 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 @@ -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'): @@ -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) diff --git a/mesonbuild/interpreter/type_checking.py b/mesonbuild/interpreter/type_checking.py index e94027bc6a72..cce794fc2e6c 100644 --- a/mesonbuild/interpreter/type_checking.py +++ b/mesonbuild/interpreter/type_checking.py @@ -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 @@ -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.""" @@ -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: @@ -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` @@ -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', @@ -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, ) @@ -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') diff --git a/test cases/unit/91 devenv/meson.build b/test cases/unit/91 devenv/meson.build index 90c4cee2f933..f5c24d87757f 100644 --- a/test cases/unit/91 devenv/meson.build +++ b/test cases/unit/91 devenv/meson.build @@ -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) diff --git a/test cases/unit/91 devenv/test-devenv.py b/test cases/unit/91 devenv/test-devenv.py index de7eec2186c0..8273805bd559 100755 --- a/test cases/unit/91 devenv/test-devenv.py +++ b/test cases/unit/91 devenv/test-devenv.py @@ -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'