fix(ios-vpn): Packet-Tunnel-.appex in eigene dst-13-Embed-Phase zwingen

Bug: die RebreakPacketTunnelExtension.appex wurde nach
ReBreak.app/Extensions/ statt PlugIns/ embedded → Install-Crash
AppexBundleMissingEXAppExtensionAttributesDict (klassische NSExtension
im ExtensionKit-Ordner).

Ursache (verifiziert gegen node_modules/xcode + E2E-Lauf der Plugin-Kette):
withXcodeProject-Mods laufen LIFO — withPacketTunnelTarget zuerst,
withExtensionTarget (NEURLFilter) danach. proj.addTarget() bettet die
.appex über buildPhaseObject() ein, das projektweit die erste
PBXCopyFilesBuildPhase mit Section-Comment "Copy Files" matcht. Der
NEURLFilter-Umbau ändert nur phase.name/dstSubfolderSpec, nicht den
Section-Comment → die PacketTunnel-.appex landet in der dst-16-Phase
des NEURLFilter-Targets.

Fix in withPacketTunnelTarget: die .appex aus allen Copy-Files-Phasen
herausziehen, exklusiv in die eigene dst-13-Phase legen UND deren
Section-Comment auf "Embed App Extensions" umbenennen — damit der
nachfolgende NEURLFilter-addTarget-Lookup die Phase nicht mehr matcht.
NEURLFilter-Code unangetastet (dessen Umbau matcht per .appex-Dateiname).

Verifiziert: frisches Projekt, vorhandenes URLFilter-Target, Idempotenz.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-05-21 23:02:01 +02:00
parent 0d28682749
commit adeaf4eb75

View File

@ -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;