I'm seeing Xcode link two copies of a Swift package module into one test process. The build succeeds without a
warning or error, but at run time the app and its hosted unit tests each use their own copy of the module's
globals and task-locals.
Environment: Xcode 27.0 (27A266a), Swift 6.4, macOS 26.6.2, iPhone 17 Simulator (iOS 27.0, arm64).
Sample project: <LINK TO HOSTED SAMPLE>
Logging package
Logging library target
LoggingTesting → depends on the Logging *target* (same package)
Content package
Content → depends on the Logging *product*
ContentTesting → depends on the Content target
Repro (app) links products Logging, Content
ReproTests (hosted unit test) links products LoggingTesting, ContentTesting
TEST_HOST / BUNDLE_LOADER = Repro.app
LoggingTesting depends on the Logging target rather than the product so it can use package-access API: a
@TaskLocal package static var label on LoggingSystem. Libraries that ship a testing companion commonly use
this layout.
xcodebuild test -project Repro.xcodeproj -scheme Repro \
-destination 'platform=iOS Simulator,name=iPhone 17,OS=27.0' -derivedDataPath /tmp/repro-dd
cd /tmp/repro-dd/Build/Products/Debug-iphonesimulator
ls Repro.app/Frameworks
nm Repro.app/PlugIns/ReproTests.xctest/ReproTests | xcrun swift-demangle | grep -v ' U ' | grep LoggingSystem
otool -L Repro.app/PlugIns/ReproTests.xctest/ReproTests
The test process contains one copy of Logging, so all three tests pass: Logger.shared is the same instance
in the test bundle and the app, and a task-local bound through LoggingTesting is visible to app code and to
Content.
All three tests fail:
✘ Test loggerShared_whenReadFromTestAndApp_isTheSameInstance() recorded an issue at ReproTests.swift:11:9: Expectation failed: ObjectIdentifier(Logger.shared) == appLoggerIdentity()
✘ Test taskLocal_whenBoundByLoggingTesting_isVisibleToApp() recorded an issue at ReproTests.swift:18:13: Expectation failed: appLoggingLabel() == "test"
✘ Test taskLocal_whenBoundByLoggingTesting_isVisibleToContent() recorded an issue at ReproTests.swift:26:13: Expectation failed: contentLoggingLabel() == "test"
The built products explain it:
-
Repro.app/FrameworkscontainsContent.frameworkandLogging_35C93DA702091ECE_PackageProduct.framework.
The app and the test bundle both linkContent, so Xcode builds it as a dynamic framework. That framework
depends on theLoggingproduct, so Xcode wraps the product in its own framework. -
ReproTestslinks the wrapper framework (otool -Llists
@rpath/Logging_35C93DA702091ECE_PackageProduct.framework/…) and defines its own copy of the module:T static Logging.LoggingSystem.label.getter : Swift.String T Logging.LoggingSystem.$label.unsafeMutableAddressor : Swift.TaskLocal<Swift.String> S static Logging.LoggingSystem.$label : Swift.TaskLocal<Swift.String> T Logging.Logger.shared.unsafeMutableAddressor : Logging.Logger S static Logging.Logger.shared : Logging.Logger D type metadata for Logging.LoggerThis static copy comes from
LoggingTesting's dependency on theLoggingtarget. -
LoggingTestingbinds$labelin the test bundle's copy, while the app andContentread the wrapper
framework's copy and still see"default".
Removing ContentTesting from ReproTests (with its import and the third test) makes the problem go away. No
second image links Content, so Xcode keeps it static and builds the Logging target as a single shared
Logging.framework. The test bundle then only references the symbols (U Logging.LoggingSystem.$label…), and
the remaining two tests pass.
- Declaring the package products
type: .static. Xcode still builds the target as a dynamic framework, and the
duplicate remains. - Moving the testing library into its own package that depends on the
Loggingproduct. This removes the
duplicate, butpackageaccess can't cross package boundaries, so the task-local's binding API has to become
publiconLogging. - Removing
Content's dependency onLogging. This also removes the duplicate, butContentneeds to log.
Our production app hit this when it adopted a new version of a package whose main library depends on our
logging package's product and that ships its own testing companion. After the update, every unit test that
asserted on logging through the logging package's testing library silently recorded nothing. Tests that inject
mock loggers directly kept passing, so the update looked healthy. We found the cause only by inspecting symbols
with nm.
- Is it expected that Xcode links a second, static copy of a package module into a hosted test bundle that
also links a framework Xcode generated for the same module? If not, I'll file a Feedback. If it is expected,
why is there no diagnostic? - What's the supported way to structure a library and its testing companion when the companion needs
packageaccess to the library, and an app and its hosted unit tests consume the library through another
package? - Is there a package declaration or build setting that makes Xcode use a single copy of
Loggingin this
graph?