Repository: conan-io/conan
Pull request #12863 (2023-01-07 22:33:09)

test for path with special chars
Changelog: Omit
Docs: Omit

When a user in Windows have some special char in their username, the path contains them.
Bat scripts (maybe Powershell too? Need to check better) were failing for this reason.
I am proposing here to make the paths from ``conanbuild.bat`` -> other .bat scripts relative to the location of ``conanbuild.bat``, so they don't contain the full path anymore, and doesn't fail.

Close https://github.com/conan-io/conan/issues/2163


CLAassistant: [![CLA assistant check](https://cla-assistant.io/pull/badge/not_signed)](https://cla-assistant.io/conan-io/conan?pullRequest=12863) <br/>Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our [Contributor License Agreement](https://cla-assistant.io/conan-io/conan?pullRequest=12863) before we can accept your contribution.<br/>**1** out of **2** committers have signed the CLA.<br/><br/>:white_check_mark: memsharded<br/>:x: MarkusRannare<br/><sub>You have signed the CLA already but the status is still pending? Let us [recheck](https://cla-assistant.io/check/conan-io/conan?pullRequest=12863) it.</sub>

Patch:
diff --git a/conan/tools/cmake/presets.py b/conan/tools/cmake/presets.py
index 306b811d3..11a443a62 100644
--- a/conan/tools/cmake/presets.py
+++ b/conan/tools/cmake/presets.py
@@ -117,7 +117,7 @@ def _configure_preset(conanfile, generator, cache_variables, toolchain_file, mul
 
 
 def _insert_preset(data, preset_name, preset):
-    position = _get_already_existing_preset_index(preset["name"], data[preset_name])
+    position = _get_already_existing_preset_index(preset["name"], data.setdefault(preset_name, []))
     if position is not None:
         data[preset_name][position] = preset
     else:
diff --git a/conan/tools/env/environment.py b/conan/tools/env/environment.py
index edc229083..1745ecc55 100644
--- a/conan/tools/env/environment.py
+++ b/conan/tools/env/environment.py
@@ -302,22 +302,22 @@ class EnvVars:
             os.environ.update(old_env)
 
     def save_bat(self, file_location, generate_deactivate=True):
-        filepath, filename = os.path.split(file_location)
-        deactivate_file = os.path.join(filepath, "deactivate_{}".format(filename))
+        _, filename = os.path.split(file_location)
+        deactivate_file = "deactivate_{}".format(filename)
         deactivate = textwrap.dedent("""\
             setlocal
-            echo @echo off > "{deactivate_file}"
-            echo echo Restoring environment >> "{deactivate_file}"
+            echo @echo off > "%~dp0/{deactivate_file}"
+            echo echo Restoring environment >> "%~dp0/{deactivate_file}"
             for %%v in ({vars}) do (
                 set foundenvvar=
                 for /f "delims== tokens=1,2" %%a in ('set') do (
                     if /I "%%a" == "%%v" (
-                        echo set "%%a=%%b">> "{deactivate_file}"
+                        echo set "%%a=%%b">> "%~dp0/{deactivate_file}"
                         set foundenvvar=1
                     )
                 )
                 if not defined foundenvvar (
-                    echo set %%v=>> "{deactivate_file}"
+                    echo set %%v=>> "%~dp0/{deactivate_file}"
                 )
             )
             endlocal
@@ -335,9 +335,10 @@ class EnvVars:
         save(file_location, content)
 
     def save_ps1(self, file_location, generate_deactivate=True,):
-        filepath, filename = os.path.split(file_location)
-        deactivate_file = os.path.join(filepath, "deactivate_{}".format(filename))
+        _, filename = os.path.split(file_location)
+        deactivate_file = "deactivate_{}".format(filename)
         deactivate = textwrap.dedent("""\
+            Push-Location $PSScriptRoot
             "echo `"Restoring environment`"" | Out-File -FilePath "{deactivate_file}"
             $vars = (Get-ChildItem env:*).name
             $updated_vars = @({vars})
@@ -354,6 +355,7 @@ class EnvVars:
                     Add-Content "{deactivate_file}" "`nif (Test-Path env:$var) {{ Remove-Item env:$var }}"
                 }}
             }}
+            Pop-Location
         """).format(
             deactivate_file=deactivate_file,
             vars=",".join(['"{}"'.format(var) for var in self._values.keys()])
diff --git a/conan/tools/files/symlinks/symlinks.py b/conan/tools/files/symlinks/symlinks.py
index 98c4b2040..7700e8713 100644
--- a/conan/tools/files/symlinks/symlinks.py
+++ b/conan/tools/files/symlinks/symlinks.py
@@ -37,7 +37,7 @@ def remove_external_symlinks(conanfile, base_folder=None):
     for fullpath in get_symlinks(base_folder):
         link_target = os.readlink(fullpath)
         if not os.path.isabs(link_target):
-            link_target = os.path.join(base_folder, link_target)
+            link_target = os.path.join(os.path.dirname(fullpath), link_target)
         if not _path_inside(base_folder, link_target):
             os.unlink(fullpath)
 
@@ -47,6 +47,6 @@ def remove_broken_symlinks(conanfile, base_folder=None):
     for fullpath in get_symlinks(base_folder):
         link_target = os.readlink(fullpath)
         if not os.path.isabs(link_target):
-            link_target = os.path.join(base_folder, link_target)
+            link_target = os.path.join(os.path.dirname(fullpath), link_target)
         if not os.path.exists(link_target):
             os.unlink(fullpath)
diff --git a/conans/client/generators/__init__.py b/conans/client/generators/__init__.py
index dad8cdc36..3413e98d0 100644
--- a/conans/client/generators/__init__.py
+++ b/conans/client/generators/__init__.py
@@ -291,12 +291,16 @@ def _generate_aggregated_env(conanfile):
         ps1s = []
         for env_script in env_scripts:
             path = os.path.join(conanfile.generators_folder, env_script)
+            # Only the .bat and .ps1 are made relative to current script
             if env_script.endswith(".bat"):
-                bats.append(path)
+                path = os.path.relpath(path, conanfile.generators_folder)
+                bats.append("%~dp0/"+path)
             elif env_script.endswith(".sh"):
                 shs.append(subsystem_path(subsystem, path))
             elif env_script.endswith(".ps1"):
-                ps1s.append(path)
+                path = os.path.relpath(path, conanfile.generators_folder)
+                # This $PSScriptRoot uses the current script directory
+                ps1s.append("$PSScriptRoot/"+path)
         if shs:
             def sh_content(files):
                 return ". " + " && . ".join('"{}"'.format(s) for s in files)


Test patch:
diff --git a/conans/test/functional/command/test_command_test.py b/conans/test/functional/command/test_command_test.py
index 970e50aaf..c34fddfff 100644
--- a/conans/test/functional/command/test_command_test.py
+++ b/conans/test/functional/command/test_command_test.py
@@ -1,4 +1,5 @@
 import os
+import platform
 import time
 import unittest
 
@@ -26,5 +27,6 @@ class ConanTestTest(unittest.TestCase):
         client.run("test test_package hello/0.1@lasote/stable -s hello:build_type=Debug "
                    "--build missing")
         self.assertIn('hello/0.1: Hello World Debug!', client.out)
+        subfolder = "Release" if platform.system() != "Windows" else ""
         assert os.path.exists(os.path.join(client.current_folder, "test_package",
-                                           "build", "generators", "conaninfo.txt"))
+                                           "build", subfolder, "generators", "conaninfo.txt"))
diff --git a/conans/test/integration/cache/test_home_special_char.py b/conans/test/integration/cache/test_home_special_char.py
new file mode 100644
index 000000000..d958edda3
--- /dev/null
+++ b/conans/test/integration/cache/test_home_special_char.py
@@ -0,0 +1,58 @@
+import os
+import platform
+
+from conans.test.utils.test_files import temp_folder
+from conans.test.utils.tools import TestClient
+
+import textwrap
+
+
+def test_home_special_chars():
+    """ the path with special characters is creating a conanbuild.bat that fails
+    """
+    path_chars = "päthñç$"
+    cache_folder = os.path.join(temp_folder(), path_chars)
+    current_folder = os.path.join(temp_folder(), path_chars)
+    c = TestClient(cache_folder, current_folder)
+
+    tool = textwrap.dedent(r"""
+        import os
+        from conan import ConanFile
+        from conan.tools.files import save, chdir
+        class Pkg(ConanFile):
+            name = "mytool"
+            version = "1.0"
+            def package(self):
+                with chdir(self, self.package_folder):
+                    echo = "@echo off\necho MYTOOL WORKS!!"
+                    save(self, "bin/mytool.bat", echo)
+                    save(self, "bin/mytool.sh", echo)
+                    os.chmod("bin/mytool.sh", 0o777)
+            """)
+    c.save({"conanfile.py": tool})
+    c.run("create .")
+
+    conan_file = textwrap.dedent("""
+        import platform
+        from conan import ConanFile
+
+        class App(ConanFile):
+            name="failure"
+            version="0.1"
+            settings = 'os', 'arch', 'compiler', 'build_type'
+            generators = "VirtualBuildEnv"
+            tool_requires = "mytool/1.0"
+            def build(self):
+                mycmd = "mytool.bat" if platform.system() == "Windows" else "mytool.sh"
+                self.run(mycmd)
+        """)
+
+    c.save({"conanfile.py": conan_file})
+    # Need the 2 profile to work correctly buildenv
+    c.run("create . -s:b build_type=Release")
+    assert path_chars in c.out
+    assert "MYTOOL WORKS!!" in c.out
+    if platform.system() == "Windows":
+        c.run("create . -s:b build_type=Release -c tools.env.virtualenv:powershell=True")
+        assert path_chars in c.out
+        assert "MYTOOL WORKS!!" in c.out
diff --git a/conans/test/integration/toolchains/env/test_virtualenv_powershell.py b/conans/test/integration/toolchains/env/test_virtualenv_powershell.py
index 4aa80ef0e..00f36f7fa 100644
--- a/conans/test/integration/toolchains/env/test_virtualenv_powershell.py
+++ b/conans/test/integration/toolchains/env/test_virtualenv_powershell.py
@@ -14,6 +14,7 @@ from conans.tools import save
 def client():
     # We use special characters and spaces, to check everything works
     # https://github.com/conan-io/conan/issues/12648
+    # FIXME: This path still fails the creation of the deactivation script
     cache_folder = os.path.join(temp_folder(), "[sub] folder")
     client = TestClient(cache_folder)
     conanfile = str(GenConanfile("pkg", "0.1"))
@@ -40,6 +41,7 @@ def test_virtualenv(client):
             name = "app"
             version = "0.1"
             requires = "pkg/0.1"
+            apply_env = False
 
             def build(self):
                 self.output.info("----------BUILD----------------")
diff --git a/conans/test/unittests/tools/files/test_symlinks.py b/conans/test/unittests/tools/files/test_symlinks.py
index 83433228a..3a2fd174f 100644
--- a/conans/test/unittests/tools/files/test_symlinks.py
+++ b/conans/test/unittests/tools/files/test_symlinks.py
@@ -17,7 +17,7 @@ def folders():
         (os.path.join(tmp, "foo/var/file.txt"), "foo/var/other/absolute.txt"),  # Absolute link
         (os.path.join(tmp, "foo/var"), "foo/var/other/other/myfolder"),  # Absolute link folder
         (os.path.join(tmp, "foo/var/file.txt"), "foo/absolute.txt"),  # Absolute link
-        ("foo/var/file.txt", "foo/var/other/relative.txt"),  # Relative link
+        ("../file.txt", "foo/var/other/relative.txt"),  # Relative link
         ("missing.txt", "foo/var/other/broken.txt"),  # Broken link
         (outside_folder, "foo/var/other/absolute_outside"),  # Absolute folder outside the folder
         ("../../../../../outside", "foo/absolute_outside"),  # Relative folder outside the folder
@@ -54,7 +54,7 @@ def test_absolute_to_relative_symlinks(folders):
     assert linked_to == "var/file.txt"
 
     linked_to = os.readlink(os.path.join(folder, "foo/var/other/relative.txt")).replace("\\", "/")
-    assert linked_to == "foo/var/file.txt"
+    assert linked_to == "../file.txt"
 
     linked_to = os.readlink(os.path.join(folder, "foo/var/other/broken.txt"))
     assert linked_to == "missing.txt"
@@ -81,7 +81,7 @@ def test_remove_external_symlinks(folders):
     assert linked_to == os.path.join(folder, "foo/var/file.txt")
 
     linked_to = os.readlink(os.path.join(folder, "foo/var/other/relative.txt"))
-    assert linked_to == "foo/var/file.txt"
+    assert linked_to == "../file.txt"
 
     linked_to = os.readlink(os.path.join(folder, "foo/var/other/broken.txt"))
     assert linked_to == "missing.txt"
@@ -112,7 +112,7 @@ def test_remove_broken_symlinks(folders):
     assert linked_to == os.path.join(folder, "foo/var/file.txt")
 
     linked_to = os.readlink(os.path.join(folder, "foo/var/other/relative.txt"))
-    assert linked_to == "foo/var/file.txt"
+    assert linked_to == "../file.txt"
 
     # This one is removed
     assert not os.path.islink(os.path.join(folder, "foo/var/other/broken.txt"))