Skip to content

rex: ActionManager.environ is case-sensitive on Windows (setenv/getenv with mixed casing don't resolve) #2164

Description

@thc1006

Summary

On Windows, environment-variable names are case-insensitive. #2089 (fixed by #2098) addressed this for a caller-supplied parent_environ. However, rex's internal accumulated environment — ActionManager.environ, i.e. variables a package sets during commands() — still stores and looks up keys verbatim (case-sensitively) on every platform. So a package that sets a variable with one casing and later reads or modifies it with a different casing does not resolve to the value it just set when running on Windows.

This is a separate, pre-existing gap from #2089, and it was intentionally left out of scope by #2098 (which only normalizes caller-supplied parent_environ lookups).

Reproduction

# package commands(), on Windows
def commands():
    env.Foo = "child"
    env.RESULT = env.FOO      # expected "child" on Windows; not what you get

Minimal, self-contained script (plain dict parent, no private symbols — runs as-is on main). The mechanism is platform-independent; the results below are correct on case-sensitive OSes but wrong on Windows, where Foo and FOO are the same variable:

from rez.rex import RexExecutor, Python

def executor(parent):
    interp = Python(target_environ={}, passive=True)
    return RexExecutor(interpreter=interp, parent_environ=parent, shebang=False)

def _rex():
    setenv("Foo", "child")
    setenv("RESULT", getenv("FOO"))   # on Windows, should resolve to "child"

# 1) Parent has FOO -> the just-set value is shadowed by the stale parent value
ex = executor({"FOO": "parent"})
ex.execute_function(_rex)
print(ex.get_output()["RESULT"])   # -> "parent"  (on Windows, expected "child")

# 2) Parent lacks FOO -> RexUndefinedVariableError
ex = executor({})
ex.execute_function(_rex)          # raises: Referenced undefined environment variable: FOO

Root cause

ActionManager._key() performs no case-folding, and ActionManager.environ is a plain dict:

  • setenv() writes self.environ[expanded_key] = ... verbatim, so setenv("Foo", ...) stores {"Foo": ...}.
  • getenv() does self.environ[expanded_key] if expanded_key in self.environ else self.parent_environ[expanded_key], so getenv("FOO") misses self.environ and falls back to parent_environ (stale value, or RexUndefinedVariableError).

The same exact-casing assumption affects defined()/undefined(), unsetenv(), prependenv()/appendenv(), resetenv(), and the EnvironmentDict variable cache (which can build separate wrappers for env.Foo and env.FOO).

Suggested direction

Make rex's internal env-var handling case-insensitive on Windows — e.g. normalize keys in _key() when platform_.name == "windows", and/or back self.environ with a case-insensitive mapping. This is a broader change than #2098: it touches set/get/unset/pend/reset/defined/undefined and the EnvironmentDict cache, and would need tests across the Python, cmd and PowerShell interpreters — which is exactly why #2098 kept its scope to parent_environ.

Environment


Disclosure: Claude Code (Opus 4.8) used to investigate, reproduce, and draft this report.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugos:windowsWindows-specificshellShell related issues

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions