Swift Package Manager

Swift Package Manager is the dependency manager built into Swift and Xcode. Many iOS and macOS SDKs are distributed as Swift packages, and some of them are no longer published as CocoaPods. Defold does not use Xcode when building the engine, but the build server uses Xcode to download and build the Swift packages an extension depends on. The result is then linked into the engine the same way as CocoaPods dependencies.

CocoaPods and Swift packages can be used in the same project, and even in the same extension.

Declaring dependencies

Native extensions can include a SwiftPackages.json file in the manifests/ios and manifests/osx folders to specify the Swift packages the extension depends on. Example:

{
  "platform": "ios",
  "minVersion": "15.0",
  "packages": [
    {
      "url": "https://github.com/firebase/firebase-ios-sdk.git",
      "version": "12.16.0",
      "products": ["FirebaseCore", "FirebaseInstallations"]
    }
  ]
}

The file is plain JSON. You do not write a Package.swift file yourself, the build server generates one from the SwiftPackages.json files of all extensions.

The file contains the following fields:

The build server collects the SwiftPackages.json files from all the extensions and uses these to resolve all dependencies, including the dependencies of the packages themselves, and includes them when building the native code.

Using packages from extension code

The headers of the products are available to the source code of the extension. Use them from Objective-C++ (.mm) source files. This example starts the Sentry SDK when the application starts:

manifests/ios/SwiftPackages.json:

{
  "platform": "ios",
  "minVersion": "15.0",
  "packages": [
    {
      "url": "https://github.com/getsentry/sentry-cocoa.git",
      "from": "9.0.0",
      "products": ["Sentry"]
    }
  ]
}

src/myextension.mm:

#include <dmsdk/sdk.h>

#if defined(DM_PLATFORM_IOS)
#import <UIKit/UIKit.h>
#import <Sentry/Sentry.h>
#import <Sentry/Sentry-Swift.h>
#endif

static dmExtension::Result AppInitializeMyExtension(dmExtension::AppParams* params)
{
#if defined(DM_PLATFORM_IOS)
    [SentrySDK startWithConfigureOptions:^(SentryOptions* options) {
        options.dsn = @"https://examplePublicKey@o0.ingest.sentry.io/0";
    }];
#endif
    return dmExtension::RESULT_OK;
}

Dependencies of several extensions

When several extensions in a project use Swift packages, all their SwiftPackages.json files for the platform are combined into a single package graph:

A project can also mix extensions using CocoaPods and extensions using Swift packages. Both are resolved and linked with the engine. The same SDK must however not come from both: if one extension gets Firebase from a Podfile and another one from a SwiftPackages.json, the engine fails to link with duplicate symbol errors. Extensions that share an SDK should use the same dependency manager.

Locking package versions

When the build succeeds, the build server returns a Package.resolved file listing the exact version and commit of every package in the graph, including the dependencies of the packages. It is placed in the build folder of the project, for example build/arm64-ios/Package.resolved.

Copy this file into the extension, next to SwiftPackages.json (for example manifests/ios/Package.resolved), to lock the package versions. The build server then uses the locked versions as long as they match the requirements in SwiftPackages.json. It also makes the build faster: without a Package.resolved file the build server discovers the dependencies of the packages step by step, which can take a long time for packages with many dependencies.

If several extensions include a Package.resolved file for the same platform, only one of them is used.

Static and dynamic linking

By default all Swift packages are combined into one static library which is linked into the engine executable.

Some SDKs cannot be combined into a static library, and the engine then fails to link with duplicate symbol errors. In this case set "wrapperType": "dynamic" in SwiftPackages.json. The packages are then built into a dynamic framework, which is embedded in the application bundle.

Resources and privacy manifests

Resource bundles and privacy manifests (PrivacyInfo.xcprivacy) of the packages are included in the application bundle in the same way as for CocoaPods dependencies.

Migrating from CocoaPods

Many SDKs are published both as a pod and as a Swift package. To move an extension from a Podfile to a SwiftPackages.json file:

  1. Find the git repository of the Swift package in the documentation of the SDK. It is often the same repository as the pod.
  2. Find the product names in the Package.swift file of the package. Pod names and product names are often different.
  3. Replace the Podfile with a SwiftPackages.json file. The platform :ios version of the Podfile becomes minVersion.
  4. Make sure the #import lines of the extension still match the module names of the products.

For example, this Podfile:

platform :ios, '12.0'
pod 'FBSDKCoreKit', '17.0.0'
pod 'FBSDKLoginKit', '17.0.0'
pod 'FBSDKShareKit', '17.0.0'
pod 'FBSDKGamingServicesKit', '17.0.0'

becomes this SwiftPackages.json:

{
  "platform": "ios",
  "minVersion": "12.0",
  "packages": [
    {
      "url": "https://github.com/facebook/facebook-ios-sdk.git",
      "version": "17.0.0",
      "products": ["FacebookCore", "FacebookLogin", "FacebookShare", "FacebookGamingServices"]
    }
  ]
}

In this case the modules keep their names, so #import works with both.

Limitations

Troubleshooting

'Module/Header.h' file not found or undefined symbols
Check that the products in SwiftPackages.json are product names from Package.swift and that the header belongs to one of them. A build server that does not support Swift Package Manager ignores SwiftPackages.json, and the extension then fails to compile or link in the same way.
The Swift package graph did not resolve within the ... time budget
The packages have too many dependencies to be discovered without a lock file. Add a Package.resolved file next to SwiftPackages.json as described in Locking package versions. Since the build did not complete, create the file locally, for example by adding the same packages to an Xcode project. Xcode stores the file in .xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved.

Local build server

Swift packages are built by the macOS build server. To use Swift packages with a local build server, it needs a full Xcode installation (the Command Line Tools alone are not enough), XcodeGen, and Swift Package Manager support enabled with the extender.spm.enabled setting. Refer to the build server documentation on GitHub for more information.