PixelLauncher 2.1.0 — стабильная сборка

Офлайн-лаунчер Minecraft: аккаунты без Microsoft, установка версий и лоадеров
(Fabric, Quilt, Forge, NeoForge), каталог модпаков Modrinth, поиск и обновление
модов, разбор вылетов, системный трей, автозагрузка, плагины, прокси и зеркала.
This commit is contained in:
kosta2014p committed 2026-10-18 22:44:19 +07:00
commit ec6f13ddcc
33 files changed
+10445

No files matched your search

+159
View File
@@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
"""Сборка установщика PixelLauncher (Inno Setup 6).
Что делает:
1) runtime: embeddable Python (с python.org) + tkinter/Tcl из локальной
установки той же версии — лаунчеру нужен только stdlib;
2) приложение: main.py + launcher/ + run.bat + README + requirements;
3) иконка: assets/icon.png -> build/launcher.ico (PNG-in-ICO);
4) компиляция: ISCC.exe installer.iss -> PixelLauncherSetup<версия>.exe
в папке проекта.
Запуск: python make_installer.py
Нужны: Inno Setup 6 (ISCC.exe) и интернет (первый раз — embeddable Python).
"""
import os
import shutil
import struct
import subprocess
import sys
import urllib.request
import zipfile
ROOT = os.path.dirname(os.path.abspath(__file__))
BUILD = os.path.join(ROOT, "build")
STAGE = os.path.join(BUILD, "stage")
ISCC = r"C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
PYVER = "%d.%d.%d" % sys.version_info[:3]
APP_VERSION = "2.1.0" # должен совпадать с LAUNCHER_VERSION в launcher/__init__.py
SETUP_NAME = "PixelLauncherSetup%s.exe" % APP_VERSION
def log(msg):
print(msg, flush=True)
def embeddable_zip():
name = "python-%s-embed-amd64.zip" % PYVER
path = os.path.join(BUILD, name)
if not os.path.isfile(path):
url = "https://www.python.org/ftp/python/%s/%s" % (PYVER, name)
log("Скачиваю %s ..." % url)
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req, timeout=300) as r, \
open(path, "wb") as f:
while True:
chunk = r.read(1 << 20)
if not chunk:
break
f.write(chunk)
return path
def build_stage():
if os.path.isdir(STAGE):
shutil.rmtree(STAGE, ignore_errors=True)
os.makedirs(STAGE)
log("Распаковка embeddable Python %s ..." % PYVER)
with zipfile.ZipFile(embeddable_zip()) as z:
z.extractall(STAGE)
# --- tkinter/Tcl: из локальной установки той же версии
base = sys.base_prefix
dlls = os.path.join(base, "DLLs")
copied = []
for fname in os.listdir(dlls):
low = fname.lower()
if low in ("_tkinter.pyd", "zlib1.dll") or (
low.endswith(".dll") and (low.startswith("tcl")
or low.startswith("tk"))):
shutil.copy2(os.path.join(dlls, fname),
os.path.join(STAGE, fname))
copied.append(fname)
tcl_src = os.path.join(base, "tcl")
assert os.path.isdir(tcl_src), "нет папки tcl в " + base
shutil.copytree(tcl_src, os.path.join(STAGE, "tcl"))
# сам пакет tkinter вырезан из embeddable — копируем из установки
tk_src = os.path.join(base, "Lib", "tkinter")
assert os.path.isdir(tk_src), "нет папки tkinter в " + base
shutil.copytree(tk_src, os.path.join(STAGE, "tkinter"),
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
copied.append("tkinter/")
log("tkinter/Tcl: " + ", ".join(copied) + " + tcl/")
# ._pth: гарантируем точку в пути (иначе import launcher не найдёт)
pth = [f for f in os.listdir(STAGE) if f.endswith("._pth")][0]
pth_path = os.path.join(STAGE, pth)
lines = open(pth_path, encoding="utf-8").read().splitlines()
if "." not in lines:
lines.insert(1, ".")
with open(pth_path, "w", encoding="utf-8") as f:
f.write("\n".join(lines) + "\n")
# --- приложение
app_files = ["main.py", "run.bat", "README.md", "requirements.txt"]
for name in app_files:
src = os.path.join(ROOT, name)
if os.path.isfile(src):
shutil.copy2(src, os.path.join(STAGE, name))
shutil.copytree(
os.path.join(ROOT, "launcher"),
os.path.join(STAGE, "launcher"),
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
# --- иконка PNG -> ICO (ICO с PNG-полезной нагрузкой)
png_path = os.path.join(ROOT, "assets", "icon.png")
if os.path.isfile(png_path):
ico = os.path.join(STAGE, "launcher.ico")
make_ico(png_path, ico)
shutil.copy2(ico, os.path.join(BUILD, "launcher.ico"))
log("иконка: launcher.ico")
log("stage: %d файлов" % sum(len(f) for _r, _d, f in os.walk(STAGE)))
def make_ico(png_src, ico_dst):
png = open(png_src, "rb").read()
w, h = struct.unpack(">II", png[16:24])
entry = struct.pack("<BBBBHHII", w if w < 256 else 0,
h if h < 256 else 0, 0, 0, 1, 32,
len(png), 6 + 16)
with open(ico_dst, "wb") as f:
f.write(struct.pack("<HHH", 0, 1, 1))
f.write(entry)
f.write(png)
def compile_setup():
assert os.path.isfile(ISCC), "ISCC.exe не найден: " + ISCC
iss = os.path.join(ROOT, "installer.iss")
log("Компиляция %s ..." % os.path.basename(iss))
r = subprocess.run([ISCC, "/Q", iss], capture_output=True, text=True,
encoding="utf-8", errors="replace")
out = (r.stdout or "") + (r.stderr or "")
if r.returncode != 0:
print(out)
raise SystemExit("ISCC failed: %d" % r.returncode)
exe = os.path.join(ROOT, SETUP_NAME)
assert os.path.isfile(exe), "нет результата: " + exe
log("ГОТОВО: %s (%.1f МБ)" % (exe, os.path.getsize(exe) / 1e6))
if __name__ == "__main__":
if not os.path.isfile(os.path.join(ROOT, "assets", "icon.png")):
print("предупреждение: assets/icon.png нет — иконка не будет")
build_stage()
# smoke: стейдж самодостаточен (включая реальный Tk + SSL)
smoke = subprocess.run(
[os.path.join(STAGE, "python.exe"), "-c",
"import tkinter,ssl,urllib.request,json,zipfile;"
"r=tkinter.Tk(); r.withdraw(); r.destroy();"
"import launcher.config, launcher.http, launcher.manifest;"
"print('stage ok')"],
capture_output=True, text=True, encoding="utf-8", errors="replace",
cwd=STAGE)
print((smoke.stdout or "").strip(), (smoke.stderr or "").strip()[-800:])
assert smoke.returncode == 0, "smoke стейджа упал"
compile_setup()