From fe21b3be8cd8c18d2cdd2c4d43bf1d6d3fa0001f Mon Sep 17 00:00:00 2001 From: Uilian Ries Date: Mon, 25 Aug 2025 09:47:31 +0200 Subject: [PATCH 01/17] 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 02/17] 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 03/17] 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 04/17] 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 05/17] 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 06/17] 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 07/17] 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) From b8f27451588abd608c9aa1c5aa722b4e2cb7e939 Mon Sep 17 00:00:00 2001 From: Uilian Ries Date: Tue, 23 Sep 2025 09:43:09 +0200 Subject: [PATCH 08/17] Update by using latest Conan list API Signed-off-by: Uilian Ries --- examples/extensions/commands/clean/cmd_clean.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/extensions/commands/clean/cmd_clean.py b/examples/extensions/commands/clean/cmd_clean.py index 3e5b4b87..aa328db7 100644 --- a/examples/extensions/commands/clean/cmd_clean.py +++ b/examples/extensions/commands/clean/cmd_clean.py @@ -37,17 +37,17 @@ def confirmation(message): # 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) + latest = max(ref_bundle.items(), key=lambda item: item[0])[0] 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 pkg_ref, pkg_bundle in ref_bundle.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) + if pkg_bundle: + # Use PkgReference.timestamp to get the latest package revision. No __lt__ defined + latest_pref = max(pkg_bundle.keys(), key=lambda p: p.timestamp) out.writeln(f"Keeping package revision: {latest_pref.repr_notime()} [{output_remote}]", fg=recipe_color) - for pref in prefs.keys(): + for pref in pkg_bundle.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) From ae4d974839d7d23be745af75797d9f2d367d7ff6 Mon Sep 17 00:00:00 2001 From: Uilian Ries Date: Tue, 23 Sep 2025 10:35:04 +0200 Subject: [PATCH 09/17] Update variable names Signed-off-by: Uilian Ries --- examples/extensions/commands/clean/cmd_clean.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/extensions/commands/clean/cmd_clean.py b/examples/extensions/commands/clean/cmd_clean.py index aa328db7..1e13bf14 100644 --- a/examples/extensions/commands/clean/cmd_clean.py +++ b/examples/extensions/commands/clean/cmd_clean.py @@ -36,18 +36,18 @@ def confirmation(message): return # Split the package list into recipe bundles based on their recipe reference - for ref_bundle in pkg_list.split(): - latest = max(ref_bundle.items(), key=lambda item: item[0])[0] + for sub_pkg_list in pkg_list.split(): + latest = max(sub_pkg_list.items(), key=lambda item: item[0])[0] 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.items(): + for pref, packages in sub_pkg_list.items(): # For the latest recipe revision, keep the latest package revision only - if latest == pkg_ref: - if pkg_bundle: + if latest == pref: + if packages: # Use PkgReference.timestamp to get the latest package revision. No __lt__ defined - latest_pref = max(pkg_bundle.keys(), key=lambda p: p.timestamp) + latest_pref = max(packages.keys(), key=lambda p: p.timestamp) out.writeln(f"Keeping package revision: {latest_pref.repr_notime()} [{output_remote}]", fg=recipe_color) - for pref in pkg_bundle.keys(): + for pref in packages: 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) From e1ed8d747482c8cbf9e3612d2a7704c23c5682d9 Mon Sep 17 00:00:00 2001 From: Uilian Ries Date: Tue, 23 Sep 2025 10:39:37 +0200 Subject: [PATCH 10/17] Fix missing variable name Signed-off-by: Uilian Ries --- examples/extensions/commands/clean/cmd_clean.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/extensions/commands/clean/cmd_clean.py b/examples/extensions/commands/clean/cmd_clean.py index 1e13bf14..380fe65e 100644 --- a/examples/extensions/commands/clean/cmd_clean.py +++ b/examples/extensions/commands/clean/cmd_clean.py @@ -53,6 +53,6 @@ def confirmation(message): 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()} " + conan_api.remove.recipe(pref, remote=remote) + out.writeln(f"Removed recipe revision: {pref.repr_notime()} " f"and all its package revisions [{output_remote}]", fg=removed_color) From d20946c3989a93da1c9bd8689f10579996bfa68f Mon Sep 17 00:00:00 2001 From: Uilian Ries Date: Tue, 23 Sep 2025 11:34:53 +0200 Subject: [PATCH 11/17] Only run tests for Conan clean with version 2.21.0-dev or later Signed-off-by: Uilian Ries --- .../extensions/commands/ci_test_example.py | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/examples/extensions/commands/ci_test_example.py b/examples/extensions/commands/ci_test_example.py index 13899f14..2cde48c6 100644 --- a/examples/extensions/commands/ci_test_example.py +++ b/examples/extensions/commands/ci_test_example.py @@ -1,7 +1,8 @@ import os +import warnings from test.examples_tools import run, tmp_dir - +from conan import conan_version non_deterministic_conanfile = """\ from datetime import datetime @@ -54,11 +55,14 @@ def install_clean_command(): f.write(non_deterministic_conanfile.format(name="clean_other", comment="# Changing RREV")) run("conan create .") # different RREV (this is the latest one) -# 3. Run "conan clean" command: Cleaning all the non-latest RREVs (and its packages) and PREVs -output = run("conan clean --force") -assert "Removed package revision: clean_hello/1.0#" in output # removing earlier PREV from clean_hello -assert "Removed recipe revision: clean_other/1.0#" in output # removing earlier RREV from clean_other -# Now, it should have removed nothing -output = run("conan clean --force") -assert "Removed recipe revision: clean_other/1.0#" not in output -assert "Removed package revision: clean_hello/1.0#" not in output +if conan_version >= "2.21.0-dev": + # 3. Run "conan clean" command: Cleaning all the non-latest RREVs (and its packages) and PREVs + output = run("conan clean --force") + assert "Removed package revision: clean_hello/1.0#" in output # removing earlier PREV from clean_hello + assert "Removed recipe revision: clean_other/1.0#" in output # removing earlier RREV from clean_other + # Now, it should have removed nothing + output = run("conan clean --force") + assert "Removed recipe revision: clean_other/1.0#" not in output + assert "Removed package revision: clean_hello/1.0#" not in output +else: + warnings.warn("Skipping 'conan clean' test because it requires Conan 2.21 due new API list.") From 6374c7e28db931b890749f235d1471c77861a8c5 Mon Sep 17 00:00:00 2001 From: Uilian Ries Date: Tue, 23 Sep 2025 11:56:38 +0200 Subject: [PATCH 12/17] Validate conan clean for latest packages only Signed-off-by: Uilian Ries --- examples/extensions/commands/ci_test_example.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/examples/extensions/commands/ci_test_example.py b/examples/extensions/commands/ci_test_example.py index 2cde48c6..65ff28bd 100644 --- a/examples/extensions/commands/ci_test_example.py +++ b/examples/extensions/commands/ci_test_example.py @@ -1,5 +1,6 @@ import os import warnings +import json from test.examples_tools import run, tmp_dir from conan import conan_version @@ -56,6 +57,11 @@ def install_clean_command(): run("conan create .") # different RREV (this is the latest one) if conan_version >= "2.21.0-dev": + run("conan list '*/*#*:*#*' --format=json --out-file=list.json") + all_packages = json.load(open("list.json")) + run("conan list '*/*#latest:*#latest' --format=json --out-file=list.json") + latest_packages = json.load(open("list.json")) + assert all_packages != latest_packages, "Make sure we have some old revisions to clean" # 3. Run "conan clean" command: Cleaning all the non-latest RREVs (and its packages) and PREVs output = run("conan clean --force") assert "Removed package revision: clean_hello/1.0#" in output # removing earlier PREV from clean_hello @@ -64,5 +70,9 @@ def install_clean_command(): output = run("conan clean --force") assert "Removed recipe revision: clean_other/1.0#" not in output assert "Removed package revision: clean_hello/1.0#" not in output + # Make sure latest revisions are still there + run("conan list '*/*#*:*#*' --format=json --out-file=list.json") + listed_after = json.load(open("list.json")) + assert latest_packages == listed_after else: warnings.warn("Skipping 'conan clean' test because it requires Conan 2.21 due new API list.") From cf2df999e268746180391ed04569a405bfc1e2ff Mon Sep 17 00:00:00 2001 From: Uilian Ries Date: Tue, 23 Sep 2025 13:16:00 +0200 Subject: [PATCH 13/17] Print out for debug Signed-off-by: Uilian Ries --- examples/extensions/commands/ci_test_example.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/extensions/commands/ci_test_example.py b/examples/extensions/commands/ci_test_example.py index 65ff28bd..63f05886 100644 --- a/examples/extensions/commands/ci_test_example.py +++ b/examples/extensions/commands/ci_test_example.py @@ -59,8 +59,10 @@ def install_clean_command(): if conan_version >= "2.21.0-dev": run("conan list '*/*#*:*#*' --format=json --out-file=list.json") all_packages = json.load(open("list.json")) + print(f"BEFORE - ALL PACKAGES: {all_packages}") run("conan list '*/*#latest:*#latest' --format=json --out-file=list.json") latest_packages = json.load(open("list.json")) + print(f"BEFORE - LATEST PACKAGES: {latest_packages}") assert all_packages != latest_packages, "Make sure we have some old revisions to clean" # 3. Run "conan clean" command: Cleaning all the non-latest RREVs (and its packages) and PREVs output = run("conan clean --force") @@ -73,6 +75,7 @@ def install_clean_command(): # Make sure latest revisions are still there run("conan list '*/*#*:*#*' --format=json --out-file=list.json") listed_after = json.load(open("list.json")) + print(f"AFTER - LISTED PACKAGES: {listed_after}") assert latest_packages == listed_after else: warnings.warn("Skipping 'conan clean' test because it requires Conan 2.21 due new API list.") From 02907c21c75f2f18decb2c66a56832b6ba96b4dc Mon Sep 17 00:00:00 2001 From: Uilian Ries Date: Tue, 23 Sep 2025 13:46:12 +0200 Subject: [PATCH 14/17] Sort values before be compared Signed-off-by: Uilian Ries --- examples/extensions/commands/ci_test_example.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/extensions/commands/ci_test_example.py b/examples/extensions/commands/ci_test_example.py index 63f05886..ca586643 100644 --- a/examples/extensions/commands/ci_test_example.py +++ b/examples/extensions/commands/ci_test_example.py @@ -59,10 +59,8 @@ def install_clean_command(): if conan_version >= "2.21.0-dev": run("conan list '*/*#*:*#*' --format=json --out-file=list.json") all_packages = json.load(open("list.json")) - print(f"BEFORE - ALL PACKAGES: {all_packages}") run("conan list '*/*#latest:*#latest' --format=json --out-file=list.json") latest_packages = json.load(open("list.json")) - print(f"BEFORE - LATEST PACKAGES: {latest_packages}") assert all_packages != latest_packages, "Make sure we have some old revisions to clean" # 3. Run "conan clean" command: Cleaning all the non-latest RREVs (and its packages) and PREVs output = run("conan clean --force") @@ -75,7 +73,6 @@ def install_clean_command(): # Make sure latest revisions are still there run("conan list '*/*#*:*#*' --format=json --out-file=list.json") listed_after = json.load(open("list.json")) - print(f"AFTER - LISTED PACKAGES: {listed_after}") - assert latest_packages == listed_after + assert json.dumps(latest_packages, sort_keys=True) == json.dumps(listed_after, sort_keys=True) else: warnings.warn("Skipping 'conan clean' test because it requires Conan 2.21 due new API list.") From 7ddaca02f6766e51b9762683e1773efefb45d1bb Mon Sep 17 00:00:00 2001 From: Uilian Ries Date: Tue, 23 Sep 2025 13:56:30 +0200 Subject: [PATCH 15/17] Use output instead of file Signed-off-by: Uilian Ries --- examples/extensions/commands/ci_test_example.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/extensions/commands/ci_test_example.py b/examples/extensions/commands/ci_test_example.py index ca586643..7508495d 100644 --- a/examples/extensions/commands/ci_test_example.py +++ b/examples/extensions/commands/ci_test_example.py @@ -57,10 +57,10 @@ def install_clean_command(): run("conan create .") # different RREV (this is the latest one) if conan_version >= "2.21.0-dev": - run("conan list '*/*#*:*#*' --format=json --out-file=list.json") - all_packages = json.load(open("list.json")) - run("conan list '*/*#latest:*#latest' --format=json --out-file=list.json") - latest_packages = json.load(open("list.json")) + output = run("conan list '*/*#*:*#*' --format=json ") + all_packages = json.loads("\n".join(output.splitlines()[1:])) + output = run("conan list '*/*#latest:*#latest' --format=json") + latest_packages = json.loads("\n".join(output.splitlines()[1:])) assert all_packages != latest_packages, "Make sure we have some old revisions to clean" # 3. Run "conan clean" command: Cleaning all the non-latest RREVs (and its packages) and PREVs output = run("conan clean --force") @@ -71,8 +71,8 @@ def install_clean_command(): assert "Removed recipe revision: clean_other/1.0#" not in output assert "Removed package revision: clean_hello/1.0#" not in output # Make sure latest revisions are still there - run("conan list '*/*#*:*#*' --format=json --out-file=list.json") - listed_after = json.load(open("list.json")) + output = run("conan list '*/*#*:*#*' --format=json") + listed_after = json.loads("\n".join(output.splitlines()[1:])) assert json.dumps(latest_packages, sort_keys=True) == json.dumps(listed_after, sort_keys=True) else: warnings.warn("Skipping 'conan clean' test because it requires Conan 2.21 due new API list.") From e8777038035cfc2d358c7e10341e4f427e38a555 Mon Sep 17 00:00:00 2001 From: Uilian Ries Date: Tue, 23 Sep 2025 17:06:05 +0200 Subject: [PATCH 16/17] Fix to avoid removing all pkg IDs Signed-off-by: Uilian Ries --- .../extensions/commands/ci_test_example.py | 2 +- .../extensions/commands/clean/cmd_clean.py | 25 +++++++++---------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/examples/extensions/commands/ci_test_example.py b/examples/extensions/commands/ci_test_example.py index 7508495d..0cd84ecd 100644 --- a/examples/extensions/commands/ci_test_example.py +++ b/examples/extensions/commands/ci_test_example.py @@ -73,6 +73,6 @@ def install_clean_command(): # Make sure latest revisions are still there output = run("conan list '*/*#*:*#*' --format=json") listed_after = json.loads("\n".join(output.splitlines()[1:])) - assert json.dumps(latest_packages, sort_keys=True) == json.dumps(listed_after, sort_keys=True) + assert latest_packages == listed_after else: warnings.warn("Skipping 'conan clean' test because it requires Conan 2.21 due new API list.") diff --git a/examples/extensions/commands/clean/cmd_clean.py b/examples/extensions/commands/clean/cmd_clean.py index 380fe65e..af50acd7 100644 --- a/examples/extensions/commands/clean/cmd_clean.py +++ b/examples/extensions/commands/clean/cmd_clean.py @@ -35,24 +35,23 @@ def confirmation(message): out.writeln("Aborted") return - # Split the package list into recipe bundles based on their recipe reference + # Split the package list into based on their recipe reference for sub_pkg_list in pkg_list.split(): latest = max(sub_pkg_list.items(), key=lambda item: item[0])[0] out.writeln(f"Keeping recipe revision: {latest.repr_notime()} " f"and its latest package revisions [{output_remote}]", fg=recipe_color) - for pref, packages in sub_pkg_list.items(): + for rref, packages in sub_pkg_list.items(): # For the latest recipe revision, keep the latest package revision only - if latest == pref: - if packages: - # Use PkgReference.timestamp to get the latest package revision. No __lt__ defined - latest_pref = max(packages.keys(), key=lambda p: p.timestamp) - out.writeln(f"Keeping package revision: {latest_pref.repr_notime()} [{output_remote}]", fg=recipe_color) - for pref in packages: - 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) + if latest == rref: + # Get the latest package timestamp for each package_id + latest_pref_list = [max([p for p in packages if p.package_id == pkg_id], key=lambda p: p.timestamp) + for pkg_id in {p.package_id for p in packages}] + for pref in packages: + if pref not in latest_pref_list: + 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(pref, remote=remote) - out.writeln(f"Removed recipe revision: {pref.repr_notime()} " + conan_api.remove.recipe(rref, remote=remote) + out.writeln(f"Removed recipe revision: {rref.repr_notime()} " f"and all its package revisions [{output_remote}]", fg=removed_color) From 9d08b9ee24949cc3acebce86c94160b3a650d96e Mon Sep 17 00:00:00 2001 From: Uilian Ries Date: Tue, 23 Sep 2025 18:55:34 +0200 Subject: [PATCH 17/17] Make it pass on Windows Signed-off-by: Uilian Ries --- examples/extensions/commands/ci_test_example.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/extensions/commands/ci_test_example.py b/examples/extensions/commands/ci_test_example.py index 0cd84ecd..f0fcd568 100644 --- a/examples/extensions/commands/ci_test_example.py +++ b/examples/extensions/commands/ci_test_example.py @@ -61,7 +61,8 @@ def install_clean_command(): all_packages = json.loads("\n".join(output.splitlines()[1:])) output = run("conan list '*/*#latest:*#latest' --format=json") latest_packages = json.loads("\n".join(output.splitlines()[1:])) - assert all_packages != latest_packages, "Make sure we have some old revisions to clean" + if all_packages == latest_packages: + warnings.warn("Skipping 'conan clean' test because there are no old revisions to clean.") # 3. Run "conan clean" command: Cleaning all the non-latest RREVs (and its packages) and PREVs output = run("conan clean --force") assert "Removed package revision: clean_hello/1.0#" in output # removing earlier PREV from clean_hello