diff --git a/src/rez/rex.py b/src/rez/rex.py index da26ef518..1af391909 100644 --- a/src/rez/rex.py +++ b/src/rez/rex.py @@ -12,8 +12,8 @@ from enum import Enum from contextlib import contextmanager from string import Formatter -from collections.abc import MutableMapping -from typing import Any, Iterable, Mapping +from collections.abc import Mapping, MutableMapping +from typing import Any, Iterable from rez.system import system from rez.config import config @@ -200,7 +200,17 @@ def __init__(self, interpreter: ActionInterpreter, ''' self.interpreter = interpreter self.verbose = verbose - self.parent_environ = os.environ if parent_environ is None else parent_environ + # On Windows env-var keys are case-insensitive; os.environ already + # behaves that way but a plain dict supplied by the caller does not, + # so wrap it on the Windows path only. See #2089. + self.parent_environ: Mapping[str, str] + if parent_environ is None: + self.parent_environ = os.environ + elif platform_.name == "windows" and not isinstance( + parent_environ, _CaseInsensitiveEnvironProxy): + self.parent_environ = _CaseInsensitiveEnvironProxy(parent_environ) + else: + self.parent_environ = parent_environ self.parent_variables = True if parent_variables is True \ else set(parent_variables or []) self.environ = {} @@ -447,6 +457,35 @@ def _keytoken(self, key): return self.interpreter.get_key_token(key) +class _CaseInsensitiveEnvironProxy(Mapping): + """Read-only case-insensitive view over an env-var mapping. + + Used by `ActionManager` to wrap a caller-supplied `parent_environ` + on Windows so rex lookups match native Windows env-var semantics + regardless of the casing used to populate the input dict. + + Kept private to this module on purpose; promote to a shared util + only when a second caller needs it. + """ + def __init__(self, data): + # keys are upper-cased once on the way in; same shape as + # os.environ on Windows. Last write wins on case collisions, + # which mirrors how os.environ resolves them too. + self._data = {k.upper(): v for k, v in data.items()} + + def __getitem__(self, key): + return self._data[key.upper()] + + def __contains__(self, key): + return isinstance(key, str) and key.upper() in self._data + + def __iter__(self): + return iter(self._data) + + def __len__(self): + return len(self._data) + + #=============================================================================== # Interpreters #=============================================================================== diff --git a/src/rez/tests/test_rex.py b/src/rez/tests/test_rex.py index ac3a8966d..8fe79c285 100644 --- a/src/rez/tests/test_rex.py +++ b/src/rez/tests/test_rex.py @@ -547,6 +547,39 @@ def test_intersects_ephemerals(self) -> None: self.assertRaises(RuntimeError, # no default intersects, ephemerals.get_range("foo.bar"), "0") + def test_parent_environ_case_insensitive_on_windows(self): + """Regression for #2089. + + On Windows env vars are case-insensitive; os.environ already + behaves that way, but a plain dict copy of it doesn't. Passing + such a dict as parent_environ used to break env. + lookups inside commands(). We now wrap on the Windows side. + """ + from rez.utils.platform_ import platform_ + if platform_.name != "windows": + self.skipTest("Windows-only behaviour") + + env = {"SomeVariable": "covfefe"} + ex = self._create_executor(env) + + # any casing of the key resolves + self.assertEqual(ex.manager.parent_environ["SomeVariable"], "covfefe") + self.assertEqual(ex.manager.parent_environ["SOMEVARIABLE"], "covfefe") + self.assertEqual(ex.manager.parent_environ["somevariable"], "covfefe") + self.assertIn("SOMEVARIABLE", ex.manager.parent_environ) + self.assertNotIn("OtherVariable", ex.manager.parent_environ) + + # end-to-end: rex code references a different casing than the + # caller's dict and the lookup must succeed (pre-fix this + # raised RexUndefinedVariableError) + def _rex(): + v = getenv("SOMEVARIABLE") # noqa: F821 - rex builtin + setenv("RESULT", v) # noqa: F821 - rex builtin + + ex2 = self._create_executor({"SomeVariable": "covfefe"}) + ex2.execute_function(_rex) + self.assertEqual(ex2.get_output().get("RESULT"), "covfefe") + if __name__ == '__main__': unittest.main()