#!/usr/bin/env bash # Baut die native iOS-Huelle und signiert sie zu einer installierbaren .ipa. # Nur auf einem Mac mit Xcode lauffaehig. Siehe ../../UMSETZUNGSPLAN.md Phase 10. # # Warum es dieses Skript gibt: `ios/` ist absichtlich gitignored (aus dem # Webbuendel jederzeit wiederherstellbar). Damit verschwindet aber auch jede # Signatureinstellung, die man in Xcode von Hand setzt, beim naechsten # `npx cap add ios` wieder. Die Team-Kennung gehoert deshalb hierher - in eine # versionierte Datei - und wird dem Build von aussen mitgegeben. # # Voraussetzung, die dieses Skript nicht herstellen kann: das Zielgeraet muss # im Team eingetragen sein. Das Ad-hoc-Profil traegt eine feste Liste von # Geraetekennungen, ein unbekanntes Telefon lehnt die fertige .ipa ab. Ein # neues Geraet wird einmalig angemeldet - per Kabel an diesem Mac, dann # erledigt es -allowProvisioningUpdates selbst, oder von Hand auf # developer.apple.com. Bricht der Archivschritt mit "Your team has no devices" # ab, fehlt genau dieser Eintrag; das ist kein Fehler im Projekt. # # Stand des Kontos, am 2026-08-29 am ausgestellten Profil nachgesehen statt # vermutet: bezahlte Mitgliedschaft, Team RMACS9VLS4, Laufzeit ein Jahr. Die # frueher hier vermerkten sieben Tage galten dem kostenlosen Personal Team und # gelten nicht mehr; ebensowenig das "keine Geraeteverwaltung auf # developer.apple.com", das daran hing. # # Der Export ueber -exportArchive ist erprobt und kein offener Punkt mehr: die # ausgelieferten .ipa vom 2026-08-29 und 2026-08-30 sind auf genau diesem Weg # entstanden und wurden ueber die Luft installiert (siehe ios-luftweg.sh). # Scheitert er doch einmal, bleibt der direkte Weg aufs angeschlossene Geraet: # xcodebuild -project ios/App/App.xcodeproj -scheme App \ # -destination 'id=' -allowProvisioningUpdates \ # DEVELOPMENT_TEAM=$TEAM install set -euo pipefail HIER="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" APP="$(dirname "$HIER")" # Dieselbe Kennung wie zu Zeiten des kostenlosen Kontos: die bezahlte # Mitgliedschaft hat das bestehende Team hochgestuft, statt ein neues # anzulegen. Zwischenzeitlich stand hier das Gegenteil - widerlegt am # 2026-08-29 durch das ausgestellte Profil (Team RMACS9VLS4, Laufzeit ein # Jahr statt sieben Tage). Ueberschreibbar ueber APPLE_TEAM_ID. TEAM="${APPLE_TEAM_ID:-RMACS9VLS4}" AUSGABE="$APP/ios/build" ARCHIV="$AUSGABE/DataMetric360.xcarchive" cd "$APP" # Ohne diese Pruefung baut/signiert das Skript zuverlaessig - aber nur den # Stand, der gerade lokal ausgecheckt ist. Ein vergessenes `git pull` waere # damit ein stiller Veraltungsfehler wie schon mehrfach an anderer Stelle in # diesem Projekt (Cache-Busting, OTA-Buendel-Abgleich, VERSIONIERUNG.md) - hier # stattdessen ein lauter Abbruch statt eines optisch/inhaltlich alten Builds. echo "==> Git-Stand pruefen" if [ -n "$(git status --porcelain)" ]; then echo "FEHLER: Uncommittete Aenderungen im Arbeitsverzeichnis." >&2 git status --short >&2 echo "Erst committen oder stashen, dann erneut versuchen." >&2 exit 1 fi git fetch origin LOKAL="$(git rev-parse HEAD)" FERN="$(git rev-parse origin/main)" if [ "$LOKAL" != "$FERN" ]; then echo "FEHLER: Lokaler Stand ($LOKAL) weicht von origin/main ($FERN) ab." >&2 echo "Erst 'git pull' ausfuehren - sonst signiert dieses Skript einen veralteten Stand." >&2 exit 1 fi echo " HEAD == origin/main ($LOKAL), aktuell." echo "==> Webbuendel bauen" npm run build echo "==> Native Huelle synchronisieren" if [ ! -d ios ]; then echo "==> ios/ fehlt, wird neu erzeugt" npx cap add ios fi npx cap sync ios # Standort-Berechtigung ins Info.plist eintragen. # # Ohne diesen Schluessel zeigt iOS beim Aufruf von navigator.geolocation in # der WebView (src/screens/Standort.tsx) ueberhaupt keine Rueckfrage: der # Aufruf scheitert sofort, die App meldet "GPS-Freigabe fehlt", und unter # Einstellungen > DataMetric360 > Ort fehlt die Auswahl "Beim Verwenden der # App" - es bleiben nur "Nie" und "Beim Teilen". Nachgewiesen am 2026-08-31 # an der ausgelieferten App.ipa: kein einziger Location-Schluessel darin. # # Warum hier und nicht in Xcode: ios/ ist gitignored und wird von # `npx cap add ios` aus Capacitors Vorlage neu erzeugt - die kennt keine # Standort-Schluessel. Ein Eintrag von Hand in Xcode waere beim naechsten # Erzeugen wieder verschwunden, genau wie es der Team-Kennung oben ergangen # waere. Deshalb steht er versioniert hier und wird nach jedem `cap sync` # neu gesetzt. # # Nur "WhenInUse": berechnet wird der Abstand zum geparkten Fahrzeug, waehrend # jemand die Standort-Seite ansieht. Hintergrundortung braucht die App nicht, # und was sie nicht braucht, soll sie auch nicht erfragen. echo "==> Standort-Berechtigung eintragen" PLIST="ios/App/App/Info.plist" GRUND="Zeigt auf der Standort-Karte, wie weit du vom geparkten Fahrzeug entfernt bist." if ! /usr/libexec/PlistBuddy -c "Set :NSLocationWhenInUseUsageDescription $GRUND" "$PLIST" 2>/dev/null; then /usr/libexec/PlistBuddy -c "Add :NSLocationWhenInUseUsageDescription string $GRUND" "$PLIST" fi /usr/libexec/PlistBuddy -c "Print :NSLocationWhenInUseUsageDescription" "$PLIST" >/dev/null || { echo "FEHLER: NSLocationWhenInUseUsageDescription liess sich nicht setzen." >&2 exit 1 } # Kalender-Berechtigung, aus demselben Grund an derselben Stelle. # # Warum ZWEI Schluessel: seit iOS 17 gibt es einen eigenen Nur-Schreiben-Zugriff # (NSCalendarsWriteOnlyAccessUsageDescription), aeltere Fassungen kennen nur den # alten Sammelschluessel. Der System-Editor (native/KalenderTermin.swift) fragt # auf iOS 17+ von sich aus gar nicht - die Schluessel sind die Absicherung fuer # den Fall, dass EventKit doch einmal fragt. Fehlt der passende, beendet iOS die # App wortlos, statt eine Rueckfrage zu zeigen. echo "==> Kalender-Berechtigung eintragen" KALENDERGRUND="Traegt Werkstatt- und Wechseltermine in deinen Kalender ein." for SCHLUESSEL in NSCalendarsWriteOnlyAccessUsageDescription NSCalendarsUsageDescription; do if ! /usr/libexec/PlistBuddy -c "Set :$SCHLUESSEL $KALENDERGRUND" "$PLIST" 2>/dev/null; then /usr/libexec/PlistBuddy -c "Add :$SCHLUESSEL string $KALENDERGRUND" "$PLIST" fi /usr/libexec/PlistBuddy -c "Print :$SCHLUESSEL" "$PLIST" >/dev/null || { echo "FEHLER: $SCHLUESSEL liess sich nicht setzen." >&2 exit 1 } done # Kamera, fuer den QR-Scanner der Ersteinrichtung. # # Gelesen wird der Code in der WebView (jsQR ueber getUserMedia), nicht von # einem nativen Plugin - siehe den Kopf von src/screens/QrScanner.tsx, warum # nicht GoogleMLKit. Dieser Schluessel ist trotzdem noetig: ohne ihn beendet # iOS die App in dem Moment, in dem sie die Kamera anfordert. echo "==> Kamera-Berechtigung eintragen" KAMERAGRUND="Liest den QR-Code deines Zugangs-Tokens bei der Ersteinrichtung." if ! /usr/libexec/PlistBuddy -c "Set :NSCameraUsageDescription $KAMERAGRUND" "$PLIST" 2>/dev/null; then /usr/libexec/PlistBuddy -c "Add :NSCameraUsageDescription string $KAMERAGRUND" "$PLIST" fi /usr/libexec/PlistBuddy -c "Print :NSCameraUsageDescription" "$PLIST" >/dev/null || { echo "FEHLER: NSCameraUsageDescription liess sich nicht setzen." >&2 exit 1 } # Share-Erweiterung ins Xcode-Projekt haengen. # # Aus demselben Grund wie die Team-Kennung und der Standort-Schluessel oben: # ios/ ist gitignored, also verschwindet jedes von Hand angelegte Ziel beim # naechsten `npx cap add ios`. Die Quellen liegen versioniert unter native/, # das Skript spielt sie bei jedem Bau wieder ein. Es ist idempotent - ein # zweiter Lauf ueber ein fertiges Projekt aendert nichts. # # Schlaegt es fehl, bricht der ganze Bau ab: eine App ohne den Eintrag im # Teilen-Blatt sieht fertig aus und ist es nicht, und der Unterschied faellt # erst auf dem Geraet auf. echo "==> Share-Erweiterung einrichten" node scripts/ios-teilen-einrichten.mjs # App-Symbol einspielen. # # Capacitors Vorlage bringt ein eigenes Platzhalter-Symbol mit, und ios/ ist # gitignored - ein in Xcode eingesetztes Symbol waere beim naechsten # `npx cap add ios` wieder weg. Die Vorlage liegt deshalb versioniert unter # native/AppIcon.appiconset und wird hier ueber die von Capacitor erzeugte # gelegt. # # Drei Ausfuehrungen, wie iOS 18 sie erwartet: hell mit eigenem Grund, dunkel # und getoent ohne - dort legt das System seinen eigenen Grund darunter, und # ein mitgeliefertes Schwarz bliebe darauf als Kasten sichtbar. echo "==> App-Symbol einspielen" SYMBOLZIEL="ios/App/App/Assets.xcassets/AppIcon.appiconset" rm -rf "$SYMBOLZIEL" mkdir -p "$SYMBOLZIEL" cp -R native/AppIcon.appiconset/. "$SYMBOLZIEL"/ [ -f "$SYMBOLZIEL/Contents.json" ] || { echo "FEHLER: App-Symbol nicht eingespielt." >&2; exit 1; } echo " hell, dunkel, getoent" # Versionsnummer aus der manifest.json in die App schreiben. # # Capacitors Vorlage setzt 1.0/1 und laesst es dabei - zwei nacheinander # aufgespielte .ipa sind auf dem Geraet dadurch nicht zu unterscheiden, und # man weiss nicht, welche gerade drauf ist. Dieselbe Zahl, die schon Panel, # OTA-Buendel und __APP_VERSION__ tragen (siehe VERSIONIERUNG.md): eine # Quelle, kein zweiter Ort zum Vergessen. echo "==> Versionsnummer eintragen" VERSION="$(node -p "require('../custom_components/audi_dashboard/manifest.json').version")" [ -n "$VERSION" ] || { echo "FEHLER: keine Version in der manifest.json." >&2; exit 1; } if ! /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" "$PLIST" 2>/dev/null; then /usr/libexec/PlistBuddy -c "Add :CFBundleShortVersionString string $VERSION" "$PLIST" fi BUILDNR="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$PLIST" 2>/dev/null || echo 1)" # In der Info.plist der App steht dort KEINE Zahl, sondern der Platzhalter # einer Build-Einstellung ($(CURRENT_PROJECT_VERSION), aus dem Capacitor- # Geruest). Fuer das App-Ziel loest Xcode ihn zu 1 auf - das Ziel der # Erweiterung kennt die Einstellung aber gar nicht, dort wurde daraus LEER, # und ein Buendel ohne CFBundleVersion lehnt iOS beim Installieren ab # ("kann nicht installiert werden"). Genau so ist die .ipa vom 04.09.2026 # unbrauchbar geworden - an der ausgelieferten Datei nachgemessen: App # CFBundleVersion 1, Erweiterung gar keine. Deshalb hier nur eine echte Zahl # weiterreichen, sonst die 1. case "$BUILDNR" in ""|*'$('*|*[!0-9.]*) BUILDNR=1 ;; esac # Die Share-Erweiterung MUSS dieselben beiden Nummern tragen wie die App. # # Apples App Extension Programming Guide verlangt das ausdruecklich, und iOS # haelt sich daran: stimmen CFBundleShortVersionString und CFBundleVersion # nicht ueberein, registriert das System die Erweiterung nicht - sie taucht im # Teilen-Blatt dann gar nicht erst auf, ohne jede Fehlermeldung. Genau das war # in der .ipa vom 03.09.2026 der Fall: App 2026.9.3.3, Erweiterung 1.0 (an der # ausgelieferten Datei nachgemessen, nicht vermutet - native/teilen/Info.plist # traegt die 1.0 fest, und dieser Schritt schrieb bis dahin nur $PLIST). ERW_PLIST="ios/App/DataMetric360Share/Info.plist" if [ -f "$ERW_PLIST" ]; then /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" "$ERW_PLIST" /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BUILDNR" "$ERW_PLIST" echo " $VERSION ($BUILDNR), App und Erweiterung" else echo " $VERSION ($BUILDNR) - Erweiterung nicht gefunden, nur die App" fi # Zwischengespeicherte Bereitstellungsprofile dieser App wegraeumen. # # `-allowProvisioningUpdates` zieht ein neues Profil nur, wenn kein passendes # herumliegt - ein vorhandenes wird stillschweigend weiterbenutzt, auch wenn # sich seither die Berechtigungen geaendert haben. Genau daran ist am # 2026-08-29 eine Installation gescheitert ("Integritaet konnte nicht # geprueft werden"): das eingebettete Profil kannte ein Geraet, das Team # hatte inzwischen zwei. Mit der App-Gruppe der Share-Erweiterung ist # derselbe Fall erneut eingetreten - jedes vor dem 2026-08-31 gezogene # Profil kennt sie nicht. # # Weggeraeumt wird nur, was zu dieser App gehoert (Bundle-ID im entpackten # Profil gesucht), nicht der ganze Ordner - dort liegen auch die Profile # anderer Projekte. Mit --profile-behalten laesst sich der Schritt # ueberspringen. if [ "${1:-}" != "--profile-behalten" ]; then echo "==> Alte Bereitstellungsprofile wegraeumen" PROFILE="$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles" if [ -d "$PROFILE" ]; then ANZAHL=0 for datei in "$PROFILE"/*.mobileprovision; do [ -e "$datei" ] || continue if security cms -D -i "$datei" 2>/dev/null | grep -qF "app.datametric360"; then rm -f "$datei" ANZAHL=$((ANZAHL + 1)) fi done echo " $ANZAHL entfernt - xcodebuild zieht sie gleich neu" else echo " keine vorhanden" fi else echo "==> Bereitstellungsprofile bleiben (--profile-behalten)" fi echo "==> Archiv bauen und signieren (Team $TEAM)" rm -rf "$AUSGABE" mkdir -p "$AUSGABE" xcodebuild \ -project ios/App/App.xcodeproj \ -scheme App \ -configuration Release \ -destination 'generic/platform=iOS' \ -archivePath "$ARCHIV" \ -allowProvisioningUpdates \ DEVELOPMENT_TEAM="$TEAM" \ archive # Ad-hoc, nicht development: das ist die von Apple vorgesehene Art fuer die # Installation ueber die Luft (siehe ios-luftweg.sh) und deckt den Weg per # Kabel gleich mit ab. Sie verlangt ein Apple-Distribution-Zertifikat, das # -allowProvisioningUpdates bei Bedarf selbst anlegt - moeglich erst seit der # bezahlten Mitgliedschaft. Die Signatur laeuft damit ein Jahr. cat > "$AUSGABE/ExportOptions.plist" < methodad-hoc teamID$TEAM signingStyleautomatic stripSwiftSymbols PLIST echo "==> .ipa exportieren" xcodebuild -exportArchive \ -archivePath "$ARCHIV" \ -exportOptionsPlist "$AUSGABE/ExportOptions.plist" \ -exportPath "$AUSGABE" \ -allowProvisioningUpdates # Am FERTIGEN Buendel gegenlesen, nicht am Projekt. # # Abschnitt AK der AGENTS.md: von fuenf Fehlern in der nativen Huelle haben # vier einen erfolgreichen Archivlauf erzeugt und sind erst am entpackten # .ipa aufgefallen. Geprueft wird hier genau das, was still schiefgehen kann: # eine Erweiterung ohne Programm, ohne App-Gruppe, oder mit abweichender # Versionsnummer (dann registriert iOS sie nicht und sie fehlt im # Teilen-Blatt). echo "==> Fertige .ipa gegenlesen" PRUEF="$(mktemp -d)" if unzip -q "$AUSGABE/App.ipa" -d "$PRUEF"; then ERW="$PRUEF/Payload/App.app/PlugIns/DataMetric360Share.appex" if [ ! -x "$ERW/DataMetric360Share" ] && [ ! -f "$ERW/DataMetric360Share" ]; then echo " WARNUNG: die Erweiterung hat kein Programm - sie wird nichts tun." fi ERW_V="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$ERW/Info.plist" 2>/dev/null || echo '?')" APP_V="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$PRUEF/Payload/App.app/Info.plist" 2>/dev/null || echo '?')" if [ "$ERW_V" != "$APP_V" ]; then echo " WARNUNG: Erweiterung $ERW_V, App $APP_V - iOS registriert die" echo " Erweiterung dann nicht, sie fehlt im Teilen-Blatt." else echo " Erweiterung und App tragen beide $APP_V" fi # CFBundleVersion ist Pflicht. Fehlt sie in EINEM der beiden Buendel, # verweigert iOS die Installation der ganzen App - ohne Hinweis darauf, # woran es liegt. Deshalb hart pruefen, nicht nur warnen. for ZIEL in "$PRUEF/Payload/App.app" "$ERW"; do BV="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$ZIEL/Info.plist" 2>/dev/null || echo '')" case "$BV" in ""|*'$('*) echo " FEHLER: $(basename "$ZIEL") hat keine brauchbare CFBundleVersion ('$BV')." >&2 echo " iOS wuerde die Installation ablehnen - Bau abgebrochen." >&2 rm -rf "$PRUEF" exit 1 ;; esac done for ZIEL in "$PRUEF/Payload/App.app" "$ERW"; do if ! codesign -d --entitlements :- "$ZIEL" 2>/dev/null | grep -q "group.app.datametric360"; then echo " WARNUNG: $(basename "$ZIEL") ohne App-Gruppe - Teilen kann nichts uebergeben." fi done # Eigene Plugins muessen in packageClassList stehen. Capacitor 8 sucht # Plugin-Klassen NICHT im Programm (registerPlugins() in # CapacitorBridge.swift liest nur diese Liste) - eine einkompilierte # Klasse, die dort fehlt, existiert fuer die Bruecke nicht. Genau so kam # die .ipa vom 04.09.2026 heraus: Zwischenablage und Kalendertermin waren # im Programm und trotzdem tot. KONF="$PRUEF/Payload/App.app/capacitor.config.json" for KLASSE in $(grep -ho "@objc([A-Za-z0-9_]*)" "$APP/native"/*.swift | sed "s/@objc(//;s/)//"); do if ! grep -q "$KLASSE" "$KONF" 2>/dev/null; then echo " FEHLER: $KLASSE fehlt in packageClassList - das Plugin bliebe stumm." >&2 rm -rf "$PRUEF" exit 1 fi done echo " packageClassList: eigene Plugins eingetragen" fi rm -rf "$PRUEF" # Erst JETZT ablegen. auslieferung/ ist versioniert und wird ausgeliefert - # eine Datei, die die Pruefung oben nicht besteht, darf dort nicht landen und # schon gar nicht committet werden. (ios/ ist gitignored, sonst existierte das # Ergebnis nur auf diesem einen Mac.) mkdir -p "$APP/auslieferung" cp "$AUSGABE/App.ipa" "$APP/auslieferung/App.ipa" # Auslieferung ueber die Luft gleich mitschreiben. # # manifest.plist und die Installationsseite hingen bisher an einem zweiten, # von Hand aufzurufenden Skript - und ohne sie ist die frische .ipa auf dem # Telefon nicht installierbar, obwohl sie fertig im Repo liegt. Ein Schritt, # den man vergessen kann, gehoert in den Bau. # # Die Basis-Adresse ist die des Gitea-Repos: es liefert die Dateien bereits # ueber HTTPS mit oeffentlich vertrauenswuerdigem Zertifikat aus, was iOS # fuer diesen Weg zwingend verlangt (siehe ios-luftweg.sh). Ueberschreibbar # ueber LUFTWEG_BASIS, falls der Weg einmal ueber einen eigenen Host geht. echo "==> Auslieferung ueber die Luft vorbereiten" LUFTWEG_BASIS="${LUFTWEG_BASIS:-https://gitea.nothaft.cloud/paul/audi-app/raw/branch/main/companion-app/auslieferung}" bash "$HIER/ios-luftweg.sh" "$LUFTWEG_BASIS" auslieferung echo echo "Fertig: $APP/auslieferung/App.ipa" echo "Version: $VERSION" echo echo "Noch zu tun:" echo " 1. git add companion-app/auslieferung && git commit && git push" echo " (die .ipa und manifest.plist muessen im Repo liegen, sonst kann" echo " das Telefon sie nicht laden)" echo " 2. Auf dem iPhone in Safari oeffnen:" echo " itms-services://?action=download-manifest&url=$LUFTWEG_BASIS/manifest.plist" echo echo "Per Kabel stattdessen: xcrun devicectl device install app --device $APP/auslieferung/App.ipa"