Undefined Symbol in Test Target When Using SPM Umbrella Libraries
I'm migrating a large project from CocoaPods to Swift Package Manager. The project has a main app target and a test target in Xcode. My approach was to create two "umbrella" SPM products — AppModules (linked to the app target) and AppTestModules (linked to the test target) — that re-export all dependencies transitively.
Here's a minimal reproduction of the package structure:
// swift-tools-version: 5.10
import PackageDescription
let dynamicProducts: Set<String> = ["AppModules", "AppTestModules"]
let targets: [Target] = [
// Feature module with production code
.target(name: "FeatureA"),
// Umbrella linked to the main app target under Frameworks & Libraries
.target(name: "AppModules", dependencies: ["FeatureA"]),
// Test support module: mocks, helpers, fixtures for FeatureA
.target(name: "FeatureATestSupport", dependencies: ["FeatureA"]),
// Umbrella linked to the test target under Frameworks & Libraries
.target(name: "AppTestModules", dependencies: ["FeatureATestSupport"]),
]
let products: [Product] = targets.map { target in
if dynamicProducts.contains(target.name) {
return .library(name: target.name, type: .dynamic, targets: [target.name])
} else {
return .library(name: target.name, targets: [target.name])
}
}
let package = Package(
name: "MyModules",
platforms: [.iOS(.v16)],
products: products,
targets: targets
)
Xcode target linkage:
MyApp→ linksAppModules.frameworkMyAppTests→ linksAppTestModules.framework
Problem 1 — Undefined symbol at link time:
The app target builds and runs fine. However, building the test target produces:
Undefined symbols for architecture arm64:
"_$s8FeatureA9SomeClassCN", referenced from:
_$s20FeatureATestSupport14SomeTestHelperCN in AppTestModules.framework
What's I think is happening, can someone confirm?:
FeatureA is imported in a test file. FeatureA is a static library (no type: specified). When SPM builds AppModules.framework (dynamic), it merges FeatureA's object code directly into it — so FeatureA's symbols live inside AppModules.framework's binary.
The test target only links AppTestModules.framework which depends on FeatureATestSupport.Our initial assumption was that since the test target already had @testable import MyApp, it would have access to all symbols that MyApp links — including everything merged into AppModules.framework. The thinking was:
MyApplinksAppModules.framework, which containsFeatureA. If the test target importsMyApp, it should be able to seeFeatureA's symbols transitively.
However the linker error persists regardless of @testable import.
What fixes it:
Adding FeatureA as an explicit dependency of AppTestModules resolves the linker error. The test target can now link FeatureA directly. The unit tests run, however now I get a large list of these errors and simulator eventually crashes.
objc[XXXX]: Class GADOMIDWindowProcessor is implemented in both
/path/to/DerivedData/MyApp/Build/Products/Debug-iphonesimulator/PackageFrameworks/AppModules.framework/AppModules (0x...)
and
/path/to/CoreSimulator/Devices/.../MyApp.app/PlugIns/MyAppTests.xctest/MyAppTests (0x...).
This may cause spurious casting failures and mysterious crashes.
One of the duplicates must be removed or renamed.
failed to demangle superclass of SomeViewSpec from mangled name '\377"\247':
subject type x does not conform to protocol SomeProtocol
CoreSimulator XXXX.XX.X - Device: iPhone XX (DEVICE-UUID) - Runtime: iOS XX.X (XXXXX) - DeviceType: iPhone XX
It looks like I have two sets of symbols now and at runtime it doesn't know how to resolve it and causes a crash.
My question:
Is there a cleaner structural approach for this pattern? The two problems together mean:
- Any static library reachable from the app but not from the test umbrella causes a linker error
- Any static library shared between two or more dynamic frameworks causes a runtime type identity crash
Will making everything dynamic solve the problem?. Is there an idiomatic way to structure umbrella products so the test target can see all symbols?