Link to fat Static Library inside Swift Package
Asked Answered
J

2

10

I’m trying to build a Swift Package that wraps a fat static library written in C: libndi_advanced_ios.a from NewTek's Apple Advanced NDI SDK.

I am having trouble linking the pre-compiled library (only headers files and .a binary package is available) to my Swift Package. I have done a lot of research and tried different solutions, but none of them worked. Here is a quick list:

  • Cannot bundle in an XCFramework because libndi_advanced_ios.a supports multiple platforms (arm_v7, i386, x86_64, arm64) and xcodebuild -create-xcframework return the error binaries with multiple platforms are not supported (this solution is discussed on Swift Forums too);

  • Using .linkedLibrary in targets as suggested on SPM Documentation (that is outdated) gives the warning system packages are deprecated; use system library targets instead, and I don’t even remember if it builds successfully;

  • Playing around with different flags and settings (like linkerSettings) has not been successful. Maybe I just missed the right combination.

I can link dozens of Stackoverflow's questions and other posts that didn’t help, but it will be useless (a, b, c).

At the moment I have this configuration:

project structure with module.modulemap

With Package.swift that contains the following code:

let package = Package(
    name: "swift-ndi",
    platforms: [.iOS(.v12)],
    products: [
        .library(
            name: "swift-ndi",
            targets: ["swift-ndi"])
    ],
    dependencies: [],
    targets: [
        .target(name: "CiOSNDI", path: "Libraries"),
        .target(
            name: "swift-ndi",
            dependencies: ["CiOSNDI"]),
        .testTarget(
            name: "swift-ndiTests",
            dependencies: ["swift-ndi"]),
    ]
    
)

You can find the whole project at alessionossa/swift-ndi.
The only result at the moment are some warnings and the module CiOSNDI do not build:

errors and warnings

I tried also .systemLibrary(name: "CiOSNDI", path: "Libraries/"), with this configuration: alessionossa/swift-ndi/tree/systemLibrary; but I get these errors:
errors and warnings 2

NOTE

NDI_include is actually an alias/symbolic link to /Library/NDI Advanced SDK for Apple/include, while NDI_iOS_lib points to /Library/NDI Advanced SDK for Apple/lib/iOS.

I always cleaned build folder after changes to Package.swift.

UPDATE 10/01/2022: libndi_advanced_ios.a requires libc++.tbd. That can be easy linked in an app in Build Phases -> Link Binary With Libraries, but I don’t know how to link in a Swift Package.

Jamal answered 2/1, 2022 at 15:39 Comment(1)
It looks like you can link in a Swift Package using the linkerSettings target parameter.Aparri
A
3

I also needed to add the NDI SDK as a Swift package dependency in my app.

I found a couple of approaches that worked:

You can create an XCFramework bundle by extracting a thin arm64 library from the universal library:

lipo "/Library/NDI SDK for Apple/lib/iOS/libndi_ios.a" -thin arm64 -output "$STAGING_DIRECTORY/libndi.a"

Then create an XCFramework bundle:

xcodebuild -create-xcframework -library "$STAGING_DIRECTORY/libndi.a" -headers "/Library/NDI SDK for Apple/include" -output "$STAGING_DIRECTORY/Clibndi.xcframework"

I ended up not using this approach because hosting the XCFramework as a downloadable binary release on GitHub required me to make my repo public (see this issue).

Instead I am using a system library target, in my Package.swift:

    targets: [
        .target(
            name: "WrapperLibrary",
            dependencies: ["Clibndi"], 
            linkerSettings: [
                .linkedFramework("Accelerate"),
                .linkedFramework("VideoToolbox"),
                .linkedLibrary("c++")
            ]),
        .systemLibrary(name: "Clibndi")
    ]

Then, I have WrapperLibrary/Sources/Clibndi/module.modulemap that looks like:

module Clibndi {
  header "/Library/NDI SDK for Apple/include/Processing.NDI.Lib.h"
  link "ndi_ios"
  export *
}

Finally, my application target (part of an Xcode project, not a Swift package) depends on WrapperLibrary, and I had to add "/Library/NDI SDK for Apple/lib/iOS" (including the quotation marks) to "Library Search Paths" in the "Build Settings" tab.

As an alternative to modifying the application target build settings, you could add a pkg-config file to a directory in your pkg-config search paths. For example, /usr/local/lib/pkgconfig/libndi_ios.pc:

NDI_SDK_ROOT=/Library/NDI\ SDK\ for\ Apple

Name: NDI SDK for iOS
Description: The NDI SDK for iOS
Version: 5.1.1
Cflags: -I${NDI_SDK_ROOT}/include
Libs: -L${NDI_SDK_ROOT}/lib/iOS -lndi_ios

Then use .systemLibrary(name: "Clibndi", pkgconfig: "libndi_ios") in your package manifest. I found this less convenient for users than just adding the setting to my application target, however.

Ideally you could add the NDI SDK's dependency library and frameworks to the pkg-config file as well (Libs: -L${NDI_SDK_ROOT}/lib/iOS -lndi_ios -lc++ -framework Accelerate -framework VideoToolbox), but it appears there is a bug in Swift's pkg-config parsing of -framework arguments, so I filed a bug: SR-15933.

Aparri answered 28/2, 2022 at 18:23 Comment(0)
M
5

Binary targets need to specified with .binary_target. See the docs and example here.

An example of a static library wrapped in an .xcframework looks like this from the file command:

$ file GoogleAppMeasurement.xcframework/ios-arm64_armv7/GoogleAppMeasurement.framework/GoogleAppMeasurement
GoogleAppMeasurement.xcframework/ios-arm64_armv7/GoogleAppMeasurement.framework/GoogleAppMeasurement: Mach-O universal binary with 2 architectures: [arm_v7:current ar archive] [arm64]
GoogleAppMeasurement.xcframework/ios-arm64_armv7/GoogleAppMeasurement.framework/GoogleAppMeasurement (for architecture armv7):  current ar archive
GoogleAppMeasurement.xcframework/ios-arm64_armv7/GoogleAppMeasurement.framework/GoogleAppMeasurement (for architecture arm64):  current ar archive

One way to create the .xcframework file is to use Firebase's ZipBuilder that creates a .xcframework files from static libraries that are specified with a CocoaPods podspec file.

Merchandising answered 5/1, 2022 at 14:46 Comment(10)
.binary_target in SPM requires that the library is an XCFramework or an artifact bundle, and as I wrote above, that’s not possible in this caseJamal
Hmm, I'm not following. An .xcframework is just a directory structure that wraps .frameworks. My understanding is that any .framework could be converted to an .xcframework.Merchandising
First, I don’t have a .framework library but .a static library. Second, I tried creating an XCFramework using xcodebuild -create-xcframework as in Apple Documentation, but you can’t create an XCFramework from a library that supports multiple platforms like libndi_advanced_ios.a: XCFramework only bundles binaries that are compiled for a single platform, and since the library is closed-source, I cannot recompile it to be compatible with XCFramework.Jamal
A static framework is a directory structure containing a static library and headers. It should be possible go create a static framework from a static library with file system commands and then to create an xcframework from a framework with additional file system commands.Merchandising
I tried implementing your solution but I got a "No such module 'NDI_iOS'" error when importing as a local XCFramework. This is a known issue (1) although they marked as solved in Xcode 13.2 (77465707). I get the same issue when importing binary via URL. You can find implementation at alessionossa/swift-ndi in the dedicated branch. Therefore this is not a working solution.Jamal
It may help to compare against the directory structures of the xcframeworks referenced by github.com/google/GoogleAppMeasurement which are created via scripts with file management commands.Merchandising
The XCFrameworks you linked bundle.framework libraries, while in my case the XCFramework wraps .a libraries. Therefore I think the two cases are not comparable.Jamal
My example includes archive libraries as well. When, a .a is bundled into a .framework, the .a is dropped from its name. If you run the file command, you'll see they're both :current ar archive. I'll update the answer with more detail.Merchandising
So you are suggesting to wrap a .a static library in a .framework and then in a .xcframewok, right? If so, is there any simple way to wrap a .a static library in a .framework (like when wrapping .a in .xcframework, it’s just a command)? I am a bit hesitant about this solution because XCFramework should support (theoretically) .a static libraries directly.Jamal
Yes. I'll add another update to the answer with one way to do it.Merchandising
A
3

I also needed to add the NDI SDK as a Swift package dependency in my app.

I found a couple of approaches that worked:

You can create an XCFramework bundle by extracting a thin arm64 library from the universal library:

lipo "/Library/NDI SDK for Apple/lib/iOS/libndi_ios.a" -thin arm64 -output "$STAGING_DIRECTORY/libndi.a"

Then create an XCFramework bundle:

xcodebuild -create-xcframework -library "$STAGING_DIRECTORY/libndi.a" -headers "/Library/NDI SDK for Apple/include" -output "$STAGING_DIRECTORY/Clibndi.xcframework"

I ended up not using this approach because hosting the XCFramework as a downloadable binary release on GitHub required me to make my repo public (see this issue).

Instead I am using a system library target, in my Package.swift:

    targets: [
        .target(
            name: "WrapperLibrary",
            dependencies: ["Clibndi"], 
            linkerSettings: [
                .linkedFramework("Accelerate"),
                .linkedFramework("VideoToolbox"),
                .linkedLibrary("c++")
            ]),
        .systemLibrary(name: "Clibndi")
    ]

Then, I have WrapperLibrary/Sources/Clibndi/module.modulemap that looks like:

module Clibndi {
  header "/Library/NDI SDK for Apple/include/Processing.NDI.Lib.h"
  link "ndi_ios"
  export *
}

Finally, my application target (part of an Xcode project, not a Swift package) depends on WrapperLibrary, and I had to add "/Library/NDI SDK for Apple/lib/iOS" (including the quotation marks) to "Library Search Paths" in the "Build Settings" tab.

As an alternative to modifying the application target build settings, you could add a pkg-config file to a directory in your pkg-config search paths. For example, /usr/local/lib/pkgconfig/libndi_ios.pc:

NDI_SDK_ROOT=/Library/NDI\ SDK\ for\ Apple

Name: NDI SDK for iOS
Description: The NDI SDK for iOS
Version: 5.1.1
Cflags: -I${NDI_SDK_ROOT}/include
Libs: -L${NDI_SDK_ROOT}/lib/iOS -lndi_ios

Then use .systemLibrary(name: "Clibndi", pkgconfig: "libndi_ios") in your package manifest. I found this less convenient for users than just adding the setting to my application target, however.

Ideally you could add the NDI SDK's dependency library and frameworks to the pkg-config file as well (Libs: -L${NDI_SDK_ROOT}/lib/iOS -lndi_ios -lc++ -framework Accelerate -framework VideoToolbox), but it appears there is a bug in Swift's pkg-config parsing of -framework arguments, so I filed a bug: SR-15933.

Aparri answered 28/2, 2022 at 18:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.