Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion charmcraft/application/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@
)
from charmcraft.application.commands.version import Version

from charmcraft.application.commands.purge import PurgeCommand


def fill_command_groups(app: craft_application.Application) -> None:
"""Fill in all the command groups for Charmcraft."""
Expand Down Expand Up @@ -119,7 +121,8 @@ def fill_command_groups(app: craft_application.Application) -> None:
Analyse,
Analyze,
InitCommand,
Version,
Version,
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing whitespace on this line. Most linters flag this as a style issue.

Suggested change
Version,
Version,

Copilot uses AI. Check for mistakes.
PurgeCommand,
],
)

Expand Down
153 changes: 153 additions & 0 deletions charmcraft/application/commands/purge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
from __future__ import annotations
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing copyright header. All files in this codebase should start with a copyright header following the pattern seen in other command files (Copyright year(s) Canonical Ltd., Apache License 2.0, and "For further info..." comment). See charmcraft/application/commands/base.py:1-15 or charmcraft/application/commands/version.py:1-15 for examples.

Copilot uses AI. Check for mistakes.

import json
import shutil
import subprocess
import textwrap
from pathlib import Path

from charmcraft.application.commands.base import CharmcraftCommand
from craft_cli import emit

Comment on lines +1 to +11
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing docstring for the module. Other command modules in this codebase include a docstring describing the purpose of the module (e.g., "Infrastructure for the 'init' command." in init.py or "Version command." in version.py).

Copilot uses AI. Check for mistakes.

class PurgeCommand(CharmcraftCommand):
name = "purge"
help_msg = "Remove cached data and build containers"
overview = textwrap.dedent(
"""
Remove cached data and build containers created by Charmcraft.

By default, this command removes the pip cache and stopped build
containers. Running containers are preserved unless explicitly
requested.
Comment on lines +20 to +22
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overview mentions "pip cache" but the implementation removes the entire charmcraft cache directory (not just pip cache). The cache directory contains more than just pip cache - it's used by craft-parts for package management. The help text should describe what's actually being removed: "charmcraft cache" or "charmcraft cache directory" rather than "pip cache".

Copilot uses AI. Check for mistakes.

Use this command to recover disk space or clean up broken build
environments.
"""
)

def fill_parser(self, parser) -> None:
super().fill_parser(parser)

parser.add_argument(
"--only-cache",
action="store_true",
help="Clear only the pip cache",
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The help text mentions "pip cache" but should say "charmcraft cache" to accurately reflect what's being cleared. The cache directory contains more than just pip-related data.

Copilot uses AI. Check for mistakes.
)
parser.add_argument(
"--only-builders",
action="store_true",
help="Remove only stopped build containers",
)
parser.add_argument(
"--include-running",
action="store_true",
help="Include running build containers",
)
parser.add_argument(
"--all",
action="store_true",
help="Remove all caches and build containers, including bases",
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The --all flag description mentions "including bases" but the implementation doesn't do anything special with bases. This flag only sets include_running to True, which affects running containers. Either the implementation should be enhanced to handle bases, or the help text should be corrected to not mention bases.

Copilot uses AI. Check for mistakes.
)


# Public entry point


def run(self, parsed_args) -> None:
Comment on lines +53 to +57
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These comment lines appear to be placeholder comments for code organization but are inconsistent with the codebase style. Consider removing them or replacing with proper docstrings if these sections need documentation.

Suggested change
# Public entry point
def run(self, parsed_args) -> None:
def run(self, parsed_args) -> None:
"""Execute the purge command based on the provided arguments."""

Copilot uses AI. Check for mistakes.
# Resolve behavior
purge_cache = True
purge_builders = True
include_running = parsed_args.include_running

if parsed_args.only_cache:
purge_builders = False

if parsed_args.only_builders:
purge_cache = False

if parsed_args.all:
purge_cache = True
purge_builders = True
include_running = True

# Execute
if purge_cache:
self._purge_cache()

if purge_builders:
self._purge_build_containers(include_running)

emit.message("Purge complete.")


# Cache handling


def _purge_cache(self) -> None:
cache_dir = Path.home() / ".cache" / "charmcraft"
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cache directory is hardcoded to Path.home() / ".cache" / "charmcraft", which doesn't respect XDG environment variables or platform conventions. The codebase uses platformdirs.user_cache_path(appname="charmcraft") in charmcraft/env.py:37 for determining the cache path, which properly handles XDG_CACHE_HOME and platform-specific conventions. This method should be used instead to ensure consistency and proper cross-platform support.

Copilot uses AI. Check for mistakes.

if not cache_dir.exists():
emit.message("No Charmcraft cache found.")
return

emit.progress(f"Removing cache directory: {cache_dir}")
shutil.rmtree(cache_dir)
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling for shutil.rmtree. If the removal fails due to permission errors or file system issues, the exception will propagate uncaught. Consider wrapping this in a try-except block and providing a user-friendly error message using emit.warning or similar, as done in _list_lxd_containers at lines 126-131.

Suggested change
shutil.rmtree(cache_dir)
try:
shutil.rmtree(cache_dir)
except OSError as exc:
emit.warning(f"Failed to remove cache directory {cache_dir}: {exc}")

Copilot uses AI. Check for mistakes.


# LXD handling

Comment on lines +97 to +99
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar placeholder comment style as above. Consider removing or replacing with proper docstrings.

Copilot uses AI. Check for mistakes.

def _purge_build_containers(self, include_running: bool) -> None:
containers = self._list_lxd_containers()
builders = self._filter_builder_containers(
containers, include_running=include_running
)

if not builders:
emit.message("No build containers to remove.")
return

for name in builders:
emit.progress(f"Removing build container: {name}")
subprocess.run(
["lxc", "delete", "--force", name],
check=True,
)
Comment on lines +113 to +116
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling for subprocess.run when deleting containers. If the deletion fails (e.g., due to LXD daemon issues, permission problems, or container state issues), the CalledProcessError will propagate uncaught and crash the command. This could leave the command in an inconsistent state where some containers are deleted but others are not. Consider wrapping this in a try-except block to handle failures gracefully and continue attempting to delete other containers, similar to the error handling pattern used in _list_lxd_containers.

Suggested change
subprocess.run(
["lxc", "delete", "--force", name],
check=True,
)
try:
subprocess.run(
["lxc", "delete", "--force", name],
check=True,
)
except FileNotFoundError:
emit.warning("LXD is not installed; stopping container cleanup.")
break
except subprocess.CalledProcessError as exc:
emit.warning(f"Failed to delete LXD container {name!r}: {exc}")

Copilot uses AI. Check for mistakes.

def _list_lxd_containers(self) -> list[dict]:
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dict type annotation should specify the expected structure. Based on the usage in _filter_builder_containers, containers have at least "name" and "status" keys. Consider using a more specific type like list[dict[str, Any]] or defining a TypedDict for the container structure to improve type safety and documentation.

Copilot uses AI. Check for mistakes.
try:
result = subprocess.run(
["lxc", "list", "--format=json"],
capture_output=True,
text=True,
check=True,
)
except FileNotFoundError:
emit.warning("LXD is not installed; skipping container cleanup.")
return []
except subprocess.CalledProcessError as exc:
emit.warning(f"Failed to list LXD containers: {exc}")
return []

return json.loads(result.stdout)
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling for json.loads. If the LXD output is malformed or not valid JSON, this will raise a JSONDecodeError and crash the command. Consider wrapping this in a try-except block and returning an empty list with a warning message if JSON parsing fails.

Suggested change
return json.loads(result.stdout)
try:
return json.loads(result.stdout)
except json.JSONDecodeError as exc:
emit.warning(f"Failed to parse LXD JSON output: {exc}; skipping container cleanup.")
return []

Copilot uses AI. Check for mistakes.

def _filter_builder_containers(
self, containers: list[dict], *, include_running: bool
) -> list[str]:
builders: list[str] = []

for container in containers:
name = container.get("name", "")
status = container.get("status", "").lower()

# Charmcraft / craft-application builders use this prefix
if not name.startswith("craft-"):
continue

if status == "running" and not include_running:
continue

builders.append(name)

return builders
Comment on lines +13 to +153
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for the new PurgeCommand. Other commands in the codebase have corresponding test files (e.g., tests/unit/commands/test_lifecycle.py for lifecycle commands). The command has several behaviors that should be tested: cache removal, container filtering, error handling for missing LXD, different flag combinations (--only-cache, --only-builders, --include-running, --all), and edge cases like permission errors or malformed JSON from LXD.

Copilot uses AI. Check for mistakes.