#!/usr/bin/env bash set -euo pipefail # ============================================================ # build-macos-app.sh # 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) -------------------- PROJECT_PATH="LehrerApp.Desktop" # Pfad zum .csproj / Projektordner APP_NAME="LehrerApp" # Name des App-Bundles (LehrerApp.app) 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 VERSION="1.0.0" # CFBundleShortVersionString MIN_MACOS="12.0" # LSMinimumSystemVersion ICON_PNG="icon.png" # Quell-Icon (min. 512x512 oder 1024x1024) RUNTIME_IDS=("osx-arm64" "osx-x64") # Architekturen, für die gebaut wird UNIVERSAL_BUILD=true # true = zusätzlich Universal Binary via lipo CODESIGN_ADHOC=true # true = Ad-hoc-Signierung am Ende # ---- Ableitungen --------------------------------------------- BUILD_DIR="./build" APP_BUNDLE="${BUILD_DIR}/${APP_NAME}.app" CONTENTS_DIR="${APP_BUNDLE}/Contents" MACOS_DIR="${CONTENTS_DIR}/MacOS" RESOURCES_DIR="${CONTENTS_DIR}/Resources" echo "=== 1/7: Alte Build-Artefakte entfernen ===" rm -rf "${BUILD_DIR}" mkdir -p "${BUILD_DIR}" echo "=== 2/7: dotnet publish für jede Architektur (Hauptapp + Bridge) ===" for RID in "${RUNTIME_IDS[@]}"; do echo " -> Publish Hauptapp für ${RID}" dotnet publish "${PROJECT_PATH}" \ -c Release \ -r "${RID}" \ --self-contained true \ -p:PublishSingleFile=false \ -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 echo "=== 3/7: App-Bundle-Struktur anlegen (Hauptapp + Bridge) ===" mkdir -p "${MACOS_DIR}" "${RESOURCES_DIR}" # Kopiert den Publish-Output einer Architektur (oder beider via lipo zu einem Universal Binary) # nach MACOS_DIR. Für Hauptapp und Bridge wiederverwendet - beide sind self-contained-Publishes # derselben TFM, gemeinsame .NET-Runtime-Dateien werden beim zweiten Aufruf einfach mit sich # 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" if [ "${UNIVERSAL_BUILD}" = true ] && [ "${#RUNTIME_IDS[@]}" -ge 2 ]; then 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 if [ ! -f "${MACOS_DIR}/${executable_name}" ]; then echo "FEHLER: Erwartete Executable '${executable_name}' nicht in ${MACOS_DIR} gefunden." echo "Tatsächlicher Inhalt von ${publish_root}/${RUNTIME_IDS[0]}/:" ls "${publish_root}/${RUNTIME_IDS[0]}/" echo "-> Passe den entsprechenden EXECUTABLE_NAME im Skript-Kopf an." exit 1 fi chmod +x "${MACOS_DIR}/${executable_name}" } echo " -> Hauptapp" place_binary "${BUILD_DIR}/publish" "${EXECUTABLE_NAME}" echo " -> Bridge" place_binary "${BUILD_DIR}/publish-bridge" "${BRIDGE_EXECUTABLE_NAME}" echo "=== 4/7: Icon konvertieren (.icns) ===" if [ -f "${ICON_PNG}" ]; then ICONSET_DIR="${BUILD_DIR}/icon.iconset" mkdir -p "${ICONSET_DIR}" sips -z 16 16 "${ICON_PNG}" --out "${ICONSET_DIR}/icon_16x16.png" > /dev/null sips -z 32 32 "${ICON_PNG}" --out "${ICONSET_DIR}/icon_16x16@2x.png" > /dev/null sips -z 32 32 "${ICON_PNG}" --out "${ICONSET_DIR}/icon_32x32.png" > /dev/null sips -z 64 64 "${ICON_PNG}" --out "${ICONSET_DIR}/icon_32x32@2x.png" > /dev/null sips -z 128 128 "${ICON_PNG}" --out "${ICONSET_DIR}/icon_128x128.png" > /dev/null sips -z 256 256 "${ICON_PNG}" --out "${ICONSET_DIR}/icon_128x128@2x.png" > /dev/null sips -z 256 256 "${ICON_PNG}" --out "${ICONSET_DIR}/icon_256x256.png" > /dev/null sips -z 512 512 "${ICON_PNG}" --out "${ICONSET_DIR}/icon_256x256@2x.png" > /dev/null sips -z 512 512 "${ICON_PNG}" --out "${ICONSET_DIR}/icon_512x512.png" > /dev/null sips -z 1024 1024 "${ICON_PNG}" --out "${ICONSET_DIR}/icon_512x512@2x.png" > /dev/null iconutil -c icns "${ICONSET_DIR}" -o "${RESOURCES_DIR}/icon.icns" ICON_KEY="CFBundleIconFileicon.icns" else echo " -> Kein '${ICON_PNG}' gefunden, App-Bundle wird ohne Icon erstellt." ICON_KEY="" fi echo "=== 5/7: Info.plist schreiben ===" cat > "${CONTENTS_DIR}/Info.plist" < CFBundleName${APP_NAME} CFBundleDisplayName${APP_NAME} CFBundleExecutable${EXECUTABLE_NAME} CFBundleIdentifier${BUNDLE_ID} CFBundlePackageTypeAPPL CFBundleShortVersionString${VERSION} CFBundleVersion${VERSION} LSMinimumSystemVersion${MIN_MACOS} NSHighResolutionCapable ${ICON_KEY} PLIST # 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 codesign --force --deep --sign - "${APP_BUNDLE}" echo " -> Ad-hoc signiert (nur lokal lauffähig, Gatekeeper-Warnung bei Weitergabe)." else echo " -> Signierung übersprungen." fi echo "" echo "Fertig! App-Bundle liegt unter: ${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}"