From fe21b3be8cd8c18d2cdd2c4d43bf1d6d3fa0001f Mon Sep 17 00:00:00 2001 From: Uilian Ries Date: Mon, 25 Aug 2025 09:47:31 +0200 Subject: [PATCH 1/7] Trigger CI Signed-off-by: Uilian Ries --- examples/extensions/commands/clean/cmd_clean.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/extensions/commands/clean/cmd_clean.py b/examples/extensions/commands/clean/cmd_clean.py index fb22e14a..8bab4e12 100644 --- a/examples/extensions/commands/clean/cmd_clean.py +++ b/examples/extensions/commands/clean/cmd_clean.py @@ -6,6 +6,7 @@ recipe_color = Color.BRIGHT_BLUE removed_color = Color.BRIGHT_YELLOW +# TRIGGER CI @conan_command(group="Custom commands") def clean(conan_api: ConanAPI, parser, *args): From 3005ea6bd8c926678846b853a714fe8049bd559d Mon Sep 17 00:00:00 2001 From: Uilian Ries Date: Mon, 25 Aug 2025 10:07:59 +0200 Subject: [PATCH 2/7] Align with Conan develop2 Signed-off-by: Uilian Ries --- .../extensions/commands/clean/cmd_clean.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/examples/extensions/commands/clean/cmd_clean.py b/examples/extensions/commands/clean/cmd_clean.py index 8bab4e12..f92230c5 100644 --- a/examples/extensions/commands/clean/cmd_clean.py +++ b/examples/extensions/commands/clean/cmd_clean.py @@ -6,7 +6,25 @@ recipe_color = Color.BRIGHT_BLUE removed_color = Color.BRIGHT_YELLOW -# TRIGGER CI +def _search_recipes(app, query: str, remote=None): + """" + Searches recipes in local cache or in a remote. + Extracted from conan/api/subapi/list.py + """ + only_none_user_channel = False + if query and query.endswith("@"): + only_none_user_channel = True + query = query[:-1] + + if remote: + refs = app.remote_manager.search_recipes(remote, query) + else: + refs = app.cache.search_recipes(query) + ret = [] + for r in refs: + if not only_none_user_channel or (r.user is None and r.channel is None): + ret.append(r) + return sorted(ret) @conan_command(group="Custom commands") def clean(conan_api: ConanAPI, parser, *args): @@ -29,7 +47,7 @@ def confirmation(message): output_remote = remote or "Local cache" # Getting all the recipes - recipes = conan_api.search.recipes("*/*", remote=remote) + recipes = _search_recipes(conan_api.app, "*/*", remote=remote) if recipes and not confirmation("Do you want to remove all the recipes revisions and their packages ones, " "except the latest package revision from the latest recipe one?"): return From 1660eb1febecb9bf4a79f32dbfd73c31a0561dc3 Mon Sep 17 00:00:00 2001 From: Uilian Ries Date: Mon, 25 Aug 2025 10:19:17 +0200 Subject: [PATCH 3/7] Create Conan app Signed-off-by: Uilian Ries --- examples/extensions/commands/clean/cmd_clean.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/extensions/commands/clean/cmd_clean.py b/examples/extensions/commands/clean/cmd_clean.py index f92230c5..b975a2c8 100644 --- a/examples/extensions/commands/clean/cmd_clean.py +++ b/examples/extensions/commands/clean/cmd_clean.py @@ -1,4 +1,5 @@ from conan.api.conan_api import ConanAPI +from conan.internal.conan_app import ConanBasicApp from conan.api.input import UserInput from conan.api.output import ConanOutput, Color from conan.cli.command import OnceArgument, conan_command @@ -47,7 +48,8 @@ def confirmation(message): output_remote = remote or "Local cache" # Getting all the recipes - recipes = _search_recipes(conan_api.app, "*/*", remote=remote) + conan_app - ConanBasicApp(conan_api) + recipes = _search_recipes(conan_app, "*/*", remote=remote) if recipes and not confirmation("Do you want to remove all the recipes revisions and their packages ones, " "except the latest package revision from the latest recipe one?"): return From 4014bbb85cd946562e92871966b3fa5ba9ff09e3 Mon Sep 17 00:00:00 2001 From: Uilian Ries Date: Mon, 25 Aug 2025 10:41:17 +0200 Subject: [PATCH 4/7] Fix bad definition Signed-off-by: Uilian Ries --- examples/extensions/commands/clean/cmd_clean.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/extensions/commands/clean/cmd_clean.py b/examples/extensions/commands/clean/cmd_clean.py index b975a2c8..0f436e20 100644 --- a/examples/extensions/commands/clean/cmd_clean.py +++ b/examples/extensions/commands/clean/cmd_clean.py @@ -48,7 +48,7 @@ def confirmation(message): output_remote = remote or "Local cache" # Getting all the recipes - conan_app - ConanBasicApp(conan_api) + conan_app = ConanBasicApp(conan_api) recipes = _search_recipes(conan_app, "*/*", remote=remote) if recipes and not confirmation("Do you want to remove all the recipes revisions and their packages ones, " "except the latest package revision from the latest recipe one?"): From 91894c3a02966fa3a12eb040e794c83a8e20417f Mon Sep 17 00:00:00 2001 From: Uilian Ries Date: Mon, 25 Aug 2025 16:10:26 +0200 Subject: [PATCH 5/7] Use Conan list API properly Signed-off-by: Uilian Ries --- .../extensions/commands/clean/cmd_clean.py | 62 ++++++------------- 1 file changed, 20 insertions(+), 42 deletions(-) diff --git a/examples/extensions/commands/clean/cmd_clean.py b/examples/extensions/commands/clean/cmd_clean.py index 0f436e20..119f7c39 100644 --- a/examples/extensions/commands/clean/cmd_clean.py +++ b/examples/extensions/commands/clean/cmd_clean.py @@ -1,5 +1,5 @@ from conan.api.conan_api import ConanAPI -from conan.internal.conan_app import ConanBasicApp +from conan.api.model import PackagesList, ListPattern from conan.api.input import UserInput from conan.api.output import ConanOutput, Color from conan.cli.command import OnceArgument, conan_command @@ -7,25 +7,6 @@ recipe_color = Color.BRIGHT_BLUE removed_color = Color.BRIGHT_YELLOW -def _search_recipes(app, query: str, remote=None): - """" - Searches recipes in local cache or in a remote. - Extracted from conan/api/subapi/list.py - """ - only_none_user_channel = False - if query and query.endswith("@"): - only_none_user_channel = True - query = query[:-1] - - if remote: - refs = app.remote_manager.search_recipes(remote, query) - else: - refs = app.cache.search_recipes(query) - ret = [] - for r in refs: - if not only_none_user_channel or (r.user is None and r.channel is None): - ret.append(r) - return sorted(ret) @conan_command(group="Custom commands") def clean(conan_api: ConanAPI, parser, *args): @@ -47,27 +28,24 @@ def confirmation(message): remote = conan_api.remotes.get(args.remote) if args.remote else None output_remote = remote or "Local cache" - # Getting all the recipes - conan_app = ConanBasicApp(conan_api) - recipes = _search_recipes(conan_app, "*/*", remote=remote) - if recipes and not confirmation("Do you want to remove all the recipes revisions and their packages ones, " + # Get all recipes and packages, where recipe revision is not the latest + pkg_list = conan_api.list.select(ListPattern("*/*#!latest:*#*", rrev=None, prev=None), remote=remote) + if pkg_list and not confirmation("Do you want to remove all the recipes revisions and their packages ones, " "except the latest package revision from the latest recipe one?"): + out.writeln("Aborted") return - for recipe in recipes: - out.writeln(f"{str(recipe)}", fg=recipe_color) - all_rrevs = conan_api.list.recipe_revisions(recipe, remote=remote) - latest_rrev = all_rrevs[0] if all_rrevs else None - for rrev in all_rrevs: - if rrev != latest_rrev: - conan_api.remove.recipe(rrev, remote=remote) - out.writeln(f"Removed recipe revision: {rrev.repr_notime()} " - f"and all its package revisions [{output_remote}]", fg=removed_color) - else: - packages = conan_api.list.packages_configurations(rrev, remote=remote) - for package_ref in packages: - all_prevs = conan_api.list.package_revisions(package_ref, remote=remote) - latest_prev = all_prevs[0] if all_prevs else None - for prev in all_prevs: - if prev != latest_prev: - conan_api.remove.package(prev, remote=remote) - out.writeln(f"Removed package revision: {prev.repr_notime()} [{output_remote}]", fg=removed_color) + + # Remove all packages for old recipe revisions + for recipe_ref, recipe_bundle in pkg_list.refs().items(): + conan_api.remove.recipe(recipe_ref, remote=remote) + out.writeln(f"Removed recipe revision: {recipe_ref.repr_notime()} " + f"and all its package revisions [{output_remote}]", fg=removed_color) + + # Get all package revisions from the latest recipe revision, except the latest package revision + pkg_list = conan_api.list.select(ListPattern("*/*#latest:*#!latest", rrev=None, prev=None), remote=remote) + for recipe_ref, recipe_bundle in pkg_list.refs().items(): + pkg_list = PackagesList.prefs(recipe_ref, recipe_bundle) + for pkg_ref in pkg_list.keys(): + # Remove all package revisions except the latest one + conan_api.remove.package(pkg_ref, remote=remote) + out.writeln(f"Removed package revision: {pkg_ref.repr_notime()} [{output_remote}]", fg=removed_color) \ No newline at end of file From 48c713521f1e8deea677a70bf75f064266ca1b8d Mon Sep 17 00:00:00 2001 From: Uilian Ries Date: Mon, 25 Aug 2025 17:52:54 +0200 Subject: [PATCH 6/7] Simplify query Signed-off-by: Uilian Ries --- examples/extensions/commands/clean/cmd_clean.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/extensions/commands/clean/cmd_clean.py b/examples/extensions/commands/clean/cmd_clean.py index 119f7c39..7c9e71e3 100644 --- a/examples/extensions/commands/clean/cmd_clean.py +++ b/examples/extensions/commands/clean/cmd_clean.py @@ -29,23 +29,23 @@ def confirmation(message): output_remote = remote or "Local cache" # Get all recipes and packages, where recipe revision is not the latest - pkg_list = conan_api.list.select(ListPattern("*/*#!latest:*#*", rrev=None, prev=None), remote=remote) + pkg_list = conan_api.list.select(ListPattern("*/*#!latest", rrev=None, prev=None), remote=remote) if pkg_list and not confirmation("Do you want to remove all the recipes revisions and their packages ones, " "except the latest package revision from the latest recipe one?"): out.writeln("Aborted") return # Remove all packages for old recipe revisions - for recipe_ref, recipe_bundle in pkg_list.refs().items(): + for recipe_ref in pkg_list.refs().keys(): conan_api.remove.recipe(recipe_ref, remote=remote) out.writeln(f"Removed recipe revision: {recipe_ref.repr_notime()} " f"and all its package revisions [{output_remote}]", fg=removed_color) # Get all package revisions from the latest recipe revision, except the latest package revision - pkg_list = conan_api.list.select(ListPattern("*/*#latest:*#!latest", rrev=None, prev=None), remote=remote) + pkg_list = conan_api.list.select(ListPattern("*/*:*#!latest", rrev=None, prev=None), remote=remote) for recipe_ref, recipe_bundle in pkg_list.refs().items(): pkg_list = PackagesList.prefs(recipe_ref, recipe_bundle) for pkg_ref in pkg_list.keys(): # Remove all package revisions except the latest one conan_api.remove.package(pkg_ref, remote=remote) - out.writeln(f"Removed package revision: {pkg_ref.repr_notime()} [{output_remote}]", fg=removed_color) \ No newline at end of file + out.writeln(f"Removed package revision: {pkg_ref.repr_notime()} [{output_remote}]", fg=removed_color) From 729e9f76314ba31f2b433d03ee3ce524960488ec Mon Sep 17 00:00:00 2001 From: Uilian Ries Date: Tue, 26 Aug 2025 16:27:10 +0200 Subject: [PATCH 7/7] Use a single select Signed-off-by: Uilian Ries --- .../extensions/commands/clean/cmd_clean.py | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/examples/extensions/commands/clean/cmd_clean.py b/examples/extensions/commands/clean/cmd_clean.py index 7c9e71e3..3e5b4b87 100644 --- a/examples/extensions/commands/clean/cmd_clean.py +++ b/examples/extensions/commands/clean/cmd_clean.py @@ -28,24 +28,31 @@ def confirmation(message): remote = conan_api.remotes.get(args.remote) if args.remote else None output_remote = remote or "Local cache" - # Get all recipes and packages, where recipe revision is not the latest - pkg_list = conan_api.list.select(ListPattern("*/*#!latest", rrev=None, prev=None), remote=remote) + # List all recipes revisions and all their packages revisions as well + pkg_list = conan_api.list.select(ListPattern("*/*#*:*#*", rrev=None, prev=None), remote=remote) if pkg_list and not confirmation("Do you want to remove all the recipes revisions and their packages ones, " "except the latest package revision from the latest recipe one?"): out.writeln("Aborted") return - # Remove all packages for old recipe revisions - for recipe_ref in pkg_list.refs().keys(): - conan_api.remove.recipe(recipe_ref, remote=remote) - out.writeln(f"Removed recipe revision: {recipe_ref.repr_notime()} " - f"and all its package revisions [{output_remote}]", fg=removed_color) - - # Get all package revisions from the latest recipe revision, except the latest package revision - pkg_list = conan_api.list.select(ListPattern("*/*:*#!latest", rrev=None, prev=None), remote=remote) - for recipe_ref, recipe_bundle in pkg_list.refs().items(): - pkg_list = PackagesList.prefs(recipe_ref, recipe_bundle) - for pkg_ref in pkg_list.keys(): - # Remove all package revisions except the latest one - conan_api.remove.package(pkg_ref, remote=remote) - out.writeln(f"Removed package revision: {pkg_ref.repr_notime()} [{output_remote}]", fg=removed_color) + # Split the package list into recipe bundles based on their recipe reference + for ref_bundle in pkg_list.split(): + latest = max(ref_bundle.refs(), key=lambda r: r.revision) + out.writeln(f"Keeping recipe revision: {latest.repr_notime()} " + f"and its latest package revisions [{output_remote}]", fg=recipe_color) + for pkg_ref, pkg_bundle in ref_bundle.refs().items(): + # For the latest recipe revision, keep the latest package revision only + if latest == pkg_ref: + prefs = PackagesList.prefs(latest, pkg_bundle) + if prefs: + latest_pref = max(prefs.keys(), key=lambda p: p.revision) + out.writeln(f"Keeping package revision: {latest_pref.repr_notime()} [{output_remote}]", fg=recipe_color) + for pref in prefs.keys(): + if latest_pref != pref: + conan_api.remove.package(pref, remote=remote) + out.writeln(f"Removed package revision: {pref.repr_notime()} [{output_remote}]", fg=removed_color) + else: + # Otherwise, remove all outdated recipe revisions and their packages + conan_api.remove.recipe(pkg_ref, remote=remote) + out.writeln(f"Removed recipe revision: {pkg_ref.repr_notime()} " + f"and all its package revisions [{output_remote}]", fg=removed_color)