Build Our Own iOS Package Registry With Carthage And Rome
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:
- Carthage - v0.35.0
- Rome - https://github.com/tmspzz/Rome
- AWS S3 Storage
- Some Bash Scripts — upload.sh, download.sh, run.sh
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.
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')
ficache_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
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 Romefilesh 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')
ficache_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-buildselse
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.sh
does 3 things:
- Copy
Romefile
, download.sh from submodule folder to project root. - Run download script, calling
download.sh
- 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.
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
andCartfile
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.