Repository: conan-io/conan
Pull request #17763 (2025-02-12 18:01:13)

workspace clean proposal
Changelog: Feature: New ``conan workspace clean`` command, removes the ``output-folder`` of editables if defined, otherwise nothing. Can be custom implemented by users in the ``conanws.py`` file.
Docs: https://github.com/conan-io/docs/pull/XXXX




Patch:
diff --git a/conan/api/subapi/workspace.py b/conan/api/subapi/workspace.py
index 176cce746..10d04f774 100644
--- a/conan/api/subapi/workspace.py
+++ b/conan/api/subapi/workspace.py
@@ -205,6 +205,10 @@ class WorkspaceAPI:
         self._check_ws()
         return self._ws.remove(path)
 
+    def clean(self):
+        self._check_ws()
+        return self._ws.clean()
+
     def info(self):
         self._check_ws()
         return {"name": self.name,
diff --git a/conan/cli/commands/workspace.py b/conan/cli/commands/workspace.py
index 0dfbf2a27..871ae2bcb 100644
--- a/conan/cli/commands/workspace.py
+++ b/conan/cli/commands/workspace.py
@@ -216,6 +216,15 @@ def workspace_install(conan_api: ConanAPI, parser, subparser, *args):
                                        envs_generation=args.envs_generation)
 
 
+@conan_subcommand()
+def workspace_clean(conan_api: ConanAPI, parser, subparser, *args):
+    """
+    Clean the temporary build folders when possible
+    """
+    parser.parse_args(*args)
+    conan_api.workspace.clean()
+
+
 @conan_command(group="Consumer")
 def workspace(conan_api, parser, *args):  # noqa
     """
diff --git a/conan/internal/model/workspace.py b/conan/internal/model/workspace.py
index b9f9f6b87..ec3b096bc 100644
--- a/conan/internal/model/workspace.py
+++ b/conan/internal/model/workspace.py
@@ -1,4 +1,5 @@
 import os
+import shutil
 
 import yaml
 
@@ -19,7 +20,7 @@ class Workspace:
         self.output = ConanOutput(scope=self.name())
 
     def name(self):
-        return os.path.basename(self.folder)
+        return self.conan_data.get("name") or os.path.basename(self.folder)
 
     def _conan_load_data(self):
         data_path = os.path.join(self.folder, "conanws.yml")
@@ -60,6 +61,19 @@ class Workspace:
         save(os.path.join(self.folder, "conanws.yml"), yaml.dump(self.conan_data))
         return found_ref
 
+    def clean(self):
+        self.output.info("Default workspace clean: Removing the output-folder of each editable")
+        for ref, info in self.conan_data.get("editables", {}).items():
+            if not info.get("output_folder"):
+                self.output.info(f"Editable {ref} doesn't have an output_folder defined")
+                continue
+            of = os.path.join(self.folder, info["output_folder"])
+            try:
+                self.output.info(f"Removing {ref} output folder: {of}")
+                shutil.rmtree(of)
+            except OSError as e:
+                self.output.warning(f"Error removing {ref} output folder: {str(e)}")
+
     def _conan_rel_path(self, path):
         if path is None:
             return None


Test patch:
diff --git a/test/integration/workspace/test_workspace.py b/test/integration/workspace/test_workspace.py
index f7220fdcb..19095daa7 100644
--- a/test/integration/workspace/test_workspace.py
+++ b/test/integration/workspace/test_workspace.py
@@ -583,3 +583,42 @@ def test_workspace_with_local_recipes_index():
     assert "zlib/1.2.11" in c.out  # It doesn't crash
     c.run("list zlib/1.2.11#*")
     assert "zlib/1.2.11" not in c.out
+
+
+class TestClean:
+    def test_clean(self):
+        # Using cmake_layout, we can clean the build folders
+        c = TestClient()
+        c.save({"conanws.yml": "name: my_workspace"})
+        pkga = GenConanfile("pkga", "0.1").with_settings("build_type")
+        pkgb = GenConanfile("pkgb", "0.1").with_requires("pkga/0.1").with_settings("build_type")
+        c.save({"pkga/conanfile.py": pkga ,
+                "pkgb/conanfile.py": pkgb,
+                "pkgc/conanfile.py": GenConanfile("pkgc", "0.1")})
+        c.run("workspace add pkga -of=build/pkga")
+        c.run("workspace add pkgb -of=build/pkgb --product")
+        c.run("workspace add pkgc")
+        c.run("workspace build")
+        assert os.path.exists(os.path.join(c.current_folder, "build", "pkga"))
+        assert os.path.exists(os.path.join(c.current_folder, "build", "pkgb"))
+        c.run("workspace clean")
+        assert "my_workspace: Removing pkga/0.1 output folder" in c.out
+        assert "my_workspace: Removing pkgb/0.1 output folder" in c.out
+        assert "Editable pkgc/0.1 doesn't have an output_folder defined" in c.out
+        assert not os.path.exists(os.path.join(c.current_folder, "build", "pkga"))
+        assert not os.path.exists(os.path.join(c.current_folder, "build", "pkgb"))
+
+    def test_custom_clean(self):
+        conanfilews = textwrap.dedent("""
+            from conan import Workspace
+
+            class Ws(Workspace):
+                def name(self):
+                    return "my_workspace"
+                def clean(self):
+                    self.output.info("MY CLEAN!!!!")
+            """)
+        c = TestClient()
+        c.save({"conanws.py": conanfilews})
+        c.run("workspace clean")
+        assert "my_workspace: MY CLEAN!!!" in c.out