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.)

1 Like

You can package the static library as an XCFramework: Apple Developer Documentation

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 GitHub - 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)

3 Likes
  • 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.

@quentinfasquel I see, I missed the fact that the checksum and everything else mattered.

@NeoNacho With the binaryTargets now declared as dependencies to the Wrapper, I'm now unable to export my App getting "IPA processing failed" with the log showing "Assertion failed: Expected 1 archs in otool output" for one of the .a files that were copied into the target app. This doesn't seem to make a lot of sense to me, given that the referenced -arm64.a slice is literally only 1 arch already.

Any clues?

I have not seen this error message before. I would suggest filing a bug report with a reproducing project if possible.

Alright! Thanks!

After some time searching around, it seems that multiple others have the same issue as I do - see Can’t archive App that uses a swif… | Apple Developer Forums

So archiving with binary targets seems to trigger an assertion with otool that might simply has the wrong expectation given the error output - or it's more than that and I'm just blindly guessing.

I guess that means though that I'll have to find another way to wrap my static libraries until this is fixed

Last update on this from me:

I've spent the weekend and then some trying to make XCFrameworks that hold static libraries work to no avail ...
While I'm no longer seeing the otool issue I was seeing initially, I'm now getting a "Symbols Tool Failed" during archive -> distribute with an error that points to the .a static libraries having an error of "error: No UUID for lib.a" indicating that Xcode incorrectly tries to symbolize the static libraries for dSYMs which fails since they are static libraries ...

That is also about what Quentin said when we noticed that Xcode seems to just copy the .a files into the bundle which seems contrary how these files should be handled.

Workarounds I tried included:

  • Make the binary targets a separate package - failed.
  • Properly tagging the resources - couldn't make distribute work

I'm not an expert on the field, but learned a lot on my journey, but it seems that this is a problem everyone with static libraries in xcframeworks faces. I think it comes down to tooling incorrectly processing the XCFramework. Hoping this might be fixed in a future version of Xcode

I guess I shouldn't ever say last ...

Obviously I ended up spending more time on this, it just bothered me that the product executables for my wrapper frameworks were the same size both with the static libraries added or with the xcframeworks and the only difference was the presence of the .a files copied in.

I did some digging and also had to deal with Firebase-SPM that had similar issues when I noted that they were able to release SPM included binaryTargets (although Frameworks, not libraries) and they solved it by deleting the frameworks from the InstallableBuildProducts directory. So I went ahead, added a build phase that deleted the incorrectly copied static libraries and tada - it works. Export and everything.

rm -rf "${TARGET_BUILD_DIR}/${TARGET_NAME}.app/Frameworks/"*.a
rm -rf "${TARGET_BUILD_DIR}/${TARGET_NAME}.app/PlugIns/"*.a
rm -rf "${TARGET_BUILD_DIR}/${TARGET_NAME}.app/Watch/"*.a

@NeoNacho I'm not sure how helpful this is, but I think all that is "broken" in the way Xcode processes XCFrameworks is that it's copying the contents of the xcframework files into the Build directories. My best guess is that this is happening due to the addition of the assets inclusion, again, best guess.

I'm happy I'm one step closer to a full SPM environment

2 Likes

Does this solve the archiving issue and also submitting to the App Store?

Yes it does. Released yesterday with this workaround and no problems

@EfficientSetting I tried your run script, but the .a files still seem to get copied into the framework folder. I tried both build phase and post-action script for archive. Is there anything else you did?

If you still see the .a files in your product, then the script wasn't working. My best guess is a path issue, maybe a space somewhere or something else. You need to make sure that this works and debug it in the build phase where needed.

This is a build phase action, not a post-action since obviously that would already be too late for the workaround/fix

I added the script to post action for release-build and it worked!!! Was able to archive and get a working build on the App Store. When looking at the output for build phase – the script runs before the .a framework is copied that doesn't make sense. Screenshot below.

Screen Shot 2020-11-16 at 12.04.25 PM

Xcode copies the *.a to everywhere inside the app bundle, includes Contents/Library/LoginItems/ and Contents/Resources/ folders.

Append a Run Script Phase to Build Phases should remove unwanted static libraries:

rm -rf "${TARGET_BUILD_DIR}/${TARGET_NAME}.app/Contents/"**/*.a
rm -rf "${TARGET_BUILD_DIR}/${TARGET_NAME}.app/Contents/"**/**/*.a