Binary Frameworks with SwiftPM

I have been searching an answer to this, and I assume there is a way to do this but haven't succeed in finding it. I have a Package.swift which looks something like this, simplified for illustration purposes.

  let package = Package(
      name: "Core",
      platforms: [
          .macOS(.v10_14), .iOS(.v12)
      ],
      products: [
          .library(
              name: "Core",
              targets: ["Core"]
          )
      ],
      dependencies: [
           // some package dependencies here. 
     ],
     targets: [
         .target(
             name: "Core",
             dependencies: [
                 "BinaryFramework",
                 "BinaryFramework2",
             ]
         ),
         .testTarget(
             name: "CoreTests",
             dependencies: ["Core"]
         )
     ]
 )

The folder structure is like this on disk.

.
β”œβ”€β”€ Frameworks
β”‚   └── Build
β”‚       └── iOS
β”‚           β”œβ”€β”€ BinaryFramework.framework
β”‚           β”‚   β”œβ”€β”€ BinaryFramework
β”‚           β”‚   β”œβ”€β”€ Info.plist
β”‚           β”‚   └── Modules
β”‚           β”‚       └── module.modulemap
β”‚           └── BinaryFramework2.framework
β”‚               β”œβ”€β”€ BinaryFramework2
β”‚               β”œβ”€β”€ Headers
β”‚               β”‚   └── FrameworkHeader.h
β”‚               β”œβ”€β”€ Info.plist
β”‚               └── Modules
β”‚                    └── module.modulemap
β”œβ”€β”€ Package.resolved
β”œβ”€β”€ Package.swift
β”œβ”€β”€ README.md
β”œβ”€β”€ Core.xcodeproj
β”‚   β”œβ”€β”€ project.pbxproj
β”œβ”€β”€ Sources
β”‚   └── RideCore
β”‚       β”œβ”€β”€ Private
β”‚       β”‚   β”œβ”€β”€ Connector.swift
β”‚       β”‚   β”œβ”€β”€ Decodable.swift
β”‚       β”‚   └── JSONRequest.swift
β”‚       └── Public
β”‚           └── Common
β”‚                β”œβ”€β”€ AuthenticationService.swift
β”‚                └── Exceptions.swift
β”œβ”€β”€ Tests
β”‚   └── CoreTests
β”‚       β”œβ”€β”€ AuthenticateTests.swift
β”‚       └── BasicNetworkTests.swift
└── ToDo.txt

The frameworks are built already and I don't have the source so I can't import it as a package. These are not C system libraries. I tried looking at linkedFrameworks and importing dependencies as shown in the Package.swift file above but it does not find the files. Any assistance would be appreciated. Thanks.

Unfortunately, right now this doesn't work. SwiftPM cannot distribute packages containing binary frameworks. With the right combination of directives SwiftPM can build the folder structure you have, but it is ineligible to be a SwiftPM dependency because it would require the use of unsafeFlags for the linker, and you cannot use those in a package that is supposed to be a dependency.

What you can do, however, is distribute the binary frameworks separately and simply require that they be present and findable in order to build your SwiftPM package. To do that, you'd have to make some changes to the target Core. This is what you have today:

.target(
    name: "Core",
    dependencies: [
        "BinaryFramework",
        "BinaryFramework2",
    ]
)

This is what you'd want:

.target(
    name: "Core",
    linkerSettings: [
        .linkedFramework("BinaryFramework"),
        .linkedFramework("BinaryFramework2"),
    ],
)

This instructs SwiftPM that you have dependencies on the binary frameworks and you expect them to be present. Note that they must be on the default linker path on the system that builds Core, so you'll need to distribute and install them appropriately.

7 Likes

Thank you, it seems that I can do this using the linkedFramework you show above:

swift build -Xswiftc -FFrameworks/Build/iOS   
swift test -Xswiftc -FFrameworks/Build/iOS 

And it finds my frameworks, linking into the main app is similar in that I can add the frameworks path in the Xcode target. Alternatively, in the manifest:

 swiftSettings: [
        .unsafeFlags(["-FFrameworks/build/iOS"]),
 ]

and the command:

 swift build

This will work for now, but hope the discussion "SPM Support for Binaries Distribution - #21 by ddunbar" leads to something longer term. UnsafeFlags seems like it won't work past development phase.

My Core library now can't be a dependency because of the unsafe flags. Back to the drawing-board. Not sure what will work now.

I think I covered the suggestion above: require that the leaf package know how to find the binary frameworks, rather than distributing them as part of your package.

Sorry, I must be missing something, how do I control default framework path in a package imported by Xcode? The Core package must be compiled in Xcode and there are no Xcode settings on the Core target to define the frameworks search path on packages in Xcode. When I try to compile Core without the unsafeFlags it complains it can't find the frameworks so that it can be successfully compiled for Core. I don't see how to get to the next step of including the frameworks with the final product. Thank you for your patience.

I think it should be sufficient to adjust the settings on the project level.

Hi:

I am also in the need of linking a binary as a dependency of a Swift package. Concretely, I have an Xcode project that generates a product (a framework), and that product I need it linked as a dependency of one of the targets the Swift package. I had limited success, I tried 2 approaches.

Lets call the target in the Swift package SwiftPackageTarget and the target in the Xcode project generating the dependency XcodeProjectTarget.

In the first approach:
1- I added linkerSettings: [.linkedFramework("XcodeProjectTarget")] to the SwiftPackageTarget target inside the Package.swift.
2- I added SwiftPackageTarget and XcodeProjectTarget as dependencies of a target app.

As a result, building the target app built and linked XcodeProjectTarget and SwiftPackageTarget successfully. The problem of this approach is that if you try to build SwiftPackageTarget individually it will fail. The main issue is that compiling the app target usually will take a while, as such target will have multiple other dependencies.

In the second approach:
1- I added the Swift package and Xcode project of XcodeProjectTarget under the same Xcode workspace.

Considering that under the Xcode schemes the option Find Implicit Dependencies is on, I expected Xcode to successfully build the SwiftPackageTarget correctly. So far, for what I tested, this does not happen.

I am using Xcode Version 11.0 beta 6 (11M392r).

Edit:
I added an example repository here: GitHub - acecilia/FindImplicitDependenciesBug: A POC showcasing that the "Find Implicit Dependencies" option does not work with Swift packages
I also opened a rdar: rdar://7192051: "Find Implicit Dependencies" does not work with a Swift package and an Xcode project under the same Xcode workspace

linkedFramework is really only supposed to be working for system frameworks. I realise that the documentation didn't make this entirely clear, I have updated it in Clarify what `linkedFramework` is for by neonichu Β· Pull Request #2331 Β· apple/swift-package-manager Β· GitHub

@NeoNacho Well, technically it can link any framework, not only system ones, as the example repository that I created shows. Adding a comment in the documentation is not going to change this fact.

On the SPM side, seems to me that this is a good way of adding binary dependencies to a Swift Package. The only think we would need is support on the Xcode side, so it automatically finds and builds the implicit dependencies.

I do not fully understand why you chose to limit this to system frameworks, instead of fixing Xcode to support it, so Swift Packages can link targets from Xcode projects or binaries.

I understand that this might work for some non-system frameworks, but that is not what this feature was designed for. Packages intentionally follow a model where dependencies are explicitly declared.

There is a pitch thread for support for binary dependencies in SwiftPM here: [PITCH] Support for binary dependencies

1 Like

I'd like some clarification of what defines a "system" framework. Are you really limiting this to, for example, Apple frameworks? I have a lot of frameworks and libraries from 3rd party source and those that I have built myself that I have no intention of building Swift packages for. Are these considered "system" frameworks? Is anything that is located via the traditional linker search?

1 Like

In theory, anything that can found by the default search paths would work using this, but I'd only consider libraries that actually ship with the system (e.g. with macOS) a good candidate, because otherwise you end up with an unexpressed dependency. We have the system library targets feature for declaring explicit dependencies on system libraries that are installed via other means, e.g. homebrew.

Thanks for the reply.
I have followed the above-mentioned step but failed and getting error.
"package has an unsupported layout, binary.framework/module.modulemap should be inside the 'include' directory"

Do you have an idea about this error?

I am not getting your note, " Note that they *must* be on the default linker path on the system that builds Core , so you'll need to distribute and install them appropriately."

Can you please guide me for the same ? can you post some example for binary framework support in Swift PM that will be helpful for other as well?

Can you post some example of same with github that will be helpful for others?

Can you post some example of same with github that will be helpful for others?

Ditto. I'm trying to link some local Swift Packages to XCFrameworks in our Carthage/Build directory and running into all kinds of problems. How in the world did the "Support for binary dependencies" pitch get accepted and implemented without actually adding support for binary dependencies that aren't, themselves, part of the package's directory structure or part of some other package's directory structure?

All I want to do is simply link an external, dynamic XCFramework to a Swift Package, so when we compile the app or the Swift Package, it works.

If this was a standard Xcode project, what we would do is, link the dependency framework into "Frameworks and Libraries" section in Xcode, and decide "Do Not Embed" or "Embed & Sign", and set "../../Carthage/Build/" in Framework Search Paths.

However there still does not seem to be any way to do something equivalent in SPM. What am I missing? How do I set the Framework Search Paths of an SPM dependency?

I've tried using the .linkedFramework option, but this doesn't seem to work because the Framework Search Paths isn't set.

Is there anyone who can help?

We add xcframeworks by wrapping them in their own Swift Package. See firebase-ios-sdk/Package.swift at master Β· firebase/firebase-ios-sdk Β· GitHub and GitHub - google/GoogleAppMeasurement

Hmm. Not sure this works for our use case.

I need to dynamically link an XCFramework, which is in RepoRoot/Carthage/Build/ directory, to a Swift Package that is in RepoRoot/MyLocalPackages/MyPackage/.

How can I do this without resorting to "unsafe flags" that make the package unable to be used by anything else?

Our app has ~15 Carthage dependency XCFrameworks that are already being linked into, and embedded into, our app and its various frameworks, none of which are Swift Packages. (There are about 40.)

Now we want to add new, local Swift packages (not distributed), which need to depend on those same XCFrameworks from the Carthage folder. How can we do this?

It seems the only facilities for linking to a binary that exist in SPM are just for people who are distributing static binaries, like Google Firebase. You cannot wrap an XCFramework in a dynamic library target in SPM. And you cannot specify a binary target that is outside of the package's own directory structure, unless it's on a remote server (as if that limitation makes sense). (Even if you could, from what I can tell, it still wouldn't work, because xcodebuild doesn't make sure the linked frameworks get put into the build folder before it tries to compile package code that depends on them, resulting in build errors.)

I just want a way to use binary frameworks from the Carthage folder with my Swift packages. Surely, there's a way to do that?

@paulb777 Here's the best I could come up with so far:

// swift-tools-version:5.3

import PackageDescription
import Foundation

let simDir = "${PROJECT_DIR}/lib/SwiftProtobuf.xcframework/ios-arm64_i386_x86_64-simulator/"
let realDir = "${PROJECT_DIR}/lib/SwiftProtobuf.xcframework/ios-arm64_armv7/"
let dir: String
  
#if targetEnvironment(simulator)

dir = simDir // problem is, this never runs

#elseif !targetEnvironment(simulator)

dir = realDir // ... & this always runs, 
              // even when compiling for simulator
#endif

let package = Package(
    name: "REDACTED",
    platforms: [
        .iOS(.v13),
    ],
    products: [
        .library(
            name: "REDACTED",
            type: .dynamic,
            targets: ["REDACTED"]
        ),
    ],
    dependencies: [],
    targets: [
        .target(
            name: "REDACTED"
            ,swiftSettings: [
                .unsafeFlags([
                    "-Fsystem", dir,
                ]),
            ]
            ,linkerSettings: [
                .linkedFramework("SwiftProtobuf"),
                .unsafeFlags([
                    "-Xlinker", "-rpath", "-Xlinker", dir,
                    "-Xlinker", "-F", "-Xlinker", dir,
                ])
            ]
        ),
    ]
)

(credit for the above settings pattern goes to Apple, see this git repo for reference)

Problem is, #if targetEnvironment(simulator) simply doesn't work in Package.swift.

Can anyone think of a way to get this to work? Since I've already tried everything else, it would really be nice if someone can think of a way. Or better yet, a way to supply the .xcframework itself to the unsafe flags?

(I would just supply the .xcframework itself to the linkedFrameworks(..) function, but ironically, that function only seems to work with regular .frameworks. So we have to link the simulator or device framework from inside the xcframework, but I'm not sure how to get this working without targetEnvironment(simulator) working.

Is there really no way to do the equivalent of .when with simulator platform vs. real?

Answering my own question, here's a workaround for the fact that #if targetEnvironment(simulator) doesn't work in a Package.swift file. (Note, this workaround only works when building for iOS device or iOS simulator. A more sophisticated method would need to be used if building for other platforms, but the same principles should still apply.

// swift-tools-version:5.3

import PackageDescription
import Foundation

let base = "${PROJECT_DIR}/../../Carthage/Build/SwiftProtobuf.xcframework"

/// If building for simulator, this will resolve to 
/// "/ios-arm64_i386_x86_64-iphonesimulator", causing simDir to be a
/// valid path. Otherwise it will be an invalid path, which only results
/// in a compiler warning. (If both `simDir` and `realDir` are valid paths,
/// we get a build error, so one of them needs to be invalid.)
let simDir = base + "/ios-arm64_i386_x86_64" + "${LLVM_TARGET_TRIPLE_SUFFIX}"

/// If building for device, this will be unaffected, causing realDir
/// to be a valid path. Otherwise it will be an invalid path, which only
/// results in a compiler warning. (If both `simDir` and `realDir` are valid paths,
/// we get a build error, so one of them needs to be invalid.)
let realDir = base + "/ios-arm64_armv7" + "${LLVM_TARGET_TRIPLE_SUFFIX}"
   
let package = Package(
    name: "REDACTED",
    platforms: [
        .iOS(.v13),
    ],
    products: [
        .library(
            name: "REDACTED",
            type: .dynamic,
            targets: ["REDACTED"]
        ),
    ],
    dependencies: [],
    targets: [
        .target(
            name: "REDACTED"
            ,swiftSettings: [
                .unsafeFlags([
                    "-Fsystem", simDir,
                    "-Fsystem", realDir,
                ]),
            ]
            ,linkerSettings: [
                .linkedFramework("SwiftProtobuf"),
                .unsafeFlags([
                    "-Xlinker", "-rpath", "-Xlinker", simDir,
                    "-Xlinker", "-F", "-Xlinker", simDir,
                    "-Xlinker", "-rpath", "-Xlinker", realDir,
                    "-Xlinker", "-F", "-Xlinker", realDir,
                ])
            ]
        ),
    ]
)

This package can be used by other local packages without complaints from the compiler.

NOTE: link this to an Xcode project, you must wrap it in another local package that has no unsafe flags, where the only source file has @_exported import PACKAGENAME at the top. Then the app will link to this wrapper and get the wrapped version.

This is seems to be a valid workaround for linking Carthage dependency XCFrameworks to local Swift Packages. Haven't tried in production yet but seems to actually work so far. YMMV.

4 Likes