diff --git a/apps/rebreak-native/plugins/with-rebreak-protection-ios.js b/apps/rebreak-native/plugins/with-rebreak-protection-ios.js index 998eb18..2a1a619 100644 --- a/apps/rebreak-native/plugins/with-rebreak-protection-ios.js +++ b/apps/rebreak-native/plugins/with-rebreak-protection-ios.js @@ -243,11 +243,15 @@ function withExtensionTarget(config) { // // KLASSISCHE App-Extension — anders als das NEURLFilter-Target: // - normaler `app_extension`-Produkttyp, -// - normale „Embed App Extensions"-Phase (dstSubfolderSpec 13, PlugIns/), +// - „Embed App Extensions"-Phase mit dstSubfolderSpec 13 (PlugIns/), // - KEIN ExtensionKit-Sonderweg (kein dstSubfolderSpec 16, kein -// EXAppExtensionAttributes). Der `addTarget`-Default ist hier schon korrekt -// — wir biegen die Embed-Phase NICHT um. +// EXAppExtensionAttributes). // - Deployment-Target = Main-App (iOS 16), KEIN 26.0-Gate. +// +// ACHTUNG: `addTarget` legt zwar eine korrekte dst-13-Phase an, embedded die +// .appex aber wegen einer Eigenheit der `xcode`-Lib in die NEURLFilter-dst-16- +// Phase (siehe ausführliche Begründung am Embed-Phase-Fix unten). Darum MUSS +// die .appex hier explizit in die eigene dst-13-Phase umgehängt werden. function withPacketTunnelTarget(config) { return withXcodeProject(config, async (cfg) => { @@ -326,13 +330,101 @@ function withPacketTunnelTarget(config) { } }); - // ── Embed-Phase: BEWUSST NICHT umbiegen ── - // proj.addTarget() hat bereits eine „Copy Files"-PBXCopyFilesBuildPhase - // (dstSubfolderSpec 13 = PlugIns/) im Haupt-Target angelegt, die unsere - // .appex einbettet. Für eine KLASSISCHE PluginKit-App-Extension (unser - // NEPacketTunnelProvider hat ein NSExtension-Dict in der Info.plist) ist - // genau das korrekt — wir lassen die Phase, wie addTarget sie anlegt. - // KEIN dstSubfolderSpec-16-Umbau wie beim NEURLFilter-Target. + // ── Embed-Phase korrigieren — die .appex MUSS allein in dst 13 (PlugIns/) ── + // + // BUG (verifiziert gegen node_modules/xcode/lib/pbxProject.js + einen + // End-to-End-Lauf der echten Plugin-Kette): + // + // 1. `withXcodeProject`-Mods laufen LIFO — `withPacketTunnelTarget` ist + // zuletzt registriert und läuft daher ZUERST, `withExtensionTarget` + // (NEURLFilter) DANACH. + // 2. `proj.addTarget('app_extension')` ruft intern (via `addProductFile`) + // `addToPbxCopyfilesBuildPhase`, BEVOR die neue dst-13-Copy-Files-Phase + // existiert. `buildPhaseObject` findet im neuen Target keine eigene + // Phase und fällt auf die ERSTE PBXCopyFilesBuildPhase zurück, deren + // SECTION-COMMENT exakt „Copy Files" ist — projektweit. + // 3. Ergebnis ohne Fix: die NEURLFilter-`addTarget` (läuft als zweites) + // biegt seine Phase per `phase.name`/`dstSubfolderSpec` auf dst 16 um, + // lässt den SECTION-COMMENT aber auf „Copy Files". Beim PacketTunnel- + // `addTarget` matcht `buildPhaseObject` diesen Comment und pusht die + // PacketTunnel-.appex in dieselbe dst-16-Phase. → .appex landet in + // `ReBreak.app/Extensions/` statt `PlugIns/` → Install-Crash + // `AppexBundleMissingEXAppExtensionAttributesDict` (klassische + // NSExtension im ExtensionKit-Ordner). + // + // Fix (zwei Teile, beide nötig): + // (a) Die PacketTunnel-.appex aus JEDER Copy-Files-Phase herausziehen und + // exklusiv in die eigene, von `addTarget` angelegte dst-13-Phase legen. + // (b) Den SECTION-COMMENT dieser dst-13-Phase von „Copy Files" auf + // „Embed App Extensions" umbenennen. SONST matcht der NACH uns laufende + // `withExtensionTarget`-`addTarget`-Aufruf via `buildPhaseObject( + // 'Copy Files')` unsere PacketTunnel-Phase und der dortige dst-16-Umbau + // zieht die PacketTunnel-.appex erneut mit. Comment-Rename entkoppelt + // beide Phasen sauber (NEURLFilters eigener Umbau matcht per + // .appex-Dateiname, nicht per Comment — bleibt also korrekt). + const ptCopyFilesPhases = proj.hash.project.objects.PBXCopyFilesBuildPhase || {}; + const ptPhaseKeys = Object.keys(ptCopyFilesPhases).filter( + (k) => typeof ptCopyFilesPhases[k] === 'object' && !/_comment$/.test(k), + ); + + // (a) Den PacketTunnel-.appex-BuildFile-Eintrag finden und aus ALLEN + // Copy-Files-Phasen entfernen. + let ptAppexBuildFile = null; + ptPhaseKeys.forEach((key) => { + const phase = ptCopyFilesPhases[key]; + const kept = []; + (phase.files || []).forEach((f) => { + if ( + typeof f?.comment === 'string' && + f.comment.includes(`${PT_TARGET_NAME}.appex`) + ) { + ptAppexBuildFile = f; + } else { + kept.push(f); + } + }); + phase.files = kept; + }); + if (!ptAppexBuildFile) { + throw new Error( + '[with-rebreak-protection-ios] PacketTunnel-.appex-BuildFile nicht gefunden', + ); + } + + // (b) Die von addTarget frisch angelegte (jetzt leere) dst-13-Phase finden, + // die .appex exklusiv dort einhängen UND den Section-Comment umbenennen. + // dst 13 = PlugIns/ = der korrekte Ort für eine klassische NSExtension- + // PluginKit-Extension. + const ptDst13Key = ptPhaseKeys.find( + (k) => + ptCopyFilesPhases[k].dstSubfolderSpec === 13 && + (ptCopyFilesPhases[k].files || []).length === 0, + ); + if (!ptDst13Key) { + throw new Error( + '[with-rebreak-protection-ios] keine leere dst-13-Copy-Files-Phase für die PacketTunnel-.appex gefunden', + ); + } + ptCopyFilesPhases[ptDst13Key].name = '"Embed App Extensions"'; + ptCopyFilesPhases[ptDst13Key].files = [ptAppexBuildFile]; + // Section-Comment-Key umbenennen — entkoppelt die Phase vom + // `buildPhaseObject('Copy Files')`-Lookup des NEURLFilter-Targets. + const ptDst13CommentKey = `${ptDst13Key}_comment`; + if (ptCopyFilesPhases[ptDst13CommentKey] === 'Copy Files') { + ptCopyFilesPhases[ptDst13CommentKey] = 'Embed App Extensions'; + } + // Auch den Build-Phase-Verweis im Target auf den neuen Comment ziehen, + // damit pbxproj-Serialisierung + Xcode-Anzeige konsistent bleiben. + const ptNativeTargets = proj.hash.project.objects.PBXNativeTarget || {}; + Object.keys(ptNativeTargets).forEach((tk) => { + const nt = ptNativeTargets[tk]; + if (typeof nt !== 'object' || !Array.isArray(nt.buildPhases)) return; + nt.buildPhases.forEach((bp) => { + if (bp.value === ptDst13Key && bp.comment === 'Copy Files') { + bp.comment = 'Embed App Extensions'; + } + }); + }); // ── Target-Dependency: Haupt-App baut die Extension vorher ── const mainTargetUuid = proj.getFirstTarget().uuid;