Repository: conan-io/conan
Pull request: basic_layout and external_sources for cmake_layout, removed meson_layout
Changelog: Feature: Added `basic_layout`, removed `meson_layout` and added argument `src_folder` to `cmake_layout`as a shortcut for adjusting `conanfile.folders.source`.
Docs: https://github.com/conan-io/docs/pull/2426

Close https://github.com/conan-io/conan/issues/10645


Problem statement:
[feature] [2.0] predefined layouts when playing locally
- When you work, let's say with CMake, with conan local commands and/or building locally with the build system, if you have a source() method to retrieve the external sources, (e.g conan-center) you may want to adjust the layout as:

```
    def layout(self):
        cmake_layout(self)
        self.folders.source = "src"
```

The `self.folders.source = "src"` is because you don't want to end up with a bunch of cloned/unzipped files at the root directory of your package.


- It happens also with any other build system, for example, using Autotools (we don't have any preset layout for that) you might want to declare something like:

```
    def layout(self):
         self.folders.build = os.path.join("build", str(self.settings.build_type))
         self.folders.generators = os.path.join(self.folders.build, "conan")
         self.folders.source = "src"
```

Because you don't want either the cloned "src" or the build/generators littering your package folder. 

- Given that the previous definition is totally random, not attached to Autotools at all, we should consider to add a `basic_layout()` helper. 
- Given that in both cases we are interested in not littering the repo with the cloned sources, consider an argument to all the layouts `external_sources=True` to assign a "src" folder.
- Check if the Meson layout is generic and it could be renamed.

EDIT: Consider not having the "build_type" setting for a basic layout.

Hints from issue discussion:

Commit message: src_folder argument

Patch to review:
diff --git a/conan/tools/cmake/layout.py b/conan/tools/cmake/layout.py
index 505b077d526..67ef5d6e378 100644
--- a/conan/tools/cmake/layout.py
+++ b/conan/tools/cmake/layout.py
@@ -3,7 +3,7 @@
 from conans.errors import ConanException
 
 
-def cmake_layout(conanfile, generator=None):
+def cmake_layout(conanfile, generator=None, src_folder="."):
     gen = conanfile.conf.get("tools.cmake.cmaketoolchain:generator", default=generator)
     if gen:
         multi = "Visual" in gen or "Xcode" in gen or "Multi-Config" in gen
@@ -14,7 +14,7 @@ def cmake_layout(conanfile, generator=None):
         else:
             multi = False
 
-    conanfile.folders.source = "."
+    conanfile.folders.source = src_folder
     try:
         build_type = str(conanfile.settings.build_type)
     except ConanException:
@@ -27,7 +27,12 @@ def cmake_layout(conanfile, generator=None):
         conanfile.folders.build = "cmake-build-{}".format(build_type)
         conanfile.folders.generators = os.path.join(conanfile.folders.build, "conan")
 
-    conanfile.cpp.source.includedirs = ["src"]
+    if src_folder == ".":
+        # The CMakeLists.txt is in the root, the includes (and sources) typically don't
+        conanfile.cpp.source.includedirs = ["src"]
+    else:
+        conanfile.cpp.source.includedirs = ["."]
+
     if multi:
         conanfile.cpp.build.libdirs = ["{}".format(build_type)]
         conanfile.cpp.build.bindirs = ["{}".format(build_type)]
diff --git a/conan/tools/layout/__init__.py b/conan/tools/layout/__init__.py
index 50da54c6a0d..3946cf958ee 100644
--- a/conan/tools/layout/__init__.py
+++ b/conan/tools/layout/__init__.py
@@ -1,3 +1,14 @@
+import os
 # FIXME: Temporary fixes to avoid breaking 1.45, to be removed 2.0
 from conan.tools.cmake import cmake_layout, clion_layout
 from conan.tools.microsoft import vs_layout
+
+
+def basic_layout(conanfile, src_folder="."):
+    conanfile.folders.build = "build"
+    if conanfile.settings.get_safe("build_type"):
+        conanfile.folders.build += "-{}".format(str(conanfile.settings.build_type).lower())
+    conanfile.folders.generators = os.path.join(conanfile.folders.build, "conan")
+    conanfile.cpp.build.bindirs = ["."]
+    conanfile.cpp.build.libdirs = ["."]
+    conanfile.folders.source = src_folder
diff --git a/conan/tools/meson/__init__.py b/conan/tools/meson/__init__.py
index 0da85a41394..9f317238a9c 100644
--- a/conan/tools/meson/__init__.py
+++ b/conan/tools/meson/__init__.py
@@ -1,3 +1,3 @@
 from conan.tools.meson.toolchain import MesonToolchain
 from conan.tools.meson.meson import Meson
-from conan.tools.meson.layout import meson_layout
+
diff --git a/conan/tools/meson/layout.py b/conan/tools/meson/layout.py
deleted file mode 100644
index becdb3b2037..00000000000
--- a/conan/tools/meson/layout.py
+++ /dev/null
@@ -1,8 +0,0 @@
-import os
-
-
-def meson_layout(conanfile):
-    conanfile.folders.build = "build-{}".format(str(conanfile.settings.build_type).lower())
-    conanfile.folders.generators = os.path.join(conanfile.folders.build, "conan")
-    conanfile.cpp.build.bindirs = ["."]
-    conanfile.cpp.build.libdirs = ["."]
diff --git a/conans/assets/templates/new_v2_meson.py b/conans/assets/templates/new_v2_meson.py
index 1de66f348ca..e0e962070b3 100644
--- a/conans/assets/templates/new_v2_meson.py
+++ b/conans/assets/templates/new_v2_meson.py
@@ -2,7 +2,8 @@
 
 conanfile_sources_v2 = """import os
 from conan import ConanFile
-from conan.tools.meson import MesonToolchain, Meson, meson_layout
+from conan.tools.meson import MesonToolchain, Meson
+from conan.tools.layout import basic_layout
 from conan.tools.files import copy
 
 class {package_name}Conan(ConanFile):
@@ -22,7 +23,7 @@ def config_options(self):
             del self.options.fPIC
 
     def layout(self):
-        meson_layout(self)
+        basic_layout(self)
 
     def generate(self):
         tc = MesonToolchain(self)
@@ -50,7 +51,8 @@ def package_info(self):
 test_conanfile_v2 = """import os
 from conan import ConanFile
 from conans import tools
-from conan.tools.meson import MesonToolchain, Meson, meson_layout
+from conan.tools.meson import MesonToolchain, Meson
+from conan.tools.layout import basic_layout
 
 
 class {package_name}TestConan(ConanFile):
@@ -66,7 +68,7 @@ def build(self):
         meson.build()
 
     def layout(self):
-        meson_layout(self)
+        basic_layout(self)
 
     def test(self):
         if not tools.cross_building(self):
diff --git a/conans/test/functional/layout/test_build_system_layout_helpers.py b/conans/test/functional/layout/test_build_system_layout_helpers.py
index 4bb730ae1ab..767e1bb6923 100644
--- a/conans/test/functional/layout/test_build_system_layout_helpers.py
+++ b/conans/test/functional/layout/test_build_system_layout_helpers.py
@@ -3,11 +3,12 @@
 import textwrap
 
 import pytest
+import six
 
 from conans.model.ref import ConanFileReference
 from conans.test.assets.genconanfile import GenConanfile
 from conans.test.utils.tools import TestClient, TurboTestClient
-from conans.util.files import save
+from conans.util.files import save, load
 
 
 @pytest.fixture
@@ -146,3 +147,149 @@ def layout(self):
     client.save({"conanfile.py": conanfile})
     client.run('install .',  assert_error=True)
     assert " 'build_type' setting not defined, it is necessary for cmake_layout()" in client.out
+
+
+@pytest.mark.skipif(six.PY2, reason="only Py3")
+def test_cmake_layout_external_sources():
+    conanfile = textwrap.dedent("""
+        import os
+        from conans import ConanFile
+        from conan.tools.cmake import cmake_layout
+        from conan.tools.files import save, copy, load
+        class Pkg(ConanFile):
+            settings = "os", "build_type"
+            exports_sources = "exported.txt"
+
+            def layout(self):
+                cmake_layout(self, src_folder="src")
+
+            def generate(self):
+                save(self, "generate.txt", "generate")
+
+            def source(self):
+                save(self, "source.txt", "foo")
+
+            def build(self):
+                c1 = load(self, os.path.join(self.source_folder, "source.txt"))
+                c2 = load(self, os.path.join(self.source_folder, "..", "exported.txt"))
+                save(self, "build.txt", c1 + c2)
+
+            def package(self):
+                copy(self, "build.txt", self.build_folder, os.path.join(self.package_folder, "res"))
+        """)
+
+    client = TestClient()
+    client.save({"conanfile.py": conanfile, "exported.txt": "exported_contents"})
+    client.run("create . foo/1.0@ -s os=Linux")
+    assert "Packaged 1 '.txt' file: build.txt" in client.out
+
+    # Local flow
+    client.run("install . foo/1.0 -s os=Linux")
+    assert os.path.exists(os.path.join(client.current_folder, "cmake-build-release", "conan", "generate.txt"))
+    client.run("source .")
+    assert os.path.exists(os.path.join(client.current_folder, "src", "source.txt"))
+    client.run("build .")
+    contents = load(os.path.join(client.current_folder, "cmake-build-release", "build.txt"))
+    assert contents == "fooexported_contents"
+    client.run("export-pkg . foo/1.0@ --force")
+    assert "Packaged 1 '.txt' file: build.txt" in client.out
+
+
+@pytest.mark.skipif(six.PY2, reason="only Py3")
+@pytest.mark.parametrize("with_build_type", [True, False])
+def test_basic_layout_external_sources(with_build_type):
+    conanfile = textwrap.dedent("""
+            import os
+            from conans import ConanFile
+            from conan.tools.layout import basic_layout
+            from conan.tools.files import save, load, copy
+            class Pkg(ConanFile):
+                settings = "os", "compiler", "arch"{}
+                exports_sources = "exported.txt"
+
+                def layout(self):
+                    basic_layout(self, src_folder="src")
+
+                def generate(self):
+                    save(self, "generate.txt", "generate")
+
+                def source(self):
+                    save(self, "source.txt", "foo")
+
+                def build(self):
+                    c1 = load(self, os.path.join(self.source_folder, "source.txt"))
+                    c2 = load(self, os.path.join(self.source_folder, "..", "exported.txt"))
+                    save(self, "build.txt", c1 + c2)
+
+                def package(self):
+                    copy(self, "build.txt", self.build_folder, os.path.join(self.package_folder, "res"))
+            """)
+    if with_build_type:
+        conanfile = conanfile.format(', "build_type"')
+    else:
+        conanfile = conanfile.format("")
+    client = TestClient()
+    client.save({"conanfile.py": conanfile, "exported.txt": "exported_contents"})
+    client.run("create . foo/1.0@ -s os=Linux")
+    assert "Packaged 1 '.txt' file: build.txt" in client.out
+
+    # Local flow
+    build_folder = "build-release" if with_build_type else "build"
+    client.run("install . foo/1.0 -s os=Linux")
+    assert os.path.exists(os.path.join(client.current_folder, build_folder, "conan", "generate.txt"))
+    client.run("source .")
+    assert os.path.exists(os.path.join(client.current_folder, "src", "source.txt"))
+    client.run("build .")
+    contents = load(os.path.join(client.current_folder, build_folder, "build.txt"))
+    assert contents == "fooexported_contents"
+    client.run("export-pkg . foo/1.0@ --force")
+    assert "Packaged 1 '.txt' file: build.txt" in client.out
+
+
+@pytest.mark.skipif(six.PY2, reason="only Py3")
+@pytest.mark.parametrize("with_build_type", [True, False])
+def test_basic_layout_no_external_sources(with_build_type):
+    conanfile = textwrap.dedent("""
+            import os
+            from conans import ConanFile
+            from conan.tools.layout import basic_layout
+            from conan.tools.files import save, load, copy
+            class Pkg(ConanFile):
+                settings = "os", "compiler", "arch"{}
+                exports_sources = "exported.txt"
+
+                def layout(self):
+                    basic_layout(self)
+
+                def generate(self):
+                    save(self, "generate.txt", "generate")
+
+                def build(self):
+                    contents = load(self, os.path.join(self.source_folder, "exported.txt"))
+                    save(self, "build.txt", contents)
+
+                def package(self):
+                    copy(self, "build.txt", self.build_folder, os.path.join(self.package_folder,
+                                                                            "res"))
+            """)
+    if with_build_type:
+        conanfile = conanfile.format(', "build_type"')
+    else:
+        conanfile = conanfile.format("")
+
+    client = TestClient()
+    client.save({"conanfile.py": conanfile, "exported.txt": "exported_contents"})
+    client.run("create . foo/1.0@ -s os=Linux")
+    assert "Packaged 1 '.txt' file: build.txt" in client.out
+
+    # Local flow
+    client.run("install . foo/1.0 -s os=Linux")
+
+    build_folder = "build-release" if with_build_type else "build"
+    assert os.path.exists(os.path.join(client.current_folder, build_folder, "conan", "generate.txt"))
+
+    client.run("build .")
+    contents = load(os.path.join(client.current_folder, build_folder, "build.txt"))
+    assert contents == "exported_contents"
+    client.run("export-pkg . foo/1.0@ --force")
+    assert "Packaged 1 '.txt' file: build.txt" in client.out


Code review comments:
diff_hunk: @@ -27,7 +27,12 @@ def cmake_layout(conanfile, generator=None):
         conanfile.folders.build = "cmake-build-{}".format(build_type)
         conanfile.folders.generators = os.path.join(conanfile.folders.build, "conan")
 
-    conanfile.cpp.source.includedirs = ["src"]
+    if src_folder == ".":
+        # The CMakeLists.txt is in the root, the includes (and sources) typically don't
path: conan/tools/cmake/layout.py
text: @user1:
This is a bit confusing, maybe this means ``includedirs`` is yet another argument ``cmake_layout(..., include_dirs=xxx``? Or that should be an after ``cmake_layout()`` call, directly calling ``self.cpp.source.includedirs = ["xxx"]`` to change it? Why ``src_folder`` yes and ``includedirs`` no?

@author:
That's why I added the comment, it is very confusing and quite random to provide "src"

@user2:
I agree, that's confusing, maybe we can also add that as an argument

@user1:
Lets make:
```python
conanfile.cpp.source.includedirs = ["include"]
```
For all cases

@author:
done