# -*- 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.2" # должен совпадать с 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("