Repository: conan-io/conan
Pull request: feature conan cache check-integrity
Changelog: Feature: New ``conan cache check-integrity`` command to replace 1.X legacy ``conan upload --skip-upload --check``.
Docs: https://github.com/conan-io/docs/pull/3147


Close https://github.com/conan-io/conan/issues/13499


Problem statement:
[question] conan2 local integrity check of cache contents based on manifest
### What is your question?

Hi!

We started an experimental migration of our packages to conan 2.x.

In our integration with conan 1.x we used the following to check the integrity of packages in the local cache based on the manifest:

`conan upload --skip-upload --all -c '*'`

In conan 2.x `--skip-upload` is gone, thus the above does not seem to be possible any more.

**Question:**
- Is a local integrity check based on the manifest possible in some other way or is the recommended way to implement a custom command for this?
- Perhaps a `conan cache check PATTERN` sub-command to conan cache would be the right place for such a feature

### Have you read the CONTRIBUTING guide?

- [X] I've read the CONTRIBUTING guide

Hints from issue discussion:
Hi @bbandix 


It is true that Conan 2.0 removed this "skip-upload" case that can be leveraged for checking packages checksums.
I think we might want to introduce this check, probably as a ``conan cache`` subcommand, labeling this as a possible feature.

In the meantime, it should be relatively easy to create your own custom command to do that, because the api exposes it, something like this could be enough:
```python
    ref_pattern = ListPattern(args.reference, package_id="*", only_recipe=args.only_recipe)
    package_list = conan_api.list.select(ref_pattern, package_query=args.package_query)
    conan_api.cache.check_integrity(package_list)
```

The https://docs.conan.io/2/examples/extensions/commands/clean/custom_command_clean_revisions.html docs might help.
Hi @memsharded,
Thanks again for the quick answer!

Commit message: feature conan cache check-integrity

Patch to review:
diff --git a/conan/cli/commands/cache.py b/conan/cli/commands/cache.py
index ea03cec7bf2..28ddf486789 100644
--- a/conan/cli/commands/cache.py
+++ b/conan/cli/commands/cache.py
@@ -77,3 +77,19 @@ def cache_clean(conan_api: ConanAPI, parser, subparser, *args):
     package_list = conan_api.list.select(ref_pattern, package_query=args.package_query)
     conan_api.cache.clean(package_list, source=args.source, build=args.build,
                           download=args.download)
+
+
+@conan_subcommand(formatters={"text": cli_out_write})
+def cache_check_integrity(conan_api: ConanAPI, parser, subparser, *args):
+    """
+    Show the path to the Conan cache for a given reference.
+    """
+    subparser.add_argument("pattern", help="Selection pattern for references to clean")
+    subparser.add_argument('-p', '--package-query', action=OnceArgument,
+                           help="Only the packages matching a specific query, e.g., "
+                                "os=Windows AND (arch=x86 OR compiler=gcc)")
+    args = parser.parse_args(*args)
+
+    ref_pattern = ListPattern(args.pattern, rrev="*", package_id="*", prev="*")
+    package_list = conan_api.list.select(ref_pattern, package_query=args.package_query)
+    conan_api.cache.check_integrity(package_list)
diff --git a/conans/search/search.py b/conans/search/search.py
index 7dbf2a8c6b5..6af6217dc8d 100644
--- a/conans/search/search.py
+++ b/conans/search/search.py
@@ -4,7 +4,6 @@
 from fnmatch import translate
 from typing import Dict
 
-from conan.api.output import ConanOutput
 from conans.errors import ConanException
 from conans.model.info import load_binary_info
 from conans.model.package_ref import PkgReference
@@ -115,8 +114,8 @@ def get_cache_packages_binary_info(cache, prefs) -> Dict[PkgReference, dict]:
         # Read conaninfo
         info_path = os.path.join(pkg_layout.package(), CONANINFO)
         if not os.path.exists(info_path):
-            ConanOutput().error("There is no conaninfo.txt: %s" % str(info_path))
-            continue
+            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)
diff --git a/conans/test/integration/command_v2/test_cache_integrity.py b/conans/test/integration/command_v2/test_cache_integrity.py
new file mode 100644
index 00000000000..090e72204d1
--- /dev/null
+++ b/conans/test/integration/command_v2/test_cache_integrity.py
@@ -0,0 +1,28 @@
+import os
+
+from conans.test.assets.genconanfile import GenConanfile
+from conans.test.utils.tools import TestClient
+from conans.util.files import save
+
+
+def test_cache_integrity():
+    t = TestClient()
+    t.save({"conanfile.py": GenConanfile()})
+    t.run("create . --name pkg1 --version 1.0")
+    t.run("create . --name pkg2 --version=2.0")
+    pref = t.created_package_reference("pkg2/2.0")
+    layout = t.get_latest_pkg_layout(pref)
+    conaninfo = os.path.join(layout.package(), "conaninfo.txt")
+    save(conaninfo, "[settings]")
+    t.run("create . --name pkg3 --version=3.0")
+    pref = t.created_package_reference("pkg3/3.0")
+    layout = t.get_latest_pkg_layout(pref)
+    conaninfo = os.path.join(layout.package(), "conaninfo.txt")
+    save(conaninfo, "[settings]")
+
+    t.run("cache check-integrity *", assert_error=True)
+    assert "pkg1/1.0: Integrity checked: ok" in t.out
+    assert "pkg1/1.0:da39a3ee5e6b4b0d3255bfef95601890afd80709: Integrity checked: ok" in t.out
+    assert "ERROR: pkg2/2.0:da39a3ee5e6b4b0d3255bfef95601890afd80709: Manifest mismatch" in t.out
+    assert "ERROR: pkg3/3.0:da39a3ee5e6b4b0d3255bfef95601890afd80709: Manifest mismatch" in t.out
+    


Code review comments:
diff_hunk: @@ -77,3 +77,19 @@ def cache_clean(conan_api: ConanAPI, parser, subparser, *args):
     package_list = conan_api.list.select(ref_pattern, package_query=args.package_query)
     conan_api.cache.clean(package_list, source=args.source, build=args.build,
                           download=args.download)
+
+
+@conan_subcommand(formatters={"text": cli_out_write})
+def cache_check_integrity(conan_api: ConanAPI, parser, subparser, *args):
+    """
+    Show the path to the Conan cache for a given reference.
+    """
+    subparser.add_argument("pattern", help="Selection pattern for references to clean")
path: conan/cli/commands/cache.py
text: @user1:
```suggestion
    subparser.add_argument("pattern", help="Selection pattern for references to check integrity for")
```

diff_hunk: @@ -77,3 +77,19 @@ def cache_clean(conan_api: ConanAPI, parser, subparser, *args):
     package_list = conan_api.list.select(ref_pattern, package_query=args.package_query)
     conan_api.cache.clean(package_list, source=args.source, build=args.build,
                           download=args.download)
+
+
+@conan_subcommand(formatters={"text": cli_out_write})
+def cache_check_integrity(conan_api: ConanAPI, parser, subparser, *args):
+    """
+    Show the path to the Conan cache for a given reference.
path: conan/cli/commands/cache.py
text: @user1:
```suggestion
    Check the integrity of the local cache for the given references
```