Files
PixelLauncher/make_installer.py
T
kosta2014p ec6f13ddcc PixelLauncher 2.1.0 — стабильная сборка
Офлайн-лаунчер Minecraft: аккаунты без Microsoft, установка версий и лоадеров
(Fabric, Quilt, Forge, NeoForge), каталог модпаков Modrinth, поиск и обновление
модов, разбор вылетов, системный трей, автозагрузка, плагины, прокси и зеркала.
2026-10-18 22:44:19 +07:00

160 lines
6.4 KiB
Python
Raw 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.
# -*- 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()