Article

How to add a dynamic Swift framework to a Command Line Tool

Sean Berry

January 26, 2018

Let’s walk through how I tried to add a dynamic framework to my Command Line Tool and discuss what went wrong each step of the way.

Step 1: Add it to the ‘Linked Frameworks and Libraries’ section

This is what happens when you run your app:

dyld: Library not loaded: @rpath/libswiftAppKit.dylibReferenced from: /Users/seanberry/Library/Developer/Xcode/DerivedData/TestCommandLineTool-fnrmhjvjmugvqueaqvbklzwhqvuv/Build/Products/Debug/ThirdParty.framework/Versions/A/ThirdPartyReason: image not found

The ThirdParty.framework is trying to find libswiftAppKit.dylib (which is part of the Swift standard libraries) in the @rpath directory.

We can see how ThirdParty.framework defines @rpath by running

$ oTool -l ThirdParty.framework/Versions/Current/ThirdPartyLoad command 27
cmd LC_RPATH
cmdsize 48
path @executable_path/../Frameworks (offset 12)
Load command 28
cmd LC_RPATH
cmdsize 40
path @loader_path/Frameworks (offset 12)

Well shoot. Those aren’t relevant to our Command Line Tool. We don’t have any folders named /Frameworks or ../Frameworks. Why is it looking for the Swift standard libraries there?

Because it’s built for iOS and Mac apps. Here’s the directory structure inside a Mac app:

That explains path @executable_path/../Frameworks

And for iOS, the directory structure inside the app is:

@loader_path equals @executable_path when the framework is loaded from the executable

And that explains path @loader_path/Frameworks (offset 12)

But what about us, the humble command line tool developer? The Swift standard libraries are statically linked inside our executable, but our third party frameworks can’t find them. Unfortunately they’re not stored in a standard place on every Mac. Developers can get access to them buried inside Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx (but please don’t require your users to install Xcode).

So what do we do? The standard practice I’ve seen is to create a custom framework for your project, import all of your dependencies into it, then copy the swift standard libraries as well.

Step 2: Create a custom framework and copy the standard Swift libraries

This will copy/paste the Swift standard libraries inside FirstParty.framework/Versions/Current/Frameworks/
Make sure to copy in your Third party dependency as well
Now your frameworks are all together!

Perfect! Now let’s go ahead and run our Command Line Tool…

objc[2335]: Class _TtC8Dispatch16DispatchWorkItem is implemented in both /Users/seanberry/Library/Developer/Xcode/DerivedData/TestCommandLineTool-fnrmhjvjmugvqueaqvbklzwhqvuv/Build/Products/Debug/FirstParty.framework/Versions/A/Frameworks/libswiftDispatch.dylib (0x1016c8530) and /Users/seanberry/Library/Developer/Xcode/DerivedData/TestCommandLineTool-fnrmhjvjmugvqueaqvbklzwhqvuv/Build/Products/Debug/TestCommandLineTool (0x1005c7698). One of the two will be used. Which one is undefined.REPEAT THE ABOVE ERROR IN 20 DIFFERENT WAYS

Your app is now seeing two different copies of the Swift standard libraries: the ones statically linked inside your executable, and the ones inside the /Frameworks folder.

There are two solutions from here.

Step 3 (option A): Make a Mac app instead and extract the executable

I investigated famous command line tools Carthage and SwiftLint to see how they handled this problem. Turns out they’re not set up as command line tools! They’re Mac apps! Why? Because a Mac app doesn’t statically link the standard libraries. They deploy themselves as command line tools by adding a run phase that extracts out the executable from the app package.

#!/bin/bash
## Extracts the carthage CLI tool from its application bundle. Meant to be run
# as part of an Xcode Run Script build phase.
cp -v "${BUILT_PRODUCTS_DIR}/${EXECUTABLE_PATH}" "${BUILT_PRODUCTS_DIR}/${EXECUTABLE_NAME}"

You can go ahead and do it that way, no problem. But I found another way around this issue.

Step 3 (option B): Disable static linking

Add these into your User Defined Build Settings:

SWIFT_FORCE_DYNAMIC_LINK_STDLIB YES
SWIFT_FORCE_STATIC_LINK_STDLIB NO

This will force your executable to dynamically link all libraries. Make sure to tell Xcode where to find them by adding the framework directory to the ‘Runpath Search Paths’

@executable_path/FirstParty.framework/Versions/Current/Frameworks

Good luck with your command line tool!

Sean tries to get Xcode to compile at Livefront
  • Sean Berry

    Software Engineer

    Sean Berry