Repository: conan-io/conan

Issue:
[bug] EnvVars.save_script() generates incorrect paths when placing a recipe in `/build/` folder
### Environment details

* Operating System+version: debian 10
* Compiler+version: gcc 8
* Conan version: 2.0.17
* Python version: 3.7


### Steps to reproduce

When building recipes in docker images, I usually use the `docker run --rm -it -w /build -v $PWD:/build my_image bash`  command. I recently discovered a strange behavior caused by the generation of incorrect paths in environment variables in the form of `$script_folder/../../..$script_folder/../../..`.

Due to the use of the rarely used working directory `/build/`, when using `cmake_layout` in recipes, the `self.generators_folder` becomes `/build/build/generators`, and conan replaces the `build` fragment with `$script_folder/../../..` twice in the `EnvVars.save_script()` implementation.

### Logs

_No response_


Discussion:
Hi @Ariox41 

Thanks for your report.

I think there could be something else beyond the generation, like those substitutions happen typically in deployers.
It would be necessary to have something we can reproduce. Ideally without the docker image, to rule out possible issues related to docker. Could you please put together some (as minimal as possible) files and instructions to reproduce the issue? Thanks!
Is there an official conan2 docker image ? I was able to reproduce the problem as follows:

```shell
docker run --rm -it -w /build python bash
python3 -m pip install conan
conan profile detect
apt-get update
apt-get install cmake
conan new cmake_exe -d name=game -d version=1.0 -d requires=zlib/1.3.1 
conan install . -b missing
cat ./build/Release/generators/conanbuild
```
Output:

```shell
script_folder="/build/build/Release/generators"
echo "echo Restoring environment" > "$script_folder/../../..$script_folder/../../../Release/generators/deactivate_conanbuildenv-release-x86_64.sh"
for v in 
do
    is_defined="true"
    value=$(printenv $v) || is_defined="" || true
    if [ -n "$value" ] || [ -n "$is_defined" ]
    then
        echo export "$v='$value'" >> "$script_folder/../../..$script_folder/../../../Release/generators/deactivate_conanbuildenv-release-x86_64.sh"
    else
        echo unset $v >> "$script_folder/../../..$script_folder/../../../Release/generators/deactivate_conanbuildenv-release-x86_64.sh"
    fi
done
```
In this case, the problem manifests itself a little differently, but the reason is the same
> Is there an official conan2 docker image ? 

We have some images in ``conanio``, like ``conanio/gcc11`` you might use

I have tried with that image and I get:

```
conan@1807b7f92fc9:~$ cat ./build/Release/generators/conanbuild.sh                                                                                            
. "/home/conan/build/Release/generators/conanbuildenv-release-x86_64.sh"conan@1807b7f92fc9:~$ cat ./build/Release/generators/conanbuildenv-release-x86_64.sh  
script_folder="/home/conan/build/Release/generators"                                                                                                          
echo "echo Restoring environment" > "$script_folder/../../../build/Release/generators/deactivate_conanbuildenv-release-x86_64.sh"                             
for v in                                                                                                                                                      
do                                                                                                                                                            
    is_defined="true"                                                                                                                                         
    value=$(printenv $v) || is_defined="" || true                                                                                                             
    if [ -n "$value" ] || [ -n "$is_defined" ]                                                                                                                
    then                                                                                                                                                      
        echo export "$v='$value'" >> "$script_folder/../../../build/Release/generators/deactivate_conanbuildenv-release-x86_64.sh"                            
    else                                                                                                                                                      
        echo unset $v >> "$script_folder/../../../build/Release/generators/deactivate_conanbuildenv-release-x86_64.sh"                                        
    fi                                                                                                                                                        
done                                                                                                                                                          
```

Note the file I am listing is ``./build/Release/generators/conanbuildenv-release-x86_64.sh``, not the one you suggested, I guess that is a typo?

Then, I tried your example, and it reproduced (with the above correction of the file). The difference is the usage of the ``/build`` folder, I don't know yet exactly why it matters, but seems related

I got what is happening.

It is the name of the matching ``/build`` root folder that matches the intermediate ``/build`` path, where the full path is ``/build/build/generators...``.
It never happens in Windows where ``C:/`` unit will break the pattern
It never happens in Linux or OSX in any folder like ``/home/user`` or similar that won't be found in paths

While I can easily fix the ``deactivate`` code, changing this for the general case of your own env-vars would be way more challenging, and require parsing the whole shell script for paths. I'd recommend to use any other name rather than just ``/build``  at the root of the file system, something like ``/mybuild`` or ``home/user/build`` should work.

Resolving patch:
diff --git a/conan/tools/env/environment.py b/conan/tools/env/environment.py
--- a/conan/tools/env/environment.py
+++ b/conan/tools/env/environment.py
@@ -3,7 +3,7 @@
 from collections import OrderedDict
 from contextlib import contextmanager
 
-from conans.client.generators import relativize_generated_file
+from conans.client.generators import relativize_paths
 from conans.client.subsystems import deduce_subsystem, WINDOWS, subsystem_path
 from conan.errors import ConanException
 from conans.model.recipe_ref import ref_matches
@@ -135,12 +135,14 @@ def compose_env_value(self, other):
             new_value[index:index + 1] = other._values  # replace the placeholder
             self._values = new_value
 
-    def get_str(self, placeholder, subsystem, pathsep):
+    def get_str(self, placeholder, subsystem, pathsep, root_path=None, script_path=None):
         """
         :param subsystem:
         :param placeholder: a OS dependant string pattern of the previous env-var value like
         $PATH, %PATH%, et
         :param pathsep: The path separator, typically ; or :
+        :param root_path: To do a relativize of paths, the base root path to be replaced
+        :param script_path: the replacement instead of the script path
         :return: a string representation of the env-var value, including the $NAME-like placeholder
         """
         values = []
@@ -151,6 +153,13 @@ def get_str(self, placeholder, subsystem, pathsep):
             else:
                 if self._path:
                     v = subsystem_path(subsystem, v)
+                    if root_path is not None:
+                        if v.startswith(root_path):  # relativize
+                            v = v.replace(root_path, script_path, 1)
+                        elif os.sep == "\\":  # Just in case user specified C:/path/to/somewhere
+                            r = root_path.replace("\\", "/")
+                            if v.startswith(r):
+                                v = v.replace(r, script_path.replace("\\", "/"))
                 values.append(v)
         if self._path:
             return pathsep.join(values)
@@ -329,7 +338,7 @@ class EnvVars:
 
     """
     def __init__(self, conanfile, values, scope):
-        self._values = values # {var_name: _EnvValue}, just a reference to the Environment
+        self._values = values  # {var_name: _EnvValue}, just a reference to the Environment
         self._conanfile = conanfile
         self._scope = scope
         self._subsystem = deduce_subsystem(conanfile, scope)
@@ -418,12 +427,13 @@ def save_bat(self, file_location, generate_deactivate=True):
             {deactivate}
             """).format(deactivate=deactivate if generate_deactivate else "")
         result = [capture]
+        abs_base_path, new_path = relativize_paths(self._conanfile, "%~dp0")
         for varname, varvalues in self._values.items():
-            value = varvalues.get_str("%{name}%", subsystem=self._subsystem, pathsep=self._pathsep)
+            value = varvalues.get_str("%{name}%", subsystem=self._subsystem, pathsep=self._pathsep,
+                                      root_path=abs_base_path, script_path=new_path)
             result.append('set "{}={}"'.format(varname, value))
 
         content = "\n".join(result)
-        content = relativize_generated_file(content, self._conanfile, "%~dp0")
         # It is very important to save it correctly with utf-8, the Conan util save() is broken
         os.makedirs(os.path.dirname(os.path.abspath(file_location)), exist_ok=True)
         open(file_location, "w", encoding="utf-8").write(content)
@@ -459,8 +469,10 @@ def save_ps1(self, file_location, generate_deactivate=True,):
             {deactivate}
         """).format(deactivate=deactivate if generate_deactivate else "")
         result = [capture]
+        abs_base_path, new_path = relativize_paths(self._conanfile, "$PSScriptRoot")
         for varname, varvalues in self._values.items():
-            value = varvalues.get_str("$env:{name}", subsystem=self._subsystem, pathsep=self._pathsep)
+            value = varvalues.get_str("$env:{name}", subsystem=self._subsystem, pathsep=self._pathsep,
+                                      root_path=abs_base_path, script_path=new_path)
             if value:
                 value = value.replace('"', '`"')  # escape quotes
                 result.append('$env:{}="{}"'.format(varname, value))
@@ -468,7 +480,6 @@ def save_ps1(self, file_location, generate_deactivate=True,):
                 result.append('if (Test-Path env:{0}) {{ Remove-Item env:{0} }}'.format(varname))
 
         content = "\n".join(result)
-        content = relativize_generated_file(content, self._conanfile, "$PSScriptRoot")
         # It is very important to save it correctly with utf-16, the Conan util save() is broken
         # and powershell uses utf-16 files!!!
         os.makedirs(os.path.dirname(os.path.abspath(file_location)), exist_ok=True)
@@ -476,7 +487,7 @@ def save_ps1(self, file_location, generate_deactivate=True,):
 
     def save_sh(self, file_location, generate_deactivate=True):
         filepath, filename = os.path.split(file_location)
-        deactivate_file = os.path.join(filepath, "deactivate_{}".format(filename))
+        deactivate_file = os.path.join("$script_folder", "deactivate_{}".format(filename))
         deactivate = textwrap.dedent("""\
            echo "echo Restoring environment" > "{deactivate_file}"
            for v in {vars}
@@ -495,8 +506,10 @@ def save_sh(self, file_location, generate_deactivate=True):
               {deactivate}
               """).format(deactivate=deactivate if generate_deactivate else "")
         result = [capture]
+        abs_base_path, new_path = relativize_paths(self._conanfile, "$script_folder")
         for varname, varvalues in self._values.items():
-            value = varvalues.get_str("${name}", self._subsystem, pathsep=self._pathsep)
+            value = varvalues.get_str("${name}", self._subsystem, pathsep=self._pathsep,
+                                      root_path=abs_base_path, script_path=new_path)
             value = value.replace('"', '\\"')
             if value:
                 result.append('export {}="{}"'.format(varname, value))
@@ -504,7 +517,6 @@ def save_sh(self, file_location, generate_deactivate=True):
                 result.append('unset {}'.format(varname))
 
         content = "\n".join(result)
-        content = relativize_generated_file(content, self._conanfile, "$script_folder")
         content = f'script_folder="{os.path.abspath(filepath)}"\n' + content
         save(file_location, content)
 
diff --git a/conans/client/generators/__init__.py b/conans/client/generators/__init__.py
--- a/conans/client/generators/__init__.py
+++ b/conans/client/generators/__init__.py
@@ -198,14 +198,21 @@ def ps1_content(files):
 
 
 def relativize_generated_file(content, conanfile, placeholder):
+    abs_base_path, new_path = relativize_paths(conanfile, placeholder)
+    if abs_base_path is None:
+        return content
+    content = content.replace(abs_base_path, new_path)
+    content = content.replace(abs_base_path.replace("\\", "/"), new_path.replace("\\", "/"))
+    return content
+
+
+def relativize_paths(conanfile, placeholder):
     abs_base_path = conanfile.folders._base_generators
     if not abs_base_path or not os.path.isabs(abs_base_path):
-        return content
+        return None, None
     abs_base_path = os.path.join(abs_base_path, "")  # For the trailing / to dissambiguate matches
     generators_folder = conanfile.generators_folder
     rel_path = os.path.relpath(abs_base_path, generators_folder)
     new_path = placeholder if rel_path == "." else os.path.join(placeholder, rel_path)
     new_path = os.path.join(new_path, "")  # For the trailing / to dissambiguate matches
-    content = content.replace(abs_base_path, new_path)
-    content = content.replace(abs_base_path.replace("\\", "/"), new_path.replace("\\", "/"))
-    return content
+    return abs_base_path, new_path


Test patch:
diff --git a/conans/test/integration/environment/test_env.py b/conans/test/integration/environment/test_env.py
--- a/conans/test/integration/environment/test_env.py
+++ b/conans/test/integration/environment/test_env.py
@@ -841,3 +841,14 @@ def test(self):
     c.run("create tool --build-require -s:b build_type=Release -s:h build_type=Debug")
     assert "tool/0.1 (test package): Building TEST_PACKAGE IN Debug!!" in c.out
     assert "MYLIBVAR=MYLIBVALUE:Release" in c.out
+
+
+def test_deactivate_relocatable_substitute():
+    c = TestClient()
+    # this cannot be tested in CI, because permissions over root folder
+    # c.current_folder = "/build"
+    c.save({"conanfile.py": GenConanfile("pkg", "0.1")})
+    c.run("install . -s os=Linux -s:b os=Linux")
+    conanbuild = c.load("conanbuildenv.sh")
+    result = os.path.join("$script_folder", "deactivate_conanbuildenv.sh")
+    assert f'"{result}"' in conanbuild