Untangling iOS Package Management

Andrew
Specto
Published in
4 min readFeb 10, 2021

Modern iOS app development involves lots of dependencies, and there are several package managers to choose from: CocoaPods, Carthage and the newest entrant, Apple’s own SPM. At Specto, we want our app performance monitoring SDK to support all of them, in order to accommodate as many developers and teams as possible. We built a backend to serve the various required specs and payloads, giving us full control over delivery of our SDK and enabling us to adapt to changes in the ecosystem.

Our backend serves specs for each package manager, for each build type we deploy.

A Tale of Three Package Managers

Each package manager has unique requirements. CocoaPods, by the standard route, uses a central spec repository/CDN. SPM is decentralized, like Carthage, but always expects a Package.swift manifest. Normally, all three are used to fetch and compile open source code, but, like us, you can also deliver precompiled binaries.

Below, we’ll look at how we support each tool’s requirements, and some unique aspects of each. Because we distribute a precompiled executable binary from a closed source repository, we’re on a path less trodden–we’ll note any sharp edges we found.

Carthage

The simplest Cartfile declares the location of an open source codebase to download and compile:

github "ReactiveCocoa/ReactiveCocoa"

To get our precompiled binary, one must similarly declare the location of a binary project specification:

# get the latest release
binary "https://users.specto.dev/binary-releases/ios/sdk/release/carthage"
# or, for a pinned version:
binary "https://users.specto.dev/binary-releases/ios/sdk/release/carthage/0.1.1"

When carthage update hits this endpoint, we serve some JSON with the requested version and payload URL:

{
"0.1.1":"https://storage.googleapis.com/specto-sdk-release/ios/framework/prod/0.1.1/Specto.zip"
}

Often, this lives in a file at a set location to which subsequent releases are appended, but since we serve a dynamic version of it, it’s always a single entry.

One gotcha here is that the version numbers serving as JSON keys cannot have build numbers or metadata. We use semver, and this bit us during a beta test where we used the version number 0.1.0-RC1 (“version 0.1.0, release candidate 1”). Since we have different endpoints for alpha and beta builds, we don't need that information in the JSON they serve.

SPM

SPM, like Carthage, is decentralized, and for binary packages, requires a manifest file, which, uniquely amongst the three, declares the payload’s SHA256 checksum:

// swift-tools-version:5.3
import PackageDescription
let package = Package(
name: "Specto",
products: [.library(name: "Specto", targets: ["Specto"])],
targets: [
.binaryTarget(
name: "Specto",
url: "https://users.specto.dev/binary-releases/ios/sdk/release/xcframework/0.1.1.zip",
checksum: "2d32b56526e2dd4ffd866e9df1d071f2b8077d52f61c7e881e1cc87ec195c681"
),
]
)

The difference between Carthage and SPM is that you must host a manifest in a git repository. We can’t just serve a JSON or podspec file from a service endpoint like we do for Carthage and CocoaPods. One nice thing about Git is that it allows a nice organization of release types using branches: main (release), beta and alpha, something we built in our service to host alpha/beta builds for the other package managers. We also tag every release so that versions can be pinned in Xcode:

Pinning a Swift Package dependency to a particular Specto version. Teams can opt to use the latest versoin by specifying our ‘main’ branch. We can also use ‘beta’ or ‘alpha’ branches to test forthcoming releases.

We hit one other gotcha: Xcode expects to download a ZIP file from the URL in Package.swift, but if that URL doesn’t actually end with a .zip extension, an error is raised before even performing the request to see what's returned. Since the path in https://users.specto.dev/binary-releases/ios/sdk/release/xcframework is not a single file but rather a set of arguments to our service, we had to add logic to allow a .zip extension on the last argument.

CocoaPods

The simplest Podfile declares simply a dependency name:

pod 'SwiftyJSON'

pod install will search a set of central stores for a podspec by that name, which, among other things, describes where to find source files to compile. Alternatively, you can specify a URL to a podspec hosted anywhere:

# get the latest release
pod 'Specto', :podspec => 'https://users.specto.dev/binary-releases/ios/sdk/release/cocoapods'
# or, for a pinned version:
pod 'Specto', :podspec => 'https://users.specto.dev/binary-releases/ios/sdk/release/cocoapods/0.1.1'

That URL is another endpoint in our service, returning a generated podspec:

Pod::Spec.new do |spec|
spec.name = 'Specto'
spec.version = '0.1.1'
spec.source = { :http => "https://storage.googleapis.com/specto-sdk-release/ios/framework/prod/0.1.1/Specto.zip"}
spec.preserve_paths = ['*.{md,sh}', 'specto-upload-symbols']
# summary, description, platform etc
end

Our SDK isn’t the only thing developers need, though: we also ship an executable that uploads dSYMs to our sever. Developers using Carthage and SPM must download this separately, but CocoaPods is unique in that other files, like READMEs and executables, can be delivered right in the payload (as shown in the snippet above).

Go, Get Specto

We streamlined our SDK delivery with a Go service that vends binaries and specs for the various iOS package managers. Spec templates abstract away all the release metadata, so we can store the boilerplate in files that rarely change and inject versions and dates on each request to our endpoints. Latest releases are returned by default, but specific versions can be requested, too. In addition to pinned versions and alpha/beta builds, we can optionally target a dev variant of each build that has increased logging and debugging capabilities–we even have a sidecar debugging SDK.

Centralizing the processes and distilling the release information to a single source of truth has simplified meeting the various tools’ requirements. It makes our job easier by reducing duplication and noise in our repo’s files and commit history: no more commits just to update the same version numbers and dates in multiple spec files.

To try the Specto SDK in your app, visit https://specto.dev.

--

--

Andrew
Specto
Editor for

iOS @ Specto. Previously at Twitter/Crashlytics & Layer.