diff --git a/.github/pr-previews/pr-265-app-store-toolchain.mp4 b/.github/pr-previews/pr-265-app-store-toolchain.mp4 new file mode 100644 index 000000000..d0c13da56 Binary files /dev/null and b/.github/pr-previews/pr-265-app-store-toolchain.mp4 differ diff --git a/.github/workflows/ci-maui-iap.yml b/.github/workflows/ci-maui-iap.yml index 6e622c0b6..0192d95fb 100644 --- a/.github/workflows/ci-maui-iap.yml +++ b/.github/workflows/ci-maui-iap.yml @@ -9,7 +9,7 @@ on: - "packages/google/**" - "packages/apple/Sources/**" - "packages/apple/wrapper/**" - - "packages/apple/scripts/build-xcframework.sh" + - "packages/apple/scripts/**" - "openiap-versions.json" - "scripts/ci/retry-gradle.sh" - ".github/workflows/ci-maui-iap.yml" @@ -21,7 +21,7 @@ on: - "packages/google/**" - "packages/apple/Sources/**" - "packages/apple/wrapper/**" - - "packages/apple/scripts/build-xcframework.sh" + - "packages/apple/scripts/**" - "openiap-versions.json" - "scripts/ci/retry-gradle.sh" - ".github/workflows/ci-maui-iap.yml" @@ -33,6 +33,9 @@ concurrency: env: XCODEGEN_VERSION: 2.45.4 XCODEGEN_SHA256: 090ec29491aad50aec10631bf6e62253fed733c50f3aab0f5ffc86bc170bdbef + APP_STORE_XCODE_VERSION: "26.6" + APP_STORE_SDK_VERSION: "26.5" + APP_STORE_LD_VERSION: "1267.0" jobs: compile-check: @@ -142,10 +145,34 @@ jobs: dotnet build src/OpenIap.Maui/OpenIap.Maui.csproj -p:TargetFrameworks=net10.0-android -p:OpenIapAndroidStore="$store" -p:BuildProjectReferences=false "${DOTNET_BUILD_ARGS[@]}" done + app-store-artifact: + name: App Store artifact (Xcode 26.6) + runs-on: macos-26 + timeout-minutes: 30 + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 1 + + - name: Set up App Store Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: ${{ env.APP_STORE_XCODE_VERSION }} + + - name: Install xcodegen + run: bash scripts/install-xcodegen.sh "$XCODEGEN_VERSION" + + - name: Build OpenIAP.xcframework + run: bash packages/apple/scripts/build-xcframework.sh + + - name: Verify App Store toolchain provenance + run: bash packages/apple/scripts/verify-app-store-xcframework.sh + ios-binding: name: iOS binding (net10.0 ios + maccatalyst) - # The MAUI package embeds a prebuilt OpenIAP.xcframework. Compile it with - # Xcode 27 so the guarded StoreKit 27 implementation is present in NuGet. + # This lane proves source compatibility with the upcoming Xcode 27 SDK. + # The separate app-store-artifact lane guards the XCFramework that can be + # packed into a stable NuGet release. if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository runs-on: xcode-27 timeout-minutes: 45 diff --git a/.github/workflows/release-maui.yml b/.github/workflows/release-maui.yml index cf7a4d2dd..7b23b7359 100644 --- a/.github/workflows/release-maui.yml +++ b/.github/workflows/release-maui.yml @@ -31,6 +31,9 @@ env: DOTNET_NOLOGO: "1" XCODEGEN_VERSION: 2.45.4 XCODEGEN_SHA256: 090ec29491aad50aec10631bf6e62253fed733c50f3aab0f5ffc86bc170bdbef + APP_STORE_XCODE_VERSION: "26.6" + APP_STORE_SDK_VERSION: "26.5" + APP_STORE_LD_VERSION: "1267.0" RELEASE_BRANCH: ${{ github.ref_name }} jobs: @@ -78,8 +81,9 @@ jobs: validate-multitarget: needs: [release-branch] name: Validate (net10.0 platform TFMs) - # The MAUI package embeds a prebuilt OpenIAP.xcframework. Compile it with - # Xcode 27 so the guarded StoreKit 27 implementation is present in NuGet. + # Keep the upcoming SDK as a source-compatibility lane. The publish job + # separately rebuilds the packaged Apple sidecar with the stable App Store + # toolchain. runs-on: xcode-27 timeout-minutes: 60 steps: @@ -121,9 +125,10 @@ jobs: publish: needs: [validate, validate-multitarget] - # Rebuild the exact native sidecar that is packed into the NuGet on the - # same Xcode 27 image used by validation. - runs-on: xcode-27 + # Rebuild the exact native sidecar packed into NuGet with an App Store + # submission toolchain. Xcode 27 remains validation-only until Apple + # accepts its SDK for App Store uploads. + runs-on: macos-26 timeout-minutes: 60 steps: - uses: actions/checkout@v7 @@ -141,6 +146,11 @@ jobs: with: dotnet-version: "10.0.x" + - name: Set up App Store Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: ${{ env.APP_STORE_XCODE_VERSION }} + - name: Install MAUI workload run: dotnet workload install maui --skip-sign-check @@ -309,6 +319,9 @@ jobs: - name: Build OpenIAP.xcframework (Apple) run: bash packages/apple/scripts/build-xcframework.sh + - name: Verify App Store toolchain provenance + run: bash packages/apple/scripts/verify-app-store-xcframework.sh + - name: Build Play AAR (Google) working-directory: packages/google run: ./gradlew :openiap:assemblePlayRelease diff --git a/libraries/godot-iap/Makefile b/libraries/godot-iap/Makefile index 6ad966d10..03779b577 100644 --- a/libraries/godot-iap/Makefile +++ b/libraries/godot-iap/Makefile @@ -108,7 +108,16 @@ ios: setup ios-build # Build iOS frameworks using xcodebuild ios-build: @echo "$(GREEN)Building iOS frameworks...$(NC)" - @cd $(IOS_GDEXT_DIR) && xcodebuild -scheme GodotIap -sdk iphoneos -destination 'generic/platform=iOS' -configuration Release -derivedDataPath .build-xcode clean build + @cd $(IOS_GDEXT_DIR) && xcodebuild \ + -scheme GodotIap \ + -sdk iphoneos \ + -destination 'generic/platform=iOS' \ + -configuration Release \ + CLANG_ENABLE_CODE_COVERAGE=NO \ + ENABLE_CODE_COVERAGE=NO \ + SWIFT_ENABLE_CODE_COVERAGE=NO \ + -derivedDataPath .build-xcode \ + clean build @echo "$(GREEN)Copying frameworks to addon...$(NC)" @$(APPLE_FRAMEWORK_INSTALLER) \ $(IOS_GDEXT_DIR)/.build-xcode/Build/Products/Release-iphoneos/PackageFrameworks/SwiftGodotRuntime.framework \ @@ -125,7 +134,18 @@ macos: setup macos-build # Build macOS frameworks using xcodebuild macos-build: @echo "$(GREEN)Building macOS frameworks...$(NC)" - @cd "$(IOS_GDEXT_DIR)" && xcodebuild -scheme GodotIap -sdk macosx -destination 'platform=macOS' ARCHS="$(MACOS_ARCHS)" PRODUCT_BUNDLE_IDENTIFIER="dev.hyo.godot-iap.GodotIap" -configuration Release -derivedDataPath .build-xcode-macos build + @cd "$(IOS_GDEXT_DIR)" && xcodebuild \ + -scheme GodotIap \ + -sdk macosx \ + -destination 'platform=macOS' \ + ARCHS="$(MACOS_ARCHS)" \ + PRODUCT_BUNDLE_IDENTIFIER="dev.hyo.godot-iap.GodotIap" \ + -configuration Release \ + CLANG_ENABLE_CODE_COVERAGE=NO \ + ENABLE_CODE_COVERAGE=NO \ + SWIFT_ENABLE_CODE_COVERAGE=NO \ + -derivedDataPath .build-xcode-macos \ + build @echo "$(GREEN)Copying frameworks to addon...$(NC)" @$(APPLE_FRAMEWORK_INSTALLER) \ $(IOS_GDEXT_DIR)/.build-xcode-macos/Build/Products/Release/PackageFrameworks/SwiftGodotRuntime.framework \ diff --git a/libraries/godot-iap/README.md b/libraries/godot-iap/README.md index b31773eda..051ea982b 100644 --- a/libraries/godot-iap/README.md +++ b/libraries/godot-iap/README.md @@ -36,11 +36,13 @@ Visit the [documentation site](https://openiap.dev/docs/setup/godot) for [instal 3. Enable the plugin in **Project → Project Settings → Plugins** Native Apple API availability is fixed when the pre-built -`GodotIap.framework` is compiled. The verified Apple 27 offer-code result -requires a framework built with Xcode 27 or later; an Xcode 26-built framework -uses the legacy `null` result even on Apple 27. The published godot-iap 3.0.0 -iOS framework is built with Xcode 27 and its release workflow rejects an older -artifact. Custom builds must use Xcode 27 to retain that result path. +`GodotIap.framework` is compiled. Release artifacts use the current +App Store-accepted stable Xcode toolchain; CI rejects frameworks carrying an +Xcode 27 SDK or beta-linker signature. Until Apple accepts Xcode 27 for App +Store submissions, the Xcode 27-only verified offer-code result is unavailable +in release builds and the bridge returns the legacy `null` result. Custom +builds intended for App Store distribution must follow the same stable-toolchain +policy. Release zips are intended for iOS export and Android. If you use a release or custom build that includes `addons/godot-iap/bin/macos`, and Godot says diff --git a/libraries/godot-iap/addons/godot-iap/bin/ios/GodotIap.framework/GodotIap b/libraries/godot-iap/addons/godot-iap/bin/ios/GodotIap.framework/GodotIap index 560e2f1e0..3ff611c93 100755 Binary files a/libraries/godot-iap/addons/godot-iap/bin/ios/GodotIap.framework/GodotIap and b/libraries/godot-iap/addons/godot-iap/bin/ios/GodotIap.framework/GodotIap differ diff --git a/libraries/godot-iap/addons/godot-iap/bin/ios/SwiftGodotRuntime.framework/SwiftGodotRuntime b/libraries/godot-iap/addons/godot-iap/bin/ios/SwiftGodotRuntime.framework/SwiftGodotRuntime index ec428fce5..9f8acdc1d 100755 Binary files a/libraries/godot-iap/addons/godot-iap/bin/ios/SwiftGodotRuntime.framework/SwiftGodotRuntime and b/libraries/godot-iap/addons/godot-iap/bin/ios/SwiftGodotRuntime.framework/SwiftGodotRuntime differ diff --git a/libraries/godot-iap/scripts/verify-ios-toolchain.sh b/libraries/godot-iap/scripts/verify-ios-toolchain.sh index 44c89777c..e387f5177 100755 --- a/libraries/godot-iap/scripts/verify-ios-toolchain.sh +++ b/libraries/godot-iap/scripts/verify-ios-toolchain.sh @@ -3,6 +3,8 @@ set -euo pipefail FRAMEWORK_ROOT="${1:-addons/godot-iap/bin/ios}" +EXPECTED_SDK_VERSION="${APP_STORE_SDK_VERSION:-26.5}" +EXPECTED_LD_VERSION="${APP_STORE_LD_VERSION:-1267.0}" for binary in \ "$FRAMEWORK_ROOT/GodotIap.framework/GodotIap" \ @@ -15,9 +17,64 @@ for binary in \ build_info="$(xcrun vtool -show-build "$binary")" echo "$build_info" - if ! grep -Eq 'version[[:space:]]+27[0-9]+([.][0-9]+)?' <<<"$build_info"; then - echo "::error::$binary was not linked by the Xcode 27 toolchain." - echo "::error::Rebuild with DEVELOPER_DIR pointing at Xcode 27 before release." + if ! awk -v expected="$EXPECTED_SDK_VERSION" ' + $1 == "sdk" { + sdk_found = 1 + if ($2 != expected) { + sdk_failed = 1 + } + } + + END { + exit (sdk_found && !sdk_failed) ? 0 : 1 + } + ' <<<"$build_info"; then + echo "::error::$binary was not built with the expected App Store SDK $EXPECTED_SDK_VERSION." + exit 1 + fi + + if ! awk -v expected="$EXPECTED_LD_VERSION" ' + function finish_build() { + if (in_build && (!saw_ld || bad_ld)) { + failed = 1 + } + } + + $1 == "cmd" && $2 == "LC_BUILD_VERSION" { + finish_build() + in_build = 1 + builds += 1 + saw_ld = 0 + bad_ld = 0 + expect_ld_version = 0 + next + } + + in_build && $1 == "tool" { + expect_ld_version = ($2 == "LD") + next + } + + in_build && $1 == "version" && expect_ld_version { + saw_ld = 1 + if ($2 != expected) { + bad_ld = 1 + } + expect_ld_version = 0 + } + + END { + finish_build() + exit (builds > 0 && !failed) ? 0 : 1 + } + ' <<<"$build_info"; then + echo "::error::$binary was not linked by the expected stable linker $EXPECTED_LD_VERSION." + echo "::error::Rebuild with Xcode 26.6 / iPhoneOS SDK $EXPECTED_SDK_VERSION before release." + exit 1 + fi + + if otool -l "$binary" | grep -Eq '__LLVM_COV|__llvm_prf_'; then + echo "::error::$binary contains code-coverage instrumentation and is not a release artifact." exit 1 fi done diff --git a/libraries/maui-iap/README.md b/libraries/maui-iap/README.md index c6e6811f1..e26d57fe2 100644 --- a/libraries/maui-iap/README.md +++ b/libraries/maui-iap/README.md @@ -31,11 +31,11 @@ Billing, Play Services, Gson, AndroidX, and Kotlin Android libraries remain normal NuGet dependencies so apps can deduplicate them with their own package graph. -OpenIap.Maui 2.0.0 rebuilds its embedded Apple XCFramework with Xcode 27 before -packing the NuGet, so the guarded StoreKit 27 implementation is present for iOS -and macCatalyst. Custom source builds need Xcode 27 to retain those paths; -runtime availability still follows the Apple OS version documented for each -API. +Stable NuGet releases rebuild the embedded Apple XCFramework with the current +App Store-accepted toolchain (Xcode 26.6 / SDK 26.5) and verify every packaged +Mach-O slice before publishing. Xcode 27 remains a source-compatibility CI lane +until Apple accepts it for App Store submissions, so its guarded StoreKit 27 +paths are not included in stable release artifacts yet. ## Usage diff --git a/packages/apple/scripts/verify-app-store-xcframework.sh b/packages/apple/scripts/verify-app-store-xcframework.sh new file mode 100755 index 000000000..c3b8d76ef --- /dev/null +++ b/packages/apple/scripts/verify-app-store-xcframework.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash + +set -euo pipefail + +XCFRAMEWORK_ROOT="${1:-packages/apple/.build/xcframework/OpenIAP.xcframework}" +EXPECTED_XCODE_VERSION="${APP_STORE_XCODE_VERSION:-26.6}" +EXPECTED_SDK_VERSION="${APP_STORE_SDK_VERSION:-26.5}" +EXPECTED_LD_VERSION="${APP_STORE_LD_VERSION:-1267.0}" + +active_xcode="$(xcodebuild -version | sed -n '1p')" +active_sdk="$(xcrun --sdk iphoneos --show-sdk-version)" + +if [[ "$active_xcode" != "Xcode $EXPECTED_XCODE_VERSION" ]]; then + echo "::error::App Store artifacts must be built with Xcode $EXPECTED_XCODE_VERSION; active toolchain is $active_xcode." + exit 1 +fi + +if [[ "$active_sdk" != "$EXPECTED_SDK_VERSION" ]]; then + echo "::error::App Store artifacts must be built with iPhoneOS SDK $EXPECTED_SDK_VERSION; active SDK is $active_sdk." + exit 1 +fi + +found=0 +failed=0 + +while IFS= read -r -d '' binary; do + found=1 + build_info="$(xcrun vtool -show-build "$binary")" + echo "==> $binary" + echo "$build_info" + + if grep -Eq 'version[[:space:]]+27[0-9]+([.][0-9]+)?' <<<"$build_info"; then + echo "::error::$binary was linked by an Xcode 27 toolchain that is not accepted for App Store submissions." + failed=1 + fi + + if ! awk -v expected="$EXPECTED_LD_VERSION" ' + function finish_build() { + if (in_build && (!saw_ld || bad_ld)) { + failed = 1 + } + } + + $1 == "cmd" && $2 == "LC_BUILD_VERSION" { + finish_build() + in_build = 1 + builds += 1 + saw_ld = 0 + bad_ld = 0 + expect_ld_version = 0 + next + } + + in_build && $1 == "tool" { + expect_ld_version = ($2 == "LD") + next + } + + in_build && $1 == "version" && expect_ld_version { + saw_ld = 1 + if ($2 != expected) { + bad_ld = 1 + } + expect_ld_version = 0 + } + + END { + finish_build() + exit (builds > 0 && !failed) ? 0 : 1 + } + ' <<<"$build_info"; then + echo "::error::$binary was not linked by the expected stable linker $EXPECTED_LD_VERSION." + failed=1 + fi + + if otool -l "$binary" | grep -Eq '__LLVM_COV|__llvm_prf_'; then + echo "::error::$binary contains code-coverage instrumentation and is not a release artifact." + failed=1 + fi + + saw_sdk=0 + while IFS= read -r sdk_version; do + [[ -z "$sdk_version" ]] && continue + saw_sdk=1 + if [[ "$sdk_version" != "$EXPECTED_SDK_VERSION" ]]; then + echo "::error::$binary records SDK $sdk_version; expected $EXPECTED_SDK_VERSION." + failed=1 + fi + done < <(awk '$1 == "sdk" { print $2 }' <<<"$build_info") + + if [[ "$saw_sdk" -eq 0 ]]; then + echo "::error::$binary has no LC_BUILD_VERSION SDK entry." + failed=1 + fi +done < <( + find "$XCFRAMEWORK_ROOT" \ + \( -path '*/OpenIAP.framework/OpenIAP' -o -path '*/OpenIAP.framework/Versions/*/OpenIAP' \) \ + -type f \ + -print0 +) + +if [[ "$found" -eq 0 ]]; then + echo "::error::No OpenIAP.framework binaries found in $XCFRAMEWORK_ROOT." + exit 1 +fi + +if [[ "$failed" -ne 0 ]]; then + exit 1 +fi + +echo "Verified App Store toolchain: Xcode $EXPECTED_XCODE_VERSION / SDK $EXPECTED_SDK_VERSION / LD $EXPECTED_LD_VERSION." diff --git a/packages/docs/src/pages/docs/updates/releases.tsx b/packages/docs/src/pages/docs/updates/releases.tsx index bb7673adc..78c5fd7ef 100644 --- a/packages/docs/src/pages/docs/updates/releases.tsx +++ b/packages/docs/src/pages/docs/updates/releases.tsx @@ -23,6 +23,11 @@ interface Note { element: React.ReactNode; } +const appStoreToolchainPatchReleases = [ + ['godot-iap 3.0.1', 'godot-iap-3.0.1'], + ['OpenIap.Maui 2.0.1', 'maui-iap-2.0.1'], +] as const; + const reactNativeListenerLifecycleReleases = [ ['react-native-iap 16.0.1', 'react-native-iap-16.0.1'], ] as const; @@ -138,6 +143,141 @@ function Releases() { useScrollToHash(); const allNotes: Note[] = [ + // August 2, 2026 - App Store toolchain compatibility patch + { + id: 'app-store-toolchain-compatibility-2026-08-02', + date: new Date('2026-08-02'), + element: ( +
+ Publishes stable-toolchain rebuilds for the framework packages that
+ distribute precompiled Apple binaries. This fixes the App Store
+ validation failure reported in{' '}
+
+ issue #264
+
+ {', where '}
+ OpenIap.Maui 2.0.0 embedded an Xcode 27 beta
+ XCFramework whose LC_BUILD_VERSION recorded SDK 27.0,
+ above the App Store maximum of 26.5.
+
OpenIap.Maui 2.0.1 and rebuild the archive before
+ resubmitting it.
+