Repository: conan-io/conan
Pull request: Relative profile path should be valid for creation commands
Conan accepts absolute and relative paths for profiles, that's well [documented](https://docs.conan.io/en/latest/reference/profiles.html).

We know the main recommendation is absolute, but relative path is not working well when is associated to same tree level, and profile names is same from default profile folder. If a default profile doesn't exist, it works well, but when duplicated, Conan prefers default profiles folder instead.

fixes #8678

Changelog: Fix: Accept relative profile path when folder is on same tree level.
Docs: https://github.com/conan-io/docs/pull/2049
- [x] Refer to the issue that supports this Pull Request.
- [x] If the issue has missing info, explain the purpose/use case/pain/need that covers this Pull Request.
- [x] I've read the [Contributing guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).
- [x] I've followed the PEP8 style guides for Python code.
- [x] I've opened another PR in the Conan docs repo to the ``develop`` branch, documenting this one. 

<sup>**Note:** By default this PR will skip the slower tests and will use a limited set of python versions. Check [here](https://github.com/conan-io/conan/blob/develop/.github/PR_INCREASE_TESTING.md) how to increase the testing level by writing some tags in the current PR body text.</sup>


Problem statement:
[question] profile search order
From some testing, it seems conan tries to resolve relative paths to profiles in the following order:

1. resolve from `.conan/profiles`
2. resolve from current directory

I'm a bit confused why this order was chosen, as it doesn't seem to be intuitive: Why try the more general solution before the more specific one?

---

I stumbled upon this when developing some profiles to later be shared and used with `conan config install`. One referenced profile from the local directory structure (path scheme: `../profiles/xyz`) caused some confusion, as there was another profile installed at `.conan/profiles/xyz` and the latter was chosen instead.

Hints from issue discussion:
Hi @PengolodhGoedath 

It's not clear which steps did you use to validate it. Could you please share your steps?

Indeed Conan tries first `.conan/profiles` because it's the default path. As documented, [profiles](https://docs.conan.io/en/latest/reference/profiles.html) can be used as absolute path, and that's the recommended way. You still can set `PROFILE_DIR` env var, if you don't want use default profile folder.

Unfortunately, even if we find it wrong, we could not change that behavior, due Conan 1.0 backward compatibility, but we can think about for Conan 2.0.
@uilianries For full context, I've had the following folder structure in a repo:

```
./
    .git/
    system/
        llvm/
             conanfile.py
    profiles/
        msvc2019
```
(I skipped irrelevant parts for demonstration purposes. Yes, I'm using MSVC to build LLVM.) Now, with a terminal in `./system`, I tried to build `conan create llvm -pr ../profiles/msvc2019 -s build_type=Release`. That build failed, because it didn't actually take the profile from `./profiles/msvc2019`, and instead took a profile from `.conan/profiles/msvc2019`. (I mostly noticed because some `build_requires` listed in `./profiles/msvc2019` were missing.)

I expected the relative path to search from the local directory first, before looking in the global one (if at all). To be fair, the documentation you linked only handles relative paths starting with `.`, not `..`, but it really surprised me. I expected all relative paths to be handled equally.

Even worse: After renaming `.conan/profiles/msvc2019`, the command above worked. So it's not like conan is completely unable to handle relative paths starting with `..`, it just prioritizes questionably and looks in `PROFILE_DIR` first.

After some testing, I also found that `include()` inside of a profile has the same behavior.

---

If feel this is a bad user experience. It makes for very brittle automation scripts (e.g. automatically building system library packages, what I was trying), where they might work in one environment, but fail in another. It also goes against common intuition, expecting all relative paths to work alike. It also goes against common practice of looking in more specific (= more local) places first before looking in more general ones.

---

I'm mostly asking whether this was intended, and hopefully an explanation why it was decided this way. If it isn't intended, I might make this into a bug report, but for now I'm just assuming I'm not getting the full picture.
> Now, with a terminal in ./system, I tried to build conan create llvm -pr ../profiles/msvc2019 -s build_type=Release. That build failed, because it didn't actually take the profile from ./profiles/msvc2019, and instead took a profile from .conan/profiles/msvc2019. (I mostly noticed because some build_requires listed in ./profiles/msvc2019 were missing.)

Indeed I can reproduce your behavior running Conan 1.34.1.

It works for absolute path, but as the documentation says:

> Profiles can be located in different folders. For example, the default <userhome>/.conan/profiles, and be referenced by absolute or relative path:

> $ conan install . --profile /abs/path/to/profile  # abs path
> $ conan install . --profile ./relpath/to/profile  # resolved to current dir
> $ conan install . --profile profile  # resolved to user/.conan/profiles/profile

It doesn't mention ../ but, oblivious, it should work too, because ./msvc2019 works. I would consider it a bug. I'll investigate better. Thanks for reporting your case.

Commit message: #8678 Support relative profile folder

Signed-off-by: Uilian Ries <uilianries@gmail.com>

Patch to review:
diff --git a/conans/client/profile_loader.py b/conans/client/profile_loader.py
index 9ba3a1cd295..598f4e92323 100644
--- a/conans/client/profile_loader.py
+++ b/conans/client/profile_loader.py
@@ -90,7 +90,9 @@ def valid_path(_profile_path):
     if os.path.isabs(profile_name):
         return valid_path(profile_name)
 
-    if profile_name[:2] in ("./", ".\\"):  # local
+    # relative local profile path
+    if profile_name[:2] in ("./", ".\\") or \
+       profile_name[:3] in ("../", "..\\"):
         profile_path = os.path.abspath(os.path.join(cwd, profile_name))
         return valid_path(profile_path)
 
diff --git a/conans/test/functional/configuration/profile_test.py b/conans/test/functional/configuration/profile_test.py
index 3b48c821760..f0d58925aa0 100644
--- a/conans/test/functional/configuration/profile_test.py
+++ b/conans/test/functional/configuration/profile_test.py
@@ -501,6 +501,77 @@ def config(self):
         self.assertIn('''Requires:
         WinRequire/0.1@lasote/stable''', self.client.out)
 
+    def test_install_profile_absolute_path(self):
+        """ It does not matter where you are, if profile is an absolute path,
+            Conan MUST use it instead of default profile with same name.
+
+            conan install foo/0.1.0@user/testing -pr /tmp/profiles/default
+        """
+        project_folder = os.path.join(self.client.current_folder, "subfolder", "project")
+        extra_profiles_folder = os.path.join(self.client.current_folder, "subfolder", "profiles")
+        tools.mkdir(extra_profiles_folder)
+        tools.mkdir(project_folder)
+
+        files = cpp_hello_conan_files("foobar", "0.1.0", build=False)
+        self.client.save(files, project_folder)
+
+        profile_name = "baz"
+        create_profile(self.client.cache.profiles_path, profile_name, settings={}, env=[("SWORD", "ATLANTEAN")])
+        create_profile(extra_profiles_folder, profile_name, settings={}, env=[("BARBARIAN", "CONAN")])
+        expected_profile_path = os.path.abspath(os.path.join(extra_profiles_folder, profile_name))
+
+        with self.client.chdir("subfolder"):
+            self.client.run("create project user/testing -pr '{}'".format(expected_profile_path))
+        assert "BARBARIAN=CONAN" in self.client.out
+        assert "WORD=ATLANTEAN" not in self.client.out
+
+    def test_install_profile_relative_dot_current_folder(self):
+        """ It does not matter where you are, if profile is an relative path one level below,
+            Conan MUST use it instead of default profile with same name.
+
+            conan install foo/0.1.0@user/testing -pr ./profiles/default
+        """
+        project_folder = os.path.join(self.client.current_folder, "subfolder", "project")
+        extra_profiles_folder = os.path.join(self.client.current_folder, "subfolder", "profiles")
+        tools.mkdir(extra_profiles_folder)
+        tools.mkdir(project_folder)
+
+        files = cpp_hello_conan_files("foobar", "0.1.0", build=False)
+        self.client.save(files, project_folder)
+
+        profile_name = "baz"
+        create_profile(self.client.cache.profiles_path, profile_name, settings={}, env=[("SWORD", "ATLANTEAN")])
+        create_profile(extra_profiles_folder, profile_name, settings={}, env=[("BARBARIAN", "CONAN")])
+        expected_profile_path = "." + os.path.join(os.sep, "profiles", profile_name)
+
+        with self.client.chdir("subfolder"):
+            self.client.run("create project user/testing -pr '{}'".format(expected_profile_path))
+        assert "BARBARIAN=CONAN" in self.client.out
+        assert "WORD=ATLANTEAN" not in self.client.out
+
+    def test_install_profile_relative_dot_dot_current_folder(self):
+        """ It does not matter where you are, if profile is an relative path at same level,
+            Conan MUST use it instead of default profile with same name.
+
+            conan install foo/0.1.0@user/testing -pr ../profiles/default
+        """
+        project_folder = os.path.join(self.client.current_folder, "subfolder", "project")
+        extra_profiles_folder = os.path.join(self.client.current_folder, "subfolder", "profiles")
+        tools.mkdir(extra_profiles_folder)
+        tools.mkdir(project_folder)
+
+        files = cpp_hello_conan_files("foobar", "0.1.0", build=False)
+        self.client.save(files, project_folder)
+
+        profile_name = "baz"
+        create_profile(self.client.cache.profiles_path, profile_name, settings={}, env=[("SWORD", "ATLANTEAN")])
+        create_profile(extra_profiles_folder, profile_name, settings={}, env=[("BARBARIAN", "CONAN")])
+        expected_profile_path = ".." + os.path.join(os.sep, "profiles", profile_name)
+
+        with self.client.chdir(project_folder):
+            self.client.run("create . user/testing -pr '{}'".format(expected_profile_path))
+        assert "BARBARIAN=CONAN" in self.client.out
+        assert "WORD=ATLANTEAN" not in self.client.out
 
 class ProfileAggregationTest(unittest.TestCase):
 


Code review comments:
diff_hunk: @@ -90,7 +90,9 @@ def valid_path(_profile_path):
     if os.path.isabs(profile_name):
         return valid_path(profile_name)
 
-    if profile_name[:2] in ("./", ".\\"):  # local
+    # relative local profile path
+    if profile_name[:2] in ("./", ".\\") or \
path: conans/client/profile_loader.py
text: @user1:
why not use `os.path.isabs` instead of our own bicycles?
that will end up in many misdetected cases, e.g. in posix `..\\myfile` is valid file name

@author:
Yes, makes total sense, I gonna improve it. Thanks.

@author:
Done.