Первый выпуск SysDevCleaner

Анализ Windows и очистка кешей, мусора и остатков разработки.

Ядро:
- каталог правил: 228 правил путей (496 шаблонов) + 43 правила артефактов
  проектов — Windows, браузеры, кеши сборки, IDE, приложения, пакетные
  менеджеры;
- рекурсивный поиск node_modules, target, build, __pycache__, .venv и т.п.
  с проверкой маркеров проекта, чтобы не ловить ложные срабатывания;
- поиск крупных файлов и дубликатов (хеш по началу/середине/концу и полная
  сверка), инвентаризация установленных программ через реестр;
- ограничение области сканирования по дискам;
- рекомендации и текстовый отчёт.

Безопасность:
- четыре уровня риска, автоматически отмечается только «Безопасно»;
- список защищённых путей проверяется и при выборе, и перед удалением;
- для каталогов вроде %TEMP% удаляется содержимое, а не сам каталог;
- удаление в Корзину по умолчанию, пробный запуск, журнал очисток.

Интерфейс на Dear PyGui:
- JetBrains Mono Nerd Font: кириллица и иконки в одном шрифте;
- тёмная и светлая темы, адаптивная раскладка;
- заголовок окна ставится через SetWindowTextW — Dear PyGui отдаёт его
  в ANSI-функцию как UTF-8 и портит кириллицу;
- запуск всегда с правами администратора: манифест UAC в exe и
  самоперезапуск через ShellExecuteW при работе из исходников.

Сборка в один exe через PyInstaller со вшитым шрифтом и значком.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-11 14:55:00 +03:00
commit b01cf37e66
33 changed files with 6472 additions and 0 deletions

201
build_exe.py Normal file
View File

@@ -0,0 +1,201 @@
#!/usr/bin/env python3
"""Сборка SysDevCleaner в исполняемый файл через PyInstaller.
Примеры:
python build_exe.py один файл SysDevCleaner.exe: шрифт вшит,
запуск сразу с правами администратора
python build_exe.py --onedir папка dist\\SysDevCleaner (запускается быстрее)
python build_exe.py --no-admin без манифеста UAC
python build_exe.py --no-font не вшивать шрифт (возьмётся системный)
python build_exe.py --console оставить окно консоли (для отладки)
Права администратора нужны для Windows\\Temp, кеша обновлений, Prefetch и
Windows.old — без них самая крупная добыча остаётся недоступной. Поэтому
манифест UAC встраивается по умолчанию.
Про шрифт: без вшитого TTF на чужой машине без JetBrains Mono NF интерфейс
откатится на Segoe UI — кириллица будет, иконки пропадут. Поэтому по умолчанию
скрипт копирует найденный Nerd Font в папку ``fonts`` и вкладывает её в сборку.
"""
from __future__ import annotations
import argparse
import os
import shutil
import subprocess
import sys
ROOT = os.path.dirname(os.path.abspath(__file__))
FONTS_DIR = os.path.join(ROOT, "fonts")
ICON = os.path.join(ROOT, "sysdevcleaner.ico")
NAME = "SysDevCleaner"
# что искать для вшивания: обычное начертание + жирное + моноширинное
WANTED_FONTS = (
"JetBrainsMonoNerdFont-Regular.ttf",
"JetBrainsMonoNerdFont-Bold.ttf",
"JetBrainsMonoNerdFontMono-Regular.ttf",
)
FALLBACK_FONTS = ("segoeui.ttf", "segoeuib.ttf", "consola.ttf")
def log(msg: str) -> None:
print(f" {msg}", flush=True)
def ensure_pyinstaller() -> bool:
try:
import PyInstaller # noqa: F401
return True
except ImportError:
pass
log("PyInstaller не найден, устанавливаю…")
res = subprocess.run([sys.executable, "-m", "pip", "install", "pyinstaller"])
if res.returncode != 0:
print("Не удалось установить PyInstaller.", file=sys.stderr)
return False
return True
def ensure_dearpygui() -> bool:
try:
import dearpygui # noqa: F401
return True
except ImportError:
pass
log("Dear PyGui не найден, устанавливаю…")
res = subprocess.run([sys.executable, "-m", "pip", "install", "dearpygui"])
return res.returncode == 0
def system_font_dirs() -> list[str]:
windir = os.environ.get("WINDIR", r"C:\Windows")
local = os.environ.get("LOCALAPPDATA", "")
dirs = [os.path.join(windir, "Fonts")]
if local:
dirs.append(os.path.join(local, "Microsoft", "Windows", "Fonts"))
return [d for d in dirs if os.path.isdir(d)]
def _copy_font(name: str) -> bool:
"""Скопировать один системный шрифт в fonts/. True, если он там оказался."""
target = os.path.join(FONTS_DIR, name)
if os.path.isfile(target):
return True
for directory in system_font_dirs():
src = os.path.join(directory, name)
if not os.path.isfile(src):
continue
try:
shutil.copy2(src, target)
log(f"шрифт: {name}")
return True
except OSError as e:
log(f"не скопировался {name}: {e}")
return False
return False
def collect_fonts() -> int:
"""Сложить шрифты в fonts/ рядом со скриптом. Возвращает их количество.
Сначала пробуем Nerd Font — только с ним в интерфейсе будут иконки.
Если его в системе нет, берём Segoe UI: кириллица останется, иконки нет.
"""
os.makedirs(FONTS_DIR, exist_ok=True)
# список, а не генератор: нужны все начертания, а не только первое удачное
if not any([_copy_font(n) for n in WANTED_FONTS]):
log("JetBrains Mono Nerd Font в системе не найден — беру системный шрифт "
"(интерфейс будет без иконок)")
for name in FALLBACK_FONTS:
_copy_font(name)
return len([f for f in os.listdir(FONTS_DIR) if f.lower().endswith(".ttf")])
def build(args: argparse.Namespace) -> int:
if not ensure_pyinstaller() or not ensure_dearpygui():
return 1
add_data: list[str] = []
if not args.no_font:
count = collect_fonts()
if count:
log(f"в сборку войдёт шрифтов: {count}")
add_data.append(f"{FONTS_DIR}{os.pathsep}fonts")
else:
log("шрифты не найдены — exe будет искать их в системе")
cmd = [
sys.executable, "-m", "PyInstaller",
"--noconfirm", "--clean",
"--name", NAME,
"--onedir" if args.onedir else "--onefile",
"--console" if args.console else "--windowed",
"--collect-all", "dearpygui",
"--exclude-module", "tkinter", # запасной интерфейс в exe не нужен
"--exclude-module", "numpy",
"--exclude-module", "PIL",
"--distpath", os.path.join(ROOT, "dist"),
"--workpath", os.path.join(ROOT, "build"),
"--specpath", os.path.join(ROOT, "build"),
]
# значок нужен и внутри сборки: его ставит себе окно приложения
if os.path.isfile(ICON):
add_data.append(f"{ICON}{os.pathsep}.")
for item in add_data:
cmd += ["--add-data", item]
if not args.no_admin:
# манифест UAC: Windows поднимет права ещё до старта Python
cmd.append("--uac-admin")
log("exe будет запрашивать права администратора при запуске")
else:
log("без манифеста UAC — права запросит уже само приложение")
if args.icon and os.path.isfile(args.icon):
cmd += ["--icon", args.icon]
log(f"значок: {os.path.basename(args.icon)}")
else:
log("значок не найден — запустите make_icon.py")
cmd.append(os.path.join(ROOT, "run.py"))
log("запускаю PyInstaller…")
print()
res = subprocess.run(cmd, cwd=ROOT)
print()
if res.returncode != 0:
print("Сборка не удалась.", file=sys.stderr)
return res.returncode
target = (os.path.join(ROOT, "dist", NAME, f"{NAME}.exe") if args.onedir
else os.path.join(ROOT, "dist", f"{NAME}.exe"))
if os.path.isfile(target):
size = os.path.getsize(target) / (1024 * 1024)
log(f"готово: {target} ({size:.1f} МБ)")
if args.onedir:
log("раздавать нужно всю папку dist\\SysDevCleaner целиком")
else:
log(f"PyInstaller отработал, но {target} не найден — проверьте вывод выше")
return 1
return 0
def main() -> int:
p = argparse.ArgumentParser(description="Сборка SysDevCleaner в exe")
p.add_argument("--onedir", action="store_true",
help="папка вместо одного файла: запуск быстрее, "
"нет распаковки во временный каталог")
p.add_argument("--no-admin", action="store_true",
help="не встраивать манифест UAC (по умолчанию он есть, и exe "
"запускается с правами администратора)")
p.add_argument("--no-font", action="store_true",
help="не вшивать шрифт в сборку")
p.add_argument("--console", action="store_true",
help="оставить консольное окно (видны ошибки)")
p.add_argument("--icon", default=ICON,
help="путь к .ico (если файла нет, значок будет стандартный)")
return build(p.parse_args())
if __name__ == "__main__":
raise SystemExit(main())