Repository: conan-io/conan
Pull request #16294 (2024-05-17 10:36:11)

Cleanup backup test, ensure we follow expected behaviour
Changelog: Omit
Docs: Omit

For https://github.com/conan-io/conan/issues/16287 (Speaking with the use in Slack, will add a summary in the issue once resolved)


Patch:
diff --git a/conan/api/model.py b/conan/api/model.py
index 9e23cdfe1..025e20dd1 100644
--- a/conan/api/model.py
+++ b/conan/api/model.py
@@ -1,5 +1,6 @@
 import fnmatch
 import json
+import os
 
 from conans.client.graph.graph import RECIPE_EDITABLE, RECIPE_CONSUMER, RECIPE_PLATFORM, \
     RECIPE_VIRTUAL, BINARY_SKIP, BINARY_MISSING, BINARY_INVALID
@@ -90,6 +91,8 @@ class MultiPackagesList:
 
     @staticmethod
     def load_graph(graphfile, graph_recipes=None, graph_binaries=None):
+        if not os.path.isfile(graphfile):
+            raise ConanException(f"Graph file not found: {graphfile}")
         graph = json.loads(load(graphfile))
         return MultiPackagesList._define_graph(graph, graph_recipes, graph_binaries)
 
diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py
index 1bf351a9f..77d8c6df8 100644
--- a/conan/api/subapi/cache.py
+++ b/conan/api/subapi/cache.py
@@ -6,10 +6,10 @@ from io import BytesIO
 
 from conan.api.model import PackagesList
 from conan.api.output import ConanOutput
+from conan.internal.cache.cache import PkgCache
 from conan.internal.cache.home_paths import HomePaths
 from conan.internal.conan_app import ConanApp
 from conan.internal.integrity_check import IntegrityChecker
-from conans.client.cache.cache import ClientCache
 from conans.client.downloaders.download_cache import DownloadCache
 from conans.errors import ConanException
 from conans.model.package_ref import PkgReference
@@ -118,9 +118,9 @@ class CacheAPI:
                     rmdir(pref_layout.download_package())
 
     def save(self, package_list, tgz_path):
-        cache_folder = self.conan_api.cache_folder
         global_conf = self.conan_api.config.global_conf
-        cache = ClientCache(cache_folder, global_conf)
+        cache = PkgCache(self.conan_api.cache_folder, global_conf)
+        cache_folder = cache.store  # Note, this is not the home, but the actual package cache
         out = ConanOutput()
         mkdir(os.path.dirname(tgz_path))
         name = os.path.basename(tgz_path)
@@ -160,19 +160,20 @@ class CacheAPI:
     def restore(self, path):
         if not os.path.isfile(path):
             raise ConanException(f"Restore archive doesn't exist in {path}")
+
+        cache = PkgCache(self.conan_api.cache_folder, self.conan_api.config.global_conf)
+        cache_folder = cache.store  # Note, this is not the home, but the actual package cache
+
         with open(path, mode='rb') as file_handler:
             the_tar = tarfile.open(fileobj=file_handler)
             fileobj = the_tar.extractfile("pkglist.json")
             pkglist = fileobj.read()
-            the_tar.extractall(path=self.conan_api.cache_folder)
+            the_tar.extractall(path=cache_folder)
             the_tar.close()
 
         # After unzipping the files, we need to update the DB that references these files
         out = ConanOutput()
         package_list = PackagesList.deserialize(json.loads(pkglist))
-        cache = ClientCache(self.conan_api.cache_folder, self.conan_api.config.global_conf)
-        # FIXME: this might be broken for ``core.cache:storage_path``?
-        cache_folder = self.conan_api.cache_folder
         for ref, ref_bundle in package_list.refs().items():
             ref.timestamp = revision_timestamp_now()
             ref_bundle["timestamp"] = ref.timestamp
diff --git a/conan/cli/commands/download.py b/conan/cli/commands/download.py
index f8087103b..dbaa4f73e 100644
--- a/conan/cli/commands/download.py
+++ b/conan/cli/commands/download.py
@@ -21,7 +21,7 @@ def download(conan_api: ConanAPI, parser, *args):
 
     parser.add_argument('pattern', nargs="?",
                         help="A pattern in the form 'pkg/version#revision:package_id#revision', "
-                             "e.g: zlib/1.2.13:* means all binaries for zlib/1.2.13. "
+                             "e.g: \"zlib/1.2.13:*\" means all binaries for zlib/1.2.13. "
                              "If revision is not specified, it is assumed latest one.")
     parser.add_argument("--only-recipe", action='store_true', default=False,
                         help='Download only the recipe/s, not the binary packages.')
diff --git a/conan/cli/commands/graph.py b/conan/cli/commands/graph.py
index bf591c461..c5fb5f168 100644
--- a/conan/cli/commands/graph.py
+++ b/conan/cli/commands/graph.py
@@ -248,7 +248,7 @@ def graph_explain(conan_api, parser,  subparser, *args):
                            help='Whether the provided reference is a build-require')
     subparser.add_argument('--missing', nargs="?",
                            help="A pattern in the form 'pkg/version#revision:package_id#revision', "
-                                "e.g: zlib/1.2.13:* means all binaries for zlib/1.2.13. "
+                                "e.g: \"zlib/1.2.13:*\" means all binaries for zlib/1.2.13. "
                                 "If revision is not specified, it is assumed latest one.")
 
     args = parser.parse_args(*args)
diff --git a/conan/cli/commands/list.py b/conan/cli/commands/list.py
index 1fd4a8811..a905c2acd 100644
--- a/conan/cli/commands/list.py
+++ b/conan/cli/commands/list.py
@@ -207,7 +207,7 @@ def list(conan_api: ConanAPI, parser, *args):
     """
     parser.add_argument('pattern', nargs="?",
                         help="A pattern in the form 'pkg/version#revision:package_id#revision', "
-                             "e.g: zlib/1.2.13:* means all binaries for zlib/1.2.13. "
+                             "e.g: \"zlib/1.2.13:*\" means all binaries for zlib/1.2.13. "
                              "If revision is not specified, it is assumed latest one.")
     parser.add_argument('-p', '--package-query', default=None, action=OnceArgument,
                         help="List only the packages matching a specific query, e.g, os=Windows AND "
diff --git a/conan/cli/commands/remove.py b/conan/cli/commands/remove.py
index 984463b4f..74ac93d99 100644
--- a/conan/cli/commands/remove.py
+++ b/conan/cli/commands/remove.py
@@ -41,7 +41,7 @@ def remove(conan_api: ConanAPI, parser, *args):
     """
     parser.add_argument('pattern', nargs="?",
                         help="A pattern in the form 'pkg/version#revision:package_id#revision', "
-                             "e.g: zlib/1.2.13:* means all binaries for zlib/1.2.13. "
+                             "e.g: \"zlib/1.2.13:*\" means all binaries for zlib/1.2.13. "
                              "If revision is not specified, it is assumed latest one.")
     parser.add_argument('-c', '--confirm', default=False, action='store_true',
                         help='Remove without requesting a confirmation')
diff --git a/conan/cli/commands/upload.py b/conan/cli/commands/upload.py
index 3d1171e04..282a70df9 100644
--- a/conan/cli/commands/upload.py
+++ b/conan/cli/commands/upload.py
@@ -50,7 +50,7 @@ def upload(conan_api: ConanAPI, parser, *args):
     """
     parser.add_argument('pattern', nargs="?",
                         help="A pattern in the form 'pkg/version#revision:package_id#revision', "
-                             "e.g: zlib/1.2.13:* means all binaries for zlib/1.2.13. "
+                             "e.g: \"zlib/1.2.13:*\" means all binaries for zlib/1.2.13. "
                              "If revision is not specified, it is assumed latest one.")
     parser.add_argument('-p', '--package-query', default=None, action=OnceArgument,
                         help="Only upload packages matching a specific query. e.g: os=Windows AND "
diff --git a/conan/internal/cache/cache.py b/conan/internal/cache/cache.py
index 2e42564e4..edb2bb94a 100644
--- a/conan/internal/cache/cache.py
+++ b/conan/internal/cache/cache.py
@@ -2,24 +2,51 @@ import hashlib
 import os
 import shutil
 import uuid
+from typing import List
 
 from conan.internal.cache.conan_reference_layout import RecipeLayout, PackageLayout
 # TODO: Random folders are no longer accessible, how to get rid of them asap?
-# TODO: Add timestamp for LRU
 # TODO: We need the workflow to remove existing references.
 from conan.internal.cache.db.cache_database import CacheDatabase
-from conans.errors import ConanReferenceAlreadyExistsInDB, ConanReferenceDoesNotExistInDB
+from conans.errors import ConanReferenceAlreadyExistsInDB, ConanReferenceDoesNotExistInDB, \
+    ConanException
 from conans.model.package_ref import PkgReference
 from conans.model.recipe_ref import RecipeReference
 from conans.util.dates import revision_timestamp_now
-from conans.util.files import rmdir, renamedir
+from conans.util.files import rmdir, renamedir, mkdir
 
 
-class DataCache:
+class PkgCache:
+    """ Class to represent the recipes and packages storage in disk
+    """
 
-    def __init__(self, base_folder, db_filename):
-        self._base_folder = os.path.abspath(base_folder)
-        self._db = CacheDatabase(filename=db_filename)
+    def __init__(self, cache_folder, global_conf):
+        # paths
+        self._store_folder = global_conf.get("core.cache:storage_path") or \
+                             os.path.join(cache_folder, "p")
+
+        try:
+            mkdir(self._store_folder)
+            db_filename = os.path.join(self._store_folder, 'cache.sqlite3')
+            self._base_folder = os.path.abspath(self._store_folder)
+            self._db = CacheDatabase(filename=db_filename)
+        except Exception as e:
+            raise ConanException(f"Couldn't initialize storage in {self._store_folder}: {e}")
+
+    @property
+    def store(self):
+        return self._base_folder
+
+    @property
+    def temp_folder(self):
+        """ temporary folder where Conan puts exports and packages before the final revision
+        is computed"""
+        # TODO: Improve the path definitions, this is very hardcoded
+        return os.path.join(self._base_folder, "t")
+
+    @property
+    def builds_folder(self):
+        return os.path.join(self._base_folder, "b")
 
     def _create_path(self, relative_path, remove_contents=True):
         path = self._full_path(relative_path)
@@ -45,34 +72,24 @@ class DataCache:
         return sha_bytes[0:13]
 
     @staticmethod
-    def _get_tmp_path(ref: RecipeReference):
-        # The reference will not have revision, but it will be always constant
-        h = ref.name[:5] + DataCache._short_hash_path(ref.repr_notime())
-        return os.path.join("t", h)
-
-    @staticmethod
-    def _get_tmp_path_pref(pref):
-        # The reference will not have revision, but it will be always constant
-        assert pref.revision is None
-        assert pref.timestamp is None
-        random_id = str(uuid.uuid4())
-        h = pref.ref.name[:5] + DataCache._short_hash_path(pref.repr_notime() + random_id)
-        return os.path.join("b", h)
-
-    @staticmethod
-    def _get_path(ref: RecipeReference):
-        return ref.name[:5] + DataCache._short_hash_path(ref.repr_notime())
+    def _get_path(ref):
+        return ref.name[:5] + PkgCache._short_hash_path(ref.repr_notime())
 
     @staticmethod
     def _get_path_pref(pref):
-        return pref.ref.name[:5] + DataCache._short_hash_path(pref.repr_notime())
+        return pref.ref.name[:5] + PkgCache._short_hash_path(pref.repr_notime())
 
     def create_export_recipe_layout(self, ref: RecipeReference):
-        # This is a temporary layout while exporting a new recipe, because the revision is not
-        # computed yet, until it is. The entry is not added to DB, just a temp folder is created
+        """  This is a temporary layout while exporting a new recipe, because the revision is not
+        computed until later. The entry is not added to DB, just a temp folder is created
+
+        This temporary export folder will be moved to permanent when revision is computed by the
+        assign_rrev() method
+        """
         assert ref.revision is None, "Recipe revision should be None"
         assert ref.timestamp is None
-        reference_path = self._get_tmp_path(ref)
+        h = ref.name[:5] + PkgCache._short_hash_path(ref.repr_notime())
+        reference_path = os.path.join("t", h)
         self._create_path(reference_path)
         return RecipeLayout(ref, os.path.join(self._base_folder, reference_path))
 
@@ -82,11 +99,14 @@ class DataCache:
         assert pref.package_id, "Package id must be known to get or create the package layout"
         assert pref.revision is None, "Package revision should be None"
         assert pref.timestamp is None
-        package_path = self._get_tmp_path_pref(pref)
+
+        random_id = str(uuid.uuid4())
+        h = pref.ref.name[:5] + PkgCache._short_hash_path(pref.repr_notime() + random_id)
+        package_path = os.path.join("b", h)
         self._create_path(package_path)
         return PackageLayout(pref, os.path.join(self._base_folder, package_path))
 
-    def get_recipe_layout(self, ref: RecipeReference):
+    def recipe_layout(self, ref: RecipeReference):
         """ the revision must exists, the folder must exist
         """
         if ref.revision is None:  # Latest one
@@ -97,10 +117,17 @@ class DataCache:
         ref = ref_data.get("ref")  # new revision with timestamp
         return RecipeLayout(ref, os.path.join(self._base_folder, ref_path))
 
+    def get_latest_recipe_reference(self, ref: RecipeReference):
+        assert ref.revision is None
+        ref_data = self._db.get_latest_recipe(ref)
+        return ref_data.get("ref")
+
     def get_recipe_revisions_references(self, ref: RecipeReference):
+        # For listing multiple revisions only
+        assert ref.revision is None
         return self._db.get_recipe_revisions_references(ref)
 
-    def get_package_layout(self, pref: PkgReference):
+    def pkg_layout(self, pref: PkgReference):
         """ the revision must exists, the folder must exist
         """
         assert pref.ref.revision, "Recipe revision must be known to get the package layout"
@@ -115,7 +142,7 @@ class DataCache:
         """ called by RemoteManager.get_recipe()
         """
         try:
-            return self.get_recipe_layout(ref)
+            return self.recipe_layout(ref)
         except ConanReferenceDoesNotExistInDB:
             assert ref.revision, "Recipe revision must be known to create the package layout"
             reference_path = self._get_path(ref)
@@ -127,7 +154,7 @@ class DataCache:
         """ called by RemoteManager.get_package() and  BinaryInstaller
         """
         try:
-            return self.get_package_layout(pref)
+            return self.pkg_layout(pref)
         except ConanReferenceDoesNotExistInDB:
             assert pref.ref.revision, "Recipe revision must be known to create the package layout"
             assert pref.package_id, "Package id must be known to create the package layout"
@@ -138,20 +165,25 @@ class DataCache:
             return PackageLayout(pref, os.path.join(self._base_folder, package_path))
 
     def update_recipe_timestamp(self, ref: RecipeReference):
+        """ when the recipe already exists in cache, but we get a new timestamp from a server
+        that would affect its order in our cache """
         assert ref.revision
         assert ref.timestamp
         self._db.update_recipe_timestamp(ref)
 
-    def list_references(self):
+    def all_refs(self):
         return self._db.list_references()
 
     def exists_prev(self, pref):
+        # Used just by download to skip downloads if prev already exists in cache
         return self._db.exists_prev(pref)
 
     def get_latest_package_reference(self, pref):
         return self._db.get_latest_package_reference(pref)
 
-    def get_package_references(self, ref: RecipeReference, only_latest_prev=True):
+    def get_package_references(self, ref: RecipeReference,
+                               only_latest_prev=True) -> List[PkgReference]:
+        """Get the latest package references"""
         return self._db.get_package_references(ref, only_latest_prev)
 
     def get_package_revisions_references(self, pref: PkgReference, only_latest_prev=False):
@@ -160,12 +192,12 @@ class DataCache:
     def get_matching_build_id(self, ref, build_id):
         return self._db.get_matching_build_id(ref, build_id)
 
-    def remove_recipe(self, layout: RecipeLayout):
+    def remove_recipe_layout(self, layout: RecipeLayout):
         layout.remove()
         # FIXME: This is clearing package binaries from DB, but not from disk/layout
         self._db.remove_recipe(layout.reference)
 
-    def remove_package(self, layout: PackageLayout):
+    def remove_package_layout(self, layout: PackageLayout):
         layout.remove()
         self._db.remove_package(layout.reference)
 
@@ -185,7 +217,7 @@ class DataCache:
         except ConanReferenceAlreadyExistsInDB:
             # TODO: Optimize this into 1 single UPSERT operation
             # There was a previous package folder for this same package reference (and prev)
-            pkg_layout = self.get_package_layout(pref)
+            pkg_layout = self.pkg_layout(pref)
             # We remove the old one and move the new one to the path of the previous one
             # this can be necessary in case of new metadata or build-folder because of "build_id()"
             pkg_layout.remove()
diff --git a/conan/internal/conan_app.py b/conan/internal/conan_app.py
index 41c6d204c..0f92e3eb2 100644
--- a/conan/internal/conan_app.py
+++ b/conan/internal/conan_app.py
@@ -1,8 +1,8 @@
 import os
 
 from conan.api.output import ConanOutput
+from conan.internal.cache.cache import PkgCache
 from conan.internal.cache.home_paths import HomePaths
-from conans.client.cache.cache import ClientCache
 from conans.client.graph.proxy import ConanProxy
 from conans.client.graph.python_requires import PyRequireLoader
 from conans.client.graph.range_resolver import RangeResolver
@@ -44,7 +44,7 @@ class ConanApp:
         cache_folder = conan_api.home_folder
         self._configure(global_conf)
         self.cache_folder = cache_folder
-        self.cache = ClientCache(self.cache_folder, global_conf)
+        self.cache = PkgCache(self.cache_folder, global_conf)
 
         home_paths = HomePaths(self.cache_folder)
         self.hook_manager = HookManager(home_paths.hooks_path)
diff --git a/conan/tools/cmake/toolchain/blocks.py b/conan/tools/cmake/toolchain/blocks.py
index adb338e5b..02d6c4723 100644
--- a/conan/tools/cmake/toolchain/blocks.py
+++ b/conan/tools/cmake/toolchain/blocks.py
@@ -480,6 +480,9 @@ class FindFiles(Block):
         {% if cmake_include_path %}
         list(PREPEND CMAKE_INCLUDE_PATH {{ cmake_include_path }})
         {% endif %}
+        {% if host_runtime_dirs %}
+        set(CONAN_RUNTIME_LIB_DIRS {{ host_runtime_dirs }} )
+        {% endif %}
 
         {% if cross_building %}
         if(NOT DEFINED CMAKE_FIND_ROOT_PATH_MODE_PACKAGE OR CMAKE_FIND_ROOT_PATH_MODE_PACKAGE STREQUAL "ONLY")
@@ -502,6 +505,40 @@ class FindFiles(Block):
         {% endif %}
     """)
 
+    def _runtime_dirs_value(self, dirs):
+        if is_multi_configuration(self._toolchain.generator):
+            return ' '.join(f'"$<$<CONFIG:{c}>:{i}>"' for c, v in dirs.items() for i in v)
+        else:
+            return ' '.join(f'"{item}"' for _, items in dirs.items() for item in items)
+
+    def _get_host_runtime_dirs(self, host_req):
+        settings = self._conanfile.settings
+        host_runtime_dirs = {}
+        is_win = self._conanfile.settings.get_safe("os") == "Windows"
+
+        # Get the previous configuration
+        if is_multi_configuration(self._toolchain.generator) and os.path.exists(CONAN_TOOLCHAIN_FILENAME):
+            existing_toolchain = load(CONAN_TOOLCHAIN_FILENAME)
+            pattern_lib_dirs = r"set\(CONAN_RUNTIME_LIB_DIRS ([^)]*)\)"
+            variable_match = re.search(pattern_lib_dirs, existing_toolchain)
+            if variable_match:
+                capture = variable_match.group(1)
+                matches = re.findall(r'"\$<\$<CONFIG:([A-Za-z]*)>:([^>]*)>"', capture)
+                host_runtime_dirs = {}
+                for k, v in matches:
+                    host_runtime_dirs.setdefault(k, []).append(v)
+        
+        # Calculate the dirs for the current build_type
+        runtime_dirs = []
+        for req in host_req:
+            cppinfo = req.cpp_info.aggregated_components()
+            runtime_dirs.extend(cppinfo.bindirs if is_win else cppinfo.libdirs)
+        
+        build_type = settings.get_safe("build_type")
+        host_runtime_dirs[build_type] = [s.replace("\\", "/") for s in runtime_dirs]
+
+        return host_runtime_dirs
+
     @staticmethod
     def _join_paths(paths):
         return " ".join(['"{}"'.format(p.replace('\\', '/')
@@ -524,6 +561,7 @@ class FindFiles(Block):
         host_req = self._conanfile.dependencies.filter({"build": False}).values()
         build_paths = []
         host_lib_paths = []
+        host_runtime_dirs = self._get_host_runtime_dirs(host_req)
         host_framework_paths = []
         host_include_paths = []
         for req in host_req:
@@ -552,6 +590,7 @@ class FindFiles(Block):
             "cmake_include_path": self._join_paths(host_include_paths),
             "is_apple": is_apple_,
             "cross_building": cross_building(self._conanfile),
+            "host_runtime_dirs": self._runtime_dirs_value(host_runtime_dirs)
         }
 
 
@@ -969,11 +1008,10 @@ class OutputDirsBlock(Block):
 
     @property
     def template(self):
-        if not self._conanfile.package_folder:
-            return ""
-
         return textwrap.dedent("""
+           {% if package_folder %}
            set(CMAKE_INSTALL_PREFIX "{{package_folder}}")
+           {% endif %}
            {% if default_bin %}
            set(CMAKE_INSTALL_BINDIR "{{default_bin}}")
            set(CMAKE_INSTALL_SBINDIR "{{default_bin}}")
@@ -998,9 +1036,8 @@ class OutputDirsBlock(Block):
         return elements[0] if elements else None
 
     def context(self):
-        if not self._conanfile.package_folder:
-            return {}
-        return {"package_folder": self._conanfile.package_folder.replace("\\", "/"),
+        pf = self._conanfile.package_folder
+        return {"package_folder": pf.replace("\\", "/") if pf else None,
                 "default_bin": self._get_cpp_info_value("bindirs"),
                 "default_lib": self._get_cpp_info_value("libdirs"),
                 "default_include": self._get_cpp_info_value("includedirs"),
diff --git a/conans/client/cache/cache.py b/conans/client/cache/cache.py
deleted file mode 100644
index 5fea2ccac..000000000
--- a/conans/client/cache/cache.py
+++ /dev/null
@@ -1,124 +0,0 @@
-import os
-from typing import List
-
-from conan.internal.cache.cache import DataCache, RecipeLayout, PackageLayout
-from conans.errors import ConanException
-from conans.model.package_ref import PkgReference
-from conans.model.recipe_ref import RecipeReference
-from conans.util.files import mkdir
-
-
-# TODO: Rename this to ClientHome
-class ClientCache:
-    """ Class to represent/store/compute all the paths involved in the execution
-    of conans commands. Accesses to real disk and reads/write things. (OLD client ConanPaths)
-    """
-
-    def __init__(self, cache_folder, global_conf):
-        # paths
-        self._store_folder = global_conf.get("core.cache:storage_path") or \
-                             os.path.join(cache_folder, "p")
-
-        try:
-            mkdir(self._store_folder)
-            db_filename = os.path.join(self._store_folder, 'cache.sqlite3')
-            self._data_cache = DataCache(self._store_folder, db_filename)
-        except Exception as e:
-            raise ConanException(f"Couldn't initialize storage in {self._store_folder}: {e}")
-
-    @property
-    def temp_folder(self):
-        """ temporary folder where Conan puts exports and packages before the final revision
-        is computed"""
-        # TODO: Improve the path definitions, this is very hardcoded
-        return os.path.join(self._store_folder, "t")
-
-    @property
-    def builds_folder(self):
-        return os.path.join(self._store_folder, "b")
-
-    def create_export_recipe_layout(self, ref: RecipeReference):
-        return self._data_cache.create_export_recipe_layout(ref)
-
-    def assign_rrev(self, layout: RecipeLayout):
-        return self._data_cache.assign_rrev(layout)
-
-    def create_build_pkg_layout(self, ref):
-        return self._data_cache.create_build_pkg_layout(ref)
-
-    def assign_prev(self, layout: PackageLayout):
-        return self._data_cache.assign_prev(layout)
-
-    # Recipe methods
-    def recipe_layout(self, ref: RecipeReference):
-        return self._data_cache.get_recipe_layout(ref)
-
-    def get_latest_recipe_reference(self, ref):
-        # TODO: We keep this for testing only, to be removed
-        assert ref.revision is None
-        return self._data_cache.get_recipe_layout(ref).reference
-
-    def get_recipe_revisions_references(self, ref):
-        # For listing multiple revisions only
-        assert ref.revision is None
-        return self._data_cache.get_recipe_revisions_references(ref)
-
-    def pkg_layout(self, ref: PkgReference):
-        return self._data_cache.get_package_layout(ref)
-
-    def get_or_create_ref_layout(self, ref: RecipeReference):
-        return self._data_cache.get_or_create_ref_layout(ref)
-
-    def get_or_create_pkg_layout(self, ref: PkgReference):
-        return self._data_cache.get_or_create_pkg_layout(ref)
-
-    def remove_recipe_layout(self, layout):
-        self._data_cache.remove_recipe(layout)
-
-    def remove_package_layout(self, layout):
-        self._data_cache.remove_package(layout)
-
-    def remove_build_id(self, pref):
-        self._data_cache.remove_build_id(pref)
-
-    def update_recipe_timestamp(self, ref):
-        """ when the recipe already exists in cache, but we get a new timestamp from a server
-        that would affect its order in our cache """
-        return self._data_cache.update_recipe_timestamp(ref)
-
-    def all_refs(self):
-        return self._data_cache.list_references()
-
-    def exists_prev(self, pref):
-        # Used just by download to skip downloads if prev already exists in cache
-        return self._data_cache.exists_prev(pref)
-
-    def get_package_revisions_references(self, pref: PkgReference, only_latest_prev=False):
-        return self._data_cache.get_package_revisions_references(pref, only_latest_prev)
-
-    def get_package_references(self, ref: RecipeReference,
-                               only_latest_prev=True) -> List[PkgReference]:
-        """Get the latest package references"""
-        return self._data_cache.get_package_references(ref, only_latest_prev)
-
-    def get_matching_build_id(self, ref, build_id):
-        return self._data_cache.get_matching_build_id(ref, build_id)
-
-    def get_latest_package_reference(self, pref):
-        return self._data_cache.get_latest_package_reference(pref)
-
-    def get_recipe_lru(self, ref):
-        return self._data_cache.get_recipe_lru(ref)
-
-    def update_recipe_lru(self, ref):
-        self._data_cache.update_recipe_lru(ref)
-
-    def get_package_lru(self, pref):
-        return self._data_cache.get_package_lru(pref)
-
-    def update_package_lru(self, pref):
-        self._data_cache.update_package_lru(pref)
-
-    @property
-    def store(self):
-        return self._store_folder
diff --git a/conans/client/downloaders/file_downloader.py b/conans/client/downloaders/file_downloader.py
index 318088a36..676af7a07 100644
--- a/conans/client/downloaders/file_downloader.py
+++ b/conans/client/downloaders/file_downloader.py
@@ -49,6 +49,7 @@ class FileDownloader:
                         time.sleep(retry_wait)
 
             self.check_checksum(file_path, md5, sha1, sha256)
+            self._output.debug(f"Downloaded {file_path} from {url}")
         except Exception:
             if os.path.exists(file_path):
                 os.remove(file_path)
diff --git a/conans/client/graph/install_graph.py b/conans/client/graph/install_graph.py
index 51cec4fcb..14775ab01 100644
--- a/conans/client/graph/install_graph.py
+++ b/conans/client/graph/install_graph.py
@@ -510,7 +510,7 @@ class InstallGraph:
 
         raise ConanException(textwrap.dedent(f'''\
            Missing prebuilt package for '{missing_pkgs}'. You can try:
-               - List all available packages using 'conan list {ref}:* -r=remote'
+               - List all available packages using 'conan list "{ref}:*" -r=remote'
                - Explain missing binaries: replace 'conan install ...' with 'conan graph explain ...'
                - {build_msg}
 
diff --git a/conans/client/graph/range_resolver.py b/conans/client/graph/range_resolver.py
index 16336607c..535cc5ce2 100644
--- a/conans/client/graph/range_resolver.py
+++ b/conans/client/graph/range_resolver.py
@@ -17,7 +17,12 @@ class RangeResolver:
         self._resolve_prereleases = global_conf.get('core.version_ranges:resolve_prereleases')
 
     def resolve(self, require, base_conanref, remotes, update):
-        version_range = require.version_range
+        try:
+            version_range = require.version_range
+        except Exception as e:
+            base = base_conanref or "conanfile"
+            raise ConanException(f"\n    Recipe '{base}' requires '{require.ref}' "
+                                 f"version-range definition error:\n    {e}")
         if version_range is None:
             return
         assert isinstance(version_range, VersionRange)


Test patch:
diff --git a/conans/test/functional/only_source_test.py b/conans/test/functional/only_source_test.py
index c51b146c3..2a1a7cc04 100644
--- a/conans/test/functional/only_source_test.py
+++ b/conans/test/functional/only_source_test.py
@@ -30,7 +30,7 @@ class OnlySourceTest(unittest.TestCase):
                       "--build=hello1/1.1@lasote/stable'",
                       client.out)
         # Only 1 reference!
-        assert "List all available packages using 'conan list hello0/0.0@lasote/stable:* -r=remote'" in client.out
+        assert "List all available packages using 'conan list \"hello0/0.0@lasote/stable:*\" -r=remote'" in client.out
 
         # We generate the package for hello0/0.0
         client.run("install --requires=hello0/0.0@lasote/stable --build hello0*")
diff --git a/conans/test/integration/cache/backup_sources_test.py b/conans/test/integration/cache/backup_sources_test.py
index 293d25755..db70e24c7 100644
--- a/conans/test/integration/cache/backup_sources_test.py
+++ b/conans/test/integration/cache/backup_sources_test.py
@@ -24,8 +24,9 @@ class TestDownloadCacheBackupSources:
                         custom_download):
             client = TestClient(default_server_user=True, light=True)
             tmp_folder = temp_folder()
-            client.save_home({"global.conf": f"core.sources:download_cache={tmp_folder}\n"
-                                        "core.sources:download_urls=['origin', 'http://myback']"})
+            client.save_home(
+                {"global.conf": f"core.sources:download_cache={tmp_folder}\n"
+                                "core.sources:download_urls=['origin', 'http://myback']"})
             sha256 = "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3"
             conanfile = textwrap.dedent(f"""
                 from conan import ConanFile
@@ -148,7 +149,8 @@ class TestDownloadCacheBackupSources:
             {"global.conf": f"core.sources:download_cache={self.download_cache_folder}\n"
                             f"core.sources:download_urls=['origin', '{self.file_server.fake_url}/backups/']\n"
                             f"core.sources:upload_url={self.file_server.fake_url}/backups/\n"
-                            f"core.sources:exclude_urls=['{self.file_server.fake_url}/mycompanystorage/', '{self.file_server.fake_url}/mycompanystorage2/']"})
+                            f"core.sources:exclude_urls=['{self.file_server.fake_url}/mycompanystorage/', "
+                            f"'{self.file_server.fake_url}/mycompanystorage2/']"})
         self.client.run("create .")
 
         self.client.run("upload * -c -r=default")
@@ -254,7 +256,9 @@ class TestDownloadCacheBackupSources:
                             f"core.sources:upload_url={self.file_server.fake_url}/backup"})
 
         self.client.save({"conanfile.py": conanfile})
-        self.client.run("create .")
+        self.client.run("create . -vv")
+        assert f"WARN: File {self.file_server.fake_url}/internet/myfile.txt not found in {self.file_server.fake_url}/backup/" in self.client.out
+        assert f"Downloaded {self.file_server.fake_url}/internet/myfile.txt from {self.file_server.fake_url}/internet/myfile.txt"
         self.client.run("upload * -c -r=default")
 
         rmdir(self.download_cache_folder)
@@ -280,10 +284,30 @@ class TestDownloadCacheBackupSources:
             {"global.conf": f"core.sources:download_cache={self.download_cache_folder}\n"
                             f"core.sources:download_urls=['{self.file_server.fake_url}/internal_error/', "
                             f"'{self.file_server.fake_url}/unused/']\n"})
+        self.client.save({"conanfile.py": conanfile})
+        self.client.run("create .", assert_error=True)
+        assert "ConanException: Error 500 downloading file" in self.client.out
+
+    def test_sources_backup_server_not_found(self):
+        conanfile = textwrap.dedent(f"""
+                   from conan import ConanFile
+                   from conan.tools.files import download
+                   class Pkg2(ConanFile):
+                       name = "pkg"
+                       version = "1.0"
+                       def source(self):
+                           download(self, "{self.file_server.fake_url}/internet/myfile.txt", "myfile.txt",
+                                    sha256="unused")
+                   """)
+
+        self.client.save_home(
+            {"global.conf": f"core.sources:download_cache={self.download_cache_folder}\n"
+                            f"core.download:retry=0\n"
+                            f"core.sources:download_urls=['http://locahost/nothing-here', 'origin']\n"})
 
         self.client.save({"conanfile.py": conanfile})
         self.client.run("create .", assert_error=True)
-        assert "ConanException: Error 500 downloading file " in self.client.out
+        assert "ConanException: Error downloading file" in self.client.out
 
     def test_upload_sources_backup_creds_needed(self):
         client = TestClient(default_server_user=True, light=True)
@@ -333,16 +357,18 @@ class TestDownloadCacheBackupSources:
                     self.output.info(f"CONTENT: {{load(self, 'myfile.txt')}}")
             """)
 
-        client.save_home({"global.conf": f"core.sources:download_cache={download_cache_folder}\n"
-                                    f"core.sources:download_urls=['http://localhost:{http_server.port}/downloader/', 'origin']\n"
-                                    f"core.sources:upload_url=http://localhost:{http_server.port}/uploader/"})
+        client.save_home(
+            {"global.conf": f"core.sources:download_cache={download_cache_folder}\n"
+                            f"core.sources:download_urls=['http://localhost:{http_server.port}/downloader/', 'origin']\n"
+                            f"core.sources:upload_url=http://localhost:{http_server.port}/uploader/"})
 
         client.save({"conanfile.py": conanfile})
         client.run("create .", assert_error=True)
         assert f"ConanException: The source backup server 'http://localhost:{http_server.port}" \
                f"/downloader/' needs authentication" in client.out
-        content = {"credentials":
-                       [{"url": f"http://localhost:{http_server.port}", "token": "mytoken"}]}
+        content = {"credentials": [
+            {"url": f"http://localhost:{http_server.port}", "token": "mytoken"}
+        ]}
         save(os.path.join(client.cache_folder, "source_credentials.json"), json.dumps(content))
 
         client.run("create .")
@@ -350,8 +376,9 @@ class TestDownloadCacheBackupSources:
         client.run("upload * -c -r=default", assert_error=True)
         assert f"The source backup server 'http://localhost:{http_server.port}" \
                f"/uploader/' needs authentication" in client.out
-        content = {"credentials":
-                       [{"url": f"http://localhost:{http_server.port}", "token": "myuploadtoken"}]}
+        content = {"credentials": [
+            {"url": f"http://localhost:{http_server.port}", "token": "myuploadtoken"}
+        ]}
         # Now use the correct UPLOAD token
         save(os.path.join(client.cache_folder, "source_credentials.json"), json.dumps(content))
         client.run("upload * -c -r=default --force")  # need --force to guarantee cached updated
@@ -363,8 +390,9 @@ class TestDownloadCacheBackupSources:
         client.run("upload * -c -r=default")
         assert "already in server, skipping upload" in client.out
 
-        content = {"credentials":
-                       [{"url": f"http://localhost:{http_server.port}", "token": "mytoken"}]}
+        content = {"credentials": [
+            {"url": f"http://localhost:{http_server.port}", "token": "mytoken"}
+        ]}
 
         save(os.path.join(client.cache_folder, "source_credentials.json"), json.dumps(content))
         rmdir(download_cache_folder)
@@ -402,7 +430,8 @@ class TestDownloadCacheBackupSources:
         self.client.save_home(
             {"global.conf": f"core.sources:download_cache={self.download_cache_folder}\n"
                             f"core.sources:upload_url={self.file_server.fake_url}/backup/\n"
-                            f"core.sources:download_urls=['origin', '{self.file_server.fake_url}/downloader/', '{self.file_server.fake_url}/backup/']"})
+                            f"core.sources:download_urls=['origin', '{self.file_server.fake_url}/downloader/', "
+                            f"'{self.file_server.fake_url}/backup/']"})
 
         self.client.save({"conanfile.py": conanfile})
         self.client.run("create .")
@@ -451,8 +480,9 @@ class TestDownloadCacheBackupSources:
                         download(self, "http://fake/myfile.txt", "myfile.txt", sha256="{sha256}")
                 """)
 
-            client.save_home({"global.conf": f"core.sources:download_cache={download_cache_folder}\n"
-                                        f"core.sources:download_urls=['origin', 'http://extrafake/']\n"})
+            client.save_home(
+                {"global.conf": f"core.sources:download_cache={download_cache_folder}\n"
+                                f"core.sources:download_urls=['origin', 'http://extrafake/']\n"})
             client.save({"conanfile.py": conanfile})
             client.run("source .", assert_error=True)
             assert "WARN: Sources for http://fake/myfile.txt failed in 'origin'" in client.out
@@ -524,10 +554,10 @@ class TestDownloadCacheBackupSources:
                    download(self, "http://localhost:{http_server.port}/internet/myfile.txt", "myfile.txt",
                             sha256="{sha256}")
            """)
-
         client.save_home({"global.conf": f"core.sources:download_cache={download_cache_folder}\n"
-                                    f"core.sources:download_urls=['http://localhost:{http_server.port}/downloader1/', "
-                                    f"'origin', 'http://localhost:{http_server.port}/downloader2/']\n"})
+                            f"core.sources:download_urls=['http://localhost:{http_server.port}/downloader1/', "
+                            f"'origin', 'http://localhost:{http_server.port}/downloader2/']\n"})
+
 
         client.save({"conanfile.py": conanfile})
         client.run("create .")
diff --git a/conans/test/integration/command_v2/test_cache_save_restore.py b/conans/test/integration/command_v2/test_cache_save_restore.py
index c5e7c5b91..53c8dda3a 100644
--- a/conans/test/integration/command_v2/test_cache_save_restore.py
+++ b/conans/test/integration/command_v2/test_cache_save_restore.py
@@ -3,7 +3,10 @@ import os
 import shutil
 import tarfile
 
+import pytest
+
 from conans.test.assets.genconanfile import GenConanfile
+from conans.test.utils.test_files import temp_folder
 from conans.test.utils.tools import TestClient
 from conans.util.files import save, load
 
@@ -218,3 +221,25 @@ def test_error_restore_not_existing():
     c = TestClient()
     c.run("cache restore potato.tgz", assert_error=True)
     assert "ERROR: Restore archive doesn't exist in " in c.out
+
+
+@pytest.mark.parametrize("src_store", (False, True))
+@pytest.mark.parametrize("dst_store", (False, True))
+def test_cache_save_restore_custom_storage_path(src_store, dst_store):
+    c = TestClient()
+    if src_store:
+        tmp_folder = temp_folder()
+        c.save_home({"global.conf": f"core.cache:storage_path={tmp_folder}"})
+    c.save({"conanfile.py": GenConanfile()})
+    c.run("create . --name=pkg --version=1.0")
+    c.run("cache save *:*")
+    cache_path = os.path.join(c.current_folder, "conan_cache_save.tgz")
+
+    c2 = TestClient()
+    if dst_store:
+        tmp_folder = temp_folder()
+        c2.save_home({"global.conf": f"core.cache:storage_path={tmp_folder}"})
+    shutil.copy2(cache_path, c2.current_folder)
+    c2.run("cache restore conan_cache_save.tgz")
+    c2.run("list *:*")
+    assert "pkg/1.0" in c2.out
diff --git a/conans/test/integration/command_v2/test_combined_pkglist_flows.py b/conans/test/integration/command_v2/test_combined_pkglist_flows.py
index fbd88317d..862400655 100644
--- a/conans/test/integration/command_v2/test_combined_pkglist_flows.py
+++ b/conans/test/integration/command_v2/test_combined_pkglist_flows.py
@@ -13,7 +13,7 @@ class TestListUpload:
 
     @pytest.fixture()
     def client(self):
-        c = TestClient(default_server_user=True)
+        c = TestClient(default_server_user=True, light=True)
         c.save({
             "zlib.py": GenConanfile("zlib"),
             "zli.py": GenConanfile("zli", "1.0.0")
@@ -54,7 +54,7 @@ class TestListUpload:
 
 class TestGraphCreatedUpload:
     def test_create_upload(self):
-        c = TestClient(default_server_user=True)
+        c = TestClient(default_server_user=True, light=True)
         c.save({"zlib/conanfile.py": GenConanfile("zlib", "1.0"),
                 "app/conanfile.py": GenConanfile("app", "1.0").with_requires("zlib/1.0")})
         c.run("create zlib")
@@ -69,7 +69,7 @@ class TestGraphCreatedUpload:
 
 class TestExportUpload:
     def test_export_upload(self):
-        c = TestClient(default_server_user=True)
+        c = TestClient(default_server_user=True, light=True)
         c.save({"zlib/conanfile.py": GenConanfile("zlib", "1.0")})
         c.run("export zlib --format=pkglist", redirect_stdout="pkglist.json")
         c.run("upload --list=pkglist.json -r=default -c")
@@ -77,29 +77,34 @@ class TestExportUpload:
 
 
 class TestCreateGraphToPkgList:
+    def test_graph_pkg_nonexistant(self):
+        c = TestClient(light=True)
+        c.run("list --graph=non-existent-file.json", assert_error=True)
+        assert "ERROR: Graph file not found" in c.out
+
     def test_graph_pkg_list_only_built(self):
-        c = TestClient()
+        c = TestClient(light=True)
         c.save({"zlib/conanfile.py": GenConanfile("zlib", "1.0"),
                 "app/conanfile.py": GenConanfile("app", "1.0").with_requires("zlib/1.0")
                                                               .with_settings("os")
                                                               .with_shared_option(False)})
         c.run("create zlib")
-        c.run("create app --format=json -s os=Windows", redirect_stdout="graph.json")
+        c.run("create app --format=json -s os=Linux", redirect_stdout="graph.json")
         c.run("list --graph=graph.json --graph-binaries=build --format=json")
         pkglist = json.loads(c.stdout)["Local Cache"]
         assert len(pkglist) == 1
         pkgs = pkglist["app/1.0"]["revisions"]["8263c3c32802e14a2f03a0b1fcce0d95"]["packages"]
         assert len(pkgs) == 1
-        pkg_app = pkgs["e0bcc80c3f095670b71e535c193114d0155426cb"]
+        pkg_app = pkgs["d8b3bdd894c3eb9bf2a3119ee0f8c70843ace0ac"]
         assert pkg_app["info"]["requires"] == ["zlib/1.0.Z"]
-        assert pkg_app["info"]["settings"] == {'os': 'Windows'}
+        assert pkg_app["info"]["settings"] == {'os': 'Linux'}
         assert pkg_app["info"]["options"] == {'shared': 'False'}
 
     def test_graph_pkg_list_all_recipes_only(self):
         """
         --graph-recipes=* selects all the recipes in the graph
         """
-        c = TestClient()
+        c = TestClient(light=True)
         c.save({"zlib/conanfile.py": GenConanfile("zlib", "1.0"),
                 "app/conanfile.py": GenConanfile("app", "1.0").with_requires("zlib/1.0")})
         c.run("create zlib")
@@ -114,7 +119,7 @@ class TestCreateGraphToPkgList:
         """
         include python_requires too
         """
-        c = TestClient(default_server_user=True)
+        c = TestClient(default_server_user=True, light=True)
         c.save({"pytool/conanfile.py": GenConanfile("pytool", "0.1"),
                 "zlib/conanfile.py": GenConanfile("zlib", "1.0").with_python_requires("pytool/0.1"),
                 "app/conanfile.py": GenConanfile("app", "1.0").with_requires("zlib/1.0")})
@@ -135,7 +140,7 @@ class TestCreateGraphToPkgList:
         """
         include python_requires too
         """
-        c = TestClient(default_server_user=True)
+        c = TestClient(default_server_user=True, light=True)
         c.save({"conanfile.py": GenConanfile("pytool", "0.1").with_package_type("python-require")})
         c.run("create . --format=json", redirect_stdout="graph.json")
         c.run("list --graph=graph.json --format=json")
@@ -146,7 +151,7 @@ class TestCreateGraphToPkgList:
 
 class TestGraphInfoToPkgList:
     def test_graph_pkg_list_only_built(self):
-        c = TestClient(default_server_user=True)
+        c = TestClient(default_server_user=True, light=True)
         c.save({"zlib/conanfile.py": GenConanfile("zlib", "1.0"),
                 "app/conanfile.py": GenConanfile("app", "1.0").with_requires("zlib/1.0")})
         c.run("create zlib")
@@ -258,7 +263,7 @@ class TestPkgListMerge:
 class TestDownloadUpload:
     @pytest.fixture()
     def client(self):
-        c = TestClient(default_server_user=True)
+        c = TestClient(default_server_user=True, light=True)
         c.save({
             "zlib.py": GenConanfile("zlib"),
             "zli.py": GenConanfile("zli", "1.0.0")
@@ -315,7 +320,7 @@ class TestDownloadUpload:
 class TestListRemove:
     @pytest.fixture()
     def client(self):
-        c = TestClient(default_server_user=True)
+        c = TestClient(default_server_user=True, light=True)
         c.save({
             "zlib.py": GenConanfile("zlib"),
             "zli.py": GenConanfile("zli", "1.0.0")
diff --git a/conans/test/integration/graph/core/graph_manager_base.py b/conans/test/integration/graph/core/graph_manager_base.py
index 1889aacb2..a22e3245f 100644
--- a/conans/test/integration/graph/core/graph_manager_base.py
+++ b/conans/test/integration/graph/core/graph_manager_base.py
@@ -5,8 +5,8 @@ import unittest
 import yaml
 
 from conan.api.conan_api import ConanAPI
+from conan.internal.cache.cache import PkgCache
 from conan.internal.cache.home_paths import HomePaths
-from conans.client.cache.cache import ClientCache
 from conans.client.conf import default_settings_yml
 from conans.model.conf import ConfDefinition
 from conans.model.manifest import FileTreeManifest
@@ -24,7 +24,7 @@ class GraphManagerTest(unittest.TestCase):
 
     def setUp(self):
         cache_folder = temp_folder()
-        cache = ClientCache(cache_folder, ConfDefinition())
+        cache = PkgCache(cache_folder, ConfDefinition())
         home = HomePaths(cache_folder)
         save(os.path.join(home.profiles_path, "default"), "")
         save(home.settings_path, "os: [Windows, Linux]")
diff --git a/conans/test/integration/lockfile/test_lock_requires_revisions.py b/conans/test/integration/lockfile/test_lock_requires_revisions.py
index e84680c08..1f6e16ab7 100644
--- a/conans/test/integration/lockfile/test_lock_requires_revisions.py
+++ b/conans/test/integration/lockfile/test_lock_requires_revisions.py
@@ -132,20 +132,20 @@ def test_conditional_os(requires):
     client.run("install consumer --lockfile=consumer.lock -s os=Windows -s:b os=Windows")
     assert "REV1!!!" in client.out
     assert "REV2!!!" not in client.out
-    assert "nix" not in client.out
+    assert "nix/0.1" not in client.out
     client.run("install consumer -s os=Windows -s:b os=Windows")
     assert "REV2!!!" in client.out
     assert "REV1!!!" not in client.out
-    assert "nix" not in client.out
+    assert "nix/0.1" not in client.out
 
     client.run("install consumer --lockfile=consumer.lock -s os=Linux -s:b os=Linux")
     assert "REV1!!!" in client.out
     assert "REV2!!!" not in client.out
-    assert "win" not in client.out
+    assert "win/0.1" not in client.out
     client.run("install consumer -s os=Linux -s:b os=Linux")
     assert "REV2!!!" in client.out
     assert "REV1!!!" not in client.out
-    assert "win" not in client.out
+    assert "win/0.1" not in client.out
 
 
 @pytest.mark.parametrize("requires", ["requires", "tool_requires"])
diff --git a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
index 032081f95..25601b1df 100644
--- a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
+++ b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
@@ -1,6 +1,7 @@
 import json
 import os
 import platform
+import re
 import textwrap
 
 import pytest
@@ -352,6 +353,67 @@ def test_find_builddirs():
         assert "/path/to/builddir" in contents
 
 
+@pytest.fixture
+def lib_dir_setup():
+    client = TestClient()
+    client.save({"conanfile.py": GenConanfile().with_generator("CMakeToolchain")})
+    client.run("create . --name=onelib --version=1.0")
+    client.run("create . --name=twolib --version=1.0")
+    conanfile = textwrap.dedent("""
+            from conan import ConanFile
+
+            class Conan(ConanFile):
+                requires = "onelib/1.0", "twolib/1.0"
+
+            """)
+    client.save({"conanfile.py": conanfile})
+    client.run("create . --name=dep --version=1.0")
+
+    conanfile = (GenConanfile().with_requires("dep/1.0").with_generator("CMakeToolchain")
+                 .with_settings("os", "arch", "compiler", "build_type"))
+
+    client.save({"conanfile.py": conanfile})
+    return client
+
+def test_runtime_lib_dirs_single_conf(lib_dir_setup):
+    client = lib_dir_setup
+    generator = ""
+    is_windows = platform.system() == "Windows"
+    if is_windows:
+        generator = '-c tools.cmake.cmaketoolchain:generator=Ninja'
+    
+    client.run(f'install . -s build_type=Release {generator}')
+    contents = client.load("conan_toolchain.cmake")
+    pattern_lib_path = r'list\(PREPEND CMAKE_LIBRARY_PATH (.*)\)'
+    pattern_lib_dirs = r'set\(CONAN_RUNTIME_LIB_DIRS (.*) \)'
+
+    # On *nix platforms: the list in `CMAKE_LIBRARY_PATH` 
+    # is the same as `CONAN_RUNTIME_LIB_DIRS`
+    # On windows, it's the same but with `bin` instead of `lib`
+    cmake_library_path = re.search(pattern_lib_path, contents).group(1)
+    conan_runtime_lib_dirs = re.search(pattern_lib_dirs, contents).group(1)
+    lib_path = cmake_library_path.replace("/p/lib", "/p/bin") if is_windows else cmake_library_path
+
+    assert lib_path == conan_runtime_lib_dirs
+
+
+def test_runtime_lib_dirs_multiconf(lib_dir_setup):
+    client = lib_dir_setup
+    generator = ""
+    if platform.system() != "Windows":
+        generator = '-c tools.cmake.cmaketoolchain:generator="Ninja Multi-Config"'
+
+    client.run(f'install . -s build_type=Release {generator}')
+    client.run(f'install . -s build_type=Debug {generator}')
+
+    contents = client.load("conan_toolchain.cmake")
+    pattern_lib_dirs = r"set\(CONAN_RUNTIME_LIB_DIRS ([^)]*)\)"
+    runtime_lib_dirs = re.search(pattern_lib_dirs, contents).group(1)
+
+    assert "<CONFIG:Release>" in runtime_lib_dirs
+    assert "<CONFIG:Debug>" in runtime_lib_dirs
+
+
 @pytest.mark.skipif(platform.system() != "Darwin", reason="Only OSX")
 def test_cmaketoolchain_cmake_system_processor_cross_apple():
     """
@@ -1438,3 +1500,78 @@ def test_toolchain_keep_absolute_paths():
     assert os.path.isabs(user_presets["include"][0])
     presets = json.loads(c.load(user_presets["include"][0]))
     assert os.path.isabs(presets["configurePresets"][0]["toolchainFile"])
+
+
+def test_output_dirs_gnudirs_local_default():
+    # https://github.com/conan-io/conan/issues/14733
+    c = TestClient()
+    conanfile = textwrap.dedent("""
+        from conan import ConanFile
+        from conan.tools.cmake import cmake_layout
+        from conan.tools.files import load
+
+        class Conan(ConanFile):
+            name = "pkg"
+            version = "0.1"
+            settings = "os", "arch", "compiler", "build_type"
+            generators = "CMakeToolchain"
+            def build(self):
+                tc = load(self, "conan_toolchain.cmake")
+                self.output.info(tc)
+        """)
+    c.save({"conanfile.py": conanfile})
+    c.run("create .")
+
+    def _assert_install(out):
+        assert 'set(CMAKE_INSTALL_BINDIR "bin")' in out
+        assert 'set(CMAKE_INSTALL_SBINDIR "bin")' in out
+        assert 'set(CMAKE_INSTALL_LIBEXECDIR "bin")' in out
+        assert 'set(CMAKE_INSTALL_LIBDIR "lib")' in out
+        assert 'set(CMAKE_INSTALL_INCLUDEDIR "include")' in out
+
+    _assert_install(c.out)
+    assert "CMAKE_INSTALL_PREFIX" in c.out
+
+    c.run("build .")
+    _assert_install(c.out)
+    assert "CMAKE_INSTALL_PREFIX" not in c.out
+
+
+def test_output_dirs_gnudirs_local_custom():
+    # https://github.com/conan-io/conan/issues/14733
+    c = TestClient()
+    conanfile = textwrap.dedent("""
+        from conan import ConanFile
+        from conan.tools.cmake import cmake_layout
+        from conan.tools.files import load
+
+        class Conan(ConanFile):
+            name = "pkg"
+            version = "0.1"
+            settings = "os", "arch", "compiler", "build_type"
+            generators = "CMakeToolchain"
+            def layout(self):
+                self.cpp.package.bindirs = ["mybindir"]
+                self.cpp.package.includedirs = ["myincludedir"]
+                self.cpp.package.libdirs = ["mylibdir"]
+
+            def build(self):
+                tc = load(self, "conan_toolchain.cmake")
+                self.output.info(tc)
+        """)
+    c.save({"conanfile.py": conanfile})
+    c.run("create .")
+
+    def _assert_install(out):
+        assert 'set(CMAKE_INSTALL_BINDIR "mybindir")' in out
+        assert 'set(CMAKE_INSTALL_SBINDIR "mybindir")' in out
+        assert 'set(CMAKE_INSTALL_LIBEXECDIR "mybindir")' in out
+        assert 'set(CMAKE_INSTALL_LIBDIR "mylibdir")' in out
+        assert 'set(CMAKE_INSTALL_INCLUDEDIR "myincludedir")' in out
+
+    _assert_install(c.out)
+    assert "CMAKE_INSTALL_PREFIX" in c.out
+
+    c.run("build .")
+    _assert_install(c.out)
+    assert "CMAKE_INSTALL_PREFIX" not in c.out
diff --git a/conans/test/unittests/client/profile_loader/compiler_cppstd_test.py b/conans/test/unittests/client/profile_loader/compiler_cppstd_test.py
index 1cd8ed1ba..360853968 100644
--- a/conans/test/unittests/client/profile_loader/compiler_cppstd_test.py
+++ b/conans/test/unittests/client/profile_loader/compiler_cppstd_test.py
@@ -5,8 +5,8 @@ import unittest
 import yaml
 from jinja2 import Template
 
+from conan.internal.cache.cache import PkgCache
 from conan.internal.cache.home_paths import HomePaths
-from conans.client.cache.cache import ClientCache
 from conans.client.conf import default_settings_yml
 from conans.client.profile_loader import ProfileLoader
 from conans.errors import ConanException
@@ -20,7 +20,7 @@ class SettingsCppStdTests(unittest.TestCase):
 
     def setUp(self):
         self.cache_folder = temp_folder()
-        self.cache = ClientCache(self.cache_folder, ConfDefinition())
+        self.cache = PkgCache(self.cache_folder, ConfDefinition())
         self.home_paths = HomePaths(self.cache_folder)
         save(self.home_paths.profile_plugin_path, "")
 
diff --git a/conans/test/unittests/model/version/test_version_range.py b/conans/test/unittests/model/version/test_version_range.py
index 7f82d625c..da684f08e 100644
--- a/conans/test/unittests/model/version/test_version_range.py
+++ b/conans/test/unittests/model/version/test_version_range.py
@@ -1,8 +1,11 @@
+import textwrap
+
 import pytest
 
 from conans.errors import ConanException
 from conans.model.version import Version
 from conans.model.version_range import VersionRange
+from conans.test.utils.tools import TestClient
 
 values = [
     ['=1.0.0',  [[['=', '1.0.0']]],                   ["1.0.0"],          ["0.1"]],
@@ -42,7 +45,7 @@ values = [
     ['>=1.0.0+2', [[['>=', '1.0.0+2']]], ["1.0.0+2", "1.0.0+3"], ["1.0.0+1"]],
     ['>=1.0.0', [[['>=', '1.0.0-']]], ["1.0.0+2", "1.0.0+3"], []],
     # Build metadata and pre-releases
-    ['>=1.0.0-pre.1+2', [[['>=', '1.0.0-pre.1+2']]], ["1.0.0+1", "1.0.0+2"], ["1.0.0-pre.1+3"]], # excluded 1+3 because is a pre-release!
+    ['>=1.0.0-pre.1+2', [[['>=', '1.0.0-pre.1+2']]], ["1.0.0+1", "1.0.0+2"], ["1.0.0-pre.1+3"]],  # excluded 1+3 because is a pre-release!
     ['>=1.0.0-pre.1+2, include_prerelease=True', [[['>=', '1.0.0-pre.1+2']]], ["1.0.0+1", "1.0.0+2", "1.0.0-pre.1+3"], ["1.0.0-pre.1+1"]],
     ['<1.0.1-pre.1+2', [[['<', '1.0.1-pre.1+2']]], ["1.0.0+1", "1.0.0+2"], ["1.0.0-pre.1+2"]],
     ['<1.0.1-pre.1+2, include_prerelease=True', [[['<', '1.0.1-pre.1+2']]], ["1.0.0+1", "1.0.0+2", "1.0.1-pre.1+1"], ["1.0.1-pre.1+2"]],
@@ -129,3 +132,19 @@ def test_wrong_range_option_syntax(version_range):
     but for now this test ensures we don't change it without realizing"""
     vr = VersionRange(version_range)
     assert all(cs.prerelease for cs in vr.condition_sets)
+
+
+def test_version_range_error_ux():
+    # https://github.com/conan-io/conan/issues/16288
+    c = TestClient()
+    conanfile = textwrap.dedent("""
+        from conan import ConanFile
+        class Pkg(ConanFile):
+            requires = "mydep/[>1.0 < 3]"
+        """)
+    c.save({"conanfile.py": conanfile})
+    c.run("install .", assert_error=True)
+    assert "Recipe 'conanfile' requires 'mydep/[>1.0 < 3]' version-range definition error" in c.out
+    c.run("export . --name=mypkg --version=0.1")
+    c.run("install --requires=mypkg/0.1", assert_error=True)
+    assert "Recipe 'mypkg/0.1' requires 'mydep/[>1.0 < 3]' version-range definition error" in c.out
diff --git a/conans/test/utils/tools.py b/conans/test/utils/tools.py
index 544b960f6..1ac1e4525 100644
--- a/conans/test/utils/tools.py
+++ b/conans/test/utils/tools.py
@@ -25,13 +25,12 @@ from requests.exceptions import HTTPError
 from webtest.app import TestApp
 
 from conan.cli.exit_codes import SUCCESS, ERROR_GENERAL
-from conan.internal.cache.cache import PackageLayout, RecipeLayout
+from conan.internal.cache.cache import PackageLayout, RecipeLayout, PkgCache
 from conan.internal.cache.home_paths import HomePaths
 from conans import REVISIONS
 from conan.api.conan_api import ConanAPI
 from conan.api.model import Remote
 from conan.cli.cli import Cli, _CONAN_INTERNAL_CUSTOM_COMMANDS_PATH
-from conans.client.cache.cache import ClientCache
 from conans.util.env import environment_update
 from conans.errors import NotFoundException, ConanException
 from conans.model.manifest import FileTreeManifest
@@ -442,9 +441,9 @@ class TestClient:
         api = ConanAPI(cache_folder=self.cache_folder)
         global_conf = api.config.global_conf
 
-        class MyCache(ClientCache, HomePaths):  # Temporary class to avoid breaking all tests
+        class MyCache(PkgCache, HomePaths):  # Temporary class to avoid breaking all tests
             def __init__(self, cache_folder):
-                ClientCache.__init__(self, cache_folder, global_conf)
+                PkgCache.__init__(self, cache_folder, global_conf)
                 HomePaths.__init__(self, cache_folder)
 
             @property