Build Our Own iOS Package Registry With Carthage And Rome

Keith Chan
GOGOX Technology
Published in
5 min readApr 24, 2020

iOS dependency management is painful, especially you have a fast rolling team and continuous integration. If using Cocoapods, as it is a built-from-sources tools, the build time is long if we run a clean build on CI server. SPM is a new player and it should be the future as libraries are adopting SPM still.

Carthage helps building prebuilt binaries. Xcode no need to build frameworks from source code. But the first time building costs time. Although Carthage provides way to download prebuilt binary from repository, it depends on repo owner to provide that. Moreover, the latest Carthage version still not respecting Swift ABI stability. Incompatibility Swift toolchain causes rebuild occurs on local machine. For CI server, Carthage basically needs to build everything from scratch if there is no caching.

Rome is the most popular Carthage cache tool. It is based on Carthage .version hashes to manage the cache of the prebuilt frameworks. Rome can cache frameworks on online storage like AWS S3, GCP Cloud. For CI server, before Carthage bootstrap, Rome downloads the cache from online storage. CI would skip the building process and hence reducing the build time.

How to build the iOS Package Registry for all our apps?

Our tools:

First of all, we need to have a repo to manage all the dependencies for all of our apps. This repository is mainly for building and uploading frameworks to S3.

ios-package-registry files

In the repo, we put all dependencies across all projects in our company in the Cartfile. Run carthage bootstrap --platform iOS .

After generate Cartfile.resolved, making Romefile with the Cartfile.resolved.

Run upload.sh to validate frameworks and upload onto S3.

### script contents# get cache_prefix
version=$(swift -version | egrep -o -e ' [0-9].[0-9].[0-9] ' | tr '.' '_' | awk '{$1=$1};1')
# sometimes swift version would omit patch versionif [ -z "$version" ]; then
version=$(swift -version | egrep -o -e ' [0-9].[0-9] ' | tr '.' '_' | awk '{$1=$1};1')
fi
cache_prefix="Swift_$version"# carthage bootstrapcarthage update --platform iOS --cache-builds# upload cacherome upload --platform iOS --concurrently --cache-prefix $cache_prefix

cache_prefix indicates the swift toolchain used for building frameworks. Our package registry stores frameworks built from different swift toolchains.

Package Registry Folder Structure on S3

|____ Swift_5_0_0
|
|____ Swift_5_2_2
|
|____ FrameworkA
| |
| |____ iOS
| | |
| | |____ FrameworkA.framework-1.0.0.zip
| | |
| | |____ FrameworkA.framework-1.0.2.zip
| | |
| | |____ xxx.bcsymbolmap-1.0.0.zip
| | |
| | |____ xxx.bcsymbolmap-1.0.2.zip
| |
| |____ FrameworkA.version-1.0.0
| |
| |____ FrameworkA.version-1.0.2
|
|____ FrameworkB
|
|____ FrameworkC
  • Top-level: swift toolchain versions
  • Second-level: Frameworks
  • Third-level: platform folders and .version files
  • Platform Folder: different versions of .framework.zip and .bcsymbolmap.zip
S3 Folder Hierarchy
Multiple versions of SDWebImage

Apply on our app projects

For each project, git submodule add the ios-package-registry repo as submodules.

In the project root, it should contains project specific Cartfile and Cartfile.resolved . The Cartfile should contain subset of dependencies of the Cartfile in ios-package-registry repo.

The missing Romefile for the app project would be copied from the submodule folder with run.sh.

sh ./xxx-ios-package-registry/run.sh###########################################################
### run.sh
BASEDIR=$(dirname "$0")# copy essential files
cp $BASEDIR/download.sh download.sh
cp $BASEDIR/Romefile Romefile
sh download.sh# remove files
rm download.sh
rm Romefile
###########################################################
### download.sh
# get cache_prefix
version=$(swift -version | egrep -o -e ' [0-9].[0-9].[0-9] ' | tr '.' '_' | awk '{$1=$1};1')
# sometimes swift version would omit patch versionif [ -z "$version" ]; then
version=$(swift -version | egrep -o -e ' [0-9].[0-9] ' | tr '.' '_' | awk '{$1=$1};1')
fi
cache_prefix="Swift_$version"# download cacherome download --platform iOS --concurrently --cache-prefix $cache_prefixif [ "$CI" = true ]; then# download and build missing frameworks
# always skip carthage update on CI
carthage bootstrap --platform iOS --cache-builds
else
echo "Running on local machine"
# update Cartfile.resolved only in local machine
carthage update --platform iOS --no-build
# download and build missing frameworks
carthage bootstrap --platform iOS --cache-builds
fi

The run.shdoes 3 things:

  1. Copy Romefile, download.sh from submodule folder to project root.
  2. Run download script, calling download.sh
  3. Delete Romefile, download.sh after the script end

Since the Romefile is the shared file among all projects, this approach ensures that the Romefile is always up-to-date.

2min — no need to build frameworks in CI server!!!

Hints

  • double check all Cartfile.resolved and make sure all versions are on S3.
  • always update frameworks in ios-package-registry repo.
  • can update Cartfile.resolved and Cartfile in app projects manually.
  • can share S3 credentials with the team, but depends on your company policy.
  • don’t add S3 credentials in the script.

Achievements

  • Reduce unnecessary build time on CI server as well as local machine.
  • Cache for multiple versions and multiple swift toolchains.
  • Build once and apply on multiple projects.
  • Decoupling Carthage with app projects. We can pre-build different versions of framework on ios-package-registry repo.

What’s Next?

We are waiting for Carthage to support XCFramework in its stable release. If frameworks can support it. We can omit the cache for different Swift toolchains as ABI stability is built-in with XCFramework.

--

--