diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f1ce5b2..cb489a2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -54,7 +54,7 @@ jobs: - name: Verify prebuilt bridges are staged run: | - for triple in aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios aarch64-apple-darwin x86_64-apple-darwin; do + for triple in aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios aarch64-apple-tvos aarch64-apple-tvos-sim aarch64-apple-darwin x86_64-apple-darwin; do test -s "crate-ios/prebuilt/$triple/libstorekit_bridge.a" || { echo "::error::missing bridge for $triple"; exit 1; } done diff --git a/README.md b/README.md index 521203b..8ab00bf 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ # @perryts/storekit -StoreKit 2 in-app purchase bindings (iOS / macOS) for [Perry](https://github.com/PerryTS/perry). For Android, use [`@perryts/play-billing`](https://github.com/PerryTS/play-billing). +StoreKit 2 in-app purchase bindings (iOS / tvOS / macOS) for [Perry](https://github.com/PerryTS/perry). For Android, use [`@perryts/play-billing`](https://github.com/PerryTS/play-billing). ## Platforms | Target | Implementation | | --------------- | ----------------------------------------------------------------------- | | iOS 16+ | Native — Swift bridge over StoreKit 2 (`Product.products(for:)`, etc.). | +| tvOS 16+ | Native — same Swift bridge. | | macOS 13+ | Native — same Swift bridge. | | Android | Stub. Use [`@perryts/play-billing`](https://github.com/PerryTS/play-billing) for Google Play Billing. | | Linux / Windows | Stub — every call resolves with a `"not available"` JSON payload. | diff --git a/crate-ios/Cargo.toml b/crate-ios/Cargo.toml index 7258e12..601fc1c 100644 --- a/crate-ios/Cargo.toml +++ b/crate-ios/Cargo.toml @@ -10,7 +10,7 @@ repository = "https://github.com/PerryTS/storekit" crate-type = ["staticlib", "rlib"] [dependencies] -# Tracks the perry-ffi v0.5 ABI surface published from the -# `PerryTS/perry` workspace. Pinned to a git rev rather than a -# crates.io release until perry-ffi is published on crates.io. -perry-ffi = { git = "https://github.com/PerryTS/perry", branch = "main" } +# perry-ffi is pinned to the Perry release the package is tested with: +# `main` moves ahead of released compilers (and needs nightly since +# 0.6.0), and its ABI has to match the Perry that links the app. +perry-ffi = { git = "https://github.com/PerryTS/perry", tag = "v0.5.1664" } diff --git a/crate-ios/build.rs b/crate-ios/build.rs index c6237fa..f73cbcf 100644 --- a/crate-ios/build.rs +++ b/crate-ios/build.rs @@ -8,7 +8,7 @@ fn main() { // Non-Apple target: lib.rs compiles the stub fallback instead of the // Swift bridge, so there is nothing to build or link here. This keeps // the crate buildable on hosts without Xcode/swiftc (e.g. Linux CI). - if target_os != "ios" && target_os != "macos" { + if target_os != "ios" && target_os != "macos" && target_os != "tvos" { return; } @@ -48,6 +48,22 @@ fn main() { "iphonesimulator".to_string(), ) } + // Apple TV device (arm64) + t if t.contains("aarch64-apple-tvos") && !t.contains("sim") => { + let tvos_version = "16.0"; + ( + format!("arm64-apple-tvos{}", tvos_version), + "appletvos".to_string(), + ) + } + // Apple TV simulator (arm64) + t if t.contains("aarch64-apple-tvos-sim") => { + let tvos_version = "16.0"; + ( + format!("arm64-apple-tvos{}-simulator", tvos_version), + "appletvsimulator".to_string(), + ) + } // macOS (arm64) t if t.contains("aarch64-apple-darwin") => { let macos_version = "13.0"; @@ -187,8 +203,8 @@ fn compile_bridge_from_source( // Link Swift standard libraries // Find the Swift library path for the target platform - let swift_lib_dir = if target_os == "ios" { - // For iOS, Swift libraries are in the SDK + let swift_lib_dir = if target_os == "ios" || target_os == "tvos" { + // For iOS and tvOS, Swift libraries are in the SDK format!("{}/usr/lib/swift", sdk_path) } else { // For macOS, Swift libraries are in the toolchain diff --git a/crate-ios/src/apple.rs b/crate-ios/src/apple.rs index e02a352..86ffee0 100644 --- a/crate-ios/src/apple.rs +++ b/crate-ios/src/apple.rs @@ -5,9 +5,9 @@ //! as the callback "context", and returns the same pointer to the perry //! runtime. When the Swift side finishes its //! `Task { … }` it invokes [`storekit_callback`] from whatever thread the -//! Swift concurrency runtime parked the continuation on. The perry-ffi -//! resolution machinery is `Send`-safe and queues the actual fulfillment -//! back onto perry's main thread. +//! Swift concurrency runtime parked the continuation on. The callback copies +//! the result and settles the promise with `resolve_with`, so the JS string +//! is allocated on perry's main thread during the resolution pump. //! //! Swift returns a JSON string for every call; the TypeScript wrapper //! parses it. We do not attempt to construct typed JsValues on the Rust @@ -15,7 +15,7 @@ //! Swift bridge trivial and avoids leaking perry-runtime layout into //! Swift code. -use perry_ffi::{read_string, JsPromise, JsString, Promise, StringHeader}; +use perry_ffi::{alloc_string, read_string, JsPromise, JsString, JsValue, Promise, StringHeader}; use std::ffi::CString; use std::os::raw::{c_char, c_void}; @@ -56,14 +56,18 @@ extern "C" { /// runtime carry the result back to the awaiter. extern "C" fn storekit_callback(context: *mut c_void, result: *const c_char) { let promise = unsafe { JsPromise::from_raw(context as *mut Promise) }; - let result_str = if result.is_null() { - "{\"error\":\"null result from StoreKit bridge\"}" + let result_str: String = if result.is_null() { + "{\"error\":\"null result from StoreKit bridge\"}".to_owned() } else { unsafe { std::ffi::CStr::from_ptr(result) } .to_str() .unwrap_or("{\"error\":\"non-UTF8 result from StoreKit bridge\"}") + .to_owned() }; - promise.resolve_string(result_str); + // Swift calls back on a concurrency-runtime thread. Building the JS string + // there would allocate it in that thread's arena (undefined behaviour, + // perry#1824): copy the text here and allocate it on the main thread. + promise.resolve_with(move || JsValue::from_string_ptr(alloc_string(&result_str).as_raw())); } /// Read a perry JS string into an owned `CString` we can hand to Swift. diff --git a/crate-ios/src/lib.rs b/crate-ios/src/lib.rs index 7e696d6..0e97c70 100644 --- a/crate-ios/src/lib.rs +++ b/crate-ios/src/lib.rs @@ -1,13 +1,13 @@ //! StoreKit 2 bindings for Perry — closes PerryTS/perry#537. //! -//! On iOS and macOS targets this crate links the Swift StoreKit 2 bridge +//! On iOS, tvOS and macOS targets this crate links the Swift StoreKit 2 bridge //! (see [`apple`]). On every other target it compiles to the same stub //! behavior as `crate-stub` — no Swift toolchain, no Apple SDK — so the //! crate builds on any machine with a Rust toolchain (`build.rs` skips //! the `swiftc` step for non-Apple targets). -#[cfg(any(target_os = "ios", target_os = "macos"))] +#[cfg(any(target_os = "ios", target_os = "macos", target_os = "tvos"))] mod apple; -#[cfg(not(any(target_os = "ios", target_os = "macos")))] +#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "tvos")))] mod stub; diff --git a/crate-stub/Cargo.toml b/crate-stub/Cargo.toml index 1dc5fe6..bafc1a7 100644 --- a/crate-stub/Cargo.toml +++ b/crate-stub/Cargo.toml @@ -10,4 +10,4 @@ repository = "https://github.com/PerryTS/storekit" crate-type = ["staticlib", "rlib"] [dependencies] -perry-ffi = { git = "https://github.com/PerryTS/perry", branch = "main" } +perry-ffi = { git = "https://github.com/PerryTS/perry", tag = "v0.5.1664" } diff --git a/package.json b/package.json index 46f744b..393ab76 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@perryts/storekit", - "version": "0.1.3", - "description": "StoreKit 2 in-app purchase bindings (iOS / macOS) for the Perry TypeScript-to-native compiler. For Android, see @perryts/play-billing.", + "version": "0.1.4", + "description": "StoreKit 2 in-app purchase bindings (iOS / tvOS / macOS) for the Perry TypeScript-to-native compiler. For Android, see @perryts/play-billing.", "license": "MIT", "repository": { "type": "git", @@ -18,6 +18,7 @@ "in-app-purchase", "iap", "ios", + "tvos", "macos", "subscriptions" ], @@ -55,6 +56,11 @@ "lib": "libperry_storekit.a", "frameworks": ["StoreKit", "Foundation"] }, + "tvos": { + "crate": "crate-ios", + "lib": "libperry_storekit.a", + "frameworks": ["StoreKit", "Foundation"] + }, "macos": { "crate": "crate-ios", "lib": "libperry_storekit.a", diff --git a/scripts/build-bridges.sh b/scripts/build-bridges.sh index 96379b3..9edb900 100755 --- a/scripts/build-bridges.sh +++ b/scripts/build-bridges.sh @@ -2,7 +2,7 @@ # Build the Swift StoreKit bridge for every Apple Rust target and stage the # archives under crate-ios/prebuilt//libstorekit_bridge.a. # -# Must run on macOS with Xcode (swiftc + the iOS/macOS SDKs). The archives +# Must run on macOS with Xcode (swiftc + the iOS/tvOS/macOS SDKs). The archives # are shipped in the npm package so non-Mac hosts (Linux CI, perry's Linux # builders) can produce iOS/macOS libraries without a Swift toolchain — # crate-ios/build.rs links these when swiftc is unavailable. @@ -21,6 +21,8 @@ TARGETS=( "aarch64-apple-ios:arm64-apple-ios16.0:iphoneos" "aarch64-apple-ios-sim:arm64-apple-ios16.0-simulator:iphonesimulator" "x86_64-apple-ios:x86_64-apple-ios16.0-simulator:iphonesimulator" + "aarch64-apple-tvos:arm64-apple-tvos16.0:appletvos" + "aarch64-apple-tvos-sim:arm64-apple-tvos16.0-simulator:appletvsimulator" "aarch64-apple-darwin:arm64-apple-macosx13.0:macosx" "x86_64-apple-darwin:x86_64-apple-macosx13.0:macosx" )