Skip to main content
Marek Fořt

Compiling Package.swift in a raw Xcode project

Motivation #

Let me first start with why I even wanted to compile Package.swift in a raw Xcode project, instead of Apple's default package integration.

Tuist has long had a custom integration of Swift Package Manager dependencies where instead of using the default Xcode integration, which has a myriad of issues, we convert the SPM projects to regular Xcode projects.

For defining the list of dependencies, we've used a Dependencies.swift manifest but using a custom manifest file broke tools like dependabot. So, for Tuist 4 we're fully moving to Package.swift.

For users, to be able to edit Package.swift as any other Tuist manifest, we wanted to include it in the project generated by the tuist edit command.

The how #

Let's start with the first naive approach – simply including the following Package.swift in an Xcode project:

// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "PackageName",
    dependencies: [
        .package(url: "https://github.com/Alamofire/Alamofire", from: "5.8.0"),
    ]
)

If you do that, you will get the following error: No such module 'PackageDescription'. The PackageDescription framework is not included in Xcode projects by default but is inserted by Xcode as part of the default SPM integration.

But where can we find PackageDescription? We can use the swift package describe --verbose command which evaluates the Package.swift – and to do that, it needs to include the PackageDescription framework.

A part of that output is, indeed, -L /Applications/Xcode-15.1.0.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/pm/ManifestAPI -lPackageDescription where -L is defined as "Add directory to library link search path" and -l as "Specifies a library which should be linked against". So, the Swift compiler knows it should try to search for PackageDescription and it also adds the ManifestAPI path to its search paths. The ManifestAPI then contains the PackageDescription itself (more specifically, PackageDescription.swiftmodule and libPackageDescription.dylib).

We can now leverage this in our raw Xcode project by adding the ManifestAPI path to the FRAMEWORK_SEARCH_PATHS and voila, the project now builds ✨

Swift tools version #

But that's not all. We have successfully added the PackageDescription framework, however, we currently don't respect the swift-tools-version defined at the top of the Package.swift file. Our raw Xcode project treats Package.swift as any other Swift file, so how could it?

Going back to the output of swift package describe --verbose, we will also find the following Swift flag: -package-description-version 5.9.0. For our raw Xcode project, we can simply include this Swift flag in OTHER_SWIFT_FLAGS and that's it! If we need to get the version number dynamically (as we do in tuist), we can use the swift package tools-version command.

Afterwards, building the Package.swift will succeed and use the right Swift tools version. If you are interested in looking at the actual PR we did for tuist edit, you can find it here.