Repository: conan-io/conan

Issue:
[bug] Environment.dumps() write prepended variable in reverse order.
### Describe the bug

Environment details: Windows, Conan 2.12.2, Python 3.10.11

I use Environment.dumps() to create profiles that I can reuse later. However, I noticed that all variables that prepend paths using those generated profiles were in the reverse order.

### How to reproduce it

Create a conanfile and generate a dummy environment: 

```python
def generate(self):
        env = Environment()
        env.prepend_path("DUMMY_ENV", "should finish last")
        env.prepend_path("DUMMY_ENV", "should finish first")
        print(env.dumps())
        env.vars(self).save_script("dummy_env")
```

Check the resulting environment script: 

```batch
rem we are in the generated dummy_env.bat

set "DUMMY_ENV=should finish first;should finish last;%DUMMY_ENV%"
```

Now in comparison create a profile with the dumped environment: 

```
# we are in the created dummy_env.profile

[buildenv]
DUMMY_ENV=+(path)should finish first
DUMMY_ENV=+(path)should finish last
```

If we use this profile to generate the environment instead of the environment script, the variable will be set in the wrong order:

```batch
rem we are in the generated conanbuild*.bat

set "DUMMY_ENV=should finish last;should finish first%DUMMY_ENV%"
```


Discussion:
Hi @todorico 

It seems this is not a bug. You are reversing the order, the order in the profile is the opposite one than in the ``conanfile``.
It should be the same order, change it to:
```
[buildenv]
DUMMY_ENV=+(path)should finish last
DUMMY_ENV=+(path)should finish first
```
So it is the same order as the one in the ``generate()`` method and you will get the expected order.
Hi @memsharded,

Thanks for your response. My issue is that I don't control the order; I reuse the output of `Environment.dumps()`, which I believe should be consistent with the original environment values.

The problem arises when loading the serialized environment, but only if the original environment contains prepended values.

Here's a code example to illustrate:

```python
# No prepended values, so no assertion error:

env = Environment()
env.define("DEFINED", "defined")
# env.prepend_path("PREPENDED", "prepend_0")
# env.prepend_path("PREPENDED", "prepend_1")
env.append_path("APPENDED", "append_0")
env.append_path("APPENDED", "append_1")
env_vars = env.vars(self)

profile_env = ProfileEnvironment.loads(env.dumps()).get_profile_env("*/*")

for var, profile_value in profile_env.vars(self).items():
    assert env_vars.get(var) == profile_value, f"mismatch {env_vars.get(var)} != {profile_value}"
```

```python
# Uncommenting the prepended values causes an assertion error:
#        assert env_vars.get(var) == profile_value, f"mismatch {env_vars.get(var)} != {profile_value}"
#        AssertionError: mismatch prepend_1;prepend_0 != prepend_0;prepend_1

env = Environment()
env.define("DEFINED", "defined")
env.prepend_path("PREPENDED", "prepend_0")
env.prepend_path("PREPENDED", "prepend_1")
env.append_path("APPENDED", "append_0")
env.append_path("APPENDED", "append_1")
env_vars = env.vars(self)

profile_env = ProfileEnvironment.loads(env.dumps()).get_profile_env("*/*")

for var, profile_value in profile_env.vars(self).items():
    assert env_vars.get(var) == profile_value, f"mismatch {env_vars.get(var)} != {profile_value}"
```

It is surprising to me because I was expecting that the loading of the serialization of an environment would be the same as the environment, but it's not always true.
I see, thanks for the feedback, I am having a look into it, I'll let you know asap.

In any case, I'd like to raise the awareness that the dumped profile in command line output is exclusively informational.
It is not designed to be used as round-trip input, and as the stability commitment reads:

> The output streams stdout, stderr, i.e. the terminal output can change at any time. Do not parse the terminal output for automation.

Likewise the ``ProfileEnvironment`` class is not public API, it is not documented and it cannot be used anywhere, it is internal for Conan usage too.



I am doing [#17863](https://github.com/conan-io/conan/pull/17863) that will fix the prepend order for ``dumps()``, for next Conan 2.14.
That's awesome, thank you very much ! 



Patch:
diff --git a/conan/tools/env/environment.py b/conan/tools/env/environment.py
index b5ef89ff5fb..8659141c546 100644
--- a/conan/tools/env/environment.py
+++ b/conan/tools/env/environment.py
@@ -89,7 +89,7 @@ def dumps(self):
             result.append("{}=!".format(self._name))
         elif _EnvVarPlaceHolder in self._values:
             index = self._values.index(_EnvVarPlaceHolder)
-            for v in self._values[:index]:
+            for v in reversed(self._values[:index]):  # Reverse to prepend
                 result.append("{}=+{}{}{}".format(self._name, path, sep, v))
             for v in self._values[index+1:]:
                 result.append("{}+={}{}{}".format(self._name, path, sep, v))


Test patch:
diff --git a/test/functional/toolchains/cmake/test_cmake_toolchain_win_clang.py b/test/functional/toolchains/cmake/test_cmake_toolchain_win_clang.py
index f8263b5a2c1..f968f0b99c2 100644
--- a/test/functional/toolchains/cmake/test_cmake_toolchain_win_clang.py
+++ b/test/functional/toolchains/cmake/test_cmake_toolchain_win_clang.py
@@ -150,14 +150,14 @@ def test_clang_visual_studio_generator(self, client):
         """ This is using the embedded ClangCL compiler, not the external one"""
         generator = "Visual Studio 17"
         client.run("create . --name=pkg --version=0.1 -pr=clang -s compiler.runtime=dynamic "
-                   "-s compiler.cppstd=17 -s compiler.runtime_version=v143 "
+                   "-s compiler.cppstd=17 -s compiler.runtime_version=v144 "
                    '-c tools.cmake.cmaketoolchain:generator="{}"'.format(generator))
         assert 'cmake -G "{}"'.format(generator) in client.out
         assert "MSVC-like command-line" in client.out
-        # My local is 17, but CI ClangCL still 16
-        assert "main __clang_major__18" in client.out
+        assert "main __clang_major__19" in client.out
         # Check this! Clang compiler in Windows is reporting MSC_VER and MSVC_LANG!
-        assert "main _MSC_VER1938" in client.out
+        # CI forced the installation of 19.38, seems to prevail there
+        assert "main _MSC_VER193" in client.out
         assert "main _MSVC_LANG201703" in client.out
         assert "main _M_X64 defined" in client.out
         assert "main __x86_64__ defined" in client.out
diff --git a/test/unittests/tools/env/test_env.py b/test/unittests/tools/env/test_env.py
index e673c3bad0d..eb916ae9470 100644
--- a/test/unittests/tools/env/test_env.py
+++ b/test/unittests/tools/env/test_env.py
@@ -368,6 +368,24 @@ def test_append(self):
             MyPath1+=(path)/my/path1
             """)
 
+    def test_append_multiple(self):
+        myprofile = textwrap.dedent("""
+            # define
+            MyVar1+=MyValue1
+            MyVar1+=MyValue2
+            MyPath1 +=(path)/my/path1
+            MyPath1 +=(path)/my/path2
+            """)
+
+        env = ProfileEnvironment.loads(myprofile)
+        text = env.dumps()
+        assert text == textwrap.dedent("""\
+            MyVar1+=MyValue1
+            MyVar1+=MyValue2
+            MyPath1+=(path)/my/path1
+            MyPath1+=(path)/my/path2
+            """)
+
     def test_prepend(self):
         myprofile = textwrap.dedent("""
             # define
@@ -382,6 +400,24 @@ def test_prepend(self):
             MyPath1=+(path)/my/path1
             """)
 
+    def test_prepend_multiple(self):
+        myprofile = textwrap.dedent("""
+           # define
+           MyVar1=+MyValue1
+           MyVar1=+MyValue2
+           MyPath1=+(path)/my/path1
+           MyPath1=+(path)/my/path2
+           """)
+
+        env = ProfileEnvironment.loads(myprofile)
+        text = env.dumps()
+        assert text == textwrap.dedent("""\
+           MyVar1=+MyValue1
+           MyVar1=+MyValue2
+           MyPath1=+(path)/my/path1
+           MyPath1=+(path)/my/path2
+           """)
+
     def test_combined(self):
         myprofile = textwrap.dedent("""
             MyVar1=+MyValue11
@@ -399,12 +435,59 @@ def test_combined(self):
             MyPath1+=(path)/my/path12
             """)
 
-    def test_combined2(self):
+    def test_combined_multiple(self):
+        myprofile = textwrap.dedent("""
+            MyVar1=+MyValue11
+            MyVar1=+MyValue12
+            MyVar1+=MyValue13
+            MyVar1+=MyValue14
+            MyPath1=+(path)/my/path11
+            MyPath1=+(path)/my/path12
+            MyPath1+=(path)/my/path13
+            MyPath1+=(path)/my/path12
+            """)
+
+        env = ProfileEnvironment.loads(myprofile)
+        text = env.dumps()
+        assert text == textwrap.dedent("""\
+            MyVar1=+MyValue11
+            MyVar1=+MyValue12
+            MyVar1+=MyValue13
+            MyVar1+=MyValue14
+            MyPath1=+(path)/my/path11
+            MyPath1=+(path)/my/path12
+            MyPath1+=(path)/my/path13
+            MyPath1+=(path)/my/path12
+            """)
+
+    def test_combined_prepend_first(self):
+        myprofile = textwrap.dedent("""
+            MyVar1+=MyValue11
+            MyVar1=+MyValue12
+            MyPath1+=(path)/my/path11
+            MyPath1=+(path)/my/path12
+            """)
+
+        env = ProfileEnvironment.loads(myprofile)
+        text = env.dumps()
+        # NOTE: This is reversed order compared to origin, prepend always first
+        assert text == textwrap.dedent("""\
+            MyVar1=+MyValue12
+            MyVar1+=MyValue11
+            MyPath1=+(path)/my/path12
+            MyPath1+=(path)/my/path11
+            """)
+
+    def test_combined_prepend_first_multiple(self):
         myprofile = textwrap.dedent("""
             MyVar1+=MyValue11
             MyVar1=+MyValue12
+            MyVar1+=MyValue13
+            MyVar1=+MyValue14
             MyPath1+=(path)/my/path11
             MyPath1=+(path)/my/path12
+            MyPath1+=(path)/my/path13
+            MyPath1=+(path)/my/path14
             """)
 
         env = ProfileEnvironment.loads(myprofile)
@@ -412,9 +495,13 @@ def test_combined2(self):
         # NOTE: This is reversed order compared to origin, prepend always first
         assert text == textwrap.dedent("""\
             MyVar1=+MyValue12
+            MyVar1=+MyValue14
             MyVar1+=MyValue11
+            MyVar1+=MyValue13
             MyPath1=+(path)/my/path12
+            MyPath1=+(path)/my/path14
             MyPath1+=(path)/my/path11
+            MyPath1+=(path)/my/path13
             """)