Первый выпуск 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:
201
build_exe.py
Normal file
201
build_exe.py
Normal 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())
|
||||
Reference in New Issue
Block a user