Packaging static library in SPM Package for iOS Executable

I have a static iOS library that is distributed as multiple .a files + .h header files in my project that I like to use in my Swift project.
To make it calling-side API a bit nicer though, I'd like to wrap it though in a Swift package.

My SPM package directory tree looks like this:

Wrapper Project
├── Sources
│   ├── Wrapper.swift (Public API - Foundation Types)
│   ├── DependencyAWrapper.swift (Private - Dependent on DependencyA)
│   ├── Frameworks
│   │   └── DependencyA
│   │       ├── DepAHeader.h
│   │       ├── libA_iOS.a
│   │       └── module.modulemap
└── Package.swift

DependencyA is the static library mentioned before, DependencyAWrapper imports DependencyA defined in the module.modulemap like this:

module DependencyA {
    header "DepAHeader.h"

    export *
}

The import is actually using @_implementationOnly import, since the iOS executable which will implement the Wrapper Project doesn't need to know about DependencyA. From what I understand though, DependencyA will still be available in the iOS executable due to SPM limitations - which is fine.

The only public API that the wrapper offers is in Wrapper.swift and uses Foundation types exclusively, nothing from DependencyA or any such imports.

The Package.swift for the Wrapper Project looks like this:

// swift-tools-version:5.3
import PackageDescription

let package = Package(
    name: "Wrapper Project",
    platforms: [
        .iOS(.v12)
    ],
    products: [
        .library(name: "WrapperProject", targets: ["WrapperProject"])
    ],
    dependencies: [],
    targets: [
        .target(
            name: "WrapperProject",
            dependencies: [],
            path: "Sources",
            cSettings: [
                .headerSearchPath("Frameworks/**")
            ]
        ),
    ]
)

Building the Wrapper in Xcode seems to work fine, although warnings are given for the .a file(s) that these are unused.

When importing the Wrapper into my iOS App/Executable with Xcode though, I'm getting Undefined Symbol and other linker issues for the API in libA. Suggesting that there's a linker issue and potentially missing the .a files completely in the iOS Executable.

When building the Wrapper and iOS Project both in Xcode though, I can build and run my project successfully after setting the "Header Search Path" and the "Library Search Path" to my Frameworks folder (on the Wrapper project).

My best assumption is that the static library gets lost because it's not included with the Wrapper Project when built with SPM and then there's also the library linking missing.

Hoping to find an answer here on how to get this working so that I can use SPM and no longer have to rely on other solutions.

(I've tried making the DependencyA a system library before, but that seemed to have ended with the same issue.)

You can package the static library as an XCFramework: https://developer.apple.com/documentation/swift_packages/distributing_binary_frameworks_as_swift_packages

1 Like

Thanks for your answer NeoNacho!

After some fiddling, I was able to build xcframework(s) for my static libraries that my Wrapper Project depend on and the original Xcode project that was/is associated with the Wrapper Project now builds when replacing the original .a files with the xcframework files after setting the "Import Path" to the location of my xcframeworks.

Now as far as my Package.swift goes - that's where I'm still running into issues...
I've declared the DependencyA.xcframework as a binary target under the targets array and added the binary targets name as a dependency to my WrapperProject target.
When I'm trying to build the WrapperProject from the Package now, I'm getting an No such module error. That's the error I've solved in Xcode through the Import Path setting before.

Any idea how to solve this last remaining issue? (At least I hope it's the last remaining issue)

@EfficientSetting it seems I might be trying to accomplish the same thing.
Check here https://github.com/quentinfasquel/MyDependencySample

Issue is that it won't compile in the sample but it does compile in the Package itself... somehow the headers of the xcframework aren't found

Hey @quentinfasquel !

I think I finally solved it!

There were a couple of issues in my path, first and foremost, xcframeworks are a mess and the xcodebuild create-xcframework option is definitely not prime time ready.

My observations so fare with that option are:

  • If you create the xcframework initially with one architecture and then trying to add a second to the same xcframework, will result in the framework being added to the filesystem but not the Info.plist of the xcframework ...
  • If you added the static library for arm64, you'll get a message that armv7 and armv7s aren't needed when you try to specify those too ... I couldn't find much information why, other than some github issue where people were guessing that those 32bit libraries are simply not supported anyway. Would love if someone could clarify this...
  • The very same applies to i386 and x86_64
  • Each of the static libs have to be for a single architecture. I don't know how true this really is, since I've seen an example where armv7, armv7s and arm64 are actually bundled into one file. If someone could clarify that too, that'd be superb!
  • I've initially specified the full path to my headers file e.g. libA/libA.h which ended up cloning the headerfile as HEADERS (no file extension) into the .xcframework folder which in turn made Xcode error (No header issue). I don't think you're supposed to specify the full path for the headerfile, but rather a directory. I later changed that and was more successful.

What helped me a lot was this post on the Apple Forums that I found after initially running into my issue.

Luckily (for once) I decided to go the painful route of writing a script right at the beginning that'll do the work for me, I would have gone crazy otherwise.

Here's my script, module.modulemap and my Package.swift in case that might help you. Other than that, I didn't add each of the headers to the user header path as described in the Apple dev forums link, but I think that wasn't necessary due to my modulemap.

// swift-tools-version:5.3
import PackageDescription

let package = Package(
    name: "Wrapper Project",
    platforms: [
        .iOS(.v12),
    ],
    products: [
        .library(name: "WrapperProject", targets: ["WrapperProject"]),
    ],
    dependencies: [],
    targets: [
        .binaryTarget(name: "DependencyA", path: "DJMetrics/Frameworks/DependencyA.xcframework"),
        .binaryTarget(name: "DependencyB", path: "DJMetrics/Frameworks/DependencyB.xcframework"),
        .binaryTarget(name: "DependencyC", path: "DJMetrics/Frameworks/DependencyC.xcframework"),
        .binaryTarget(name: "DependencyD", path: "DJMetrics/Frameworks/DependencyD.xcframework"),
        .binaryTarget(name: "DependencyE", path: "DJMetrics/Frameworks/DependencyE.xcframework"),
        .binaryTarget(name: "DependencyF", path: "DJMetrics/Frameworks/DependencyF.xcframework"),
        .target(
            name: "WrapperProject",
            dependencies: [
                "DependencyA",
                "DependencyB",
                "DependencyC",
                "DependencyD",
                "DependencyE",
                "DependencyF",
            ],
            path: "Sources",
            exclude: [
                "Frameworks",
                "Supporting Files/Info.plist",
            ],
            cSettings: [
                .headerSearchPath("Frameworks/**"),
            ]
        ),
    ]
)
module DependencyA {
    header "DependencyA.xcframework/ios-arm64/Headers/DepA.h" #Same header file for every Arch, hence this works for me (or so I hope)

    export *
}

And here's my script:

#!/bin/bash

set -x

rm -rf *.xcframework
rm -rf ./*-Thin

find . -name '*.a' | while read -r FRAMEWORK
do
    FRAMEWORK_NAME="${FRAMEWORK%.*}"
    FRAMEWORK_NAME="$(basename -- "$FRAMEWORK_NAME")"
    echo "Framework is $FRAMEWORK_NAME"

    FRAMEWORK_THIN_FOLDER="${FRAMEWORK_NAME}-Thin"
    mkdir ${FRAMEWORK_THIN_FOLDER}

    ARCHS="x86_64 arm64" #Only extracting these two architectures, since xcframework won't take more anyways

    for ARCH in $ARCHS
    do
        echo "Extracting $ARCH from $FRAMEWORK"
        lipo -extract "$ARCH" "$FRAMEWORK" -o "${FRAMEWORK_THIN_FOLDER}/$FRAMEWORK_NAME-$ARCH.a"
    done

    echo "Creating XCFramework for $FRAMEWORK_NAME:"

    # Matching the Headerfiles
    case $FRAMEWORK_NAME in

        lib1)
        HEADERFILES=("./header.h")
        ;;

        lib2)
        HEADERFILES=("./header.h")
        ;;

        lib3)
        HEADERFILES=("./header.h" "./header.h")
        ;;

        lib4)
        HEADERFILES=("./header.h")
        ;;

        lib5)
        HEADERFILES=("./header.h")
        ;;

        lib6)
        HEADERFILES=("./Header1.h" "./Header2.h")
        ;;

        *)
        exit 1
        ;;

    esac

    # Put Headers into place
    mkdir "${FRAMEWORK}"-Headers
    cp ${HEADERFILES[@]} "${FRAMEWORK}"-Headers/

    # Matching the -library and -headers parameters
    THINFRAMEWORKSFILES="$(find ${FRAMEWORK_THIN_FOLDER} -name "*.a")"
    THINFRAMEWORKSARRAY=($THINFRAMEWORKSFILES)

    echo "Building XCFramework for $THINFRAMEWORKSARRAY with ${HEADERFILES[@]/#/-headers }"
    xcodebuild -create-xcframework -library ${THINFRAMEWORKSARRAY[0]} -headers "${FRAMEWORK}"-Headers/ -library ${THINFRAMEWORKSARRAY[1]} -headers "${FRAMEWORK}"-Headers/ -output ${HEADERFILES[0]%.*}.xcframework

    # Remove Temp Headers
    rm -rf "${FRAMEWORK}"-Headers/ 
done

# Cleaning Thin Folders
rm -rf ./*-Thin

I hope this helps (and someone can still verify the remaining of my assumptions)

1 Like
  • I understand your situation and I think you got it right.
  • In my situation, DependencyX.a and its headers are C++, so module.map doesn't make much sense but my source is in Objective-C, all it needs is to find the headers.
  • I believe I was able to have "i386_x64_86"
  • As for armv7 / arm7vs, check which version of iOS you support but there's a chance you only need arm64 anyways

My problem is the following :

What if you want DependencyX to be delivered as a .package dependency (in xcframework**.zip**) ? Then you can't have the cSettings with custom header path. How do yo deal with that ? That's what my example shows.

Xcode knows where to find a xcframework headers if you're simply in your Swift Package.
But when you bring your package into an app, then it doesn't.t

Finally, double-check your final app product, I found that having an xcframework packaging a static lib makes a copy of the static lib under "MyApp.app/Frameworks/DependencyX.a", which doesn't make any sense and makes your app heavier.

@NeoNacho would you have any insight on the issue I am facing ?

I finally looked into your Repo and I think I was able to build it.
I'm no expert in Swift dependency management though, specifically not when it comes go to interoperability, so take that with a grain of salt.

I've added cSettings to the package in MyWrapperLibrary for both the headers in xcframework and the include folder in the sources to go around the error outlined in the Apple Dev forums.
Next, I took the MyWrapperLibrary out of the MyDependencySample Project and Folder as I wanted to integrate the WrapperLibrary as a SwiftPM dependency rather than integrating the package into Xcode - I'm not sure if that makes a difference after all tbh.

When I build the MyDependencySample then, the MyWrapperLibrary SPM package is correctly built and the DependencySample only has some issues with the EventAPI symbol since it doesn't correctly translate the NS_SWIFT_NAME macro, but fixing that compiles the Sample and it seems it's all happening as expected ?!

Again, there's still room for error with what I did here, but my expectation was that when you integrate a SPM package that has the header includes, it would search the header include within the package and it seemed that this went wrong because the dependency is within the wrapping app. Removing that and setting the correct paths seemed to do it.

FWIW, the .a files are still copied into the final app product, I wouldn't know how this could be any different though since this is how it always worked for me with closed source static libraries. I'd be happy to hear how this usually works though.

Hi @EfficientSetting I see what you tried. However consider that it won't be a local path but a url + checksum for the binaryTarget, therefore you cannot have any cSettings towards a path you don't know.

And in the end, MyWrapperLibrary needs to be able to find headers "magically" through the .xcframework, but these headers shouldn't be visible to DependencySample.

Terms of Service

Privacy Policy

Cookie Policy