diff --git a/app.json b/app.json index ed6ddf194..0edd20a85 100644 --- a/app.json +++ b/app.json @@ -133,6 +133,7 @@ ], "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/withSwiftUICoreWeakLink.js b/plugins/withSwiftUICoreWeakLink.js new file mode 100644 index 000000000..8134f3094 --- /dev/null +++ b/plugins/withSwiftUICoreWeakLink.js @@ -0,0 +1,60 @@ +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;