Files
lightweight-cobot/cobot/commands/delete.py
T

317 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import argparse
import shutil
import subprocess
from pathlib import Path
from typing import Optional
from textual.app import App
from cobot.tui import SCREEN_CSS, LogScreen, PickScreen
_PROJECT_DIR = Path(__file__).parent.parent.parent
# Stop and remove all Docker containers whose name contains "lwc".
# Останавливаем и удаляем все Docker-контейнеры, чьё имя содержит "lwc".
def _stop_docker_containers(write) -> None:
"""Stop and force-remove all Docker containers whose name contains "lwc".
Останавливает и принудительно удаляет все Docker-контейнеры с "lwc" в имени.
"""
write("[cyan][*][/cyan] Stopping Docker containers...")
result = subprocess.run(
["docker", "ps", "-a", "--filter", "name=lwc", "--format", "{{.Names}}"],
capture_output=True, text=True,
)
containers = [c for c in result.stdout.strip().splitlines() if c]
if not containers:
write("[dim]No project containers found.[/dim]")
return
for name in containers:
subprocess.run(["docker", "rm", "-f", name], capture_output=True)
write(f"[green][ok][/green] Removed container: {name}")
# Remove all Docker images whose repository or tag contains "lwc".
# Удаляем все Docker-образы, репозиторий или тег которых содержит "lwc".
def _remove_docker_images(write) -> None:
"""Force-remove all local Docker images whose name or tag contains "lwc".
Принудительно удаляет все локальные Docker-образы с "lwc" в имени или теге.
"""
write("[cyan][*][/cyan] Removing Docker images...")
result = subprocess.run(
["docker", "images", "--format", "{{.Repository}}:{{.Tag}}"],
capture_output=True, text=True,
)
project_images = [
img for img in result.stdout.strip().splitlines()
if "lwc" in img.lower()
]
if not project_images:
write("[dim]No project images found.[/dim]")
return
for img in project_images:
subprocess.run(["docker", "rmi", "-f", img], capture_output=True)
write(f"[green][ok][/green] Removed image: {img}")
# Remove the Docker volume that stores the Webots asset cache.
# Удаляем Docker volume с кэшем ассетов Webots.
def _remove_webots_volume(write) -> None:
"""Remove the lwc-webots-cache Docker volume if it exists. Skips silently if absent.
Удаляет Docker volume lwc-webots-cache если он существует. Молча пропускает если отсутствует.
"""
result = subprocess.run(
["docker", "volume", "inspect", "lwc-webots-cache"],
capture_output=True,
)
if result.returncode != 0:
write("[dim]Webots cache volume not found, skipping.[/dim]")
return
subprocess.run(["docker", "volume", "rm", "lwc-webots-cache"], capture_output=True)
write("[green][ok][/green] Removed Docker volume: lwc-webots-cache")
# Remove ROS2 Jazzy packages via apt and clean up the source line from shell configs.
# Uses the official removal commands to also unregister the ROS2 apt repository.
# Удаляем пакеты ROS2 Jazzy через apt и очищаем строку source из конфигов оболочки.
# Используем официальные команды удаления, которые также снимают регистрацию apt-репозитория ROS2.
def _remove_ros2(write) -> None:
"""Remove all ros-jazzy-* packages, the ros2-apt-source package, and the ROS2 source
line from .bashrc / .zshrc. Does nothing if /opt/ros/jazzy is not present.
Удаляет все пакеты ros-jazzy-*, пакет ros2-apt-source и строку source ROS2 из
.bashrc / .zshrc. Ничего не делает если /opt/ros/jazzy отсутствует.
"""
write("[cyan][*][/cyan] Removing ROS2 Jazzy packages...")
if not Path("/opt/ros/jazzy").exists():
write("[dim]ROS2 Jazzy not found, skipping.[/dim]")
else:
# Remove all ros-jazzy-* packages matched by the apt regex pattern ~n<name>.
# Удаляем все пакеты ros-jazzy-* по regex-паттерну apt ~n<имя>.
subprocess.run(
["sudo", "apt", "remove", "-y", "~nros-jazzy-*"],
capture_output=True,
)
subprocess.run(["sudo", "apt", "autoremove", "-y"], capture_output=True)
write("[green][ok][/green] ROS2 Jazzy packages removed")
# Remove the ROS2 apt source package that added the repository.
# Удаляем пакет apt-источника ROS2, который добавил репозиторий.
subprocess.run(
["sudo", "apt", "remove", "-y", "ros2-apt-source"],
capture_output=True,
)
subprocess.run(["sudo", "apt", "update", "-qq"], capture_output=True)
subprocess.run(["sudo", "apt", "autoremove", "-y"], capture_output=True)
write("[green][ok][/green] ROS2 apt repository removed")
# Clean up the source line that local-setup added to the shell config.
# Очищаем строку source, добавленную local-setup в конфиг оболочки.
source_line = "source /opt/ros/jazzy/setup.bash"
for rc_name in [".bashrc", ".zshrc"]:
rc = Path.home() / rc_name
if not rc.exists():
continue
content = rc.read_text()
if source_line not in content:
continue
# Remove the whole block that was added by local-setup, not just the single line.
# Удаляем весь блок добавленный local-setup, а не только одну строку.
new_content = content.replace(f"\n# ROS2 Jazzy\n{source_line}\n", "\n")
new_content = new_content.replace(source_line, "")
rc.write_text(new_content)
write(f"[green][ok][/green] Cleaned up ~/{rc_name}")
# Remove Webots from the system via apt.
# Удаляем Webots из системы через apt.
def _remove_webots(write) -> None:
"""Remove the webots package via apt, run autoremove, and clean up WEBOTS_HOME
from .bashrc / .zshrc. Skips if webots is not found on PATH.
Удаляет пакет webots через apt, запускает autoremove и очищает WEBOTS_HOME из
.bashrc / .zshrc. Пропускает если webots не найден в PATH.
"""
write("[cyan][*][/cyan] Removing Webots...")
if not shutil.which("webots"):
write("[dim]Webots not found, skipping.[/dim]")
return
subprocess.run(["sudo", "apt", "remove", "-y", "webots"], capture_output=True)
subprocess.run(["sudo", "apt", "autoremove", "-y"], capture_output=True)
write("[green][ok][/green] Webots removed")
# Remove the WEBOTS_HOME block that install_webots.sh added to shell configs.
# Удаляем блок WEBOTS_HOME, добавленный install_webots.sh в конфиги оболочки.
for rc_name in [".bashrc", ".zshrc"]:
rc = Path.home() / rc_name
if not rc.exists():
continue
content = rc.read_text()
if "WEBOTS_HOME" not in content:
continue
new_content = content.replace("\n# Webots\nexport WEBOTS_HOME=/usr/local/webots\n", "\n")
new_content = new_content.replace("export WEBOTS_HOME=/usr/local/webots\n", "")
new_content = new_content.replace("# Webots\n", "")
if new_content != content:
rc.write_text(new_content)
write(f"[green][ok][/green] Cleaned WEBOTS_HOME from ~/{rc_name}")
# Uninstall the cobot CLI from the uv tool store.
# Удаляем cobot CLI из хранилища инструментов uv.
def _uninstall_cobot(write) -> None:
"""Uninstall the lightweight-cobot package from the uv tool store.
Удаляет пакет lightweight-cobot из хранилища инструментов uv.
"""
write("[cyan][*][/cyan] Uninstalling cobot CLI...")
result = subprocess.run(
["uv", "tool", "uninstall", "lightweight-cobot"],
capture_output=True, text=True,
)
if result.returncode == 0:
write("[green][ok][/green] cobot uninstalled")
else:
write(f"[yellow]Warning:[/yellow] {result.stderr.strip() or 'could not uninstall cobot'}")
# Delete the entire project directory from disk.
# Удаляем всю директорию проекта с диска.
def _remove_project_dir(write) -> None:
"""Recursively delete the entire project directory (_PROJECT_DIR) from disk.
Рекурсивно удаляет всю директорию проекта (_PROJECT_DIR) с диска.
"""
write(f"[cyan][*][/cyan] Removing project directory...")
try:
shutil.rmtree(_PROJECT_DIR)
write(f"[green][ok][/green] Removed {_PROJECT_DIR}")
except Exception as exc:
write(f"[red]Failed:[/red] {exc}")
raise
# Run all deletion steps in order.
# Progress ranges are split evenly across the active steps so the bar always reaches 100%.
# Выполняем все шаги удаления по порядку.
# Диапазоны прогресса делятся равномерно между активными шагами, чтобы бар всегда доходил до 100%.
def _task_delete(screen: LogScreen, remove_ros: bool, remove_webots: bool) -> None:
"""Worker function that runs inside LogScreen. Runs all deletion steps in order:
containers -> images -> ROS2 (optional) -> Webots (optional) -> cobot CLI -> project dir.
Рабочая функция внутри LogScreen. Выполняет все шаги удаления по порядку:
контейнеры -> образы -> ROS2 (опционально) -> Webots (опционально) -> cobot CLI -> директория.
"""
try:
screen.set_progress(0, "Stopping containers...")
_stop_docker_containers(screen.write)
_remove_webots_volume(screen.write)
screen.set_progress(20, "Removing Docker images...")
_remove_docker_images(screen.write)
pct = 40
if remove_ros:
screen.set_progress(pct, "Removing ROS2 Jazzy...")
_remove_ros2(screen.write)
pct = 65
if remove_webots:
screen.set_progress(pct, "Removing Webots...")
_remove_webots(screen.write)
pct = 75
screen.set_progress(pct, "Uninstalling cobot CLI...")
_uninstall_cobot(screen.write)
screen.set_progress(88, "Removing project directory...")
_remove_project_dir(screen.write)
if not screen.is_stopped():
screen.set_progress(100, "Done")
screen.write("\n[green]Project fully removed.[/green]")
screen.finish(True)
except Exception as exc:
if not screen.is_stopped():
screen.write(f"\n[red]Error:[/red] {exc}")
screen.finish(False)
# Multi-step confirmation wizard before anything is deleted.
# Shows extra questions only when the relevant software is actually installed.
# Многошаговый мастер подтверждения перед удалением.
# Дополнительные вопросы показываются только если соответствующее ПО действительно установлено.
class _DeleteApp(App[None]):
"""Deletion wizard that asks for confirmation, then optionally asks about ROS2 and Webots,
then launches LogScreen running _task_delete.
Мастер удаления: просит подтверждение, затем опционально спрашивает про ROS2 и Webots,
затем запускает LogScreen с _task_delete.
"""
CSS = SCREEN_CSS
def on_mount(self) -> None:
self.push_screen(
PickScreen(
"Confirm deletion",
"This will permanently delete the project, Docker images and containers. Are you sure?",
["No, cancel", "Yes, delete everything"],
"No, cancel",
),
self._on_confirm,
)
def _on_confirm(self, choice: Optional[str]) -> None:
if choice is None or choice.startswith("No"):
self.exit()
return
self.push_screen(
PickScreen(
"ROS2 Jazzy",
"Also remove ROS2 Jazzy from the system?",
["No, keep ROS2", "Yes, remove ROS2 Jazzy"],
"No, keep ROS2",
),
self._on_ros_choice,
)
def _on_ros_choice(self, choice: Optional[str]) -> None:
remove_ros = choice is not None and choice.startswith("Yes")
# Only ask about Webots if it is actually installed on this machine.
# Спрашиваем про Webots только если он действительно установлен на этой машине.
if shutil.which("webots"):
self.push_screen(
PickScreen(
"Webots",
"Also remove Webots from the system?",
["No, keep Webots", "Yes, remove Webots"],
"No, keep Webots",
),
lambda c: self._on_webots_choice(c, remove_ros),
)
else:
self._start_deletion(remove_ros, remove_webots=False)
def _on_webots_choice(self, choice: Optional[str], remove_ros: bool) -> None:
remove_webots = choice is not None and choice.startswith("Yes")
self._start_deletion(remove_ros, remove_webots)
def _start_deletion(self, remove_ros: bool, remove_webots: bool) -> None:
self.push_screen(
LogScreen(
"Deleting project",
lambda s: _task_delete(s, remove_ros, remove_webots),
show_progress=True,
),
lambda _: self.exit(),
)
def register(subparsers: argparse._SubParsersAction) -> None:
p = subparsers.add_parser(
"delete",
help="Remove the project, Docker images, containers, and optionally ROS2",
)
p.set_defaults(func=run)
def run(args: argparse.Namespace) -> None:
_DeleteApp().run()