Repository: conan-io/conan
Pull request #13470 (2023-03-17 07:31:48)

Add basic package filter for conan list html output
Changelog: Feature: Add filtering for options and settings in conan list html output.
Docs: omit

This is also passing the arguments from the cli to show the input in the top navbar in the html

![image](https://user-images.githubusercontent.com/5045666/225901759-514caf02-4cac-4882-9c72-167e0bd82225.png)





Patch:
diff --git a/conan/cli/commands/list.py b/conan/cli/commands/list.py
index 9114e4b12..fc4453b1f 100644
--- a/conan/cli/commands/list.py
+++ b/conan/cli/commands/list.py
@@ -120,7 +120,9 @@ def list(conan_api: ConanAPI, parser, *args):
             results[name] = {"error": str(e)}
         else:
             results[name] = list_bundle.serialize()
+
     return {
         "results": results,
-        "conan_api": conan_api
+        "conan_api": conan_api,
+        "cli_args": " ".join([f"{arg}={getattr(args, arg)}" for arg in vars(args) if getattr(args, arg)])
     }
diff --git a/conan/cli/formatters/list/list.py b/conan/cli/formatters/list/list.py
index feeb5a2fb..13c114f33 100644
--- a/conan/cli/formatters/list/list.py
+++ b/conan/cli/formatters/list/list.py
@@ -11,11 +11,12 @@ from conans import __version__ as client_version
 
 def list_packages_html(result):
     results = result["results"]
+    cli_args = result["cli_args"]
     conan_api = result["conan_api"]
     template_folder = os.path.join(conan_api.cache_folder, "templates")
     user_template = os.path.join(template_folder, "list_packages.html")
     template = load(user_template) if os.path.isfile(user_template) else list_packages_html_template
     template = Template(template, autoescape=select_autoescape(['html', 'xml']))
     content = template.render(results=json.dumps(results), base_template_path=template_folder,
-                              version=client_version)
+                              version=client_version, cli_args=cli_args)
     cli_out_write(content)
diff --git a/conan/cli/formatters/list/search_table_html.py b/conan/cli/formatters/list/search_table_html.py
index e88e0a09a..a57075f55 100644
--- a/conan/cli/formatters/list/search_table_html.py
+++ b/conan/cli/formatters/list/search_table_html.py
@@ -29,7 +29,7 @@ list_packages_html_template = r"""
     <script>
         var list_results = {{ results| safe }};
 
-        function getRefLink(origin, ref) {
+        function replaceChars(origin, ref) {
             return origin + "_" + ref.replaceAll(".", "_").replaceAll("/", "_").replaceAll("#", "_").replaceAll("@", "_").replaceAll(":", "_").replaceAll(" ", "_")
         }
 
@@ -53,40 +53,149 @@ list_packages_html_template = r"""
             return new Date(timeStamp * 1000).toLocaleDateString('en', options);
         }
 
-        function getInfoFieldsBadges(property, info) {
+        function getInfoFieldsBadges(info) {
             let style = '';
             let badges = '';
-            if (property in info) {
-                for (const [key, value] of Object.entries(info[property])) {
-                    style = (key == 'os') ? 'text-bg-info' : 'text-bg-secondary';
-                    badges += `<span class="badge ${style}">${key}: ${value}</span>&nbsp;`
+            for (property of ["settings", "options"]) {
+                if (property in info) {
+                    for (const [key, value] of Object.entries(info[property])) {
+                        style = (key == 'os') ? 'text-bg-info' : 'text-bg-secondary';
+                        badges += `<span class="badge ${style}">${key}: ${value}</span>&nbsp;`
+                    }
                 }
             }
             return badges;
         }
 
-        function getTabContent(tabID, revID, revInfo) {
+        function isSubset(setA, setB) {
+            for (let elem of setA) {
+                if (!setB.has(elem)) {
+                    return false;
+                }
+            }
+            return true;
+        }
 
-            tabContent = `<div class="tab-pane" id="${tabID}" role="tabpanel">`;
+        function isFiltered(info, filters) {
+            if (filters.length == 0) {
+                return false;
+            }
+            packageProperties = [];
+            for (property of ["settings", "options"]) {
+                if (property in info) {
+                    for (const [key, value] of Object.entries(info[property])) {
+                        packageProperties.push(`${key}=${value}`);
+                    }
+                }
+            }
+            packageSet = new Set(packageProperties);
+            filtersSet = new Set(filters);
+            if (isSubset(filtersSet, packageSet)) {
+                return false;
+            }
+            return true;
+        }
 
-            if ("packages" in revInfo && Object.entries(revInfo["packages"]).length > 0) {
+        function getUniqueSettingsOptions(revInfo) {
+            let options = new Set();
+            let settings = new Set();
+            for (const [package, packageInfo] of Object.entries(revInfo["packages"])) {
+                let info = packageInfo["info"];
+                if ("options" in info) {
+                    for (const [key, value] of Object.entries(info["options"])) {
+                        options.add(`${key}=${value}`)
+                    }
+                }
+                if ("settings" in info) {
+                    for (const [key, value] of Object.entries(info["settings"])) {
+                        settings.add(`${key}=${value}`)
+                    }
+                }
+            }
+            return [options, settings]
+        }
 
-                tabContent += `<h3>Packages for revision ${revID}</h3>`;
-                for (const [package, packageInfo] of Object.entries(revInfo["packages"])) {
+        function getFilters(revID, revInfo) {
+            let options_settings = getUniqueSettingsOptions(revInfo);
+            let options = Array.from(options_settings[0]);
+            let settings = Array.from(options_settings[1]);
+            let filter = `<h6>Filter packages:</h6>`
+            for (setting of settings) {
+                filter += `
+                        <div class="form-check form-check-inline">
+                            <input class="form-check-input" type="checkbox" id="${revID}#${setting}" value="${setting}">
+                            <label class="form-check-label" for="${revID}#${setting}">${setting}</label>
+                        </div>
+                      `
+            }
+            for (option of options) {
+                filter += `
+                        <div class="form-check form-check-inline">
+                            <input class="form-check-input" type="checkbox" id="${revID}#${option}" value="${option}">
+                            <label class="form-check-label" for="${revID}#${option}">${option}</label>
+                        </div>
+                      `
+            }
+            return filter;
+        }
+
+        let activeRevision = {};
+        let tabsInfo = {};
 
-                    tabContent += `<div class="bg-light">`;
-                    tabContent += `<h6 class="mb-1">${package}</h6>`;
-                    tabContent += `${getInfoFieldsBadges("settings", packageInfo["info"])}${getInfoFieldsBadges("options", packageInfo["info"])}`;
-                    tabContent += `<br><br><b>Package revisions:</>`;
-                    tabContent += `<ul>`;
+        function filterPackages() {
+            const activeTab = document.querySelector('.tab-pane.active');
+            const packageList = activeTab.querySelector('#packageList');
+            activeRevision.revision = tabsInfo[activeTab.getAttribute('id')].revision;
+            activeRevision.revInfo = tabsInfo[activeTab.getAttribute('id')].revInfo;
+            packageList.innerHTML = getPackagesList(activeRevision.revision, activeRevision.revInfo);
+        }
+
+        function getPackagesList(revision, revInfo) {
+            const checkboxes = document.querySelectorAll('input[type="checkbox"]');
+            filters = [];
+            checkboxes.forEach(function (checkbox) {
+                if (checkbox.checked) {
+                    filters.push(checkbox.value)
+                }
+            })
+
+            activeRevInfo = revInfo;
+            let packageList = ``;
+            for (const [package, packageInfo] of Object.entries(revInfo["packages"])) {
+
+                if (!isFiltered(packageInfo["info"], filters)) {
+                    packageList += `<div class="bg-light">`;
+                    packageList += `<h6 class="mb-1">${package}</h6>`;
+                    packageList += `${getInfoFieldsBadges(packageInfo["info"])}`;
+
+                    packageList += `<br><br><b>Package revisions:</>`;
+                    packageList += `<ul>`;
                     for (const [packageRev, packageRevInfo] of Object.entries(packageInfo["revisions"])) {
-                        tabContent += `<li>${packageRev}&nbsp(${formatDate(packageRevInfo["timestamp"])})</li>`;
+                        packageList += `<li>${packageRev}&nbsp(${formatDate(packageRevInfo["timestamp"])})</li>`;
                     }
-                    tabContent += `</ul>`;
-                    tabContent += `</div>`;
-                    tabContent += `<br>`;
+                    packageList += `</ul>`;
+                    packageList += `</div>`;
+                    packageList += `<br>`;
                 }
             }
+            return packageList;
+        }
+
+        function getTabContent(tabID, revID, revInfo) {
+
+            let tabContent = `<div class="tab-pane" id="${tabID}" role="tabpanel">`;
+
+            if ("packages" in revInfo && Object.entries(revInfo["packages"]).length > 0) {
+
+                tabContent += `<h3>Packages for revision ${revID}</h3>`;
+
+                tabContent += getFilters(revID, revInfo);
+
+                tabContent += `<div id="packageList">`;
+                tabContent += getPackagesList(revID, revInfo);
+                tabContent += `</div>`;
+
+            }
             tabContent += `<h3>JSON</h3>`;
             tabContent += `<pre class="p-3 mb-2 bg-light text-dark">${JSON.stringify(revInfo, null, 2)}</pre>`;
             tabContent += `</div>`
@@ -97,7 +206,7 @@ list_packages_html_template = r"""
         let tabs = `<div class="tab-content">`;
 
         for (const [origin, references] of Object.entries(list_results)) {
-            if (Object.keys(references).length>0) {
+            if (Object.keys(references).length > 0) {
                 menu += `<li class="list-group-item"><b>${origin}</b>`;
                 if ("error" in references) {
                     menu += `<pre>${references["error"]}</pre>`;
@@ -105,7 +214,7 @@ list_packages_html_template = r"""
                 else {
                     for (const [reference, revisions] of Object.entries(references)) {
                         let originStr = origin.replaceAll(" ", "_");
-                        const refLink = getRefLink(originStr, reference);
+                        const refLink = replaceChars(originStr, reference);
 
                         menu += `<div class="accordion accordion-flush" id="accordion_${originStr}">`;
                         menu += `<div class="accordion-item">`;
@@ -120,6 +229,8 @@ list_packages_html_template = r"""
                                 packageBadge = (packageCount == 0) ? '' : `&nbsp<span class="badge rounded-pill text-bg-success">${packageCount}</span>`;
                                 let tabID = `${originStr}_${revision}`;
                                 menu += `<a class="list-group-item list-group-item-action" id="left_${revision}" data-bs-toggle="list" href="#${tabID}" role="tab" aria-controls="list-home">${revision.substring(0, 6)}&nbsp(${formatDate(revInfo["timestamp"])})${packageBadge}</a>`;
+
+                                tabsInfo[tabID] = { "revision": revision, "revInfo": revInfo }
                                 tabs += getTabContent(tabID, revision, revInfo);
                             }
                         }
@@ -155,6 +266,14 @@ list_packages_html_template = r"""
                     tabTrigger.show()
                 })
             })
+
+            const checkboxes = document.querySelectorAll('input[type="checkbox"]');
+            checkboxes.forEach(function (checkbox) {
+                checkbox.addEventListener('change', function () {
+                    filterPackages();
+                });
+            });
+
         });
     </script>
 </head>
@@ -162,7 +281,7 @@ list_packages_html_template = r"""
 <body>
     <nav class="navbar navbar-expand-lg bg-light">
         <div class="container-fluid">
-            <a class="navbar-brand">conan list results</a>
+            <a class="navbar-brand">conan list: {{ cli_args }}</a>
         </div>
     </nav>
 
diff --git a/conan/tools/build/flags.py b/conan/tools/build/flags.py
index 2e378e018..ddf92dfbf 100644
--- a/conan/tools/build/flags.py
+++ b/conan/tools/build/flags.py
@@ -185,7 +185,7 @@ def cppstd_flag(settings):
     return flag
 
 
-def _cppstd_msvc(visual_version, cppstd):
+def cppstd_msvc_flag(visual_version, cppstd):
     # https://docs.microsoft.com/en-us/cpp/build/reference/std-specify-language-standard-version
     v14 = None
     v17 = None
@@ -198,11 +198,15 @@ def _cppstd_msvc(visual_version, cppstd):
     if visual_version >= "191":
         v17 = "c++17"
         v20 = "c++latest"
-    if visual_version >= "193":
+    if visual_version >= "192":
         v20 = "c++20"
+    if visual_version >= "193":
         v23 = "c++latest"
 
-    flag = {"14": v14, "17": v17, "20": v20, "23": v23}.get(str(cppstd), None)
+    return {"14": v14, "17": v17, "20": v20, "23": v23}.get(str(cppstd), None)
+
+def _cppstd_msvc(visual_version, cppstd):
+    flag = cppstd_msvc_flag(visual_version, cppstd)
     return "/std:%s" % flag if flag else None
 
 
diff --git a/conan/tools/meson/helpers.py b/conan/tools/meson/helpers.py
index fc5231c90..de919e4aa 100644
--- a/conan/tools/meson/helpers.py
+++ b/conan/tools/meson/helpers.py
@@ -1,3 +1,5 @@
+from conan.tools.build.flags import cppstd_msvc_flag
+
 __all__ = ["to_meson_machine", "to_meson_value", "to_cppstd_flag"]
 
 # https://mesonbuild.com/Reference-tables.html#operating-system-names
@@ -47,11 +49,6 @@ _meson_cpu_family_map = {
     'x86_64': ('x86_64', 'x86_64', 'little')
 }
 
-_vs_cppstd_map = {
-    '14': "vc++14",
-    '17': "vc++17",
-    '20': "vc++latest"
-}
 
 # Meson valid values
 # "none", "c++98", "c++03", "c++11", "c++14", "c++17", "c++1z", "c++2a", "c++20",
@@ -102,14 +99,19 @@ def to_meson_value(value):
 
 
 # FIXME: Move to another more common module
-def to_cppstd_flag(compiler, cppstd):
+def to_cppstd_flag(compiler, compiler_version, cppstd):
     """Gets a valid cppstd flag.
 
     :param compiler: ``str`` compiler name.
+    :param compiler_version: ``str`` compiler version.
     :param cppstd: ``str`` cppstd version.
     :return: ``str`` cppstd flag.
     """
     if compiler == "msvc":
-        return _vs_cppstd_map.get(cppstd)
+        # Meson's logic with 'vc++X' vs 'c++X' is possibly a little outdated.
+        # Presumably the intent is 'vc++X' is permissive and 'c++X' is not,
+        # but '/permissive-' is the default since 16.8.
+        flag = cppstd_msvc_flag(compiler_version, cppstd)
+        return 'v%s' % flag if flag else None
     else:
         return _cppstd_map.get(cppstd)
diff --git a/conan/tools/meson/toolchain.py b/conan/tools/meson/toolchain.py
index 9ed22b466..5e90aba79 100644
--- a/conan/tools/meson/toolchain.py
+++ b/conan/tools/meson/toolchain.py
@@ -122,8 +122,12 @@ class MesonToolchain(object):
         compiler = self._conanfile.settings.get_safe("compiler")
         if compiler is None:
             raise ConanException("MesonToolchain needs 'settings.compiler', but it is not defined")
+        compiler_version = self._conanfile.settings.get_safe("compiler.version")
+        if compiler_version is None:
+            raise ConanException("MesonToolchain needs 'settings.compiler.version', but it is not defined")
+
         cppstd = self._conanfile.settings.get_safe("compiler.cppstd")
-        self._cpp_std = to_cppstd_flag(compiler, cppstd)
+        self._cpp_std = to_cppstd_flag(compiler, compiler_version, cppstd)
 
         if compiler == "msvc":
             vscrt = msvc_runtime_flag(self._conanfile)
diff --git a/conans/client/profile_loader.py b/conans/client/profile_loader.py
index dde09adc7..42603045d 100644
--- a/conans/client/profile_loader.py
+++ b/conans/client/profile_loader.py
@@ -280,12 +280,19 @@ class _ProfileValueParser(object):
 
         # Create or update the profile
         base_profile = base_profile or Profile()
-        base_profile.system_tools = system_tools
+        current_system_tools = {r.name: r for r in base_profile.system_tools}
+        current_system_tools.update({r.name: r for r in system_tools})
+        base_profile.system_tools = list(current_system_tools.values())
+
         base_profile.settings.update(settings)
         for pkg_name, values_dict in package_settings.items():
             base_profile.package_settings[pkg_name].update(values_dict)
         for pattern, refs in tool_requires.items():
-            base_profile.tool_requires.setdefault(pattern, []).extend(refs)
+            # If the same package, different version is added, the latest version prevail
+            current = base_profile.tool_requires.setdefault(pattern, [])
+            current_dict = {r.name: r for r in current}
+            current_dict.update({r.name: r for r in refs})
+            current[:] = list(current_dict.values())
         if options is not None:
             base_profile.options.update_options(options)
         if conf is not None:


Test patch:
diff --git a/conans/test/unittests/client/build/cpp_std_flags_test.py b/conans/test/unittests/client/build/cpp_std_flags_test.py
index f76c5593e..abe0593d4 100644
--- a/conans/test/unittests/client/build/cpp_std_flags_test.py
+++ b/conans/test/unittests/client/build/cpp_std_flags_test.py
@@ -235,6 +235,7 @@ class CompilerFlagsTest(unittest.TestCase):
         self.assertEqual(_make_cppstd_flag("msvc", "170", "17"), None)
 
         self.assertEqual(_make_cppstd_flag("msvc", "180", "11"), None)
+
         self.assertEqual(_make_cppstd_flag("msvc", "190", "14"), '/std:c++14')
         self.assertEqual(_make_cppstd_flag("msvc", "190", "17"), '/std:c++latest')
 
@@ -244,7 +245,7 @@ class CompilerFlagsTest(unittest.TestCase):
         self.assertEqual(_make_cppstd_flag("msvc", "191", "20"), '/std:c++latest')
 
         self.assertEqual(_make_cppstd_flag("msvc", "192", "17"), '/std:c++17')
-        self.assertEqual(_make_cppstd_flag("msvc", "192", "20"), '/std:c++latest')
+        self.assertEqual(_make_cppstd_flag("msvc", "192", "20"), '/std:c++20')
 
         self.assertEqual(_make_cppstd_flag("msvc", "193", "20"), '/std:c++20')
         self.assertEqual(_make_cppstd_flag("msvc", "193", "23"), '/std:c++latest')
diff --git a/conans/test/unittests/client/profile_loader/profile_loader_test.py b/conans/test/unittests/client/profile_loader/profile_loader_test.py
index 07d9c4ee3..07a23b449 100644
--- a/conans/test/unittests/client/profile_loader/profile_loader_test.py
+++ b/conans/test/unittests/client/profile_loader/profile_loader_test.py
@@ -109,6 +109,47 @@ def test_profiles_includes():
                                            RecipeReference.loads("two/1.2@lasote/stable")]}
 
 
+def test_profile_compose_system_tools():
+    tmp = temp_folder()
+    save(os.path.join(tmp, "profile0"), "[system_tools]\ntool1/1.0")
+    save(os.path.join(tmp, "profile1"), "[system_tools]\ntool2/2.0")
+    save(os.path.join(tmp, "profile2"), "include(./profile0)\n[system_tools]\ntool3/3.0")
+    save(os.path.join(tmp, "profile3"), "include(./profile0)\n[system_tools]\ntool1/1.1")
+
+    profile_loader = ProfileLoader(cache=None)  # If not used cache, will not error
+    profile2 = profile_loader.load_profile("./profile2", tmp)
+    assert profile2.system_tools == [RecipeReference.loads("tool1/1.0"),
+                                     RecipeReference.loads("tool3/3.0")]
+    profile3 = profile_loader.load_profile("./profile3", tmp)
+    assert profile3.system_tools == [RecipeReference.loads("tool1/1.1")]
+    profile0 = profile_loader.load_profile("./profile0", tmp)
+    profile1 = profile_loader.load_profile("./profile1", tmp)
+    profile0.compose_profile(profile1)
+    assert profile0.system_tools == [RecipeReference.loads("tool1/1.0"),
+                                     RecipeReference.loads("tool2/2.0")]
+
+
+def test_profile_compose_tool_requires():
+    tmp = temp_folder()
+    save(os.path.join(tmp, "profile0"), "[tool_requires]\ntool1/1.0")
+    save(os.path.join(tmp, "profile1"), "[tool_requires]\ntool2/2.0")
+    save(os.path.join(tmp, "profile2"), "include(./profile0)\n[tool_requires]\ntool3/3.0")
+    save(os.path.join(tmp, "profile3"), "include(./profile0)\n[tool_requires]\ntool1/1.1")
+
+    profile_loader = ProfileLoader(cache=None)  # If not used cache, will not error
+    profile2 = profile_loader.load_profile("./profile2", tmp)
+    assert profile2.tool_requires == {"*": [RecipeReference.loads("tool1/1.0"),
+                                            RecipeReference.loads("tool3/3.0")]}
+    profile3 = profile_loader.load_profile("./profile3", tmp)
+    assert profile3.tool_requires == {"*": [RecipeReference.loads("tool1/1.1")]}
+
+    profile0 = profile_loader.load_profile("./profile0", tmp)
+    profile1 = profile_loader.load_profile("./profile1", tmp)
+    profile0.compose_profile(profile1)
+    assert profile0.tool_requires == {"*": [RecipeReference.loads("tool1/1.0"),
+                                            RecipeReference.loads("tool2/2.0")]}
+
+
 def test_profile_include_order():
     tmp = temp_folder()