Weird Crash Involving Swift Packages and Generics

Hello,

We have recently came across a very strange issue related to Swift Packages and Xcode, which also involves Generics

I was able to recreate a minimum reproducible example of the issue we are encountering which I will use the describe the issue we have

I created a 'New Project' using Xcode 14.3 and have configured it with unit tests.
In the new project I have added a local Swift Package 'MyLibrary' (default name)
as you can see below

Package.swift
let package = Package(
    name: "MyLibrary",
    products: [
        .library(
            name: "MyLibrary",
            targets: ["MyLibrary"]
        ),
        .library(
            name: "MyLibraryMocks",
            targets: ["MyLibraryMocks"]
        ),
    ],
    dependencies: [],
    targets: [
        .target(
            name: "MyLibrary",
            dependencies: []
        ),
        .target(
            name: "MyLibraryMocks",
            dependencies: [
                "MyLibrary"
            ]
        )
    ]
)

The package is made up of a "main" target called 'MyLibrary', and a mocks target called 'MyLibraryMocks' which has a dependency on the MyLibrary target. MyLibraryMocks is intended to be used only for unit testing, so it will not be included in the main application target, but rather in the unit tests target of the Xcode project.

MyLibrary.swift contains a simple 1:1 relationship between a 'Component' protocol and its 'Model' counterpart

MyLibrary.swift
public protocol Component<M> where M.C == Self {
    associatedtype M: Model
}

public protocol Model<C>: Hashable where C.M == Self {
    associatedtype C: Component
}

public final class A: Component {
    public typealias M = B

    public init() { }
}

public struct B: Model {
    public typealias C = A

    public init() { }
}

MyLibraryMocks.swift contains mock implementations for both the Component and the Model protocols

MyLibraryMocks.swift
import MyLibrary

public struct Mock: Component {
    public typealias M = MockModel

    public init() { }
}

public struct MockModel: Model {
    public typealias C = Mock

    public init() { }
}

I have added MyLibrary as a dependency in the main application target and have created and ComponentAdapter class which follows the Adapter design pattern.

ComponentAdapter.swift
final class Adapter {
    func adapt() -> some Model {
        B()
    }
}

As you can see, Adapter.adapt method returns an opaque implementation of the Model protocol, because we are really not interested in the actual type that is returned, we only care that it conforms to the Model protocol. This gives us the flexibility to change the object returned by the method, in the future, without breaking the code of the consumer, as the actual type is only an implementation detail.

In the project's unit tests target, we have added MyLibraryMocks as a dependency, since it is not included in the application's target.

MyAppTestsTarget

Strangely enough, when testing the Adapter class, we are not able to cast the opaque some Model returned by the adapt method to its actual implementation, even though we know for sure what the actual type is

// AdapterTests.swift

import XCTest
@testable import MyLibrary
@testable import MyLibraryMocks
@testable import MyApp

final class AdapterTests: XCTestCase {
    func testExample() throws {
        let expectedModel = B()
        let adapter = Adapter()
        let model = adapter.adapt() as! B  // Thread 1: signal SIGABRT

        XCTAssertEqual(expectedModel, model)
    }
}

Running the above test actually crashes at runtime and we are left completely clueless on why that is so.

Xcode console shows the following output

objc[33111]: Class _TtC9MyLibrary1A is implemented in both /Users/victorsocaciu/Library/Developer/XCTestDevices/D7379CE3-8129-4E21-81DC-D7F4B586372F/data/Containers/Bundle/Application/35261BB7-60EF-4706-B3D7-3178E6E40D1E/MyApp.app/MyApp (0x1005555b0) and /Users/victorsocaciu/Library/Developer/XCTestDevices/D7379CE3-8129-4E21-81DC-D7F4B586372F/data/Containers/Bundle/Application/35261BB7-60EF-4706-B3D7-3178E6E40D1E/MyApp.app/PlugIns/MyAppTests.xctest/MyAppTests (0x1018cc2d0). One of the two will be used. Which one is undefined.
Test Suite 'AdapterTests' started at 2023-04-21 14:04:45.614
Test Case '-[MyAppTests.AdapterTests testExample]' started.
Could not cast value of type 'MyLibrary.B' (0x100550338) to 'MyLibrary.B' (0x1018c8280).
2023-04-21 14:04:45.614851+0400 MyApp[33111:22124448] Could not cast value of type 'MyLibrary.B' (0x100550338) to 'MyLibrary.B' (0x1018c8280).

While still connected to the debugger, when running the following command in the console po model as! B, the debugger doesn't crash and it actually prints the correct output MyLibrary.B()

the following test also fails

func testExample2() throws {
        let expectedModel = B()
        let adapter = Adapter()
        let model = adapter.adapt()
        let modelType = type(of: model)
        print(modelType) // B
        XCTAssertEqual(
            ObjectIdentifier(B.self),
            ObjectIdentifier(modelType)
        ) // testExample2(): XCTAssertEqual failed: ("ObjectIdentifier(0x00000001037982c0)") is not equal to ("ObjectIdentifier(0x00000001025b8338)")
    }

Has anyone else faced this before? Are we misusing the Swift Packages somehow?

EDIT:
I have uploaded the sample project to github GitHub - vykut/SwiftPackageDuplicateSymbols

1 Like

We are literally unable to test our logic properly due to this issue which is now plaguing all our unit tests. @NeoNacho @allevato @Max_Desiatov would any of you be so kind to assist us?
Thank you

Speculation: is there some kind of infinite regress? In order to find out if some Model can be cast as a B it has to check that its C can be cast as an A, but to check if its C can be cast as an A, it needs to find out if the C's M can be cast as a B and so on ad infinitum.

Once the MyLibraryMocks dependency is removed from the unit tests target, the tests pass successfully and there's no problem anymore, so it's not really an 'infinite' cast problem.

objc[33111]: Class _TtC9MyLibrary1A is implemented in both /Users/victorsocaciu/Library/Developer/XCTestDevices/D7379CE3-8129-4E21-81DC-D7F4B586372F/data/Containers/Bundle/Application/35261BB7-60EF-4706-B3D7-3178E6E40D1E/MyApp.app/MyApp (0x1005555b0) and /Users/victorsocaciu/Library/Developer/XCTestDevices/D7379CE3-8129-4E21-81DC-D7F4B586372F/data/Containers/Bundle/Application/35261BB7-60EF-4706-B3D7-3178E6E40D1E/MyApp.app/PlugIns/MyAppTests.xctest/MyAppTests (0x1018cc2d0). One of the two will be used. Which one is undefined.

I'd assume this lies at the root of the issue here, multiple copies of MyLibrary.A exist at runtime and I would assume the same is true for MyLibrary.B, likely because MyLibrary has been statically linked into both your app and your test bundle. Xcode's build system tries to automatically resolve these types of issues by building package products/targets dynamically, but that hasn't happen here.

Manually ensuring that dynamic linking happens is unfortunately a little bit laborious since while you can mark products as dynamic explicitly (see here), you can't reference products from your own package. So you'll have to split up the MyLibrary package and make MyLibrary a dynamic product. MyLibraryMocks would be in its own package, linking MyLibrary dynamically.

It would also be appreciated if you could file a feedback report for this so that the Xcode build system team can investigate why the automatism for this kind of situation did not work in your case.

Thank you very much for your help @NeoNacho!

So our initial package structure was as follows:

MyLibraryA
-> MyLibraryA
-> MyLibraryAMocks
-> MyLibraryATests

MyLibraryB
-> MyLibraryB
-> MyLibraryBMocks
-> MyLibraryBTests

MyLibraryC
-> MyLibraryC
-> MyLibraryCMocks
-> MyLibraryCTests

We ended up making all the products dynamic and splitting all the mocks targets and test targets into their own packages, as follows

MyLibraryA
-> MyLibraryA (dynamic)

MyLibraryB
-> MyLibraryB (dynamic)

MyLibraryC
-> MyLibraryC (dynamic)

Mocks
-> MyLibraryAMocks (depends on MyLibraryA)
-> MyLibraryBMocks (depends on MyLibraryB)
-> MyLibraryCMocks (depends on MyLibraryC)

Tests
-> MyLibraryATests (depends on MyLibraryA and MyLibraryAMocks)
-> MyLibraryBTests (depends on MyLibraryB and MyLibraryBMocks)
-> MyLibraryCTests (depends on MyLibraryC and MyLibraryCMocks)

In the main application we import MyLibraryA, MyLibraryB, MyLibraryC as before, but in our unit tests target we now only import the Mocks package.

We never import the Tests package, we only reference the tests in our test plan

Our initial problem is now solved and there are no duplicate symbols anymore, BUT we have encountered another problem, which may or may not be related to how we ended up structuring our packages. I will explain it below

MyLibraryC contains an .xcassets resource for assets that are used in the application. We initialize these assets in an UIImage using Bundle.module. On the app side, everything works as expected, so no problems there, but when we run the MyLibraryC tests - which reference some of these UIImages - it crashes with the following error
2023-04-28 12:31:55.311419+0400 xctest[52348:895342] MyLibraryC/resource_bundle_accessor.swift:40: Fatal error: unable to find bundle named MyLibraryC_MyLibraryC

Package definitions
// MyLibraryC/Package.swift 

.target(
    name: "MyLibraryC",
    dependencies: [
        .product(name: "MyLibraryA", package: "MyLibraryA")
    ],
    resources: [
        .process("Icons/Icons.xcassets"),
    ]
),

// Tests/Package.swift

.testTarget(
  name: "MyLibraryCTests",
  dependencies: [
    .product(name: "MyLibraryC", package: "MyLibraryC"),
    .product(name: "MyLibraryCMocks", package: "MyLibraryCMocks"),
  ]
),

// Mocks/Package.swift

.target(
    name: "MyLibraryCMocks",
    dependencies: [
        .product(name: "MyLibraryC", package: "MyLibraryC"),
    ]
),

We have removed MyLibraryCTests from the test plan while we investigate.

We greatly appreciate your support @NeoNacho :pray:

Unfortunately I am not too firm with how resources work in packages when building with Xcode these days. Best suggestion I have is also filing a feedback report for this.

For anyone else who might stumble upon this thread, after you mark your packages as dynamic, you will need to select 'Embed Without Signing" when importing them into your app, otherwise TestFlight/ AppStore builds will crash at launch. Initially these were marked as "Do not embed".

Screenshot 2023-05-01 at 11.52.36

1 Like

@vykut I think I'm experiencing the same issues over on: Duplicate symbols across app and test targets — runtime cast from `MyView` to `MyView` fails

Fixing a duplicate symbol issue across app and test targets by using SPM's dynamic product configuration leads to resource/bundle location errors.

Did you manage to address the bundle path issue in a way that works during dev and distribution?

Hi @mwaterfall,

I have resorted to using this workaround Providing XCAssets Folder in Swift… | Apple Developer Forums

Hope it will work in your case as well