0

I need to pack an API that depends on a C binary into a single xcframework.

This task seems to be quite tricky because I cannot find any helpful resources. I don't know how to proceed.

I have a working Swift Package:

// swift-tools-version: 5.9

import PackageDescription

let package = Package(
    name: "MyLibrary",
    platforms: [
        .iOS(.v17)
    ],
    products: [
        .library(
            name: "MyLibrary",
            targets: ["MyLibrary"]
        ),
    ],
    targets: [
        .binaryTarget(
            name: "MyBinaryFramework",
            path: "libs/myBinaryFramework/ios/MyBinaryFramework.xcframework"
        ),
        .target(
            name: "CMyBinaryFramework",
            dependencies: ["MyBinaryFramework"],
            path: "Sources/CMyBinaryFramework",
            sources: ["dummy.c"],
            publicHeadersPath: "include"
        ),
        .target(
            name: "MyLibrary",
            dependencies: ["CMyBinaryFramework"],
            path: "Sources/MyLibrary"
        ),
    ]
)

MyBinaryFramework.xcframework contains a C library, and Sources/CMyBinaryFramework contains an include folder with an umbrella header and module.modulemap.

I tried using this tool to create an xcframework from the package, but it does not support binary targets:
https://github.com/segment-integrations/swift-create-xcframework

Is my approach possible in general, or do I need another solution?

Thanks in advance.

New contributor
Chris85 is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.

1 Answer 1

0

In this dependency graph:

MyBinaryFramework -> CMyBinaryFramework -> MyLibrary

  1. You can safely remove the CMyBinaryFramework dependency as it is unnecessary, Swift Package Manager supports binary target(xcframeworks). Your MyLibrary can use c/c++ public symbols from MyBinaryFramework directly if you provide module.modulemap in the framework.

  2. Your MyBinaryFramework.xcframework's folder structure should look like this:

.
├── Info.plist
├── ios-arm64
│   └── libavformat.framework
│       ├── Headers
│       │   ├── avformat.h
│       │   ├── avio.h
│       │   ├── config.h
│       │   ├── os_support.h
│       │   ├── version_major.h
│       │   └── version.h
│       ├── Info.plist
│       ├── libavformat
│       └── Modules
│           └── module.modulemap
├── ios-arm64_x86_64-simulator
│   └── libavformat.framework
│       ├── Headers
│       │   ├── avformat.h
│       │   ├── avio.h
│       │   ├── config.h
│       │   ├── os_support.h
│       │   ├── version_major.h
│       │   └── version.h
│       ├── Info.plist
│       ├── libavformat
│       └── Modules
│           └── module.modulemap
├── macos-arm64_x86_64
│   └── libavformat.framework
│       ├── Headers -> Versions/Current/Headers
│       ├── libavformat -> Versions/Current/libavformat
│       ├── Modules
│       │   └── module.modulemap
│       ├── Resources -> Versions/Current/Resources
│       └── Versions
│           ├── A
│           │   ├── Headers
│           │   │   ├── avformat.h
│           │   │   ├── avio.h
│           │   │   ├── config.h
│           │   │   ├── os_support.h
│           │   │   ├── version_major.h
│           │   │   └── version.h
│           │   ├── libavformat
│           │   └── Resources
│           │       └── Info.plist
│           └── Current -> A
├── tvos-arm64
│   └── libavformat.framework
│       ├── Headers
│       │   ├── avformat.h
│       │   ├── avio.h
│       │   ├── config.h
│       │   ├── os_support.h
│       │   ├── version_major.h
│       │   └── version.h
│       ├── Info.plist
│       ├── libavformat
│       └── Modules
│           └── module.modulemap
└── tvos-arm64_x86_64-simulator
    └── libavformat.framework
        ├── Headers
        │   ├── avformat.h
        │   ├── avio.h
        │   ├── config.h
        │   ├── os_support.h
        │   ├── version_major.h
        │   └── version.h
        ├── Info.plist
        ├── libavformat
        └── Modules
            └── module.modulemap

27 directories, 47 files

This legal bundle structure is addressed in Placing content in a bundle, heads up:

  • MacOS uses a structure called versioned bundle to support multiple library versions. Using iOS's bundle structure on a macOS target allows you to run your app, but you can't finally archive it. It will be mentioned in below.

  • If your xcframework is correctly made, you don't need to wrap it with SPM. Just drag it into Xcode, and your swift code can use that framework.


Below are the steps I used to create a multi-platform xcframework from command line, I strongly recommend you to follow the steps:

  1. Build for Each Platform/Architecture

First, build your library for each platform and architecture combination, the library can be static(.a) or dynamic(.dylib):

# Build for each platform (iOS, macOS, tvOS, etc.) and architecture (arm64, x86_64)
  1. Create fat/universal libraries using lipo for each platform, including iOS, iOS Simulator, macOS, tvOS, and tvOS Simulator, among others.

Combine architectures for each platform:

# Static library
lipo -create \
  ./build/ios-simulator/thin/arm64/lib/libmylib.a \
  ./build/ios-simulator/thin/x86_64/lib/libmylib.a \
  -output ./mylib/ios-simulator/libmylib.framework/libmylib

# Or Dynamic library
lipo -create \
  ./build/ios-simulator/thin/arm64/lib/libmylib.dylib \
  ./build/ios-simulator/thin/x86_64/lib/libmylib.dylib \
  -output ./mylib/ios-simulator/libmylib.framework/libmylib

  1. Now we have a fat libmylib, we then need to Create Framework Bundle Structure

For iOS/tvOS (flat structure), they are just folders in this structure:

libmylib.framework/
├── libmylib              # The binary
├── Headers/              # Public headers
│   └── *.h
├── Modules/
│   └── module.modulemap
└── Info.plist

For macOS (versioned bundle structure, -> means symbolic link, create them with ln, e.g. ln -s Versions/Current/libmylib libmylib):

libmylib.framework/
├── libmylib           -> Versions/Current/libmylib
├── Headers            -> Versions/Current/Headers
├── Resources          -> Versions/Current/Resources
├── Modules/
│   └── module.modulemap
└── Versions/
    ├── A/
    │   ├── libmylib   # The actual binary
    │   ├── Headers/
    │   │   └── *.h
    │   └── Resources/
    │       └── Info.plist
    └── Current        -> A

You can omit the mac platform if you don't need it.

  1. Create module.modulemap(required if you need to export public symbols to swift)

create that file with this content:

framework module libmylib [system] {
    umbrella "."
    exclude header "internal.h"  # Optional: exclude private headers
    export *
}
  1. Create Info.plist

You may need to modify the CFBundleSupportedPlatforms in each plist, they may be one of these values:

  • iPhoneOS: For applications or frameworks intended for iOS devices (actual iPhones, iPads).
  • iPhoneSimulator: For applications or frameworks intended for the iOS Simulator.
  • MacOSX: For applications or frameworks intended for macOS.
  • AppleTVOS: For applications or frameworks intended for tvOS devices.
  • AppleTVSimulator: For applications or frameworks intended for the tvOS Simulator.
  • WatchOS: For applications or frameworks intended for watchOS devices.
  • WatchSimulator: For applications or frameworks intended for the watchOS
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleExecutable</key>
    <string>libmylib</string>
    <key>CFBundleIdentifier</key>
    <string>com.example.libmylib</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleName</key>
    <string>libmylib</string>
    <key>CFBundlePackageType</key>
    <string>FMWK</string>
    <key>CFBundleShortVersionString</key>
    <string>1.0.0</string>
    <key>CFBundleVersion</key>
    <string>1.0.0</string>
    <key>MinimumOSVersion</key>
    <string>13.0</string>
    <key>CFBundleSupportedPlatforms</key>
    <array>
        <string>iPhoneOS</string>
    </array>
</dict>
</plist>
  1. Create XCFramework with xcodebuild

With the libmylib.framework folder structure ready for each platform, time to create a XCFramework with xcodebuild

xcodebuild -create-xcframework \
  -framework ./mylib/ios/libmylib.framework \
  -framework ./mylib/iossimulator/libmylib.framework \
  -framework ./mylib/macos/libmylib.framework \
  -framework ./mylib/tvos/libmylib.framework \
  -framework ./mylib/tvossimulator/libmylib.framework \
  -output ./Sources/libmylib.xcframework
  1. Integrating with Swift Package Manager

Finally you can integrate with SPM, just put the xcframework in Sources folder

// swift-tools-version:5.9
import PackageDescription

let package = Package(
    name: "MyLibrary",
    platforms: [
        .iOS(.v13),
        .macOS(.v10_15),
        .tvOS(.v13)
    ],
    products: [
        .library(name: "MyLibrary", targets: ["MyLibrary"])
    ],
    targets: [
        // Binary target for XCFramework
        .binaryTarget(
            name: "libmylib",
            path: "Sources/libmylib.xcframework"
        ),
        
        // Swift wrapper target (optional)
        .target(
            name: "MyLibrary",
            dependencies: ["libmylib"],
            linkerSettings: [
                .linkedFramework("VideoToolbox"),
                .linkedFramework("CoreMedia"),
                .linkedLibrary("z"),
                .linkedLibrary("bz2")
            ]
        )
    ]
)

Comment below if you need some help!

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.