diff --git a/docs/ios-setup.md b/docs/ios-setup.md index fbc3f530..61692ae5 100644 --- a/docs/ios-setup.md +++ b/docs/ios-setup.md @@ -1,113 +1,95 @@ # iOS Setup Guide -## SPM Framework Embedding Required +## SPM Framework Signing (Physical Devices) -QuickCrypto uses Swift Package Manager (SPM) dependencies that must be manually embedded in your app bundle: +QuickCrypto uses OpenSSL via Swift Package Manager (SPM). On **physical iOS devices**, SPM frameworks require additional configuration to be properly embedded and code-signed in your app bundle. -- **OpenSSL 3.6+** (required) - For ML-DSA post-quantum cryptography support -- **Sodium** (optional) - For XSalsa20 cipher support via libsodium (when `SODIUM_ENABLED=1`) +> **Simulator builds work without this configuration.** This is only required for physical device deployment. -### Why is this needed? +### Quick Setup -CocoaPods doesn't automatically embed SPM frameworks into the final app bundle. Without this configuration, you'll encounter runtime errors: +Add to your `ios/Podfile`: -``` -dyld: Library not loaded: @rpath/OpenSSL.framework/OpenSSL +```ruby +# At the top of your Podfile +require_relative '../node_modules/react-native-quick-crypto/scripts/quickcrypto_spm_fix' + +target 'YourAppName' do + # ... your pods ... + + post_install do |installer| + react_native_post_install(installer) # if you have this + + # Fix QuickCrypto SPM framework signing for physical devices + quickcrypto_fix_spm_signing(installer) + end +end ``` -This is a temporary limitation of mixing CocoaPods + SPM. It will be resolved when React Native fully migrates to SPM (expected 2026). +Then run: -## Configuration +```bash +cd ios && pod install +``` + +### Why is this needed? -Add the following to your `ios/Podfile` inside the `post_install` hook: +When you try to install on a physical device without this fix, you'll see: -```ruby -post_install do |installer| - # ... your existing post_install code (react_native_post_install, etc.) ... - - # Embed SPM frameworks from QuickCrypto - main_project_path = File.join(installer.sandbox.root.parent, 'YourAppName.xcodeproj') - main_project = Xcodeproj::Project.open(main_project_path) - app_target = main_project.targets.find { |t| t.name == 'YourAppName' } - - if app_target - embed_phase_name = 'Embed SPM Frameworks (QuickCrypto)' - existing_phase = app_target.shell_script_build_phases.find { |p| p.name == embed_phase_name } - - unless existing_phase - phase = app_target.new_shell_script_build_phase(embed_phase_name) - phase.shell_script = <<~SCRIPT - mkdir -p "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}" - - # Embed OpenSSL.framework (required for ML-DSA) - if [ -d "${BUILT_PRODUCTS_DIR}/OpenSSL.framework" ]; then - rsync -av --delete "${BUILT_PRODUCTS_DIR}/OpenSSL.framework" "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/" - if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" ]; then - /usr/bin/codesign --force --sign "${EXPANDED_CODE_SIGN_IDENTITY}" --preserve-metadata=identifier,entitlements "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework" - fi - fi - - # Embed Sodium.framework (optional, if SODIUM_ENABLED=1) - if [ -d "${BUILT_PRODUCTS_DIR}/Sodium.framework" ]; then - rsync -av --delete "${BUILT_PRODUCTS_DIR}/Sodium.framework" "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/" - if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" ]; then - /usr/bin/codesign --force --sign "${EXPANDED_CODE_SIGN_IDENTITY}" --preserve-metadata=identifier,entitlements "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sodium.framework" - fi - fi - SCRIPT - - # Insert before the CocoaPods embed frameworks phase - embed_pods_phase = app_target.shell_script_build_phases.find { |p| p.name == '[CP] Embed Pods Frameworks' } - if embed_pods_phase - app_target.build_phases.move(phase, app_target.build_phases.index(embed_pods_phase)) - end - - main_project.save - end - end -end ``` +Failed to verify code signature of .../OpenSSL.framework : 0xe8008015 +``` + +This happens because: +1. OpenSSL is distributed as a pre-built, pre-signed xcframework via SPM +2. CocoaPods' `spm_dependency` adds it to the Pods project but doesn't embed it in your app +3. The framework must be re-signed with your app's code signing identity + +This is a known limitation of mixing CocoaPods + SPM. See [issue #857](https://github.com/margelo/react-native-quick-crypto/issues/857). -**Important:** Replace `YourAppName` with your actual Xcode target name (usually matches your app name). +### Multiple Targets -## Example +If you have multiple app targets, specify which one: -See the [example app's Podfile](../../example/ios/Podfile) for a complete working reference. +```ruby +quickcrypto_fix_spm_signing(installer, app_target_name: 'YourSpecificTarget') +``` ## Enabling libsodium (Optional) -To enable XSalsa20 cipher support, set the environment variable before installing pods: +For XSalsa20 cipher support, set the environment variable before your target: ```ruby -# At the top of your Podfile ENV['SODIUM_ENABLED'] = '1' -``` - -Then run: -```bash -cd ios && pod install +target 'YourAppName' do + # ... +end ``` ## Troubleshooting -### Error: "Library not loaded: @rpath/OpenSSL.framework/OpenSSL" +### Error: `0xe8008015` on physical device -This means the SPM frameworks aren't being embedded. Verify: +This is the code signing error. Make sure you've added `quickcrypto_fix_spm_signing(installer)` to your Podfile's `post_install` hook and run `pod install`. -1. The `post_install` hook is properly configured in your Podfile -2. You're using `use_frameworks! :linkage => :dynamic` (required for SPM dependencies) -3. Run `cd ios && pod install` after modifying the Podfile -4. Clean build folder in Xcode (Cmd+Shift+K) and rebuild +### Error: "Library not loaded: @rpath/OpenSSL.framework" -### Dynamic Frameworks Required +Same fix - the `quickcrypto_fix_spm_signing` function handles both embedding and signing. -QuickCrypto requires dynamic framework linking due to SPM dependencies. Add this to your Podfile: +### Build still fails after adding the fix -```ruby -use_frameworks! :linkage => :dynamic -``` +1. Clean build: `Cmd+Shift+K` in Xcode +2. Delete derived data: `rm -rf ~/Library/Developer/Xcode/DerivedData` +3. Run `pod install` again +4. Rebuild + +### "Could not find main Xcode project" warning + +The helper script couldn't find your `.xcodeproj` file. Use the `app_target_name` parameter or check that your project structure is standard. + +## The SPM Situation -## Future +Yes, this is unfortunate. CocoaPods is being deprecated, SPM is supposed to be the future, but SPM's handling of binary frameworks with CocoaPods is broken. The `spm_dependency` bridge in React Native doesn't properly handle framework embedding and code signing. -When React Native completes its migration to Swift Package Manager (expected 2026), this manual embedding step will no longer be necessary. SPM packages will be properly integrated by default. +This workaround will be unnecessary when React Native fully migrates to SPM (timeline unclear). diff --git a/example/ios/Podfile b/example/ios/Podfile index 9f6e94bc..c7221623 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,6 +1,9 @@ ENV['RCT_NEW_ARCH_ENABLED'] = '1' ENV['SODIUM_ENABLED'] = '1' +# Fix QuickCrypto SPM framework signing for physical devices +require_relative '../../packages/react-native-quick-crypto/scripts/quickcrypto_spm_fix' + # Resolve react_native_pods.rb with node to allow for hoisting require Pod::Executable.execute_command('node', ['-p', 'require.resolve( @@ -59,45 +62,7 @@ target 'QuickCryptoExample' do end end - # Embed SPM frameworks from QuickCrypto into the app bundle - # SPM frameworks added to Pods project need manual embedding - main_project_path = File.join(installer.sandbox.root.parent, 'QuickCryptoExample.xcodeproj') - main_project = Xcodeproj::Project.open(main_project_path) - app_target = main_project.targets.find { |t| t.name == 'QuickCryptoExample' } - - if app_target - embed_phase_name = 'Embed SPM Frameworks (QuickCrypto)' - existing_phase = app_target.shell_script_build_phases.find { |p| p.name == embed_phase_name } - - unless existing_phase - phase = app_target.new_shell_script_build_phase(embed_phase_name) - phase.shell_script = <<~SCRIPT - # Embed OpenSSL.framework from SPM build into app bundle - # SPM builds the framework to BUILT_PRODUCTS_DIR but doesn't embed it - OPENSSL_FRAMEWORK="${BUILT_PRODUCTS_DIR}/OpenSSL.framework" - - if [ -d "$OPENSSL_FRAMEWORK" ]; then - echo "Found OpenSSL.framework at $OPENSSL_FRAMEWORK" - rsync -av --delete "$OPENSSL_FRAMEWORK" "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/" - - if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" ] && [ "${CODE_SIGNING_REQUIRED:-}" != "NO" ]; then - /usr/bin/codesign --force --sign "${EXPANDED_CODE_SIGN_IDENTITY}" --preserve-metadata=identifier,entitlements "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework" - fi - echo "OpenSSL.framework embedded successfully" - else - echo "warning: OpenSSL.framework not found at $OPENSSL_FRAMEWORK" - fi - SCRIPT - - # Move it before the existing embed frameworks phase - embed_pods_phase = app_target.shell_script_build_phases.find { |p| p.name == '[CP] Embed Pods Frameworks' } - if embed_pods_phase - app_target.build_phases.move(phase, app_target.build_phases.index(embed_pods_phase)) - end - - main_project.save - Pod::UI.puts "[QuickCrypto] Added 'Embed SPM Frameworks (QuickCrypto)' build phase" - end - end + # Fix QuickCrypto SPM framework signing for physical devices + quickcrypto_fix_spm_signing(installer) end end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index fe6169c5..864bee05 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2845,6 +2845,6 @@ SPEC CHECKSUMS: SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Yoga: 11c9686a21e2cd82a094a723649d9f4507200fb0 -PODFILE CHECKSUM: a55db5270505ab83586da7672ed603a4ad498447 +PODFILE CHECKSUM: c53d893905e8bc54582367d8191b1d2814070820 COCOAPODS: 1.15.2 diff --git a/example/ios/QuickCryptoExample.xcodeproj/project.pbxproj b/example/ios/QuickCryptoExample.xcodeproj/project.pbxproj index 919f2521..742f1c14 100644 --- a/example/ios/QuickCryptoExample.xcodeproj/project.pbxproj +++ b/example/ios/QuickCryptoExample.xcodeproj/project.pbxproj @@ -114,11 +114,9 @@ 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, - 8C63FDE6D14F20AAC744357A /* Embed SPM Frameworks (OpenSSL) */, - F008C3222B5CD8109E252C6E /* Embed SPM Frameworks (QuickCrypto) */, 00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */, E235C05ADACE081382539298 /* [CP] Copy Pods Resources */, - 0F887C4BF3EDDB98EDD8A258 /* [CP-User] [CP-User] Embed OpenSSL Framework */, + 218A62117F2E6018338554EF /* [QuickCrypto] Embed & Sign SPM Frameworks */, ); buildRules = ( ); @@ -207,23 +205,7 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-QuickCryptoExample/Pods-QuickCryptoExample-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 0F887C4BF3EDDB98EDD8A258 /* [CP-User] [CP-User] Embed OpenSSL Framework */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${BUILT_PRODUCTS_DIR}/OpenSSL.framework", - ); - name = "[CP-User] [CP-User] Embed OpenSSL Framework"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "#!/bin/bash\nset -e\n\nOPENSSL_FRAMEWORK=\"${BUILT_PRODUCTS_DIR}/OpenSSL.framework\"\n\nif [ -d \"$OPENSSL_FRAMEWORK\" ]; then\n echo \"[QuickCrypto] Copying OpenSSL.framework to app bundle\"\n mkdir -p \"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}\"\n cp -Rf \"$OPENSSL_FRAMEWORK\" \"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/\"\n \n # Code sign the framework (only if code signing is required and not a simulator build)\n if [ \"${CODE_SIGNING_REQUIRED}\" = \"YES\" ] && [ \"${EFFECTIVE_PLATFORM_NAME}\" != \"-iphonesimulator\" ]; then\n codesign --force --sign \"${EXPANDED_CODE_SIGN_IDENTITY}\" --preserve-metadata=identifier,entitlements --timestamp=none \"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework\" || true\n fi\n echo \"[QuickCrypto] Successfully embedded OpenSSL.framework\"\nelse\n echo \"[QuickCrypto] Warning: OpenSSL.framework not found at $OPENSSL_FRAMEWORK\"\nfi\n"; - }; - 8C63FDE6D14F20AAC744357A /* Embed SPM Frameworks (OpenSSL) */ = { + 218A62117F2E6018338554EF /* [QuickCrypto] Embed & Sign SPM Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -232,14 +214,14 @@ ); inputPaths = ( ); - name = "Embed SPM Frameworks (OpenSSL)"; + name = "[QuickCrypto] Embed & Sign SPM Frameworks"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# Embed OpenSSL.framework from SPM build into app bundle\n# SPM builds the framework to BUILT_PRODUCTS_DIR but doesn't embed it\nOPENSSL_FRAMEWORK=\"${BUILT_PRODUCTS_DIR}/OpenSSL.framework\"\n\nif [ -d \"$OPENSSL_FRAMEWORK\" ]; then\n echo \"Found OpenSSL.framework at $OPENSSL_FRAMEWORK\"\n mkdir -p \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}\"\n rsync -av --delete \"$OPENSSL_FRAMEWORK\" \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/\"\n\n # Code sign if required\n if [ -n \"${EXPANDED_CODE_SIGN_IDENTITY:-}\" ] && [ \"${CODE_SIGNING_REQUIRED:-}\" != \"NO\" ]; then\n /usr/bin/codesign --force --sign \"${EXPANDED_CODE_SIGN_IDENTITY}\" --preserve-metadata=identifier,entitlements \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework\"\n fi\n echo \"OpenSSL.framework embedded successfully\"\nelse\n echo \"warning: OpenSSL.framework not found at $OPENSSL_FRAMEWORK\"\nfi\n"; + shellScript = "set -euo pipefail\n\n# Embed and sign SPM frameworks (OpenSSL) from QuickCrypto\n# This phase MUST run LAST, after all other framework embedding\n# See: https://github.com/margelo/react-native-quick-crypto/issues/857\n\nFRAMEWORKS_DIR=\"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}\"\nmkdir -p \"$FRAMEWORKS_DIR\"\n\nsign_framework() {\n local framework_path=\"$1\"\n local framework_name=$(basename \"$framework_path\")\n\n if [ ! -d \"$framework_path\" ]; then\n echo \"warning: $framework_name not found at $framework_path, skipping\"\n return 0\n fi\n\n echo \"[QuickCrypto] Processing $framework_name...\"\n\n # Copy to app bundle\n rsync -av --delete \"$framework_path\" \"$FRAMEWORKS_DIR/\"\n\n local dest_framework=\"$FRAMEWORKS_DIR/$framework_name\"\n\n # Sign if required (physical device builds only)\n if [ \"${CODE_SIGNING_REQUIRED:-NO}\" = \"YES\" ] && [ -n \"${EXPANDED_CODE_SIGN_IDENTITY:-}\" ]; then\n echo \"[QuickCrypto] Signing $framework_name with identity: ${EXPANDED_CODE_SIGN_IDENTITY}\"\n\n # Make framework writable (rsync preserves read-only from source)\n chmod -R u+w \"$dest_framework\"\n\n # Strip existing signature and re-sign with app's identity\n # This is required for pre-signed xcframeworks from SPM\n /usr/bin/codesign --force --deep --sign \"${EXPANDED_CODE_SIGN_IDENTITY}\" \\\n --timestamp=none \\\n \"$dest_framework\"\n\n echo \"[QuickCrypto] Successfully signed $framework_name\"\n else\n echo \"[QuickCrypto] Code signing not required (simulator build)\"\n fi\n}\n\n# Sign OpenSSL.framework from SPM\nsign_framework \"${BUILT_PRODUCTS_DIR}/OpenSSL.framework\"\n\necho \"[QuickCrypto] SPM framework embedding complete\"\n"; }; C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; @@ -280,24 +262,6 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-QuickCryptoExample/Pods-QuickCryptoExample-resources.sh\"\n"; showEnvVarsInLog = 0; }; - F008C3222B5CD8109E252C6E /* Embed SPM Frameworks (QuickCrypto) */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Embed SPM Frameworks (QuickCrypto)"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "# Embed SPM frameworks (OpenSSL, Clibsodium) from QuickCrypto into app bundle\n# SPM builds frameworks to BUILT_PRODUCTS_DIR but doesn't embed them automatically\n\nmkdir -p \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}\"\n\n# Embed OpenSSL.framework (always required for ML-DSA)\nOPENSSL_FRAMEWORK=\"${BUILT_PRODUCTS_DIR}/OpenSSL.framework\"\nif [ -d \"$OPENSSL_FRAMEWORK\" ]; then\n echo \"Found OpenSSL.framework at $OPENSSL_FRAMEWORK\"\n rsync -av --delete \"$OPENSSL_FRAMEWORK\" \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/\"\n\n if [ -n \"${EXPANDED_CODE_SIGN_IDENTITY:-}\" ] && [ \"${CODE_SIGNING_REQUIRED:-}\" != \"NO\" ]; then\n /usr/bin/codesign --force --sign \"${EXPANDED_CODE_SIGN_IDENTITY}\" --preserve-metadata=identifier,entitlements \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework\"\n fi\n echo \"OpenSSL.framework embedded successfully\"\nelse\n echo \"warning: OpenSSL.framework not found at $OPENSSL_FRAMEWORK\"\nfi\n\n# Embed Clibsodium.framework (optional, when SODIUM_ENABLED=1)\nCLIBSODIUM_FRAMEWORK=\"${BUILT_PRODUCTS_DIR}/Clibsodium.framework\"\nif [ -d \"$CLIBSODIUM_FRAMEWORK\" ]; then\n echo \"Found Clibsodium.framework at $CLIBSODIUM_FRAMEWORK\"\n rsync -av --delete \"$CLIBSODIUM_FRAMEWORK\" \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/\"\n\n if [ -n \"${EXPANDED_CODE_SIGN_IDENTITY:-}\" ] && [ \"${CODE_SIGNING_REQUIRED:-}\" != \"NO\" ]; then\n /usr/bin/codesign --force --sign \"${EXPANDED_CODE_SIGN_IDENTITY}\" --preserve-metadata=identifier,entitlements \"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/Clibsodium.framework\"\n fi\n echo \"Clibsodium.framework embedded successfully\"\nfi\n"; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -319,6 +283,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 64977D8TY3; ENABLE_BITCODE = NO; INFOPLIST_FILE = QuickCryptoExample/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.1; @@ -347,6 +312,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 64977D8TY3; INFOPLIST_FILE = QuickCryptoExample/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 16.1; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/packages/react-native-quick-crypto/scripts/quickcrypto_spm_fix.rb b/packages/react-native-quick-crypto/scripts/quickcrypto_spm_fix.rb new file mode 100644 index 00000000..94326ab9 --- /dev/null +++ b/packages/react-native-quick-crypto/scripts/quickcrypto_spm_fix.rb @@ -0,0 +1,132 @@ +# +# quickcrypto_spm_fix.rb +# Fixes OpenSSL.framework code signing for physical iOS devices +# See: https://github.com/margelo/react-native-quick-crypto/issues/857 +# +# REQUIRED: Add this to your ios/Podfile: +# +# require_relative '../node_modules/react-native-quick-crypto/scripts/quickcrypto_spm_fix' +# +# # ... target block ... +# +# post_install do |installer| +# # ... other post_install code (like react_native_post_install) ... +# +# # Fix QuickCrypto SPM framework signing for physical devices +# quickcrypto_fix_spm_signing(installer) +# end +# +# Options: +# quickcrypto_fix_spm_signing(installer, app_target_name: 'MyAppName') +# - Use app_target_name if you have multiple targets or non-standard naming +# + +def quickcrypto_fix_spm_signing(installer, app_target_name: nil) + # Find the main project (user's app project, not Pods project) + main_project_path = File.join(installer.sandbox.root.parent, "#{installer.sandbox.root.parent.basename}.xcodeproj") + + # If the default path doesn't exist, try to find any .xcodeproj + unless File.exist?(main_project_path) + xcodeproj_files = Dir.glob(File.join(installer.sandbox.root.parent, "*.xcodeproj")) + main_project_path = xcodeproj_files.first if xcodeproj_files.any? + end + + unless main_project_path && File.exist?(main_project_path) + Pod::UI.warn "[QuickCrypto] Could not find main Xcode project. SPM framework signing must be configured manually." + Pod::UI.warn "[QuickCrypto] See: https://github.com/margelo/react-native-quick-crypto/issues/857" + return + end + + main_project = Xcodeproj::Project.open(main_project_path) + + # Find the app target + app_target = if app_target_name + main_project.targets.find { |t| t.name == app_target_name } + else + # Find first application target + main_project.targets.find { |t| t.product_type == "com.apple.product-type.application" } + end + + unless app_target + Pod::UI.warn "[QuickCrypto] Could not find app target. SPM framework signing must be configured manually." + return + end + + Pod::UI.puts "[QuickCrypto] Configuring SPM framework signing for target: #{app_target.name}" + + # Remove old/duplicate SPM embed phases + old_phase_names = [ + 'Embed SPM Frameworks (QuickCrypto)', + 'Embed SPM Frameworks (OpenSSL)', + '[CP-User] Embed OpenSSL Framework', + '[CP-User] [CP-User] Embed OpenSSL Framework', + '[QuickCrypto] Embed & Sign SPM Frameworks' + ] + + phases_to_remove = app_target.shell_script_build_phases.select { |p| old_phase_names.include?(p.name) } + phases_to_remove.each do |phase| + app_target.build_phases.delete(phase) + Pod::UI.puts "[QuickCrypto] Removed old build phase: #{phase.name}" + end + + # Create new consolidated build phase + embed_phase_name = '[QuickCrypto] Embed & Sign SPM Frameworks' + + phase = app_target.new_shell_script_build_phase(embed_phase_name) + phase.shell_script = <<~'SCRIPT' + set -euo pipefail + + # Embed and sign SPM frameworks (OpenSSL) from QuickCrypto + # This phase MUST run LAST, after all other framework embedding + # See: https://github.com/margelo/react-native-quick-crypto/issues/857 + + FRAMEWORKS_DIR="${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}" + mkdir -p "$FRAMEWORKS_DIR" + + sign_framework() { + local framework_path="$1" + local framework_name=$(basename "$framework_path") + + if [ ! -d "$framework_path" ]; then + echo "warning: $framework_name not found at $framework_path, skipping" + return 0 + fi + + echo "[QuickCrypto] Processing $framework_name..." + + # Copy to app bundle + rsync -av --delete "$framework_path" "$FRAMEWORKS_DIR/" + + local dest_framework="$FRAMEWORKS_DIR/$framework_name" + + # Sign if required (physical device builds only) + if [ "${CODE_SIGNING_REQUIRED:-NO}" = "YES" ] && [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" ]; then + echo "[QuickCrypto] Signing $framework_name with identity: ${EXPANDED_CODE_SIGN_IDENTITY}" + + # Make framework writable (rsync preserves read-only from source) + chmod -R u+w "$dest_framework" + + # Strip existing signature and re-sign with app's identity + # This is required for pre-signed xcframeworks from SPM + /usr/bin/codesign --force --deep --sign "${EXPANDED_CODE_SIGN_IDENTITY}" \ + --timestamp=none \ + "$dest_framework" + + echo "[QuickCrypto] Successfully signed $framework_name" + else + echo "[QuickCrypto] Code signing not required (simulator build)" + fi + } + + # Sign OpenSSL.framework from SPM + sign_framework "${BUILT_PRODUCTS_DIR}/OpenSSL.framework" + + echo "[QuickCrypto] SPM framework embedding complete" + SCRIPT + + # Move to the very end of build phases (after all CocoaPods phases) + app_target.build_phases.move(phase, app_target.build_phases.count - 1) + + main_project.save + Pod::UI.puts "[QuickCrypto] Added '#{embed_phase_name}' build phase (placed last for proper signing)" +end