Files
audi-app/companion-app/scripts/ios-signieren.sh
T
tobias 63a45d4ee2 Die .ipa war nicht installierbar, plus QR-Scanner (2026.9.4.8)
Der Erweiterung fehlte CFBundleVersion - eine Pflichtangabe, ohne die iOS
die Installation der ganzen App verweigert ("kann nicht installiert werden").
Verursacht von der Versionsangleichung vom Vortag: das Skript reichte den
Platzhalter $(CURRENT_PROJECT_VERSION) statt einer Zahl weiter, und das per
Skript angelegte Ziel der Erweiterung kennt diese Build-Einstellung nicht.
An den entpackten .ipa nachgemessen, 2026.9.3.3 gegen 2026.9.4.5.

ios-signieren.sh reicht nur noch echte Zahlen weiter, das Gegenlesen der
fertigen .ipa bricht ab statt zu warnen, und abgelegt wird erst danach -
eine Datei, die die Pruefung nicht besteht, darf nicht in auslieferung/
landen.

Dazu:
- Kopfmarke statt Firmierung des Betreibers in der Tankstellen-Zeile
- dritter Suchbegriff: der Name ohne fuehrende Marke. Gemessen: Nominatim
  findet "Shell, Pascalstr. 8, Ingolstadt" nicht, "Pascalstr. 8, Ingolstadt"
  schon - daran blieb die Belegkarte der realen Instanz leer
- der Tankstellen-Verweis zeigt die Tankstelle, statt eine Route zu rechnen
- Cache-Brecher auf .ipa, Symbolen und manifest.plist
- QR-Scanner fuer den Zugangs-Token bei der Ersteinrichtung (jsQR statt
  GoogleMLKit, damit der Xcode-Lauf kein pod install riskiert)

214/214 Tests, Panel als Modul geparst, live gegen audi_ha_test geprueft.
2026-09-04 11:51:44 +02:00

375 lines
18 KiB
Bash
Executable File

#!/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=<UDID>' -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" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key><string>ad-hoc</string>
<key>teamID</key><string>$TEAM</string>
<key>signingStyle</key><string>automatic</string>
<key>stripSwiftSymbols</key><true/>
</dict>
</plist>
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
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 <UDID> $APP/auslieferung/App.ipa"