Skip to content

Commit f3db9fb

Browse files
committed
Fix rex parent_environ case sensitivity on Windows
On Windows env-var keys are case-insensitive natively. os.environ preserves that contract via os._Environ, but a plain dict copy of it (or any user-built dict) does not. Until now, ActionManager consumed whatever Mapping the caller passed as-is, so a package commands() that referenced env.SomeVariable against a parent_environ holding the same key under a different case raised RexUndefinedVariableError. Wrap the caller-supplied parent_environ in a small read-only case-insensitive proxy on the Windows path only. The proxy is module private; the wrapping is gated on platform_.name == "windows" and is idempotent so re-entering the gate is a no-op. Linux and macOS take the existing identity assignment unchanged. Also adds a Windows-only regression test that mirrors the issue reproducer and asserts both direct lookup and end-to-end rex code paths against a mixed-case parent_environ. Closes #2089 Reported by @nrusch. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
1 parent dc6b3a1 commit f3db9fb

2 files changed

Lines changed: 75 additions & 3 deletions

File tree

src/rez/rex.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
from enum import Enum
1313
from contextlib import contextmanager
1414
from string import Formatter
15-
from collections.abc import MutableMapping
16-
from typing import Any, Iterable, Mapping
15+
from collections.abc import Mapping, MutableMapping
16+
from typing import Any, Iterable
1717

1818
from rez.system import system
1919
from rez.config import config
@@ -200,7 +200,17 @@ def __init__(self, interpreter: ActionInterpreter,
200200
'''
201201
self.interpreter = interpreter
202202
self.verbose = verbose
203-
self.parent_environ = os.environ if parent_environ is None else parent_environ
203+
# On Windows env-var keys are case-insensitive; os.environ already
204+
# behaves that way but a plain dict supplied by the caller does not,
205+
# so wrap it on the Windows path only. See #2089.
206+
self.parent_environ: Mapping[str, str]
207+
if parent_environ is None:
208+
self.parent_environ = os.environ
209+
elif platform_.name == "windows" and not isinstance(
210+
parent_environ, _CaseInsensitiveEnvironProxy):
211+
self.parent_environ = _CaseInsensitiveEnvironProxy(parent_environ)
212+
else:
213+
self.parent_environ = parent_environ
204214
self.parent_variables = True if parent_variables is True \
205215
else set(parent_variables or [])
206216
self.environ = {}
@@ -447,6 +457,35 @@ def _keytoken(self, key):
447457
return self.interpreter.get_key_token(key)
448458

449459

460+
class _CaseInsensitiveEnvironProxy(Mapping):
461+
"""Read-only case-insensitive view over an env-var mapping.
462+
463+
Used by `ActionManager` to wrap a caller-supplied `parent_environ`
464+
on Windows so rex lookups match native Windows env-var semantics
465+
regardless of the casing used to populate the input dict.
466+
467+
Kept private to this module on purpose; promote to a shared util
468+
only when a second caller needs it.
469+
"""
470+
def __init__(self, data):
471+
# keys are upper-cased once on the way in; same shape as
472+
# os.environ on Windows. Last write wins on case collisions,
473+
# which mirrors how os.environ resolves them too.
474+
self._data = {k.upper(): v for k, v in data.items()}
475+
476+
def __getitem__(self, key):
477+
return self._data[key.upper()]
478+
479+
def __contains__(self, key):
480+
return isinstance(key, str) and key.upper() in self._data
481+
482+
def __iter__(self):
483+
return iter(self._data)
484+
485+
def __len__(self):
486+
return len(self._data)
487+
488+
450489
#===============================================================================
451490
# Interpreters
452491
#===============================================================================

src/rez/tests/test_rex.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,39 @@ def test_intersects_ephemerals(self) -> None:
547547
self.assertRaises(RuntimeError, # no default
548548
intersects, ephemerals.get_range("foo.bar"), "0")
549549

550+
def test_parent_environ_case_insensitive_on_windows(self):
551+
"""Regression for #2089.
552+
553+
On Windows env vars are case-insensitive; os.environ already
554+
behaves that way, but a plain dict copy of it doesn't. Passing
555+
such a dict as parent_environ used to break env.<MixedCase>
556+
lookups inside commands(). We now wrap on the Windows side.
557+
"""
558+
from rez.utils.platform_ import platform_
559+
if platform_.name != "windows":
560+
self.skipTest("Windows-only behaviour")
561+
562+
env = {"SomeVariable": "covfefe"}
563+
ex = self._create_executor(env)
564+
565+
# any casing of the key resolves
566+
self.assertEqual(ex.manager.parent_environ["SomeVariable"], "covfefe")
567+
self.assertEqual(ex.manager.parent_environ["SOMEVARIABLE"], "covfefe")
568+
self.assertEqual(ex.manager.parent_environ["somevariable"], "covfefe")
569+
self.assertIn("SOMEVARIABLE", ex.manager.parent_environ)
570+
self.assertNotIn("OtherVariable", ex.manager.parent_environ)
571+
572+
# end-to-end: rex code references a different casing than the
573+
# caller's dict and the lookup must succeed (pre-fix this
574+
# raised RexUndefinedVariableError)
575+
def _rex():
576+
v = getenv("SOMEVARIABLE") # noqa: F821 - rex builtin
577+
setenv("RESULT", v) # noqa: F821 - rex builtin
578+
579+
ex2 = self._create_executor({"SomeVariable": "covfefe"})
580+
ex2.execute_function(_rex)
581+
self.assertEqual(ex2.get_output().get("RESULT"), "covfefe")
582+
550583

551584
if __name__ == '__main__':
552585
unittest.main()

0 commit comments

Comments
 (0)