Repository: conan-io/conan

Issue:
unstable/incorrect PATH variable definition when the name variable is lowercased
I finally found the time to hunt down the unstable PATH definition which I discovered while writing a test for the virtualenv generator (see #2556).

The basic setup is quite simple. I have two packages, `base` and `derived` where `derived` requires `base`. Both packages should contribute two directories to the PATH variable, `basedir/bin:samebin` and `deriveddir/bin:samebin` respectively. However, in the `base` package PATH is written in lowercase (which was a typo).

Base Package:
~~~python
import os
from conans import ConanFile

class BaseConan(ConanFile):
    name = "base"
    version = "0.1"

    def package_info(self):
        self.env_info.path.extend([os.path.join("basedir", "bin"),"samebin"])
~~~

Derived Package:
~~~python
import os
from conans import ConanFile

class DerivedConan(ConanFile):
    name = "dummy"
    version = "0.1"
    requires = "base/0.1@lasote/testing"

    def package_info(self):
        self.env_info.PATH = [os.path.join("deriveddir", "bin"),"samebin"]
~~~
conanfile.txt:
~~~
[requires]
derived/0.1@lasote/testing
[generators]
virtualenv
~~~

When I now use the derived package, for example with the virtualenv generator, we randomly get one of the two following values for PATH:
* `PATH="deriveddir/bin":"samebin":"basedir/bin":"samebin":$PATH`
* `PATH="basedir/bin":"samebin":"deriveddir/bin":"samebin":$PATH`

The reason for this strange behavior is actually (an undocumented feature?) in the [update method of the EnvValues class](
https://github.com/conan-io/conan/blob/8151c4c39a5ffbf42f21a10d586fe88b8f1c8f04/conans/model/env_info.py#L145) where path is uppercased automatically. However, somehow also the lowercase `path` is still in one of the env dicts. Finally, depending on the random order between `PATH` and `path` in the unordered dict, either the one or the other result is generated.

Interestingly, when `PATH` is used instead of `path` in the `base` package, the returned value is `PATH="deriveddir/bin":"basedir/bin":"samebin":$PATH`. Note that the [lists have been deduplicated](https://github.com/conan-io/conan/blob/8151c4c39a5ffbf42f21a10d586fe88b8f1c8f04/conans/model/env_info.py#L236) which I did not expect. Is this another undocumented feature for env_info?

I am not sure what the best fix for this situation is. On the one hand, using `path` in the `base` package was a typo and I would not recommend to do it in general. On the other hand, given that there is special handling in place it kind of works sometimes which is the absolute worst case. I currently lean towards removing the special case for PATH completely in order to get deterministic results and to fail early when writing an incorrect recipe. However, I am clearly missing the big picture here to actually decide on the best solution. Maybe @lasote can shed some light on this issue.

Best,
Mario



Discussion:

Resolving patch:
diff --git a/conans/model/env_info.py b/conans/model/env_info.py
--- a/conans/model/env_info.py
+++ b/conans/model/env_info.py
@@ -142,7 +142,6 @@ def update(self, env_obj):
             # DepsEnvInfo. the OLD values are always kept, never overwrite,
             elif isinstance(env_obj, DepsEnvInfo):
                 for (name, value) in env_obj.vars.items():
-                    name = name.upper() if name.lower() == "path" else name
                     self.add(name, value)
             else:
                 raise ConanException("unknown env type: %s" % env_obj)
@@ -197,10 +196,17 @@ class EnvInfo(object):
     def __init__(self):
         self._values_ = {}
 
+    @staticmethod
+    def _adjust_casing(name):
+        """We don't want to mix "path" with "PATH", actually we don`t want to mix anything
+        with different casing. Furthermore in Windows all is uppercase, but managing all in
+        upper case will be breaking."""
+        return name.upper() if name.lower() == "path" else name
+
     def __getattr__(self, name):
         if name.startswith("_") and name.endswith("_"):
             return super(EnvInfo, self).__getattr__(name)
-
+        name = self._adjust_casing(name)
         attr = self._values_.get(name)
         if not attr:
             self._values_[name] = []
@@ -209,6 +215,7 @@ def __getattr__(self, name):
     def __setattr__(self, name, value):
         if name.startswith("_") and name.endswith("_"):
             return super(EnvInfo, self).__setattr__(name, value)
+        name = self._adjust_casing(name)
         self._values_[name] = value
 
     @property


Test patch:
diff --git a/conans/test/integration/conan_env_test.py b/conans/test/integration/conan_env_test.py
--- a/conans/test/integration/conan_env_test.py
+++ b/conans/test/integration/conan_env_test.py
@@ -626,6 +626,54 @@ def test_complex_deps_propagation_override(self):
         self.assertInSep("VAR2=>24:23*", client.user_io.out)
         self.assertInSep("VAR3=>bestvalue*", client.user_io.out)
 
+    def mix_path_case_test(self):
+        client = TestClient()
+        conanfile = """
+from conans import ConanFile
+class LibConan(ConanFile):
+    name = "libB"
+    version = "1.0"
+
+    def package_info(self):
+        self.env_info.path = ["path_from_B"]
+"""
+        client.save({"conanfile.py": conanfile})
+        client.run("create . user/channel")
+
+        conanfile = """
+from conans import ConanFile
+class LibConan(ConanFile):
+    name = "libA"
+    version = "1.0"
+    requires = "libB/1.0@user/channel"
+
+    def package_info(self):
+        self.env_info.PATH.extend(["path_from_A"])
+"""
+        client.save({"conanfile.py": conanfile})
+        client.run("create . user/channel")
+
+        conanfile = """
+[requires]
+libA/1.0@user/channel
+[generators]
+virtualenv
+"""
+        client.save({"conanfile.txt": conanfile}, clean_first=True)
+        client.run("install .")
+        info = load(os.path.join(client.current_folder, "conanbuildinfo.txt"))
+        info = info.replace("\r\n", "\n")
+        self.assertIn("""
+[ENV_libA]
+PATH=["path_from_A"]
+[ENV_libB]
+PATH=["path_from_B"]""", info)
+        if platform.system() != "Windows":
+            activate = load(os.path.join(client.current_folder, "activate.sh"))
+            self.assertIn('PATH="path_from_A":"path_from_B":$PATH', activate)
+        else:
+            activate = load(os.path.join(client.current_folder, "activate.bat"))
+            self.assertIn('PATH=path_from_A;path_from_B;%PATH%', activate)
 
     def check_conaninfo_completion_test(self):
         """