Shared Dependency between Framework and App leads to Run-Time Crash

Overview

In short, I'm observing a runtime crash, in a Swift Package with debug compiler directives (#if DEBUG) that is shared between an XCFramework and an App.

The framework only links to the Swift Package and the App embeds both. The issue occurs when the framework is compiled in Release, but the App in Debug configuration.

This issue has forced me to distribute both Release and Debug artefacts, which is really cumbersome for my users, especially when integrating my XCFramework with Swift Package Manager.

Reproduction

I have created a sample project on GitHub that provides a setup to reproduce this issue. You can clone it, open MyApp.xcodeproj and just run the MyApp Target.

The project MyApp consists of:

  • an App Target called MyApp
  • a static library called MyLibrary
  • a local Swift Package called MyPackage
  • an aggregated target MyFramework
    • Creates an XCFramework of MyLibrary
  • a 2nd local Swift Package MyFramework
    • Serves "MyLibrary.xcframework" to MyApp as a Package

The Dependency Graph looks like this:

MyApp
└── embeds MyFramework (MyLibrary.xcframework)
    └── links to MyPackage
└── embeds MyPackage

If we compile MyFramework in release configuration and then build MyApp using the XCFramework, the following crash be witnessed:

Thread 0 Crashed::  Dispatch queue: com.apple.main-thread
0   libswiftCore.dylib            	       0x192cd786c swift::RefCounts<swift::RefCountBitsT<(swift::RefCountInlinedness)1>>::incrementSlow(swift::RefCountBitsT<(swift::RefCountInlinedness)1>, unsigned int) + 52
1   MyApp                         	       0x1022ce050 HelloPackage.hello() + 164 (MyPackage.swift:14)
2   MyApp                         	       0x1022cd1d0 closure #1 in ContentView.body.getter + 44 (ContentView.swift:14)
3   SwiftUI                       	       0x1c49e38b8 0x1c4371000 + 6760632
4   MyApp                         	       0x1022ccf14 ContentView.body.getter + 92 (ContentView.swift:13)
5   MyApp                         	       0x1022cd6f4 protocol witness for View.body.getter in conformance ContentView + 12

The Source File in question inside MyPackage is the following:

public struct HelloPackage {
    private let message: String = "MyPackage"
#if DEBUG
    private let debug: String = "Debug"
#endif

    public init() {}

    public func hello() -> String {
#if DEBUG
        "\(debug) \(message)" # << CRASH OCCURS HERE
#else
        "Release \(message)"
#endif
    }
}

Note here, that all #if DEBUG directives are covering private properties or are inside method implementations. Nevertheless, running this in the App in the Debug configuration creates a crash, as if let debug was not initialised or doesn't exist. But shouldn't it have been compiled again, as it's not part of the XCFramework?

Conclusion

  • Should Compiler Directives like #if DEBUG not be distributed in public Source Files of a Swift Package and are known to cause crashes when linked to binary frameworks?
  • What kind of references to a Swift Package dependency are compiled into a static library by Xcode, when the Package is only added via Target Dependencies, but not to Frameworks, Libraries and Embedded Content?
  • Or is there some step missing in my XCFramework creation? Do I need to remove something from the binary?
# Build Simulator Framework
xcodebuild archive -project MyApp.xcodeproj \
 -scheme MyLibrary \
 -configuration ${CONFIGURATION} \
 -destination "generic/platform=iOS Simulator" \
 -archivePath ${SRCROOT}/build/${CONFIGURATION}-iphonesimulator \
 SKIP_INSTALL=NO

# Create XCFramework 
xcodebuild -create-xcframework \
 -library ${SRCROOT}/build/${CONFIGURATION}-iphonesimulator.xcarchive/Products/usr/local/lib/libMyLibrary.a \
 -output ${SRCROOT}/build/MyLibrary.xcframework
 
# Copy Modules
cp -R ${OBJROOT}/ArchiveIntermediates/MyLibrary/BuildProductsPath/${CONFIGURATION}-iphonesimulator/MyLibrary.swiftmodule ${SRCROOT}/build/MyLibrary.xcframework/ios-arm64_x86_64-simulator