PixelLauncher 2.1.0 — стабильная сборка
Офлайн-лаунчер Minecraft: аккаунты без Microsoft, установка версий и лоадеров (Fabric, Quilt, Forge, NeoForge), каталог модпаков Modrinth, поиск и обновление модов, разбор вылетов, системный трей, автозагрузка, плагины, прокси и зеркала.
This commit is contained in:
commit
ec6f13ddcc
33 files changed
+10445
No files matched your search
@@ -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()
|
||||
Reference in new issue
Block a user