build: LehrerApp.McpBridge ins macOS-App-Bundle aufnehmen

build-macos-app.sh publiziert und platziert jetzt zusätzlich zur
Hauptapp die MCP-Bridge-Binary (Contents/MacOS/LehrerApp.McpBridge),
inklusive Universal-Binary-Handling und eigenständiger Ad-hoc-Signierung
vor dem Bundle-weiten codesign --deep-Schritt. Publish/Lipo/Kopier-Logik
dafür in eine wiederverwendbare place_binary()-Funktion extrahiert.

Nur auf Bash-Syntax geprüft (kein macOS in dieser Session verfügbar) -
noch nicht auf echtem macOS gebaut/verifiziert, siehe TODO.md 4.5.27.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-11 20:23:45 +02:00
co-authored by Claude Sonnet 5
parent 9567d8d616
commit 0c60a54c4d
2 changed files with 92 additions and 29 deletions
+21
View File
@@ -2536,6 +2536,27 @@ folgenden Punkte gehören direkt in `LehrerApp.Desktop`:
einer rohen GUID enthält, und dass eine unveränderte Gruppenmitgliedschaft keine erneute einer rohen GUID enthält, und dass eine unveränderte Gruppenmitgliedschaft keine erneute
Nachfrage auslöst. Nachfrage auslöst.
- [x] **4.5.27** `build-macos-app.sh` um die Bridge-Binary erweitert (2026-09-11, aus 4.5.25/4.5.26
zurückgestellt): `LehrerApp.McpBridge` wird jetzt als zweites Self-Contained-Publish pro
Architektur gebaut und als lose Executable neben die Hauptapp nach
`Contents/MacOS/LehrerApp.McpBridge` gelegt (nicht als `CFBundleExecutable` - sie wird nie
von Finder/launchd gestartet, sondern vom KI-Client direkt über ihren vollen Pfad als
Kindprozess). Die bisher einmalige Publish/Universal-Binary/Kopier-Logik ist in eine
`place_binary()`-Funktion extrahiert und wird für Hauptapp und Bridge gleichermaßen
aufgerufen. Bridge wird **zusätzlich einzeln** ad-hoc signiert (Schritt 6/7), bevor der
Bundle-weite `codesign --deep`-Schritt (7/7) läuft — sie liegt lose in `Contents/MacOS`
statt in einem eigenen `.framework`/`.bundle`, daher nicht verlassen auf das (in dem Fall
nicht sicher dokumentierte) Rekursionsverhalten von `--deep`.
- **Nicht gelöst, bewusst zurückgestellt:** Laufzeit-Duplikation (beide Publishes bringen
ihre eigene self-contained .NET-Runtime mit) — siehe Planungsdokument, spätere Optimierung
über einen framework-dependent Bridge-Build + angepasste `runtimeconfig.json`.
- **Nicht verifiziert:** Diese Session lief auf Windows, ohne Zugriff auf `sips`/`iconutil`/
`lipo`/`codesign` — das Skript wurde nur auf Bash-Syntax geprüft (`bash -n`), nicht auf
einem echten Mac gebaut/ausgeführt. Vor Verlass auf das erzeugte Bundle: auf macOS
`./build-macos-app.sh` laufen lassen und `open build/LehrerApp.app` sowie einen
MCP-Client, der auf `build/LehrerApp.app/Contents/MacOS/LehrerApp.McpBridge` zeigt, gegen
die laufende App testen.
**Wichtige Abweichung von der ursprünglichen Planung (5.2):** Vor der Umsetzung zeigte sich, **Wichtige Abweichung von der ursprünglichen Planung (5.2):** Vor der Umsetzung zeigte sich,
dass 5.2 wie ursprünglich beschrieben eine zweite, parallele Fehlzeiten-Erfassung neben dem dass 5.2 wie ursprünglich beschrieben eine zweite, parallele Fehlzeiten-Erfassung neben dem
bereits bestehenden Anwesenheits-Tracking aus Kapitel 3 (`ParticipationEntry.Attendance`, bereits bestehenden Anwesenheits-Tracking aus Kapitel 3 (`ParticipationEntry.Attendance`,
+71 -29
View File
@@ -3,13 +3,20 @@ set -euo pipefail
# ============================================================ # ============================================================
# build-macos-app.sh # build-macos-app.sh
# Baut LehrerApp.Desktop als natives macOS .app-Bundle # Baut LehrerApp.Desktop als natives macOS .app-Bundle, inklusive
# LehrerApp.McpBridge als Companion-Binary im selben Bundle
# (Contents/MacOS/LehrerApp.McpBridge neben Contents/MacOS/LehrerApp.Desktop) -
# siehe TODO.md 4.5.25 zur Architektur-Begründung (Bridge muss im selben
# Bundle liegen, damit sie ihren Pfad relativ zur Hauptapp ermitteln und sich
# selbst in die MCP-Server-Config eines KI-Clients eintragen kann).
# ============================================================ # ============================================================
# ---- Konfiguration (bei Bedarf anpassen) -------------------- # ---- Konfiguration (bei Bedarf anpassen) --------------------
PROJECT_PATH="LehrerApp.Desktop" # Pfad zum .csproj / Projektordner PROJECT_PATH="LehrerApp.Desktop" # Pfad zum .csproj / Projektordner
APP_NAME="LehrerApp" # Name des App-Bundles (LehrerApp.app) APP_NAME="LehrerApp" # Name des App-Bundles (LehrerApp.app)
EXECUTABLE_NAME="LehrerApp.Desktop" # Name der Binary aus dotnet publish (= AssemblyName des Projekts!) EXECUTABLE_NAME="LehrerApp.Desktop" # Name der Binary aus dotnet publish (= AssemblyName des Projekts!)
BRIDGE_PROJECT_PATH="LehrerApp.McpBridge" # Pfad zum Bridge-.csproj / Projektordner
BRIDGE_EXECUTABLE_NAME="LehrerApp.McpBridge" # Name der Bridge-Binary (= AssemblyName des Bridge-Projekts!)
BUNDLE_ID="de.deinname.lehrerapp" # CFBundleIdentifier BUNDLE_ID="de.deinname.lehrerapp" # CFBundleIdentifier
VERSION="1.0.0" # CFBundleShortVersionString VERSION="1.0.0" # CFBundleShortVersionString
MIN_MACOS="12.0" # LSMinimumSystemVersion MIN_MACOS="12.0" # LSMinimumSystemVersion
@@ -25,49 +32,67 @@ CONTENTS_DIR="${APP_BUNDLE}/Contents"
MACOS_DIR="${CONTENTS_DIR}/MacOS" MACOS_DIR="${CONTENTS_DIR}/MacOS"
RESOURCES_DIR="${CONTENTS_DIR}/Resources" RESOURCES_DIR="${CONTENTS_DIR}/Resources"
echo "=== 1/6: Alte Build-Artefakte entfernen ===" echo "=== 1/7: Alte Build-Artefakte entfernen ==="
rm -rf "${BUILD_DIR}" rm -rf "${BUILD_DIR}"
mkdir -p "${BUILD_DIR}" mkdir -p "${BUILD_DIR}"
echo "=== 2/6: dotnet publish für jede Architektur ===" echo "=== 2/7: dotnet publish für jede Architektur (Hauptapp + Bridge) ==="
for RID in "${RUNTIME_IDS[@]}"; do for RID in "${RUNTIME_IDS[@]}"; do
echo " -> Publish für ${RID}" echo " -> Publish Hauptapp für ${RID}"
dotnet publish "${PROJECT_PATH}" \ dotnet publish "${PROJECT_PATH}" \
-c Release \ -c Release \
-r "${RID}" \ -r "${RID}" \
--self-contained true \ --self-contained true \
-p:PublishSingleFile=false \ -p:PublishSingleFile=false \
-o "${BUILD_DIR}/publish/${RID}" -o "${BUILD_DIR}/publish/${RID}"
echo " -> Publish McpBridge für ${RID}"
dotnet publish "${BRIDGE_PROJECT_PATH}" \
-c Release \
-r "${RID}" \
--self-contained true \
-p:PublishSingleFile=false \
-o "${BUILD_DIR}/publish-bridge/${RID}"
done done
echo "=== 3/6: App-Bundle-Struktur anlegen ===" echo "=== 3/7: App-Bundle-Struktur anlegen (Hauptapp + Bridge) ==="
mkdir -p "${MACOS_DIR}" "${RESOURCES_DIR}" mkdir -p "${MACOS_DIR}" "${RESOURCES_DIR}"
if [ "${UNIVERSAL_BUILD}" = true ] && [ "${#RUNTIME_IDS[@]}" -ge 2 ]; then # Kopiert den Publish-Output einer Architektur (oder beider via lipo zu einem Universal Binary)
echo " -> Universal Binary via lipo erzeugen" # nach MACOS_DIR. Für Hauptapp und Bridge wiederverwendet - beide sind self-contained-Publishes
SRC_A="${BUILD_DIR}/publish/${RUNTIME_IDS[0]}/${EXECUTABLE_NAME}" # derselben TFM, gemeinsame .NET-Runtime-Dateien werden beim zweiten Aufruf einfach mit sich
SRC_B="${BUILD_DIR}/publish/${RUNTIME_IDS[1]}/${EXECUTABLE_NAME}" # selbst überschrieben (keine Laufzeit-Duplikate im fertigen Bundle vermieden, nur der
# Publish-Ordner enthält sie doppelt - siehe TODO.md 4.5.25 zur noch offenen
# Runtime-Dedup-Optimierung, für v1 bewusst nicht angegangen).
place_binary() {
local publish_root="$1"
local executable_name="$2"
# Alle Nicht-Binärdateien (Assets etc.) aus der ersten Architektur übernehmen if [ "${UNIVERSAL_BUILD}" = true ] && [ "${#RUNTIME_IDS[@]}" -ge 2 ]; then
rsync -a --exclude "${EXECUTABLE_NAME}" "${BUILD_DIR}/publish/${RUNTIME_IDS[0]}/" "${MACOS_DIR}/" local src_a="${publish_root}/${RUNTIME_IDS[0]}/${executable_name}"
local src_b="${publish_root}/${RUNTIME_IDS[1]}/${executable_name}"
rsync -a --exclude "${executable_name}" "${publish_root}/${RUNTIME_IDS[0]}/" "${MACOS_DIR}/"
lipo -create -output "${MACOS_DIR}/${executable_name}" "${src_a}" "${src_b}"
else
cp -r "${publish_root}/${RUNTIME_IDS[0]}/"* "${MACOS_DIR}/"
fi
lipo -create -output "${MACOS_DIR}/${EXECUTABLE_NAME}" "${SRC_A}" "${SRC_B}" if [ ! -f "${MACOS_DIR}/${executable_name}" ]; then
else echo "FEHLER: Erwartete Executable '${executable_name}' nicht in ${MACOS_DIR} gefunden."
echo " -> Einzelne Architektur (${RUNTIME_IDS[0]}) übernehmen" echo "Tatsächlicher Inhalt von ${publish_root}/${RUNTIME_IDS[0]}/:"
cp -r "${BUILD_DIR}/publish/${RUNTIME_IDS[0]}/"* "${MACOS_DIR}/" ls "${publish_root}/${RUNTIME_IDS[0]}/"
fi echo "-> Passe den entsprechenden EXECUTABLE_NAME im Skript-Kopf an."
exit 1
fi
chmod +x "${MACOS_DIR}/${executable_name}"
}
if [ ! -f "${MACOS_DIR}/${EXECUTABLE_NAME}" ]; then echo " -> Hauptapp"
echo "FEHLER: Erwartete Executable '${EXECUTABLE_NAME}' nicht in ${MACOS_DIR} gefunden." place_binary "${BUILD_DIR}/publish" "${EXECUTABLE_NAME}"
echo "Tatsächlicher Inhalt von ${BUILD_DIR}/publish/${RUNTIME_IDS[0]}/:" echo " -> Bridge"
ls "${BUILD_DIR}/publish/${RUNTIME_IDS[0]}/" place_binary "${BUILD_DIR}/publish-bridge" "${BRIDGE_EXECUTABLE_NAME}"
echo "-> Passe EXECUTABLE_NAME im Skript-Kopf entsprechend an."
exit 1
fi
chmod +x "${MACOS_DIR}/${EXECUTABLE_NAME}" echo "=== 4/7: Icon konvertieren (.icns) ==="
echo "=== 4/6: Icon konvertieren (.icns) ==="
if [ -f "${ICON_PNG}" ]; then if [ -f "${ICON_PNG}" ]; then
ICONSET_DIR="${BUILD_DIR}/icon.iconset" ICONSET_DIR="${BUILD_DIR}/icon.iconset"
mkdir -p "${ICONSET_DIR}" mkdir -p "${ICONSET_DIR}"
@@ -90,7 +115,7 @@ else
ICON_KEY="" ICON_KEY=""
fi fi
echo "=== 5/6: Info.plist schreiben ===" echo "=== 5/7: Info.plist schreiben ==="
cat > "${CONTENTS_DIR}/Info.plist" <<PLIST cat > "${CONTENTS_DIR}/Info.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?> <?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"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -110,7 +135,23 @@ cat > "${CONTENTS_DIR}/Info.plist" <<PLIST
</plist> </plist>
PLIST PLIST
echo "=== 6/6: Signieren ===" # Die Bridge ist bewusst NICHT CFBundleExecutable - sie wird nie von Finder/launchd gestartet,
# sondern ausschließlich vom KI-Client als Kindprozess über ihren vollen Pfad
# (Contents/MacOS/LehrerApp.McpBridge). Kein eigener Eintrag im Info.plist nötig.
echo "=== 6/7: Bridge-Binary einzeln signieren ==="
if [ "${CODESIGN_ADHOC}" = true ]; then
# Getrennt vom Bundle-weiten --deep-Signieren in Schritt 7: die Bridge ist eine zweite, lose
# liegende Mach-O-Executable direkt in Contents/MacOS (kein eigenes .framework/.bundle) - explizit
# zuerst signieren statt sich allein auf die Rekursion von --deep zu verlassen, damit die
# Bridge auch dann ein gültiges Signat trägt, falls --deep das nicht selbst abdeckt.
codesign --force --sign - "${MACOS_DIR}/${BRIDGE_EXECUTABLE_NAME}"
echo " -> Bridge ad-hoc signiert."
else
echo " -> Signierung übersprungen."
fi
echo "=== 7/7: Bundle-weit signieren ==="
if [ "${CODESIGN_ADHOC}" = true ]; then if [ "${CODESIGN_ADHOC}" = true ]; then
codesign --force --deep --sign - "${APP_BUNDLE}" codesign --force --deep --sign - "${APP_BUNDLE}"
echo " -> Ad-hoc signiert (nur lokal lauffähig, Gatekeeper-Warnung bei Weitergabe)." echo " -> Ad-hoc signiert (nur lokal lauffähig, Gatekeeper-Warnung bei Weitergabe)."
@@ -120,4 +161,5 @@ fi
echo "" echo ""
echo "Fertig! App-Bundle liegt unter: ${APP_BUNDLE}" echo "Fertig! App-Bundle liegt unter: ${APP_BUNDLE}"
echo "Test mit: open '${APP_BUNDLE}'" echo "Test mit: open '${APP_BUNDLE}'"
echo "Bridge-Binary für die MCP-Server-Config eines KI-Clients: ${APP_BUNDLE}/Contents/MacOS/${BRIDGE_EXECUTABLE_NAME}"