fix(ios): weak-link SwiftUICore on app target to bypass Xcode 26 autolink error

Build #8 confirmed BOTH iOS jobs (signed + unsigned) fail at the same step:
the Streamyfin app-target link (`Ld ... Streamyfin`), not any pod framework.
Under use_frameworks static + Xcode 26 the SwiftUI pods' object files carry a
`-framework SwiftUICore` autolink directive that flows into the app link; ld
rejects it with "cannot link directly with 'SwiftUICore' because product being
built is not an allowed client of it".

forceStaticLinking the SwiftUI pods was treating a symptom. The real fix is to
weakly link SwiftUICore on the app target so the allowed-client check is
bypassed and the symbols resolve via SwiftUI's re-export at runtime.

New plugin withSwiftUICoreWeakLink scopes the flag to product-type application
only, leaving the tvOS TopShelf app-extension untouched (a broad weak-link
previously broke that target).
This commit is contained in:
Gauvain
2026-05-29 17:38:01 +02:00
parent b6ea6d4f14
commit 04e75c81a4
2 changed files with 61 additions and 0 deletions

View File

@@ -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"],

View File

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