Building XCFrameworks from a hierarchy of Swift Packages

This post is meant to provide useful information on how to build a set of xcframeworks from a hierarchy of Swift Packages containing a mix of ObjC and Swift. If you only want to build a single xcframework from a Swift Package, as this is not obvious either, you should find some useful info too.

Stuff that needs to be done for building an XCFramework from a Swift Package through xcodebuild

  • set library type to .dynamic in Package.swift
  • set SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES OTHER_SWIFT_FLAGS="-no-verify-emitted-module-interface" in xcodebuild command
  • no need to use 'archive' xcodebuild command, xcodebuild alone is enough

In generated framework :

  • copy public headers (in case of ObjC package)
  • copy (or create) module.modulemap file, or copy .swiftmodule file
  • copy umbrella header FooPackage-Swift.h from derived data (use -derivedDataPath xcodebuild option to retrieve it) in case of Swift Package, otherwise won't be useable from ObjC
  • copy resource bundle (if any)

In many cases I found that people are using xcodebuild archive for this task, but I find that not necessary. The archive doesn't contain anything more than what you get from xcodebuild only.

Here's the script I ended up writing :

Setup

A set of Swift Packages, containing Swift and ObjC code, with dependencies. We need to use these Packages in an app and also to create XCFrameworks from them.

The (simplified) dependencies are as follows :

CommonObjC <- Common <- Package1, Package2... <- GlobalPackage <- MainApp

Problems encountered, and their solutions

Many of these problems come from the fact that we need to use these modules both as Swift Packages and as XCFrameworks.


xcodebuild needs the library defined in the Swift Package to be explicitly declared of type .dynamic, otherwise only a .o is generated for a package

But if all libs are dynamic, then, in the Swift Package usecase, symbols in CommonObjC won't be visible from MainApp. If CommonObjC is explicitly added to MainApp's linked frameworks, Xcode issues a build error saying "CommonObjC is linked as a static library but cannot be built dynamically because there is a package product with the same name".

Solution : since we don't want libs to be dynamic for the case where the Swift Packages are used directly, have the library type set depending on a Environment var. There doesn't seem to be a way to pass Swift Compiler flags (-DFOO) to xcodebuild so that they are used when evaluating a Package.swift file.

This works :

func runtimeLibType() -> Product.Library.LibraryType?
{
    return ProcessInfo.processInfo.environment["SPM_GENERATE_FRAMEWORK"] != nil ? .dynamic : nil
}

and then

let package = Package(
    name: "FooPackage",
    products: [
        .library(
            name: "FooPackage",
            type: runtimeLibType(),
            targets: ["FooPackage"]),
    ],
...


Symbols from a package Foo's dependency are not visible from another package or app using package Foo.

If Package Foo is in Swift, the solution is to use '@_exported' in the import statement for the dependency :

PackageFoo/SomeFile.swift :

@_exported import Dependency

then in App :

import PackageFoo

-> symbols from Dependency will be visible.

There is no equivalent solution in ObjC


Symbol export issues with "composed" package (ie a package built from two targets)

Solution is simply to not do that. Stick to one package being built from one target.

Importing a 2nd-level dependency in a public header works when building from SPM, but not when an app is importing the resulting xcframework (dependencies are not transient in this case)

For instance :

PackageRT depends on PackageCore which depends on PackageCoreObjC

PackageRT/include/public_header.h can '@import' PackageCoreObjC. It works as long as you build from SPM, but when you build PackageRT into an xcframework, importing the resulting xcframework will fail because the @import PackageCoreObjC in PackageRT/include/public_header.h fails because PackageCoreObjC module is not found.

Solution is to explicitly add PackageCoreObjC as a dependency to PackageRT.

Post not yet marked as solved Up vote post of glaurent Down vote post of glaurent
1.2k views

Replies

Please file an Enhancement Request in Feedback Assistant for a supported way to build an XCFramework from a Swift Package. Please post the number here once you do. There isn't a supported way to do this with Xcode 15, and as you found, there's lots of interesting corners that supported solution needs to fully consider.

  • Thanks, here's the FB number : FB13449783.

Add a Comment