Files
SysDevCleaner/build_exe.py
HeagBoKaT b01cf37e66 Первый выпуск 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>
2026-09-11 14:55:00 +03:00

202 lines
8.6 KiB
Python
Raw Permalink 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.
#!/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())