Анализ 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>
202 lines
8.6 KiB
Python
202 lines
8.6 KiB
Python
#!/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())
|