Repository: conan-io/conan
Pull request #17851 (2025-02-25 22:26:44)

disallow relative paths in ConanAPI constructor
Changelog: Fix: Raise if a relative path is passed to ``ConanAPI()`` constructor
Docs: Omit

Close https://github.com/conan-io/conan/issues/17850



Patch:
diff --git a/conan/api/conan_api.py b/conan/api/conan_api.py
index 30fe82cc2..8a518b21d 100644
--- a/conan/api/conan_api.py
+++ b/conan/api/conan_api.py
@@ -1,3 +1,4 @@
+import os
 import sys
 
 from conan.api.output import init_colorama
@@ -35,6 +36,8 @@ class ConanAPI:
         version = sys.version_info
         if version.major == 2 or version.minor < 6:
             raise ConanException("Conan needs Python >= 3.6")
+        if cache_folder is not None and not os.path.isabs(cache_folder):
+            raise ConanException("cache_folder has to be an absolute path")
 
         init_colorama(sys.stderr)
         self.workspace = WorkspaceAPI(self)
diff --git a/conan/tools/env/environment.py b/conan/tools/env/environment.py
index c42d9f060..b5ef89ff5 100644
--- a/conan/tools/env/environment.py
+++ b/conan/tools/env/environment.py
@@ -84,18 +84,19 @@ class _EnvValue:
     def dumps(self):
         result = []
         path = "(path)" if self._path else ""
+        sep = f"(sep={self._sep})" if self._sep != " " and not self._path else ""
         if not self._values:  # Empty means unset
             result.append("{}=!".format(self._name))
         elif _EnvVarPlaceHolder in self._values:
             index = self._values.index(_EnvVarPlaceHolder)
             for v in self._values[:index]:
-                result.append("{}=+{}{}".format(self._name, path, v))
+                result.append("{}=+{}{}{}".format(self._name, path, sep, v))
             for v in self._values[index+1:]:
-                result.append("{}+={}{}".format(self._name, path, v))
+                result.append("{}+={}{}{}".format(self._name, path, sep, v))
         else:
             append = ""
             for v in self._values:
-                result.append("{}{}={}{}".format(self._name, append, path, v))
+                result.append("{}{}={}{}{}".format(self._name, append, path, sep, v))
                 append = "+"
         return "\n".join(result)
 
@@ -644,6 +645,14 @@ class ProfileEnvironment:
                 env = Environment()
                 if method == "unset":
                     env.unset(name)
+                elif value.strip().startswith("(sep="):
+                    value = value.strip()
+                    sep = value[5]
+                    value = value[7:]
+                    if value.strip().startswith("(path)"):
+                        msg = f"Cannot use (sep) and (path) qualifiers simultaneously: {line}"
+                        raise ConanException(msg)
+                    getattr(env, method)(name, value, separator=sep)
                 else:
                     if value.strip().startswith("(path)"):
                         value = value.strip()


Test patch:
diff --git a/test/functional/subsystems_build_test.py b/test/functional/subsystems_build_test.py
index c5f8336f7..0b396663e 100644
--- a/test/functional/subsystems_build_test.py
+++ b/test/functional/subsystems_build_test.py
@@ -83,6 +83,7 @@ class TestSubsystems:
         client.run_command('uname', assert_error=True)
         assert "'uname' is not recognized as an internal or external command" in client.out
 
+
 @pytest.mark.skipif(platform.system() != "Windows", reason="Tests Windows Subsystems")
 class TestSubsystemsBuild:
 
@@ -512,3 +513,49 @@ class TestSubsystemsCMakeBuild:
         self._build(client, generator="Visual Studio 17 2022", toolset="ClangCL")
         check_exe_run(client.out, "main", "clang", None, "Debug", "x86_64", None, subsystem=None)
         check_vs_runtime("Debug/app.exe", client, "15", "Debug", subsystem=None)
+
+
+@pytest.mark.tool("msys2")
+def test_msys2_env_vars_paths():
+    c = TestClient()
+    # A tool-requires injecting PATHs for native, should not use "_path" calls, and use
+    # 'separator=;' explicitly
+    tool = textwrap.dedent("""
+        from conan import ConanFile
+        class HelloConan(ConanFile):
+            name = "tool"
+            version = "0.1"
+            def package_info(self):
+                self.buildenv_info.append("INCLUDE", "C:/mytool/path", separator=";")
+        """)
+    conanfile = textwrap.dedent("""
+        from conan import ConanFile
+        class HelloConan(ConanFile):
+            win_bash = True
+            tool_requires = "tool/0.1"
+
+            def build(self):
+                self.run('echo "INCLUDE=$INCLUDE"')
+        """)
+    profile = textwrap.dedent("""
+        [conf]
+        tools.microsoft.bash:subsystem=msys2
+        tools.microsoft.bash:path=bash
+
+        [buildenv]
+        INCLUDE=+(sep=;)C:/prepended/path
+        INCLUDE+=(sep=;)C:/appended/path
+        """)
+    c.save({"tool/conanfile.py": tool,
+            "consumer/conanfile.py": conanfile,
+            "profile": profile})
+    c.run("create tool")
+    with environment_update({"INCLUDE": "C:/my/abs path/folder;C:/other path/subfolder"}):
+        c.run("build consumer -pr=profile")
+
+    # Check the profile is outputed correctly
+    assert "INCLUDE=+(sep=;)C:/prepended/path" in c.out
+    assert "INCLUDE+=(sep=;)C:/appended/path" in c.out
+    # check the composition is correct
+    assert "INCLUDE=C:/prepended/path;C:/my/abs path/folder;C:/other path/subfolder;" \
+           "C:/mytool/path;C:/appended/path" in c.out
diff --git a/test/integration/conan_api/test_profile_api.py b/test/integration/conan_api/test_profile_api.py
new file mode 100644
index 000000000..4f06d9d10
--- /dev/null
+++ b/test/integration/conan_api/test_profile_api.py
@@ -0,0 +1,11 @@
+import pytest
+
+from conan.api.conan_api import ConanAPI
+from conan.errors import ConanException
+
+
+def test_profile_api():
+    # It must be an absolute path
+    with pytest.raises(ConanException) as e:
+        ConanAPI(cache_folder="test")
+    assert "cache_folder has to be an absolute path" in str(e.value)
diff --git a/test/unittests/tools/env/test_env.py b/test/unittests/tools/env/test_env.py
index 156484905..e673c3bad 100644
--- a/test/unittests/tools/env/test_env.py
+++ b/test/unittests/tools/env/test_env.py
@@ -5,6 +5,7 @@ import textwrap
 
 import pytest
 
+from conan.errors import ConanException
 from conan.tools.env import Environment
 from conan.tools.env.environment import ProfileEnvironment
 from conans.client.subsystems import WINDOWS
@@ -426,3 +427,26 @@ def test_custom_placeholder():
            f"$penv{{MyVar}}:MyValue"
     items = {k: v for k, v in env.items(variable_reference="$penv{{{name}}}")}
     assert items == {"MyVar": f"$penv{{MyVar}}:MyValue"}
+
+
+class TestProfileSeparators:
+
+    def test_define(self):
+        myprofile = "MyPath1 = (sep=@)/my/path1"
+        env = ProfileEnvironment.loads(myprofile).get_profile_env(ref="")
+        assert env.dumps() == "MyPath1=(sep=@)/my/path1"
+        env.append("MyPath1", "/other/value")
+        assert env.vars(ConanFileMock()).get("MyPath1") == "/my/path1@/other/value"
+
+    def test_append(self):
+        myprofile = "MyPath1 +=(sep=@)/my/path1"
+        env = ProfileEnvironment.loads(myprofile).get_profile_env(ref="")
+        assert env.dumps() == "MyPath1+=(sep=@)/my/path1"
+        env.append("MyPath1", "/other/value")
+        assert env.vars(ConanFileMock()).get("MyPath1") == "/my/path1@/other/value"
+
+    def test_error(self):
+        myprofile = "MyPath1 = (sep=@) (path)/my/path1"
+        with pytest.raises(ConanException) as e:
+            ProfileEnvironment.loads(myprofile)
+        assert "Cannot use (sep) and (path) qualifiers simultaneously" in str(e.value)