Repository: conan-io/conan
Pull request #16602 (2024-07-03 17:13:40)

new test for vendor + export-pkg
Changelog: Omit
Docs: Omit

Close https://github.com/conan-io/conan/issues/16601



Patch:
diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py
index e8504efc1..19a6db3da 100644
--- a/conan/api/subapi/cache.py
+++ b/conan/api/subapi/cache.py
@@ -9,7 +9,7 @@ from conan.api.output import ConanOutput
 from conan.internal.cache.cache import PkgCache
 from conan.internal.cache.home_paths import HomePaths
 from conan.internal.conan_app import ConanApp
-from conan.internal.integrity_check import IntegrityChecker
+from conan.internal.cache.integrity_check import IntegrityChecker
 from conans.client.downloaders.download_cache import DownloadCache
 from conans.errors import ConanException
 from conans.model.package_ref import PkgReference
diff --git a/conan/api/subapi/list.py b/conan/api/subapi/list.py
index a7452ecfe..fecf61ab3 100644
--- a/conan/api/subapi/list.py
+++ b/conan/api/subapi/list.py
@@ -1,14 +1,18 @@
+import os
+from collections import OrderedDict
 from typing import Dict
 
 from conan.api.model import PackagesList
 from conan.api.output import ConanOutput, TimedOutput
+from conan.internal.api.list.query_parse import filter_package_configs
 from conan.internal.conan_app import ConanApp
+from conan.internal.paths import CONANINFO
 from conans.errors import ConanException, NotFoundException
 from conans.model.info import load_binary_info
 from conans.model.package_ref import PkgReference
 from conans.model.recipe_ref import RecipeReference, ref_matches
-from conans.search.search import get_cache_packages_binary_info, filter_packages
 from conans.util.dates import timelimit
+from conans.util.files import load
 
 
 class ListAPI:
@@ -69,7 +73,7 @@ class ListAPI:
         if not remote:
             app = ConanApp(self.conan_api)
             prefs = app.cache.get_package_references(ref)
-            packages = get_cache_packages_binary_info(app.cache, prefs)
+            packages = _get_cache_packages_binary_info(app.cache, prefs)
         else:
             app = ConanApp(self.conan_api)
             if ref.revision == "latest":
@@ -85,7 +89,18 @@ class ListAPI:
         :param query: str like "os=Windows AND (arch=x86 OR compiler=gcc)"
         :return: Dict[PkgReference, PkgConfiguration]
         """
-        return filter_packages(query, pkg_configurations)
+        if query is None:
+            return pkg_configurations
+        try:
+            if "!" in query:
+                raise ConanException("'!' character is not allowed")
+            if "~" in query:
+                raise ConanException("'~' character is not allowed")
+            if " not " in query or query.startswith("not "):
+                raise ConanException("'not' operator is not allowed")
+            return filter_package_configs(pkg_configurations, query)
+        except Exception as exc:
+            raise ConanException("Invalid package query: %s. %s" % (query, exc))
 
     @staticmethod
     def filter_packages_profile(packages, profile, ref):
@@ -361,3 +376,29 @@ class _BinaryDistance:
                 "python_requires": self.python_requires_diff,
                 "confs": self.confs_diff,
                 "explanation": self.explanation()}
+
+
+def _get_cache_packages_binary_info(cache, prefs) -> Dict[PkgReference, dict]:
+    """
+    param package_layout: Layout for the given reference
+    """
+
+    result = OrderedDict()
+
+    for pref in prefs:
+        latest_prev = cache.get_latest_package_reference(pref)
+        pkg_layout = cache.pkg_layout(latest_prev)
+
+        # Read conaninfo
+        info_path = os.path.join(pkg_layout.package(), CONANINFO)
+        if not os.path.exists(info_path):
+            raise ConanException(f"Corrupted package '{pkg_layout.reference}' "
+                                 f"without conaninfo.txt in: {info_path}")
+        conan_info_content = load(info_path)
+        info = load_binary_info(conan_info_content)
+        pref = pkg_layout.reference
+        # The key shoudln't have the latest package revision, we are asking for package configs
+        pref.revision = None
+        result[pkg_layout.reference] = info
+
+    return result
diff --git a/conan/api/subapi/search.py b/conan/api/subapi/search.py
index b70f1aa02..582b753c6 100644
--- a/conan/api/subapi/search.py
+++ b/conan/api/subapi/search.py
@@ -1,5 +1,4 @@
 from conan.internal.conan_app import ConanApp
-from conans.search.search import search_recipes
 
 
 class SearchAPI:
@@ -17,7 +16,7 @@ class SearchAPI:
         if remote:
             refs = app.remote_manager.search_recipes(remote, query)
         else:
-            references = search_recipes(app.cache, query)
+            references = app.cache.search_recipes(query)
             # For consistency with the remote search, we return references without revisions
             # user could use further the API to look for the revisions
             refs = []
diff --git a/conan/cli/commands/graph.py b/conan/cli/commands/graph.py
index 85d7cb510..8f4124744 100644
--- a/conan/cli/commands/graph.py
+++ b/conan/cli/commands/graph.py
@@ -8,6 +8,7 @@ from conan.cli.args import common_graph_args, validate_common_graph_args
 from conan.cli.command import conan_command, conan_subcommand
 from conan.cli.commands.list import prepare_pkglist_compact, print_serial
 from conan.cli.formatters.graph import format_graph_html, format_graph_json, format_graph_dot
+from conan.cli.formatters.graph.build_order_html import format_build_order_html
 from conan.cli.formatters.graph.graph_info_text import format_graph_info
 from conan.cli.printers.graph import print_graph_packages, print_graph_basic
 from conan.errors import ConanException
@@ -62,7 +63,8 @@ def json_build_order(build_order):
     cli_out_write(json.dumps(build_order, indent=4))
 
 
-@conan_subcommand(formatters={"text": cli_build_order, "json": json_build_order})
+@conan_subcommand(formatters={"text": cli_build_order, "json": json_build_order,
+                              "html": format_build_order_html})
 def graph_build_order(conan_api, parser, subparser, *args):
     """
     Compute the build order of a dependency graph.
@@ -129,7 +131,8 @@ def graph_build_order(conan_api, parser, subparser, *args):
     return install_order_serialized
 
 
-@conan_subcommand(formatters={"text": cli_build_order, "json": json_build_order})
+@conan_subcommand(formatters={"text": cli_build_order, "json": json_build_order,
+                              "html": format_build_order_html})
 def graph_build_order_merge(conan_api, parser, subparser, *args):
     """
     Merge more than 1 build-order file.
diff --git a/conan/cli/formatters/graph/build_order_html.py b/conan/cli/formatters/graph/build_order_html.py
new file mode 100644
index 000000000..e785bb01b
--- /dev/null
+++ b/conan/cli/formatters/graph/build_order_html.py
@@ -0,0 +1,272 @@
+from jinja2 import select_autoescape, Template
+from conan.api.output import cli_out_write
+
+build_order_html = r"""
+<html lang="en">
+    <head>
+        <script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.30.0/cytoscape.min.js"
+        integrity="sha512-zHc90yHSbkgx0bvVpDK/nVgxANlE+yKN/jKy91tZ4P/vId8AL7HyjSpZqHmEujWDWNwxYXcfaLdYWjAULl35MQ=="
+        crossorigin="anonymous"
+        referrerpolicy="no-referrer"></script>
+    </head>
+
+    <body>
+        <style>
+            body {
+                font: 14px helvetica neue, helvetica, arial, sans-serif;
+                display: flex;
+                margin: 0;
+                padding: 0;
+                height: 100vh;
+            }
+
+            .sidebar {
+                background: #f9f9f9;
+                border-right: 1px solid #ccc;
+                padding: 20px;
+                box-sizing: border-box;
+                overflow-y: auto;
+                font-size: 14px;
+                width: 440px;
+            }
+
+            .content {
+                flex-grow: 1;
+                display: flex;
+                flex-direction: column;
+                overflow: hidden;
+            }
+
+            #cy {
+                flex-grow: 1;
+            }
+
+            #node-info {
+                margin-top: 20px;
+                background: #f9f9f9;
+                padding: 12px;
+                font-family: monospace;
+                white-space: pre-wrap;
+                font-size: 12px;
+                word-wrap: break-word;
+            }
+
+            .legend {
+                margin-top: 20px;
+                font-size: 14px;
+            }
+
+            .legend-item {
+                display: flex;
+                align-items: center;
+                margin-bottom: 5px;
+            }
+
+            .legend-color {
+                width: 20px;
+                height: 20px;
+                margin-right: 10px;
+            }
+        </style>
+
+        <div class="sidebar">
+            <div class="legend">
+                <div class="legend-item">
+                    <div class="legend-color" style="background-color: #ffff37;"></div>
+                    <span>All packages need to be built</span>
+                </div>
+                <div class="legend-item">
+                    <div class="legend-color" style="background-color: #ff9b28;"></div>
+                    <span>Some packages need to be built</span>
+                </div>
+                <div class="legend-item">
+                    <div class="legend-color" style="background-color: #70c7e6;"></div>
+                    <span>Cache</span>
+                </div>
+                <div class="legend-item">
+                    <div class="legend-color" style="background-color: #79eb8a;"></div>
+                    <span>Download</span>
+                </div>
+                <div class="legend-item">
+                    <div class="legend-color" style="border: 1px solid black; width: 20px; height: 20px;"></div>
+                    <span>Requirements in the <i>host</i> context</span>
+                </div>
+                <div class="legend-item">
+                    <div class="legend-color" style="border: 1px solid black; border-radius: 50%; width: 20px; height: 20px;"></div>
+                    <span>Requirements in the <i>build</i> context</span>
+                </div>
+            </div>
+            <div id="node-info">
+                <p>Click on a node to see details.</p>
+            </div>
+        </div>
+        <div class="content">
+            <div id="cy"></div>
+        </div>
+
+        <script type="text/javascript">
+            document.addEventListener("DOMContentLoaded", function() {
+                var buildOrderData = {{ build_order | tojson }};
+
+                var elements = [];
+                var edges = [];
+
+                var yOffset = 50;
+                var posX = 0;
+                var posY = 0;
+                var columns = 4;
+
+                buildOrderData.forEach((step, stepIndex) => {
+                    var stepId = 'step' + stepIndex;
+                    elements.push({
+                        data: { id: stepId, label: 'Step ' + (stepIndex + 1) },
+                        position: { x: posX, y: posY }
+                    });
+
+                    step.forEach((lib, libIndex) => {
+                        var libId = stepId + '_lib' + libIndex;
+                        var libLabel = lib.ref.split('#')[0];
+
+                        // Determine the type of the library
+                        var libData = Array.isArray(lib.packages) ? lib.packages[0][0] : lib;
+                        var libType = libData.binary || libData.build || libData.cache;
+
+                        var isAllBuild = true;
+                        var isSomeBuild = false;
+                        var shape = 'rectangle';
+                        var borderColor = '#00695C';
+
+                        if (Array.isArray(lib.packages)) {
+                            lib.packages.forEach(pkgArray => {
+                                pkgArray.forEach(pkg => {
+                                    if (pkg.binary === "Build") {
+                                        isSomeBuild = true;
+                                    } else {
+                                        isAllBuild = false;
+                                    }
+                                    if (pkg.context === "build") {
+                                        shape = 'ellipse';
+                                        borderColor = '#0000FF';  // Different border color for build context
+                                    }
+                                });
+                            });
+                        }
+
+                        var nodeColor;
+                        if (isAllBuild) {
+                            nodeColor = "#ffff37"; // Light orange for all build
+                        } else if (isSomeBuild) {
+                            nodeColor = "#ff9b28"; // Yellow for some build
+                        } else if (libType === "Cache") {
+                            nodeColor = "#70c7e6"; // Light green
+                        } else if (libType === "Download") {
+                            nodeColor = "#79eb8a"; // Light blue
+                        } else {
+                            nodeColor = "#FFFFFF"; // Default color
+                        }
+
+                        if (libIndex % columns === 0) {
+                            posX = 0;
+                            posY += yOffset; // move to the next row
+                        }
+
+                        elements.push({
+                            data: { id: libId, parent: stepId, label: libLabel, info: lib, color: nodeColor, shape: shape, borderColor: borderColor },
+                            position: { x: posX + libLabel.length / 2.0 * 12, y: posY }
+                        });
+                        posX += libLabel.length * 12;
+                    });
+
+                    if (stepIndex > 0) {
+                        var prevStepId = 'step' + (stepIndex - 1);
+                        edges.push({ data: { id: prevStepId + '_to_' + stepId, source: prevStepId, target: stepId } });
+                    }
+
+                    posY += yOffset * 2;
+                    posX = 0;
+                });
+
+                var cy = cytoscape({
+                    container: document.getElementById('cy'),
+                    boxSelectionEnabled: false,
+                    style: [
+                        {
+                            selector: 'node[color][shape][borderColor]',
+                            style: {
+                                'shape': 'data(shape)',
+                                'content': 'data(label)',
+                                'text-valign': 'center',
+                                'text-halign': 'center',
+                                'background-color': 'data(color)',
+                                'border-color': 'data(borderColor)',
+                                'border-width': 1,
+                                'width': function(ele) { return ele.data('label').length * 10.5; },
+                                'padding': '5px',
+                                'font-family': 'monospace',
+                                'font-size': '16px'
+                            }
+                        },
+                        {
+                            selector: ':parent',
+                            style: {
+                                'text-valign': 'top',
+                                'text-halign': 'center',
+                                'shape': 'round-rectangle',
+                                'background-opacity': 0.1,
+                                'border-color': '#004D40',
+                                'border-width': 2,
+                                'padding': 10,
+                                'font-family': 'monospace',
+                                'font-size': '16px'
+                            }
+                        },
+                        {
+                            selector: 'edge',
+                            style: {
+                                'curve-style': 'bezier',
+                                'target-arrow-shape': 'triangle',
+                                'line-color': '#004D40',
+                                'target-arrow-color': '#004D40',
+                                'width': 2
+                            }
+                        }
+                    ],
+                    elements: {
+                        nodes: elements,
+                        edges: edges
+                    },
+                    layout: {
+                        name: 'preset',
+                        padding: 5,
+                        fit: true
+                    }
+                });
+
+                // Add click event listener to nodes
+                cy.on('tap', 'node', function(evt){
+                    var node = evt.target;
+                    var info = node.data('info');
+                    var infoHtml = '';
+                    for (var key in info) {
+                        if (info.hasOwnProperty(key)) {
+                            infoHtml += '<p><strong>' + key + ':</strong> ' + JSON.stringify(info[key], null, 2) + '</p>';
+                        }
+                    }
+                    document.getElementById('node-info').innerHTML = infoHtml;
+                });
+            });
+        </script>
+    </body>
+</html>
+"""
+
+def _render_build_order(build_order, template):
+    from conans import __version__ as client_version
+    context = {'build_order': build_order, 'version': client_version, }
+    return template.render(context)
+
+
+def format_build_order_html(build_order):
+    build_order = build_order["order"] if isinstance(build_order, dict) else build_order
+    template = Template(build_order_html, autoescape=select_autoescape(['html', 'xml']))
+    cli_out_write(_render_build_order(build_order, template))
diff --git a/conans/search/__init__.py b/conan/internal/api/list/__init__.py
similarity index 100%
rename from conans/search/__init__.py
rename to conan/internal/api/list/__init__.py
diff --git a/conans/search/query_parse.py b/conan/internal/api/list/query_parse.py
similarity index 65%
rename from conans/search/query_parse.py
rename to conan/internal/api/list/query_parse.py
index 2cc717ab0..8e33e078d 100644
--- a/conans/search/query_parse.py
+++ b/conan/internal/api/list/query_parse.py
@@ -1,14 +1,59 @@
+from collections import OrderedDict
 
-def is_operator(el):
+
+def filter_package_configs(pkg_configurations, query):
+    postfix = _infix_to_postfix(query) if query else []
+    result = OrderedDict()
+    for pref, data in pkg_configurations.items():
+        if _evaluate_postfix_with_info(postfix, data):
+            result[pref] = data
+    return result
+
+
+def _evaluate_postfix_with_info(postfix, binary_info):
+
+    # Evaluate conaninfo with the expression
+
+    def evaluate_info(expression):
+        """Receives an expression like compiler.version="12"
+        Uses conan_vars_info in the closure to evaluate it"""
+        name, value = expression.split("=", 1)
+        value = value.replace("\"", "")
+        return _evaluate(name, value, binary_info)
+
+    return _evaluate_postfix(postfix, evaluate_info)
+
+
+def _evaluate(prop_name, prop_value, binary_info):
+    """
+    Evaluates a single prop_name, prop_value like "os", "Windows" against
+    conan_vars_info.serialize_min()
+    """
+
+    def compatible_prop(setting_value, _prop_value):
+        return (_prop_value == setting_value) or (_prop_value == "None" and setting_value is None)
+
+    # TODO: Necessary to generalize this query evaluation to include all possible fields
+    info_settings = binary_info.get("settings", {})
+    info_options = binary_info.get("options", {})
+
+    if not prop_name.startswith("options."):
+        return compatible_prop(info_settings.get(prop_name), prop_value)
+    else:
+        prop_name = prop_name[len("options."):]
+        return compatible_prop(info_options.get(prop_name), prop_value)
+
+
+def _is_operator(el):
     return el in ["|", "&"]
 
 
 def _parse_expression(subexp):
-    '''Expressions like:
+    """Expressions like:
      compiler.version=12
      compiler="Visual Studio"
      arch="x86"
-     Could be replaced with another one to parse different queries'''
+     Could be replaced with another one to parse different queries """
     ret = ""
     quoted = False
     for char in subexp:
@@ -19,7 +64,7 @@ def _parse_expression(subexp):
 
         if quoted:
             ret += char
-        elif char == " " or is_operator(char) or char in [")", "("]:
+        elif char == " " or _is_operator(char) or char in [")", "("]:
             break
         else:
             ret += char
@@ -30,7 +75,7 @@ def _parse_expression(subexp):
     return ret
 
 
-def evaluate_postfix(postfix, evaluator):
+def _evaluate_postfix(postfix, evaluator):
     """
     Evaluates a postfix expression and returns a boolean
     @param postfix:  Postfix expression as a list
@@ -43,7 +88,7 @@ def evaluate_postfix(postfix, evaluator):
 
     stack = []
     for el in postfix:
-        if not is_operator(el):
+        if not _is_operator(el):
             stack.append(el)
         else:
             o1 = stack.pop()
@@ -66,7 +111,7 @@ def evaluate_postfix(postfix, evaluator):
         return stack[0]
 
 
-def infix_to_postfix(exp):
+def _infix_to_postfix(exp):
     """
     Translates an infix expression to postfix using an standard algorithm
     with little hacks for parse complex expressions like "compiler.version=4"
@@ -98,11 +143,11 @@ def infix_to_postfix(exp):
                     output.append(popped)
             if popped != "(":
                 raise Exception("Bad expression, not balanced parenthesis")
-        elif is_operator(char):
+        elif _is_operator(char):
             # Same operations has the same priority
             # replace this lines if the operators need to have
             # some priority
-            if stack and is_operator(stack[:-1]):
+            if stack and _is_operator(stack[:-1]):
                 popped = stack.pop()
                 output.append(popped)
             stack.append(char)
diff --git a/conan/internal/cache/cache.py b/conan/internal/cache/cache.py
index 92af0a603..5a8e754f2 100644
--- a/conan/internal/cache/cache.py
+++ b/conan/internal/cache/cache.py
@@ -1,7 +1,9 @@
 import hashlib
 import os
+import re
 import shutil
 import uuid
+from fnmatch import translate
 from typing import List
 
 from conan.internal.cache.conan_reference_layout import RecipeLayout, PackageLayout
@@ -168,8 +170,18 @@ class PkgCache:
         assert ref.timestamp
         self._db.update_recipe_timestamp(ref)
 
-    def all_refs(self):
-        return self._db.list_references()
+    def search_recipes(self, pattern=None, ignorecase=True):
+        # Conan references in main storage
+        if pattern:
+            if isinstance(pattern, RecipeReference):
+                pattern = repr(pattern)
+            pattern = translate(pattern)
+            pattern = re.compile(pattern, re.IGNORECASE if ignorecase else 0)
+
+        refs = self._db.list_references()
+        if pattern:
+            refs = [r for r in refs if r.partial_match(pattern)]
+        return refs
 
     def exists_prev(self, pref):
         # Used just by download to skip downloads if prev already exists in cache
diff --git a/conan/internal/integrity_check.py b/conan/internal/cache/integrity_check.py
similarity index 100%
rename from conan/internal/integrity_check.py
rename to conan/internal/cache/integrity_check.py
diff --git a/conans/client/downloaders/download_cache.py b/conans/client/downloaders/download_cache.py
index 51c587b93..c32765621 100644
--- a/conans/client/downloaders/download_cache.py
+++ b/conans/client/downloaders/download_cache.py
@@ -6,7 +6,7 @@ from threading import Lock
 from conans.errors import ConanException
 from conans.util.dates import timestamp_now
 from conans.util.files import load, save, remove_if_dirty
-from conans.util.locks import SimpleLock
+from conans.util.locks import simple_lock
 from conans.util.sha import sha256 as compute_sha256
 
 
@@ -35,7 +35,7 @@ class DownloadCache:
     @contextmanager
     def lock(self, lock_id):
         lock = os.path.join(self._path, self._LOCKS, lock_id)
-        with SimpleLock(lock):
+        with simple_lock(lock):
             # Once the process has access, make sure multithread is locked too
             # as SimpleLock doesn't work multithread
             thread_lock = self._thread_locks.setdefault(lock, Lock())
diff --git a/conans/client/graph/range_resolver.py b/conans/client/graph/range_resolver.py
index 535cc5ce2..31df11c37 100644
--- a/conans/client/graph/range_resolver.py
+++ b/conans/client/graph/range_resolver.py
@@ -2,7 +2,6 @@ from conans.client.graph.proxy import should_update_reference
 from conans.errors import ConanException
 from conans.model.recipe_ref import RecipeReference
 from conans.model.version_range import VersionRange
-from conans.search.search import search_recipes
 
 
 class RangeResolver:
@@ -57,7 +56,7 @@ class RangeResolver:
         local_found = self._cached_cache.get(pattern)
         if local_found is None:
             # This local_found is weird, it contains multiple revisions, not just latest
-            local_found = search_recipes(self._cache, pattern)
+            local_found = self._cache.search_recipes(pattern)
             # TODO: This is still necessary to filter user/channel, until search_recipes is fixed
             local_found = [ref for ref in local_found if ref.user == search_ref.user
                            and ref.channel == search_ref.channel]
diff --git a/conans/model/recipe_ref.py b/conans/model/recipe_ref.py
index 0d86ff3fa..ca58c5984 100644
--- a/conans/model/recipe_ref.py
+++ b/conans/model/recipe_ref.py
@@ -187,6 +187,23 @@ class RecipeReference:
             return not condition
         return condition
 
+    def partial_match(self, pattern):
+        """
+        Finds if pattern matches any of partial sums of tokens of conan reference
+        """
+        tokens = [self.name, "/", str(self.version)]
+        if self.user:
+            tokens += ["@", self.user]
+        if self.channel:
+            tokens += ["/", self.channel]
+        if self.revision:
+            tokens += ["#", self.revision]
+        partial = ""
+        for token in tokens:
+            partial += token
+            if pattern.match(partial):
+                return True
+
 
 def ref_matches(ref, pattern, is_consumer):
     if not ref or not str(ref):
diff --git a/conans/search/search.py b/conans/search/search.py
deleted file mode 100644
index c0adea1e7..000000000
--- a/conans/search/search.py
+++ /dev/null
@@ -1,130 +0,0 @@
-import os
-import re
-from collections import OrderedDict
-from fnmatch import translate
-from typing import Dict
-
-from conans.errors import ConanException
-from conans.model.info import load_binary_info
-from conans.model.package_ref import PkgReference
-from conans.model.recipe_ref import RecipeReference
-from conan.internal.paths import CONANINFO
-from conans.search.query_parse import evaluate_postfix, infix_to_postfix
-from conans.util.files import load
-
-
-def filter_packages(query, results: Dict[PkgReference, dict]):
-    if query is None:
-        return results
-    try:
-        if "!" in query:
-            raise ConanException("'!' character is not allowed")
-        if "~" in query:
-            raise ConanException("'~' character is not allowed")
-        if " not " in query or query.startswith("not "):
-            raise ConanException("'not' operator is not allowed")
-        postfix = infix_to_postfix(query) if query else []
-        result = OrderedDict()
-        for pref, data in results.items():
-            if _evaluate_postfix_with_info(postfix, data):
-                result[pref] = data
-        return result
-    except Exception as exc:
-        raise ConanException("Invalid package query: %s. %s" % (query, exc))
-
-
-def _evaluate_postfix_with_info(postfix, binary_info):
-
-    # Evaluate conaninfo with the expression
-
-    def evaluate_info(expression):
-        """Receives an expression like compiler.version="12"
-        Uses conan_vars_info in the closure to evaluate it"""
-        name, value = expression.split("=", 1)
-        value = value.replace("\"", "")
-        return _evaluate(name, value, binary_info)
-
-    return evaluate_postfix(postfix, evaluate_info)
-
-
-def _evaluate(prop_name, prop_value, binary_info):
-    """
-    Evaluates a single prop_name, prop_value like "os", "Windows" against
-    conan_vars_info.serialize_min()
-    """
-
-    def compatible_prop(setting_value, _prop_value):
-        return (_prop_value == setting_value) or (_prop_value == "None" and setting_value is None)
-
-    # TODO: Necessary to generalize this query evaluation to include all possible fields
-    info_settings = binary_info.get("settings", {})
-    info_options = binary_info.get("options", {})
-
-    if not prop_name.startswith("options."):
-        return compatible_prop(info_settings.get(prop_name), prop_value)
-    else:
-        prop_name = prop_name[len("options."):]
-        return compatible_prop(info_options.get(prop_name), prop_value)
-
-
-def search_recipes(cache, pattern=None, ignorecase=True):
-    # Conan references in main storage
-    if pattern:
-        if isinstance(pattern, RecipeReference):
-            pattern = repr(pattern)
-        pattern = translate(pattern)
-        pattern = re.compile(pattern, re.IGNORECASE) if ignorecase else re.compile(pattern)
-
-    refs = cache.all_refs()
-    if pattern:
-        _refs = []
-        for r in refs:
-            match_ref = str(r) if not r.revision else repr(r)
-            if _partial_match(pattern, match_ref):
-                _refs.append(r)
-        refs = _refs
-    return refs
-
-
-def _partial_match(pattern, reference):
-    """
-    Finds if pattern matches any of partial sums of tokens of conan reference
-    """
-    tokens = reference.replace('/', ' / ').replace('@', ' @ ').replace('#', ' # ').split()
-
-    def partial_sums(iterable):
-        partial = ''
-        for i in iterable:
-            partial += i
-            yield partial
-
-    return any(map(pattern.match, list(partial_sums(tokens))))
-
-
-def get_cache_packages_binary_info(cache, prefs) -> Dict[PkgReference, dict]:
-    """
-    param package_layout: Layout for the given reference
-    """
-
-    result = OrderedDict()
-
-    package_layouts = []
-    for pref in prefs:
-        latest_prev = cache.get_latest_package_reference(pref)
-        package_layouts.append(cache.pkg_layout(latest_prev))
-
-    for pkg_layout in package_layouts:
-        # Read conaninfo
-        info_path = os.path.join(pkg_layout.package(), CONANINFO)
-        if not os.path.exists(info_path):
-            raise ConanException(f"Corrupted package '{pkg_layout.reference}' "
-                                 f"without conaninfo.txt in: {info_path}")
-        conan_info_content = load(info_path)
-
-        info = load_binary_info(conan_info_content)
-        pref = pkg_layout.reference
-        # The key shoudln't have the latest package revision, we are asking for package configs
-        pref.revision = None
-        result[pkg_layout.reference] = info
-
-    return result
diff --git a/conans/server/service/v2/search.py b/conans/server/service/v2/search.py
index 66ec092ff..b91c4f6e2 100644
--- a/conans/server/service/v2/search.py
+++ b/conans/server/service/v2/search.py
@@ -7,7 +7,6 @@ from conans.errors import ForbiddenException, RecipeNotFoundException
 from conans.model.package_ref import PkgReference
 from conans.model.recipe_ref import RecipeReference
 from conan.internal.paths import CONANINFO
-from conans.search.search import _partial_match
 from conans.server.utils.files import list_folder_subdirs
 from conans.util.files import load
 
@@ -93,7 +92,7 @@ class SearchService(object):
                 fields_dir = [underscore_to_none(d) for d in subdir.split("/")]
                 new_ref = RecipeReference(*fields_dir)
                 new_ref.revision = None
-                if _partial_match(b_pattern, repr(new_ref)):
+                if new_ref.partial_match(b_pattern):
                     ret.add(new_ref)
 
             return sorted(ret)
diff --git a/conans/util/locks.py b/conans/util/locks.py
index 40a0d7016..04abec3f1 100644
--- a/conans/util/locks.py
+++ b/conans/util/locks.py
@@ -1,120 +1,24 @@
-import os
-import time
+from contextlib import contextmanager
 
 import fasteners
 
-from conans.util.files import load, save
 
-logger = None
+@contextmanager
+def simple_lock(lock_path):
+    lock = fasteners.InterProcessLock(lock_path)
+    with lock:
+        yield
 
-class NoLock(object):
 
-    def __enter__(self):
-        pass
+@contextmanager
+def read_lock(lock_path):
+    rw_lock = fasteners.InterProcessReaderWriterLock(lock_path)
+    with rw_lock.read_lock():
+        yield
 
-    def __exit__(self, exc_type, exc_val, exc_tb):  # @UnusedVariable
-        pass
 
-
-class SimpleLock(object):
-
-    def __init__(self, filename):
-        self._lock = fasteners.InterProcessLock(filename, logger=logger)
-
-    def __enter__(self):
-        self._lock.acquire()
-
-    def __exit__(self, exc_type, exc_val, exc_tb):  # @UnusedVariable
-        self._lock.release()
-
-
-READ_BUSY_DELAY = 0.5
-WRITE_BUSY_DELAY = 0.25
-
-
-class Lock(object):
-
-    @staticmethod
-    def clean(folder):
-        if os.path.exists(folder + ".count"):
-            os.remove(folder + ".count")
-        if os.path.exists(folder + ".count.lock"):
-            os.remove(folder + ".count.lock")
-
-    def __init__(self, folder, locked_item, output):
-        self._count_file = folder + ".count"
-        self._count_lock_file = folder + ".count.lock"
-        self._locked_item = locked_item
-        self._output = output
-        self._first_lock = True
-
-    @property
-    def files(self):
-        return self._count_file, self._count_lock_file
-
-    def _info_locked(self):
-        if self._first_lock:
-            self._first_lock = False
-            self._output.info("%s is locked by another concurrent conan process, wait..."
-                              % str(self._locked_item))
-            self._output.info("If not the case, quit, and do 'conan remove --locks'")
-
-    def _readers(self):
-        try:
-            return int(load(self._count_file))
-        except IOError:
-            return 0
-        except (UnicodeEncodeError, ValueError):
-            self._output.warning("%s does not contain a number!" % self._count_file)
-            return 0
-
-
-class ReadLock(Lock):
-
-    def __enter__(self):
-        while True:
-            with fasteners.InterProcessLock(self._count_lock_file, logger=logger):
-                readers = self._readers()
-                if readers >= 0:
-                    save(self._count_file, str(readers + 1))
-                    break
-            self._info_locked()
-            time.sleep(READ_BUSY_DELAY)
-
-    def __exit__(self, exc_type, exc_val, exc_tb):   # @UnusedVariable
-        with fasteners.InterProcessLock(self._count_lock_file, logger=logger):
-            readers = self._readers()
-            save(self._count_file, str(readers - 1))
-
-
-class WriteLock(Lock):
-
-    def __enter__(self):
-        while True:
-            with fasteners.InterProcessLock(self._count_lock_file, logger=logger):
-                readers = self._readers()
-                if readers == 0:
-                    save(self._count_file, "-1")
-                    break
-            self._info_locked()
-            time.sleep(WRITE_BUSY_DELAY)
-
-    def __exit__(self, exc_type, exc_val, exc_tb):  # @UnusedVariable
-        with fasteners.InterProcessLock(self._count_lock_file, logger=logger):
-            save(self._count_file, "0")
-
-        if exc_type is not None:
-            # If there was an exception while locking this, might be empty
-            # Try to clean up the trailing filelocks
-            try:
-                os.remove(self._count_file)
-                os.remove(self._count_lock_file)
-                path = os.path.dirname(self._count_file)
-                for _ in range(3):
-                    try:  # Take advantage that os.rmdir does not delete non-empty dirs
-                        os.rmdir(path)
-                    except Exception:
-                        break  # not empty
-                    path = os.path.dirname(path)
-            except Exception:
-                pass
+@contextmanager
+def write_lock(lock_path):
+    rw_lock = fasteners.InterProcessReaderWriterLock(lock_path)
+    with rw_lock.write_lock():
+        yield


Test patch:
diff --git a/.ci/jenkins/testsv2.jenkins b/.ci/jenkins/testsv2.jenkins
index 3e0085ea4..f443a2a7e 100644
--- a/.ci/jenkins/testsv2.jenkins
+++ b/.ci/jenkins/testsv2.jenkins
@@ -21,9 +21,9 @@ List<Map> getConfigurations(String moduleName, String branchName, String jobName
         configs.add([node: "Linux", pyvers: ["PY312"]])
     }
     if (branchName =~ /(^PR-.*)/) {
-        configs.add([node: "Linux", pyvers: ["PY36"]])
-        configs.add([node: "Windows", pyvers: ["PY36"]])
-        configs.add([node: "M2Macos", pyvers: ["PY36"]])
+        configs.add([node: "Linux", pyvers: ["PY38"]])
+        configs.add([node: "Windows", pyvers: ["PY38"]])
+        configs.add([node: "M2Macos", pyvers: ["PY38"]])
     }
     else if (jobName == "ConanNightlyv2" || branchName =~ /^release.*/) {
         configs.add([node: "Linux", pyvers: ["PY36", "PY38"]])
@@ -79,7 +79,7 @@ private Closure runTests(String nodeName, String pythonVersion, String module) {
                                      + "conan --version && conan --help && "
 
 
-                def launchTests = "python -m pytest ${module} -n=4 --durations=20"
+                def launchTests = "python -m pytest ${module} -n=4 --durations=20 -rs"
 
                 if (shouldTestServer(module)) {
                     def rm = (nodeName=="Windows") ? "del" : "rm"
diff --git a/test/integration/command_v2/test_info_build_order.py b/test/integration/command_v2/test_info_build_order.py
index cf95afb54..b735920e1 100644
--- a/test/integration/command_v2/test_info_build_order.py
+++ b/test/integration/command_v2/test_info_build_order.py
@@ -73,6 +73,14 @@ def test_info_build_order():
     assert bo_json["order_by"] == "recipe"
     assert bo_json["order"] == result
 
+    # test html format
+    c.run("graph build-order consumer --build=missing --format=html")
+    assert "<body>" in c.stdout
+    c.run("graph build-order consumer --order-by=recipe --format=html")
+    assert "<body>" in c.stdout
+    c.run("graph build-order consumer --order-by=configuration --format=html")
+    assert "<body>" in c.stdout
+
 
 def test_info_build_order_configuration():
     c = TestClient()
@@ -332,6 +340,11 @@ def test_info_build_order_merge_multi_product():
 
     assert bo_json == result
 
+    # test that html format for build-order-merge generates something
+    c.run("graph build-order-merge --file=bo1.json --file=bo2.json --format=html")
+    assert "<body>" in c.stdout
+
+
 
 def test_info_build_order_merge_multi_product_configurations():
     c = TestClient()
diff --git a/test/integration/settings/per_package_settings_test.py b/test/integration/settings/per_package_settings_test.py
index 4cf0d8687..482be6976 100644
--- a/test/integration/settings/per_package_settings_test.py
+++ b/test/integration/settings/per_package_settings_test.py
@@ -148,3 +148,35 @@ def test_per_package_settings_build():
     c.run("install --tool-requires=cmake/0.1 -s:b build_type=Release -s:b cmake*:build_type=Debug "
           "--build=missing")
     assert "cmake/0.1: Building myself with Debug!!" in c.out
+
+
+def test_package_settings_mixed_patterns():
+    # https://github.com/conan-io/conan/issues/16606
+    c = TestClient()
+    profile = textwrap.dedent("""
+        [settings]
+        arch=x86_64
+        *@test/*:build_type=Release
+
+        os=Linux
+        compiler=gcc
+        &:compiler.version=12
+        compiler.libcxx = libstdc++
+        """)
+    conanfile = textwrap.dedent("""
+        from conan import ConanFile
+        class Pkg(ConanFile):
+            name = "mypkg"
+            version = "0.1"
+            settings = "os", "arch", "compiler", "build_type"
+            def build(self):
+                self.output.info(f"BUILD_TYPE={self.settings.build_type}!")
+                self.output.info(f"COMPILER_VERSION={self.settings.compiler.version}!")
+                self.output.info(f"COMPILER_LIBCXX={self.settings.compiler.libcxx}!")
+            """)
+    c.save({"profile": profile,
+            "conanfile.py": conanfile})
+    c.run("create . -pr=profile --user=test --channel=test")
+    assert "mypkg/0.1@test/test: BUILD_TYPE=Release!" in c.out
+    assert "mypkg/0.1@test/test: COMPILER_VERSION=12!" in c.out
+    assert "mypkg/0.1@test/test: COMPILER_LIBCXX=libstdc++!" in c.out
diff --git a/test/integration/test_package_vendor.py b/test/integration/test_package_vendor.py
index 562ae4cd3..08c3e4c35 100644
--- a/test/integration/test_package_vendor.py
+++ b/test/integration/test_package_vendor.py
@@ -138,3 +138,42 @@ def test_vendor_dont_propagate_options():
     c.run("install consumer_shared --build=app/* -c tools.graph:vendor=build")
     c.assert_listed_binary({"pkga/0.1": ("55c609fe8808aa5308134cb5989d23d3caffccf2", "Cache"),
                             "app/0.1": ("da39a3ee5e6b4b0d3255bfef95601890afd80709", "Build")})
+
+
+def test_package_vendor_export_pkg():
+    c = TestClient()
+    app = textwrap.dedent("""
+        import os
+        from conan import ConanFile
+        from conan.tools.files import copy, save
+
+        class App(ConanFile):
+            name = "app"
+            version = "0.1"
+            package_type = "application"
+            vendor = True
+            requires = "pkga/0.1"
+            def package(self):
+                copy(self, "*", src=self.dependencies["pkga"].package_folder,
+                     dst=self.package_folder)
+                save(self, os.path.join(self.package_folder, "app.exe"), "app")
+            """)
+
+    c.save({"pkga/conanfile.py": GenConanfile("pkga", "0.1").with_package_type("shared-library")
+                                                            .with_package_file("pkga.dll", "dll"),
+            "app/conanfile.py": app
+            })
+    c.run("create pkga")
+    c.run("build app")  # -c tools.graph:vendor=build will be automatic
+    c.run("export-pkg app")
+    assert "pkga/0.1" in c.out  # In the export-pkg process, dependencies are still needed
+    assert "conanfile.py (app/0.1): package(): Packaged 1 '.dll' file: pkga.dll" in c.out
+
+    # we can safely remove pkga, once the package is created, it can work without deps
+    c.run("remove pkg* -c")
+    c.run("list app:*")
+    assert "pkga" not in c.out  # The binary doesn't depend on pkga
+    c.run("install --requires=app/0.1 --deployer=full_deploy")
+    assert "pkga" not in c.out
+    assert c.load("full_deploy/host/app/0.1/app.exe") == "app"
+    assert c.load("full_deploy/host/app/0.1/pkga.dll") == "dll"
diff --git a/test/unittests/search/search_query_parse_test.py b/test/unittests/search/search_query_parse_test.py
index 2504d9eca..c3177d4a6 100644
--- a/test/unittests/search/search_query_parse_test.py
+++ b/test/unittests/search/search_query_parse_test.py
@@ -1,28 +1,28 @@
 import unittest
 
-from conans.search.query_parse import evaluate_postfix, infix_to_postfix
+from conan.internal.api.list.query_parse import _evaluate_postfix, _infix_to_postfix
 
 
 class QueryParseTest(unittest.TestCase):
 
     def test_get_postfix(self):
-        r = infix_to_postfix("")
+        r = _infix_to_postfix("")
         self.assertEqual(r, [])
 
-        r = infix_to_postfix("a=2")
+        r = _infix_to_postfix("a=2")
         self.assertEqual(r, ["a=2"])
 
-        r = infix_to_postfix("a=2 OR b=3")
+        r = _infix_to_postfix("a=2 OR b=3")
         self.assertEqual(r, ["a=2", "b=3", "|"])
 
-        r = infix_to_postfix("a= OR b=")
+        r = _infix_to_postfix("a= OR b=")
         self.assertEqual(r, ["a=", "b=", "|"])  # Equivalent to ""
 
-        r = infix_to_postfix("(a=2 OR b=3) AND (j=34 AND j=45) OR (a=1)")
+        r = _infix_to_postfix("(a=2 OR b=3) AND (j=34 AND j=45) OR (a=1)")
         self.assertEqual(r, ["a=2", "b=3", "|", "j=34", "j=45", "&", "a=1", "&", "|"])
 
         with self.assertRaisesRegex(Exception, "Invalid expression: 2"):
-            r = infix_to_postfix("a= 2 OR b=3")
+            r = _infix_to_postfix("a= 2 OR b=3")
 
     def test_evaluate_postfix(self):
 
@@ -30,8 +30,8 @@ class QueryParseTest(unittest.TestCase):
             return expr in ("a=2", "j=45")
 
         def evaluate(q):
-            r = infix_to_postfix(q)
-            return evaluate_postfix(r, evaluator)
+            r = _infix_to_postfix(q)
+            return _evaluate_postfix(r, evaluator)
 
         self.assertTrue(evaluate("a=2"))
         self.assertFalse(evaluate("a=4"))