Compiling a dynamic framework with a statically linked library creates dependencies in ".swiftmodule" file

Steps to reproduce:

  1. Create a new project in Xcode with template Cocoa Touch Framework, and name it TestFramework.
  2. Add another target in this project with template Cocoa Touch Static Library, and name it TestLibrary.
  3. In the auto-generated file TestLibrary.swift, add a public interface like:
    public struct TestLibrary {
        public init() { }
        public let name = "static library"
    }
  1. In target TestFramework, link libTestLibrary.a.
  2. In target TestFramework, create a new file with the name TestFramework.swift.
  3. In TestFramework.swift, import TestLibrary and use the API it provides without exposing any symbol in public, like
    import TestLibrary
    public struct TestFramework {
        public init() { }
        public let library = TestLibrary().name
    }
  1. Build target TestFramework, and locate the product TestFramework.framework.
  2. Create a new project in Xcode with template Single View Application, and name it TestApplication.
  3. Drag the TestFramework.framework from step 7 into the Embedded Binaries build phase of TestApplication.
  4. In the file AppDelegate.swift of TestApplication, import TestFramework like
    import UIKit
    import TestFramework
  1. Run the app.
  2. At the line where TestFramework is imported, an error is prompted:

:exclamation: Missing required module 'TestLibrary'

  1. If you command-click on that import TestFramework, Xcode will generate the public interfaces of TestFramework, and it shows as:
    import Foundation
    import SwiftOnoneSupport
    import TestFramework
    import TestFramework.Swift
    import TestLibrary
    import UIKit
    ...

So this is where I got confused.

TestLibrary is statically linked with TestFramework, which means the binary of TestFramework has all the symbols from TestLibrary. Plus, TestFramework is not exposing any APIs from TestLibrary, but only using them. This means TestFramework is fully functional on its own regardless of TestLibrary being presented or not.

So why the .swiftmodule file of TestFramework creates an additional (probably unnecessary) dependency on TestLibrary?

As a result, when a user uses TestFramework to build an app, the app won't be able to compile because it can not find the TestLibrary, but it can run perfectly because it actually does not use TestLibrary directly if we can somehow bypass that dependency check.

So I am wondering if there is a way to remove a dependency from .swiftmodule file?
Or is there any compile/link flag I can pass in to prevent it generates any unnecessary dependency?

Just found another similar description of this problem while I was searching through the internet for a solution:
https://gist.github.com/briancroom/5d0f1b966fa9ef0ae4950e97f9d76f77

2 Likes

Ran into this as well, this was the closest thread to describing the problem.

We've got a static Objective-C library (InternalLib) with an umbrella header that we want to force-load into a dynamic Swift framework (SharedFramework). InternalLib we explicitly don't want to be importable or accessible.

It seems like the symbols are fine and in the binary, but also run into the same compile error where either in the Swift generated header it has an @import InternalLib that's exposed, which won't succeed, I'm assuming it's the same root cause here.

Guess this brings me to the broader question of how do I properly force-load that InternalLib into the dynamic SharedFramework such that an import SharedFramework is all that is needed.

There's not really a supported way to do this at this time. The work that would be necessary is described in @_exported and fixing import visibility and Update on implementation-only imports.

2 Likes

Yeah the implementation imports work pretty well but it seems like they still show up in the Swift generated header; not sure if that's a bug or not, haven't looked into how they work enough yet

AFAIK you need to copy module map of static library to the root of the dynamic framework and then path to that folder needs to be exposed in Import Path under swift build settings for Host App.

Hi all, I've been working hard on this issue as well. Seems prevalent when trying to set up some variation of static libraries packaged inside frameworks. Oddly, not at all covered by the creators of this software (shame). I found the same as Ruchish states above. In my case, if I manually moved the .swiftmodule files that are the equivalent of "module map" for swift headers, into the Headers directory of the containing framework, then import the path in Import Path under Swift build settings, things are great. You can do this by importing the module.framework recursively. Such a shame, obviously I don't want those headers to be exposed, but rather contained in the framework (they are my own libraries that I want to keep private) but this system is forcing me to publicize them.

1 Like

@ylorn I know this is an old thread, but I have tried achieving this and it appears to be possible with Swift Packages.

The only difference to your setup is that rather than the TestLibrary being a Cocoa Touch Static library, it's a Swift Package that produces a static library. I'm not sure what's different between the two that allows this. TestLibrary still appears in the Import of TestFramework, but the App Target does not need to explicitly depend on TestLibrary. As @jrose says, there's additional work that needs to be implemented with @_implementationOnly or something like an internal import to prevent any leakage of TestLibrary, but this seems like an OK solution until then.

1 Like