Skip to content

[build-tools] - support Xcode DerivedData pod products caching for iOS builds#3491

Open
AbbanMustafa wants to merge 11 commits intomainfrom
mus/derdata-cache
Open

[build-tools] - support Xcode DerivedData pod products caching for iOS builds#3491
AbbanMustafa wants to merge 11 commits intomainfrom
mus/derdata-cache

Conversation

@AbbanMustafa
Copy link
Contributor

@AbbanMustafa AbbanMustafa commented Mar 10, 2026

Why

iOS builds currently recompile all CocoaPods from scratch on every build. Unlike local development where Xcode's DerivedData persists compiled products between builds, CI builds start with a clean DerivedData every time.

The xcodebuild step spends the majority of its time rebuilding pod dependencies that haven't changed between builds. This adds Xcode build product caching to skip that redundant recompilation. Gated behind EAS_XCODE_CACHE=1.

How

Cache pre-built pod products (.a l ibraries, .modulemap files, headers) from DerivedData after a successful build, keyed by lockfile hash. On subsequent builds, restore the cached products and patch the Xcodeproject to skip pod recompilation.

Restore (RESTORE_CACHE): Extract cached products to /tmp/pods-cache/ and rewrite absolute paths in .modulemap files. Products live outside DerivedData because xcodebuild archive wipes ArchiveIntermediates/ at the start of each run.

Patch (INSTALL_PODS, after pod install): Placing pre-built products on disk isn't enough. Xcode still sees pod targets in Pods.xcodeproj and will rebuild them. Provide a Ruby script removes non-umbrella pod targets from
the project entirely, then patches PODS_CONFIGURATION_BUILD_DIR in the xcconfig files to point at /tmp/pods-cache/ so the linker resolves to cached .a files instead of empty DerivedData.

Save (SAVE_CACHE): Locate build products in ArchiveIntermediates/.../BuildProductsPath/Release-* (archive) or Build/Products/Release-* (simulator) and upload. derived_data_path is set to ./build in the Gymfile so
products land in a known location. Each save replaces the entire cache, so stale products from removed pods are naturally pruned.

Cache key is ios-xcode-cache-{device|sim}-{lockfile_hash} — separate keys for device (Release-iphoneos, arm64) and simulator (Debug-iphonesimulator, x86_64/arm64) builds to prevent architecture mismatches.

Performance: Archive builds go from 🙌~3 min to ~30 sec on cache hit (6x speedup)🙌

Test Plan

  • Ran iOS archive build with XCODE_CACHE=1 — first build misses, saves cache after completion
  • Ran second build — cache hit, pod targets removed from xcodeproj, xcodebuild skips recompilation
  • Ran simulator build — uses separate sim cache key, no architecture mismatch with device cache
  • Ran without XCODE_CACHE=1 — all cache phases no-op
  • Verified modulemap paths are correctly rewritten to /tmp/pods-cache/
Screenshot 2026-03-17 at 1 26 59 AM

@AbbanMustafa AbbanMustafa added the no changelog PR that doesn't require a changelog entry label Mar 16, 2026
@AbbanMustafa AbbanMustafa changed the title derived data cache [build-tools] - support Xcode DerivedData pod products caching for iOS builds Mar 17, 2026
@AbbanMustafa AbbanMustafa marked this pull request as ready for review March 17, 2026 07:03
@github-actions
Copy link

Subscribed to pull request

File Patterns Mentions
**/* @douglowder

Generated by CodeMention

@codecov
Copy link

codecov bot commented Mar 17, 2026

Codecov Report

❌ Patch coverage is 12.75168% with 130 lines in your changes missing coverage. Please review.
✅ Project coverage is 53.44%. Comparing base (f3fd05d) to head (fef31f7).
⚠️ Report is 17 commits behind head on main.

Files with missing lines Patch % Lines
...ild-tools/src/steps/functions/restoreBuildCache.ts 10.98% 73 Missing ⚠️
.../build-tools/src/steps/functions/saveBuildCache.ts 7.70% 48 Missing ⚠️
packages/build-tools/src/utils/xcodeCacheKey.ts 42.86% 8 Missing ⚠️
...ages/build-tools/src/steps/functionGroups/build.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3491      +/-   ##
==========================================
+ Coverage   53.39%   53.44%   +0.06%     
==========================================
  Files         813      816       +3     
  Lines       34438    34783     +345     
  Branches     7151     7217      +66     
==========================================
+ Hits        18384    18587     +203     
- Misses      15972    16115     +143     
+ Partials       82       81       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link

⏩ The changelog entry check has been skipped since the "no changelog" label is present.

@AbbanMustafa
Copy link
Contributor Author

AbbanMustafa commented Mar 18, 2026

Screenshot 2026-03-18 at 1 12 10 AM

@AbbanMustafa AbbanMustafa requested a review from sjchmiela March 18, 2026 06:48
Copy link
Contributor

@sjchmiela sjchmiela left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you see / try:

Can you please add comments explaining the logic / subjects / actions the code is doing? There's a lot to understand here and magic prefixes and everything and making it clearer will also maybe make it clearer to review


export async function generateXcodeCacheKeyAsync(
workingDirectory: string,
simulator?: boolean
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is simulator cache helpful for device build and/or vice versa? It seems it's not based on PR descirption. Let's make the parameter required and an inline enumerator "simulator" | "device" or sth like that.


import { findPackagerRootDir } from './packageManager';

export const XCODE_CACHE_KEY_PREFIX = 'ios-xcode-cache-';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not export it like that if it should never restore "just" ios-xcode-cache- cache?

logger.info(`Rewrote paths in ${patchCount} modulemap files`);
}

const PATCH_RUBY_SCRIPT = `
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this achievable using Expo config plugins code?

@sjchmiela
Copy link
Contributor

I'll also let myself add @lukmccall as a reviewer — he has much more experience in the area.

I also wonder how does https://github.com/software-mansion/rnrepo do it for the recently added iOS support. If this pull request's main thing is to cache Pod artifacts and they do build Pod artifacts for reuse…

@sjchmiela sjchmiela requested a review from lukmccall March 18, 2026 15:40
Copy link

@lukmccall lukmccall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure that we're not leaking any personal information by caching DerivedData? I'm not sure if this directory was intended to be cached.

const variant = simulator ? 'sim' : 'device';

try {
return `${XCODE_CACHE_KEY_PREFIX}${variant}-${hashFiles([lockPath])}`;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lockfile probably doesn't change if we change the XCode version. So, we probably need to include it in the key.

const PATCH_RUBY_SCRIPT = `
require 'xcodeproj'

project = Xcodeproj::Project.open('ios/Pods/Pods.xcodeproj')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider searching for the xcodeproj rather than relying on the hardcoded path.

@sjchmiela
Copy link
Contributor

Are we sure that we're not leaking any personal information by caching DerivedData? I'm not sure if this directory was intended to be cached.

Oh so this is not going to be public, this cache is scoped per-app

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no changelog PR that doesn't require a changelog entry

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants