Repository: conan-io/conan
Pull request #11556 (2022-06-30 12:53:20)

Add new layout() test for projects sharing common code in parent folders
Changelog: Added a new ``self.folders.subproject`` for ``layout()`` to be able to define layouts that goes up to the parent or siblings folders, together with the ``self.folders.root = ".."``.
Docs: TODO


memsharded: I have changed the approach, with a ``self.folders.subproject`` definition, and extended its application to other generators as well. Please re-check.

Patch:
diff --git a/conan/tools/apple/xcodedeps.py b/conan/tools/apple/xcodedeps.py
index e972423d2..a15c533d1 100644
--- a/conan/tools/apple/xcodedeps.py
+++ b/conan/tools/apple/xcodedeps.py
@@ -137,7 +137,7 @@ class XcodeDeps(object):
             'lib_dirs': " ".join('"{}"'.format(p) for p in _merged_vars("libdirs")),
             'libs': " ".join("-l{}".format(lib) for lib in _merged_vars("libs")),
             'system_libs': " ".join("-l{}".format(sys_lib) for sys_lib in _merged_vars("system_libs")),
-            'frameworksdirs': " ".join('"{}"'.format(p) for p in _merged_vars("frameworkdirs")),
+            'frameworkdirs': " ".join('"{}"'.format(p) for p in _merged_vars("frameworkdirs")),
             'frameworks': " ".join("-framework {}".format(framework) for framework in _merged_vars("frameworks")),
             'definitions': " ".join('"{}"'.format(p.replace('"', '\\"')) for p in _merged_vars("defines")),
             'c_compiler_flags': " ".join('"{}"'.format(p.replace('"', '\\"')) for p in _merged_vars("cflags")),
diff --git a/conan/tools/cmake/layout.py b/conan/tools/cmake/layout.py
index 18f78c5e3..6a464f5c3 100644
--- a/conan/tools/cmake/layout.py
+++ b/conan/tools/cmake/layout.py
@@ -14,13 +14,14 @@ def cmake_layout(conanfile, generator=None, src_folder="."):
         else:
             multi = False
 
-    conanfile.folders.source = src_folder
+    subproject = conanfile.folders.subproject
+    conanfile.folders.source = src_folder if not subproject else os.path.join(subproject, src_folder)
     try:
         build_type = str(conanfile.settings.build_type)
     except ConanException:
         raise ConanException("'build_type' setting not defined, it is necessary for cmake_layout()")
 
-    build_folder = "build"
+    build_folder = "build" if not subproject else os.path.join(subproject, "build")
     custom_conf = get_build_folder_custom_vars(conanfile)
     if custom_conf:
         build_folder = "{}/{}".format(build_folder, custom_conf)
diff --git a/conan/tools/cmake/toolchain/blocks.py b/conan/tools/cmake/toolchain/blocks.py
index 1f0d0a198..5db4af04e 100644
--- a/conan/tools/cmake/toolchain/blocks.py
+++ b/conan/tools/cmake/toolchain/blocks.py
@@ -854,13 +854,21 @@ class OutputDirsBlock(Block):
 
         return textwrap.dedent("""
            set(CMAKE_INSTALL_PREFIX "{{package_folder}}")
+           {% if default_bin %}
            set(CMAKE_INSTALL_BINDIR "{{default_bin}}")
            set(CMAKE_INSTALL_SBINDIR "{{default_bin}}")
            set(CMAKE_INSTALL_LIBEXECDIR "{{default_bin}}")
+           {% endif %}
+           {% if default_lib %}
            set(CMAKE_INSTALL_LIBDIR "{{default_lib}}")
+           {% endif %}
+           {% if default_include %}
            set(CMAKE_INSTALL_INCLUDEDIR "{{default_include}}")
            set(CMAKE_INSTALL_OLDINCLUDEDIR "{{default_include}}")
+           {% endif %}
+           {% if default_res %}
            set(CMAKE_INSTALL_DATAROOTDIR "{{default_res}}")
+           {% endif %}
         """)
 
     def _get_cpp_info_value(self, name):
diff --git a/conan/tools/layout/__init__.py b/conan/tools/layout/__init__.py
index 9cd971712..8f64e14e7 100644
--- a/conan/tools/layout/__init__.py
+++ b/conan/tools/layout/__init__.py
@@ -6,10 +6,12 @@ from conan.tools.google import bazel_layout
 
 
 def basic_layout(conanfile, src_folder="."):
-    conanfile.folders.build = "build"
+    subproject = conanfile.folders.subproject
+
+    conanfile.folders.source = src_folder if not subproject else os.path.join(subproject, src_folder)
+    conanfile.folders.build = "build" if not subproject else os.path.join(subproject, "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/microsoft/layout.py b/conan/tools/microsoft/layout.py
index 9d3a045e0..4da792179 100644
--- a/conan/tools/microsoft/layout.py
+++ b/conan/tools/microsoft/layout.py
@@ -15,13 +15,14 @@ def vs_layout(conanfile):
         if not arch:
             raise ConanException("The 'vs_layout' doesn't "
                                  "work with the arch '{}'".format(conanfile.settings.arch))
-        base = os.path.join(arch, str(conanfile.settings.build_type))
+        bindirs = os.path.join(arch, str(conanfile.settings.build_type))
     else:
-        base = str(conanfile.settings.build_type)
+        bindirs = str(conanfile.settings.build_type)
 
-    conanfile.folders.build = "."
-    conanfile.folders.generators = "conan"
-    conanfile.folders.source = "."
-    conanfile.cpp.build.libdirs = [base]
-    conanfile.cpp.build.bindirs = [base]
+    subproject = conanfile.folders.subproject
+    conanfile.folders.build = subproject or "."
+    conanfile.folders.generators = os.path.join(subproject, "conan") if subproject else "conan"
+    conanfile.folders.source = subproject or "."
+    conanfile.cpp.build.libdirs = [bindirs]
+    conanfile.cpp.build.bindirs = [bindirs]
     conanfile.cpp.source.includedirs = ["include"]
diff --git a/conans/client/conan_command_output.py b/conans/client/conan_command_output.py
index 588b48240..91a6fff6a 100644
--- a/conans/client/conan_command_output.py
+++ b/conans/client/conan_command_output.py
@@ -140,6 +140,10 @@ class CommandOutputer(object):
             item_data["build_id"] = build_id(conanfile)
             item_data["context"] = conanfile.context
 
+            item_data["invalid_build"] = node.cant_build is not False
+            if node.cant_build:
+                item_data["invalid_build_reason"] = node.cant_build
+
             python_requires = getattr(conanfile, "python_requires", None)
             if python_requires and not isinstance(python_requires, dict):  # no old python requires
                 item_data["python_requires"] = [repr(r)
diff --git a/conans/client/graph/graph.py b/conans/client/graph/graph.py
index dbfc5ea5c..fbcfac5c5 100644
--- a/conans/client/graph/graph.py
+++ b/conans/client/graph/graph.py
@@ -91,6 +91,8 @@ class Node(object):
         self.id_direct_prefs = None
         self.id_indirect_prefs = None
 
+        self.cant_build = False  # It will set to a str with a reason if the validate_build() fails
+
     @property
     def id(self):
         return self._id
diff --git a/conans/client/graph/graph_binaries.py b/conans/client/graph/graph_binaries.py
index 103832b2a..4977e2d68 100644
--- a/conans/client/graph/graph_binaries.py
+++ b/conans/client/graph/graph_binaries.py
@@ -48,7 +48,10 @@ class GraphBinariesAnalyzer(object):
                     break
         if build_mode.forced(conanfile, ref, with_deps_to_build):
             conanfile.output.info('Forced build from source')
-            node.binary = BINARY_BUILD
+            if node.cant_build:
+                node.binary = BINARY_INVALID
+            else:
+                node.binary = BINARY_BUILD
             node.prev = None
             return True
 
@@ -178,7 +181,10 @@ class GraphBinariesAnalyzer(object):
             pref = PackageReference(locked.ref, locked.package_id, locked.prev)  # Keep locked PREV
             self._process_node(node, pref, build_mode, update, remotes)
             if node.binary == BINARY_MISSING and build_mode.allowed(node.conanfile):
-                node.binary = BINARY_BUILD
+                if node.cant_build:
+                    node.binary = BINARY_INVALID
+                else:
+                    node.binary = BINARY_BUILD
             if node.binary == BINARY_BUILD:
                 locked.unlock_prev()
 
@@ -231,7 +237,10 @@ class GraphBinariesAnalyzer(object):
                     if node.binary == BINARY_MISSING and node.package_id == PACKAGE_ID_INVALID:
                         node.binary = BINARY_INVALID
                 if node.binary == BINARY_MISSING and build_mode.allowed(node.conanfile):
-                    node.binary = BINARY_BUILD
+                    if node.cant_build:
+                        node.binary = BINARY_INVALID
+                    else:
+                        node.binary = BINARY_BUILD
 
             if locked:
                 # package_id was not locked, this means a base lockfile that is being completed
@@ -253,6 +262,8 @@ class GraphBinariesAnalyzer(object):
             node.binary = BINARY_INVALID
             return
 
+
+
         if self._evaluate_build(node, build_mode):
             return
 
@@ -293,7 +304,10 @@ class GraphBinariesAnalyzer(object):
                 local_recipe_hash = package_layout.recipe_manifest().summary_hash
                 if local_recipe_hash != recipe_hash:
                     conanfile.output.info("Outdated package!")
-                    node.binary = BINARY_BUILD
+                    if node.cant_build:
+                        node.binary = BINARY_INVALID
+                    else:
+                        node.binary = BINARY_BUILD
                     node.prev = None
                 else:
                     conanfile.output.info("Package is up to date")
@@ -400,6 +414,14 @@ class GraphBinariesAnalyzer(object):
                 except ConanInvalidConfiguration as e:
                     conanfile.info.invalid = str(e)
 
+        if hasattr(conanfile, "validate_build") and callable(conanfile.validate_build):
+            with conanfile_exception_formatter(str(conanfile), "validate_build"):
+                try:
+                    conanfile.validate_build()
+                except ConanInvalidConfiguration as e:
+                    # This 'cant_build' will be ignored if we don't have to build the node.
+                    node.cant_build = str(e)
+
         info = conanfile.info
         node.package_id = info.package_id()
 
diff --git a/conans/client/graph/graph_builder.py b/conans/client/graph/graph_builder.py
index 9f9d3baa6..05b7ef1df 100644
--- a/conans/client/graph/graph_builder.py
+++ b/conans/client/graph/graph_builder.py
@@ -3,7 +3,7 @@ import time
 from conans.client.conanfile.configure import run_configure_method
 from conans.client.graph.graph import DepsGraph, Node, RECIPE_EDITABLE, CONTEXT_HOST, CONTEXT_BUILD
 from conans.errors import (ConanException, ConanExceptionInUserConanfileMethod,
-                           conanfile_exception_formatter)
+                           conanfile_exception_formatter, ConanInvalidConfiguration)
 from conans.model.conan_file import get_env_context_manager
 from conans.model.ref import ConanFileReference
 from conans.model.requires import Requirements, Requirement
@@ -65,7 +65,9 @@ class DepsGraphBuilder(object):
         t1 = time.time()
         self._expand_node(root_node, dep_graph, Requirements(), None, None, check_updates,
                           update, remotes, profile_host, profile_build, graph_lock)
+
         logger.debug("GRAPH: Time to load deps %s" % (time.time() - t1))
+
         return dep_graph
 
     def extend_build_requires(self, graph, node, build_requires_refs, check_updates, update,
@@ -471,4 +473,5 @@ class DepsGraphBuilder(object):
 
         dep_graph.add_node(new_node)
         dep_graph.add_edge(current_node, new_node, requirement)
+
         return new_node
diff --git a/conans/client/graph/graph_manager.py b/conans/client/graph/graph_manager.py
index 3568566e7..10a75d463 100644
--- a/conans/client/graph/graph_manager.py
+++ b/conans/client/graph/graph_manager.py
@@ -124,6 +124,7 @@ class GraphManager(object):
         root_node = self._load_root_node(reference, create_reference, profile_host, graph_lock,
                                          root_ref, lockfile_node_id, is_build_require,
                                          require_overrides)
+
         deps_graph = self._resolve_graph(root_node, profile_host, profile_build, graph_lock,
                                          build_mode, check_updates, update, remotes, recorder,
                                          apply_build_requires=apply_build_requires)
diff --git a/conans/client/installer.py b/conans/client/installer.py
index 65649b942..260c7dcdd 100644
--- a/conans/client/installer.py
+++ b/conans/client/installer.py
@@ -423,7 +423,13 @@ class BinaryInstaller(object):
         if invalid:
             msg = ["There are invalid packages (packages that cannot exist for this configuration):"]
             for node in invalid:
-                msg.append("{}: Invalid ID: {}".format(node.conanfile, node.conanfile.info.invalid))
+                if node.cant_build:
+                    msg.append("{}: Cannot build "
+                               "for this configuration: {}".format(node.conanfile,
+                                                                   node.cant_build))
+                else:
+                    msg.append("{}: Invalid ID: {}".format(node.conanfile,
+                                                           node.conanfile.info.invalid))
             raise ConanInvalidConfiguration("\n".join(msg))
         self._raise_missing(missing)
         processed_package_refs = {}
diff --git a/conans/model/layout.py b/conans/model/layout.py
index cce15ddd5..d3c4828cb 100644
--- a/conans/model/layout.py
+++ b/conans/model/layout.py
@@ -31,6 +31,10 @@ class Folders(object):
         # Relative location of the project root, if the conanfile is not in that project root, but
         # in a subfolder: e.g: If the conanfile is in a subfolder then self.root = ".."
         self.root = None
+        # The relative location with respect to the project root of the subproject containing the
+        # conanfile.py, that makes most of the output folders defined in layouts (cmake_layout, etc)
+        # start from the subproject again
+        self.subproject = None
 
     def __repr__(self):
         return str(self.__dict__)


Test patch:
diff --git a/conans/test/functional/layout/test_in_subfolder.py b/conans/test/functional/layout/test_in_subfolder.py
index 33925fa67..af3f58293 100644
--- a/conans/test/functional/layout/test_in_subfolder.py
+++ b/conans/test/functional/layout/test_in_subfolder.py
@@ -1,5 +1,9 @@
 import textwrap
 
+import pytest
+
+from conans.test.assets.cmake import gen_cmakelists
+from conans.test.assets.sources import gen_function_cpp
 from conans.test.utils.tools import TestClient
 
 
@@ -54,3 +58,113 @@ def test_exports_sources_own_code_in_subfolder():
     c.run("build conan")
     assert "conanfile.py (pkg/0.1): MYCMAKE-BUILD: mycmake!" in c.out
     assert c.load("build/mylib.a") == "mylib"
+
+
+def test_exports_sources_common_code():
+    """ very similar to the above, but intended for a multi-package project sharing some
+    common code
+    """
+    c = TestClient()
+    conanfile = textwrap.dedent("""
+        import os
+        from conan import ConanFile
+        from conan.tools.files import load, copy, save
+        class Pkg(ConanFile):
+            name = "pkg"
+            version = "0.1"
+
+            def layout(self):
+                self.folders.root = ".."
+                self.folders.source = "pkg"
+                self.folders.build = "build"
+
+            def export_sources(self):
+                source_folder = os.path.join(self.recipe_folder, "..")
+                copy(self, "*.txt", source_folder, self.export_sources_folder)
+                copy(self, "*.cmake", source_folder, self.export_sources_folder)
+
+            def source(self):
+                cmake = load(self, "CMakeLists.txt")
+                self.output.info("MYCMAKE-SRC: {}".format(cmake))
+
+            def build(self):
+                path = os.path.join(self.source_folder, "CMakeLists.txt")
+                cmake = load(self, path)
+                self.output.info("MYCMAKE-BUILD: {}".format(cmake))
+
+                path = os.path.join(self.export_sources_folder, "common", "myutils.cmake")
+                cmake = load(self, path)
+                self.output.info("MYUTILS-BUILD: {}".format(cmake))
+            """)
+    c.save({"pkg/conanfile.py": conanfile,
+            "pkg/CMakeLists.txt": "mycmake!",
+            "common/myutils.cmake": "myutils!"})
+    c.run("create pkg")
+    assert "pkg/0.1: MYCMAKE-SRC: mycmake!" in c.out
+    assert "pkg/0.1: MYCMAKE-BUILD: mycmake!" in c.out
+    assert "pkg/0.1: MYUTILS-BUILD: myutils!" in c.out
+
+    # Local flow
+    c.run("install pkg")
+    # SOURCE NOT CALLED! It doesnt make sense (will fail due to local exports)
+    c.run("build pkg")
+    assert "conanfile.py (pkg/0.1): MYCMAKE-BUILD: mycmake!" in c.out
+    assert "conanfile.py (pkg/0.1): MYUTILS-BUILD: myutils!" in c.out
+
+
+@pytest.mark.tool_cmake
+def test_exports_sources_common_code_layout():
+    """ Equal to the previous test, but actually building and using cmake_layout
+    """
+    c = TestClient()
+    conanfile = textwrap.dedent("""
+        import os
+        from conan import ConanFile
+        from conan.tools.cmake import cmake_layout, CMake
+        from conan.tools.files import load, copy, save
+        class Pkg(ConanFile):
+            name = "pkg"
+            version = "0.1"
+            settings = "os", "compiler", "build_type", "arch"
+            generators = "CMakeToolchain"
+
+            def layout(self):
+                self.folders.root = ".."
+                self.folders.subproject = "pkg"
+                cmake_layout(self)
+
+            def export_sources(self):
+                source_folder = os.path.join(self.recipe_folder, "..")
+                copy(self, "*", source_folder, self.export_sources_folder)
+
+            def build(self):
+                cmake = CMake(self)
+                cmake.configure()
+                cmake.build()
+                self.run(os.path.join(self.cpp.build.bindirs[0], "myapp"))
+            """)
+    cmake_include = "include(${CMAKE_CURRENT_LIST_DIR}/../common/myutils.cmake)"
+    c.save({"pkg/conanfile.py": conanfile,
+            "pkg/app.cpp": gen_function_cpp(name="main", includes=["../common/myheader"],
+                                            preprocessor=["MYDEFINE"]),
+            "pkg/CMakeLists.txt": gen_cmakelists(appsources=["app.cpp"],
+                                                 custom_content=cmake_include),
+            "common/myutils.cmake": 'message(STATUS "MYUTILS.CMAKE!")',
+            "common/myheader.h": '#define MYDEFINE "MYDEFINEVALUE"'})
+    c.run("create pkg")
+    assert "MYUTILS.CMAKE!" in c.out
+    assert "main: Release!" in c.out
+    assert "MYDEFINE: MYDEFINEVALUE" in c.out
+
+    # Local flow
+    c.run("install pkg")
+    c.run("build pkg")
+    assert "MYUTILS.CMAKE!" in c.out
+    assert "main: Release!" in c.out
+    assert "MYDEFINE: MYDEFINEVALUE" in c.out
+
+    c.run("install pkg -s build_type=Debug")
+    c.run("build pkg")
+    assert "MYUTILS.CMAKE!" in c.out
+    assert "main: Debug!" in c.out
+    assert "MYDEFINE: MYDEFINEVALUE" in c.out
diff --git a/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py b/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
index c7101504c..095ae5c61 100644
--- a/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
+++ b/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
@@ -918,3 +918,94 @@ def test_cmake_presets_forbidden_build_type():
     client.run("install . {}".format(settings_layout), assert_error=True)
     assert "Error, don't include 'settings.build_type' in the " \
            "'tools.cmake.cmake_layout:build_folder_vars' conf" in client.out
+
+
+def test_resdirs_cmake_install():
+    """If resdirs is declared, the CMAKE_INSTALL_DATAROOTDIR folder is set"""
+
+    client = TestClient(path_with_spaces=False)
+
+    conanfile = textwrap.dedent("""
+            from conan import ConanFile
+            from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout
+
+            class AppConan(ConanFile):
+                settings = "os", "compiler", "build_type", "arch"
+                exports_sources = "CMakeLists.txt", "my_license"
+                name = "foo"
+                version = "1.0"
+
+                def generate(self):
+                    tc = CMakeToolchain(self)
+                    tc.generate()
+
+                def layout(self):
+                    self.cpp.package.resdirs = ["res"]
+
+                def build(self):
+                    cmake = CMake(self)
+                    cmake.configure()
+                    cmake.build()
+
+                def package(self):
+                    cmake = CMake(self)
+                    cmake.install()
+            """)
+
+    cmake = """
+    cmake_minimum_required(VERSION 3.15)
+    set(CMAKE_CXX_COMPILER_WORKS 1)
+    project(foo)
+    if(NOT CMAKE_INSTALL_DATAROOTDIR)
+        message(FATAL_ERROR "Cannot install stuff")
+    endif()
+    install(FILES my_license DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/licenses)
+    """
+
+    client.save({"conanfile.py": conanfile, "CMakeLists.txt": cmake, "my_license": "MIT"})
+    client.run("create .")
+    assert "/res/licenses/my_license" in client.out
+    assert "Packaged 1 file: my_license" in client.out
+
+
+def test_resdirs_none_cmake_install():
+    """If no resdirs are declared, the CMAKE_INSTALL_DATAROOTDIR folder is not set"""
+
+    client = TestClient(path_with_spaces=False)
+
+    conanfile = textwrap.dedent("""
+            from conan import ConanFile
+            from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout
+
+            class AppConan(ConanFile):
+                settings = "os", "compiler", "build_type", "arch"
+                exports_sources = "CMakeLists.txt", "my_license"
+                name = "foo"
+                version = "1.0"
+
+                def generate(self):
+                    tc = CMakeToolchain(self)
+                    tc.generate()
+
+                def build(self):
+                    cmake = CMake(self)
+                    cmake.configure()
+                    cmake.build()
+
+                def package(self):
+                    cmake = CMake(self)
+                    cmake.install()
+            """)
+
+    cmake = """
+    cmake_minimum_required(VERSION 3.15)
+    set(CMAKE_CXX_COMPILER_WORKS 1)
+    project(foo)
+    if(NOT CMAKE_INSTALL_DATAROOTDIR)
+        message(FATAL_ERROR "Cannot install stuff")
+    endif()
+    """
+
+    client.save({"conanfile.py": conanfile, "CMakeLists.txt": cmake, "my_license": "MIT"})
+    client.run("create .", assert_error=True)
+    assert "Cannot install stuff" in client.out
diff --git a/conans/test/integration/graph/test_validate_build.py b/conans/test/integration/graph/test_validate_build.py
new file mode 100644
index 000000000..65ab33011
--- /dev/null
+++ b/conans/test/integration/graph/test_validate_build.py
@@ -0,0 +1,83 @@
+import json
+import textwrap
+
+from conans.test.assets.genconanfile import GenConanfile
+from conans.test.utils.tools import TestClient
+
+
+def test_basic_validate_build_test():
+
+    t = TestClient()
+    conanfile = textwrap.dedent("""
+    from conan import ConanFile
+    from conans.errors import ConanInvalidConfiguration
+
+    class myConan(ConanFile):
+        name = "foo"
+        version = "1.0"
+        settings = "os", "arch", "compiler"
+
+        def validate_build(self):
+            if self.settings.compiler == "gcc":
+                raise ConanInvalidConfiguration("This doesn't build in GCC")
+
+        def package_id(self):
+            del self.info.settings.compiler
+    """)
+
+    settings_gcc = "-s compiler=gcc -s compiler.libcxx=libstdc++11 -s compiler.version=11"
+    settings_clang = "-s compiler=clang -s compiler.libcxx=libc++ -s compiler.version=8"
+
+    t.save({"conanfile.py": conanfile})
+    t.run(f"create . {settings_gcc}", assert_error=True)
+
+    assert "foo/1.0: Cannot build for this configuration: This doesn't build in GCC" in t.out
+
+    t.run(f"create . {settings_clang}")
+
+    # Now with GCC again, but now we have the binary, we don't need to build, so it doesn't fail
+    t.run(f"create . {settings_gcc} --build missing")
+    assert "foo/1.0: Already installed!" in t.out
+
+    # But if I force the build... it will fail
+    t.run(f"create . {settings_gcc} ", assert_error=True)
+    assert "foo/1.0: Cannot build for this configuration: This doesn't build in GCC" in t.out
+
+    # What happens with a conan info?
+    t.run(f"info foo/1.0@ {settings_gcc} --json=myjson")
+    myjson = json.loads(t.load("myjson"))
+    assert myjson[0]["invalid_build"] is True
+    assert myjson[0]["invalid_build_reason"] == "This doesn't build in GCC"
+
+    t.run(f"info foo/1.0@ {settings_clang} --json=myjson")
+    myjson = json.loads(t.load("myjson"))
+    assert myjson[0]["invalid_build"] is False
+    assert "invalid_build_reason" not in myjson[0]
+
+
+def test_with_options_validate_build_test():
+    t = TestClient()
+    conanfile = textwrap.dedent("""
+    from conan import ConanFile
+    from conans.errors import ConanInvalidConfiguration
+
+    class myConan(ConanFile):
+        name = "foo"
+        version = "1.0"
+        options = {"my_option": [True, False]}
+        default_options = {"my_option": True}
+
+        def validate_build(self):
+            if not self.options.my_option:
+                raise ConanInvalidConfiguration("This doesn't build with False option")
+
+    """)
+    t.save({"conanfile.py": conanfile})
+    t.run("export .")
+    consumer = GenConanfile().with_require("foo/1.0").with_name("consumer").with_version("1.0")
+    t.save({"consumer.py": consumer})
+    t.run("create consumer.py --build missing -o foo/*:my_option=False", assert_error=True)
+    assert "foo/1.0: Cannot build for this configuration: This doesn't build " \
+           "with False option" in t.out
+
+    t.run("create consumer.py --build missing -o foo/*:my_option=True")
diff --git a/conans/test/integration/layout/__init__.py b/conans/test/integration/layout/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/conans/test/integration/toolchains/apple/test_xcodedeps.py b/conans/test/integration/toolchains/apple/test_xcodedeps.py
index c60ea523a..32c9b2385 100644
--- a/conans/test/integration/toolchains/apple/test_xcodedeps.py
+++ b/conans/test/integration/toolchains/apple/test_xcodedeps.py
@@ -174,3 +174,25 @@ def test_xcodedeps_aggregate_components():
 
     # folders are aggregated
     assert "mylibdir" in component4_vars
+
+
+@pytest.mark.skipif(platform.system() != "Darwin", reason="Only for MacOS")
+def test_xcodedeps_frameworkdirs():
+    client = TestClient()
+
+    conanfile_py = textwrap.dedent("""
+        from conan import ConanFile
+        class LibConan(ConanFile):
+            settings = "os", "compiler", "build_type", "arch"
+            def package_info(self):
+                self.cpp_info.frameworkdirs = ["lib_a_frameworkdir"]
+        """)
+
+    client.save({"conanfile.py": conanfile_py})
+    client.run("create conanfile.py lib_a/1.0@")
+
+    client.run("install lib_a/1.0@ -g XcodeDeps")
+
+    lib_a_xcconfig = client.load("conan_lib_a_lib_a_release_x86_64.xcconfig")
+
+    assert "lib_a_frameworkdir" in lib_a_xcconfig
diff --git a/conans/test/integration/toolchains/gnu/test_basic_layout.py b/conans/test/integration/toolchains/gnu/test_basic_layout.py
new file mode 100644
index 000000000..19cd9ebb6
--- /dev/null
+++ b/conans/test/integration/toolchains/gnu/test_basic_layout.py
@@ -0,0 +1,25 @@
+import os
+import platform
+import textwrap
+
+from conans.test.utils.tools import TestClient
+
+
+def test_basic_layout_subproject():
+    c = TestClient()
+    conanfile = textwrap.dedent("""
+        from conan import ConanFile
+        from conan.tools.layout import basic_layout
+        class Pkg(ConanFile):
+            settings = "os", "compiler", "build_type", "arch"
+            generators = "AutotoolsToolchain"
+            def layout(self):
+                self.folders.root = ".."
+                self.folders.subproject = "pkg"
+                basic_layout(self)
+        """)
+    c.save({"pkg/conanfile.py": conanfile})
+    c.run("install pkg")
+    ext = "sh" if platform.system() != "Windows" else "bat"
+    assert os.path.isfile(os.path.join(c.current_folder, "pkg", "build-release", "conan",
+                                       "conanautotoolstoolchain.{}".format(ext)))
diff --git a/conans/test/integration/toolchains/microsoft/test_vs_layout.py b/conans/test/integration/toolchains/microsoft/test_vs_layout.py
new file mode 100644
index 000000000..5a446da73
--- /dev/null
+++ b/conans/test/integration/toolchains/microsoft/test_vs_layout.py
@@ -0,0 +1,22 @@
+import os
+import textwrap
+
+from conans.test.utils.tools import TestClient
+
+
+def test_vs_layout_subproject():
+    c = TestClient()
+    conanfile = textwrap.dedent("""
+        from conan import ConanFile
+        from conan.tools.microsoft import vs_layout
+        class Pkg(ConanFile):
+            settings = "os", "compiler", "build_type", "arch"
+            generators = "MSBuildToolchain"
+            def layout(self):
+                self.folders.root = ".."
+                self.folders.subproject = "pkg"
+                vs_layout(self)
+        """)
+    c.save({"pkg/conanfile.py": conanfile})
+    c.run("install pkg")
+    assert os.path.isfile(os.path.join(c.current_folder, "pkg", "conan", "conantoolchain.props"))