Ampel / ampel (push) Successful in 54s
Beides vom Commander gemeldet, beides gemessen statt vermutet.
## 1. Weisses leeres Blatt statt Symbol
In deploy/worker-windows/rippy.ico steckte GENAU EIN Bild:
Bilder = 1
256x256 32 bit 5657 Bytes PNG
Windows holt sich fuer jede Stelle die passende Groesse -- 16x16 fuer die
Taskleiste, 32/48 fuer Desktop und Startmenue. Fehlt sie, skaliert die Shell
nicht zuverlaessig selbst, sondern zeigt das leere Blatt. Bei einer Datei mit
nur einem PNG-komprimierten 256er passiert das regelmaessig.
packaging/windows/icon.py baut die Datei jetzt mit neun Groessen
(16/20/24/32/40/48/64/128/256, LANCZOS beim Verkleinern). Nachgemessen an
der neuen EXE: 16x16 mit 138 Farben, 32x32 mit 264 -- kein leeres Blatt mehr.
build.py laesst ein unvollstaendiges Symbol gar nicht mehr durch, und
test_icon.py haelt die Pflichtgroessen fest (mit dem alten Stand rot gesehen).
## 2. Rippy stand nicht in "Installierte Apps"
Die Ursache war MEINE Testsuite. Gemessen:
Rippy.exe --nicht-starten -> Eintrag: DA (Rippy 2.0.0)
pytest -q -> Eintrag: WEG
test_installation_legt_die_dateien_an_... schrieb in den ECHTEN
Uninstall-Schluessel und loeschte ihn im finally wieder -- also auch den des
Commanders. Der Autostart-Eintrag ging denselben Weg.
Der Schluesselname stand als VORGABEWERT in jeder Signatur, und Vorgabewerte
wertet Python zur Definitionszeit aus: Ein Test konnte ihn gar nicht umbiegen.
Jetzt steht dort None, aufgeloest zur Laufzeit -- damit wirkt ein
monkeypatch.setattr(reg, "SCHLUESSEL", ...) ueberall.
Dazu ein Waechter (test_die_testsuite_ruehrt_den_ECHTEN_eintrag_nicht_an):
Er merkt sich den echten Zustand vorher, laesst eine vollstaendige
Test-Installation samt Autostart laufen und prueft danach, dass sich am
echten Eintrag nichts geaendert hat.
Das ist derselbe Fehler wie vorhin bei den Verknuepfungen, nur an anderer
Stelle. Die Regel gehoert in den Code, nicht in meinen Kopf: **Ein Test
raeumt nur weg, was er selbst angelegt hat.**
Ampel lokal: 590 gruen, ruff sauber.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
143 lines
4.7 KiB
Python
143 lines
4.7 KiB
Python
"""Ein Windows-Symbol, das Windows auch wirklich anzeigt.
|
|
|
|
## Der Befund (28.08.2026)
|
|
|
|
Commander: *„Startmenü, Desktop und Taskleisten-Icon ohne icon file (Weißes
|
|
Leeres Blatt)."*
|
|
|
|
Nachgesehen, was in `deploy/worker-windows/rippy.ico` steckte:
|
|
|
|
Bilder = 1
|
|
256x256 32 bit 5657 Bytes PNG
|
|
|
|
**Genau ein Bild, und zwar das größte.** Windows holt sich für jede Stelle
|
|
die passende Größe aus der Datei:
|
|
|
|
Taskleiste, Verknüpfung klein 16x16
|
|
Desktop, Startmenü 32x32 / 48x48
|
|
Große Kacheln, Alt-Tab 256x256
|
|
|
|
Fehlt die passende Größe, skaliert die Shell nicht immer selbst — sie zeigt
|
|
das leere Blatt. Bei einer Datei, die NUR ein PNG-komprimiertes 256er
|
|
enthält, passiert das zuverlässig.
|
|
|
|
## Was diese Datei tut
|
|
|
|
Sie baut aus dem vorhandenen Symbol eine `.ico` mit allen gebrauchten Größen.
|
|
Die kleinen Größen werden mit `LANCZOS` verkleinert — bei 16x16 ist das der
|
|
Unterschied zwischen einem erkennbaren Zeichen und Pixelbrei.
|
|
|
|
Aufruf (macht der Bau selbst, siehe `build.py`):
|
|
|
|
python packaging/windows/icon.py [--quelle DATEI] [--ziel DATEI]
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
|
|
# Was Windows an den einzelnen Stellen holt. 24 und 64 sind fuer hohe
|
|
# Bildschirmaufloesungen (150 %/200 % Skalierung) — ohne sie skaliert die
|
|
# Shell aus 32 bzw. 48 hoch, und das sieht ausgefranst aus.
|
|
GROESSEN = (16, 20, 24, 32, 40, 48, 64, 128, 256)
|
|
|
|
HIER = os.path.dirname(os.path.abspath(__file__))
|
|
REPO = os.path.dirname(os.path.dirname(HIER))
|
|
QUELLE = os.path.join(REPO, "deploy", "worker-windows", "rippy.ico")
|
|
|
|
|
|
def groessen_in(pfad: str) -> list:
|
|
"""Welche Bildgroessen stecken in einer .ico? (ohne Pillow, reines Lesen)
|
|
|
|
Fuer die Pruefung im Bau: Eine Datei mit nur EINER Groesse ist genau der
|
|
Fehler, den es hier zu verhindern gilt.
|
|
"""
|
|
import struct
|
|
|
|
with open(pfad, "rb") as f:
|
|
roh = f.read()
|
|
if len(roh) < 6:
|
|
return []
|
|
reserviert, typ, anzahl = struct.unpack("<HHH", roh[:6])
|
|
if reserviert != 0 or typ != 1:
|
|
return []
|
|
gefunden = []
|
|
for i in range(anzahl):
|
|
block = roh[6 + i * 16: 22 + i * 16]
|
|
if len(block) < 16:
|
|
break
|
|
breite = block[0] or 256 # 0 heisst 256 (passt nicht in ein Byte)
|
|
gefunden.append(breite)
|
|
return sorted(gefunden)
|
|
|
|
|
|
# Ohne diese Groessen sieht der Nutzer an der jeweiligen Stelle ein leeres
|
|
# Blatt. Gehoert hierher und nicht in den Test, damit der BAU sie kennt.
|
|
PFLICHTGROESSEN = (16, 32, 48, 256)
|
|
|
|
|
|
def fehlende_pflichtgroessen(pfad: str) -> tuple:
|
|
"""Welche Pflichtgroessen fehlen? Leer heisst: alles da."""
|
|
vorhanden = set(groessen_in(pfad))
|
|
return tuple(k for k in PFLICHTGROESSEN if k not in vorhanden)
|
|
|
|
|
|
def bauen(quelle: str = None, ziel: str = None) -> str:
|
|
"""Baut die vollstaendige .ico. Gibt den Zielpfad zurueck.
|
|
|
|
Wirft, wenn es nicht geht — ein Symbol, das still fehlschlaegt, fuehrt
|
|
genau zu dem leeren Blatt, das hier abgestellt werden soll.
|
|
"""
|
|
from PIL import Image
|
|
|
|
quelle = quelle or QUELLE
|
|
ziel = ziel or quelle
|
|
|
|
bild = Image.open(quelle)
|
|
bild = bild.convert("RGBA")
|
|
|
|
# Aus dem groessten vorhandenen Bild verkleinern, nicht aus einem
|
|
# zufaellig ausgewaehlten: Pillow oeffnet bei einer .ico sonst die erste
|
|
# Groesse, und aus 16x16 laesst sich kein 256er machen.
|
|
if hasattr(bild, "size") and bild.size[0] < 256:
|
|
try:
|
|
bild = Image.open(quelle)
|
|
bild.size = (256, 256) # Pillow: gewuenschte Groesse waehlen
|
|
bild = bild.convert("RGBA")
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
|
|
ebenen = []
|
|
for kante in GROESSEN:
|
|
if kante >= bild.size[0]:
|
|
ebenen.append(bild.copy().resize((kante, kante), Image.LANCZOS))
|
|
else:
|
|
# LANCZOS auch beim Verkleinern: bei 16x16 ist das der Unterschied
|
|
# zwischen einem erkennbaren Zeichen und Pixelbrei.
|
|
ebenen.append(bild.resize((kante, kante), Image.LANCZOS))
|
|
|
|
ebenen[-1].save(ziel, format="ICO",
|
|
sizes=[(k, k) for k in GROESSEN])
|
|
return ziel
|
|
|
|
|
|
def main(argv=None) -> int:
|
|
p = argparse.ArgumentParser(description="Baut eine vollstaendige rippy.ico")
|
|
p.add_argument("--quelle", default=QUELLE)
|
|
p.add_argument("--ziel", default=None)
|
|
p.add_argument("--nur-pruefen", action="store_true")
|
|
args = p.parse_args(argv)
|
|
|
|
vorher = groessen_in(args.quelle)
|
|
print("Vorher: %s" % (vorher or "keine lesbare .ico"))
|
|
if args.nur_pruefen:
|
|
return 0 if len(vorher) > 1 else 1
|
|
|
|
ziel = bauen(args.quelle, args.ziel)
|
|
print("Nachher: %s" % groessen_in(ziel))
|
|
print("Geschrieben: %s (%d Bytes)" % (ziel, os.path.getsize(ziel)))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|