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/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/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..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,6 +29,10 @@ func _exit_tree() -> void: class GodotIapExportPlugin extends EditorExportPlugin: const PLUGIN_NAME = "GodotIap" + const IOS_FRAMEWORKS: Array[String] = [ + "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,39 @@ 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 + 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) + 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..27705dda 100755 --- a/libraries/godot-iap/scripts/fix_ios_embed.sh +++ b/libraries/godot-iap/scripts/fix_ios_embed.sh @@ -1,147 +1,327 @@ #!/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 + 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 + 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) + + 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 +# 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 -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] +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(?=[\"\s;]))", + r"\1wrapper.framework\2", + text, + ) + text = re.sub( + 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")) + ) -# 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, ); }}; }}; -''' -# Find Embed Frameworks section dynamically first to validate +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) + 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 = [] + + +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 -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 */" +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?\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) -# 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 = remove_pbx_list_item( + updated_framework_phase_block, build_id + ) + + 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 + 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\};", + 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..c2fba2ce 100644 --- a/packages/docs/src/pages/docs/setup/godot.tsx +++ b/packages/docs/src/pages/docs/setup/godot.tsx @@ -141,59 +141,90 @@ 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. -
  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`} +

+ 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`} -

Then configure the runpath:

-
    -
  1. - Go to the Build Settings tab -
  2. +

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

    +
    • - Search for "Runpath Search Paths" ( - LD_RUNPATH_SEARCH_PATHS) + GodotIap.framework
    • - Add @executable_path/Frameworks if not already present + SwiftGodotRuntime.framework
    • -
+ +

+ 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 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. +
+
- 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 +247,7 @@ make android

- iOS: Fix Missing Info.plist (Required) + iOS: Missing Info.plist Fallback # @@ -230,13 +261,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 +297,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.
diff --git a/packages/docs/src/pages/docs/updates/releases.tsx b/packages/docs/src/pages/docs/updates/releases.tsx index 1fc5fee8..71f5f42f 100644 --- a/packages/docs/src/pages/docs/updates/releases.tsx +++ b/packages/docs/src/pages/docs/updates/releases.tsx @@ -26,6 +26,119 @@ function Releases() { useScrollToHash(); const allNotes: Note[] = [ + // 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'), + element: ( +
+ + May 10, 2026 — godot-iap 2.2.8 iOS export framework embedding patch + + +

+ 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 + 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. +
  • +
+ + {/* Package Releases */} +
+
Package Releases
+ +
+
+ ), + }, + // May 8, 2026 — openiap-apple 2.1.8 promoted IAP cold-start fix { id: 'apple-2-1-8-promoted-iap-cold-start',