From 28c1f7dcc2bd5b1dcab187d0d4e36415550b20be Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 23 Feb 2026 13:49:43 -0800 Subject: [PATCH 1/6] Strip debug symbols from iOS production binaries Add a shared script and Xcode Run Script build phases to both the HybridApp (Mobile-Expensify) and standalone (NewExpensify) projects that strip debug symbols from the app binary and embedded frameworks. This runs after dSYM generation and Sentry upload, preserving crash symbolication while reducing production IPA size by ~22MB (~11%). The script skips Debug configurations so local development is unaffected. Co-authored-by: Cursor --- ios/NewExpensify.xcodeproj/project.pbxproj | 20 ++++++++++++ scripts/strip-ios-debug-symbols.sh | 37 ++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100755 scripts/strip-ios-debug-symbols.sh diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index 36ca04fe34a9b..a867cd2cfb05d 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -375,6 +375,7 @@ 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, 498240F82C49553900C15857 /* Run Fullstory Asset Uploader */, 5124824122A346BD617AD428 /* [CP] Embed Pods Frameworks */, + 87174A5CC3CF40CC94B76870 /* Strip Debug Symbols */, D687A4E020266C9380167930 /* [CP] Copy Pods Resources */, 072FAC2AE1685E6A5862C00A /* [CP-User] [RNFB] Core Configuration */, ); @@ -585,6 +586,25 @@ shellPath = /bin/sh; shellScript = "if [ \"$CONFIGURATION\" != \"DebugDevelopment\" ]; then\n \"${PODS_ROOT}/FullStory/tools/FullStoryCommandLine\" \"${CONFIGURATION_BUILD_DIR}/${WRAPPER_NAME}\"\nelse\n echo \"Skipping FullStory Asset Uploader phase for DebugDevelopment scheme.\"\nfi\n"; }; + 87174A5CC3CF40CC94B76870 /* Strip Debug Symbols */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${EXECUTABLE_NAME}", + ); + name = "Strip Debug Symbols"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "bash \"${PROJECT_DIR}/../scripts/strip-ios-debug-symbols.sh\"\n"; + }; 5124824122A346BD617AD428 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/scripts/strip-ios-debug-symbols.sh b/scripts/strip-ios-debug-symbols.sh new file mode 100755 index 0000000000000..2bab98a27b4b0 --- /dev/null +++ b/scripts/strip-ios-debug-symbols.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# +# Strips debug symbols from the app binary and embedded frameworks for non-Debug builds. +# This reduces the IPA size by ~22MB without affecting crash symbolication, because Xcode +# generates dSYMs from the unstripped binary before this phase runs. +# +# Expected Xcode environment variables: +# CONFIGURATION, BUILT_PRODUCTS_DIR, EXECUTABLE_FOLDER_PATH, EXECUTABLE_NAME + +set -e + +case "$CONFIGURATION" in + Debug*) + echo "Skipping symbol stripping for $CONFIGURATION build." + exit 0 + ;; +esac + +APP_DIR_PATH="${BUILT_PRODUCTS_DIR}/${EXECUTABLE_FOLDER_PATH}" + +echo "Stripping main binary: ${APP_DIR_PATH}/${EXECUTABLE_NAME}" +strip -rSTx "${APP_DIR_PATH}/${EXECUTABLE_NAME}" + +APP_FRAMEWORKS_DIR="${APP_DIR_PATH}/Frameworks" +if [ -d "$APP_FRAMEWORKS_DIR" ]; then + find "$APP_FRAMEWORKS_DIR" -maxdepth 2 -mindepth 2 -type f -perm -111 | while read -r framework_binary; do + # Skip Apple-signed frameworks (modifying them would invalidate the signature) + if codesign -v -R="anchor apple" "$framework_binary" &> /dev/null; then + echo "Skipping Apple-signed framework: $framework_binary" + continue + fi + echo "Stripping framework: $framework_binary" + strip -rSTx "$framework_binary" + done +fi + +echo "Symbol stripping complete." From f36b9a642a70cbbc6c79bd90f1cf94221cd8b2e7 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 26 Feb 2026 16:17:43 -0800 Subject: [PATCH 2/6] Fix dependency cycle by removing dSYM inputPath from Strip Debug Symbols phase The dSYM inputPath created a cycle in Xcode's build graph. Instead, use alwaysOutOfDate=1 to run the phase every build, matching how the Sentry upload phases work. Made-with: Cursor --- ios/NewExpensify.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/NewExpensify.xcodeproj/project.pbxproj b/ios/NewExpensify.xcodeproj/project.pbxproj index a867cd2cfb05d..36d9085e496ed 100644 --- a/ios/NewExpensify.xcodeproj/project.pbxproj +++ b/ios/NewExpensify.xcodeproj/project.pbxproj @@ -588,13 +588,13 @@ }; 87174A5CC3CF40CC94B76870 /* Strip Debug Symbols */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( - "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${EXECUTABLE_NAME}", ); name = "Strip Debug Symbols"; outputFileListPaths = ( From 7bac0eabef4176ebeae3cda2a8ba7a0f1775294e Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 26 Feb 2026 16:50:37 -0800 Subject: [PATCH 3/6] Fix: use -exec pattern for framework stripping and add diagnostics Switch to the Sentry-recommended -exec bash -c pattern for the find command to match the proven approach. Also add a diagnostic message when the Frameworks directory is not found. Made-with: Cursor --- scripts/strip-ios-debug-symbols.sh | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/scripts/strip-ios-debug-symbols.sh b/scripts/strip-ios-debug-symbols.sh index 2bab98a27b4b0..6f5f081b47225 100755 --- a/scripts/strip-ios-debug-symbols.sh +++ b/scripts/strip-ios-debug-symbols.sh @@ -23,15 +23,11 @@ strip -rSTx "${APP_DIR_PATH}/${EXECUTABLE_NAME}" APP_FRAMEWORKS_DIR="${APP_DIR_PATH}/Frameworks" if [ -d "$APP_FRAMEWORKS_DIR" ]; then - find "$APP_FRAMEWORKS_DIR" -maxdepth 2 -mindepth 2 -type f -perm -111 | while read -r framework_binary; do - # Skip Apple-signed frameworks (modifying them would invalidate the signature) - if codesign -v -R="anchor apple" "$framework_binary" &> /dev/null; then - echo "Skipping Apple-signed framework: $framework_binary" - continue - fi - echo "Stripping framework: $framework_binary" - strip -rSTx "$framework_binary" - done + find "$APP_FRAMEWORKS_DIR" -maxdepth 2 -mindepth 2 -type f -perm -111 -exec bash -c ' + codesign -v -R="anchor apple" "$1" &> /dev/null || strip -rSTx "$1" + ' _ {} \; +else + echo "No Frameworks directory found at $APP_FRAMEWORKS_DIR" fi echo "Symbol stripping complete." From c590cb7f52027195f6d6a12bbb52a9364d5a0f8b Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 26 Feb 2026 17:23:29 -0800 Subject: [PATCH 4/6] Fix: find framework binaries by name convention, not permission bits XCFrameworks (like Onfido) extracted from ZIPs may lack the execute permission bits that the previous -perm -111 check required. Instead, iterate over .framework directories and locate binaries by the standard naming convention (binary name matches framework name without extension). Made-with: Cursor --- scripts/strip-ios-debug-symbols.sh | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/scripts/strip-ios-debug-symbols.sh b/scripts/strip-ios-debug-symbols.sh index 6f5f081b47225..a5ff729dea5b3 100755 --- a/scripts/strip-ios-debug-symbols.sh +++ b/scripts/strip-ios-debug-symbols.sh @@ -23,9 +23,19 @@ strip -rSTx "${APP_DIR_PATH}/${EXECUTABLE_NAME}" APP_FRAMEWORKS_DIR="${APP_DIR_PATH}/Frameworks" if [ -d "$APP_FRAMEWORKS_DIR" ]; then - find "$APP_FRAMEWORKS_DIR" -maxdepth 2 -mindepth 2 -type f -perm -111 -exec bash -c ' - codesign -v -R="anchor apple" "$1" &> /dev/null || strip -rSTx "$1" - ' _ {} \; + find "$APP_FRAMEWORKS_DIR" -name "*.framework" -maxdepth 1 -type d | while read -r framework_dir; do + framework_name=$(basename "$framework_dir" .framework) + framework_binary="$framework_dir/$framework_name" + if [ ! -f "$framework_binary" ]; then + continue + fi + if codesign -v -R="anchor apple" "$framework_binary" &> /dev/null; then + echo "Skipping Apple-signed: $framework_name" + continue + fi + echo "Stripping framework: $framework_name" + strip -rSTx "$framework_binary" + done else echo "No Frameworks directory found at $APP_FRAMEWORKS_DIR" fi From 27f8a323384287254dd4dd33c808a78255e72968 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 26 Feb 2026 18:17:39 -0800 Subject: [PATCH 5/6] Add 'codesign' to cspell dictionary Made-with: Cursor --- cspell.json | 1 + 1 file changed, 1 insertion(+) diff --git a/cspell.json b/cspell.json index 03a577500ee48..c3427217a9476 100644 --- a/cspell.json +++ b/cspell.json @@ -138,6 +138,7 @@ "cocoapods", "Codat", "codegen", + "codesign", "codeshare", "Codice", "Combustors", From 2650aad8f9b318822c9676690f1568549d185fe6 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 26 Feb 2026 21:10:32 -0800 Subject: [PATCH 6/6] Re-sign frameworks after stripping to restore code signatures Stripping invalidates the embedded code signature. While Xcode's final code signing step handles this, explicitly re-signing each framework after stripping is more robust and doesn't rely on implicit behavior. Made-with: Cursor --- scripts/strip-ios-debug-symbols.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/strip-ios-debug-symbols.sh b/scripts/strip-ios-debug-symbols.sh index a5ff729dea5b3..08baac2d0bfe9 100755 --- a/scripts/strip-ios-debug-symbols.sh +++ b/scripts/strip-ios-debug-symbols.sh @@ -5,7 +5,8 @@ # generates dSYMs from the unstripped binary before this phase runs. # # Expected Xcode environment variables: -# CONFIGURATION, BUILT_PRODUCTS_DIR, EXECUTABLE_FOLDER_PATH, EXECUTABLE_NAME +# CONFIGURATION, BUILT_PRODUCTS_DIR, EXECUTABLE_FOLDER_PATH, EXECUTABLE_NAME, +# EXPANDED_CODE_SIGN_IDENTITY set -e @@ -35,6 +36,7 @@ if [ -d "$APP_FRAMEWORKS_DIR" ]; then fi echo "Stripping framework: $framework_name" strip -rSTx "$framework_binary" + codesign --force --sign "${EXPANDED_CODE_SIGN_IDENTITY:--}" --preserve-metadata=identifier,entitlements "$framework_dir" done else echo "No Frameworks directory found at $APP_FRAMEWORKS_DIR"