I am that person who hates waiting ⌛ and seems I am lucky that decided to work with iOS apps 🙃 (sarcasm)
But anyway I think you should know that pain 😞 when building an iOS app on CI
🖥️ Lets have look our setup (GitHub Actions):
OS: macOS 10.15.7
CPU: (4) x64 Intel(R) Xeon(R) CPU E5-1650 v2 @ 3.50GHz
Memory: 8.11 GB / 12.00 GB
🗄️ Cache sizes:
node_modules: ~852 MB
pods: ~115 MB
pods derived data: ~1258 MB
⏲️ Build time:
So how you can see I save 33m 08s😲🎉💃!
I hope results impress you and you want to how to get these results? 😉
I have read 📖 a lot of articles that describe different approaches:
1) ccache
2) Rome tool
3) cocoapods-binary-cache - work strangely for me, built only 4 dependencies that no effect on build speed.
4) cocoapods-binary - required to use use_frameworks!
I noticed that that usually changes made on a js side, but native code changes rarely. And a big part of build time takes building native code.
So the main idea was to cache Pods build result until ios/Podfile.lock
changes
If you use GitHub Actions, your pipeline will be looks like that:
jobs:
build_ios:
runs-on: macos-latest
steps:
# Checkout repo, Install deps (node.js, cocacpods, ruby gems) ...
- uses: actions/cache@master
with:
# Path to Derived Data
path: .local_derived_data
# Restore cache by Podfile.lock hashsum
key: ${{ runner.os }}-pods-derived-data-${{ hashFiles('**/Podfile.lock') }}
# Run build
Also I use a fastlane tool to run build scripts. So code without optimizations looks like that:
# Fastfile
platform :ios do
desc "Build iOS"
lane :build do
# Code sign ...
gym(
scheme: "MyApp",
workspace: "./ios/MyApp.xcworkspace",
export_method: "ad-hoc",
configuration: "Release",
clean: true
)
# Publish to firebase...
end
end
Nothing special, right? And now version with optimization:
platform :ios do
desc "Build iOS"
lane :build do
scheme = "MyApp"
build_configuration = "Release"
# !!! Path to the folder that you will cache on CI !!!
ios_derived_data_path = File.expand_path("../.local_derived_data")
cache_folder = File.expand_path("#{ios_derived_data_path}/Build/Intermediates.noindex/ArchiveIntermediates/#{scheme}/BuildProductsPath/#{build_configuration}-iphoneos")
# Code sign ...
# Step 0) Check if cache exists
if(File.exist?(cache_folder))
# Step 1) Apply a fix of "Copy Pods Resources" Build Phase
# Before:
# "${PODS_ROOT}/Target Support Files/Pods-MyApp/Pods-MyApp-resources.sh"
#
# After:
# BUILT_PRODUCTS_DIR=/a/b/c "${PODS_ROOT}/Target Support Files/Pods-MyApp/Pods-MyApp-resources.sh"
fastlane_require 'xcodeproj'
project = Xcodeproj::Project.open("../ios/MyApp.xcodeproj")
target = project.targets.select { |target| target.name == 'MyApp' }.first
phase = target.shell_script_build_phases.select { |phase| phase.name && phase.name.include?('Copy Pods Resources') }.first
if (!phase.shell_script.start_with?('BUILT_PRODUCTS_DIR'))
phase.shell_script = "BUILT_PRODUCTS_DIR=#{cache_folder} #{phase.shell_script}"
project.save()
end
# Step 2) Build only .xcodeproj
gym(
clean: false,
project: './ios/MyApp.xcodeproj',
scheme: scheme,
configuration: build_configuration,
export_method: "ad-hoc",
destination: 'generic/platform=iOS',
export_options: {
compileBitcode: false,
uploadBitcode: false,
uploadSymbols: false
},
xcargs: [
# Step 3) Provide paths where xcode can't find pods binaries
"PODS_CONFIGURATION_BUILD_DIR=#{cache_folder}",
"FRAMEWORK_SEARCH_PATHS='#{cache_folder} $(inherited)'",
"LIBRARY_SEARCH_PATHS='#{cache_folder} $(inherited)'",
"SWIFT_INCLUDE_PATHS=#{cache_folder}"
].join(" ")
)
else
# Step 4) Build full app .xcworkspace
gym(
scheme: "MyApp",
workspace: "./ios/MyApp.xcworkspace",
derived_data_path: ios_derived_data_path,
export_method: "ad-hoc",
configuration: build_configuration,
clean: true
)
# Step 5) Remove not a Pods binaries to reduce cache size
require 'fileutils';
dirs = [
File.expand_path("#{ios_derived_data_path}/info.plist"),
File.expand_path("#{ios_derived_data_path}/Logs"),
File.expand_path("#{ios_derived_data_path}/SourcePackages"),
File.expand_path("#{ios_derived_data_path}/ModuleCache.noindex"),
File.expand_path("#{ios_derived_data_path}/Build/Intermediates.noindex/ArchiveIntermediates/MyApp/IntermediateBuildFilesPath/MyApp.build"),
File.expand_path("#{ios_derived_data_path}/Build/Intermediates.noindex/ArchiveIntermediates/MyApp/IntermediateBuildFilesPath/XCBuildData"),
File.expand_path("#{ios_derived_data_path}/Build/Intermediates.noindex/ArchiveIntermediates/MyApp/BuildProductsPath/SwiftSupport"),
File.expand_path("#{ios_derived_data_path}/Build/Intermediates.noindex/ArchiveIntermediates/MyApp/PrecompiledHeaders")
]
dirs.each { |dir| FileUtils.rm_rf(dir) }
end
# Publish to firebase...
end
end
Some notes to understand what happens:
First of all, I check the cache exist? (see
Step 0
)If it not exists I will invoke
gym()
with default options (seeStep 4
), but add a new optionsderived_data_path: ...
. Then a little bit clean this folder, removing a non Pods files. (seeStep 5
)If cache successfully restored need to update a Build Phase
[CP] Copy Pods Resources
. (seeStep 1
)
[CP] Copy Pods Resources
is run script that CocoaPods automatically adds to your project. It takes care of copying pod resources to the proper directory so that they'll be part of the final archive.
- Then I can invoke
gym()
but it has a different configuration (seeStep 2
). At this time I will build not whole a workspace just project. As I as build the App project, I also need to provide some crucialxcargs
to help a linker searching Pods at linking time. (seeStep 3
)
gym(
- workspace: "./ios/MyApp.xcworkspace",
+ project: './ios/MyApp.xcodeproj',
+ xcargs: [
+ "PODS_CONFIGURATION_BUILD_DIR=#{cache_folder}",
+ "FRAMEWORK_SEARCH_PATHS='#{cache_folder} $(inherited)'",
+ "LIBRARY_SEARCH_PATHS='#{cache_folder} $(inherited)'",
+ "SWIFT_INCLUDE_PATHS=#{cache_folder}"
+ ].join(" ")
- clean: true,
+ clean: false,
# ...
)
Another intriguing strategy to shorten build times:
- https://www.uglydirtylittlestrawberry.co.uk/posts/react-native-ios-build-and-inject-bundle/
- https://reactnative.dev/docs/build-speed
- https://github.com/mikehardy/buildcache-action
if you have any question I am glad to discuss them in the comments!
© MurAmur
Top comments (7)
Thank you for this post. Implementing this right now on GitLab CI. Not using fastlane, so without
gym
(xcodebuild
is not as convenient, but 100% bearable). Got build time from 1h 30m to 38 minutes :yay:Can you show us the whole yaml file?
Really cool what you are doing, thanks! Do you do some consulting work ?
it depends, what exactly you need)
I created a demo project based on this blog post. See github.com/dirkpostma/react-native...
Also contains example CI script for Azure Pipelines, meant for inspiration
I saw that you did a presentation) Did you speak in public?
Not really public, I did it for about 10/15 colleagues, all React Native developers. They really liked it and some of them are going to implement Pods caching in their projects as well. So big thanks to you!