Repository: conan-io/conan

Issue:
[workspaces] No out-of-source builds are possible after upgrading to 1.13.2
Most recent edition of Workspace feature is designed in the way that build folder of sub-libraries are relative to location of the libraries, and thus always pollutes source folder. With previous edition of Workspace feature we were able keep build artifacts of sub-libraries, participating in workspace, in workspace build dir, as sub-folders. That was convenient - sources of the libraries were unaffected, and one was able to have multiple workspaces active, with the same libraries used in it.

Right now such a setup is impossible - new layouts are imposing relative-to-library pathes only. It would be nice if we can have optional ability to use old logic for build artifacts location, out-of-sources workspace building was one of the key features of Conan for us.
 
- [x] I've read the [CONTRIBUTING guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).
- [x] I've specified the Conan version, operating system version and any tool that can be relevant.
- [x] I've explained the steps to reproduce the error or the motivation/use case of the question/suggestion.




Discussion:
I second that. What is the rationale for having relative-to-projects build folders?
I think it might be possible indeed. 
We needed to step back a bit, to re-organize all the code, start using the editables, and then move forward and build on top on that. Lets try to move it forward.

Resolving patch:
diff --git a/conans/util/templates.py b/conans/util/templates.py
--- a/conans/util/templates.py
+++ b/conans/util/templates.py
@@ -5,4 +5,4 @@
 
 def render_layout_file(content, ref=None, settings=None, options=None):
     t = Template(content)
-    return t.render(reference=str(ref), settings=settings, options=options)
+    return t.render(reference=ref, settings=settings, options=options)


Test patch:
diff --git a/conans/test/functional/workspace/workspace_test.py b/conans/test/functional/workspace/workspace_test.py
--- a/conans/test/functional/workspace/workspace_test.py
+++ b/conans/test/functional/workspace/workspace_test.py
@@ -98,7 +98,7 @@ def parse_test(self):
         project = "root: Hellob/0.1@lasote/stable"
         save(path, project)
         with six.assertRaisesRegex(self, ConanException,
-                                     "Root Hellob/0.1@lasote/stable is not defined as editable"):
+                                   "Root Hellob/0.1@lasote/stable is not defined as editable"):
             Workspace(path, None)
 
         project = dedent("""
@@ -111,7 +111,7 @@ def parse_test(self):
         save(path, project)
 
         with six.assertRaisesRegex(self, ConanException,
-                                     "Workspace unrecognized fields: {'random': 'something'}"):
+                                   "Workspace unrecognized fields: {'random': 'something'}"):
             Workspace(path, None)
 
         project = dedent("""
@@ -124,7 +124,7 @@ def parse_test(self):
         save(path, project)
 
         with six.assertRaisesRegex(self, ConanException,
-                                     "Workspace unrecognized fields: {'random': 'something'}"):
+                                   "Workspace unrecognized fields: {'random': 'something'}"):
             Workspace(path, None)
 
         project = dedent("""
@@ -135,8 +135,8 @@ def parse_test(self):
         save(path, project)
 
         with six.assertRaisesRegex(self, ConanException,
-                                     "Workspace editable HelloB/0.1@lasote/stable "
-                                     "does not define path"):
+                                   "Workspace editable HelloB/0.1@lasote/stable "
+                                   "does not define path"):
             Workspace(path, None)
 
         project = dedent("""
@@ -148,8 +148,8 @@ def parse_test(self):
         save(path, project)
 
         with six.assertRaisesRegex(self, ConanException,
-                                     "Workspace editable HelloB/0.1@lasote/stable "
-                                     "does not define path"):
+                                   "Workspace editable HelloB/0.1@lasote/stable "
+                                   "does not define path"):
             Workspace(path, None)
 
     def simple_test(self):
@@ -380,6 +380,79 @@ def files(name, depend=None):
         self.assertIn("Hello World B Debug!", client.out)
         self.assertIn("Hello World A Debug!", client.out)
 
+    def simple_out_of_source_build_test(self):
+        client = TestClient()
+
+        def files(name, depend=None):
+            includes = ('#include "hello%s.h"' % depend) if depend else ""
+            calls = ('hello%s();' % depend) if depend else ""
+            deps = ('"Hello%s/0.1@lasote/stable"' % depend) if depend else "None"
+            return {"conanfile.py": conanfile_build.format(deps=deps, name=name),
+                    "src/hello%s.h" % name: hello_h.format(name=name),
+                    "src/hello.cpp": hello_cpp.format(name=name, includes=includes, calls=calls),
+                    "src/CMakeLists.txt": cmake.format(name=name)}
+
+        client.save(files("C"), path=os.path.join(client.current_folder, "HelloC"))
+        client.save(files("B", "C"), path=os.path.join(client.current_folder, "HelloB"))
+        a = files("A", "B")
+        a["src/CMakeLists.txt"] += ("add_executable(app main.cpp)\n"
+                                    "target_link_libraries(app helloA)\n")
+        a["src/main.cpp"] = main_cpp
+        client.save(a, path=os.path.join(client.current_folder, "HelloA"))
+
+        project = dedent("""
+            editables:
+                HelloB/0.1@lasote/stable:
+                    path: HelloB
+                HelloC/0.1@lasote/stable:
+                    path: HelloC
+                HelloA/0.1@lasote/stable:
+                    path: HelloA
+            layout: layout
+            root: HelloA/0.1@lasote/stable
+            """)
+        layout = dedent("""
+            [build_folder]
+            ../build/{{reference.name}}/{{settings.build_type}}
+
+            [includedirs]
+            src
+
+            [libdirs]
+            ../build/{{reference.name}}/{{settings.build_type}}/lib
+            """)
+
+        client.save({"conanws.yml": project,
+                     "layout": layout})
+        client.run("workspace install conanws.yml")
+        client.run("workspace install conanws.yml -s build_type=Debug")
+        self.assertIn("HelloA/0.1@lasote/stable from user folder - Editable", client.out)
+        self.assertIn("HelloB/0.1@lasote/stable from user folder - Editable", client.out)
+        self.assertIn("HelloC/0.1@lasote/stable from user folder - Editable", client.out)
+
+        build_type = "Release"
+        client.run("build HelloC -bf=build/HelloC/%s" % build_type)
+        client.run("build HelloB -bf=build/HelloB/%s" % build_type)
+        client.run("build HelloA -bf=build/HelloA/%s" % build_type)
+
+        cmd_release = os.path.normpath("./build/HelloA/Release/bin/app")
+        cmd_debug = os.path.normpath("./build/HelloA/Debug/bin/app")
+
+        client.runner(cmd_release, cwd=client.current_folder)
+        self.assertIn("Hello World C Release!", client.out)
+        self.assertIn("Hello World B Release!", client.out)
+        self.assertIn("Hello World A Release!", client.out)
+
+        build_type = "Debug"
+        client.run("build HelloC -bf=build/HelloC/%s" % build_type)
+        client.run("build HelloB -bf=build/HelloB/%s" % build_type)
+        client.run("build HelloA -bf=build/HelloA/%s" % build_type)
+
+        client.runner(cmd_debug, cwd=client.current_folder)
+        self.assertIn("Hello World C Debug!", client.out)
+        self.assertIn("Hello World B Debug!", client.out)
+        self.assertIn("Hello World A Debug!", client.out)
+
     def complete_single_conf_build_test(self):
         client = TestClient()