Repository: conan-io/conan

Issue:
[QoL] Allow `conan remove -t *`
In the CI with the cache, it's needed to reinstall system requirements when the machine has been changed. Currently, it's only supported with a full reference.


Discussion:
Consider #4370
I agree. But I would say the command should be `conan remove "*" -t`

(An internal note for the dev team or any other collaborator that develop this feature:
Calling directly the "cache" from the command.py is not correct and it should have a dedicated function in the conan_api. `self._cache.remove_package_system_reqs(ref)`)
Should it remove global system requirements, or only from all packages?
What tools do we have to remove only global system requirements from conan cli? 
I'm not sure it makes sense to remove global system requirements too. Or better to say - applying to this use case it makes sense, but I feel like the usage would be a bit confusing and a little bit unexpected.
So maybe we could create a new option for global requirements

Anyway, that would be a different feature. Let's keep things clear.

Resolving patch:
diff --git a/conans/client/command.py b/conans/client/command.py
--- a/conans/client/command.py
+++ b/conans/client/command.py
@@ -16,7 +16,7 @@
 from conans.client.printer import Printer
 from conans.errors import ConanException, ConanInvalidConfiguration, NoRemoteAvailable, \
     ConanMigrationError
-from conans.model.ref import ConanFileReference, PackageReference
+from conans.model.ref import ConanFileReference, PackageReference, check_valid_ref
 from conans.unicode import get_cwd
 from conans.util.config_parser import get_bool_from_text
 from conans.util.files import exception_message_safe
@@ -938,10 +938,6 @@ def remove(self, *args):
 
         self._warn_python2()
 
-        # NOTE: returns the expanded pattern (if a pattern was given), and checks
-        # that the query parameter wasn't abused
-        reference = self._check_query_parameter(args.pattern_or_reference, args.query)
-
         if args.packages is not None and args.query:
             raise ConanException("'-q' and '-p' parameters can't be used at the same time")
 
@@ -958,16 +954,16 @@ def remove(self, *args):
             self._user_io.out.info("Cache locks removed")
             return
         elif args.system_reqs:
-            if not reference:
-                raise ConanException("Please specify a valid package reference to be cleaned")
             if args.packages:
                 raise ConanException("'-t' and '-p' parameters can't be used at the same time")
-            try:
-                self._conan.remove_system_reqs(reference)
-                self._user_io.out.info("Cache system_reqs from %s has been removed" % reference)
-                return
-            except Exception as error:
-                raise ConanException("Unable to remove system_reqs: %s" % error)
+            if not args.pattern_or_reference:
+                raise ConanException("Please specify a valid pattern or reference to be cleaned")
+
+            if check_valid_ref(args.pattern_or_reference, allow_pattern=False):
+                ref = ConanFileReference.loads(args.pattern_or_reference)
+                return self._conan.remove_system_reqs(ref)
+
+            return self._conan.remove_system_reqs_by_pattern(args.pattern_or_reference)
         else:
             if not args.pattern_or_reference:
                 raise ConanException('Please specify a pattern to be removed ("*" for all)')
diff --git a/conans/client/conan_api.py b/conans/client/conan_api.py
--- a/conans/client/conan_api.py
+++ b/conans/client/conan_api.py
@@ -53,6 +53,7 @@
 from conans.model.version import Version
 from conans.model.workspace import Workspace
 from conans.paths import BUILD_INFO, CONANINFO, get_conan_user_home
+from conans.search.search import search_recipes
 from conans.tools import set_global_instances
 from conans.unicode import get_cwd
 from conans.util.env_reader import get_env
@@ -962,13 +963,22 @@ def remote_clean(self):
         return self._cache.registry.clear()
 
     @api_method
-    def remove_locks(self):
-        self._cache.remove_locks()
+    def remove_system_reqs(self, ref):
+        try:
+            self._cache.package_layout(ref).remove_system_reqs()
+            self._user_io.out.info(
+                "Cache system_reqs from %s has been removed" % repr(ref))
+        except Exception as error:
+            raise ConanException("Unable to remove system_reqs: %s" % error)
 
     @api_method
-    def remove_system_reqs(self, reference):
-        ref = ConanFileReference.loads(reference)
-        self._cache.package_layout(ref).remove_system_reqs()
+    def remove_system_reqs_by_pattern(self, pattern):
+        for ref in search_recipes(self._cache, pattern=pattern):
+            self.remove_system_reqs(ref)
+
+    @api_method
+    def remove_locks(self):
+        self._cache.remove_locks()
 
     @api_method
     def profile_list(self):


Test patch:
diff --git a/conans/test/integration/system_reqs_test.py b/conans/test/integration/system_reqs_test.py
--- a/conans/test/integration/system_reqs_test.py
+++ b/conans/test/integration/system_reqs_test.py
@@ -177,11 +177,44 @@ def remove_system_reqs_test(self):
         self.assertIn("*+Running system requirements+*", client.user_io.out)
         self.assertTrue(os.path.exists(system_reqs_path))
 
+        # Wildcard system_reqs removal
+        ref_other = ConanFileReference.loads("Test/0.1@user/channel_other")
+        system_reqs_path_other = os.path.dirname(client.cache.package_layout(ref_other).system_reqs())
+
+        client.run("create . user/channel_other")
+        client.run("remove --system-reqs '*'")
+        self.assertIn("Cache system_reqs from Test/0.1@user/channel has been removed",
+                      client.user_io.out)
+        self.assertIn("Cache system_reqs from Test/0.1@user/channel_other has been removed",
+                      client.user_io.out)
+        self.assertFalse(os.path.exists(system_reqs_path))
+        self.assertFalse(os.path.exists(system_reqs_path_other))
+
+        # Check that wildcard isn't triggered randomly
+        client.run("create . user/channel_other")
+        client.run("remove --system-reqs Test/0.1@user/channel")
+        self.assertIn("Cache system_reqs from Test/0.1@user/channel has been removed",
+                      client.user_io.out)
+        self.assertNotIn("Cache system_reqs from Test/0.1@user/channel_other has been removed",
+                      client.user_io.out)
+        self.assertFalse(os.path.exists(system_reqs_path))
+        self.assertTrue(os.path.exists(system_reqs_path_other))
+
+        # Check partial wildcard
+        client.run("create . user/channel")
+        client.run("remove --system-reqs Test/0.1@user/channel_*")
+        self.assertNotIn("Cache system_reqs from Test/0.1@user/channel has been removed",
+                      client.user_io.out)
+        self.assertIn("Cache system_reqs from Test/0.1@user/channel_other has been removed",
+                      client.user_io.out)
+        self.assertTrue(os.path.exists(system_reqs_path))
+        self.assertFalse(os.path.exists(system_reqs_path_other))
+
     def invalid_remove_reqs_test(self):
         client = TestClient()
 
         with six.assertRaisesRegex(self, Exception,
-                                   "ERROR: Please specify a valid package reference to be cleaned"):
+                                   "ERROR: Please specify a valid pattern or reference to be cleaned"):
             client.run("remove --system-reqs")
 
         # wrong file reference should be treated as error