From 6e223596f621dd134ccf46565903531fea5694fe Mon Sep 17 00:00:00 2001 From: Gauvain Date: Fri, 29 May 2026 18:17:02 +0200 Subject: [PATCH] fix(ios): drop SwiftUICore autolink on pods so the app links via SwiftUI re-export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build #9 proved `-weak_framework SwiftUICore` does NOT bypass the allowed-client check, and applying it to the tvOS app target regressed tvOS — reverted that plugin (withSwiftUICoreWeakLink). Confirmed root cause from build #8/#9 logs: both iOS jobs fail at the app *executable* link (`Ld … Streamyfin`), not at any pod. SwiftUI was split into SwiftUI + SwiftUICore on iOS 26; the SwiftUI pods emit a `-framework SwiftUICore` autolink directive that, under use_frameworks :static, is inherited by the app's static link, and the app isn't an allowed client of the private SwiftUICore.tbd. Fix: in the pod post_install, compile pods with `-Xfrontend -disable-autolink-framework -Xfrontend SwiftUICore` so they stop emitting that direct autolink. SwiftUICore symbols then resolve through SwiftUI's re-export (SwiftUI.tbd re-exports SwiftUICore). Scoped to phone (ENV['EXPO_TV'] != '1') to leave the green tvOS build untouched. Also harden scripts/ios/build-ios.ts: displayBuildError now surfaces the "Undefined symbols for architecture …" linker block, which the error:-only pattern filter was swallowing (so unsigned-build failures show the real symbol). --- app.json | 1 - plugins/with-runtime-framework-headers.js | 11 +++++ plugins/withSwiftUICoreWeakLink.js | 60 ----------------------- scripts/ios/build-ios.ts | 19 +++++-- 4 files changed, 27 insertions(+), 64 deletions(-) delete mode 100644 plugins/withSwiftUICoreWeakLink.js diff --git a/app.json b/app.json index 0edd20a85..ed6ddf194 100644 --- a/app.json +++ b/app.json @@ -133,7 +133,6 @@ ], "expo-web-browser", ["./plugins/with-runtime-framework-headers.js"], - ["./plugins/withSwiftUICoreWeakLink.js"], ["./plugins/withChangeNativeAndroidTextToWhite.js"], ["./plugins/withAndroidAlertColors.js"], ["./plugins/withAndroidManifest.js"], diff --git a/plugins/with-runtime-framework-headers.js b/plugins/with-runtime-framework-headers.js index 97d11b9de..23e7d1011 100644 --- a/plugins/with-runtime-framework-headers.js +++ b/plugins/with-runtime-framework-headers.js @@ -25,6 +25,17 @@ function buildPatch() { " cfg.build_settings['HEADER_SEARCH_PATHS'] ||= '$(inherited)'", " cfg.build_settings['HEADER_SEARCH_PATHS'] << \" #{extra_hdrs.join(' ')}\"", " cfg.build_settings['CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES'] = 'YES'", + " # iOS 26 / Xcode 26: SwiftUI was split into SwiftUI + SwiftUICore. The SwiftUI", + " # pods (ExpoUI, glass-effect, glass-poster, …) emit a `-framework SwiftUICore`", + " # autolink directive that, under use_frameworks :static, flows into the app", + " # executable's link. The app isn't an allowed client of the private", + " # SwiftUICore.tbd → `cannot link directly with 'SwiftUICore'`. Dropping that one", + " # autolink at the Swift frontend lets the symbols resolve via SwiftUI's", + " # re-export instead. Phone-only — tvOS links fine and must stay untouched.", + " if ENV['EXPO_TV'] != '1'", + " cfg.build_settings['OTHER_SWIFT_FLAGS'] ||= '$(inherited)'", + " cfg.build_settings['OTHER_SWIFT_FLAGS'] << ' -Xfrontend -disable-autolink-framework -Xfrontend SwiftUICore'", + " end", " end", " end", "", diff --git a/plugins/withSwiftUICoreWeakLink.js b/plugins/withSwiftUICoreWeakLink.js deleted file mode 100644 index 8134f3094..000000000 --- a/plugins/withSwiftUICoreWeakLink.js +++ /dev/null @@ -1,60 +0,0 @@ -const { withXcodeProject } = require("@expo/config-plugins"); - -// Tokens written verbatim as OTHER_LDFLAGS array entries. -const LDFLAG_TOKENS = ['"-weak_framework"', '"SwiftUICore"']; - -/** - * Xcode 26 + `use_frameworks! :linkage => :static` makes the main app target - * auto-link SwiftUICore directly (SwiftUI was split into SwiftUI + SwiftUICore on - * recent SDKs, and the SwiftUI pods' object files carry a `-framework SwiftUICore` - * autolink directive that flows into the app link). The linker then rejects it: - * ld: cannot link directly with 'SwiftUICore' because product being built is - * not an allowed client of it - * Weakly linking SwiftUICore on the app target bypasses the allowed-client check; - * the symbols still resolve at runtime via SwiftUI's re-export. - * - * Scoped to `com.apple.product-type.application` ONLY — it must not touch the - * tvOS TopShelf app-extension (which legitimately links SwiftUI); applying the - * flag there breaks that target. - */ -const withSwiftUICoreWeakLink = (config) => - withXcodeProject(config, (config) => { - const project = config.modResults; - const nativeTargets = project.pbxNativeTargetSection(); - const configLists = project.pbxXCConfigurationList(); - const buildConfigs = project.pbxXCBuildConfigurationSection(); - - // Collect build-configuration UUIDs that belong to application targets only. - const appConfigIds = new Set(); - for (const key of Object.keys(nativeTargets)) { - const target = nativeTargets[key]; - if (!target || typeof target !== "object" || !target.productType) - continue; - const productType = String(target.productType).replace(/"/g, ""); - if (productType !== "com.apple.product-type.application") continue; - const list = configLists[target.buildConfigurationList]; - if (!list || !list.buildConfigurations) continue; - for (const bc of list.buildConfigurations) appConfigIds.add(bc.value); - } - - for (const id of appConfigIds) { - const entry = buildConfigs[id]; - if (!entry || typeof entry !== "object" || !entry.buildSettings) continue; - const settings = entry.buildSettings; - let flags = settings.OTHER_LDFLAGS; - if (flags == null || flags === '""' || flags === "") { - flags = ['"$(inherited)"']; - } else if (typeof flags === "string") { - flags = [flags]; - } - const already = flags.some((f) => String(f).includes("SwiftUICore")); - if (!already) { - flags.push(...LDFLAG_TOKENS); - settings.OTHER_LDFLAGS = flags; - } - } - - return config; - }); - -module.exports = withSwiftUICoreWeakLink; diff --git a/scripts/ios/build-ios.ts b/scripts/ios/build-ios.ts index 1d7bf77c7..33110507b 100644 --- a/scripts/ios/build-ios.ts +++ b/scripts/ios/build-ios.ts @@ -525,10 +525,23 @@ function displayBuildError( console.error(line); } console.error("--- End Build Errors ---\n"); - } else if (stdout.trim()) { + } + + // Linker failures ("Undefined symbols for architecture …", the SwiftUICore + // autolink rejection, "ld: …") don't carry an "error:" token, so the pattern + // filter above drops the symbol name and "referenced from" context that + // actually pinpoints the culprit. Surface that block explicitly. + const stdoutLines = stdout.split("\n"); + const undefIdx = stdoutLines.findIndex((line: string) => + line.includes("Undefined symbols"), + ); + if (undefIdx >= 0) { + console.error("\n--- Linker error detail ---"); + console.error(stdoutLines.slice(undefIdx, undefIdx + 40).join("\n")); + console.error("--- End linker error detail ---\n"); + } else if (errorLines.length === 0 && stdout.trim()) { // No specific error patterns found, show last N lines of stdout - const lines = stdout.split("\n"); - const lastLines = lines.slice(-ERROR_OUTPUT_TAIL_LINES).join("\n"); + const lastLines = stdoutLines.slice(-ERROR_OUTPUT_TAIL_LINES).join("\n"); console.error( `\n--- Last ${ERROR_OUTPUT_TAIL_LINES} lines of build output ---`, );