Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
35 changes: 31 additions & 4 deletions .github/workflows/ci-maui-iap.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
23 changes: 18 additions & 5 deletions .github/workflows/release-maui.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
24 changes: 22 additions & 2 deletions libraries/godot-iap/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -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 \
Expand Down
12 changes: 7 additions & 5 deletions libraries/godot-iap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Binary file not shown.
Binary file not shown.
63 changes: 60 additions & 3 deletions libraries/godot-iap/scripts/verify-ios-toolchain.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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" \
Expand All @@ -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
10 changes: 5 additions & 5 deletions libraries/maui-iap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
111 changes: 111 additions & 0 deletions packages/apple/scripts/verify-app-store-xcframework.sh
Original file line number Diff line number Diff line change
@@ -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."
Loading