Repository: conan-io/conan
Pull request #18012 (2025-03-24 23:04:41)

explore version range patterns
Changelog: Feature: Add version-ranges patterns defined with ``[1.2.3.4.*]`` with the ``*`` at the end of the string.
Docs: https://github.com/conan-io/docs/pull/XXXX

Close https://github.com/conan-io/conan/issues/18010


Patch:
diff --git a/conan/api/model/list.py b/conan/api/model/list.py
index c7149e1b2..520d221fa 100644
--- a/conan/api/model/list.py
+++ b/conan/api/model/list.py
@@ -3,9 +3,9 @@ import json
 import os
 from json import JSONDecodeError
 
+from conan.api.model import RecipeReference, PkgReference
 from conan.errors import ConanException
 from conan.internal.errors import NotFoundException
-from conan.api.model import RecipeReference, PkgReference
 from conan.internal.model.version_range import VersionRange
 from conans.client.graph.graph import RECIPE_EDITABLE, RECIPE_CONSUMER, RECIPE_PLATFORM, \
     RECIPE_VIRTUAL, BINARY_SKIP, BINARY_MISSING, BINARY_INVALID
@@ -36,6 +36,10 @@ class MultiPackagesList:
         for k, v in other.lists.items():
             self.lists.setdefault(k, PackagesList()).merge(v)
 
+    def keep_outer(self, other):
+        for namespace, other_pkg_list in other.lists.items():
+            self.lists.get(namespace, PackagesList()).keep_outer(other_pkg_list)
+
     @staticmethod
     def load(file):
         try:
@@ -55,19 +59,30 @@ class MultiPackagesList:
         return pkglist
 
     @staticmethod
-    def load_graph(graphfile, graph_recipes=None, graph_binaries=None):
+    def load_graph(graphfile, graph_recipes=None, graph_binaries=None, context=None):
         if not os.path.isfile(graphfile):
             raise ConanException(f"Graph file not found: {graphfile}")
         try:
+            base_context = context.split("-")[0] if context else None
             graph = json.loads(load(graphfile))
-            return MultiPackagesList._define_graph(graph, graph_recipes, graph_binaries)
+            mpkglist =  MultiPackagesList._define_graph(graph, graph_recipes, graph_binaries,
+                                                        context=base_context)
+            if context == "build-only":
+                host = MultiPackagesList._define_graph(graph, graph_recipes, graph_binaries,
+                                                       context="host")
+                mpkglist.keep_outer(host)
+            elif context == "host-only":
+                build = MultiPackagesList._define_graph(graph, graph_recipes, graph_binaries,
+                                                        context="build")
+                mpkglist.keep_outer(build)
+            return mpkglist
         except JSONDecodeError as e:
             raise ConanException(f"Graph file invalid JSON: {graphfile}\n{e}")
         except Exception as e:
             raise ConanException(f"Graph file broken: {graphfile}\n{e}")
 
     @staticmethod
-    def _define_graph(graph, graph_recipes=None, graph_binaries=None):
+    def _define_graph(graph, graph_recipes=None, graph_binaries=None, context=None):
         pkglist = MultiPackagesList()
         cache_list = PackagesList()
         if graph_recipes is None and graph_binaries is None:
@@ -79,6 +94,9 @@ class MultiPackagesList:
 
         pkglist.lists["Local Cache"] = cache_list
         for node in graph["graph"]["nodes"].values():
+            if context and node['context'] != context:
+                continue
+
             # We need to add the python_requires too
             python_requires = node.get("python_requires")
             if python_requires is not None:
@@ -142,6 +160,14 @@ class PackagesList:
             return d
         recursive_dict_update(self.recipes, other.recipes)
 
+    def keep_outer(self, other):
+        if not self.recipes:
+            return
+
+        for ref, info in other.recipes.items():
+            if self.recipes.get(ref, {}) == info:
+                self.recipes.pop(ref)
+
     def split(self):
         """
         Returns a list of PackageList, splitted one per reference.
diff --git a/conan/cli/commands/list.py b/conan/cli/commands/list.py
index da032afc9..addfb6848 100644
--- a/conan/cli/commands/list.py
+++ b/conan/cli/commands/list.py
@@ -9,7 +9,6 @@ from conan.cli.formatters.list import list_packages_html
 from conan.errors import ConanException
 from conans.util.dates import timestamp_to_str
 
-
 # Keep them so we don't break other commands that import them, but TODO: Remove later
 remote_color = Color.BRIGHT_BLUE
 recipe_name_color = Color.GREEN
@@ -225,6 +224,9 @@ def list(conan_api: ConanAPI, parser, *args):
     parser.add_argument("-g", "--graph", help="Graph json file")
     parser.add_argument("-gb", "--graph-binaries", help="Which binaries are listed", action="append")
     parser.add_argument("-gr", "--graph-recipes", help="Which recipes are listed", action="append")
+    parser.add_argument("-gc", "--graph-context", help="Filter the results by the given context",
+                        default=None, action=OnceArgument,
+                        choices=["build", "host", "build-only", "host-only"])
     parser.add_argument('--lru', default=None, action=OnceArgument,
                         help="List recipes and binaries that have not been recently used. Use a"
                              " time limit like --lru=5d (days) or --lru=4w (weeks),"
@@ -248,7 +250,8 @@ def list(conan_api: ConanAPI, parser, *args):
 
     if args.graph:
         graphfile = make_abs_path(args.graph)
-        pkglist = MultiPackagesList.load_graph(graphfile, args.graph_recipes, args.graph_binaries)
+        pkglist = MultiPackagesList.load_graph(graphfile, args.graph_recipes, args.graph_binaries,
+                                               context=args.graph_context)
     else:
         ref_pattern = ListPattern(args.pattern, rrev=None, prev=None)
         if not ref_pattern.package_id and (args.package_query or args.filter_profile or
diff --git a/conan/internal/api/install/generators.py b/conan/internal/api/install/generators.py
index fe553891f..946133a7d 100644
--- a/conan/internal/api/install/generators.py
+++ b/conan/internal/api/install/generators.py
@@ -242,15 +242,20 @@ def relativize_paths(conanfile, placeholder):
     return abs_base_path, new_path
 
 
-def relativize_path(path, conanfile, placeholder):
-    abs_base_path, new_path = relativize_paths(conanfile, placeholder)
-    if abs_base_path is None:
+def relativize_path(path, conanfile, placeholder, normalize=True):
+    """
+    relative path from the "generators_folder" to "path", asuming the root file, like
+    conan_toolchain.cmake will be directly in the "generators_folder"
+    """
+    base_common_folder = conanfile.folders._base_generators # noqa
+    if not base_common_folder or not os.path.isabs(base_common_folder):
         return path
-    if path.startswith(abs_base_path):
-        path = path.replace(abs_base_path, new_path, 1)
-    else:
-        abs_base_path = abs_base_path.replace("\\", "/")
-        new_path = new_path.replace("\\", "/")
-        if path.startswith(abs_base_path):
-            path = path.replace(abs_base_path, new_path, 1)
+    try:
+        common_path = os.path.commonpath([path, conanfile.generators_folder, base_common_folder])
+        if common_path == base_common_folder:
+            rel_path = os.path.relpath(path, conanfile.generators_folder)
+            new_path = os.path.join(placeholder, rel_path)
+            return new_path.replace("\\", "/") if normalize else new_path
+    except ValueError:  # In case the unit in Windows is different, path cannot be made relative
+        pass
     return path
diff --git a/conan/internal/model/conf.py b/conan/internal/model/conf.py
index 335d7c08c..3e3e95959 100644
--- a/conan/internal/model/conf.py
+++ b/conan/internal/model/conf.py
@@ -610,18 +610,16 @@ class ConfDefinition:
         else:
             self._pattern_confs[pattern] = conf
 
-    def rebase_conf_definition(self, other):
+    def rebase_conf_definition(self, global_conf):
         """
         for taking the new global.conf and composing with the profile [conf]
-        :type other: ConfDefinition
+        :type global_conf: ConfDefinition
         """
-        for pattern, conf in other._pattern_confs.items():
-            new_conf = conf.filter_user_modules()  # Creates a copy, filtered
-            existing = self._pattern_confs.get(pattern)
-            if existing:
-                existing.compose_conf(new_conf)
-            else:
-                self._pattern_confs[pattern] = new_conf
+        result = ConfDefinition()
+        result._pattern_confs = global_conf._pattern_confs.copy()
+        result.update_conf_definition(self)
+        self._pattern_confs = result._pattern_confs
+        return
 
     def update(self, key, value, profile=False, method="define"):
         """
diff --git a/conan/internal/model/version_range.py b/conan/internal/model/version_range.py
index 0b75fb27a..aabd3c744 100644
--- a/conan/internal/model/version_range.py
+++ b/conan/internal/model/version_range.py
@@ -84,7 +84,11 @@ class _ConditionSet:
 
         operator = expression[0]
         if operator not in (">", "<", "^", "~", "="):
-            operator = "="
+            if expression[-1] == "*":  # Handle patterns like "1.2.*"
+                operator = "*"
+                expression = expression[:-1]
+            else:
+                operator = "="
             index = 0
         else:
             index = 1
@@ -142,6 +146,9 @@ class _ConditionSet:
             elif condition.operator == "=":
                 if not version == condition.version:
                     return False
+            elif condition.operator == "*":
+                if not str(version).startswith(str(condition.version)):
+                    return False
         return True
 
 
diff --git a/conan/tools/files/files.py b/conan/tools/files/files.py
index 66a2a43a9..21ee2f1a0 100644
--- a/conan/tools/files/files.py
+++ b/conan/tools/files/files.py
@@ -356,15 +356,14 @@ def unzip(conanfile, filename, destination=".", keep_permissions=False, pattern=
 def untargz(filename, destination=".", pattern=None, strip_root=False, extract_filter=None):
     # NOT EXPOSED at `conan.tools.files` but used in tests
     import tarfile
-    with FileProgress(filename, msg="Uncompressing") as fileobj, tarfile.TarFile.open(fileobj=fileobj, mode='r:*') as tarredgzippedFile:
+    with tarfile.TarFile.open(filename, mode='r:*') as tarredgzippedFile:
         f = getattr(tarfile, f"{extract_filter}_filter", None) if extract_filter else None
         tarredgzippedFile.extraction_filter = f or (lambda member_, _: member_)
         if not pattern and not strip_root:
-            # Simple case: extract everything
             tarredgzippedFile.extractall(destination)
         else:
-            # Read members one by one and process them
             common_folder = None
+            members = []
             for member in tarredgzippedFile:
                 if pattern and not fnmatch(member.name, pattern):
                     continue  # Skip files that don’t match the pattern
@@ -380,7 +379,6 @@ def untargz(filename, destination=".", pattern=None, strip_root=False, extract_f
                             raise ConanException("Can't untar a tgz containing files in the root with strip_root enabled")
                     if not name.startswith(common_folder):
                         raise ConanException("The tgz file contains more than 1 folder in the root")
-
                     # Adjust the member's name for extraction
                     member.name = name[len(common_folder) + 1:]
                     member.path = member.name
@@ -388,9 +386,10 @@ def untargz(filename, destination=".", pattern=None, strip_root=False, extract_f
                         # https://github.com/conan-io/conan/issues/11065
                         member.linkpath = member.linkpath[len(common_folder) + 1:].replace("\\", "/")
                         member.linkname = member.linkpath
-                # Extract file one by one, avoiding `extractall()` with members parameter:
-                # This will avoid a first whole file read resulting in a performant improvement around 25%
-                tarredgzippedFile.extract(member, path=destination)
+                # Let's gather each member
+                members.append(member)
+            tarredgzippedFile.extractall(destination, members=members)
+
 
 def check_sha1(conanfile, file_path, signature):
     """
diff --git a/conan/tools/microsoft/msbuilddeps.py b/conan/tools/microsoft/msbuilddeps.py
index 58b0d87dd..ad2c89c84 100644
--- a/conan/tools/microsoft/msbuilddeps.py
+++ b/conan/tools/microsoft/msbuilddeps.py
@@ -185,7 +185,7 @@ class MSBuildDeps:
         root_folder = escape_path(root_folder)
         # Make the root_folder relative to the generated conan_vars_xxx.props file
         relative_root_folder = relativize_path(root_folder, self._conanfile,
-                                               "$(MSBuildThisFileDirectory)")
+                                               "$(MSBuildThisFileDirectory)", normalize=False)
 
         bin_dirs = join_paths(cpp_info.bindirs)
         res_dirs = join_paths(cpp_info.resdirs)
@@ -336,9 +336,14 @@ class MSBuildDeps:
                 public_deps = []  # To store the xml dependencies/file names
                 for required_pkg, required_comp in comp_info.parsed_requires():
                     if required_pkg is not None:  # Points to a component of a different package
-                        if required_pkg in pkg_deps:  # The transitive dep might have been skipped
-                            public_deps.append(required_pkg if required_pkg == required_comp
-                                               else "{}_{}".format(required_pkg, required_comp))
+                        try:
+                            required = pkg_deps[required_pkg]
+                        except KeyError:  # The transitive dep might have been skipped
+                            required = None
+                        if required:  # The transitive dep might have been skipped
+                            required_name = required.ref.name
+                            public_deps.append(required_name if required_pkg == required_comp
+                                               else "{}_{}".format(required_name, required_comp))
                     else:  # Points to a component of same package
                         public_deps.append("{}_{}".format(dep_name, required_comp))
                 public_deps = [self._get_valid_xml_format(d) for d in public_deps]
diff --git a/conan/tools/sbom/cyclonedx.py b/conan/tools/sbom/cyclonedx.py
index c370073e2..bca937a33 100644
--- a/conan/tools/sbom/cyclonedx.py
+++ b/conan/tools/sbom/cyclonedx.py
@@ -33,9 +33,9 @@ def cyclonedx_1_4(conanfile, name=None, add_build=False, add_tests=False, **kwar
 
     name_default = getattr(graph.root.ref, "name", False) or "conan-sbom"
     name_default += f"/{graph.root.ref.version}" if bool(getattr(graph.root.ref, "version", False)) else ""
-    components = [node for node in graph.nodes if (node.context == "host" or add_build) and (not node.test or add_tests)]
+    nodes = [node for node in graph.nodes if (node.context == "host" or add_build) and (not node.test or add_tests)]
     if has_special_root_node:
-        components = components[1:]
+        nodes = nodes[1:]
 
     dependencies = []
     if has_special_root_node:
@@ -43,7 +43,7 @@ def cyclonedx_1_4(conanfile, name=None, add_build=False, add_tests=False, **kwar
                 "dependsOn": [f"pkg:conan/{d.dst.name}@{d.dst.ref.version}?rref={d.dst.ref.revision}"
                               for d in graph.root.dependencies]}
         dependencies.append(deps)
-    for c in components:
+    for c in nodes:
         deps = {"ref": f"pkg:conan/{c.name}@{c.ref.version}?rref={c.ref.revision}"}
         dep = [d for d in c.dependencies if (d.dst.context == "host" or add_build) and (not d.dst.test or add_tests)]
 
@@ -63,23 +63,23 @@ def cyclonedx_1_4(conanfile, name=None, add_build=False, add_tests=False, **kwar
 
     sbom_cyclonedx_1_4 = {
         **({"components": [{
-            "author": "Conan",
-            "bom-ref": special_id if has_special_root_node else f"pkg:conan/{c.name}@{c.ref.version}?rref={c.ref.revision}",
-            "description": c.conanfile.description,
+            "author": node.conanfile.author or "Unknown",
+            "bom-ref": special_id if has_special_root_node else f"pkg:conan/{node.name}@{node.ref.version}?rref={node.ref.revision}",
+            "description": node.conanfile.description,
             **({"externalReferences": [{
                 "type": "website",
-                "url": c.conanfile.homepage
-            }]} if c.conanfile.homepage else {}),
-            **({"licenses": _calculate_licenses(c)} if c.conanfile.license else {}),
-            "name": c.name,
-            "purl": f"pkg:conan/{c.name}@{c.ref.version}",
+                "url": node.conanfile.homepage
+            }]} if node.conanfile.homepage else {}),
+            **({"licenses": _calculate_licenses(node)} if node.conanfile.license else {}),
+            "name": node.name,
+            "purl": f"pkg:conan/{node.name}@{node.ref.version}",
             "type": "library",
-            "version": str(c.ref.version),
-        } for c in components]} if components else {}),
+            "version": str(node.ref.version),
+        } for node in nodes]} if nodes else {}),
         **({"dependencies": dependencies} if dependencies else {}),
         "metadata": {
             "component": {
-                "author": "Conan",
+                "author": conanfile.author or "Unknown",
                 "bom-ref": special_id if has_special_root_node else f"pkg:conan/{conanfile.name}@{conanfile.ref.version}?rref={conanfile.ref.revision}",
                 "name": name if name else name_default,
                 "type": "library"
diff --git a/conans/client/downloaders/download_cache.py b/conans/client/downloaders/download_cache.py
index f38b32746..6201b3d0b 100644
--- a/conans/client/downloaders/download_cache.py
+++ b/conans/client/downloaders/download_cache.py
@@ -100,9 +100,10 @@ class DownloadCache:
                 raise ConanException(f"Missing metadata file for backup source {blob_path}")
             metadata = json.loads(load(metadata_path))
             refs = metadata["references"]
-            # unknown entries are not uploaded at this moment unless no package_list is passed
             for ref, urls in refs.items():
-                if not has_excluded_urls(urls) and (package_list is None or ref in all_refs):
+                if not has_excluded_urls(urls) and (not only_upload
+                                                    or package_list is None
+                                                    or ref in all_refs):
                     files_to_upload.append(metadata_path)
                     files_to_upload.append(blob_path)
                     break
diff --git a/conans/client/graph/graph.py b/conans/client/graph/graph.py
index b5037c271..1c8efc7fd 100644
--- a/conans/client/graph/graph.py
+++ b/conans/client/graph/graph.py
@@ -418,13 +418,6 @@ class DepsGraph(object):
 
         return result
 
-    def build_time_nodes(self):
-        """ return all the nodes in the graph that are build-requires (either directly or
-        transitively). Nodes that are both in requires and build_requires will not be returned.
-        This is used just for output purposes, printing deps, HTML graph, etc.
-        """
-        return [n for n in self.nodes if n.context == CONTEXT_BUILD]
-
     def report_graph_error(self):
         if self.error:
             raise self.error
diff --git a/conans/client/graph/proxy.py b/conans/client/graph/proxy.py
index 66a56c283..72b927516 100644
--- a/conans/client/graph/proxy.py
+++ b/conans/client/graph/proxy.py
@@ -106,7 +106,7 @@ class ConanProxy:
                 continue
             output.info(f"Checking remote: {remote.name}")
             try:
-                if self._legacy_update and need_update:
+                if self._legacy_update and need_update and reference.revision is None:
                     if not getattr(ConanProxy, "update_policy_legacy_warning", None):
                         ConanProxy.update_policy_legacy_warning = True
                         ConanOutput().warning("The 'core:update_policy' conf is deprecated and will "


Test patch:
diff --git a/.ci/docker/conan-tests b/.ci/docker/conan-tests
index 88b979455..ef26a5530 100644
--- a/.ci/docker/conan-tests
+++ b/.ci/docker/conan-tests
@@ -16,6 +16,7 @@ ENV PY36=3.6.15 \
     CMAKE_3_19=/usr/share/cmake-3.19.7/bin/cmake \
     CMAKE_3_23=/usr/share/cmake-3.23.5/bin/cmake \
     CMAKE_3_27=/usr/share/cmake-3.27.9/bin/cmake \
+    CMAKE_4_0=/usr/share/cmake-4.0.0-rc3/bin/cmake \
     GCC_9=/usr/bin/gcc-9 \
     GXX_9=/usr/bin/g++-9 \
     GCC_11=/usr/bin/gcc-11 \
@@ -120,12 +121,17 @@ RUN wget https://github.com/Kitware/CMake/releases/download/v3.15.7/cmake-3.15.7
     tar -xvzf cmake-3.23.5-Linux-x86_64.tar.gz && mv cmake-3.23.5-linux-x86_64/ /usr/share/cmake-3.23.5 && \
     wget https://github.com/Kitware/CMake/releases/download/v3.27.9/cmake-3.27.9-Linux-x86_64.tar.gz   && \
     tar -xvzf cmake-3.27.9-Linux-x86_64.tar.gz && mv cmake-3.27.9-linux-x86_64/ /usr/share/cmake-3.27.9 && \
+    wget https://cmake.org/files/v4.0/cmake-4.0.0-rc3-linux-x86_64.tar.gz   && \
+    tar -xvzf cmake-4.0.0-rc3-linux-x86_64.tar.gz && mv cmake-4.0.0-rc3-linux-x86_64/ /usr/share/cmake-4.0.0-rc3 && \
     update-alternatives --install /usr/bin/cmake cmake $CMAKE_3_15 10 && \
     update-alternatives --install /usr/bin/cmake cmake $CMAKE_3_16 20 && \
     update-alternatives --install /usr/bin/cmake cmake $CMAKE_3_17 30 && \
     update-alternatives --install /usr/bin/cmake cmake $CMAKE_3_19 40 && \
     update-alternatives --install /usr/bin/cmake cmake $CMAKE_3_23 50 && \
-    update-alternatives --install /usr/bin/cmake cmake $CMAKE_3_27 60
+    update-alternatives --install /usr/bin/cmake cmake $CMAKE_3_27 60 && \
+    update-alternatives --install /usr/bin/cmake cmake $CMAKE_4_0 70 && \
+    # set CMake 3.15 as default
+    update-alternatives --set cmake $CMAKE_3_15
 
 RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 10 && \
     update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 30 && \
diff --git a/.github/workflows/osx-tests.yml b/.github/workflows/osx-tests.yml
index 1942d955c..c02a16596 100644
--- a/.github/workflows/osx-tests.yml
+++ b/.github/workflows/osx-tests.yml
@@ -58,6 +58,7 @@ jobs:
             ~/Applications/CMake/3.19.7
             ~/Applications/CMake/3.23.5
             ~/Applications/CMake/3.27.9
+            ~/Applications/CMake/4.0.0-rc3
             ~/Applications/bazel/6.5.0
             ~/Applications/bazel/7.4.1
             ~/Applications/bazel/8.0.0
@@ -86,7 +87,7 @@ jobs:
         if: steps.cache-tools.outputs.cache-hit != 'true'
         run: |
           set -e
-          CMAKE_PRECOMP_VERSIONS=("3.19.7" "3.23.5" "3.27.9")
+          CMAKE_PRECOMP_VERSIONS=("3.19.7" "3.23.5" "3.27.9" "4.0.0-rc3")
           for version in "${CMAKE_PRECOMP_VERSIONS[@]}"; do
             echo "Downloading and installing precompiled universal CMake version ${version}..."
             wget -q --no-check-certificate https://cmake.org/files/v${version%.*}/cmake-${version}-macos-universal.tar.gz
@@ -146,6 +147,7 @@ jobs:
             ~/Applications/CMake/3.19.7
             ~/Applications/CMake/3.23.5
             ~/Applications/CMake/3.27.9
+            ~/Applications/CMake/4.0.0-rc3
             ~/Applications/bazel/6.5.0
             ~/Applications/bazel/7.4.1
             ~/Applications/bazel/8.0.0
diff --git a/.github/workflows/win-tests.yml b/.github/workflows/win-tests.yml
index 75e21a273..ee3d55f0c 100644
--- a/.github/workflows/win-tests.yml
+++ b/.github/workflows/win-tests.yml
@@ -161,6 +161,7 @@ jobs:
             C:\tools\cmake\3.19.7
             C:\tools\cmake\3.23.5
             C:\tools\cmake\3.27.9
+            C:\tools\cmake\4.0.0-rc3
             C:\tools\bazel\6.5.0
             C:\tools\bazel\7.4.1
             C:\tools\bazel\8.0.0
@@ -169,7 +170,7 @@ jobs:
       - name: Install CMake versions
         if: steps.cache-tools.outputs.cache-hit != 'true'
         run: |
-          $CMAKE_BUILD_VERSIONS = "3.15.7", "3.19.7", "3.23.5", "3.27.9"
+          $CMAKE_BUILD_VERSIONS = "3.15.7", "3.19.7", "3.23.5", "3.27.9", "4.0.0-rc3"
           foreach ($version in $CMAKE_BUILD_VERSIONS) {
             Write-Host "Downloading CMake version $version for Windows..."
             $destination = "C:\tools\cmake\$version"
diff --git a/test/conftest.py b/test/conftest.py
index 4583c44fc..a0f6ead5a 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -86,6 +86,11 @@ tools_locations = {
             "path": {'Windows': 'C:/tools/cmake/3.27.9/cmake-3.27.9-windows-x86_64/bin',
                      'Darwin': '/Users/runner/Applications/CMake/3.27.9/bin',
                      'Linux': "/usr/share/cmake-3.27.9/bin"}
+        },
+        "4.0": {
+            "path": {'Windows': 'C:/tools/cmake/4.0.0-rc3/cmake-4.0.0-rc3-windows-x86_64/bin',
+                     'Darwin': '/Users/runner/Applications/CMake/4.0.0-rc3/bin',
+                     'Linux': "/usr/share/cmake-4.0.0-rc3/bin"}
         }
     },
     'ninja': {
diff --git a/test/functional/command/test_install_deploy.py b/test/functional/command/test_install_deploy.py
index faaeb98cf..0a824b06f 100644
--- a/test/functional/command/test_install_deploy.py
+++ b/test/functional/command/test_install_deploy.py
@@ -511,9 +511,10 @@ class TestRuntimeDeployer:
         expected = sorted(["pkga.so", "pkgb.so", "pkga.dll"])
         assert sorted(os.listdir(os.path.join(c.current_folder, "myruntime"))) == expected
 
+
 @pytest.mark.parametrize("symlink, expected",
                          [(True, ["libfoo.so.0.1.0", "libfoo.so.0", "libfoo.so"]),
-                          (False, ["libfoo.so.0.1.0",])])
+                          (False, ["libfoo.so.0.1.0"])])
 def test_runtime_deploy_symlinks(symlink, expected):
     """ The deployer runtime_deploy should preserve symlinks when deploying shared libraries
     """
@@ -531,7 +532,7 @@ def test_runtime_deploy_symlinks(symlink, expected):
                        os.symlink(src="libfoo.so.0", dst="libfoo.so")
            """)
     c.save({"foo/conanfile.py": conanfile,
-            "foo/lib/libfoo.so.0.1.0": "",})
+            "foo/lib/libfoo.so.0.1.0": ""})
     c.run("export-pkg foo/ --name=foo --version=0.1.0")
     c.run(f"install --requires=foo/0.1.0 --deployer=runtime_deploy --deployer-folder=output -c:a tools.deployer:symlinks={symlink}")
 
@@ -565,3 +566,38 @@ def test_deployer_errors():
     c.run("install . --deployer=mydeploy2.py", assert_error=True)
     # The error message says conanfile, not a big deal still path to file is shown
     assert "ERROR: Unable to load conanfile" in c.out
+
+
+def test_deploy_relative_paths():
+    c = TestClient()
+    consumer = textwrap.dedent("""
+        import os
+        from conan import ConanFile
+        class Consumer(ConanFile):
+            requires = "pkg/0.1"
+            settings = "build_type"
+            os = "build_type"
+            generators = "CMakeDeps"
+            def layout(self):
+                self.folders.build = os.path.join("some/sub/folders")
+                self.folders.generators = os.path.join(self.folders.build, "generators")
+        """)
+    deploy = textwrap.dedent("""
+        import os, shutil
+        def deploy(graph, output_folder):
+            conanfile = graph.root.conanfile
+            output_folder = os.path.join(conanfile.build_folder, "installed")
+            for dep in conanfile.dependencies.values():
+                new_folder = os.path.join(output_folder, dep.ref.name)
+                shutil.copytree(dep.package_folder, new_folder, symlinks=True)
+                dep.set_deploy_folder(new_folder)
+        """)
+    c.save({"pkg/conanfile.py": GenConanfile("pkg", "0.1"),
+            "consumer/conanfile.py": consumer,
+            "mydeploy.py": deploy})
+    c.run("create pkg")
+
+    # If we don't change to another folder, the full_deploy will be recursive and fail
+    c.run("install consumer --build=missing --deployer=mydeploy.py")
+    data = c.load("consumer/some/sub/folders/generators/pkg-release-data.cmake")
+    assert 'set(pkg_PACKAGE_FOLDER_RELEASE "${CMAKE_CURRENT_LIST_DIR}/../installed/pkg")' in data
diff --git a/test/functional/layout/test_editable_cmake_components.py b/test/functional/layout/test_editable_cmake_components.py
index db407274d..70ffe4cec 100644
--- a/test/functional/layout/test_editable_cmake_components.py
+++ b/test/functional/layout/test_editable_cmake_components.py
@@ -2,10 +2,13 @@ import os
 import shutil
 import textwrap
 
+import pytest
+
 from conan.test.assets.sources import gen_function_cpp, gen_function_h
 from conan.test.utils.tools import TestClient
 
 
+@pytest.mark.tool("cmake")
 def test_editable_cmake_components():
     hello_h = gen_function_h(name="hello")
     hello_cpp = gen_function_cpp(name="hello", includes=["hello"])
diff --git a/test/functional/toolchains/cmake/test_cmake_toolchain.py b/test/functional/toolchains/cmake/test_cmake_toolchain.py
index 14ed165c6..76101e05c 100644
--- a/test/functional/toolchains/cmake/test_cmake_toolchain.py
+++ b/test/functional/toolchains/cmake/test_cmake_toolchain.py
@@ -1119,6 +1119,7 @@ def test_resdirs_none_cmake_install():
     assert "Cannot install stuff" in client.out
 
 
+@pytest.mark.tool("cmake")
 def test_cmake_toolchain_vars_when_option_declared():
     t = TestClient()
 
diff --git a/test/functional/toolchains/meson/test_install.py b/test/functional/toolchains/meson/test_install.py
index c839d02f3..2d1aa78ce 100644
--- a/test/functional/toolchains/meson/test_install.py
+++ b/test/functional/toolchains/meson/test_install.py
@@ -97,6 +97,7 @@ class MesonInstall(TestMesonBase):
         """)
 
     @pytest.mark.tool("meson")
+    @pytest.mark.tool("cmake")
     def test_install(self):
         hello_cpp = gen_function_cpp(name="hello")
         hello_h = gen_function_h(name="hello")
diff --git a/test/integration/cache/backup_sources_test.py b/test/integration/cache/backup_sources_test.py
index 2fdb4be52..4bff228e9 100644
--- a/test/integration/cache/backup_sources_test.py
+++ b/test/integration/cache/backup_sources_test.py
@@ -203,6 +203,44 @@ class TestDownloadCacheBackupSources:
         assert f"Sources for {self.file_server.fake_url}/internet/myfile.txt found in remote backup" in self.client.out
         assert f"File {self.file_server.fake_url}/mycompanystorage/mycompanyfile.txt not found in {self.file_server.fake_url}/backups/" in self.client.out
 
+    def test_unknown_handling(self):
+        http_server_base_folder_internet = os.path.join(self.file_server.store, "internet")
+
+        save(os.path.join(http_server_base_folder_internet, "myfile.txt"), "Hello, world!")
+
+        sha256 = "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3"
+        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="{sha256}")
+                            """)
+
+        self.client.save_home(
+            {"global.conf": f"core.sources:download_cache={self.download_cache_folder}\n"
+                            f"core.sources:download_urls=['origin']\n"
+                            f"core.sources:upload_url={self.file_server.fake_url}/backups/"})
+
+        self.client.save({"conanfile.py": conanfile})
+        self.client.run("source .")
+        json_path = os.path.join(self.download_cache_folder, "s", sha256 + ".json")
+        contents = json.loads(load(json_path))
+        assert "unknown" in contents["references"]
+
+        s_folder = os.path.join(self.download_cache_folder, "s")
+        assert len(os.listdir(s_folder)) == 2
+
+        self.client.run("export .")
+        self.client.run("upload * -c -r=default")
+        assert "No backup sources files to upload" in self.client.out
+
+        self.client.run("cache clean --backup-sources")
+        assert len(os.listdir(s_folder)) == 0
+
     def test_download_origin_first(self):
         http_server_base_folder_internet = os.path.join(self.file_server.store, "internet")
 
diff --git a/test/integration/command/install/install_update_test.py b/test/integration/command/install/install_update_test.py
index 450d771be..18c238f92 100644
--- a/test/integration/command/install/install_update_test.py
+++ b/test/integration/command/install/install_update_test.py
@@ -233,3 +233,15 @@ class TestUpdateOldPolicy:
         c.run("remove * -c")
         c.run("graph info --requires=pkg/0.1 --update -cc core:update_policy=legacy")
         assert f"pkg/0.1#{rev2} - Downloaded (r1)" in c.out
+
+    def test_lockfile(self):
+        # https://github.com/conan-io/conan/issues/18006
+        c = TestClient(default_server_user=True, light=True)  # needs server to fail
+        c.save_home({"global.conf": "core:update_policy=legacy"})
+        c.save({"dep/conanfile.py": GenConanfile("dep", "0.1"),
+                "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_python_requires("dep/0.1")})
+        c.run("create dep")
+        c.run("lock create pkg --lockfile-out base.lock --build=* --update")
+        c.run("lock create pkg --lockfile base.lock --lockfile-out full.lock --build=* --update")
+        # it doesn't crash
+        assert "Generated lockfile" in c.out
diff --git a/test/integration/command/list/test_combined_pkglist_flows.py b/test/integration/command/list/test_combined_pkglist_flows.py
index 659069232..e1e8d281e 100644
--- a/test/integration/command/list/test_combined_pkglist_flows.py
+++ b/test/integration/command/list/test_combined_pkglist_flows.py
@@ -382,3 +382,49 @@ class TestListRemove:
         client.run(f"list *:* {remote}")
         assert "zli/1.0.0" in client.out
         assert "zlib/1.0.0@user/channel" in client.out
+
+
+class TestListGraphContext:
+
+    @pytest.mark.parametrize("context, refs, not_refs", [
+        ("build", ["cmake/1.0", "m4/1.0", "protobuf/1.0"], []),
+        ("build-only", ["cmake/1.0", "m4/1.0"], ["protobuf/1.0"]),
+        ("host", ["zlib/1.0", "protobuf/1.0"], []),
+        ("host-only", ["zlib/1.0"], ["protobuf/1.0"])
+    ])
+    def test_list_graph_context(self, context, refs, not_refs):
+        c = TestClient(default_server_user=True, light=True)
+        c.save({
+            "zlib/conanfile.py": GenConanfile("zlib", "1.0").with_settings("os"),
+            "cmake/conanfile.py": GenConanfile("cmake", "1.0").with_settings("os"),
+            "m4/conanfile.py": GenConanfile("m4", "1.0").with_settings("os"),
+            "protobuf/conanfile.py": GenConanfile("protobuf", "1.0").with_settings("os"),
+            "app/conanfile.py": GenConanfile("app", "1.0").with_settings("os")
+            .with_requires("zlib/1.0", "protobuf/1.0")
+            .with_tool_requires("cmake/1.0", "m4/1.0", "protobuf/1.0")})
+        c.run("create zlib")
+        c.run("create protobuf")
+        c.run("create cmake")
+        c.run("create m4")
+        c.run("create app --format=json", redirect_stdout="graph.json")
+        # Now, let's filter the pkgs
+        c.run(f"list --graph=graph.json --graph-context={context} --format=json",
+              redirect_stdout="pkglist.json")
+        content = json.loads(c.load("pkglist.json"))
+        for ref in refs:
+            assert content["Local Cache"][ref]["revisions"]
+        for ref in not_refs:
+            assert content["Local Cache"].get(ref) is None
+        # Let's upload all the packages
+        c.run(f"list --graph=graph.json --format=json", redirect_stdout="pkglist.json")
+        c.run("upload --list=pkglist.json -r=default")
+        c.run("remove * -c")
+        c.run("create app --format=json", redirect_stdout="graph.json")
+        # Now, let's filter the pkgs
+        c.run(f"list --graph=graph.json --graph-context={context} -r=default --format=json",
+              redirect_stdout="pkglist.json")
+        content = json.loads(c.load("pkglist.json"))
+        for ref in refs:
+            assert content["default"][ref]["revisions"]
+        for ref in not_refs:
+            assert content["default"].get(ref) is None
diff --git a/test/integration/graph/test_replace_requires.py b/test/integration/graph/test_replace_requires.py
index 4b832ebac..ef6e86bb4 100644
--- a/test/integration/graph/test_replace_requires.py
+++ b/test/integration/graph/test_replace_requires.py
@@ -418,14 +418,15 @@ class TestReplaceRequiresTransitiveGenerators:
             class App(ConanFile):
                 name = "app"
                 version = "0.1"
-                settings = "build_type"
+                settings = "build_type", "arch"
                 requires = "openssl/0.1", {zlib}
                 package_type = "application"
-                generators = "CMakeDeps", "PkgConfigDeps"
+                generators = "CMakeDeps", "PkgConfigDeps", "MSBuildDeps"
             """)
         profile = textwrap.dedent("""
             [settings]
             build_type = Release
+            arch=x86_64
 
             [replace_requires]
             zlib/0.1: zlib-ng/0.1
@@ -454,6 +455,13 @@ class TestReplaceRequiresTransitiveGenerators:
         assert "set_property(TARGET openssl::openssl APPEND PROPERTY INTERFACE_LINK_LIBRARIES\n" \
                '             "$<$<CONFIG:RELEASE>:ZLIB::ZLIB>")' in cmake
 
+        # checking MSBuildDeps
+        zlib_ng_props = c.load("app/conan_zlib-ng.props")
+        assert 'Project="conan_zlib-ng_release_x64.props"' in zlib_ng_props
+        props = c.load("app/conan_openssl_release_x64.props")
+        assert "<Import Condition=\"'$(conan_zlib-ng_props_imported)' != 'True'\"" \
+               " Project=\"conan_zlib-ng.props\"/>" in props
+
     @pytest.mark.parametrize("diamond", [True, False])
     def test_openssl_components(self, diamond):
         c = TestClient()
@@ -490,14 +498,15 @@ class TestReplaceRequiresTransitiveGenerators:
             class App(ConanFile):
                 name = "app"
                 version = "0.1"
-                settings = "build_type"
+                settings = "build_type", "arch"
                 requires = "openssl/0.1", {zlib}
                 package_type = "application"
-                generators = "CMakeDeps", "PkgConfigDeps"
+                generators = "CMakeDeps", "PkgConfigDeps", "MSBuildDeps"
             """)
         profile = textwrap.dedent("""
             [settings]
             build_type = Release
+            arch=x86_64
 
             [replace_requires]
             zlib/0.1: zlib-ng/0.1
@@ -526,6 +535,14 @@ class TestReplaceRequiresTransitiveGenerators:
         assert "set_property(TARGET openssl::crypto APPEND PROPERTY INTERFACE_LINK_LIBRARIES\n" \
                '             "$<$<CONFIG:RELEASE>:ZLIB::ZLIB>")' in cmake
 
+        # checking MSBuildDeps
+        zlib_ng_props = c.load("app/conan_zlib-ng.props")
+        assert 'Project="conan_zlib-ng_release_x64.props"' in zlib_ng_props
+
+        props = c.load("app/conan_openssl_crypto_release_x64.props")
+        assert "<Import Condition=\"'$(conan_zlib-ng_props_imported)' != 'True'\"" \
+               " Project=\"conan_zlib-ng.props\"/>" in props
+
     @pytest.mark.parametrize("diamond", [True, False])
     @pytest.mark.parametrize("explicit_requires", [True, False])
     def test_zlib_components(self, diamond, explicit_requires):
@@ -565,14 +582,15 @@ class TestReplaceRequiresTransitiveGenerators:
             class App(ConanFile):
                 name = "app"
                 version = "0.1"
-                settings = "build_type"
+                settings = "build_type", "arch"
                 requires = "openssl/0.1", {zlib}
                 package_type = "application"
-                generators = "CMakeDeps", "PkgConfigDeps"
+                generators = "CMakeDeps", "PkgConfigDeps", "MSBuildDeps"
             """)
         profile = textwrap.dedent("""
             [settings]
             build_type = Release
+            arch = x86_64
 
             [replace_requires]
             zlib/0.1: zlib-ng/0.1
@@ -604,6 +622,15 @@ class TestReplaceRequiresTransitiveGenerators:
         assert "set_property(TARGET openssl::openssl APPEND PROPERTY INTERFACE_LINK_LIBRARIES\n" \
                '             "$<$<CONFIG:RELEASE>:zlib-ng::zlib-ng>")' in cmake
 
+        # checking MSBuildDeps
+        zlib_ng_props = c.load("app/conan_zlib-ng.props")
+        assert "<Import Condition=\"'$(conan_zlib-ng_myzlib_props_imported)' != 'True'\" " \
+               "Project=\"conan_zlib-ng_myzlib.props\"/" in zlib_ng_props
+
+        props = c.load("app/conan_openssl_release_x64.props")
+        assert "<Import Condition=\"'$(conan_zlib-ng_props_imported)' != 'True'\"" \
+               " Project=\"conan_zlib-ng.props\"/>" in props
+
     @pytest.mark.parametrize("diamond", [True, False])
     @pytest.mark.parametrize("package_requires", [False, True])
     def test_both_components(self, diamond, package_requires):
@@ -645,14 +672,15 @@ class TestReplaceRequiresTransitiveGenerators:
             class App(ConanFile):
                 name = "app"
                 version = "0.1"
-                settings = "build_type"
+                settings = "build_type", "arch"
                 requires = "openssl/0.1", {zlib}
                 package_type = "application"
-                generators = "CMakeDeps", "PkgConfigDeps"
+                generators = "CMakeDeps", "PkgConfigDeps", "MSBuildDeps"
             """)
         profile = textwrap.dedent("""
             [settings]
             build_type = Release
+            arch = x86_64
 
             [replace_requires]
             zlib/0.1: zlib-ng/0.1
@@ -687,3 +715,16 @@ class TestReplaceRequiresTransitiveGenerators:
         else:
             assert "set_property(TARGET openssl::crypto APPEND PROPERTY INTERFACE_LINK_LIBRARIES\n" \
                    '             "$<$<CONFIG:RELEASE>:ZLIB::ZLIB>")' in cmake
+
+        # checking MSBuildDeps
+        zlib_ng_props = c.load("app/conan_zlib-ng.props")
+        assert "<Import Condition=\"'$(conan_zlib-ng_myzlib_props_imported)' != 'True'\" " \
+               "Project=\"conan_zlib-ng_myzlib.props\"/" in zlib_ng_props
+
+        props = c.load("app/conan_openssl_crypto_release_x64.props")
+        if package_requires:
+            assert "<Import Condition=\"'$(conan_zlib-ng_props_imported)' != 'True'\"" \
+                   " Project=\"conan_zlib-ng.props\"/>" in props
+        else:
+            assert "<Import Condition=\"'$(conan_zlib-ng_myzlib_props_imported)' != 'True'\"" \
+                   " Project=\"conan_zlib-ng_myzlib.props\"/>" in props
diff --git a/test/integration/graph/test_skip_build.py b/test/integration/graph/test_skip_build.py
index 44e74e0fb..3eb419a50 100644
--- a/test/integration/graph/test_skip_build.py
+++ b/test/integration/graph/test_skip_build.py
@@ -1,3 +1,4 @@
+import re
 import textwrap
 
 from conan.test.assets.genconanfile import GenConanfile
@@ -46,3 +47,38 @@ def test_graph_skip_build_test():
     c.run("install app -c tools.graph:skip_build=True --build=pkg/*", assert_error=True)
     assert "ERROR: Package pkg/1.0 skipped its test/tool requires with tools.graph:skip_build, " \
            "but was marked to be built " in c.out
+
+
+def test_skip():
+    # https://github.com/conan-io/conan/issues/13439
+    c = TestClient()
+    global_conf = textwrap.dedent("""
+        tools.graph:skip_test=True
+        tools.build:skip_test=True
+        """)
+    c.save_home({"global.conf": global_conf})
+    c.save({"pkga/conanfile.py": GenConanfile("pkga", "1.0.0"),
+            "pkgb/conanfile.py": GenConanfile("pkgb", "1.0.0").with_test_requires("pkga/1.0.0"),
+            "pkgc/conanfile.py": GenConanfile("pkgc", "1.0.0").with_test_requires("pkgb/1.0.0")})
+    c.run("create pkga")
+    c.run("create pkgb")
+
+    # Always skipped
+    c.run("install pkgc")
+    # correct, pkga and pkgb are not in the output at all, they have been skipped
+    assert "pkga" not in c.out
+    assert "pkgb" not in c.out
+
+    # not skipping test-requires
+    c.run("install pkgc -c tools.graph:skip_test=False")
+    # correct, pkga and pkgb are not skipped now
+    assert "pkga" in c.out
+    assert "pkgb" in c.out
+    # but pkga binary is not really necessary
+    assert re.search(r"Skipped binaries(\s*)pkga/1.0.0", c.out)
+
+    # skipping all but the current one
+    c.run("install pkgc -c &:tools.graph:skip_test=False")
+    # correct, only pkga  is skipped now
+    assert "pkga" not in c.out
+    assert "pkgb" in c.out
diff --git a/test/unittests/model/test_conf.py b/test/unittests/model/test_conf.py
index 25dfbdedf..a20b132b0 100644
--- a/test/unittests/model/test_conf.py
+++ b/test/unittests/model/test_conf.py
@@ -76,6 +76,71 @@ def test_conf_rebase(conf_definition):
     assert c.dumps() == result
 
 
+def test_conf_rebase_patterns():
+    text = textwrap.dedent("""\
+        user.company:conf1=value1
+        user.company:conf2=[1, 2, 3]
+        &:user.company:conf1=value2
+        &:user.company:conf2+=[4, 5]
+        """)
+    global_conf = ConfDefinition()
+    global_conf.loads(text)
+    text = textwrap.dedent("""\
+        user.company:conf1=newvalue1
+        user.company:conf2=[6, 7]
+        """)
+    profile = ConfDefinition()
+    profile.loads(text)
+    profile.rebase_conf_definition(global_conf)
+    # TODO: at the moment dumps() doesn't print += append
+    result = textwrap.dedent("""\
+        user.company:conf1=newvalue1
+        user.company:conf2=[6, 7]
+        &:user.company:conf1=value2
+        &:user.company:conf2=[4, 5]
+    """)
+    assert profile.dumps() == result
+
+    text = textwrap.dedent("""\
+        &:user.company:conf1=newvalue1
+        &:user.company:conf2=[6, 7]
+        """)
+    profile = ConfDefinition()
+    profile.loads(text)
+    profile.rebase_conf_definition(global_conf)
+    result = textwrap.dedent("""\
+        user.company:conf1=value1
+        user.company:conf2=[1, 2, 3]
+        &:user.company:conf1=newvalue1
+        &:user.company:conf2=[6, 7]
+       """)
+    assert profile.dumps() == result
+
+    text = "user.company:conf2+=[6, 7]"
+    profile = ConfDefinition()
+    profile.loads(text)
+    profile.rebase_conf_definition(global_conf)
+    result = textwrap.dedent("""\
+        user.company:conf1=value1
+        user.company:conf2=[1, 2, 3, 6, 7]
+        &:user.company:conf1=value2
+        &:user.company:conf2=[4, 5]
+        """)
+    assert profile.dumps() == result
+
+    text = "&:user.company:conf2=+[6, 7]"
+    profile = ConfDefinition()
+    profile.loads(text)
+    profile.rebase_conf_definition(global_conf)
+    result = textwrap.dedent("""\
+        user.company:conf1=value1
+        user.company:conf2=[1, 2, 3]
+        &:user.company:conf1=value2
+        &:user.company:conf2=[6, 7, 4, 5]
+        """)
+    assert profile.dumps() == result
+
+
 def test_conf_error_per_package():
     text = "*:core:non_interactive=minimal"
     c = ConfDefinition()
diff --git a/test/unittests/model/version/test_version_range.py b/test/unittests/model/version/test_version_range.py
index a50cab6e5..a2586c26d 100644
--- a/test/unittests/model/version/test_version_range.py
+++ b/test/unittests/model/version/test_version_range.py
@@ -15,6 +15,7 @@ values = [
     # tilde
     ['~2.5',    [[['>=', '2.5-'], ['<', '2.6-']]],      ["2.5.0", "2.5.3"], ["2.7", "2.6.1"]],
     ['~2.5.1',  [[['>=', '2.5.1-'], ['<', '2.6.0-']]],  ["2.5.1", "2.5.3"], ["2.5", "2.6.1"]],
+    ['~2.5.1.3',  [[['>=', '2.5.1.3-'], ['<', '2.6.0-']]], ["2.5.1.4", "2.5.3"], ["2.5.1", "2.6.1"]],
     ['~1',      [[['>=', '1-'], ['<', '2-']]],          ["1.3", "1.8.1"],   ["0.8", "2.2"]],
     # caret
     ['^1.2',    [[['>=', '1.2-'], ['<', '2.0-']]],      ["1.2.1", "1.51"],  ["1", "2", "2.0.1"]],
@@ -26,6 +27,9 @@ values = [
     # Any
     ['*',       [[[">=", "0.0.0-"]]],                  ["1.0", "a.b"],          []],
     ['',        [[[">=", "0.0.0-"]]],                  ["1.0", "a.b"],          []],
+    # Patterns
+    ['1.2.3.*',       [[["*", "1.2.3."]]],   ["1.2.3.1", "1.2.3.A"], ["1.2.3", "1.2.31", "1.2.4"]],
+    ['1.2.3+4.5.*',      [[["*", "1.2.3+4.5."]]],   ["1.2.3+4.5.6"], ["1.2.3+4.7"]],
     # Unions
     ['1.0.0 || 2.1.3',  [[["=", "1.0.0"]], [["=", "2.1.3"]]],  ["1.0.0", "2.1.3"],  ["2", "1.0.1"]],
     ['>1 <2.0 || ^3.2 ',  [[['>', '1'], ['<', '2.0-']],
diff --git a/test/unittests/util/files/strip_root_extract_test.py b/test/unittests/util/files/strip_root_extract_test.py
index d65bcb367..a3b9910da 100644
--- a/test/unittests/util/files/strip_root_extract_test.py
+++ b/test/unittests/util/files/strip_root_extract_test.py
@@ -272,3 +272,44 @@ class TarExtractPlainTest(unittest.TestCase):
         extract_folder = temp_folder()
         with self.assertRaisesRegex(ConanException, "Can't untar a tgz containing files in the root with strip_root enabled"):
             unzip(ConanFileMock(), tgz_file, destination=extract_folder, strip_root=True)
+
+
+def _compress_root_folder(folder, tgz_path, root_folder_name="root"):
+    """
+    Compress the whole folder adding withing the tar file also the folders. For instance:
+
+    $ tar -tf '/tmp/folder/file.tar.gz'
+    root/
+    root/parent/
+    root/parent/bin/
+    root/parent/bin/file1
+    root/parent/bin/file2
+    """
+    with open(tgz_path, "wb") as tgz_handle:
+        tgz = gzopen_without_timestamps("name", mode="w", fileobj=tgz_handle)
+        files, _ = gather_files(folder)
+        tgz.add(root_folder_name, arcname=os.path.basename(root_folder_name))
+        tgz.close()
+
+
+def test_decompressing_folders_with_different_modes():
+    """
+    When compressed folders come with restrictive permissions, untar() function should not
+    raise a PermissionError.
+
+    Issue related: https://github.com/conan-io/conan/issues/17987
+    """
+    tmp_folder = temp_folder()
+    tgz_folder = temp_folder()
+    tgz_file = os.path.join(tgz_folder, "file.tar.gz")
+    with chdir(tmp_folder):
+        save("root/parent/bin/file1", "contentsfile1")
+        save("root/parent/bin/file2", "contentsfile2")
+        os.chmod(os.path.join("root", "parent"), mode=0o555)
+        os.chmod(os.path.join("root", "parent", "bin"), mode=0o700)
+        _compress_root_folder(tmp_folder, tgz_file, root_folder_name="root")
+
+    # Tgz unzipped regularly
+    extract_folder = temp_folder()
+    # Do not raise any PermissionError
+    untargz(tgz_file, destination=extract_folder, strip_root=True)