Repository: conan-io/conan
Pull request #11669 (2022-07-18 17:01:35)

Feature/msbuilddeps components
Changelog: Feature: Support for components in MSBuildDeps
Docs: TODO

Prior step before https://github.com/conan-io/conan/issues/11520


Patch:
diff --git a/conan/tools/cmake/presets.py b/conan/tools/cmake/presets.py
index 918f6a831..b50aa1724 100644
--- a/conan/tools/cmake/presets.py
+++ b/conan/tools/cmake/presets.py
@@ -53,8 +53,13 @@ def _add_configure_preset(conanfile, generator, cache_variables, toolchain_file,
             "description": "'{}' configure using '{}' generator".format(name, generator),
             "generator": generator,
             "cacheVariables": cache_variables,
-            "toolchainFile": toolchain_file,
+
            }
+    if not _forced_schema_2(conanfile):
+        ret["toolchainFile"] = toolchain_file
+    else:
+        ret["cacheVariables"]["CMAKE_TOOLCHAIN_FILE"] = toolchain_file
+
     if conanfile.build_folder:
         # If we are installing a ref: "conan install <ref>", we don't have build_folder, because
         # we don't even have a conanfile with a `layout()` to determine the build folder.
@@ -63,8 +68,34 @@ def _add_configure_preset(conanfile, generator, cache_variables, toolchain_file,
     return ret
 
 
+def _forced_schema_2(conanfile):
+    version = conanfile.conf.get("tools.cmake.cmaketoolchain.presets:max_schema_version",
+                                 check_type=int)
+    if not version:
+        return False
+
+    if version < 2:
+        raise ConanException("The minimum value for 'tools.cmake.cmaketoolchain.presets:"
+                             "schema_version' is 2")
+    if version < 4:
+        return True
+
+    return False
+
+
+def _schema_version(conanfile, default):
+    if _forced_schema_2(conanfile):
+        return 2
+
+    return default
+
+
 def _contents(conanfile, toolchain_file, cache_variables, generator):
-    ret = {"version": 3,
+    """
+    Contents for the CMakePresets.json
+    It uses schema version 3 unless it is forced to 2
+    """
+    ret = {"version": _schema_version(conanfile, default=3),
            "cmakeMinimumRequired": {"major": 3, "minor": 15, "patch": 0},
            "configurePresets": [],
            "buildPresets": [],
@@ -117,25 +148,51 @@ def write_cmake_presets(conanfile, toolchain_file, generator, cache_variables):
 
     data = json.dumps(data, indent=4)
     save(preset_path, data)
+    save_cmake_user_presets(conanfile, preset_path)
 
+
+def save_cmake_user_presets(conanfile, preset_path):
     # Try to save the CMakeUserPresets.json if layout declared and CMakeLists.txt found
     if conanfile.source_folder and conanfile.source_folder != conanfile.generators_folder:
         if os.path.exists(os.path.join(conanfile.source_folder, "CMakeLists.txt")):
+            """
+                Contents for the CMakeUserPresets.json
+                It uses schema version 4 unless it is forced to 2
+            """
             user_presets_path = os.path.join(conanfile.source_folder, "CMakeUserPresets.json")
             if not os.path.exists(user_presets_path):
-                data = {"version": 4, "include": [preset_path], "vendor": {"conan": dict()}}
+                data = {"version": _schema_version(conanfile, default=4),
+                        "vendor": {"conan": dict()}}
             else:
                 data = json.loads(load(user_presets_path))
-                if "conan" in data.get("vendor", {}):
-                    # Clear the folders that have been deleted
-                    data["include"] = [i for i in data.get("include", []) if os.path.exists(i)]
-                    if preset_path not in data["include"]:
-                        data["include"].append(preset_path)
-
+                if "conan" not in data.get("vendor", {}):
+                    # The file is not ours, we cannot overwrite it
+                    return
+            data = _append_user_preset_path(conanfile, data, preset_path)
             data = json.dumps(data, indent=4)
             save(user_presets_path, data)
 
 
+def _append_user_preset_path(conanfile, data, preset_path):
+    if not _forced_schema_2(conanfile):
+        if "include" not in data:
+            data["include"] = []
+        # Clear the folders that have been deleted
+        data["include"] = [i for i in data.get("include", []) if os.path.exists(i)]
+        if preset_path not in data["include"]:
+            data["include"].append(preset_path)
+        return data
+    else:
+        # Merge the presets
+        cmake_preset = json.loads(load(preset_path))
+        for preset_type in ("configurePresets", "buildPresets", "testPresets"):
+            for preset in cmake_preset.get(preset_type, []):
+                if preset_type not in data:
+                    data[preset_type] = []
+                data[preset_type].append(preset)
+        return data
+
+
 def load_cmake_presets(folder):
     tmp = load(os.path.join(folder, "CMakePresets.json"))
     return json.loads(tmp)
diff --git a/conan/tools/microsoft/msbuilddeps.py b/conan/tools/microsoft/msbuilddeps.py
index 1ba1fd7e5..8b1165823 100644
--- a/conan/tools/microsoft/msbuilddeps.py
+++ b/conan/tools/microsoft/msbuilddeps.py
@@ -6,6 +6,7 @@ from xml.dom import minidom
 from jinja2 import Template
 
 from conan.tools._check_build_profile import check_using_build_profile
+from conans.client.graph.graph import CONTEXT_BUILD
 from conans.errors import ConanException
 from conans.util.files import load, save
 
@@ -13,6 +14,10 @@ VALID_LIB_EXTENSIONS = (".so", ".lib", ".a", ".dylib", ".bc")
 
 
 class MSBuildDeps(object):
+    """
+    conandeps.props: unconditional import of all *direct* dependencies only
+
+    """
 
     _vars_props = textwrap.dedent("""\
         <?xml version="1.0" encoding="utf-8"?>
@@ -118,65 +123,21 @@ class MSBuildDeps(object):
         condition = " And ".join("'$(%s)' == '%s'" % (k, v) for k, v in props)
         return condition
 
-    def _conf_props_file(self, dep_name, vars_props_name, deps, build):
-        """
-        content for conan_poco_x86_release.props, containing the activation
-        """
-        # TODO: This must include somehow the user/channel, most likely pattern to exclude/include
-        # Probably also the negation pattern, exclude all not @mycompany/*
-        ca_exclude = any(fnmatch.fnmatch(dep_name, p) for p in self.exclude_code_analysis or ())
-
-        deps = [] if build else deps  # build-requires do not propagate dependencies
-        template = Template(self._conf_props, trim_blocks=True, lstrip_blocks=True)
-        content_multi = template.render(host_context=not build,
-                                        name=dep_name, ca_exclude=ca_exclude,
-                                        vars_filename=vars_props_name, deps=deps)
-        return content_multi
-
     @staticmethod
-    def _dep_props_file(name, name_general, dep_props_filename, condition):
-        # Current directory is the generators_folder
-        multi_path = name_general
-        if os.path.isfile(multi_path):
-            content_multi = load(multi_path)
-        else:
-            content_multi = textwrap.dedent("""\
-            <?xml version="1.0" encoding="utf-8"?>
-            <Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
-              <ImportGroup Label="PropertySheets">
-              </ImportGroup>
-              <PropertyGroup>
-                <conan_{{name}}_props_imported>True</conan_{{name}}_props_imported>
-              </PropertyGroup>
-            </Project>
-            """)
-            content_multi = Template(content_multi).render({"name": name})
-
-        # parse the multi_file and add new import statement if needed
-        dom = minidom.parseString(content_multi)
-        import_vars = dom.getElementsByTagName('ImportGroup')[0]
-
-        # Current vars
-        children = import_vars.getElementsByTagName("Import")
-        for node in children:
-            if (dep_props_filename == node.getAttribute("Project") and
-                    condition == node.getAttribute("Condition")):
-                break  # the import statement already exists
-        else:  # create a new import statement
-            import_node = dom.createElement('Import')
-            import_node.setAttribute('Condition', condition)
-            import_node.setAttribute('Project', dep_props_filename)
-            import_vars.appendChild(import_node)
-
-        content_multi = dom.toprettyxml()
-        content_multi = "\n".join(line for line in content_multi.splitlines() if line.strip())
-        return content_multi
+    def _dep_name(dep, build):
+        dep_name = dep.ref.name
+        dep_name = dep_name.replace(".", "_")
+        if build:  # dep.context == CONTEXT_BUILD:
+            dep_name += "_build"
+        return dep_name
 
     def _vars_props_file(self, dep, name, cpp_info, deps, build):
         """
-        content for conan_vars_poco_x86_release.props, containing the variables
+        content for conan_vars_poco_x86_release.props, containing the variables for 1 config
+        This will be for 1 package or for one component of a package
+        :return: varfile content
         """
-        # returns a .props file with the variables definition for one package for one configuration
+
         def add_valid_ext(libname):
             ext = os.path.splitext(libname)[1]
             return '%s;' % libname if ext in VALID_LIB_EXTENSIONS else '%s.lib;' % libname
@@ -190,18 +151,15 @@ class MSBuildDeps(object):
             return path.replace("\\", "/").lstrip("/")
 
         def join_paths(paths):
-            # ALmost copied from CMakeDeps TargetDataContext
+            # TODO: ALmost copied from CMakeDeps TargetDataContext
             ret = []
             for p in paths:
                 assert os.path.isabs(p), "{} is not absolute".format(p)
-
-                if p.startswith(package_folder):
-                    rel = p[len(package_folder):]
-                    rel = escape_path(rel)
-                    norm_path = ("${%s}/%s" % (pkg_placeholder, rel))
-                else:
-                    norm_path = escape_path(p)
-                ret.append(norm_path)
+                full_path = escape_path(p)
+                if full_path.startswith(package_folder):
+                    rel = full_path[len(package_folder)+1:]
+                    full_path = ("%s/%s" % (pkg_placeholder, rel))
+                ret.append(full_path)
             return "".join("{};".format(e) for e in ret)
 
         package_folder = escape_path(dep.package_folder)
@@ -219,56 +177,96 @@ class MSBuildDeps(object):
             'definitions': "".join("%s;" % d for d in cpp_info.defines),
             'compiler_flags': " ".join(cpp_info.cxxflags + cpp_info.cflags),
             'linker_flags': " ".join(cpp_info.sharedlinkflags + cpp_info.exelinkflags),
-            'dependencies': ";".join(deps) if not build else "",
+            'dependencies': ";".join(deps),
             'host_context': not build
         }
         formatted_template = Template(self._vars_props, trim_blocks=True,
                                       lstrip_blocks=True).render(**fields)
         return formatted_template
 
-    def _all_props_file(self, name_general):
-        """ this is a .props file including direct declared dependencies
+    def _activate_props_file(self, dep_name, vars_filename, deps, build):
+        """
+        Actual activation of the VS variables, per configuration
+            - conan_pkgname_x86_release.props / conan_pkgname_compname_x86_release.props
+        :param dep_name: pkgname / pkgname_compname
+        :param deps: the name of other things to be included: [dep1, dep2:compA, ...]
+        :param build: if it is a build require or not
+        """
+
+        # TODO: This must include somehow the user/channel, most likely pattern to exclude/include
+        # Probably also the negation pattern, exclude all not @mycompany/*
+        ca_exclude = any(fnmatch.fnmatch(dep_name, p) for p in self.exclude_code_analysis or ())
+        template = Template(self._conf_props, trim_blocks=True, lstrip_blocks=True)
+        content_multi = template.render(host_context=not build, name=dep_name, ca_exclude=ca_exclude,
+                                        vars_filename=vars_filename, deps=deps)
+        return content_multi
+
+    @staticmethod
+    def _dep_props_file(dep_name, filename, aggregated_filename, condition, content=None):
+        """
+        The file aggregating all configurations for a given pkg / component
+            - conan_pkgname.props
         """
         # Current directory is the generators_folder
-        multi_path = name_general
-        if os.path.isfile(multi_path):
-            content_multi = load(multi_path)
+        if content:
+            content_multi = content  # Useful for aggregating multiple components in one pass
+        elif os.path.isfile(filename):
+            content_multi = load(filename)
         else:
             content_multi = textwrap.dedent("""\
             <?xml version="1.0" encoding="utf-8"?>
             <Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
               <ImportGroup Label="PropertySheets">
               </ImportGroup>
+              <PropertyGroup>
+                <conan_{{name}}_props_imported>True</conan_{{name}}_props_imported>
+              </PropertyGroup>
             </Project>
             """)
+            content_multi = Template(content_multi).render({"name": dep_name})
 
-        # parse the multi_file and add a new import statement if needed
+        # parse the multi_file and add new import statement if needed
         dom = minidom.parseString(content_multi)
-        import_group = dom.getElementsByTagName('ImportGroup')[0]
-        children = import_group.getElementsByTagName("Import")
-        direct_deps = self._conanfile.dependencies.filter({"direct": True})
-        for req, dep in direct_deps.items():
-            dep_name = dep.ref.name.replace(".", "_")
-            if req.build:
-                dep_name += "_build"
-            conf_props_name = "conan_%s.props" % dep_name
-            for node in children:
-                if conf_props_name == node.getAttribute("Project"):
-                    # the import statement already exists
-                    break
-            else:
-                # create a new import statement
-                import_node = dom.createElement('Import')
-                dep_imported = "'$(conan_%s_props_imported)' != 'True'" % dep_name
-                import_node.setAttribute('Project', conf_props_name)
-                import_node.setAttribute('Condition', dep_imported)
-                # add it to the import group
-                import_group.appendChild(import_node)
+        import_vars = dom.getElementsByTagName('ImportGroup')[0]
+
+        # Current vars
+        children = import_vars.getElementsByTagName("Import")
+        for node in children:
+            if aggregated_filename == node.getAttribute("Project") \
+                    and condition == node.getAttribute("Condition"):
+                break
+        else:  # create a new import statement
+            import_node = dom.createElement('Import')
+            import_node.setAttribute('Condition', condition)
+            import_node.setAttribute('Project', aggregated_filename)
+            import_vars.appendChild(import_node)
+
         content_multi = dom.toprettyxml()
-        # To remove all extra blank lines
         content_multi = "\n".join(line for line in content_multi.splitlines() if line.strip())
         return content_multi
 
+    def _conandeps(self):
+        """ this is a .props file including direct declared dependencies
+        """
+        # Current directory is the generators_folder
+        conandeps_filename = "conandeps.props"
+        direct_deps = self._conanfile.dependencies.filter({"direct": True})
+        pkg_aggregated_content = textwrap.dedent("""\
+            <?xml version="1.0" encoding="utf-8"?>
+            <Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+              <ImportGroup Label="PropertySheets">
+              </ImportGroup>
+            </Project>
+            """)
+        for req, dep in direct_deps.items():
+            dep_name = self._dep_name(dep, req.build)
+            filename = "conan_%s.props" % dep_name
+            comp_condition = "'$(conan_%s_props_imported)' != 'True'" % dep_name
+            pkg_aggregated_content = self._dep_props_file("", conandeps_filename, filename,
+                                                          condition=comp_condition,
+                                                          content=pkg_aggregated_content)
+        return {conandeps_filename: pkg_aggregated_content}
+
     def _package_props_files(self, dep, build=False):
         """ all the files for a given package:
         - conan_pkgname_vars_config.props: definition of variables, one per config
@@ -278,26 +276,50 @@ class MSBuildDeps(object):
         """
         conf_name = self._config_filename()
         condition = self._condition()
-        dep_name = dep.ref.name
-        dep_name = dep_name.replace(".", "_")
-        if build:
-            dep_name += "_build"
-        cpp_info = dep.cpp_info.aggregated_components()
-        public_deps = [d.ref.name.replace(".", "_")
-                       for r, d in dep.dependencies.direct_host.items() if r.visible]
+        dep_name = self._dep_name(dep, build)
         result = {}
-        # One file per configuration, with just the variables
-        vars_props_name = "conan_%s_vars%s.props" % (dep_name, conf_name)
-        result[vars_props_name] = self._vars_props_file(dep, dep_name, cpp_info, public_deps,
-                                                        build=build)
-        props_name = "conan_%s%s.props" % (dep_name, conf_name)
-        result[props_name] = self._conf_props_file(dep_name, vars_props_name, public_deps,
-                                                   build=build)
-
-        # The entry point for each package, it will have conditionals to the others
-        file_dep_name = "conan_%s.props" % dep_name
-        dep_content = self._dep_props_file(dep_name, file_dep_name, props_name, condition)
-        result[file_dep_name] = dep_content
+        if dep.cpp_info.has_components:
+            pkg_aggregated_content = None
+            for comp_name, comp_info in dep.cpp_info.components.items():
+                if comp_name is None:
+                    continue
+                full_comp_name = "{}_{}".format(dep_name, comp_name)
+                vars_filename = "conan_%s_vars%s.props" % (full_comp_name, conf_name)
+                activate_filename = "conan_%s%s.props" % (full_comp_name, conf_name)
+                comp_filename = "conan_%s.props" % full_comp_name
+                pkg_filename = "conan_%s.props" % dep_name
+
+                public_deps = []
+                for r in comp_info.requires:
+                    if "::" in r:  # Points to a component of a different package
+                        pkg, cmp_name = r.split("::")
+                        public_deps.append(pkg if pkg == cmp_name else "{}_{}".format(pkg, cmp_name))
+                    else:  # Points to a component of same package
+                        public_deps.append("{}_{}".format(dep_name, r))
+                result[vars_filename] = self._vars_props_file(dep, full_comp_name, comp_info,
+                                                              public_deps, build=build)
+                result[activate_filename] = self._activate_props_file(full_comp_name, vars_filename,
+                                                                      public_deps, build=build)
+                result[comp_filename] = self._dep_props_file(full_comp_name, comp_filename,
+                                                             activate_filename, condition)
+                comp_condition = "'$(conan_%s_props_imported)' != 'True'" % full_comp_name
+                pkg_aggregated_content = self._dep_props_file(dep_name, pkg_filename, comp_filename,
+                                                              condition=comp_condition,
+                                                              content=pkg_aggregated_content)
+                result[pkg_filename] = pkg_aggregated_content
+        else:
+            cpp_info = dep.cpp_info
+            vars_filename = "conan_%s_vars%s.props" % (dep_name, conf_name)
+            activate_filename = "conan_%s%s.props" % (dep_name, conf_name)
+            pkg_filename = "conan_%s.props" % dep_name
+            public_deps = [self._dep_name(d, build)
+                           for r, d in dep.dependencies.direct_host.items() if r.visible]
+            result[vars_filename] = self._vars_props_file(dep, dep_name, cpp_info,
+                                                          public_deps, build=build)
+            result[activate_filename] = self._activate_props_file(dep_name, vars_filename,
+                                                                  public_deps, build=build)
+            result[pkg_filename] = self._dep_props_file(dep_name, pkg_filename, activate_filename,
+                                                        condition=condition)
         return result
 
     def _content(self):
@@ -307,18 +329,14 @@ class MSBuildDeps(object):
 
         host_req = list(self._conanfile.dependencies.host.values())
         test_req = list(self._conanfile.dependencies.test.values())
-
         for dep in host_req + test_req:
-            files = self._package_props_files(dep, build=False)
-            result.update(files)
+            result.update(self._package_props_files(dep, build=False))
 
         build_req = list(self._conanfile.dependencies.build.values())
         for dep in build_req:
-            files = self._package_props_files(dep, build=True)
-            result.update(files)
+            result.update(self._package_props_files(dep, build=True))
 
         # Include all direct build_requires for host context. This might change
-        conandeps_filename = "conandeps.props"
-        result[conandeps_filename] = self._all_props_file(conandeps_filename)
+        result.update(self._conandeps())
 
         return result
diff --git a/conan/tools/system/package_manager.py b/conan/tools/system/package_manager.py
index bcef638e4..b868965b2 100644
--- a/conan/tools/system/package_manager.py
+++ b/conan/tools/system/package_manager.py
@@ -11,6 +11,9 @@ class _SystemPackageManagerTool(object):
     install_command = ""
     update_command = ""
     check_command = ""
+    accepted_install_codes = [0]
+    accepted_update_codes = [0]
+    accepted_check_codes = [0, 1]
 
     def __init__(self, conanfile):
         self._conanfile = conanfile
@@ -61,6 +64,12 @@ class _SystemPackageManagerTool(object):
         if self._active_tool == self.__class__.tool_name:
             return method(*args, **kwargs)
 
+    def _conanfile_run(self, command, accepted_returns):
+        ret = self._conanfile.run(command, ignore_errors=True)
+        if ret not in accepted_returns:
+            raise ConanException("Command '%s' failed" % command)
+        return ret
+
     def install_substitutes(self, *args, **kwargs):
         return self.run(self._install_substitutes, *args, **kwargs)
 
@@ -109,7 +118,7 @@ class _SystemPackageManagerTool(object):
                                                       tool=self.tool_name,
                                                       packages=" ".join(packages_arch),
                                                       **kwargs)
-                return self._conanfile.run(command)
+                return self._conanfile_run(command, self.accepted_install_codes)
         else:
             self._conanfile.output.info("System requirements: {} already "
                                         "installed".format(" ".join(packages)))
@@ -124,7 +133,7 @@ class _SystemPackageManagerTool(object):
                                  "'-c tools.system.package_manager:mode={1}'".format(self.mode_check,
                                                                                      self.mode_install))
         command = self.update_command.format(sudo=self.sudo_str, tool=self.tool_name)
-        return self._conanfile.run(command)
+        return self._conanfile_run(command, self.accepted_update_codes)
 
     def _check(self, packages):
         missing = [pkg for pkg in packages if self.check_package(self.get_package_name(pkg)) != 0]
@@ -133,7 +142,7 @@ class _SystemPackageManagerTool(object):
     def check_package(self, package):
         command = self.check_command.format(tool=self.tool_name,
                                             package=package)
-        return self._conanfile.run(command, ignore_errors=True)
+        return self._conanfile_run(command, self.accepted_check_codes)
 
 
 class Apt(_SystemPackageManagerTool):
@@ -167,6 +176,7 @@ class Yum(_SystemPackageManagerTool):
     install_command = "{sudo}{tool} install -y {packages}"
     update_command = "{sudo}{tool} check-update -y"
     check_command = "rpm -q {package}"
+    accepted_update_codes = [0, 100]
 
     def __init__(self, conanfile, arch_names=None):
         super(Yum, self).__init__(conanfile)
diff --git a/conans/model/conf.py b/conans/model/conf.py
index 1c37c95e3..968bcd87b 100644
--- a/conans/model/conf.py
+++ b/conans/model/conf.py
@@ -21,6 +21,7 @@ BUILT_IN_CONFS = {
     "tools.cmake.cmaketoolchain:system_name": "Define CMAKE_SYSTEM_NAME in CMakeToolchain",
     "tools.cmake.cmaketoolchain:system_version": "Define CMAKE_SYSTEM_VERSION in CMakeToolchain",
     "tools.cmake.cmaketoolchain:system_processor": "Define CMAKE_SYSTEM_PROCESSOR in CMakeToolchain",
+    "tools.cmake.cmaketoolchain.presets:max_schema_version": "Generate CMakeUserPreset.json compatible with the supplied schema version",
     "tools.env.virtualenv:auto_use": "Automatically activate virtualenv file generation",
     "tools.cmake.cmake_layout:build_folder_vars": "Settings and Options that will produce a different build folder and different CMake presets names",
     "tools.files.download:retry": "Number of retries in case of failure when downloading",


Test patch:
diff --git a/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py b/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
index 095ae5c61..c4b25b272 100644
--- a/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
+++ b/conans/test/functional/toolchains/cmake/test_cmake_toolchain.py
@@ -76,7 +76,8 @@ def test_cmake_toolchain_custom_toolchain():
                     reason="Single config test, Linux CI still without 3.23")
 @pytest.mark.tool_cmake(version="3.23")
 @pytest.mark.parametrize("existing_user_presets", [None, "user_provided", "conan_generated"])
-def test_cmake_user_presets_load(existing_user_presets):
+@pytest.mark.parametrize("schema2", [True, False])
+def test_cmake_user_presets_load(existing_user_presets, schema2):
     """
     Test if the CMakeUserPresets.cmake is generated and use CMake to use it to verify the right
     syntax of generated CMakeUserPresets.cmake and CMakePresets.cmake. If the user already provided
@@ -813,6 +814,48 @@ def test_cmake_presets_multiple_settings_multi_config():
     assert "MSVC_LANG2017" in client.out
 
 
+@pytest.mark.tool_cmake(version="3.23")
+def test_user_presets_version2():
+    client = TestClient(path_with_spaces=False)
+    client.run("new hello/0.1 --template=cmake_exe")
+    configs = ["-c tools.cmake.cmaketoolchain.presets:max_schema_version=2 ",
+               "-c tools.cmake.cmake_layout:build_folder_vars='[\"settings.compiler.cppstd\"]'"]
+    client.run("install . {} -s compiler.cppstd=14".format(" ".join(configs)))
+    client.run("install . {} -s compiler.cppstd=17".format(" ".join(configs)))
+    client.run("install . {} -s compiler.cppstd=20".format(" ".join(configs)))
+
+    if platform.system() == "Windows":
+        client.run_command("cmake . --preset 14")
+        client.run_command("cmake --build --preset 14-release")
+        client.run_command(r"build\14\Release\hello.exe")
+    else:
+        client.run_command("cmake . --preset 14-release")
+        client.run_command("cmake --build --preset 14-release")
+        client.run_command("./build/14/Release/hello")
+
+    assert "Hello World Release!" in client.out
+
+    if platform.system() != "Windows":
+        assert "__cplusplus2014" in client.out
+    else:
+        assert "MSVC_LANG2014" in client.out
+
+    if platform.system() == "Windows":
+        client.run_command("cmake . --preset 17")
+        client.run_command("cmake --build --preset 17-release")
+        client.run_command(r"build\17\Release\hello.exe")
+    else:
+        client.run_command("cmake . --preset 17-release")
+        client.run_command("cmake --build --preset 17-release")
+        client.run_command("./build/17/Release/hello")
+
+    assert "Hello World Release!" in client.out
+    if platform.system() != "Windows":
+        assert "__cplusplus2017" in client.out
+    else:
+        assert "MSVC_LANG2017" in client.out
+
+
 @pytest.mark.tool_cmake
 def test_cmaketoolchain_sysroot():
     client = TestClient(path_with_spaces=False)
diff --git a/conans/test/functional/toolchains/microsoft/test_msbuilddeps.py b/conans/test/functional/toolchains/microsoft/test_msbuilddeps.py
index 4c78b57d0..e950df38e 100644
--- a/conans/test/functional/toolchains/microsoft/test_msbuilddeps.py
+++ b/conans/test/functional/toolchains/microsoft/test_msbuilddeps.py
@@ -1004,6 +1004,7 @@ def test_build_requires_transitives():
     c.run("create tool")
     c.run("install consumer -g MSBuildDeps")
     tool = c.load("conan_tool_build_release_x64.props")
-    assert "conan_dep" not in tool
+    assert "conan_dep_build.props" in tool
+    assert "conan_dep.props" not in tool
     tool_vars = c.load("conan_tool_build_vars_release_x64.props")
-    assert "<Conantool_buildDependencies></Conantool_buildDependencies>" in tool_vars
+    assert "<Conantool_buildDependencies>dep_build</Conantool_buildDependencies>" in tool_vars
diff --git a/conans/test/functional/toolchains/microsoft/test_msbuilddeps_components.py b/conans/test/functional/toolchains/microsoft/test_msbuilddeps_components.py
new file mode 100644
index 000000000..ae62297b9
--- /dev/null
+++ b/conans/test/functional/toolchains/microsoft/test_msbuilddeps_components.py
@@ -0,0 +1,172 @@
+import platform
+import textwrap
+
+import pytest
+
+from conans.test.assets.sources import gen_function_cpp
+from conans.test.utils.tools import TestClient
+
+
+@pytest.mark.skipif(platform.system() != "Windows", reason="Requires Windows")
+def test_msbuild_deps_components():
+    # TODO: Duplicated from xcodedeps_components
+    client = TestClient()
+
+    client.run("new tcp/1.0 -m=cmake_lib")
+    client.run("create . -tf=None")
+
+    header = textwrap.dedent("""
+        #pragma once
+        void {name}();
+        """)
+
+    source = textwrap.dedent("""
+        #include <iostream>
+        #include "{name}.h"
+        {include}
+
+        void {name}(){{
+            {call}
+            #ifdef NDEBUG
+            std::cout << "{name}/1.0: Hello World Release!" << std::endl;
+            #else
+            std::cout << "{name}/1.0: Hello World Debug!" << std::endl;
+            #endif
+        }}
+        """)
+
+    cmakelists = textwrap.dedent("""
+        cmake_minimum_required(VERSION 3.15)
+        project(myproject CXX)
+
+        find_package(tcp REQUIRED CONFIG)
+
+        add_library(core src/core.cpp include/core.h)
+        add_library(client src/client.cpp include/client.h)
+        add_library(server src/server.cpp include/server.h)
+
+        target_include_directories(core PUBLIC include)
+        target_include_directories(client PUBLIC include)
+        target_include_directories(server PUBLIC include)
+
+        set_target_properties(core PROPERTIES PUBLIC_HEADER "include/core.h")
+        set_target_properties(client PROPERTIES PUBLIC_HEADER "include/client.h")
+        set_target_properties(server PROPERTIES PUBLIC_HEADER "include/server.h")
+
+        target_link_libraries(client core tcp::tcp)
+        target_link_libraries(server core tcp::tcp)
+
+        install(TARGETS core client server)
+        """)
+
+    conanfile_py = textwrap.dedent("""
+        import os
+
+        from conan import ConanFile
+        from conan.tools.cmake import CMake, cmake_layout
+
+
+        class LibConan(ConanFile):
+            settings = "os", "compiler", "build_type", "arch"
+            exports_sources = "CMakeLists.txt", "src/*", "include/*"
+            {requires}
+            generators = "CMakeToolchain", "CMakeDeps"
+
+            def layout(self):
+                cmake_layout(self)
+
+            def build(self):
+                cmake = CMake(self)
+                cmake.configure()
+                cmake.build()
+
+            def package(self):
+                cmake = CMake(self)
+                cmake.install()
+            {package_info}
+        """)
+
+    network_pi = """
+    def package_info(self):
+        self.cpp_info.components["core"].libs = ["core"]
+        self.cpp_info.components["core"].includedirs.append("include")
+        self.cpp_info.components["core"].libdirs.append("lib")
+
+        self.cpp_info.components["client"].libs = ["client"]
+        self.cpp_info.components["client"].includedirs.append("include")
+        self.cpp_info.components["client"].libdirs.append("lib")
+        self.cpp_info.components["client"].requires.extend(["core", "tcp::tcp"])
+
+        self.cpp_info.components["server"].libs = ["server"]
+        self.cpp_info.components["server"].includedirs.append("include")
+        self.cpp_info.components["server"].libdirs.append("lib")
+        self.cpp_info.components["server"].requires.extend(["core", "tcp::tcp"])
+    """
+
+    client.save({
+        "include/core.h": header.format(name="core"),
+        "include/server.h": header.format(name="server"),
+        "include/client.h": header.format(name="client"),
+        "src/core.cpp": source.format(name="core", include="", call=""),
+        "src/server.cpp": source.format(name="server",
+                                        include='#include "core.h"\n#include "tcp.h"',
+                                        call="core(); tcp();"),
+        "src/client.cpp": source.format(name="client", include='#include "core.h"\n#include "tcp.h"',
+                                        call="core(); tcp();"),
+        "conanfile.py": conanfile_py.format(requires='requires= "tcp/1.0"', package_info=network_pi),
+        "CMakeLists.txt": cmakelists,
+    }, clean_first=True)
+
+    client.run("create . network/1.0@")
+
+    chat_pi = """
+    def package_info(self):
+        self.cpp_info.libs = ["chat"]
+        self.cpp_info.includedirs.append("include")
+        self.cpp_info.libdirs.append("lib")
+        self.cpp_info.requires.append("network::client")
+    """
+
+    cmakelists_chat = textwrap.dedent("""
+        cmake_minimum_required(VERSION 3.15)
+        project(chat CXX)
+        find_package(network REQUIRED CONFIG)
+        add_library(chat src/chat.cpp include/chat.h)
+        target_include_directories(chat PUBLIC include)
+        set_target_properties(chat PROPERTIES PUBLIC_HEADER "include/chat.h")
+        target_link_libraries(chat network::client)
+        install(TARGETS chat)
+        """)
+
+    client.save({
+        "include/chat.h": header.format(name="chat"),
+        "src/chat.cpp": source.format(name="chat", include='#include "client.h"', call="client();"),
+        "conanfile.py": conanfile_py.format(requires='requires= "network/1.0"',
+                                            package_info=chat_pi),
+        "CMakeLists.txt": cmakelists_chat,
+    }, clean_first=True)
+
+    client.run("create . chat/1.0@")
+
+    client.run("new greet/0.1 --template=msbuild_exe")
+    conanfile = client.load("conanfile.py").replace("settings = ",
+                                                    'requires="chat/1.0"\n'
+                                                    '    generators = "MSBuildDeps"\n'
+                                                    '    settings = ')
+    vcproj = client.load("greet.vcxproj")
+    vcproj2 = vcproj.replace(r'<Import Project="conan\\conantoolchain.props" />',
+                             r"""<Import Project="conan\\conantoolchain.props" />
+                               <Import Project="conan\\conandeps.props" />
+                             """)
+    assert vcproj2 != vcproj
+    client.save({"conanfile.py": conanfile,
+                 "src/greet.cpp": gen_function_cpp(name="main", includes=["chat"],
+                                                   calls=["chat"]),
+                 "greet.vcxproj": vcproj2})
+    client.run("create .")
+    assert "main: Release!" in client.out
+    assert "core/1.0: Hello World Release!" in client.out
+    assert "tcp/1.0: Hello World Release!" in client.out
+    assert "client/1.0: Hello World Release!" in client.out
+    assert "chat/1.0: Hello World Release!" in client.out
+    assert "server/1.0" not in client.out
diff --git a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
index 657e6eff3..cbfbb48a7 100644
--- a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
+++ b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
@@ -492,6 +492,66 @@ def test_android_c_library():
     client.run("create . foo/1.0@ -s os=Android -s os.api_level=23 -c tools.android:ndk_path=/foo")
 
 
+def test_user_presets_version2():
+    client = TestClient()
+    conanfile = textwrap.dedent("""
+                from conan import ConanFile
+                from conan.tools.cmake import cmake_layout
+
+                class Conan(ConanFile):
+                    name = "foo"
+                    version = "1.0"
+                    settings = "os", "arch", "compiler", "build_type"
+                    generators = "CMakeToolchain"
+
+                    def layout(self):
+                        cmake_layout(self)
+
+            """)
+    client.save({"conanfile.py": conanfile, "CMakeLists.txt": "foo"})
+    configs = ["-c tools.cmake.cmaketoolchain.presets:max_schema_version=2 ",
+               "-c tools.cmake.cmake_layout:build_folder_vars='[\"settings.compiler.cppstd\"]'"]
+    client.run("install . {} -s compiler.cppstd=14".format(" ".join(configs)))
+    client.run("install . {} -s compiler.cppstd=17".format(" ".join(configs)))
+
+    presets = json.loads(client.load("CMakeUserPresets.json"))
+    assert len(presets["configurePresets"]) == 2
+    assert presets["version"] == 2
+    assert "build/14/generators/conan_toolchain.cmake" \
+           in presets["configurePresets"][0]["cacheVariables"]["CMAKE_TOOLCHAIN_FILE"].replace("\\",
+                                                                                               "/")
+    assert "build/17/generators/conan_toolchain.cmake" \
+           in presets["configurePresets"][1]["cacheVariables"]["CMAKE_TOOLCHAIN_FILE"].replace("\\",
+                                                                                               "/")
+
+
+def test_user_presets_version2_no_overwrite_user():
+
+    client = TestClient()
+    conanfile = textwrap.dedent("""
+            from conan import ConanFile
+            from conan.tools.cmake import cmake_layout
+
+            class Conan(ConanFile):
+                name = "foo"
+                version = "1.0"
+                settings = "os", "arch", "compiler", "build_type"
+                generators = "CMakeToolchain"
+
+                def layout(self):
+                    cmake_layout(self)
+
+            """)
+    client.save({"conanfile.py": conanfile, "CMakeLists.txt": "foo",
+                 "CMakeUserPresets.json": '{"from_user": 1}'})
+    configs = ["-c tools.cmake.cmaketoolchain.presets:max_schema_version=2 ",
+               "-c tools.cmake.cmake_layout:build_folder_vars='[\"settings.compiler.cppstd\"]'"]
+    client.run("install . {} -s compiler.cppstd=14".format(" ".join(configs)))
+
+    presets = json.loads(client.load("CMakeUserPresets.json"))
+    assert presets == {"from_user": 1}
+
+
 @pytest.mark.skipif(platform.system() != "Windows", reason="Only Windows")
 def test_presets_paths_correct():
     client = TestClient()
@@ -506,12 +566,18 @@ def test_presets_paths_correct():
                 def layout(self):
                     cmake_layout(self)
             """)
-    client.save({"conanfile.py": conanfile})
-    client.run("install . ")
-    contents = json.loads(client.load("build/generators/CMakePresets.json"))
-    toolchain_file = contents["configurePresets"][0]["toolchainFile"]
-    assert "/" not in toolchain_file
-
-    binary_dir = contents["configurePresets"][0]["binaryDir"]
-    assert "/" not in binary_dir
+    client.save({"conanfile.py": conanfile, "CMakeLists.txt": "foo"})
+    configs = ["-c tools.cmake.cmaketoolchain.presets:max_schema_version=2 ",
+               "-c tools.cmake.cmake_layout:build_folder_vars='[\"settings.compiler.cppstd\"]'"]
+    client.run("install . {} -s compiler.cppstd=14".format(" ".join(configs)))
+    client.run("install . {} -s compiler.cppstd=17".format(" ".join(configs)))
 
+    presets = json.loads(client.load("CMakeUserPresets.json"))
+    assert len(presets["configurePresets"]) == 2
+    assert presets["version"] == 2
+    assert "build/14/generators/conan_toolchain.cmake" \
+           in presets["configurePresets"][0]["cacheVariables"]["CMAKE_TOOLCHAIN_FILE"].replace("\\",
+                                                                                               "/")
+    assert "build/17/generators/conan_toolchain.cmake" \
+           in presets["configurePresets"][1]["cacheVariables"]["CMAKE_TOOLCHAIN_FILE"].replace("\\",
+                                                                                               "/")
diff --git a/conans/test/integration/tools/system/package_manager_test.py b/conans/test/integration/tools/system/package_manager_test.py
index b5becec39..cbab2a846 100644
--- a/conans/test/integration/tools/system/package_manager_test.py
+++ b/conans/test/integration/tools/system/package_manager_test.py
@@ -1,4 +1,5 @@
 import platform
+from unittest.mock import MagicMock, patch
 
 import mock
 import pytest
@@ -111,7 +112,11 @@ def test_tools_install_mode_check(tool_class):
         context_mock.return_value = "host"
         tool = tool_class(conanfile)
         with pytest.raises(ConanException) as exc_info:
-            tool.install(["package1", "package2"])
+            def fake_check(*args, **kwargs):
+                return ["package1", "package2"]
+            from conan.tools.system.package_manager import _SystemPackageManagerTool
+            with patch.object(_SystemPackageManagerTool, 'check', MagicMock(side_effect=fake_check)):
+                tool.install(["package1", "package2"])
         assert exc_info.value.args[0] == "System requirements: 'package1, package2' are missing but " \
                                          "can't install because tools.system.package_manager:mode is " \
                                          "'check'.Please update packages manually or set " \
@@ -140,7 +145,6 @@ def test_tools_update_mode_check(tool_class):
                                          "'-c tools.system.package_manager:mode=install'"
 
 
-
 @pytest.mark.parametrize("tool_class, result", [
     (Apt, "apt-get update"),
     (Yum, "yum check-update -y"),
@@ -165,6 +169,42 @@ def test_tools_update_mode_install(tool_class, result):
     assert tool._conanfile.command == result
 
 
+@pytest.mark.parametrize("tool_class, result", [
+    (Yum, "yum check-update -y"),
+    (Dnf, "dnf check-update -y"),
+])
+def test_dnf_yum_return_code_100(tool_class, result):
+    # https://github.com/conan-io/conan/issues/11661
+    conanfile = ConanFileMock()
+    conanfile.conf = Conf()
+    conanfile.settings = Settings()
+    conanfile.conf["tools.system.package_manager:tool"] = tool_class.tool_name
+    conanfile.conf["tools.system.package_manager:mode"] = "install"
+    with mock.patch('conans.ConanFile.context', new_callable=PropertyMock) as context_mock:
+        context_mock.return_value = "host"
+        tool = tool_class(conanfile)
+
+        def fake_run(command, win_bash=False, subsystem=None, env=None, ignore_errors=False):
+            assert command == result
+            return 100 if "check-update" in command else 0
+
+        conanfile.run = fake_run
+        tool.update()
+
+    #check that some random return code fails
+    with mock.patch('conans.ConanFile.context', new_callable=PropertyMock) as context_mock:
+        context_mock.return_value = "host"
+        tool = tool_class(conanfile)
+
+        def fake_run(command, win_bash=False, subsystem=None, env=None, ignore_errors=False):
+            return 55 if "check-update" in command else 0
+
+        conanfile.run = fake_run
+        with pytest.raises(ConanException) as exc_info:
+            tool.update()
+        assert f"Command '{result}' failed" == str(exc_info.value)
+
+
 @pytest.mark.parametrize("tool_class, result", [
     (Apt, 'apt-get install -y --no-install-recommends package1 package2'),
     (Yum, 'yum install -y package1 package2'),
@@ -185,8 +225,12 @@ def test_tools_install_mode_install(tool_class, result):
     with mock.patch('conans.ConanFile.context', new_callable=PropertyMock) as context_mock:
         context_mock.return_value = "host"
         tool = tool_class(conanfile)
-        tool.install(["package1", "package2"])
-    assert tool._conanfile.command == result
+
+        def fake_check(*args, **kwargs):
+            return ["package1", "package2"]
+        from conan.tools.system.package_manager import _SystemPackageManagerTool
+        with patch.object(_SystemPackageManagerTool, 'check', MagicMock(side_effect=fake_check)):
+            tool.install(["package1", "package2"])
 
 
 @pytest.mark.parametrize("tool_class, result", [
@@ -209,4 +253,3 @@ def test_tools_check(tool_class, result):
         context_mock.return_value = "host"
         tool = tool_class(conanfile)
         tool.check(["package"])
-    assert tool._conanfile.command == result
diff --git a/conans/test/utils/mocks.py b/conans/test/utils/mocks.py
index 4103bc4f3..08ae59928 100644
--- a/conans/test/utils/mocks.py
+++ b/conans/test/utils/mocks.py
@@ -197,6 +197,7 @@ class ConanFileMock(ConanFile):
         self.command = command
         self.path = os.environ["PATH"]
         self.captured_env = {key: value for key, value in os.environ.items()}
+        return 0
 
 
 MockOptions = MockSettings