Repository: conan-io/conan

Issue:
[bug] missing CMakeUserPresets.json
Conan 1.48.0 does not generate `CMakeUserPresets.json` if `conanfile.txt` used instead of `.py`.

Probably this is duplicate of #11172.

### Environment Details
  * Operating System: linux
  * Conan version: 1.48.0

### Steps to reproduce
```
cd "$(mktemp -d)"
conan new hello/0.1 -m=cmake_exe
rm conanfile.py
echo -e "[generators]\nCMakeToolchain" > conanfile.txt
mkdir build
(cd build && conan install ..)
tree "$(pwd)"
```

### Output
```
/tmp/tmp.6FdEpSMGTR
├── CMakeLists.txt
├── build
│   ├── CMakePresets.json
│   ├── conan.lock
│   ├── conan_toolchain.cmake
│   ├── conanbuildinfo.txt
│   ├── conaninfo.txt
│   └── graph_info.json
├── conanfile.txt
├── include
│   └── hello.h
├── src
│   ├── hello.cpp
│   └── main.cpp
└── test_package
    └── conanfile.py
```


Discussion:
Hi @magras 

That's expected because it needs a ``layout`` to create the ``CMakeUserPresets.json``, so it's impossible to do that without a ``conanfile.py``:

```python
    def layout(self):
        cmake_layout(self)
```

That's why it's failing when a ``conanfile.txt`` is there instead.
More information [here](https://docs.conan.io/en/latest/reference/conanfile/tools/cmake/cmaketoolchain.html#using-the-toolchain-in-developer-flow).

> That's expected because it needs a `layout` to create the `CMakeUserPresets.json`

So what I should do if I'm using `.txt` and `CMakeToolchain` generator in my projects?
* As a short term solution I guess I can symlink  `CMakePresets.json` to the root of the source dir.
* What will be the long term solution? Rewrite `.txt` to `.py`?
I guess it could be better to rewrite it and try a simple one. For instance, this could be good enough:

```python
from conan import ConanFile
from conan.tools.cmake import CMake, cmake_layout


class PkgConan(ConanFile):
    settings = "os", "compiler", "build_type", "arch"
    generators = "CMakeToolchain"

    def layout(self):
        cmake_layout(self)

    def build(self):
        cmake = CMake(self)
        cmake.configure()
        cmake.build()

    def package(self):
        cmake = CMake(self)
        cmake.install()
```
If you only want to use that generator, then I think you can use this one. The advantages of using it are that you can always customize it, e.g., if you need more ad-hoc configurations, builds, etc.

UPDATED: even simpler:

```python
from conan import ConanFile
from conan.tools.cmake import cmake_layout


class PkgConan(ConanFile):
    settings = "os", "compiler", "build_type", "arch"
    generators = "CMakeToolchain"

    def layout(self):
        cmake_layout(self)
```
IIRC toolchain generator should become the main generator for cmake in conan 2.0.
Does it mean that usage of `.txt` is discouraged and will be deprecated in 2.0?
> Does it mean that usage of .txt is discouraged and will be deprecated in 2.0?

No, the ``.txt`` will still be there. And the new CMake integration will still generate a ``CMakePresets.json`` file, it is only the ``CMakeUserPresets.json`` that might be generated under certain conditions (it is not already existing), and that might be used to aggregate multiple different configurations (each one might have a different ``CMakePresets.json`` in a different folder), but for this functionality to be manageable, a ``layout`` is necessary, otherwise it becomes too messy. So it is the ``CMakeUserPresets.json`` that only will be generated for more advanced cases that will require a ``conanfile.py``, while for simple usage a ``conanfile.txt`` with the ``CMakePresets.json`` would be the solution.
So again what is the long term solution for `.txt`?
* to use symlink or to write my own `CMakeUserPresets.json` that includes `build/CMakePresets.json`?
  It sounds reasonable for a dev environment, but I dislike it in most other scenarios, such as on a build server.
* to ask cmake devs for a new option to specify preset file location?
Of course, requesting the possibility to CMake to allow defining a custom location of the presets file would be ideal. I don't understand why they won't allow specifying the location of a presets file. 

Besides that, yes, you have different possibilities, the one you commented. Or just use a ``conanfile.py``, for 98% of usage it is irrelevant if .py or .txt, because it is just used, and for the other cases, a simple .py is practically as trivial as the conanfile.txt.

We could re-check if it would make sense to generate a CMakeUserPresets.json too for that case, but maybe it won't, lets see what @lasote thinks.
We have discussed this, and this requires some changes that could be breaking in 1.X, so we are moving this to 2.0 scope:

The changes are:
- Change the default ``layout``, so the build and generators folders do not pollute the current directory, something like ``build/generators``
- Make the ``CMakeToolchain`` generate ``CMakeUserPresets.json``, also for ``conanfile.txt``

In the meantime, please use the ``CMakePresets`` (copy or link it), or move to a ``conanfile.py``. Thanks!
I think that minimizing the difference in behavior between `.txt` and `.py` will lead to less confusion among the users. I appreciate your decision. Thank you.

Resolving patch:
diff --git a/conans/client/loader.py b/conans/client/loader.py
--- a/conans/client/loader.py
+++ b/conans/client/loader.py
@@ -3,10 +3,14 @@
 import inspect
 import os
 import sys
+import types
 import uuid
 
 import yaml
 
+from conan.tools.cmake import cmake_layout
+from conan.tools.google import bazel_layout
+from conan.tools.microsoft import vs_layout
 from conans.client.conf.required_version import validate_conan_version
 from conans.client.loader_txt import ConanFileTextLoader
 from conans.client.tools.files import chdir
@@ -304,9 +308,20 @@ def _parse_conan_txt(self, contents, path, display_name, profile):
             if not hasattr(conanfile, "build_requires"):
                 conanfile.build_requires = []
             conanfile.build_requires.append(build_reference)
+        if parser.layout:
+            layout_method = {"cmake_layout": cmake_layout,
+                             "vs_layout": vs_layout,
+                             "bazel_layout": bazel_layout}.get(parser.layout)
+            if not layout_method:
+                raise ConanException("Unknown predefined layout '{}' declared in "
+                                     "conanfile.txt".format(parser.layout))
 
-        conanfile.generators = parser.generators
+            def layout(self):
+                layout_method(self)
+
+            conanfile.layout = types.MethodType(layout, conanfile)
 
+        conanfile.generators = parser.generators
         try:
             options = OptionsValues.loads(parser.options)
         except Exception:
diff --git a/conans/client/loader_txt.py b/conans/client/loader_txt.py
--- a/conans/client/loader_txt.py
+++ b/conans/client/loader_txt.py
@@ -8,9 +8,19 @@ class ConanFileTextLoader(object):
     def __init__(self, input_text):
         # Prefer composition over inheritance, the __getattr__ was breaking things
         self._config_parser = ConfigParser(input_text,  ["requires", "generators", "options",
-                                                         "imports", "build_requires", "tool_requires"],
+                                                         "imports", "build_requires",
+                                                         "tool_requires", "layout"],
                                            parse_lines=True)
 
+    @property
+    def layout(self):
+        """returns the declared layout"""
+        tmp = [r.strip() for r in self._config_parser.layout.splitlines()]
+        if len(tmp) > 1:
+            raise ConanException("Only one layout can be declared in the [layout] section of "
+                                 "the conanfile.txt")
+        return tmp[0] if tmp else None
+
     @property
     def requirements(self):
         """returns a list of requires


Test patch:
diff --git a/conans/test/conftest.py b/conans/test/conftest.py
--- a/conans/test/conftest.py
+++ b/conans/test/conftest.py
@@ -76,7 +76,8 @@
         "3.23": {
             "path": {'Windows': 'C:/cmake/cmake-3.23.1-win64-x64/bin',
                      'Darwin': '/Users/jenkins/cmake/cmake-3.23.1/bin',
-                     'Linux': '/usr/share/cmake-3.23.1/bin'}
+                     # Not available in Linux
+                     'Linux': None}
         }
     },
     'ninja': {
@@ -188,6 +189,16 @@ def _get_tool(name, version):
                 _cached_tools[name][version] = False
                 return False
             tool_path = tool_version.get("path", {}).get(tool_platform)
+            # To allow to skip for a platform, we can put the path to None
+            # "cmake": { "3.23": {
+            #               "path": {'Windows': 'C:/cmake/cmake-3.23.1-win64-x64/bin',
+            #                        'Darwin': '/Users/jenkins/cmake/cmake-3.23.1/bin',
+            #                        'Linux': None}}
+            #          }
+            if tool_path is None:
+                _cached_tools[name][version] = False
+                return False
+
         else:
             if version is not None:  # if the version is specified, it should be in the conf
                 _cached_tools[name][version] = True
diff --git a/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py b/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
--- a/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
+++ b/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
@@ -788,3 +788,49 @@ def build(self):
     })
     client.run("create . app/1.0@")
     assert "sysroot: '{}'".format(output_fake_sysroot) in client.out
+
+
+# FIXME: DEVELOP2: @pytest.mark.tool("cmake", "3.23")
+@pytest.mark.tool_cmake(version="3.23")
+def test_cmake_presets_with_conanfile_txt():
+    c = TestClient()
+
+    # FIXME: DEVELOP 2: c.run("new cmake_exe -d name=foo -d version=1.0")
+    c.run("new foo/1.0 --template cmake_exe")
+    os.unlink(os.path.join(c.current_folder, "conanfile.py"))
+    c.save({"conanfile.txt": textwrap.dedent("""
+
+    [generators]
+    CMakeToolchain
+
+    [layout]
+    cmake_layout
+
+    """)})
+
+    c.run("install .")
+    c.run("install . -s build_type=Debug")
+    assert os.path.exists(os.path.join(c.current_folder, "CMakeUserPresets.json"))
+    presets_path = os.path.join(c.current_folder, "build", "generators", "CMakePresets.json")
+    assert os.path.exists(presets_path)
+
+    if platform.system() != "Windows":
+        c.run_command("cmake --preset Debug")
+        c.run_command("cmake --build --preset Debug")
+        c.run_command("./cmake-build-debug/foo")
+    else:
+        c.run_command("cmake --preset default")
+        c.run_command("cmake --build --preset Debug")
+        c.run_command("build\\Debug\\foo")
+
+    assert "Hello World Debug!" in c.out
+
+    if platform.system() != "Windows":
+        c.run_command("cmake --preset Release")
+        c.run_command("cmake --build --preset Release")
+        c.run_command("./cmake-build-release/foo")
+    else:
+        c.run_command("cmake --build --preset Release")
+        c.run_command("build\\Release\\foo")
+
+    assert "Hello World Release!" in c.out
diff --git a/conans/test/unittests/client/conanfile_loader_test.py b/conans/test/unittests/client/conanfile_loader_test.py
--- a/conans/test/unittests/client/conanfile_loader_test.py
+++ b/conans/test/unittests/client/conanfile_loader_test.py
@@ -253,6 +253,34 @@ def test_load_options_error(self):
                                    "Options should be specified as 'pkg:option=value'"):
             loader.load_conanfile_txt(file_path, create_profile())
 
+    def test_layout_not_predefined(self):
+        txt = textwrap.dedent("""
+                    [layout]
+                    missing
+                """)
+        tmp_dir = temp_folder()
+        file_path = os.path.join(tmp_dir, "conanfile.txt")
+        save(file_path, txt)
+        with pytest.raises(ConanException) as exc:
+            loader = ConanFileLoader(None, Mock(), None)
+            loader.load_conanfile_txt(file_path, create_profile())
+        assert "Unknown predefined layout 'missing'" in str(exc.value)
+
+    def test_layout_multiple(self):
+        txt = textwrap.dedent("""
+                    [layout]
+                    cmake_layout
+                    vs_layout
+                """)
+        tmp_dir = temp_folder()
+        file_path = os.path.join(tmp_dir, "conanfile.txt")
+        save(file_path, txt)
+        with pytest.raises(ConanException) as exc:
+            loader = ConanFileLoader(None, Mock(), None)
+            loader.load_conanfile_txt(file_path, create_profile())
+        assert "Only one layout can be declared in the [layout] section of the conanfile.txt" \
+               in str(exc.value)
+
 
 class ImportModuleLoaderTest(unittest.TestCase):
     @staticmethod