DEV Community

loading...
Cover image for React Native How Speed Up iOS Build 4x using cache Pods

React Native How Speed Up iOS Build 4x using cache Pods

retyui profile image David Narbutovich ・4 min read

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
Enter fullscreen mode Exit fullscreen mode

πŸ—„οΈ Cache sizes:

node_modules: ~852 MB
pods: ~115 MB
pods derived data: ~1258 MB
Enter fullscreen mode Exit fullscreen mode

⏲️ Build time:

Build time ios app

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 (see Step 4), but add a new options derived_data_path: .... Then a little bit clean this folder, removing a non Pods files. (see Step 5)

  • If cache successfully restored need to update a Build Phase [CP] Copy Pods Resources. (see Step 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 (see Step 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 crucial xcargs to help a linker searching Pods at linking time. (see Step 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,
         # ... 
      )
Enter fullscreen mode Exit fullscreen mode

if you have any question I am glad to discuss them in the comments!

Β© MurAmur

Discussion (4)

Forem Open with the Forem app