From b72e9df8fd5de5c48e45d1b073017061a0ff7ad6 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sun, 29 Sep 2024 20:44:51 +0800 Subject: [PATCH 1/3] refactor(self): move self-cleaning/uninstallation out of the CLI function --- ruyi/cli/self_cli.py | 44 ++++++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/ruyi/cli/self_cli.py b/ruyi/cli/self_cli.py index bcc1f6d..3f2e6eb 100644 --- a/ruyi/cli/self_cli.py +++ b/ruyi/cli/self_cli.py @@ -46,24 +46,44 @@ def cli_self_uninstall(cfg: config.GlobalConfig, args: argparse.Namespace) -> in else: log.I("uninstallation consent given over CLI, proceeding") - if purge: + _do_reset( + cfg, + installed_pkgs=purge, + all_state=purge, + all_cache=purge, + self_binary=True, + ) + + log.I("[yellow]ruyi[/yellow] is uninstalled") + + return 0 + + +def _do_reset( + cfg: config.GlobalConfig, + *, + installed_pkgs: bool = False, + all_state: bool = False, + all_cache: bool = False, + self_binary: bool = False, +) -> None: + if installed_pkgs: log.I("removing installed packages") shutil.rmtree(cfg.data_root, True) + if all_state: log.I("removing state data") shutil.rmtree(cfg.state_root, True) + if all_cache: log.I("removing cached data") shutil.rmtree(cfg.cache_root, True) - log.I("removing the ruyi binary") - try: - os.unlink(ruyi.self_exe()) - except FileNotFoundError: - # we might have already removed ourselves during the purge; nothing to - # do now. - pass - - log.I("[yellow]ruyi[/yellow] is uninstalled") - - return 0 + if self_binary: + log.I("removing the ruyi binary") + try: + os.unlink(ruyi.self_exe()) + except FileNotFoundError: + # we might have already removed ourselves during the purge; nothing to + # do now. + pass From af336b5181a613f905eb58729debed9b21223285 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sun, 29 Sep 2024 21:28:04 +0800 Subject: [PATCH 2/3] feat(self): add `ruyi self clean` Fixes: #200 --- ruyi/cli/__init__.py | 39 +++++++++++++++++++- ruyi/cli/self_cli.py | 82 +++++++++++++++++++++++++++++++++++++++-- ruyi/config/__init__.py | 7 +++- ruyi/telemetry/store.py | 5 ++- 4 files changed, 124 insertions(+), 9 deletions(-) diff --git a/ruyi/cli/__init__.py b/ruyi/cli/__init__.py index bb7c1a2..157d10f 100644 --- a/ruyi/cli/__init__.py +++ b/ruyi/cli/__init__.py @@ -34,7 +34,7 @@ def init_argparse() -> argparse.ArgumentParser: from ..ruyipkg.profile_cli import cli_list_profiles from ..ruyipkg.update_cli import cli_update from ..version import RUYI_SEMVER - from .self_cli import cli_self_uninstall + from .self_cli import cli_self_clean, cli_self_uninstall from .version_cli import cli_version native_host_str = get_native_host() @@ -295,6 +295,43 @@ def init_argparse() -> argparse.ArgumentParser: title="subcommands", ) + self_clean = selfsp.add_parser( + "clean", + help="Remove various Ruyi-managed data to reclaim storage", + ) + self_clean.add_argument( + "--quiet", + "-q", + action="store_true", + help="Do not print out the actions being performed", + ) + self_clean.add_argument( + "--distfiles", + action="store_true", + help="Remove all downloaded distfiles if any", + ) + self_clean.add_argument( + "--installed-pkgs", + action="store_true", + help="Remove all installed packages if any", + ) + self_clean.add_argument( + "--progcache", + action="store_true", + help="Clear the Ruyi program cache", + ) + self_clean.add_argument( + "--repo", + action="store_true", + help="Remove the Ruyi repo if located in Ruyi-managed cache directory", + ) + self_clean.add_argument( + "--telemetry", + action="store_true", + help="Remove all telemetry data recorded if any", + ) + self_clean.set_defaults(func=cli_self_clean, tele_key="self clean") + self_uninstall = selfsp.add_parser( "uninstall", help="Uninstall Ruyi", diff --git a/ruyi/cli/self_cli.py b/ruyi/cli/self_cli.py index 3f2e6eb..138b836 100644 --- a/ruyi/cli/self_cli.py +++ b/ruyi/cli/self_cli.py @@ -1,5 +1,6 @@ import argparse import os +import pathlib import shutil import ruyi @@ -20,6 +21,34 @@ """ +def cli_self_clean(cfg: config.GlobalConfig, args: argparse.Namespace) -> int: + quiet: bool = args.quiet + distfiles: bool = args.distfiles + installed_pkgs: bool = args.installed_pkgs + progcache: bool = args.progcache + repo: bool = args.repo + telemetry: bool = args.telemetry + + if not any([distfiles, installed_pkgs, progcache, repo, telemetry]): + log.F("no data specified for cleaning") + log.I( + "please check [yellow]ruyi self clean --help[/] for a list of cleanable data" + ) + return 1 + + _do_reset( + cfg, + quiet=quiet, + distfiles=distfiles, + installed_pkgs=installed_pkgs, + progcache=progcache, + repo=repo, + telemetry=telemetry, + ) + + return 0 + + def cli_self_uninstall(cfg: config.GlobalConfig, args: argparse.Namespace) -> int: purge: bool = args.purge consent: bool = args.consent @@ -48,6 +77,7 @@ def cli_self_uninstall(cfg: config.GlobalConfig, args: argparse.Namespace) -> in _do_reset( cfg, + quiet=False, installed_pkgs=purge, all_state=purge, all_cache=purge, @@ -61,26 +91,70 @@ def cli_self_uninstall(cfg: config.GlobalConfig, args: argparse.Namespace) -> in def _do_reset( cfg: config.GlobalConfig, + quiet: bool = False, *, installed_pkgs: bool = False, all_state: bool = False, + telemetry: bool = False, # ignored if all_state=True all_cache: bool = False, + distfiles: bool = False, # ignored if all_cache=True + progcache: bool = False, # ignored if all_cache=True + repo: bool = False, # ignored if all_cache=True self_binary: bool = False, ) -> None: + def status(s: str) -> None: + if quiet: + return + log.I(s) + if installed_pkgs: - log.I("removing installed packages") + status("removing installed packages") shutil.rmtree(cfg.data_root, True) if all_state: - log.I("removing state data") + status("removing state data") shutil.rmtree(cfg.state_root, True) + else: + if telemetry: + status("removing all telemetry data") + shutil.rmtree(cfg.telemetry_root, True) if all_cache: - log.I("removing cached data") + status("removing cached data") shutil.rmtree(cfg.cache_root, True) + else: + if distfiles: + status("removing downloaded distfiles") + # TODO: deduplicate the path derivation + shutil.rmtree(os.path.join(cfg.cache_root, "distfiles"), True) + + if progcache: + status("clearing the Ruyi program cache") + # TODO: deduplicate the path derivation + shutil.rmtree(os.path.join(cfg.cache_root, "progcache"), True) + + if repo: + # for safety, don't remove the repo if it's outside of Ruyi's XDG + # cache root + repo_dir = pathlib.Path(cfg.get_repo_dir()).resolve() + cache_root = pathlib.Path(cfg.cache_root).resolve() + + repo_is_below_cache_root = False + for p in repo_dir.parents: + if p == cache_root: + repo_is_below_cache_root = True + break + + if not repo_is_below_cache_root: + log.W( + "not removing the Ruyi repo: it is outside of the Ruyi cache directory" + ) + else: + status("removing the Ruyi repo") + shutil.rmtree(repo_dir, True) if self_binary: - log.I("removing the ruyi binary") + status("removing the ruyi binary") try: os.unlink(ruyi.self_exe()) except FileNotFoundError: diff --git a/ruyi/config/__init__.py b/ruyi/config/__init__.py index 062000b..4b004e2 100644 --- a/ruyi/config/__init__.py +++ b/ruyi/config/__init__.py @@ -139,6 +139,10 @@ def news_read_status(self) -> NewsReadStatusStore: self._news_read_status_store = NewsReadStatusStore(filename) return self._news_read_status_store + @property + def telemetry_root(self) -> os.PathLike[Any]: + return pathlib.Path(self.ensure_state_dir()) / "telemetry" + @property def telemetry(self) -> TelemetryStore | None: if self.telemetry_mode == "off": @@ -147,8 +151,7 @@ def telemetry(self) -> TelemetryStore | None: return self._telemetry_store local_mode = self.telemetry_mode == "local" - dirname = os.path.join(self.ensure_state_dir(), "telemetry") - self._telemetry_store = TelemetryStore(dirname, local_mode) + self._telemetry_store = TelemetryStore(self.telemetry_root, local_mode) return self._telemetry_store @property diff --git a/ruyi/telemetry/store.py b/ruyi/telemetry/store.py index 10ae3e8..70a0e7e 100644 --- a/ruyi/telemetry/store.py +++ b/ruyi/telemetry/store.py @@ -1,7 +1,8 @@ import json +import os import pathlib import time -from typing import TypedDict +from typing import Any, TypedDict import uuid from .. import log @@ -15,7 +16,7 @@ class TelemetryEvent(TypedDict): class TelemetryStore: - def __init__(self, store_root: str, local: bool) -> None: + def __init__(self, store_root: os.PathLike[Any], local: bool) -> None: self.store_root = pathlib.Path(store_root) self.local = local self._events: list[TelemetryEvent] = [] From ecb949dca193f930f5430a20ae3a8b2fbef31ac2 Mon Sep 17 00:00:00 2001 From: WANG Xuerui Date: Sun, 29 Sep 2024 21:39:36 +0800 Subject: [PATCH 3/3] fix(cli): fix runtime function signature mismatches and missing telemetry keys --- ruyi/cli/__init__.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/ruyi/cli/__init__.py b/ruyi/cli/__init__.py index 157d10f..75ef59d 100644 --- a/ruyi/cli/__init__.py +++ b/ruyi/cli/__init__.py @@ -2,7 +2,7 @@ import atexit import os import sys -from typing import Callable, List +from typing import Callable, IO, List, Protocol from ..config import GlobalConfig @@ -24,6 +24,18 @@ def is_called_as_ruyi(argv0: str) -> bool: CLIEntrypoint = Callable[[GlobalConfig, argparse.Namespace], int] +class _PrintHelp(Protocol): + def print_help(self, file: IO[str] | None = None) -> None: ... + + +def _wrap_help(x: _PrintHelp) -> CLIEntrypoint: + def _wrapped_(gc: GlobalConfig, args: argparse.Namespace) -> int: + x.print_help() + return 0 + + return _wrapped_ + + def init_argparse() -> argparse.ArgumentParser: from ..device.provision_cli import cli_device_provision from ..mux.venv.venv_cli import cli_venv @@ -43,7 +55,7 @@ def init_argparse() -> argparse.ArgumentParser: prog=RUYI_ENTRYPOINT_NAME, description=f"RuyiSDK Package Manager {RUYI_SEMVER}", ) - root.set_defaults(func=lambda _: root.print_help()) + root.set_defaults(func=_wrap_help(root), tele_key="") root.add_argument( "-V", @@ -69,7 +81,7 @@ def init_argparse() -> argparse.ArgumentParser: "device", help="Manage devices", ) - device.set_defaults(func=lambda _: device.print_help()) + device.set_defaults(func=_wrap_help(device), tele_key="device") devicesp = device.add_subparsers( title="subcommands", ) @@ -150,7 +162,7 @@ def init_argparse() -> argparse.ArgumentParser: "news", help="List and read news items from configured repository", ) - news.set_defaults(func=lambda _: news.print_help()) + news.set_defaults(func=_wrap_help(news), tele_key="news") newssp = news.add_subparsers(title="subcommands") news_list = newssp.add_parser( @@ -239,7 +251,7 @@ def init_argparse() -> argparse.ArgumentParser: # help=argparse.SUPPRESS, help="(NOT FOR REGULAR USERS) Subcommands for managing Ruyi repos", ) - admin.set_defaults(func=lambda _: admin.print_help()) + admin.set_defaults(func=_wrap_help(admin), tele_key="admin") adminsp = admin.add_subparsers( title="subcommands", ) @@ -290,7 +302,7 @@ def init_argparse() -> argparse.ArgumentParser: "self", help="Manage this Ruyi installation", ) - self.set_defaults(func=lambda _: self.print_help()) + self.set_defaults(func=_wrap_help(self), tele_key="self") selfsp = self.add_subparsers( title="subcommands", )