From afbbbbd502373743f2ab51600974e7ab03420add Mon Sep 17 00:00:00 2001 From: hyochan Date: Mon, 11 May 2026 01:57:05 +0900 Subject: [PATCH 1/5] fix(godot): embed ios frameworks on export Register GodotIap iOS frameworks during export and keep the post-export fixer available in release artifacts. Update the fixer to restore framework Info.plist files, normalize framework bundle references, and avoid duplicate Xcode framework links. Document the automatic export path and fallback workflow for Godot iOS setup. --- .github/workflows/release-godot.yml | 3 + .../godot-iap/addons/godot-iap/godot_iap.gd | 1 + .../addons/godot-iap/godot_iap_plugin.gd | 32 ++ libraries/godot-iap/scripts/fix_ios_embed.sh | 391 +++++++++++++----- packages/docs/src/pages/docs/setup/godot.tsx | 107 +++-- 5 files changed, 382 insertions(+), 152 deletions(-) diff --git a/.github/workflows/release-godot.yml b/.github/workflows/release-godot.yml index 94192b7a..519897a7 100644 --- a/.github/workflows/release-godot.yml +++ b/.github/workflows/release-godot.yml @@ -190,9 +190,12 @@ jobs: run: | mkdir -p dist/addons/godot-iap/android mkdir -p dist/addons/godot-iap/bin/ios + mkdir -p dist/addons/godot-iap/scripts cp addons/godot-iap/*.gd dist/addons/godot-iap/ cp addons/godot-iap/plugin.cfg dist/addons/godot-iap/ + cp scripts/fix_ios_embed.sh dist/addons/godot-iap/scripts/ + chmod +x dist/addons/godot-iap/scripts/fix_ios_embed.sh cp android/build/outputs/aar/godot-iap-release.aar dist/addons/godot-iap/android/GodotIap.release.aar cp android/build/outputs/aar/godot-iap-release.aar dist/addons/godot-iap/android/GodotIap.debug.aar diff --git a/libraries/godot-iap/addons/godot-iap/godot_iap.gd b/libraries/godot-iap/addons/godot-iap/godot_iap.gd index f0cd5564..db5145a9 100644 --- a/libraries/godot-iap/addons/godot-iap/godot_iap.gd +++ b/libraries/godot-iap/addons/godot-iap/godot_iap.gd @@ -996,6 +996,7 @@ func _await_products_fetched_for(method: String, request_id: String = "") -> Dic if payload is Dictionary and payload.get("method", "") == method: if request_id.is_empty() or payload.get("requestId", "") == request_id: return payload as Dictionary + return {} ## Extract the native `requestId` token from the synchronous "pending" JSON ## returned by a GDExtension @Callable, or empty string if missing. diff --git a/libraries/godot-iap/addons/godot-iap/godot_iap_plugin.gd b/libraries/godot-iap/addons/godot-iap/godot_iap_plugin.gd index 4845eb43..c4807363 100644 --- a/libraries/godot-iap/addons/godot-iap/godot_iap_plugin.gd +++ b/libraries/godot-iap/addons/godot-iap/godot_iap_plugin.gd @@ -29,6 +29,10 @@ func _exit_tree() -> void: class GodotIapExportPlugin extends EditorExportPlugin: const PLUGIN_NAME = "GodotIap" + const IOS_FRAMEWORKS = [ + "res://addons/godot-iap/bin/ios/GodotIap.framework", + "res://addons/godot-iap/bin/ios/SwiftGodotRuntime.framework", + ] func _get_name() -> String: return PLUGIN_NAME @@ -36,8 +40,36 @@ class GodotIapExportPlugin extends EditorExportPlugin: func _supports_platform(platform: EditorExportPlatform) -> bool: if platform is EditorExportPlatformAndroid: return true + if platform is EditorExportPlatformIOS: + return true return false + func _export_begin(features: PackedStringArray, _is_debug: bool, _path: String, _flags: int) -> void: + if not _is_ios_export(features): + return + + for framework_path in IOS_FRAMEWORKS: + if not DirAccess.dir_exists_absolute(framework_path): + push_warning("[GodotIap] Missing iOS framework: %s" % framework_path) + continue + _add_ios_embedded_framework(framework_path) + + func _is_ios_export(features: PackedStringArray) -> bool: + var platform = get_export_platform() + return ( + platform is EditorExportPlatformIOS + or features.has("ios") + or features.has("iOS") + ) + + func _add_ios_embedded_framework(path: String) -> void: + if has_method("add_apple_embedded_platform_embedded_framework"): + call("add_apple_embedded_platform_embedded_framework", path) + return + + # Godot 4.3/4.4 still expose the iOS-specific export API. + add_ios_embedded_framework(path) + func _get_android_libraries(platform: EditorExportPlatform, debug: bool) -> PackedStringArray: # Path is relative to the project root (res://) if debug: diff --git a/libraries/godot-iap/scripts/fix_ios_embed.sh b/libraries/godot-iap/scripts/fix_ios_embed.sh index d07fcb78..c924d81f 100755 --- a/libraries/godot-iap/scripts/fix_ios_embed.sh +++ b/libraries/godot-iap/scripts/fix_ios_embed.sh @@ -1,147 +1,314 @@ #!/bin/bash -# Fix iOS Xcode project to embed GodotIap frameworks -# Run this after Godot iOS export +# Fix an exported Godot iOS Xcode project so GodotIap frameworks are embedded +# as framework bundles and retain their Info.plist files. -set -e +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" -# Use IOS_EXPORT_DIR if set, otherwise default to Example/ios -IOS_EXPORT_DIR="${IOS_EXPORT_DIR:-$PROJECT_ROOT/Example/ios}" -PBXPROJ="$IOS_EXPORT_DIR/Martie.xcodeproj/project.pbxproj" +DEFAULT_ADDON_SOURCE_DIR="$PROJECT_ROOT/addons/godot-iap" +if [ ! -d "$DEFAULT_ADDON_SOURCE_DIR/bin/ios" ] && [ -d "$PROJECT_ROOT/bin/ios" ]; then + DEFAULT_ADDON_SOURCE_DIR="$PROJECT_ROOT" +fi + +DEFAULT_IOS_EXPORT_DIR="$PROJECT_ROOT/Example/ios" +if [ ! -d "$DEFAULT_IOS_EXPORT_DIR" ] && [ -d "$PROJECT_ROOT/../.." ]; then + DEFAULT_IOS_EXPORT_DIR="$(cd "$PROJECT_ROOT/../.." && pwd)/ios" +fi + +IOS_EXPORT_DIR="${IOS_EXPORT_DIR:-$DEFAULT_IOS_EXPORT_DIR}" +ADDON_SOURCE_DIR="${GODOT_IAP_ADDON_DIR:-$DEFAULT_ADDON_SOURCE_DIR}" +XCODEPROJ="${XCODEPROJ:-}" + +if [ -z "$XCODEPROJ" ]; then + XCODEPROJ="$(find "$IOS_EXPORT_DIR" -maxdepth 1 -name "*.xcodeproj" -type d | sort | head -n 1)" +fi + +if [ -z "$XCODEPROJ" ] || [ ! -d "$XCODEPROJ" ]; then + echo "Error: .xcodeproj not found in $IOS_EXPORT_DIR" + echo "Set XCODEPROJ=/path/to/App.xcodeproj or IOS_EXPORT_DIR=/path/to/export." + exit 1 +fi + +PBXPROJ="$XCODEPROJ/project.pbxproj" if [ ! -f "$PBXPROJ" ]; then echo "Error: project.pbxproj not found at $PBXPROJ" exit 1 fi -echo "Fixing iOS project to embed frameworks..." +copy_framework_plists() { + for framework in GodotIap SwiftGodotRuntime; do + local source_plist="$ADDON_SOURCE_DIR/bin/ios/$framework.framework/Info.plist" + + if [ ! -f "$source_plist" ]; then + echo "Warning: source Info.plist not found: $source_plist" + continue + fi + + local copied=0 + while IFS= read -r framework_dir; do + cp "$source_plist" "$framework_dir/Info.plist" + copied=1 + done < <(find "$IOS_EXPORT_DIR" -type d -path "*/addons/godot-iap/bin/ios/$framework.framework" 2>/dev/null) -# Backup original + if [ "$copied" -eq 0 ]; then + echo "Warning: exported $framework.framework not found under $IOS_EXPORT_DIR" + fi + done +} + +echo "Fixing iOS project: $XCODEPROJ" +copy_framework_plists + +# Backup original once per run. cp "$PBXPROJ" "$PBXPROJ.backup" -# Read the file and make modifications -python3 << EOF +export PBXPROJ +python3 <<'PY' +import hashlib +import os import re +import sys + +pbxproj = os.environ["PBXPROJ"] +frameworks = ["GodotIap", "SwiftGodotRuntime"] + +with open(pbxproj, "r", encoding="utf-8") as file: + content = file.read() + + +def find_framework_ref(framework_name): + patterns = [ + rf"([A-F0-9]{{24}}|\w+)\s*/\*\s*{framework_name}\.framework\s*\*/\s*=\s*\{{isa\s*=\s*PBXFileReference;[^}}]*\}};", + rf"([A-F0-9]{{24}}|\w+)\s*=\s*\{{isa\s*=\s*PBXFileReference;[^}}]*{framework_name}\.framework(?:/{framework_name})?[^}}]*\}};", + ] + + for pattern in patterns: + match = re.search(pattern, content) + if match: + return match.group(1) + return None + + +def normalize_framework_reference(text, framework_name): + text = re.sub( + rf'(path\s*=\s*"[^"]*{framework_name}\.framework)/{framework_name}"', + r'\1"', + text, + ) + text = re.sub( + rf"(lastKnownFileType\s*=\s*)file(\s*;[^}}]*{framework_name}\.framework)", + r"\1wrapper.framework\2", + text, + ) + text = re.sub( + rf"(explicitFileType\s*=\s*)file(\s*;[^}}]*{framework_name}\.framework)", + r"\1wrapper.framework\2", + text, + ) + return text -with open("$PBXPROJ", "r") as f: - content = f.read() - -# Check if GodotIap framework references exist -if "GodotIap.framework" not in content: - print("Warning: GodotIap.framework not found in project") - exit(0) - -# Find the framework file reference IDs -godotiap_ref_match = re.search(r'(\w+)\s*/\*\s*GodotIap\.framework\s*\*/\s*=\s*\{isa\s*=\s*PBXFileReference', content) -swiftgodot_ref_match = re.search(r'(\w+)\s*/\*\s*SwiftGodotRuntime\.framework\s*\*/\s*=\s*\{isa\s*=\s*PBXFileReference', content) - -# Also try alternate pattern (without comment) -if not godotiap_ref_match: - godotiap_ref_match = re.search(r'(\w+)\s*=\s*\{isa\s*=\s*PBXFileReference[^}]*GodotIap\.framework', content) -if not swiftgodot_ref_match: - swiftgodot_ref_match = re.search(r'(\w+)\s*=\s*\{isa\s*=\s*PBXFileReference[^}]*SwiftGodotRuntime\.framework', content) - -# Try to find by path pattern -if not godotiap_ref_match: - godotiap_ref_match = re.search(r'(\w+)\s*=\s*\{isa\s*=\s*PBXFileReference;[^}]*path\s*=\s*"[^"]*GodotIap\.framework[^"]*"', content) -if not swiftgodot_ref_match: - swiftgodot_ref_match = re.search(r'(\w+)\s*=\s*\{isa\s*=\s*PBXFileReference;[^}]*path\s*=\s*"[^"]*SwiftGodotRuntime\.framework[^"]*"', content) - -# Find by the specific pattern Godot uses -godotiap_ref = None -swiftgodot_ref = None - -for match in re.finditer(r'(\w+)\s*=\s*\{isa\s*=\s*PBXFileReference;[^}]+\}', content): - block = match.group(0) - ref_id = match.group(1) - if 'GodotIap.framework/GodotIap' in block or 'GodotIap.framework"' in block: - godotiap_ref = ref_id - if 'SwiftGodotRuntime.framework/SwiftGodotRuntime' in block or 'SwiftGodotRuntime.framework"' in block: - swiftgodot_ref = ref_id - -print(f"GodotIap ref: {godotiap_ref}") -print(f"SwiftGodotRuntime ref: {swiftgodot_ref}") - -if not godotiap_ref or not swiftgodot_ref: - print("Could not find framework references") - # Print first few PBXFileReference entries for debugging - refs = re.findall(r'(\w+)\s*=\s*\{isa\s*=\s*PBXFileReference;[^}]+\}', content)[:10] - for r in refs: - print(f"Found ref: {r[:200]}...") - exit(1) - -# Generate new IDs for embed build files using hash to avoid collisions -import hashlib -embed_godotiap_id = hashlib.md5(f"{godotiap_ref}_embed".encode()).hexdigest().upper()[:24] -embed_swiftgodot_id = hashlib.md5(f"{swiftgodot_ref}_embed".encode()).hexdigest().upper()[:24] -# Add embed build file entries after PBXBuildFile section start -embed_entries = f''' {embed_godotiap_id} /* GodotIap.framework in Embed Frameworks */ = {{isa = PBXBuildFile; fileRef = {godotiap_ref}; settings = {{ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }}; }}; - {embed_swiftgodot_id} /* SwiftGodotRuntime.framework in Embed Frameworks */ = {{isa = PBXBuildFile; fileRef = {swiftgodot_ref}; settings = {{ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }}; }}; -''' +framework_refs = {} +for framework in frameworks: + content = normalize_framework_reference(content, framework) + ref = find_framework_ref(framework) + if ref: + framework_refs[framework] = ref + +missing = [framework for framework in frameworks if framework not in framework_refs] +if missing: + print( + "Warning: framework references not found in Xcode project: " + + ", ".join(f"{framework}.framework" for framework in missing) + ) + print("Enable the GodotIap plugin in the iOS export preset and export again.") + with open(pbxproj, "w", encoding="utf-8") as file: + file.write(content) + sys.exit(0) -# Find Embed Frameworks section dynamically first to validate embed_phase_match = re.search( - r'(\w+)\s*/\*\s*Embed Frameworks\s*\*/\s*=\s*\{[^}]*isa\s*=\s*PBXCopyFilesBuildPhase', - content + r"([A-F0-9]{24}|\w+)\s*/\*\s*Embed Frameworks\s*\*/\s*=\s*\{.*?isa\s*=\s*PBXCopyFilesBuildPhase;.*?\n\t\t\};", + content, + re.DOTALL, ) if not embed_phase_match: print("Error: Could not find Embed Frameworks build phase") - print("The project may not be properly configured for framework embedding") - exit(1) + sys.exit(1) + +embed_phase_block = embed_phase_match.group(0) +build_entries = [] +embed_entries = [] -embed_phase_id = embed_phase_match.group(1) -print(f"Found Embed Frameworks phase: {embed_phase_id}") -# Check if embed entries already exist -if embed_godotiap_id in content: - print("Embed entries already exist") -else: - # Add after "/* Begin PBXBuildFile section */" +def embedded_build_file_id(ref): + build_file_pattern = re.compile( + rf"([A-F0-9]{{24}}|\w+)\s*(?:/\*[^*]*\*/\s*)?=\s*\{{isa\s*=\s*PBXBuildFile;\s*fileRef\s*=\s*{ref};[^}}]*\}};", + re.DOTALL, + ) + + fallback_id = None + for match in build_file_pattern.finditer(content): + build_id = match.group(1) + block = match.group(0) + if build_id not in embed_phase_block: + continue + fallback_id = build_id + if "CodeSignOnCopy" in block: + return build_id + return fallback_id + + +for framework, ref in framework_refs.items(): + embed_id = embedded_build_file_id(ref) + build_comment = f"{framework}.framework in Embed Frameworks" + + if not embed_id: + embed_id = hashlib.md5(f"{ref}_embed".encode()).hexdigest().upper()[:24] + build_entries.append( + f"\t\t{embed_id} /* {build_comment} */ = " + f"{{isa = PBXBuildFile; fileRef = {ref}; " + "settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }};\n" + ) + + if embed_id not in embed_phase_block: + embed_entries.append(f"\t\t\t\t{embed_id} /* {build_comment} */,\n") + +if build_entries: content = content.replace( "/* Begin PBXBuildFile section */\n", - "/* Begin PBXBuildFile section */\n" + embed_entries + "/* Begin PBXBuildFile section */\n" + "".join(build_entries), + 1, ) -# Replace empty Embed Frameworks files using dynamic UUID -content = re.sub( - rf'({embed_phase_id}\s*/\*\s*Embed Frameworks\s*\*/\s*=\s*\{{[^}}]*files\s*=\s*\()[^)]*(\);)', - rf'\g<1>\n\t\t\t\t\t{embed_godotiap_id} /* GodotIap.framework in Embed Frameworks */,\n\t\t\t\t\t{embed_swiftgodot_id} /* SwiftGodotRuntime.framework in Embed Frameworks */,\n\t\t\t\t\2', - content -) +if embed_entries: + files_match = re.search(r"files\s*=\s*\(\n?(.*?)\n?\t\t\t\);", embed_phase_block, re.DOTALL) + if not files_match: + print("Error: Could not find files list in Embed Frameworks build phase") + sys.exit(1) -# Fix framework references to point to .framework folder, not binary inside -content = re.sub( - r'(path\s*=\s*"[^"]*GodotIap\.framework)/GodotIap"', - r'\1"', - content -) -content = re.sub( - r'(path\s*=\s*"[^"]*SwiftGodotRuntime\.framework)/SwiftGodotRuntime"', - r'\1"', - content -) + files_start, files_end = files_match.span(1) + current_files = files_match.group(1) + separator = "" if current_files.endswith("\n") or not current_files.strip() else "\n" + updated_files = current_files + separator + "".join(embed_entries) + updated_embed_phase_block = ( + embed_phase_block[:files_start] + updated_files + embed_phase_block[files_end:] + ) + content = content.replace(embed_phase_block, updated_embed_phase_block, 1) -# Change lastKnownFileType from file to wrapper.framework -content = re.sub( - r'(lastKnownFileType\s*=\s*)file(\s*;[^}]*GodotIap)', - r'\1wrapper.framework\2', - content -) -content = re.sub( - r'(lastKnownFileType\s*=\s*)file(\s*;[^}]*SwiftGodotRuntime)', - r'\1wrapper.framework\2', - content -) -with open("$PBXPROJ", "w") as f: - f.write(content) +def remove_duplicate_framework_links(text): + framework_phase_match = re.search( + r"([A-F0-9]{24}|\w+)\s*/\*\s*Frameworks\s*\*/\s*=\s*\{.*?isa\s*=\s*PBXFrameworksBuildPhase;.*?\n\t\t\};", + text, + re.DOTALL, + ) + if not framework_phase_match: + return text + + framework_phase_block = framework_phase_match.group(0) + file_refs = {} + for match in re.finditer( + r"^\s*([A-F0-9]{24}|\w+)\s*(?:/\*[^*]*\*/\s*)?=\s*\{isa\s*=\s*PBXFileReference;(.*?)\};", + text, + re.MULTILINE, + ): + file_refs[match.group(1)] = match.group(2) + + build_files = {} + for match in re.finditer( + r"^\s*([A-F0-9]{24}|\w+)\s*(?:/\*[^*]*\*/\s*)?=\s*\{isa\s*=\s*PBXBuildFile;\s*fileRef\s*=\s*([A-F0-9]{24}|\w+);.*?\};", + text, + re.MULTILINE, + ): + build_files[match.group(1)] = match.group(2) + + duplicate_build_ids = [] + duplicate_file_refs = [] + for framework, canonical_ref in framework_refs.items(): + linked_ids = [ + build_id + for build_id, ref in build_files.items() + if build_id in framework_phase_block + and f"{framework}.framework" in file_refs.get(ref, "") + ] + if len(linked_ids) <= 1: + continue + + keep_id = next( + (build_id for build_id in linked_ids if build_files[build_id] == canonical_ref), + linked_ids[0], + ) + for build_id in linked_ids: + if build_id == keep_id: + continue + duplicate_build_ids.append(build_id) + duplicate_file_refs.append(build_files[build_id]) + + if not duplicate_build_ids: + return text + + updated_framework_phase_block = framework_phase_block + for build_id in duplicate_build_ids: + updated_framework_phase_block = re.sub( + rf"\n[ \t]*{build_id}(?:\s*/\*[^*]*\*/)?\s*,?(?=\s*(?:\n|\)))", + "", + updated_framework_phase_block, + ) + updated_framework_phase_block = re.sub( + r",\s*\);", ",\n\t\t\t);", updated_framework_phase_block + ) + + text = text.replace(framework_phase_block, updated_framework_phase_block, 1) + for build_id in duplicate_build_ids: + text = re.sub( + rf"^\s*{build_id}\s*(?:/\*[^*]*\*/\s*)?=\s*\{{isa\s*=\s*PBXBuildFile;\s*fileRef\s*=\s*([A-F0-9]{{24}}|\w+);.*?\}};\n", + "", + text, + flags=re.MULTILINE, + ) + + def remove_ref_from_groups(project_text, ref): + def replace_group(match): + group_block = match.group(0) + if ref not in group_block: + return group_block + group_block = re.sub( + rf"\n[ \t]*{ref}(?:\s*/\*[^*]*\*/)?\s*,?(?=\s*(?:\n|\)))", + "", + group_block, + ) + return re.sub(r",\s*\);", ",\n\t\t\t);", group_block) + + return re.sub( + r"^\s*([A-F0-9]{24}|\w+)\s*(?:/\*[^*]*\*/\s*)?=\s*\{[^}]*isa\s*=\s*PBXGroup;.*?\n\t\t\};", + replace_group, + project_text, + flags=re.MULTILINE | re.DOTALL, + ) + + for ref in duplicate_file_refs: + if ref in framework_refs.values(): + continue + text = remove_ref_from_groups(text, ref) + text = re.sub( + rf"^\s*{ref}\s*(?:/\*[^*]*\*/\s*)?=\s*\{{isa\s*=\s*PBXFileReference;.*?\}};\n", + "", + text, + flags=re.MULTILINE, + ) + return text + + +content = remove_duplicate_framework_links(content) + +with open(pbxproj, "w", encoding="utf-8") as file: + file.write(content) -print("Done!") -EOF +print("Framework embedding fixed.") +PY -echo "Framework embedding fixed!" -echo "Now open Xcode and build the project." +echo "Framework Info.plist files copied." diff --git a/packages/docs/src/pages/docs/setup/godot.tsx b/packages/docs/src/pages/docs/setup/godot.tsx index b34f9b26..7d6d631e 100644 --- a/packages/docs/src/pages/docs/setup/godot.tsx +++ b/packages/docs/src/pages/docs/setup/godot.tsx @@ -141,47 +141,77 @@ make android

- iOS: Xcode Configuration (Required) + iOS: Xcode Framework Embedding #

- After exporting from Godot, you must configure the - frameworks in Xcode: + The GodotIap export plugin registers the iOS frameworks during export + so they are added to Xcode's Embed Frameworks build + phase automatically. Before exporting, make sure GodotIap is enabled + in the iOS export preset's Plugins section.

-
    -
  1. - Open the exported .xcodeproj in Xcode -
  2. -
  3. - Select your target > General tab -
  4. -
  5. - Scroll to{' '} - Frameworks, Libraries, and Embedded Content -
  6. +

    + If you exported with an older plugin version, or if Xcode still shows + the frameworks as file references instead of framework bundles, run + the post-export fixer from your Godot project root: +

    + + {`IOS_EXPORT_DIR=/path/to/ios-export \\ + ./addons/godot-iap/scripts/fix_ios_embed.sh`} + +

    + The script finds the exported .xcodeproj, embeds: +

    +
+ +

+ It also converts framework file references to framework bundles and + restores any missing framework Info.plist files. +

+ +
+ Manual Xcode fallback +
    +
  1. + Open the exported .xcodeproj in Xcode +
  2. +
  3. + Select your target > General tab +
  4. +
  5. + Scroll to{' '} + Frameworks, Libraries, and Embedded Content +
  6. +
  7. + Click + and add: +
      +
    • + GodotIap.framework +
    • +
    • + SwiftGodotRuntime.framework +
    • +
    +
  8. +
  9. + Set both to "Embed & Sign" +
  10. +
+

The frameworks are located at:

{`[exported_project]/addons/godot-iap/bin/ios/GodotIap.framework [exported_project]/addons/godot-iap/bin/ios/SwiftGodotRuntime.framework`} -

Then configure the runpath:

+

Then confirm the runpath:

  1. Go to the Build Settings tab @@ -204,10 +234,10 @@ make android margin: '1rem 0', }} > - Warning: If you skip embedding the frameworks, the - app will crash on launch with:{' '} + Warning: If the frameworks are not embedded, the app + will crash on launch with:{' '} Library not loaded: @rpath/GodotIap.framework/GodotIap. - If you skip the Runpath setting, it will crash with:{' '} + If the runpath is missing, it can crash with:{' '} Library not loaded: @rpath/SwiftGodotRuntime.framework/SwiftGodotRuntime @@ -216,7 +246,7 @@ make android

    - iOS: Fix Missing Info.plist (Required) + iOS: Missing Info.plist Fallback # @@ -230,13 +260,11 @@ make android > Godot export bug - , the Info.plist files inside frameworks may not be - copied during export. This causes the build to fail with:{' '} - Framework did not contain an Info.plist. -

    -

    - Solution: Add a build phase script in Xcode: + , some exports may omit Info.plist files inside embedded + frameworks. The fix_ios_embed.sh script above copies the + missing files automatically.

    +

    If you still need an Xcode build phase fallback:

    1. Select your target > Build Phases tab @@ -268,7 +296,6 @@ fi`} Frameworks" phase
    -
    - Tip: This script automatically copies the missing{' '} - Info.plist files from the source frameworks to the build - output. You only need to set this up once per project. + Tip: Prefer the post-export fixer when possible; the + build phase is only a fallback for projects that cannot run the script + after export.
    From 47f5b4064dae98c57974471ccb92a6dff6626f3d Mon Sep 17 00:00:00 2001 From: hyochan Date: Mon, 11 May 2026 02:20:32 +0900 Subject: [PATCH 2/5] fix(godot): address ios export review Tighten the iOS export fallback script, add an extra Apple framework API fallback, and move manual runpath guidance into the manual docs section. --- .../addons/godot-iap/godot_iap_plugin.gd | 5 ++- libraries/godot-iap/scripts/fix_ios_embed.sh | 45 ++++++++++++------- packages/docs/src/pages/docs/setup/godot.tsx | 37 +++++++-------- 3 files changed, 52 insertions(+), 35 deletions(-) diff --git a/libraries/godot-iap/addons/godot-iap/godot_iap_plugin.gd b/libraries/godot-iap/addons/godot-iap/godot_iap_plugin.gd index c4807363..c909d68d 100644 --- a/libraries/godot-iap/addons/godot-iap/godot_iap_plugin.gd +++ b/libraries/godot-iap/addons/godot-iap/godot_iap_plugin.gd @@ -29,7 +29,7 @@ func _exit_tree() -> void: class GodotIapExportPlugin extends EditorExportPlugin: const PLUGIN_NAME = "GodotIap" - const IOS_FRAMEWORKS = [ + const IOS_FRAMEWORKS: Array[String] = [ "res://addons/godot-iap/bin/ios/GodotIap.framework", "res://addons/godot-iap/bin/ios/SwiftGodotRuntime.framework", ] @@ -66,6 +66,9 @@ class GodotIapExportPlugin extends EditorExportPlugin: if has_method("add_apple_embedded_platform_embedded_framework"): call("add_apple_embedded_platform_embedded_framework", path) return + if has_method("add_apple_embedded_framework"): + call("add_apple_embedded_framework", path) + return # Godot 4.3/4.4 still expose the iOS-specific export API. add_ios_embedded_framework(path) diff --git a/libraries/godot-iap/scripts/fix_ios_embed.sh b/libraries/godot-iap/scripts/fix_ios_embed.sh index c924d81f..a528ea37 100755 --- a/libraries/godot-iap/scripts/fix_ios_embed.sh +++ b/libraries/godot-iap/scripts/fix_ios_embed.sh @@ -22,7 +22,19 @@ ADDON_SOURCE_DIR="${GODOT_IAP_ADDON_DIR:-$DEFAULT_ADDON_SOURCE_DIR}" XCODEPROJ="${XCODEPROJ:-}" if [ -z "$XCODEPROJ" ]; then - XCODEPROJ="$(find "$IOS_EXPORT_DIR" -maxdepth 1 -name "*.xcodeproj" -type d | sort | head -n 1)" + XCODEPROJS=() + while IFS= read -r project; do + XCODEPROJS+=("$project") + done < <(find "$IOS_EXPORT_DIR" -maxdepth 1 -name "*.xcodeproj" -type d | sort) + + if [ "${#XCODEPROJS[@]}" -gt 1 ]; then + echo "Error: multiple .xcodeproj files found in $IOS_EXPORT_DIR:" >&2 + printf ' %s\n' "${XCODEPROJS[@]}" >&2 + echo "Set XCODEPROJ=/path/to/App.xcodeproj to disambiguate." >&2 + exit 1 + fi + + XCODEPROJ="${XCODEPROJS[0]:-}" fi if [ -z "$XCODEPROJ" ] || [ ! -d "$XCODEPROJ" ]; then @@ -99,18 +111,29 @@ def normalize_framework_reference(text, framework_name): text, ) text = re.sub( - rf"(lastKnownFileType\s*=\s*)file(\s*;[^}}]*{framework_name}\.framework)", + rf"(lastKnownFileType\s*=\s*)file(\s*;[^}}]*{framework_name}\.framework(?=[\"\s;]))", r"\1wrapper.framework\2", text, ) text = re.sub( - rf"(explicitFileType\s*=\s*)file(\s*;[^}}]*{framework_name}\.framework)", + rf"(explicitFileType\s*=\s*)file(\s*;[^}}]*{framework_name}\.framework(?=[\"\s;]))", r"\1wrapper.framework\2", text, ) return text +def remove_pbx_list_item(block, item_id): + item_pattern = re.compile( + rf"^[ \t]*{item_id}(?:\s*/\*[^*]*\*/)?\s*,?\s*$" + ) + return "".join( + line + for line in block.splitlines(keepends=True) + if not item_pattern.match(line.rstrip("\r\n")) + ) + + framework_refs = {} for framework in frameworks: content = normalize_framework_reference(content, framework) @@ -253,14 +276,9 @@ def remove_duplicate_framework_links(text): updated_framework_phase_block = framework_phase_block for build_id in duplicate_build_ids: - updated_framework_phase_block = re.sub( - rf"\n[ \t]*{build_id}(?:\s*/\*[^*]*\*/)?\s*,?(?=\s*(?:\n|\)))", - "", - updated_framework_phase_block, + updated_framework_phase_block = remove_pbx_list_item( + updated_framework_phase_block, build_id ) - updated_framework_phase_block = re.sub( - r",\s*\);", ",\n\t\t\t);", updated_framework_phase_block - ) text = text.replace(framework_phase_block, updated_framework_phase_block, 1) for build_id in duplicate_build_ids: @@ -276,12 +294,7 @@ def remove_duplicate_framework_links(text): group_block = match.group(0) if ref not in group_block: return group_block - group_block = re.sub( - rf"\n[ \t]*{ref}(?:\s*/\*[^*]*\*/)?\s*,?(?=\s*(?:\n|\)))", - "", - group_block, - ) - return re.sub(r",\s*\);", ",\n\t\t\t);", group_block) + return remove_pbx_list_item(group_block, ref) return re.sub( r"^\s*([A-F0-9]{24}|\w+)\s*(?:/\*[^*]*\*/\s*)?=\s*\{[^}]*isa\s*=\s*PBXGroup;.*?\n\t\t\};", diff --git a/packages/docs/src/pages/docs/setup/godot.tsx b/packages/docs/src/pages/docs/setup/godot.tsx index 7d6d631e..c2fba2ce 100644 --- a/packages/docs/src/pages/docs/setup/godot.tsx +++ b/packages/docs/src/pages/docs/setup/godot.tsx @@ -205,25 +205,26 @@ make android Set both to "Embed & Sign"

- -

The frameworks are located at:

- - {`[exported_project]/addons/godot-iap/bin/ios/GodotIap.framework +

The frameworks are located at:

+ + {`[exported_project]/addons/godot-iap/bin/ios/GodotIap.framework [exported_project]/addons/godot-iap/bin/ios/SwiftGodotRuntime.framework`} - -

Then confirm the runpath:

-
    -
  1. - Go to the Build Settings tab -
  2. -
  3. - Search for "Runpath Search Paths" ( - LD_RUNPATH_SEARCH_PATHS) -
  4. -
  5. - Add @executable_path/Frameworks if not already present -
  6. -
+
+

Then confirm the runpath:

+
    +
  1. + Go to the Build Settings tab +
  2. +
  3. + Search for "Runpath Search Paths" ( + LD_RUNPATH_SEARCH_PATHS) +
  4. +
  5. + Add @executable_path/Frameworks if not already + present +
  6. +
+
Date: Mon, 11 May 2026 02:24:48 +0900 Subject: [PATCH 3/5] docs(releases): add godot ios export note --- .../docs/src/pages/docs/updates/releases.tsx | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/packages/docs/src/pages/docs/updates/releases.tsx b/packages/docs/src/pages/docs/updates/releases.tsx index 1fc5fee8..154c9936 100644 --- a/packages/docs/src/pages/docs/updates/releases.tsx +++ b/packages/docs/src/pages/docs/updates/releases.tsx @@ -26,6 +26,111 @@ function Releases() { useScrollToHash(); const allNotes: Note[] = [ + // May 10, 2026 — godot-iap 2.2.8 planned iOS export embedding patch + { + id: 'godot-iap-2-2-8-ios-export-framework-embedding', + date: new Date('2026-05-10'), + element: ( +
+ + May 10, 2026 — godot-iap 2.2.8 iOS export framework embedding patch + + +

+ Prepares a godot-iap 2.2.8 patch release for the + iOS export workflow. GodotIap now registers its iOS frameworks + during export so Xcode receives GodotIap.framework and{' '} + SwiftGodotRuntime.framework as embedded framework + bundles automatically. The release also ships the post-export fixer + inside the addon package for projects that were exported with an + older plugin version. See{' '} + + discussion #146 + {' '} + and{' '} + + PR #148 + + . +

+ +
    +
  • + Automatic iOS embedding — the Godot export plugin + now supports iOS export presets and adds both Swift GDExtension + frameworks to Xcode's Embed Frameworks build + phase when the plugin is enabled. +
  • +
  • + Post-export fallback —{' '} + fix_ios_embed.sh is included in release artifacts and + can repair existing exports by copying missing framework{' '} + Info.plist files, normalizing framework bundle + references, and avoiding duplicate framework link entries. +
  • +
  • + Safer Xcode project handling — the fixer now asks + users to set XCODEPROJ when multiple{' '} + .xcodeproj files are present, rather than silently + patching the first project it finds. +
  • +
  • + Setup docs — the{' '} + Godot setup guide now + documents automatic framework embedding first and keeps manual + Xcode / Info.plist steps as fallback guidance. +
  • +
+ + {/* Planned Package Releases */} +
+
Planned Package Releases
+
    +
  • godot-iap 2.2.8 (planned)
  • +
+
+
+ ), + }, + // May 8, 2026 — openiap-apple 2.1.8 promoted IAP cold-start fix { id: 'apple-2-1-8-promoted-iap-cold-start', From 86a71bb6e3ad862c68c4b03719e52e5f1f1b90e0 Mon Sep 17 00:00:00 2001 From: hyochan Date: Mon, 11 May 2026 02:43:05 +0900 Subject: [PATCH 4/5] fix(godot): relax embed phase parsing --- libraries/godot-iap/scripts/fix_ios_embed.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/godot-iap/scripts/fix_ios_embed.sh b/libraries/godot-iap/scripts/fix_ios_embed.sh index a528ea37..27705dda 100755 --- a/libraries/godot-iap/scripts/fix_ios_embed.sh +++ b/libraries/godot-iap/scripts/fix_ios_embed.sh @@ -208,7 +208,7 @@ if build_entries: ) if embed_entries: - files_match = re.search(r"files\s*=\s*\(\n?(.*?)\n?\t\t\t\);", embed_phase_block, re.DOTALL) + files_match = re.search(r"files\s*=\s*\(\n?(.*?)\n?\s*\);", embed_phase_block, re.DOTALL) if not files_match: print("Error: Could not find files list in Embed Frameworks build phase") sys.exit(1) From c8bdfb278d95bba557408ddd26487b67dab6b0aa Mon Sep 17 00:00:00 2001 From: hyochan Date: Mon, 11 May 2026 03:11:03 +0900 Subject: [PATCH 5/5] docs(releases): link godot patch note --- knowledge/internal/05-docs-patterns.md | 19 +++++++++++----- .../docs/src/pages/docs/updates/releases.tsx | 22 +++++++++++++------ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/knowledge/internal/05-docs-patterns.md b/knowledge/internal/05-docs-patterns.md index 5c23509c..cfebc742 100644 --- a/knowledge/internal/05-docs-patterns.md +++ b/knowledge/internal/05-docs-patterns.md @@ -246,11 +246,20 @@ Before adding or editing a `Package Releases` list: 2. Read the current package metadata from `origin/main`, not from memory. 3. For planned patch releases, add exactly one patch version to each affected framework package and label the block `Planned Package Releases`. -4. For published release links, confirm each tag exists with - `gh release view --repo hyodotdev/openiap` before adding an ``. -5. If a release workflow is still running, keep the entry as plain text with - planned wording. Add links only after the GitHub Release exists. -6. Run `bun run audit:docs`; the audit fails when a published +4. If the user explicitly asks to write the note as already released, says to + "assume it will be deployed/published", or asks to follow the existing linked + release-note style, do **not** use `Planned Package Releases` or + `(planned)`. Write the block as `Package Releases`, add the expected GitHub + Release tag link (for example `godot-iap-2.2.8`), and use shipped wording + such as "Publishes" / "Ships" instead of "Prepares". +5. For links to releases that should already exist in GitHub, confirm each tag + exists with `gh release view --repo hyodotdev/openiap` before adding an + ``. This existence check is skipped only when step 4 applies because + the user explicitly requested an assumed post-release note. +6. If a release workflow is still running and the user has not requested an + already-released note, keep the entry as plain text with planned wording. Add + links only after the GitHub Release exists. +7. Run `bun run audit:docs`; the audit fails when a published `Package Releases` block contains a package/version item without a GitHub Release link. diff --git a/packages/docs/src/pages/docs/updates/releases.tsx b/packages/docs/src/pages/docs/updates/releases.tsx index 154c9936..71f5f42f 100644 --- a/packages/docs/src/pages/docs/updates/releases.tsx +++ b/packages/docs/src/pages/docs/updates/releases.tsx @@ -26,7 +26,7 @@ function Releases() { useScrollToHash(); const allNotes: Note[] = [ - // May 10, 2026 — godot-iap 2.2.8 planned iOS export embedding patch + // May 10, 2026 — godot-iap 2.2.8 iOS export embedding patch { id: 'godot-iap-2-2-8-ios-export-framework-embedding', date: new Date('2026-05-10'), @@ -48,9 +48,9 @@ function Releases() { color: 'var(--text-secondary)', }} > - Prepares a godot-iap 2.2.8 patch release for the - iOS export workflow. GodotIap now registers its iOS frameworks - during export so Xcode receives GodotIap.framework and{' '} + Publishes godot-iap 2.2.8 for the iOS export + workflow. GodotIap now registers its iOS frameworks during export so + Xcode receives GodotIap.framework and{' '} SwiftGodotRuntime.framework as embedded framework bundles automatically. The release also ships the post-export fixer inside the addon package for projects that were exported with an @@ -109,14 +109,14 @@ function Releases() { - {/* Planned Package Releases */} + {/* Package Releases */}