Seine Ansagen
"gibt es noch das popup install ordner auswaehlen, das stimmt ja nicht"
"wie soll das rechnungstool und steuertool wissen wo es ist, das muss in
ravokk liegen und dann zusammen, sonst kommt das eine nicht an das andere
ran - die sind doch nur ueber die gemeinsame sqlite verbunden und mit api"
"es gibt ravokk als main verzeichnis, da liegt config und sqlite drin, und
dann gibt es den ordner rechnungstool und steuertool"
"ravokk ist meine firma und die programme gehoeren zu der firma"
"du solltest die auch mal deinstallieren und neu installieren ... dass der
ordner dann leer ist und sauber - will keine tools die unsauber sind"
Die Struktur
%LOCALAPPDATA%/ravokk/
config.json, journal.sqlite3, Nummernbuch, berichte/ <- Daten
Rechnungstool/ <- Programm
Steuerrechnungstool/ <- Programm
Die Daten liegen NEBEN dem Programm, nicht darin. Nur dadurch koennen
beide Forderungen gleichzeitig gelten: der Programmordner wird beim
Deinstallieren restlos geleert, und die Belege bleiben zehn Jahre stehen
(§ 147 AO).
Kein Ordner-Dialog mehr beim ersten Start
gemeinsam.standard_ordner() nennt den festen Ort, beide Programme legen
ihn selbst an. Gefragt wird nur noch, wer ihn ausdruecklich verschieben
will (Menue -> Gemeinsamer Ordner) - etwa auf ein Netzlaufwerk.
Der Knopf hiess "Install-Ordner auswaehlen..." und der Text riet zum
"Ordner, in dem die Programme liegen". Beides war falsch und seit dem
Installer erst recht: die beiden liegen in getrennten Unterordnern, und
ein Deinstallieren haette das Nummernbuch mitgenommen.
⚠ Dabei gefunden: das Journal lag im TEMP-Ordner
Der Merkzettel der installierten Fassung zeigte auf
%TEMP%/abnahme_bst/Daten - haengengeblieben aus einem Abnahmelauf vom
03.09. Windows raeumt den Temp-Ordner automatisch auf; das Journal waere
ohne Vorwarnung verschwunden. ablage.py verwirft einen solchen Merkzettel
jetzt, statt ihm zu folgen.
Uninstaller
Er raeumt jetzt auch, was der UPDATER angelegt hat - jede geladene Fassung
und installiert.json. Die kannte der Installer nicht; mit jeder Fassung
waere eine EXE mehr liegen geblieben. Gemessen vorher: zwei EXEs und der
halbe Datenordner.
pruef_installation.py
Installiert wirklich, legt eine Datei an wie der Updater es taete,
deinstalliert und sieht nach: Programmordner leer, Daten unangetastet.
Vorhandene Daten werden vorher gesichert und danach zurueckgelegt - ein
Pruefstand fasst niemandem seine Buchhaltung an.
pruef_gemeinsam_automatisch.py
Prueft, dass beide Programme denselben Ordner nennen und dass findet, was
das andere eintraegt - ohne dass jemand einen Pfad eingegeben hat.
Symbol
Das Windows-Symbol ist jetzt eine helle Kachel mit runden Ecken und dem
Zeichen darauf, in allen Groessen von 16 bis 256 - "so wie das rustdesk,
nur halt alle ecken in der rundung". Ein freigestelltes Logo ging auf
einem dunklen Desktop unter. Die zweite, aufgehellte ICO ist damit
ueberfluessig; das aufgehellte Logo IM Programm (Kopfzeile, Wasserzeichen)
bleibt unveraendert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
138 lines
5.3 KiB
Python
138 lines
5.3 KiB
Python
# -*- 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()
|