diff --git a/conan/api/subapi/list.py b/conan/api/subapi/list.py index 343992ac0a2..f748de9ac48 100644 --- a/conan/api/subapi/list.py +++ b/conan/api/subapi/list.py @@ -6,7 +6,7 @@ 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 +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 @@ -87,7 +87,35 @@ def filter_packages_configurations(pkg_configurations, query): """ return filter_packages(query, pkg_configurations) - def select(self, pattern, package_query=None, remote=None, lru=None): + @staticmethod + def filter_packages_profile(packages, profile, ref): + result = {} + profile_settings = profile.processed_settings.serialize() + # Options are those for dependencies, like *:shared=True + profile_options = profile.options._deps_package_options + for pref, data in packages.items(): + settings = data.get("settings", {}) + settings_match = options_match = True + for k, v in settings.items(): # Only the defined settings that don't match + value = profile_settings.get(k) + if value is not None and value != v: + settings_match = False + break + options = data.get("options", {}) + for k, v in options.items(): + for pattern, pattern_options in profile_options.items(): + if ref_matches(ref, pattern, None): + value = pattern_options.get_safe(k) + if value is not None and value != v: + options_match = False + break + + if settings_match and options_match: + result[pref] = data + + return result + + def select(self, pattern, package_query=None, remote=None, lru=None, profile=None): if package_query and pattern.package_id and "*" not in pattern.package_id: raise ConanException("Cannot specify '-p' package queries, " "if 'package_id' is not a pattern") @@ -150,6 +178,8 @@ def msg_format(msg, item, total): packages = self.packages_configurations(rrev, remote) if package_query is not None: packages = self.filter_packages_configurations(packages, package_query) + if profile is not None: + packages = self.filter_packages_profile(packages, profile, rrev) prefs = packages.keys() prefs = pattern.filter_prefs(prefs) packages = {pref: conf for pref, conf in packages.items() if pref in prefs} diff --git a/conan/cli/commands/list.py b/conan/cli/commands/list.py index 61b067a766d..743c30ac0dc 100644 --- a/conan/cli/commands/list.py +++ b/conan/cli/commands/list.py @@ -212,6 +212,12 @@ def list(conan_api: ConanAPI, parser, *args): parser.add_argument('-p', '--package-query', default=None, action=OnceArgument, help="List only the packages matching a specific query, e.g, os=Windows AND " "(arch=x86 OR compiler=gcc)") + parser.add_argument('-fp', '--filter-profile', action="append", + help="Profiles to filter the binaries") + parser.add_argument('-fs', '--filter-settings', action="append", + help="Settings to filter the binaries") + parser.add_argument('-fo', '--filter-options', action="append", + help="Options to filter the binaries") parser.add_argument("-r", "--remote", default=None, action="append", help="Remote names. Accepts wildcards ('*' means all the remotes available)") parser.add_argument("-c", "--cache", action='store_true', help="Search in the local cache") @@ -227,10 +233,13 @@ def list(conan_api: ConanAPI, parser, *args): if args.pattern is None and args.graph is None: raise ConanException("Missing pattern or graph json file") - if args.pattern and args.graph: - raise ConanException("Cannot define both the pattern and the graph json file") - if args.graph and args.lru: - raise ConanException("Cannot define lru when loading a graph json file") + if args.graph: # a few arguments are not compatible with this + if args.pattern: + raise ConanException("Cannot define both the pattern and the graph json file") + if args.lru: + raise ConanException("Cannot define lru when loading a graph json file") + if args.filter_profile or args.filter_settings or args.filter_options: + raise ConanException("Filtering binaries cannot be done when loading a graph json file") if (args.graph_recipes or args.graph_binaries) and not args.graph: raise ConanException("--graph-recipes and --graph-binaries require a --graph input") if args.remote and args.lru: @@ -245,8 +254,12 @@ def list(conan_api: ConanAPI, parser, *args): pkglist = MultiPackagesList() if args.cache or not args.remote: try: + profile = conan_api.profiles.get_profile(args.filter_profile or [], + args.filter_settings, + args.filter_options) \ + if args.filter_profile or args.filter_settings or args.filter_options else None cache_list = conan_api.list.select(ref_pattern, args.package_query, remote=None, - lru=args.lru) + lru=args.lru, profile=profile) except Exception as e: pkglist.add_error("Local Cache", str(e)) else: diff --git a/conans/test/integration/command_v2/list_test.py b/conans/test/integration/command_v2/list_test.py index 47abd3c4f11..a7f9c109baa 100644 --- a/conans/test/integration/command_v2/list_test.py +++ b/conans/test/integration/command_v2/list_test.py @@ -793,3 +793,64 @@ def test_list_compact_no_settings_no_options(self): """) assert expected == expected_output + + +class TestListBinaryFilter: + def test_list_filter(self): + c = TestClient() + c.save({"pkg/conanfile.py": GenConanfile("pkg", "1.0").with_settings("os", "arch") + .with_shared_option(False), + "header/conanfile.py": GenConanfile("header", "1.0"), + "profile_linux": "[settings]\nos=Linux", + "profile_armv8": "[settings]\narch=armv8", + "profile_shared": "[options]\n*:shared=True"}) + c.run("create pkg -s os=Windows -s arch=x86") + c.run("create pkg -s os=Linux -s arch=armv8") + c.run("create pkg -s os=Macos -s arch=armv8 -o shared=True") + c.run("create header") + + c.run("list *:* -fp=profile_linux --format=json") + result = json.loads(c.stdout) + header = result["Local Cache"]["header/1.0"]["revisions"]["747cc49983b14bdd00df50a0671bd8b3"] + assert header["packages"] == {"da39a3ee5e6b4b0d3255bfef95601890afd80709": {"info": {}}} + pkg = result["Local Cache"]["pkg/1.0"]["revisions"]["03591c8b22497dd74214e08b3bf2a56f"] + assert len(pkg["packages"]) == 1 + settings = pkg["packages"]["2d46abc802bbffdf2af11591e3e452bc6149ea2b"]["info"]["settings"] + assert settings == {"arch": "armv8", "os": "Linux"} + + # for linux + x86 only the header-only is a match + c.run("list *:* -fp=profile_linux -fs=arch=x86 --format=json") + result = json.loads(c.stdout) + header = result["Local Cache"]["header/1.0"]["revisions"]["747cc49983b14bdd00df50a0671bd8b3"] + assert header["packages"] == {"da39a3ee5e6b4b0d3255bfef95601890afd80709": {"info": {}}} + pkg = result["Local Cache"]["pkg/1.0"]["revisions"]["03591c8b22497dd74214e08b3bf2a56f"] + assert pkg["packages"] == {} + + c.run("list *:* -fp=profile_armv8 --format=json") + result = json.loads(c.stdout) + header = result["Local Cache"]["header/1.0"]["revisions"]["747cc49983b14bdd00df50a0671bd8b3"] + assert header["packages"] == {"da39a3ee5e6b4b0d3255bfef95601890afd80709": {"info": {}}} + pkg = result["Local Cache"]["pkg/1.0"]["revisions"]["03591c8b22497dd74214e08b3bf2a56f"] + assert len(pkg["packages"]) == 2 + settings = pkg["packages"]["2d46abc802bbffdf2af11591e3e452bc6149ea2b"]["info"]["settings"] + assert settings == {"arch": "armv8", "os": "Linux"} + settings = pkg["packages"]["2a67a51fbf36a4ee345b2125dd2642be60ffd3ec"]["info"]["settings"] + assert settings == {"arch": "armv8", "os": "Macos"} + + c.run("list *:* -fp=profile_shared --format=json") + result = json.loads(c.stdout) + header = result["Local Cache"]["header/1.0"]["revisions"]["747cc49983b14bdd00df50a0671bd8b3"] + assert header["packages"] == {"da39a3ee5e6b4b0d3255bfef95601890afd80709": {"info": {}}} + pkg = result["Local Cache"]["pkg/1.0"]["revisions"]["03591c8b22497dd74214e08b3bf2a56f"] + assert len(pkg["packages"]) == 1 + settings = pkg["packages"]["2a67a51fbf36a4ee345b2125dd2642be60ffd3ec"]["info"]["settings"] + assert settings == {"arch": "armv8", "os": "Macos"} + + c.run("list *:* -fs os=Windows -fo *:shared=False --format=json") + result = json.loads(c.stdout) + header = result["Local Cache"]["header/1.0"]["revisions"]["747cc49983b14bdd00df50a0671bd8b3"] + assert header["packages"] == {"da39a3ee5e6b4b0d3255bfef95601890afd80709": {"info": {}}} + pkg = result["Local Cache"]["pkg/1.0"]["revisions"]["03591c8b22497dd74214e08b3bf2a56f"] + assert len(pkg["packages"]) == 1 + settings = pkg["packages"]["d2e97769569ac0a583d72c10a37d5ca26de7c9fa"]["info"]["settings"] + assert settings == {"arch": "x86", "os": "Windows"}