# -*- coding: utf-8 -*- """Der ganze Weg von der Quelle zur Setup-Datei - an EINER Stelle. WARUM DAS HIER STEHT -------------------- Bis hierher lag der Bau in einer Folge von Befehlen, die jemand von Hand eintippen musste: PyInstaller fuers Programm, PyInstaller fuer den Updater, umbenennen, Inno Setup mit den richtigen Schaltern. Wer einen Schritt vergisst, liefert eine Setup-Datei aus, die eine alte EXE enthaelt - und niemand sieht es ihr an. Genau das ist am 06.09.2026 passiert (der Installer schrieb ueber eine laufende, gesperrte EXE hinweg und meldete trotzdem Erfolg). Ab jetzt ist der Bau ein Befehl: python bauen.py WAS ER TUT ---------- 1. Die Nummer kommt aus version.py - nicht von der Kommandozeile. So kann sie nicht auseinanderlaufen mit dem, was das Programm ueber sich selbst sagt. 2. Programm und Updater werden gebaut. 3. Die Programm-EXE bekommt die Nummer in den Namen. Der Updater erkennt daran, welche Fassungen installiert sind, und legt neue DANEBEN statt darueber - deshalb kann er auch dann austauschen, wenn Windows die alte Datei sperrt. 4. Inno Setup baut die Setup-Datei. 5. Zum Schluss stehen die SHA256-Summen da. Die gehoeren in den Release-Text: nur damit laesst sich spaeter nachweisen, dass die heruntergeladene Datei genau die gebaute ist. """ import hashlib import os import shutil import subprocess import sys HIER = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HIER) import version # noqa: E402 # Inno Setup liegt bei ihm unter LOCALAPPDATA (Installation ohne Adminrechte). ISCC_ORTE = [ os.path.join(os.environ.get("LOCALAPPDATA", ""), "Programs", "Inno Setup 6", "ISCC.exe"), r"C:\Program Files (x86)\Inno Setup 6\ISCC.exe", r"C:\Program Files\Inno Setup 6\ISCC.exe", ] def summe(pfad): h = hashlib.sha256() with open(pfad, "rb") as f: for brocken in iter(lambda: f.read(1 << 20), b""): h.update(brocken) return h.hexdigest() def lauf(befehl, was): print(f"--- {was}") e = subprocess.run(befehl, cwd=HIER, capture_output=True, text=True, encoding="utf-8", errors="replace") if e.returncode != 0: print(e.stdout[-3000:]) print(e.stderr[-3000:]) raise SystemExit(f"{was} fehlgeschlagen (Rueckgabe {e.returncode})") def iscc(): for p in ISCC_ORTE: if p and os.path.exists(p): return p raise SystemExit("ISCC.exe nicht gefunden - Inno Setup 6 fehlt") def main(): stamm, endung = os.path.splitext(version.EXE_NAME) mit_nummer = f"{stamm}-{version.VERSION}{endung}" # Die Bauordner werden geleert. Ein PyInstaller-Lauf ueber einen alten # Bauordner uebernimmt sonst Reste, die es in der Quelle nicht mehr gibt - # und in setup/ laege noch die Datei der VORIGEN Nummer herum. Beim # Ausliefern greift man dann leicht zur falschen. for ordner in ("build", "dist", "setup", "release"): shutil.rmtree(os.path.join(HIER, ordner), ignore_errors=True) # Das Windows-Symbol aus dem Logo bauen: helle Kachel mit runden Ecken, # Zeichen darauf, in allen Groessen von 16 bis 256. Es wird HIER erzeugt # und nicht zur Laufzeit - ein installiertes Programm soll nicht in seinen # eigenen Ordner schreiben muessen. sys.path.insert(0, HIER) import wasserzeichen ico = wasserzeichen.symbol_bauen( os.path.join(HIER, "assets", "programm.ico")) print(f"--- Symbol: {ico or 'FEHLT'}") lauf([sys.executable, "-m", "PyInstaller", "--noconfirm", f"{stamm}.spec"], f"{stamm} {version.VERSION}") lauf([sys.executable, "-m", "PyInstaller", "--noconfirm", "Updater.spec"], "Updater") quelle = os.path.join(HIER, "dist", version.EXE_NAME) ziel = os.path.join(HIER, "dist", mit_nummer) os.replace(quelle, ziel) lauf([iscc(), f"/DVersion={version.VERSION}", f"/DExeProgramm={mit_nummer}", "Installer.iss"], "Setup") # ---- release/ : genau das, was ins Release gehoert --------------------- # Der Anhang, den der Updater sucht, heisst OHNE Nummer - so steht es in # version.EXE_NAME, und danach sucht passendes_release(). Haengt man ihn # mit Nummer an, findet der Updater nie ein Update und meldet stumm # "aktuell". Die Nummer kommt erst beim Ablegen dazu (updater.dateiname). # # Daneben die Pruefsumme als eigene Datei: ohne sie prueft der Updater nur # die GROESSE. Eine Datei, die unterwegs verfaelscht wurde, hat aber genau # die richtige Groesse. freigabe = os.path.join(HIER, "release") os.makedirs(freigabe, exist_ok=True) shutil.copy2(ziel, os.path.join(freigabe, version.EXE_NAME)) with open(os.path.join(freigabe, version.EXE_NAME + ".sha256"), "w", encoding="ascii") as f: f.write(summe(ziel) + chr(10)) shutil.copy2(os.path.join(HIER, "dist", "Updater.exe"), os.path.join(freigabe, "Updater.exe")) for name in os.listdir(os.path.join(HIER, "setup")): shutil.copy2(os.path.join(HIER, "setup", name), os.path.join(freigabe, name)) print() print(f"FERTIG | {stamm} {version.VERSION}") print() print(" release/ - genau diese Dateien gehoeren ans Release:") for name in sorted(os.listdir(freigabe)): p = os.path.join(freigabe, name) print(f" {name:44s} {os.path.getsize(p):>10,d} B {summe(p)}") if __name__ == "__main__": main()