Repository: conan-io/conan
Pull request #16289 (2024-05-17 07:21:41)

better version ranges errors
Changelog: Fix: Better error message for incorrect version-ranges definitions
Docs: Omit

Close https://github.com/conan-io/conan/issues/16288



Patch:
diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py
index 4fcfeebc7..1bf351a9f 100644
--- a/conan/api/subapi/cache.py
+++ b/conan/api/subapi/cache.py
@@ -171,12 +171,14 @@ class CacheAPI:
         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
             recipe_layout = cache.get_or_create_ref_layout(ref)  # DB folder entry
             recipe_folder = ref_bundle["recipe_folder"]
-            rel_path = os.path.relpath(recipe_layout.base_folder, cache.cache_folder)
+            rel_path = os.path.relpath(recipe_layout.base_folder, cache_folder)
             rel_path = rel_path.replace("\\", "/")
             # In the case of recipes, they are always "in place", so just checking it
             assert rel_path == recipe_folder, f"{rel_path}!={recipe_folder}"
@@ -189,25 +191,25 @@ class CacheAPI:
                 out.info(f"Restore: {pref} in {unzipped_pkg_folder}")
                 # If the DB folder entry is different to the disk unzipped one, we need to move it
                 # This happens for built (not downloaded) packages in the source "conan cache save"
-                db_pkg_folder = os.path.relpath(pkg_layout.package(), cache.cache_folder)
+                db_pkg_folder = os.path.relpath(pkg_layout.package(), cache_folder)
                 db_pkg_folder = db_pkg_folder.replace("\\", "/")
                 if db_pkg_folder != unzipped_pkg_folder:
                     # If a previous package exists, like a previous restore, then remove it
                     if os.path.exists(pkg_layout.package()):
                         shutil.rmtree(pkg_layout.package())
-                    shutil.move(os.path.join(cache.cache_folder, unzipped_pkg_folder),
+                    shutil.move(os.path.join(cache_folder, unzipped_pkg_folder),
                                 pkg_layout.package())
                     pref_bundle["package_folder"] = db_pkg_folder
                 unzipped_metadata_folder = pref_bundle.get("metadata_folder")
                 if unzipped_metadata_folder:
                     out.info(f"Restore: {pref} metadata in {unzipped_metadata_folder}")
-                    db_metadata_folder = os.path.relpath(pkg_layout.metadata(), cache.cache_folder)
+                    db_metadata_folder = os.path.relpath(pkg_layout.metadata(), cache_folder)
                     db_metadata_folder = db_metadata_folder.replace("\\", "/")
                     if db_metadata_folder != unzipped_metadata_folder:
                         # We need to put the package in the final location in the cache
                         if os.path.exists(pkg_layout.metadata()):
                             shutil.rmtree(pkg_layout.metadata())
-                        shutil.move(os.path.join(cache.cache_folder, unzipped_metadata_folder),
+                        shutil.move(os.path.join(cache_folder, unzipped_metadata_folder),
                                     pkg_layout.metadata())
                         pref_bundle["metadata_folder"] = db_metadata_folder
 
diff --git a/conan/api/subapi/remotes.py b/conan/api/subapi/remotes.py
index da6520695..1a54e61be 100644
--- a/conan/api/subapi/remotes.py
+++ b/conan/api/subapi/remotes.py
@@ -9,6 +9,7 @@ from conan.internal.cache.home_paths import HomePaths
 from conan.internal.conan_app import ConanApp
 from conans.client.rest_client_local_recipe_index import add_local_recipes_index_remote, \
     remove_local_recipes_index_remote
+from conans.client.store.localdb import LocalDB
 from conans.errors import ConanException
 from conans.util.files import save, load
 
@@ -141,10 +142,10 @@ class RemotesAPI:
         removed = _filter(remotes, pattern, only_enabled=False)
         remotes = [r for r in remotes if r not in removed]
         _save(self._remotes_file, remotes)
-        app = ConanApp(self.conan_api)
+        localdb = LocalDB(self.conan_api.cache_folder)
         for remote in removed:
             remove_local_recipes_index_remote(self.conan_api, remote)
-            app.cache.localdb.clean(remote_url=remote.url)
+            localdb.clean(remote_url=remote.url)
         return removed
 
     def update(self, remote_name: str, url=None, secure=None, disabled=None, index=None,
@@ -200,9 +201,9 @@ class RemotesAPI:
 
     def user_info(self, remote: Remote):
         # TODO: Review
-        app = ConanApp(self.conan_api)
+        localdb = LocalDB(self.conan_api.cache_folder)
         user_info = {}
-        user, token, _ = app.cache.localdb.get_login(remote.url)
+        user, token, _ = localdb.get_login(remote.url)
         user_info["name"] = remote.name
         user_info["user_name"] = user
         user_info["authenticated"] = True if token else False
@@ -225,29 +226,29 @@ class RemotesAPI:
 
         :param remote: The ``Remote`` object to logout
         """
-        app = ConanApp(self.conan_api)
+        localdb = LocalDB(self.conan_api.cache_folder)
         # The localdb only stores url + username + token, not remote name, so use URL as key
-        app.cache.localdb.clean(remote_url=remote.url)
+        localdb.clean(remote_url=remote.url)
 
     def user_set(self, remote: Remote, username):
         # TODO: Review
-        app = ConanApp(self.conan_api)
+        localdb = LocalDB(self.conan_api.cache_folder)
         if username == "":
             username = None
-        app.cache.localdb.store(username, token=None, refresh_token=None, remote_url=remote.url)
+        localdb.store(username, token=None, refresh_token=None, remote_url=remote.url)
 
     def user_auth(self, remote: Remote, with_user=False):
         # TODO: Review
         app = ConanApp(self.conan_api)
         if with_user:
-            user, token, _ = app.cache.localdb.get_login(remote.url)
+            user, token, _ = app.localdb.get_login(remote.url)
             if not user:
                 var_name = f"CONAN_LOGIN_USERNAME_{remote.name.replace('-', '_').upper()}"
                 user = os.getenv(var_name, None) or os.getenv("CONAN_LOGIN_USERNAME", None)
             if not user:
                 return
         app.remote_manager.check_credentials(remote)
-        user, token, _ = app.cache.localdb.get_login(remote.url)
+        user, token, _ = app.localdb.get_login(remote.url)
         return user
 
 
diff --git a/conan/api/subapi/upload.py b/conan/api/subapi/upload.py
index f39c3f3f6..f8ad2dc3e 100644
--- a/conan/api/subapi/upload.py
+++ b/conan/api/subapi/upload.py
@@ -47,7 +47,7 @@ class UploadAPI:
         preparator.prepare(package_list, enabled_remotes)
         if metadata != ['']:
             gather_metadata(package_list, app.cache, metadata)
-        signer = PkgSignaturesPlugin(app.cache)
+        signer = PkgSignaturesPlugin(app.cache, app.cache_folder)
         # This might add files entries to package_list with signatures
         signer.sign(package_list)
 
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 8e0b58159..2e42564e4 100644
--- a/conan/internal/cache/cache.py
+++ b/conan/internal/cache/cache.py
@@ -33,10 +33,6 @@ class DataCache:
         path = os.path.realpath(os.path.join(self._base_folder, relative_path))
         return path
 
-    @property
-    def base_folder(self):
-        return self._base_folder
-
     @staticmethod
     def _short_hash_path(h):
         """:param h: Unicode text to reduce"""
diff --git a/conan/internal/conan_app.py b/conan/internal/conan_app.py
index 3ce4eed48..41c6d204c 100644
--- a/conan/internal/conan_app.py
+++ b/conan/internal/conan_app.py
@@ -12,6 +12,7 @@ from conans.client.remote_manager import RemoteManager
 from conans.client.rest.auth_manager import ConanApiAuthManager
 from conans.client.rest.conan_requester import ConanRequester
 from conans.client.rest.rest_client import RestApiClientFactory
+from conans.client.store.localdb import LocalDB
 
 
 class CmdWrapper:
@@ -29,11 +30,12 @@ class CmdWrapper:
 
 
 class ConanFileHelpers:
-    def __init__(self, requester, cmd_wrapper, global_conf, cache):
+    def __init__(self, requester, cmd_wrapper, global_conf, cache, home_folder):
         self.requester = requester
         self.cmd_wrapper = cmd_wrapper
         self.global_conf = global_conf
         self.cache = cache
+        self.home_folder = home_folder
 
 
 class ConanApp:
@@ -52,16 +54,19 @@ class ConanApp:
         # To handle remote connections
         rest_client_factory = RestApiClientFactory(self.requester, global_conf)
         # Wraps RestApiClient to add authentication support (same interface)
-        auth_manager = ConanApiAuthManager(rest_client_factory, self.cache, global_conf)
+        self.localdb = LocalDB(cache_folder)
+        auth_manager = ConanApiAuthManager(rest_client_factory, cache_folder, self.localdb,
+                                           global_conf)
         # Handle remote connections
-        self.remote_manager = RemoteManager(self.cache, auth_manager)
+        self.remote_manager = RemoteManager(self.cache, auth_manager, cache_folder)
 
         self.proxy = ConanProxy(self, conan_api.local.editable_packages)
         self.range_resolver = RangeResolver(self, global_conf, conan_api.local.editable_packages)
 
         self.pyreq_loader = PyRequireLoader(self, global_conf)
         cmd_wrap = CmdWrapper(home_paths.wrapper_path)
-        conanfile_helpers = ConanFileHelpers(self.requester, cmd_wrap, global_conf, self.cache)
+        conanfile_helpers = ConanFileHelpers(self.requester, cmd_wrap, global_conf, self.cache,
+                                             self.cache_folder)
         self.loader = ConanFileLoader(self.pyreq_loader, conanfile_helpers)
 
     @staticmethod
diff --git a/conan/tools/cmake/toolchain/blocks.py b/conan/tools/cmake/toolchain/blocks.py
index adb338e5b..1855f8f60 100644
--- a/conan/tools/cmake/toolchain/blocks.py
+++ b/conan/tools/cmake/toolchain/blocks.py
@@ -969,11 +969,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 +997,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
index 8c494cf02..5fea2ccac 100644
--- a/conans/client/cache/cache.py
+++ b/conans/client/cache/cache.py
@@ -2,26 +2,22 @@ import os
 from typing import List
 
 from conan.internal.cache.cache import DataCache, RecipeLayout, PackageLayout
-from conans.client.store.localdb import LocalDB
 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
 
-LOCALDB = ".conan.db"
-
 
 # TODO: Rename this to ClientHome
-class ClientCache(object):
+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):
-        self.cache_folder = cache_folder
         # paths
         self._store_folder = global_conf.get("core.cache:storage_path") or \
-                             os.path.join(self.cache_folder, "p")
+                             os.path.join(cache_folder, "p")
 
         try:
             mkdir(self._store_folder)
@@ -126,8 +122,3 @@ class ClientCache(object):
     @property
     def store(self):
         return self._store_folder
-
-    @property
-    def localdb(self):
-        localdb_filename = os.path.join(self.cache_folder, LOCALDB)
-        return LocalDB(localdb_filename)
diff --git a/conans/client/conf/config_installer.py b/conans/client/conf/config_installer.py
index 82f39daa0..4cb240fa6 100644
--- a/conans/client/conf/config_installer.py
+++ b/conans/client/conf/config_installer.py
@@ -8,7 +8,7 @@ from contextlib import contextmanager
 from conan.api.output import ConanOutput
 from conans.client.downloaders.file_downloader import FileDownloader
 from conans.errors import ConanException
-from conans.util.files import mkdir, rmdir, remove, unzip, chdir, load
+from conans.util.files import mkdir, rmdir, remove, unzip, chdir
 from conans.util.runners import detect_runner
 
 
@@ -44,8 +44,8 @@ def _hide_password(resource):
 
 
 @contextmanager
-def tmp_config_install_folder(cache):
-    tmp_folder = os.path.join(cache.cache_folder, "tmp_config_install")
+def tmp_config_install_folder(cache_folder):
+    tmp_folder = os.path.join(cache_folder, "tmp_config_install")
     # necessary for Mac OSX, where the temp folders in /var/ are symlinks to /private/var/
     tmp_folder = os.path.abspath(tmp_folder)
     rmdir(tmp_folder)
@@ -56,24 +56,24 @@ def tmp_config_install_folder(cache):
         rmdir(tmp_folder)
 
 
-def _process_git_repo(config, cache):
+def _process_git_repo(config, cache_folder):
     output = ConanOutput()
     output.info("Trying to clone repo: %s" % config.uri)
-    with tmp_config_install_folder(cache) as tmp_folder:
+    with tmp_config_install_folder(cache_folder) as tmp_folder:
         with chdir(tmp_folder):
             args = config.args or ""
             ret, out = detect_runner('git clone "{}" . {}'.format(config.uri, args))
             if ret != 0:
                 raise ConanException("Can't clone repo: {}".format(out))
             output.info("Repo cloned!")
-        _process_folder(config, tmp_folder, cache)
+        _process_folder(config, tmp_folder, cache_folder)
 
 
-def _process_zip_file(config, zippath, cache, tmp_folder, first_remove=False):
+def _process_zip_file(config, zippath, cache_folder, tmp_folder, first_remove=False):
     unzip(zippath, tmp_folder)
     if first_remove:
         os.unlink(zippath)
-    _process_folder(config, tmp_folder, cache)
+    _process_folder(config, tmp_folder, cache_folder)
 
 
 def _filecopy(src, filename, dst):
@@ -91,20 +91,20 @@ def _filecopy(src, filename, dst):
     shutil.copyfile(src, dst)
 
 
-def _process_file(directory, filename, config, cache, folder):
+def _process_file(directory, filename, config, cache_folder, folder):
     output = ConanOutput()
     if filename == "settings.yml":
         output.info("Installing settings.yml")
-        _filecopy(directory, filename, cache.cache_folder)
+        _filecopy(directory, filename, cache_folder)
     elif filename == "remotes.json":
         output.info("Defining remotes from remotes.json")
-        _filecopy(directory, filename, cache.cache_folder)
+        _filecopy(directory, filename, cache_folder)
     else:
         relpath = os.path.relpath(directory, folder)
         if config.target_folder:
-            target_folder = os.path.join(cache.cache_folder, config.target_folder, relpath)
+            target_folder = os.path.join(cache_folder, config.target_folder, relpath)
         else:
-            target_folder = os.path.join(cache.cache_folder, relpath)
+            target_folder = os.path.join(cache_folder, relpath)
 
         if os.path.isfile(target_folder):  # Existed as a file and now should be a folder
             remove(target_folder)
@@ -114,7 +114,7 @@ def _process_file(directory, filename, config, cache, folder):
         _filecopy(directory, filename, target_folder)
 
 
-def _process_folder(config, folder, cache, ignore=None):
+def _process_folder(config, folder, cache_folder, ignore=None):
     if not os.path.isdir(folder):
         raise ConanException("No such directory: '%s'" % str(folder))
     if config.source_folder:
@@ -127,12 +127,12 @@ def _process_folder(config, folder, cache, ignore=None):
         for f in files:
             rel_path = os.path.relpath(os.path.join(root, f), folder)
             if not conanignore.matches(rel_path):
-                _process_file(root, f, config, cache, folder)
+                _process_file(root, f, config, cache_folder, folder)
 
 
-def _process_download(config, cache, requester):
+def _process_download(config, cache_folder, requester):
     output = ConanOutput()
-    with tmp_config_install_folder(cache) as tmp_folder:
+    with tmp_config_install_folder(cache_folder) as tmp_folder:
         output.info("Trying to download  %s" % _hide_password(config.uri))
         path = urlsplit(config.uri).path
         filename = os.path.basename(path)
@@ -141,7 +141,7 @@ def _process_download(config, cache, requester):
             downloader = FileDownloader(requester=requester)
             downloader.download(url=config.uri, file_path=zippath, verify_ssl=config.verify_ssl,
                                 retry=1)
-            _process_zip_file(config, zippath, cache, tmp_folder, first_remove=True)
+            _process_zip_file(config, zippath, cache_folder, tmp_folder, first_remove=True)
         except Exception as e:
             raise ConanException("Error while installing config from %s\n%s" % (config.uri, str(e)))
 
@@ -181,22 +181,23 @@ def _is_compressed_file(filename):
 
 def configuration_install(app, uri, verify_ssl, config_type=None,
                           args=None, source_folder=None, target_folder=None, ignore=None):
-    cache, requester = app.cache, app.requester
+    requester = app.requester
+    cache_folder = app.cache_folder
     config = _ConfigOrigin(uri, config_type, verify_ssl, args, source_folder, target_folder)
     try:
         if config.type == "git":
-            _process_git_repo(config, cache)
+            _process_git_repo(config, cache_folder)
         elif config.type == "dir":
-            _process_folder(config, config.uri, cache, ignore)
+            _process_folder(config, config.uri, cache_folder, ignore)
         elif config.type == "file":
             if _is_compressed_file(config.uri):
-                with tmp_config_install_folder(cache) as tmp_folder:
-                    _process_zip_file(config, config.uri, cache, tmp_folder)
+                with tmp_config_install_folder(cache_folder) as tmp_folder:
+                    _process_zip_file(config, config.uri, cache_folder, tmp_folder)
             else:
                 dirname, filename = os.path.split(config.uri)
-                _process_file(dirname, filename, config, cache, dirname)
+                _process_file(dirname, filename, config, cache_folder, dirname)
         elif config.type == "url":
-            _process_download(config, cache, requester=requester)
+            _process_download(config, cache_folder, requester=requester)
         else:
             raise ConanException("Unable to process config install: %s" % config.uri)
     except Exception as e:
diff --git a/conans/client/downloaders/caching_file_downloader.py b/conans/client/downloaders/caching_file_downloader.py
index 4b7914f60..24155ad11 100644
--- a/conans/client/downloaders/caching_file_downloader.py
+++ b/conans/client/downloaders/caching_file_downloader.py
@@ -21,7 +21,7 @@ class SourcesCachingDownloader:
         helpers = getattr(conanfile, "_conan_helpers")
         self._global_conf = helpers.global_conf
         self._file_downloader = FileDownloader(helpers.requester, scope=conanfile.display_name)
-        self._cache = helpers.cache
+        self._home_folder = helpers.home_folder
         self._output = conanfile.output
         self._conanfile = conanfile
 
@@ -49,7 +49,7 @@ class SourcesCachingDownloader:
         something is found.
         """
         # We are going to use the download_urls definition for backups
-        download_cache_folder = download_cache_folder or HomePaths(self._cache.cache_folder).default_sources_backup_folder
+        download_cache_folder = download_cache_folder or HomePaths(self._home_folder).default_sources_backup_folder
         # regular local shared download cache, not using Conan backup sources servers
         backups_urls = backups_urls or ["origin"]
         if download_cache_folder and not os.path.isabs(download_cache_folder):
diff --git a/conans/client/graph/graph_binaries.py b/conans/client/graph/graph_binaries.py
index 46ef40517..333893d2a 100644
--- a/conans/client/graph/graph_binaries.py
+++ b/conans/client/graph/graph_binaries.py
@@ -25,11 +25,12 @@ class GraphBinariesAnalyzer(object):
 
     def __init__(self, conan_app, global_conf):
         self._cache = conan_app.cache
+        self._home_folder = conan_app.cache_folder
         self._global_conf = global_conf
         self._remote_manager = conan_app.remote_manager
         # These are the nodes with pref (not including PREV) that have been evaluated
         self._evaluated = {}  # {pref: [nodes]}
-        compat_folder = HomePaths(self._cache.cache_folder).compatibility_plugin_path
+        compat_folder = HomePaths(conan_app.cache_folder).compatibility_plugin_path
         self._compatibility = BinaryCompatibility(compat_folder)
 
     @staticmethod
@@ -335,7 +336,7 @@ class GraphBinariesAnalyzer(object):
         config_mode = self._global_conf.get("core.package_id:config_mode", default=None)
         if config_mode is None:
             return
-        config_version_file = HomePaths(self._cache.cache_folder).config_version_path
+        config_version_file = HomePaths(self._home_folder).config_version_path
         try:
             config_refs = json.loads(load(config_version_file))["config_version"]
             result = OrderedDict()
@@ -350,7 +351,7 @@ class GraphBinariesAnalyzer(object):
         except Exception as e:
             raise ConanException(f"core.package_id:config_mode defined, but error while loading "
                                  f"'{os.path.basename(config_version_file)}'"
-                                 f" file in cache: {self._cache.cache_folder}: {e}")
+                                 f" file in cache: {self._home_folder}: {e}")
         return RequirementsInfo(result)
 
     def _evaluate_package_id(self, node, config_version):
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)
diff --git a/conans/client/pkg_sign.py b/conans/client/pkg_sign.py
index b1addaaa1..40207d362 100644
--- a/conans/client/pkg_sign.py
+++ b/conans/client/pkg_sign.py
@@ -7,9 +7,9 @@ from conans.util.files import mkdir
 
 
 class PkgSignaturesPlugin:
-    def __init__(self, cache):
+    def __init__(self, cache, home_folder):
         self._cache = cache
-        signer = HomePaths(cache.cache_folder).sign_plugin_path
+        signer = HomePaths(home_folder).sign_plugin_path
         if os.path.isfile(signer):
             mod, _ = load_python_file(signer)
             # TODO: At the moment it requires both methods sign and verify, but that might be relaxed
diff --git a/conans/client/remote_manager.py b/conans/client/remote_manager.py
index 4feb4a094..9827b83d7 100644
--- a/conans/client/remote_manager.py
+++ b/conans/client/remote_manager.py
@@ -22,14 +22,15 @@ from conans.util.files import mkdir, tar_extract
 
 class RemoteManager:
     """ Will handle the remotes to get recipes, packages etc """
-    def __init__(self, cache, auth_manager):
+    def __init__(self, cache, auth_manager, home_folder):
         self._cache = cache
         self._auth_manager = auth_manager
-        self._signer = PkgSignaturesPlugin(cache)
+        self._signer = PkgSignaturesPlugin(cache, home_folder)
+        self._home_folder = home_folder
 
     def _local_folder_remote(self, remote):
         if remote.remote_type == LOCAL_RECIPES_INDEX:
-            return RestApiClientLocalRecipesIndex(remote, self._cache)
+            return RestApiClientLocalRecipesIndex(remote, self._home_folder)
 
     def check_credentials(self, remote):
         self._call_remote(remote, "check_credentials")
diff --git a/conans/client/rest/auth_manager.py b/conans/client/rest/auth_manager.py
index 603cc5177..cf1ab8dde 100644
--- a/conans/client/rest/auth_manager.py
+++ b/conans/client/rest/auth_manager.py
@@ -23,11 +23,11 @@ LOGIN_RETRIES = 3
 
 class ConanApiAuthManager(object):
 
-    def __init__(self, rest_client_factory, cache, global_conf):
+    def __init__(self, rest_client_factory, cache_folder, localdb, global_conf):
         self._rest_client_factory = rest_client_factory
-        self._localdb = cache.localdb
+        self._localdb = localdb
         self._global_conf = global_conf
-        self._cache_folder = cache.cache_folder
+        self._cache_folder = cache_folder
 
     def call_rest_api_method(self, remote, method_name, *args, **kwargs):
         """Handles AuthenticationException and request user to input a user and a password"""
diff --git a/conans/client/rest_client_local_recipe_index.py b/conans/client/rest_client_local_recipe_index.py
index 5aa92690f..36d3b516e 100644
--- a/conans/client/rest_client_local_recipe_index.py
+++ b/conans/client/rest_client_local_recipe_index.py
@@ -54,9 +54,9 @@ class RestApiClientLocalRecipesIndex:
     a local folder assuming the conan-center-index repo layout
     """
 
-    def __init__(self, remote, cache):
+    def __init__(self, remote, home_folder):
         self._remote = remote
-        local_recipes_index_path = HomePaths(cache.cache_folder).local_recipes_index_path
+        local_recipes_index_path = HomePaths(home_folder).local_recipes_index_path
         local_recipes_index_path = os.path.join(local_recipes_index_path, remote.name)
         local_recipes_index_path = os.path.join(local_recipes_index_path, ".conan")
         repo_folder = self._remote.url
diff --git a/conans/client/store/localdb.py b/conans/client/store/localdb.py
index 8525256b6..9b95d8701 100644
--- a/conans/client/store/localdb.py
+++ b/conans/client/store/localdb.py
@@ -7,21 +7,22 @@ from conans.errors import ConanException
 from conans.util import encrypt
 
 REMOTES_USER_TABLE = "users_remotes"
+LOCALDB = ".conan.db"
 
 _localdb_encryption_key = os.environ.pop('CONAN_LOGIN_ENCRYPTION_KEY', None)
 
 
 class LocalDB:
 
-    def __init__(self, dbfile):
-        self.dbfile = dbfile
+    def __init__(self, dbfolder):
+        self.dbfile = os.path.join(dbfolder, LOCALDB)
         self.encryption_key = _localdb_encryption_key
 
         # Create the database file if it doesn't exist
-        if not os.path.exists(dbfile):
-            par = os.path.dirname(dbfile)
+        if not os.path.exists(self.dbfile):
+            par = os.path.dirname(self.dbfile)
             os.makedirs(par, exist_ok=True)
-            open(dbfile, 'w').close()
+            open(self.dbfile, 'w').close()
 
             with self._connect() as connection:
                 try:
@@ -30,7 +31,7 @@ class LocalDB:
                                    "(remote_url TEXT UNIQUE, user TEXT, "
                                    "token TEXT, refresh_token TEXT)" % REMOTES_USER_TABLE)
                 except Exception as e:
-                    message = f"Could not initialize local sqlite database {dbfile}"
+                    message = f"Could not initialize local sqlite database {self.dbfile}"
                     raise ConanException(message, e)
 
     def _encode(self, value):


Test patch:
diff --git a/conans/test/functional/command/config_install_test.py b/conans/test/functional/command/config_install_test.py
index 7ee9c34bd..43114c6b5 100644
--- a/conans/test/functional/command/config_install_test.py
+++ b/conans/test/functional/command/config_install_test.py
@@ -587,7 +587,7 @@ class ConfigInstallSchedTest(unittest.TestCase):
         assert os.path.basename(client.cache_folder) == DEFAULT_CONAN_HOME
         client.run('config install "%s/.git" --type git' % self.folder)
         conf = load(client.cache.new_config_path)
-        dirs = os.listdir(client.cache.cache_folder)
+        dirs = os.listdir(client.cache_folder)
         assert ".git" not in dirs
 
 
diff --git a/conans/test/functional/layout/test_editable_cmake.py b/conans/test/functional/layout/test_editable_cmake.py
index 16568c844..7fd00b57f 100644
--- a/conans/test/functional/layout/test_editable_cmake.py
+++ b/conans/test/functional/layout/test_editable_cmake.py
@@ -13,8 +13,7 @@ from conans.test.utils.tools import TestClient
 def editable_cmake(generator, build_folder=None):
     c = TestClient()
     if generator is not None:
-        c.save({"global.conf": "tools.cmake.cmaketoolchain:generator={}".format(generator)},
-               path=os.path.join(c.cache.cache_folder))
+        c.save_home({"global.conf": "tools.cmake.cmaketoolchain:generator={}".format(generator)})
     c.save(pkg_cmake("dep", "0.1"), path=os.path.join(c.current_folder, "dep"))
     c.save(pkg_cmake_app("pkg", "0.1", requires=["dep/0.1"]),
            path=os.path.join(c.current_folder, "pkg"))
@@ -102,8 +101,7 @@ def editable_cmake_exe(generator):
     # --install folder
     c = TestClient()
     if generator is not None:
-        c.save({"global.conf": "tools.cmake.cmaketoolchain:generator={}".format(generator)},
-               path=os.path.join(c.cache.cache_folder))
+        c.save_home({"global.conf": "tools.cmake.cmaketoolchain:generator={}".format(generator)})
     c.save(pkg_cmake("dep", "0.1", exe=True), path=os.path.join(c.current_folder, "dep"))
 
     def build_dep():
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/command/user_test.py b/conans/test/integration/command/user_test.py
index 5fd2b1893..b81b517bd 100644
--- a/conans/test/integration/command/user_test.py
+++ b/conans/test/integration/command/user_test.py
@@ -5,6 +5,7 @@ import unittest
 from collections import OrderedDict
 from datetime import timedelta
 
+from conans.client.store.localdb import LocalDB
 from conans.test.utils.tools import TestClient, TestServer
 from conans.util.env import environment_update
 
@@ -55,7 +56,7 @@ class UserTest(unittest.TestCase):
         client.run('remote set-user default john')
         self.assertIn("Changed user of remote 'default' from 'None' (anonymous) to 'john'",
                       client.out)
-        localdb = client.cache.localdb
+        localdb = LocalDB(client.cache_folder)
         self.assertEqual(('john', None, None), localdb.get_login(test_server.fake_url))
 
         client.run('remote set-user default will')
@@ -86,7 +87,7 @@ class UserTest(unittest.TestCase):
         client.run('remote logout default')
         self.assertIn("Changed user of remote 'default' from 'admin' (authenticated) "
                       "to 'None' (anonymous)", client.out)
-        localdb = client.cache.localdb
+        localdb = LocalDB(client.cache_folder)
         self.assertEqual((None, None, None), localdb.get_login(test_server.fake_url))
         client.run('remote list-users')
         assert 'default:\n  No user' in client.out
@@ -335,10 +336,11 @@ def test_user_removed_remote_removed():
     c = TestClient(default_server_user=True)
     server_url = c.servers["default"].fake_url
     c.run("remote login default admin -p password")
-    login = c.cache.localdb.get_login(server_url)
+    localdb = LocalDB(c.cache_folder)
+    login = localdb.get_login(server_url)
     assert login[0] == "admin"
     c.run("remote remove default")
-    login = c.cache.localdb.get_login(server_url)
+    login = localdb.get_login(server_url)
     assert login == (None, None, None)
 
 
diff --git a/conans/test/integration/package_id/test_config_package_id.py b/conans/test/integration/package_id/test_config_package_id.py
index 9932497dd..1dfcb7953 100644
--- a/conans/test/integration/package_id/test_config_package_id.py
+++ b/conans/test/integration/package_id/test_config_package_id.py
@@ -34,7 +34,7 @@ def test_config_package_id(config_version, mode, result):
 
 def test_error_config_package_id():
     c = TestClient()
-    save(c.cache.global_conf_path, f"core.package_id:config_mode=minor_mode")
+    c.save_home({"global.conf": "core.package_id:config_mode=minor_mode"})
     c.save({"conanfile.py": GenConanfile("pkg", "0.1")})
     c.run("create .", assert_error=True)
     assert "ERROR: core.package_id:config_mode defined, " \
diff --git a/conans/test/integration/remote/auth_test.py b/conans/test/integration/remote/auth_test.py
index cba84dc9b..cd1b9df6a 100644
--- a/conans/test/integration/remote/auth_test.py
+++ b/conans/test/integration/remote/auth_test.py
@@ -5,6 +5,7 @@ import unittest
 
 from requests.models import Response
 
+from conans.client.store.localdb import LocalDB
 from conans.errors import AuthenticationException
 from conans.model.recipe_ref import RecipeReference
 from conans.paths import CONANFILE
@@ -206,7 +207,8 @@ def test_token_expired():
     c.save({"conanfile.py": GenConanfile()})
     c.run("create . --name=pkg --version=0.1 --user=user --channel=stable")
     c.run("upload * -r=default -c")
-    user, token, _ = c.cache.localdb.get_login(server.fake_url)
+    localdb = LocalDB(c.cache_folder)
+    user, token, _ = localdb.get_login(server.fake_url)
     assert user == "admin"
     assert token is not None
 
@@ -214,10 +216,10 @@ def test_token_expired():
     time.sleep(3)
     c.users = {}
     conan_conf = "core:non_interactive=True"
-    c.save({"global.conf": conan_conf}, path=c.cache.cache_folder)
+    c.save_home({"global.conf": conan_conf})
     c.run("remove * -c")
     c.run("install --requires=pkg/0.1@user/stable")
-    user, token, _ = c.cache.localdb.get_login(server.fake_url)
+    user, token, _ = localdb.get_login(server.fake_url)
     assert user == "admin"
     assert token is None
 
diff --git a/conans/test/integration/remote/rest_api_test.py b/conans/test/integration/remote/rest_api_test.py
index 94bdaf5e7..324a673e5 100644
--- a/conans/test/integration/remote/rest_api_test.py
+++ b/conans/test/integration/remote/rest_api_test.py
@@ -24,7 +24,6 @@ from conans.test.utils.tools import get_free_port
 from conans.util.files import md5, save
 
 
-@pytest.mark.slow
 @pytest.mark.rest_api
 class RestApiTest(unittest.TestCase):
     """Open a real server (sockets) to test rest_api function."""
@@ -46,15 +45,13 @@ class RestApiTest(unittest.TestCase):
                 client_factory = RestApiClientFactory(requester=requester,
                                                       config=config)
                 localdb = LocalDBMock()
-                cache = Mock()
-                cache.localdb = localdb
 
                 mocked_user_input = UserInput(non_interactive=False)
                 mocked_user_input.get_username = Mock(return_value="private_user")
                 mocked_user_input.get_password = Mock(return_value="private_pass")
 
                 # FIXME: Missing mock
-                cls.auth_manager = ConanApiAuthManager(client_factory, cache, config)
+                cls.auth_manager = ConanApiAuthManager(client_factory, temp_folder(), localdb, config)
                 cls.remote = Remote("myremote", "http://127.0.0.1:%s" % str(cls.server.port), True,
                                     True)
                 cls.auth_manager._authenticate(cls.remote, user="private_user",
diff --git a/conans/test/integration/remote/token_refresh_test.py b/conans/test/integration/remote/token_refresh_test.py
index 2d57d6b1c..7679d60b5 100644
--- a/conans/test/integration/remote/token_refresh_test.py
+++ b/conans/test/integration/remote/token_refresh_test.py
@@ -1,7 +1,6 @@
 import unittest
 
 import mock
-from mock import Mock
 
 from conan.api.model import Remote
 from conans.client.rest.auth_manager import ConanApiAuthManager
@@ -81,10 +80,7 @@ class TestTokenRefresh(unittest.TestCase):
         config = ConfDefinition()
         self.rest_client_factory = RestApiClientFactory(requester, config=config)
         self.localdb = LocalDBMock()
-        cache = Mock()
-        cache.localdb = self.localdb
-        cache.cache_folder = temp_folder()
-        self.auth_manager = ConanApiAuthManager(self.rest_client_factory, cache, config)
+        self.auth_manager = ConanApiAuthManager(self.rest_client_factory, temp_folder(), self.localdb, config)
         self.remote = Remote("myremote", "myurl", True, True)
         self.ref = RecipeReference.loads("lib/1.0@conan/stable#myreciperev")
 
diff --git a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
index 032081f95..a9b108d83 100644
--- a/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
+++ b/conans/test/integration/toolchains/cmake/test_cmaketoolchain.py
@@ -1438,3 +1438,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/conf/config_installer/test_install_folder.py b/conans/test/unittests/client/conf/config_installer/test_install_folder.py
index 47a7fe319..77a8efdae 100644
--- a/conans/test/unittests/client/conf/config_installer/test_install_folder.py
+++ b/conans/test/unittests/client/conf/config_installer/test_install_folder.py
@@ -1,10 +1,8 @@
-# coding=utf-8
-
 import unittest
 import os
 
 from conans.client.conf.config_installer import tmp_config_install_folder
-from conans.test.utils.tools import TestClient
+from conans.test.utils.test_files import temp_folder
 
 
 class InstallFolderTests(unittest.TestCase):
@@ -15,11 +13,11 @@ class InstallFolderTests(unittest.TestCase):
         tmp_config_install_folder must create the same folder, but all items must be exclude when a
         new folder is created.
         """
-        client = TestClient()
+        cache_folder = temp_folder()
 
-        with tmp_config_install_folder(client.cache) as tmp_folder_first:
+        with tmp_config_install_folder(cache_folder) as tmp_folder_first:
             temp_file = os.path.join(tmp_folder_first, "foobar.txt")
             open(temp_file, "w+")
-            with tmp_config_install_folder(client.cache) as tmp_folder_second:
+            with tmp_config_install_folder(cache_folder) as tmp_folder_second:
                 self.assertEqual(tmp_folder_first, tmp_folder_second)
                 self.assertFalse(os.path.exists(temp_file))
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/mocks.py b/conans/test/utils/mocks.py
index cb538224b..6c1a160ef 100644
--- a/conans/test/utils/mocks.py
+++ b/conans/test/utils/mocks.py
@@ -103,7 +103,7 @@ class ConanFileMock(ConanFile):
         self.win_bash = None
         self.command = None
         self._commands = []
-        self._conan_helpers = ConanFileHelpers(None, None, self.conf, None)
+        self._conan_helpers = ConanFileHelpers(None, None, self.conf, None, None)
 
     def run(self, *args, **kwargs):
         self.command = args[0]