Ampel / ampel (push) Failing after 55s
Commander: "Unterbinde das CMD Fenster was sich beim oeffnen mitoeffnet."
Mein erster Anlauf wollte die Konsole VERSTECKEN und war falsch, aus zwei
Gruenden:
* Eine Onefile-EXE ist ZWEI Prozesse (gemessen: PID 33172 mit 8 MB, PID
33580 mit 103 MB, beide Rippy.exe). Die Pruefung "haengt genau EIN Prozess
an der Konsole" war damit nie wahr, das Fenster blieb einfach stehen.
* Selbst wenn sie ginge, waere das Fenster erst nach ein bis zwei Sekunden
weg. Ein Aufblitzen ist kein "unterbunden".
Jetzt wird die Konsole gar nicht erst erzeugt: --windowed statt --console.
Nachgemessen am fertigen Bau -- PE-Subsystem 2 (GUI) statt 3 (Konsole), und
beim Klick auf die Desktop-Verknuepfung ist genau EIN sichtbares Fenster da:
die WinForms-Oberflaeche. Kein ConsoleWindowClass-Fenster von Rippy, weder
sichtbar noch unsichtbar.
Was dazugehoert, damit daraus kein stiller Fehlschlag wird:
* Ohne Konsole ist sys.stdout None. uvicorn schreibt auf stderr und waere
mitten im Start gestorben, ohne dass irgendwo etwas stuende. einstieg.py
haengt sich deshalb an die Konsole des Aufrufers (--status in PowerShell
schreibt weiter dorthin) oder leitet nach %LOCALAPPDATA%\Rippy\rippy.log um.
* Der Installer meldet sein Ergebnis in einem Fenster, wenn keine Konsole da
ist -- sonst saehe ein gescheiterter Doppelklick aus wie einer, der nichts
tut. Bei --still und mit Konsole bleibt es bei der Textzeile.
Gegengeprueft am laufenden Programm: /api/health, / und /api/capabilities
antworten mit 200, und rippy.log faengt auf, was vorher ins Leere ging.
fix(drives): linux.py reicht die detection-Funktionen wieder durch
Die Ampel war seit Lauf 181 rot -- vier Commits lang, und ich hatte es
uebersehen. Ursache:
docker/api/main.py:54
AttributeError: module 'rippy.drives.linux' has no attribute 'drive_status'
main.py holt sich beim Import device_discovery.drive_status. Der
Windows-Treiber hat die Funktion; linux.py hatte sie nicht mehr, seit
detection (und damit fcntl) bewusst nicht mehr oben importiert wird -- sonst
waere main.py unter Windows nicht ladbar. Unter Windows lief also alles, auf
Linux waere der API-Container gar nicht hochgekommen.
Behoben mit PEP 562 (__getattr__), demselben Kniff wie in store/__init__.py:
Der Import bleibt ohne fcntl moeglich, wer die Funktionen BENUTZT bekommt sie.
Dazu drei weitere Fehler, die nur auf Linux auftraten:
* katalog.py baute Windows-Pfade mit os.path.join. Auf dem Linux-Runner wurde
daraus C:\Program Files (x86)\MakeMKV/makemkvcon64.exe. Jetzt entscheidet
der Pfad selbst ueber den Trenner, nicht die rechnende Maschine.
* verknuepfungen.py nahm os.path.dirname fuer den Arbeitsordner einer .lnk.
Eine .lnk zeigt IMMER auf einen Windows-Pfad -- also ntpath.
* waechter.py las 0.0 als Zeitpunkt statt als "noch nie geprueft". Auf einer
frisch gestarteten Maschine ist time.monotonic() klein, also fiel die erste
Laufwerks-Abfrage aus. None statt 0.0.
Alle vier haben jetzt Tests, die auf JEDER Plattform laufen -- der
Treiber-Test schiebt eine detection-Attrappe unter, der Waechter-Test stellt
eine Uhr bei 0,5 s nach, die Pfad-Tests pruefen beide Zweige. Beim
Wegnehmen der Durchreiche rot gesehen (5 Fehlschlaege), danach wieder gruen.
Damit faengt die Ampel so etwas kuenftig, ohne dass es eine Linux-Umgebung
auf dem Entwicklungsrechner braucht.
Ampel lokal: 582 gruen, ruff sauber.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
221 lines
8.8 KiB
Python
221 lines
8.8 KiB
Python
"""Baut RippySetup.exe — eine Datei, die installiert, laeuft und sich entfernt.
|
|
|
|
## Aufruf
|
|
|
|
python packaging/windows/build.py [--ausgabe ORDNER] [--version 2.0.0]
|
|
|
|
Voraussetzungen (in der Bau-Umgebung, nicht beim Nutzer):
|
|
* Python 3.12 mit den API-Abhaengigkeiten (docker/api/requirements.txt)
|
|
* pyinstaller, pystray, pillow
|
|
* Das UI muss gebaut sein: cd docker/ui && npm run build
|
|
|
|
## Warum die Versions-Ressource nicht optional ist
|
|
|
|
Ohne sie steht im Taskmanager unter „Beschreibung" nichts, und in den
|
|
Datei-Eigenschaften fehlt jede Angabe zu Herausgeber und Version. Eine EXE
|
|
ohne diese Angaben sieht aus wie etwas, das man nicht ausfuehren sollte —
|
|
und Windows SmartScreen sieht das genauso.
|
|
|
|
Der Commander hat ausdruecklich einen Eintrag im Taskmanager gewuenscht.
|
|
Der Prozessname kommt vom Dateinamen (`Rippy.exe`), die BESCHREIBUNG
|
|
daneben aus dieser Ressource.
|
|
|
|
## Was mit hinein muss
|
|
|
|
docker/api/ die API-Module (main.py und Nachbarn)
|
|
docker/ui/dist/ die gebaute Weboberflaeche
|
|
src/rippy/ der Kern (kommt ueber den Import automatisch mit)
|
|
rippy.ico Symbol fuer Tray, Verknuepfung und Programmliste
|
|
|
|
`docker/api` liegt im Paket unter `api/`, `docker/ui/dist` unter `ui/` —
|
|
genau dort sucht `rippy/daemon.py` sie, wenn `sys._MEIPASS` gesetzt ist.
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
HIER = os.path.dirname(os.path.abspath(__file__))
|
|
REPO = os.path.dirname(os.path.dirname(HIER))
|
|
|
|
VERSION_VORLAGE = """
|
|
VSVersionInfo(
|
|
ffi=FixedFileInfo(
|
|
filevers=({v0}, {v1}, {v2}, 0),
|
|
prodvers=({v0}, {v1}, {v2}, 0),
|
|
mask=0x3f, flags=0x0, OS=0x40004, fileType=0x1, subtype=0x0,
|
|
date=(0, 0)
|
|
),
|
|
kids=[
|
|
StringFileInfo([
|
|
StringTable(u'040704b0', [
|
|
StringStruct(u'CompanyName', u'Rippy'),
|
|
StringStruct(u'FileDescription', u'Rippy — automatisches Ripping'),
|
|
StringStruct(u'FileVersion', u'{version}'),
|
|
StringStruct(u'InternalName', u'Rippy'),
|
|
StringStruct(u'OriginalFilename', u'Rippy.exe'),
|
|
StringStruct(u'ProductName', u'Rippy'),
|
|
StringStruct(u'ProductVersion', u'{version}')])
|
|
]),
|
|
# 0x0407 = Deutsch, 0x04b0 = Unicode. Ohne den passenden Eintrag zeigt
|
|
# Windows die Angaben je nach Systemsprache gar nicht an.
|
|
VarFileInfo([VarStruct(u'Translation', [1031, 1200])])
|
|
]
|
|
)
|
|
"""
|
|
|
|
|
|
def versionsdatei(version: str, ordner: str) -> str:
|
|
teile = (version.split("-")[0].split(".") + ["0", "0", "0"])[:3]
|
|
pfad = os.path.join(ordner, "version_info.txt")
|
|
with open(pfad, "w", encoding="utf-8") as f:
|
|
f.write(VERSION_VORLAGE.format(
|
|
version=version, v0=int(teile[0]), v1=int(teile[1]), v2=int(teile[2])))
|
|
return pfad
|
|
|
|
|
|
def pruefen() -> list:
|
|
"""Alles da? Erst pruefen, dann bauen — wie install.sh es macht.
|
|
|
|
Ein Bau, der auf halber Strecke abbricht, hinterlaesst eine EXE, die
|
|
aussieht wie fertig und es nicht ist.
|
|
"""
|
|
maengel = []
|
|
if sys.platform != "win32":
|
|
maengel.append("Eine Windows-.exe laesst sich nur unter Windows bauen.")
|
|
if not os.path.isfile(os.path.join(REPO, "docker", "api", "main.py")):
|
|
maengel.append("docker/api/main.py fehlt.")
|
|
if not os.path.isfile(os.path.join(REPO, "docker", "worker", "ablauf.py")):
|
|
maengel.append("docker/worker/ablauf.py fehlt — ohne den Ablauf kann "
|
|
"die App nicht rippen.")
|
|
if not os.path.isdir(os.path.join(REPO, "docker", "ui", "dist")):
|
|
maengel.append(
|
|
"docker/ui/dist fehlt — die Oberflaeche ist nicht gebaut. "
|
|
"Abhilfe: cd docker/ui && npm ci && npm run build")
|
|
if not os.path.isfile(os.path.join(REPO, "deploy", "worker-windows", "rippy.ico")):
|
|
maengel.append("deploy/worker-windows/rippy.ico fehlt.")
|
|
try:
|
|
import PyInstaller # noqa: F401
|
|
except ImportError:
|
|
maengel.append("pyinstaller fehlt (pip install pyinstaller).")
|
|
for modul, paket, wofuer in (
|
|
("pystray", "pystray", "kein Tray-Symbol"),
|
|
("PIL", "pillow", "kein Tray-Symbol"),
|
|
("webview", "pywebview", "kein eigenes Fenster, nur ein Browser-Tab"),
|
|
("clr", "pythonnet", "kein eigenes Fenster (WebView2 laeuft ueber .NET)"),
|
|
):
|
|
try:
|
|
__import__(modul)
|
|
except ImportError:
|
|
maengel.append(
|
|
f"{modul} fehlt (pip install {paket}) — ohne das {wofuer}.")
|
|
return maengel
|
|
|
|
|
|
def bauen(ausgabe: str, version: str) -> str:
|
|
arbeit = os.path.join(ausgabe, "_bau")
|
|
os.makedirs(arbeit, exist_ok=True)
|
|
|
|
trenner = ";" # Windows-Trennzeichen fuer --add-data
|
|
befehl = [
|
|
sys.executable, "-m", "PyInstaller",
|
|
"--noconfirm", "--clean",
|
|
"--onefile",
|
|
# KEINE Konsole. Wunsch des Commanders am 28.08.2026: "Unterbinde
|
|
# das CMD Fenster was sich beim Oeffnen mitoeffnet." Ein Verstecken
|
|
# zur Laufzeit reicht nicht -- eine Onefile-EXE laeuft als ZWEI
|
|
# Prozesse (die Pruefung "haengt genau einer an der Konsole" ist damit
|
|
# nie wahr), und selbst wenn, blitzte das Fenster ein bis zwei
|
|
# Sekunden auf, bis Python so weit ist.
|
|
#
|
|
# Was dafuer noetig ist, steht in packaging/windows/einstieg.py:
|
|
# ohne Konsole ist sys.stdout None, und uvicorn wuerde daran sterben.
|
|
"--windowed",
|
|
"--name", "RippySetup",
|
|
"--icon", os.path.join(REPO, "deploy", "worker-windows", "rippy.ico"),
|
|
"--version-file", versionsdatei(version, arbeit),
|
|
"--distpath", ausgabe,
|
|
"--workpath", os.path.join(arbeit, "work"),
|
|
"--specpath", arbeit,
|
|
"--paths", os.path.join(REPO, "src"),
|
|
"--paths", os.path.join(REPO, "docker", "api"),
|
|
"--paths", os.path.join(REPO, "docker", "worker"),
|
|
"--add-data", os.path.join(REPO, "docker", "api") + trenner + "api",
|
|
# Die Worker-Module — ohne sie kann Rippy anzeigen, aber nicht rippen.
|
|
# ablauf.py, ripping.py, medien.py, caps.py und Nachbarn.
|
|
"--add-data", os.path.join(REPO, "docker", "worker") + trenner + "worker",
|
|
"--add-data", os.path.join(REPO, "docker", "ui", "dist") + trenner + "ui",
|
|
"--add-data", os.path.join(REPO, "deploy", "worker-windows", "rippy.ico") + trenner + ".",
|
|
# Was PyInstaller nicht von allein findet: dynamisch importierte Module.
|
|
"--hidden-import", "uvicorn.logging",
|
|
"--hidden-import", "uvicorn.loops.auto",
|
|
"--hidden-import", "uvicorn.protocols.http.auto",
|
|
"--hidden-import", "uvicorn.protocols.websockets.auto",
|
|
"--hidden-import", "uvicorn.lifespan.on",
|
|
"--hidden-import", "rippy.drives.windows",
|
|
"--hidden-import", "pystray._win32",
|
|
# pywebview waehlt seine Plattform zur LAUFZEIT aus (importlib), also
|
|
# sieht PyInstaller den Import nicht. Ohne diese drei geht das Fenster
|
|
# nicht auf und Rippy faellt still auf den Browser zurueck -- also
|
|
# genau dorthin, wovon wir gerade weg wollten.
|
|
"--hidden-import", "webview",
|
|
"--hidden-import", "webview.platforms.winforms",
|
|
"--hidden-import", "webview.platforms.edgechromium",
|
|
# Nicht mitnehmen: Der Windows-Treiber braucht kein psycopg2/celery,
|
|
# und beides zieht Ballast nach.
|
|
"--exclude-module", "psycopg2",
|
|
"--exclude-module", "tkinter",
|
|
os.path.join(REPO, "packaging", "windows", "einstieg.py"),
|
|
]
|
|
|
|
print("PyInstaller wird aufgerufen …\n")
|
|
ergebnis = subprocess.run(befehl, cwd=REPO)
|
|
if ergebnis.returncode != 0:
|
|
raise SystemExit("PyInstaller ist gescheitert (Code %d)." % ergebnis.returncode)
|
|
|
|
exe = os.path.join(ausgabe, "RippySetup.exe")
|
|
if not os.path.isfile(exe):
|
|
raise SystemExit("PyInstaller meldete Erfolg, aber %s gibt es nicht." % exe)
|
|
return exe
|
|
|
|
|
|
def main(argv=None) -> int:
|
|
p = argparse.ArgumentParser(description="Baut RippySetup.exe")
|
|
p.add_argument("--ausgabe", default=os.path.join(REPO, "dist", "windows"))
|
|
p.add_argument("--version", default=os.environ.get("RIPPY_VERSION", "2.0.0"))
|
|
p.add_argument("--nur-pruefen", action="store_true")
|
|
args = p.parse_args(argv)
|
|
|
|
print("Rippy — Windows-Bau")
|
|
print(" Repo %s" % REPO)
|
|
print(" Ausgabe %s" % args.ausgabe)
|
|
print(" Version %s" % args.version)
|
|
print()
|
|
|
|
maengel = pruefen()
|
|
if maengel:
|
|
print("Der Bau kann so nicht laufen:")
|
|
for m in maengel:
|
|
print(" X %s" % m)
|
|
return 1
|
|
print("Alle Voraussetzungen erfuellt.")
|
|
if args.nur_pruefen:
|
|
return 0
|
|
|
|
os.makedirs(args.ausgabe, exist_ok=True)
|
|
exe = bauen(args.ausgabe, args.version)
|
|
|
|
groesse = os.path.getsize(exe) / 1024**2
|
|
print()
|
|
print("Fertig: %s (%.1f MB)" % (exe, groesse))
|
|
|
|
arbeit = os.path.join(args.ausgabe, "_bau")
|
|
shutil.rmtree(arbeit, ignore_errors=True)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|