#!/usr/bin/env node /** * Hängt die Share-Erweiterung ins erzeugte Xcode-Projekt. * * WARUM ES DIESES SKRIPT GIBT * --------------------------- * `companion-app/ios/` ist absichtlich gitignored — `npx cap add ios` baut den * Ordner aus dem Webbündel jederzeit neu. Damit verschwindet aber auch jedes * Ziel, das man in Xcode von Hand anlegt. Dieselbe Falle, die schon die * Team-Kennung und den Standort-Schlüssel in `ios-signieren.sh` getrieben hat: * was den nächsten `cap add` überleben soll, muss versioniert sein und bei * jedem Bau neu eingespielt werden. * * Die Quellen liegen deshalb unter `companion-app/native/`; dieses Skript * kopiert sie nach `ios/App/` und trägt das Erweiterungsziel in die * `project.pbxproj` ein. * * WAS ES ANLEGT * ------------- * - Ziel `DataMetric360Share` (App-Erweiterung, Bundle-ID * `app.datametric360.teilen`) * - dessen Quelldateien, Info.plist und Entitlements * - die App-Gruppe `group.app.datametric360` auf BEIDEN Zielen * - die Einbettung der Erweiterung in die App (Build-Phase * „Embed App Extensions") * - das URL-Schema `datametric360://`, über das die Erweiterung die App holt * * IDEMPOTENT * ---------- * Ein zweiter Lauf über ein bereits eingerichtetes Projekt ändert nichts und * meldet das. Wichtig, weil `ios-signieren.sh` bei jedem Bau durchläuft und * `ios/` nicht immer neu erzeugt wird. * * NICHT GETESTET AUF EINEM MAC * ---------------------------- * Geschrieben ohne Zugriff auf macOS/Xcode. Die pbxproj-Bearbeitung folgt der * API des npm-Pakets `xcode`; scheitert sie, bricht das Skript mit einer * klaren Meldung ab und `docs/SHARE_EXTENSION.md` beschreibt die Handgriffe, * die dasselbe in Xcode erledigen. Lieber ein lauter Abbruch mit Anleitung * als ein halb eingerichtetes Projekt. */ import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync } from "node:fs" import { dirname, join } from "node:path" import { fileURLToPath } from "node:url" import xcode from "xcode" const HIER = dirname(fileURLToPath(import.meta.url)) const APP = dirname(HIER) const ZIEL_NAME = "DataMetric360Share" const BUNDLE_ID = "app.datametric360.teilen" const GRUPPE = "group.app.datametric360" const SCHEMA = "datametric360" const iosApp = join(APP, "ios", "App") const pbxPfad = join(iosApp, "App.xcodeproj", "project.pbxproj") const quelleErw = join(APP, "native", "teilen") const zielErw = join(iosApp, ZIEL_NAME) function melden(text) { console.log(" " + text) } function abbruch(text) { console.error("") console.error("FEHLER: " + text) console.error("") console.error("Die Share-Erweiterung ist damit NICHT eingerichtet. Die App selbst") console.error("baut und läuft weiterhin - nur der Eintrag im Teilen-Blatt fehlt.") console.error("Von Hand nachziehen: companion-app/docs/SHARE_EXTENSION.md") process.exit(1) } if (!existsSync(pbxPfad)) { abbruch(`${pbxPfad} nicht gefunden - erst 'npx cap sync ios' laufen lassen.`) } /* ---------------------------------------------------------------- Dateien */ mkdirSync(zielErw, { recursive: true }) for (const datei of ["ShareViewController.swift", "Info.plist", `${ZIEL_NAME}.entitlements`]) { copyFileSync(join(quelleErw, datei), join(zielErw, datei)) } copyFileSync(join(APP, "native", "App.entitlements"), join(iosApp, "App", "App.entitlements")) melden(`Quellen nach ios/App/${ZIEL_NAME}/ kopiert`) /* ------------------------------------------------------------- URL-Schema */ /* Ohne dieses Schema kann die Erweiterung die App nicht holen - sie legt den Beleg zwar ab, aber niemand merkt es, bis die App das nächste Mal von Hand geöffnet wird. */ const appPlistPfad = join(iosApp, "App", "Info.plist") let appPlist = readFileSync(appPlistPfad, "utf8") if (!appPlist.includes("CFBundleURLTypes")) { const eintrag = [ "\tCFBundleURLTypes", "\t", "\t\t", "\t\t\tCFBundleURLName", `\t\t\tapp.datametric360`, "\t\t\tCFBundleURLSchemes", "\t\t\t", `\t\t\t\t${SCHEMA}`, "\t\t\t", "\t\t", "\t", "", ].join("\n") const letztes = appPlist.lastIndexOf("") if (letztes < 0) abbruch("ios/App/App/Info.plist hat keine schließende -Marke.") appPlist = appPlist.slice(0, letztes) + eintrag + appPlist.slice(letztes + "".length) writeFileSync(appPlistPfad, appPlist, "utf8") melden(`URL-Schema ${SCHEMA}:// eingetragen`) } else { melden("URL-Schema war bereits eingetragen") } /* ---------------------------------------------------------------- Projekt */ const projekt = xcode.project(pbxPfad) projekt.parseSync() const vorhanden = Object.values(projekt.pbxNativeTargetSection()).some( (z) => z && typeof z === "object" && z.name === ZIEL_NAME, ) if (vorhanden) { melden(`Ziel ${ZIEL_NAME} war bereits im Projekt - nichts geändert`) process.exit(0) } let ziel try { ziel = projekt.addTarget(ZIEL_NAME, "app_extension", ZIEL_NAME, BUNDLE_ID) // Eigene Build-Phasen: ohne sie hat das Ziel keine Quelldateien und keine // Ressourcen, und Xcode baut ein leeres Bündel. projekt.addBuildPhase([], "PBXSourcesBuildPhase", "Sources", ziel.uuid) projekt.addBuildPhase([], "PBXResourcesBuildPhase", "Resources", ziel.uuid) projekt.addBuildPhase([], "PBXFrameworksBuildPhase", "Frameworks", ziel.uuid) const gruppe = projekt.addPbxGroup( ["ShareViewController.swift", "Info.plist", `${ZIEL_NAME}.entitlements`], ZIEL_NAME, ZIEL_NAME, ) // In die Projektwurzel einhängen, sonst taucht die Gruppe in Xcode nicht auf. const wurzel = Object.keys(projekt.hash.project.objects.PBXGroup).find( (k) => projekt.hash.project.objects.PBXGroup[k].name === "CustomTemplate", ) if (wurzel) projekt.addToPbxGroup(gruppe.uuid, wurzel) projekt.addSourceFile("ShareViewController.swift", { target: ziel.uuid }, gruppe.uuid) // Einstellungen, die `addTarget` nicht selbst setzt. const konfigs = projekt.pbxXCBuildConfigurationSection() for (const schluessel of Object.keys(konfigs)) { const eintrag = konfigs[schluessel] if (!eintrag || typeof eintrag !== "object" || !eintrag.buildSettings) continue const s = eintrag.buildSettings if (s.PRODUCT_BUNDLE_IDENTIFIER === `"${BUNDLE_ID}"` || s.PRODUCT_NAME === `"${ZIEL_NAME}"`) { s.INFOPLIST_FILE = `"${ZIEL_NAME}/Info.plist"` s.CODE_SIGN_ENTITLEMENTS = `"${ZIEL_NAME}/${ZIEL_NAME}.entitlements"` s.IPHONEOS_DEPLOYMENT_TARGET = "14.0" s.SWIFT_VERSION = "5.0" s.TARGETED_DEVICE_FAMILY = `"1,2"` s.SKIP_INSTALL = "YES" } // Die App selbst braucht ihre neue Entitlements-Datei. if (s.PRODUCT_BUNDLE_IDENTIFIER === `"app.datametric360"`) { s.CODE_SIGN_ENTITLEMENTS = `"App/App.entitlements"` } } // Erweiterung in die App einbetten - ohne diese Phase liegt sie zwar im // Projekt, wandert aber nicht mit ins fertige .ipa. projekt.addBuildPhase( [`${ZIEL_NAME}.appex`], "PBXCopyFilesBuildPhase", "Embed App Extensions", projekt.getFirstTarget().uuid, "app_extension", ) writeFileSync(pbxPfad, projekt.writeSync(), "utf8") } catch (fehler) { abbruch(`Das Xcode-Projekt ließ sich nicht bearbeiten: ${fehler.message}`) } melden(`Ziel ${ZIEL_NAME} angelegt und eingebettet`) melden(`App-Gruppe ${GRUPPE} auf beiden Zielen`) console.log("") console.log(" Einmalig im Apple-Entwicklerportal nötig, sonst schlägt das Signieren fehl:") console.log(` - App-Gruppe ${GRUPPE} anlegen`) console.log(` - App-ID ${BUNDLE_ID} anlegen und der Gruppe zuordnen`) console.log(` - App-ID app.datametric360 ebenfalls der Gruppe zuordnen`) console.log("")