Swift Package Manager builds iOS frameworks

(Updated. Xcode 10.2 Beta)

Artem Loenko
Published in
8 min readFeb 25, 2019

--

Swift Package Manager doesn’t work with iOS. That’s probably all you can say about the current state of SPM, but insomnia forced me to expand on the issue and compile the following essay.

TL;DR

  • Yes, you can build iOS frameworks with Swift Package Manager;
  • Yes, you can adjust settings via .xcconfig and reuse generated .xcodeproj in a real iOS application without 3rd party tools to parse a project structure, scripts on Ruby, etc.;
  • Yes, you can use swift build to build your package but only as a library (but with some additional limitations);
  • No, you cannot use swift test because you have to spawn a simulator to run them.

Current State of Swift Package Manager

iOS support is a hot topic in the Swift Package Manager community, and from time to time someone raises this same question again but the following comments sum up the community’s reactions (full thread on forums.swift.org):

@Rick Ballard
I think that this will be best provided by native IDE integration. However, in the meantime, I’d welcome contributions to help improve Xcode’s project generation support.

@Adrian Kashivskyy
The last thing I want from a platform-agnostic open-source package manager is built-in integration with a single-platform commercial closed-source IDE. :confused: I think this should be done independently by the DT team, without any special favouritism by SPM.

And you know what? I agree with these statements. Especially after some research. In general, SPM is a very ‘young’ and inflexible project. There are a lot of limitations, especially around generate-xcodeproj options of the swift package tool. And this is understandable given that Swift is a language, and all related tools need to be platform-agnostic as far as possible. Yeah, iOS is the biggest Swift consumer, and Apple contributes to Swift mostly because of iOS, but…

It’s going to be almost impossible to grow Swift into a mature technology if you’re limited and restricted by Xcode / iOS specific things / etc. And, it seems, this is the primary goal for Swift to remain just be a language. The fate of Objective C is a good example of why the Apple and Swift’s communities are trying to be agnostic. They are trying to build something big, and lack of iOS support is the price (among many others) at the moment. :kiss:

Having said this, we have exciting news about SPM and iOS friendship. SE-0236: Package Manager Platform Deployment Settings is accepted with some modifications. And the implementation of this proposal will help a lot to move forward in the case of iOS. The main goal is clear and straightforward:

Packages should be able to declare the minimum required platform deployment target version. SwiftPM currently uses a hardcoded value for the macOS deployment target. This creates friction for packages which want to use APIs that were introduced after the hardcoded deployment target version.

Why will it completely fail to solve the problem with iOS? Just read the “This option doesn’t resolve these problems” section.

Xcode 10.2 Beta

SE-0236 was accepted and implemented in Swift 5, and Apple shipped it with the latest Xcode 10.2 beta. This means that you can specify an iOS as a deployment target for your packages with a simple line in your Package.swift file:

See an example in xcode_10_2_beta branch in the example repository. It is still a beta implementation, and you will have a lot of issues with build command, and run doesn’t work either, but it is a step toward eventual iOS support.

I hope that Apple will announce something good at WWDC’19. A new IDE for Swift, perhaps? Or open-sourced xcodebuild? Or xcodebuild replacement based on llbuild? We’ll see. The current state is as complicated as it can be. We are trying to inject open-sourced platform-agnostic tools into a legacy (?) world of Xcode, and this problem can only be solved in one of two ways: either iOS specific build tools go open source, or Xcode team supports Swift infrastructure.

Is it possible to build an iOS framework with SPM? Yes! Absolutely!

Search for solutions for building iOS frameworks with SPM on DuckDuckGo and you will find some instructions (1, 2). But all of them have this one step that I hate: sudo gem install xcodeproj :disgusting:. Can we do any better? Let’s give it a try.

First of all, let’s generate a template:

Now we have to convince SPM that we want an iOS project when it generates xcodeproj. How? With xcconfig, of course. Create a file ios.xcconfig and add it to ./Sources folder. For example, let’s start with a basic version:

Looks good. Let’s see what SPM thinks of it:

Didn’t know about xcconfig-overrides? Me neither. It’s a hidden and undocumented feature (commit), thanks to @Daniel Dunbar! Time to ask Xcode what it thinks of it.

Note: You do not have to specify a custom .xcconfig file if you are using Xcode 10.2 Beta with Swift 5 Toolchain. Check the branch for more details.

It works! Let’s celebrate! But nope. We’re not there yet, need to dig deeper. Let’s check how the ‘Unit Tests’ target works, for example:

It doesn’t. Due to this strange and suspicious XCTestCaseEntry. What is this? According to the swift-corelibs-xctest source code:

This is a compound type used by XCTMain to show tests to run. It combines an XCTestCase subclass type with the list of test case methods to invoke on the class.

And the typealias looks like this:

Why doesn’t it work? For the same reason:

CoreLibs XCTest only supports desktop platforms

Thanks go to @larryonoff for his work on multi-platform support. But unfortunately we still can’t use it for our needs. Join this Add Unit Testing Infrastructure thread if you want to learn more about the current state of swift-corelibs-xctest. We will move on and apply the fix to XCTestManifests.swift:

See this && !os(iOS)? Let’s keep going. Run Tests again and… We got what we need.

Then I created a simple iOS ExampleApp and added the generated xcodeproj as a dependency. Of course, I’ve added some iOS specific code to the framework:

And then reuse it from the example app:

The full example is available on GitHub. Thanks to CLANG_MODULES_AUTOLINK, all iOS frameworks will be linked automatically. I didn’t try more complex scenarios (where one iOS module depends on another, etc.) because it’s not my goal here. But, in general, it does just work albeit some limitations. SPM doesn’t set up our xcconfig for some targets, and you have to include the SPM-generated .xcodeproj to your .xcodeproj, but all these tradeoffs seem reasonable in the interests of this research and achieving our current goal.

Back to Swift Package Manager

See. We can do better 🥳. But we forgot about SPM during this Xcode journey. Let’s close our fancy, dark-themed Xcode, open Terminal and run swift build for our iOS’ish package (I’m going to use the package from the example project mentioned above):

Okay. There’s no such ‘UIKit’ module. Can we do better? I doubt it, but let’s try. First of all, we have to find out where SPM gets all these environment variables from. We can ask it with swift build — verbose:

Nice. No smoke or mirrors. Let’s try to changing some swiftc options to build the project against proper sdk and target:

Looks better. We built the binary:

Let’s carry out some inspections, just to make sure that everything is OK:

lipo is a bit useless in this case because we were building for a simulator, but nm shows us everything we need to know — iOS frameworks symbols are available. Unfortunately, swift build doesn’t produce .framework by default. I think it’s doable even in this case but let’s look at that ‘next time’.

Epilogue: swift test

And the final call. We already have one unit-test for our package: it uses UIKit, and I would rate this experiment as successful if we could run the test target with swift test. It’s almost impossible though, because usually unit-tests for simulator have to spawn to a simulator process. I don’t even think that it’s possible for an actual iOS project. Anyway.

And there’s another problem. .xctest bundle is compiled but xctest tool gets confused with search paths:

Likely, swift build & test produce beneficial debug information and store it in .build/debug.yaml with all previous options and arguments. This goes for the options for a module itself as well, so it’s time for our command line friends again:

As you can see, for some reasons, it tries to link UIKit.framework twice:

  • via the /System/Library/Frameworks path
  • and via the expected @rpath/libswiftUIKit.dylib.

Let’s check the information from the module itself with otool, to be sure that load is describing the correct framework to link:

Seems correct to me. So, to eliminate the confusion let’s pass the linking option directly with -Xswiftc “-lswiftUIKit”:

Yep, but built for simulator (not macOS)). This is my final conclusion, my friend. It doesn’t take that much effort to link proper frameworks, but it’s impossible to run these tests for iOS device or simulator without SPM support. It seems that not even xcrun with xctest are up to the job. xcodebuild assistance is needed here. But enough of these weird logs and useless rants, let’s summarise.

Summary

Thanks for reading, first of all! So, what did we learn?

  • We can use SPM in sporadic cases for iOS, just for integration, thanks to .xcconfig;
  • The future of the friendship between iOS and SPM is unsure even with this SE-0236 proposal;
  • Swift build and test helpers are useless to us without iOS-specific features and full Xcode support. And that’s unlikely to happen. Of course, SPM can be integrated on top of Xcode by Xcode team. For example, they will extend xcodebuild functionality. But concerning SPM, it is going to be a different story.

Regarding SPM. Generally, I think it’s doable, and we can improve SPM to support any platform you like. My best advice at the moment is to introduce pipeline plugins for SPM, enabling you to transfer the control flow to a separate tool, with expected input and output. Something like Xcode custom build phases but is smarter and more flexible. It will allow SPM to be platform-agnostic as now, but the Xcode team can create a plugin for the whole iOS flow support. Or Uber. Or Google. Or me. Whatever.

Stay tuned and hydrated!

--

--