diff --git a/.github/workflows/npm_release.yml b/.github/workflows/npm_release.yml index caa1c84e3..f040b472c 100644 --- a/.github/workflows/npm_release.yml +++ b/.github/workflows/npm_release.yml @@ -26,7 +26,7 @@ permissions: # (github.com/NativeScript/ios-spm) at them via binaryTarget(url:checksum:). The # slim npm package only carries the Xcode project template + metadata generator. # -# Flow: setup → build (ios, visionos) → test → github-release (assets) → +# Flow: setup → build (ios, visionos, tvos) → test → github-release (assets) → # spm-update (generate manifest + tag ios-spm) → publish (npm) → verify-spm # # The release half (github-release, spm-update, publish, verify-spm) is gated on @@ -74,10 +74,13 @@ jobs: needs: setup strategy: fail-fast: false - # Computed in `setup`: ios always; visionos only for real releases - # (npm_tag != "next"), never for the rolling "next" channel. Each entry - # carries `target` (package identity, @nativescript/) and `script` - # (the npm build script; note the vision script is build-vision). + # Computed in `setup`: ios always; visionos and tvos only for real + # releases (npm_tag != "next"), never for the rolling "next" channel. Each + # entry carries `target` (package identity, @nativescript/) and + # `script` (the npm build script; note the vision script is build-vision). + # The tvos leg installs the pinned V8 release's tvOS slices + # (download_v8.sh --tvos), so V8_RELEASE must point at a + # v8-buildscripts release that ships them. matrix: ${{ fromJSON(needs.setup.outputs.build_matrix) }} env: NPM_VERSION: ${{ needs.setup.outputs.npm_version }} @@ -323,9 +326,9 @@ jobs: - name: Generate Package.swift # The generator emits the WHOLE manifest, shaped by the checksums it # finds: "next" builds produce only checksums-ios.env, so the next - # manifest omits the visionOS product/targets whose Release assets - # don't exist (SwiftPM eagerly downloads every binaryTarget in a - # resolved manifest — an unshipped asset 404s for every consumer). + # manifest omits the visionOS and tvOS products/targets whose Release + # assets don't exist (SwiftPM eagerly downloads every binaryTarget in + # a resolved manifest — an unshipped asset 404s for every consumer). # Any non-"next" --channel requires the full checksum set, so a real # release can never silently ship an iOS-only manifest. run: | @@ -367,9 +370,9 @@ jobs: id-token: write strategy: fail-fast: false - # Same targets as the build matrix (computed in `setup`): visionos is - # published only for real releases, never for the "next" channel. The - # unused `script` key on each entry is harmless here. + # Same targets as the build matrix (computed in `setup`): visionos and + # tvos are published only for real releases, never for the "next" + # channel. The unused `script` key on each entry is harmless here. matrix: ${{ fromJSON(needs.setup.outputs.build_matrix) }} env: NPM_VERSION: ${{ needs.setup.outputs.npm_version }} @@ -395,8 +398,11 @@ jobs: test "$(npm --version)" = "11.5.1" test "$(npx --version)" = "11.5.1" - name: Resolve package name - run: | - if [ "${{ matrix.target }}" = "ios" ]; then echo "PKG=ios" >> $GITHUB_ENV; else echo "PKG=visionos" >> $GITHUB_ENV; fi + # matrix.target is the package suffix: @nativescript/ + env: + # env indirection keeps matrix values out of shell interpolation + TARGET: ${{ matrix.target }} + run: echo "PKG=$TARGET" >> $GITHUB_ENV - name: Publish package (OIDC trusted publishing) if: ${{ vars.USE_NPM_TOKEN != 'true' }} run: | @@ -422,9 +428,10 @@ jobs: # One probe for every channel, against the REAL released manifest. The # manifest's target set is channel-shaped by generate-spm-manifest.mjs # ("next" manifests only declare the iOS targets, because next Releases have - # no visionos assets), so resolving ios-spm at the released tag exercises - # exactly the artifact set a consumer's xcodebuild will download: iOS-only on - # next, iOS + visionos on real releases (v* tag / manual version). + # no visionos/tvos assets), so resolving ios-spm at the released tag + # exercises exactly the artifact set a consumer's xcodebuild will download: + # iOS-only on next, iOS + visionos + tvos on real releases (v* tag / manual + # version). verify-spm: name: Verify SPM resolution runs-on: macos-15 @@ -466,6 +473,7 @@ jobs: # Resolution fetches each binaryTarget's zip and verifies its SHA-256 # against the manifest checksum; a mismatch or a missing release asset # fails the release here. For next the manifest declares the iOS - # artifacts only; real releases also declare (and verify) visionos. + # artifacts only; real releases also declare (and verify) visionos + # and tvos. swift package resolve echo "ios-spm@$NPM_VERSION resolved and checksum-verified." diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 41999f7da..c0ce174cb 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -27,6 +27,13 @@ jobs: path: ./llvm key: ${{ hashFiles('download_llvm.sh') }} - uses: ./.github/actions/setup-build-env + - name: Template and SwiftPM manifest checks + # tvOS is not built on PRs (like visionOS), but its template must stay + # the iOS template plus the tvOS settings, and the ios-spm manifest + # generator must still accept every platform's checksums. + run: | + python3 tests/tvos/template.py + python3 tests/spm/manifest.py - name: Get Current Version run: | echo NPM_VERSION=$(node -e "console.log(require('./package.json').version);") >> $GITHUB_ENV diff --git a/.gitignore b/.gitignore index 1f93390bd..6bcddc181 100644 --- a/.gitignore +++ b/.gitignore @@ -62,6 +62,8 @@ v8_build /project-template-ios/__PROJECT_NAME__.xcodeproj/project.xcworkspace/xcshareddata/ /project-template-vision/.build_env_vars.sh /project-template-vision/__PROJECT_NAME__.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +/project-template-tvos/.build_env_vars.sh +/project-template-tvos/__PROJECT_NAME__.xcodeproj/project.xcworkspace/ # V8 prebuilts, fetched by download_v8.sh from the release # pinned in V8_RELEASE. Not committed: the full matrix cannot be built @@ -93,3 +95,4 @@ NativeScript/inspector/absl/ #AI agents .claude/* !.claude/settings.json +NativeScript/lib/.tvos-v8-version diff --git a/NativeScript/lib/arm64-appletvos/libffi.a b/NativeScript/lib/arm64-appletvos/libffi.a new file mode 100644 index 000000000..8d882d83b Binary files /dev/null and b/NativeScript/lib/arm64-appletvos/libffi.a differ diff --git a/NativeScript/lib/arm64-appletvsimulator/libffi.a b/NativeScript/lib/arm64-appletvsimulator/libffi.a new file mode 100644 index 000000000..926c7d9b9 Binary files /dev/null and b/NativeScript/lib/arm64-appletvsimulator/libffi.a differ diff --git a/NativeScript/runtime/NativeScriptException.mm b/NativeScript/runtime/NativeScriptException.mm index e3f8ebb98..c2f5997a0 100644 --- a/NativeScript/runtime/NativeScriptException.mm +++ b/NativeScript/runtime/NativeScriptException.mm @@ -1469,8 +1469,11 @@ static void ShowErrorModalSynchronously(const std::string& title, const std::str alpha:1.0]; // Terminal green stackTraceTextView.backgroundColor = [UIColor clearColor]; stackTraceTextView.font = [UIFont fontWithName:@"Menlo" size:16]; // Monospace +#if !TARGET_OS_TV + // tvOS UITextView is never editable/selectable; the properties do not exist there. stackTraceTextView.editable = NO; stackTraceTextView.selectable = YES; +#endif stackTraceTextView.scrollEnabled = YES; stackTraceTextView.contentInset = UIEdgeInsetsMake(15, 15, 15, 15); stackTraceTextView.translatesAutoresizingMaskIntoConstraints = NO; diff --git a/V8_RELEASE b/V8_RELEASE index ac872018f..18dd75643 100644 --- a/V8_RELEASE +++ b/V8_RELEASE @@ -1 +1 @@ -v8-14.9.207.39-7 +v8-14.9.207.39-8 diff --git a/build_all_tvos.sh b/build_all_tvos.sh new file mode 100755 index 000000000..db03139ec --- /dev/null +++ b/build_all_tvos.sh @@ -0,0 +1,34 @@ +#!/bin/bash +set -e + +# Arguments (e.g. --spm-mode ) are forwarded to +# build_npm_tvos.sh; run it with --help for details. +for arg in "$@"; do + if [ "$arg" = "-h" ] || [ "$arg" = "--help" ]; then + echo "Usage: ./build_all_tvos.sh [--spm-mode ]" + echo "Arguments are forwarded to build_npm_tvos.sh (see ./build_npm_tvos.sh --help)." + echo "" + echo "V8: the pinned release's tvOS slices are installed with download_v8.sh --tvos." + echo "Set V8_TVOS_BUILD to a built NativeScript/v8-buildscripts checkout to use a" + echo "source build instead (see scripts/install-tvos-v8.sh)." + exit 0 + fi +done + +if [ -n "${V8_TVOS_BUILD:-}" ]; then + ./scripts/install-tvos-v8.sh "$V8_TVOS_BUILD" + # The source build supplied the archives, headers and inspector sources; + # the pinned release must not overwrite them from here on. + export V8_SKIP_DOWNLOAD=1 +else + ./download_v8.sh --tvos +fi + +rm -rf ./dist +./update_version.sh +./build_metadata_generator.sh +./build_nativescript.sh --no-catalyst --no-iphone --no-sim --no-vision --tvos +./build_tklivesync.sh --no-catalyst --no-iphone --no-sim --no-vision --tvos +./prepare_dSYMs.sh +./build_spm_artifacts.sh tvos +./build_npm_tvos.sh "$@" diff --git a/build_libffi.sh b/build_libffi.sh index 45545ece0..52852f5b5 100755 --- a/build_libffi.sh +++ b/build_libffi.sh @@ -21,6 +21,8 @@ BUILD_ROOT="$SCRIPT_DIR/build/libffi" DIST_DIR="$BUILD_ROOT/dist" ALL_SLICES=( + arm64-appletvos + arm64-appletvsimulator arm64-iphoneos arm64-iphonesimulator arm64-xros @@ -29,6 +31,7 @@ ALL_SLICES=( x86_64-maccatalyst ) +TVOS_MIN=13.0 IOS_MIN=13.0 XROS_MIN=2.0 CATALYST_MIN=13.3 @@ -98,6 +101,12 @@ slice_config() { arm64-iphonesimulator) echo "iphonesimulator|aarch64-apple-darwin|-arch arm64 -mios-simulator-version-min=$IOS_MIN" ;; + arm64-appletvos) + echo "appletvos|aarch64-apple-darwin|-target arm64-apple-tvos$TVOS_MIN" + ;; + arm64-appletvsimulator) + echo "appletvsimulator|aarch64-apple-darwin|-target arm64-apple-tvos$TVOS_MIN-simulator" + ;; arm64-xros) echo "xros|aarch64-apple-darwin|-target arm64-apple-xros$XROS_MIN" ;; diff --git a/build_nativescript.sh b/build_nativescript.sh index 0311fcb8f..b1eb7733a 100755 --- a/build_nativescript.sh +++ b/build_nativescript.sh @@ -31,6 +31,7 @@ BUILD_CATALYST=$(to_bool ${BUILD_CATALYST:=true}) BUILD_IPHONE=$(to_bool ${BUILD_IPHONE:=true}) BUILD_SIMULATOR=$(to_bool ${BUILD_SIMULATOR:=true}) BUILD_VISION=$(to_bool ${BUILD_VISION:=true}) +BUILD_TVOS=$(to_bool ${BUILD_TVOS:=false}) VERBOSE=$(to_bool ${VERBOSE:=false}) for arg in $@; do @@ -43,6 +44,8 @@ for arg in $@; do --no-iphone|--no-device) BUILD_IPHONE=false ;; --xr|--vision) BUILD_VISION=true ;; --no-xr|--no-vision) BUILD_VISION=false ;; + --tv|--tvos) BUILD_TVOS=true ;; + --no-tv|--no-tvos) BUILD_TVOS=false ;; --verbose|-v) VERBOSE=true ;; *) ;; esac @@ -60,14 +63,14 @@ mkdir -p $DIST mkdir -p $DIST/intermediates checkpoint "Cleanup NativeScript" -xcodebuild -project v8ios.xcodeproj \ +xcodebuild SYMROOT="$DIST/build" -project v8ios.xcodeproj \ -target "NativeScript" \ -configuration Release clean \ $QUIET if $BUILD_CATALYST; then checkpoint "Building NativeScript for Mac Catalyst" -xcodebuild archive -project v8ios.xcodeproj \ +xcodebuild archive -derivedDataPath "$DIST/DerivedData" -project v8ios.xcodeproj \ -scheme "NativeScript" \ -configuration Release \ -destination "platform=macOS,variant=Mac Catalyst" \ @@ -81,7 +84,7 @@ fi if $BUILD_SIMULATOR; then checkpoint "Building NativeScript for iphone simulators (multi-arch)" -xcodebuild archive -project v8ios.xcodeproj \ +xcodebuild archive -derivedDataPath "$DIST/DerivedData" -project v8ios.xcodeproj \ -scheme "NativeScript" \ -configuration Release \ -destination "generic/platform=iOS Simulator" \ @@ -96,7 +99,7 @@ fi if $BUILD_IPHONE; then checkpoint "Building NativeScript for ARM64 device" -xcodebuild archive -project v8ios.xcodeproj \ +xcodebuild archive -derivedDataPath "$DIST/DerivedData" -project v8ios.xcodeproj \ -scheme "NativeScript" \ -configuration Release \ -destination "generic/platform=iOS" \ @@ -111,7 +114,7 @@ fi if $BUILD_CATALYST; then checkpoint "Building NativeScript for Mac Catalyst" -xcodebuild archive -project v8ios.xcodeproj \ +xcodebuild archive -derivedDataPath "$DIST/DerivedData" -project v8ios.xcodeproj \ -scheme "NativeScript" \ -configuration Release \ -destination "generic/platform=macOS,variant=Mac Catalyst" \ @@ -125,7 +128,7 @@ fi if $BUILD_VISION; then checkpoint "Building NativeScript for visionOS Device" -xcodebuild archive -project v8ios.xcodeproj \ +xcodebuild archive -derivedDataPath "$DIST/DerivedData" -project v8ios.xcodeproj \ -scheme "NativeScript" \ -configuration Release \ -destination "generic/platform=visionOS" \ @@ -138,7 +141,7 @@ xcodebuild archive -project v8ios.xcodeproj \ -archivePath $DIST/intermediates/NativeScript.xros.xcarchive checkpoint "Building NativeScript for visionOS Simulators" -xcodebuild archive -project v8ios.xcodeproj \ +xcodebuild archive -derivedDataPath "$DIST/DerivedData" -project v8ios.xcodeproj \ -scheme "NativeScript" \ -configuration Release \ -destination "generic/platform=visionOS Simulator" \ @@ -151,6 +154,35 @@ xcodebuild archive -project v8ios.xcodeproj \ -archivePath $DIST/intermediates/NativeScript.xrsimulator.xcarchive fi +if $BUILD_TVOS; then + +checkpoint "Building NativeScript for tvOS Device" +xcodebuild archive -derivedDataPath "$DIST/DerivedData" -project v8ios.xcodeproj \ + -scheme "NativeScript" \ + -configuration Release \ + -destination "generic/platform=tvOS" \ + $QUIET \ + EXCLUDED_ARCHS="i386 x86_64" \ + VALID_ARCHS=arm64 \ + DEVELOPMENT_TEAM=$DEV_TEAM \ + SKIP_INSTALL=NO \ + BUILD_LIBRARY_FOR_DISTRIBUTION=YES \ + -archivePath $DIST/intermediates/NativeScript.appletvos.xcarchive + +checkpoint "Building NativeScript for tvOS Simulators" +xcodebuild archive -derivedDataPath "$DIST/DerivedData" -project v8ios.xcodeproj \ + -scheme "NativeScript" \ + -configuration Release \ + -destination "generic/platform=tvOS Simulator" \ + $QUIET \ + EXCLUDED_ARCHS="i386 x86_64" \ + VALID_ARCHS=arm64 \ + DEVELOPMENT_TEAM=$DEV_TEAM \ + SKIP_INSTALL=NO \ + BUILD_LIBRARY_FOR_DISTRIBUTION=YES \ + -archivePath $DIST/intermediates/NativeScript.appletvsimulator.xcarchive +fi + XCFRAMEWORKS=() if $BUILD_CATALYST; then XCFRAMEWORKS+=( -framework "$DIST/intermediates/NativeScript.maccatalyst.xcarchive/Products/Library/Frameworks/NativeScript.framework" \ @@ -174,6 +206,13 @@ if $BUILD_VISION; then -debug-symbols "$DIST/intermediates/NativeScript.xrsimulator.xcarchive/dSYMs/NativeScript.framework.dSYM" ) fi +if $BUILD_TVOS; then + XCFRAMEWORKS+=( -framework "$DIST/intermediates/NativeScript.appletvos.xcarchive/Products/Library/Frameworks/NativeScript.framework" \ + -debug-symbols "$DIST/intermediates/NativeScript.appletvos.xcarchive/dSYMs/NativeScript.framework.dSYM" ) + XCFRAMEWORKS+=( -framework "$DIST/intermediates/NativeScript.appletvsimulator.xcarchive/Products/Library/Frameworks/NativeScript.framework" \ + -debug-symbols "$DIST/intermediates/NativeScript.appletvsimulator.xcarchive/dSYMs/NativeScript.framework.dSYM" ) +fi + checkpoint "Creating NativeScript.xcframework" OUTPUT_DIR="$DIST/NativeScript.xcframework" rm -rf $OUTPUT_DIR diff --git a/build_npm_tvos.sh b/build_npm_tvos.sh new file mode 100755 index 000000000..dd27b22e7 --- /dev/null +++ b/build_npm_tvos.sh @@ -0,0 +1,74 @@ +#!/bin/bash +set -e +source "$(dirname "$0")/build_utils.sh" + +usage() { + echo "Usage: ./build_npm_tvos.sh [--spm-mode ]" + echo "" + echo " --spm-mode embedded (default) self-contained package: the xcframework zips" + echo " from dist/artifacts are embedded at" + echo " framework/internal/local-spm and the app template" + echo " references them by relative path. Portable — for local" + echo " testing (ns platform add tvos --framework-path=...)." + echo " --spm-mode remote deploy shape used by the release workflow: no binaries" + echo " embedded; the app template pins" + echo " github.com/NativeScript/ios-spm at exactly this package" + echo " version. Only resolves for versions shipped by the" + echo " release pipeline." + echo " -h, --help show this help" +} + +parse_spm_mode_args "$@" + +checkpoint "Preparing npm package for tvOS ($SPM_MODE SwiftPM mode)..." +OUTPUT_DIR="dist/npm" +rm -rf "$OUTPUT_DIR" +mkdir -p "$OUTPUT_DIR" +mkdir -p "$OUTPUT_DIR/framework" +cp ./package.json "$OUTPUT_DIR" +cp -r ./types "$OUTPUT_DIR/types" + +cp -r "./project-template-tvos/" "$OUTPUT_DIR/framework" + +# The runtime xcframeworks are consumed via SwiftPM in both modes; what differs +# is where the SwiftPM package lives (see usage above). The zips come from +# build_spm_artifacts.sh — zipped because npm strips symlinks and SwiftPM +# extracts local zip binary targets itself. +if [ "$SPM_MODE" = "embedded" ]; then + ARTIFACTS_DIR="dist/artifacts" + LOCAL_SPM_DIR="$OUTPUT_DIR/framework/internal/local-spm" + for zip in NativeScript.tvos.xcframework.zip TKLiveSync.tvos.xcframework.zip; do + if [ ! -f "$ARTIFACTS_DIR/$zip" ]; then + echo "Missing $ARTIFACTS_DIR/$zip — run ./build_spm_artifacts.sh tvos first." >&2 + exit 1 + fi + done + mkdir -p "$LOCAL_SPM_DIR" + cp "./spm-templates/local-spm-tvos/Package.swift" "$LOCAL_SPM_DIR/" + cp "$ARTIFACTS_DIR/NativeScript.tvos.xcframework.zip" \ + "$ARTIFACTS_DIR/TKLiveSync.tvos.xcframework.zip" \ + "$LOCAL_SPM_DIR/" + node ./scripts/stamp-template-local-spm.mjs \ + "$OUTPUT_DIR/framework/__PROJECT_NAME__.xcodeproj/project.pbxproj" \ + "internal/local-spm" \ + --package-dir "$LOCAL_SPM_DIR" +else + NPM_VERSION=$(node -e "console.log(require('./package.json').version)") + node ./scripts/stamp-template-version.mjs \ + "$OUTPUT_DIR/framework/__PROJECT_NAME__.xcodeproj/project.pbxproj" \ + "$NPM_VERSION" +fi + +# Build-time metadata generator is still shipped in npm (Phase 1). +mkdir -p "$OUTPUT_DIR/framework/internal/metadata-generator-x86_64" +cp -r "metadata-generator/dist/x86_64/." "$OUTPUT_DIR/framework/internal/metadata-generator-x86_64" + +mkdir -p "$OUTPUT_DIR/framework/internal/metadata-generator-arm64" +cp -r "metadata-generator/dist/arm64/." "$OUTPUT_DIR/framework/internal/metadata-generator-arm64" + +pushd "$OUTPUT_DIR" +npm pack +mv *.tgz ../ +popd + +checkpoint "npm package created." diff --git a/build_spm_artifacts.sh b/build_spm_artifacts.sh index a9b35c3d5..d096db35d 100755 --- a/build_spm_artifacts.sh +++ b/build_spm_artifacts.sh @@ -6,7 +6,7 @@ # Output goes to dist/artifacts/: # .zip and a combined checksums.env (KEY=sha256 lines) # -# Usage: ./build_spm_artifacts.sh [ios|visionos] (default: ios) +# Usage: ./build_spm_artifacts.sh [ios|visionos|tvos] (default: ios) # # These artifacts are uploaded to the GitHub Release and referenced by # github.com/NativeScript/ios-spm (see scripts/generate-spm-manifest.mjs). @@ -31,8 +31,15 @@ case "$TARGET" in NS_KEY="NS_CHECKSUM_NATIVESCRIPT_VISIONOS" TK_KEY="NS_CHECKSUM_TKLIVESYNC_VISIONOS" ;; + tvos|tv) + TARGET="tvos" + NS_ZIP="NativeScript.tvos.xcframework.zip" + TK_ZIP="TKLiveSync.tvos.xcframework.zip" + NS_KEY="NS_CHECKSUM_NATIVESCRIPT_TVOS" + TK_KEY="NS_CHECKSUM_TKLIVESYNC_TVOS" + ;; *) - echo "Unknown target '$TARGET' (expected ios or visionos)" >&2 + echo "Unknown target '$TARGET' (expected ios, visionos or tvos)" >&2 exit 1 ;; esac @@ -69,8 +76,8 @@ zip_xcframework "TKLiveSync.xcframework" "$TK_ZIP" NS_SUM="$(compute_checksum "$OUT/$NS_ZIP")" TK_SUM="$(compute_checksum "$OUT/$TK_ZIP")" -# Per-target filename so the iOS and visionOS env files don't collide when the -# release/stamp jobs merge both platforms' artifacts into one directory. +# Per-target filename so the per-platform env files don't collide when the +# release/stamp jobs merge every platform's artifacts into one directory. CHECKSUMS="$OUT/checksums-$TARGET.env" { echo "${NS_KEY}=${NS_SUM}" diff --git a/build_tklivesync.sh b/build_tklivesync.sh index e24a0e3b1..e798a3024 100755 --- a/build_tklivesync.sh +++ b/build_tklivesync.sh @@ -27,6 +27,7 @@ BUILD_CATALYST=$(to_bool ${BUILD_CATALYST:=true}) BUILD_IPHONE=$(to_bool ${BUILD_IPHONE:=true}) BUILD_SIMULATOR=$(to_bool ${BUILD_SIMULATOR:=true}) BUILD_VISION=$(to_bool ${BUILD_VISION:=true}) +BUILD_TVOS=$(to_bool ${BUILD_TVOS:=false}) VERBOSE=$(to_bool ${VERBOSE:=false}) for arg in $@; do @@ -39,6 +40,8 @@ for arg in $@; do --no-iphone|--no-device) BUILD_IPHONE=false ;; --xr|--vision) BUILD_VISION=true ;; --no-xr|--no-vision) BUILD_VISION=false ;; + --tv|--tvos) BUILD_TVOS=true ;; + --no-tv|--no-tvos) BUILD_TVOS=false ;; --verbose|-v) VERBOSE=true ;; *) ;; esac @@ -51,7 +54,7 @@ mkdir -p $DIST/intermediates #cleanup checkpoint "Cleanup TKLiveSync" -xcodebuild -project v8ios.xcodeproj \ +xcodebuild SYMROOT="$DIST/build" -project v8ios.xcodeproj \ -target TKLiveSync \ -configuration Release clean \ -quiet @@ -59,7 +62,7 @@ xcodebuild -project v8ios.xcodeproj \ if $BUILD_SIMULATOR; then # generates library for simulator targets (usually includes arm64, x86_64) checkpoint "Building TKLiveSync for iphone simulators (multi-arch)" -xcodebuild archive -project v8ios.xcodeproj \ +xcodebuild archive -derivedDataPath "$DIST/DerivedData" -project v8ios.xcodeproj \ -scheme TKLiveSync \ -configuration Release \ -destination "generic/platform=iOS Simulator" \ @@ -72,7 +75,7 @@ fi if $BUILD_IPHONE; then #generates library for device target checkpoint "Building TKLiveSync for ARM64 device" -xcodebuild archive -project v8ios.xcodeproj \ +xcodebuild archive -derivedDataPath "$DIST/DerivedData" -project v8ios.xcodeproj \ -scheme TKLiveSync \ -configuration Release \ -destination "generic/platform=iOS" \ @@ -85,7 +88,7 @@ fi if $BUILD_CATALYST; then #generates library for Mac Catalyst target checkpoint "Building TKLiveSync for Mac Catalyst" -xcodebuild archive -project v8ios.xcodeproj \ +xcodebuild archive -derivedDataPath "$DIST/DerivedData" -project v8ios.xcodeproj \ -scheme TKLiveSync \ -configuration Release \ -destination "generic/platform=macOS,variant=Mac Catalyst" \ @@ -98,7 +101,7 @@ fi if $BUILD_VISION; then #generates library for visionOS targets checkpoint "Building TKLiveSync for visionOS Simulators" -xcodebuild archive -project v8ios.xcodeproj \ +xcodebuild archive -derivedDataPath "$DIST/DerivedData" -project v8ios.xcodeproj \ -scheme "TKLiveSync" \ -configuration Release \ -destination "generic/platform=visionOS Simulator" \ @@ -108,7 +111,7 @@ xcodebuild archive -project v8ios.xcodeproj \ -archivePath $DIST/intermediates/TKLiveSync.xrsimulator.xcarchive checkpoint "Building TKLiveSync for visionOS Device" -xcodebuild archive -project v8ios.xcodeproj \ +xcodebuild archive -derivedDataPath "$DIST/DerivedData" -project v8ios.xcodeproj \ -scheme "TKLiveSync" \ -configuration Release \ -destination "generic/platform=visionOS" \ @@ -118,6 +121,28 @@ xcodebuild archive -project v8ios.xcodeproj \ -archivePath $DIST/intermediates/TKLiveSync.xros.xcarchive fi +if $BUILD_TVOS; then +checkpoint "Building TKLiveSync for tvOS Simulators" +xcodebuild archive -derivedDataPath "$DIST/DerivedData" -project v8ios.xcodeproj \ + -scheme "TKLiveSync" \ + -configuration Release \ + -destination "generic/platform=tvOS Simulator" \ + -quiet \ + SKIP_INSTALL=NO \ + BUILD_LIBRARY_FOR_DISTRIBUTION=YES \ + -archivePath $DIST/intermediates/TKLiveSync.appletvsimulator.xcarchive + +checkpoint "Building TKLiveSync for tvOS Device" +xcodebuild archive -derivedDataPath "$DIST/DerivedData" -project v8ios.xcodeproj \ + -scheme "TKLiveSync" \ + -configuration Release \ + -destination "generic/platform=tvOS" \ + -quiet \ + SKIP_INSTALL=NO \ + BUILD_LIBRARY_FOR_DISTRIBUTION=YES \ + -archivePath $DIST/intermediates/TKLiveSync.appletvos.xcarchive +fi + #Creates directory for fat-library OUTPUT_DIR="$DIST/TKLiveSync.xcframework" rm -rf "${OUTPUT_PATH}" @@ -161,6 +186,13 @@ if $BUILD_VISION; then -debug-symbols "$DIST/intermediates/TKLiveSync.xrsimulator.xcarchive/dSYMs/TKLiveSync.framework.dSYM" ) fi +if $BUILD_TVOS; then + XCFRAMEWORKS+=( -framework "$DIST/intermediates/TKLiveSync.appletvos.xcarchive/Products/Library/Frameworks/TKLiveSync.framework" \ + -debug-symbols "$DIST/intermediates/TKLiveSync.appletvos.xcarchive/dSYMs/TKLiveSync.framework.dSYM" ) + XCFRAMEWORKS+=( -framework "$DIST/intermediates/TKLiveSync.appletvsimulator.xcarchive/Products/Library/Frameworks/TKLiveSync.framework" \ + -debug-symbols "$DIST/intermediates/TKLiveSync.appletvsimulator.xcarchive/dSYMs/TKLiveSync.framework.dSYM" ) +fi + checkpoint "Creating TKLiveSync.xcframework" OUTPUT_DIR="$DIST/TKLiveSync.xcframework" rm -rf $OUTPUT_DIR diff --git a/build_utils.sh b/build_utils.sh index 88261bdd3..39230d52d 100644 --- a/build_utils.sh +++ b/build_utils.sh @@ -18,7 +18,7 @@ function checkpoint { echo "" } -# Shared --spm-mode argument parsing for build_npm_ios.sh / build_npm_vision.sh. +# Shared --spm-mode argument parsing for the build_npm_.sh scripts. # Sets SPM_MODE (embedded|remote, default embedded). The calling script must # define usage(); accepts both "--spm-mode " and "--spm-mode=". function parse_spm_mode_args { diff --git a/download_v8.sh b/download_v8.sh index aa85bc49b..e3dbf9459 100755 --- a/download_v8.sh +++ b/download_v8.sh @@ -16,7 +16,7 @@ set -euo pipefail # Set V8_SKIP_DOWNLOAD=1 to make it a no-op -- use that when you have built V8 # yourself and do not want a pinned release overwriting it. # -# Usage: download_v8.sh [--release ] [--force] +# Usage: download_v8.sh [--release ] [--tvos] [--force] # REPO_ROOT="$(cd "$(dirname "$0")" && pwd)" @@ -30,12 +30,16 @@ STAMP="$LIB_DIR/.v8-release-stamp" RELEASE="" FORCE=0 +TVOS=0 usage() { cat <] [--force] +Usage: $(basename "$0") [--release ] [--tvos] [--force] --release Release to install (default: contents of V8_RELEASE) + --tvos Also install the tvOS slices (arm64-appletvos, + arm64-appletvsimulator). Off by default: only the tvOS + runtime build needs them, and not every release ships them. --force Reinstall even if the pinned release is already in place Downloads are cached in $CACHE_DIR (override with \$V8_PREBUILT_CACHE). @@ -46,6 +50,7 @@ while [ $# -gt 0 ]; do case "$1" in --release) RELEASE="$2"; shift 2 ;; --release=*) RELEASE="${1#*=}"; shift ;; + --tvos) TVOS=1; shift ;; --force) FORCE=1; shift ;; -h|--help) usage; exit 0 ;; *) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;; @@ -62,25 +67,16 @@ if [ -z "$RELEASE" ]; then RELEASE="$(tr -d '[:space:]' < "$RELEASE_FILE")" fi -# The stamp alone is not enough: it says which release was installed, not that -# the three trees are still on disk. Re-install rather than leave a half-removed -# checkout looking up to date. -installed() { - [ -f "$STAMP" ] && [ "$(cat "$STAMP")" = "$RELEASE" ] \ - && [ -f "$NS_DIR/include/v8.h" ] && [ -d "$NS_DIR/inspector" ] -} - -if [ "$FORCE" = "0" ] && installed; then - echo "V8 $RELEASE already installed. Use --force to reinstall." - exit 0 -fi - # V8 has no visionOS target -- target_environment is only simulator, device or # catalyst. It does not need one: the platform tag on a member of a static # archive is advisory, and only the final linked image carries an # LC_BUILD_VERSION, so a visionOS binary links the iOS archives directly. The # xros directories are therefore copies of their iOS counterparts, which is # exactly what shipped before this change. +# +# tvOS is different: V8 does have a tvOS target (target_platform="tvos"), and +# the build repo ships it as its own variants rather than a relabelled iOS +# build, so those slices are only installed when asked for. VARIANTS=( "arm64-device:arm64-iphoneos arm64-xros" "arm64-simulator:arm64-iphonesimulator arm64-xrsimulator" @@ -88,6 +84,32 @@ VARIANTS=( "arm64-catalyst:arm64-maccatalyst" "x64-catalyst:x86_64-maccatalyst" ) +if [ "$TVOS" = "1" ]; then + VARIANTS+=( + "arm64-tvdevice:arm64-appletvos" + "arm64-tvsimulator:arm64-appletvsimulator" + ) +fi + +# The stamp alone is not enough: it says which release was installed, not that +# the trees are still on disk, nor which slices -- an iOS-only install stamps +# the same release a --tvos install does. Re-install rather than leave a +# half-removed or slice-short checkout looking up to date. +installed() { + [ -f "$STAMP" ] && [ "$(cat "$STAMP")" = "$RELEASE" ] \ + && [ -f "$NS_DIR/include/v8.h" ] && [ -d "$NS_DIR/inspector" ] || return 1 + local entry dest + for entry in "${VARIANTS[@]}"; do + for dest in ${entry#*:}; do + [ -f "$LIB_DIR/$dest/libv8_base_without_compiler.a" ] || return 1 + done + done +} + +if [ "$FORCE" = "0" ] && installed; then + echo "V8 $RELEASE already installed. Use --force to reinstall." + exit 0 +fi BASE_URL="https://github.com/$UPSTREAM/releases/download/$RELEASE" DL="$CACHE_DIR/$RELEASE" @@ -107,12 +129,21 @@ fetch SHA256SUMS ASSETS=() for entry in "${VARIANTS[@]}"; do variant="${entry%%:*}" - ASSETS+=("$(grep -oE "v8-[^ ]*-ios-$variant\.tar\.gz" "$DL/SHA256SUMS" | head -1)") + asset="$(grep -oE "v8-[^ ]*-ios-$variant\.tar\.gz" "$DL/SHA256SUMS" | head -1 || true)" + if [ -z "$asset" ]; then + echo "Release $RELEASE has no ios-$variant asset." >&2 + case "$variant" in + *-tv*) echo "The tvOS slices need a $UPSTREAM release that builds the arm64-tvdevice and arm64-tvsimulator variants; pin one in V8_RELEASE." >&2 ;; + esac + exit 1 + fi + ASSETS+=("$asset") done -ASSETS+=("$(grep -oE 'v8-[^ ]*-src-headers\.tar\.gz' "$DL/SHA256SUMS" | head -1)") +src_headers="$(grep -oE 'v8-[^ ]*-src-headers\.tar\.gz' "$DL/SHA256SUMS" | head -1 || true)" +[ -n "$src_headers" ] || { echo "Release $RELEASE has no src-headers asset." >&2; exit 1; } +ASSETS+=("$src_headers") for a in "${ASSETS[@]}"; do - [ -n "$a" ] || { echo "Release $RELEASE is missing an expected asset." >&2; exit 1; } fetch "$a" done diff --git a/metadata-generator/build-step-metadata-generator.py b/metadata-generator/build-step-metadata-generator.py index 1acd946c9..cbcd23441 100755 --- a/metadata-generator/build-step-metadata-generator.py +++ b/metadata-generator/build-step-metadata-generator.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import os +import re import shlex import subprocess import sys @@ -66,17 +67,20 @@ def map_and_list(func, iterable): elif effective_platform_name == "-appletvos": docset_platform = "tvOS" default_deployment_target_flag_name = "-mappletvos-version-min" - default_deployment_target_clang_env_name = "APPLETVOS_DEPLOYMENT_TARGET" + default_deployment_target_clang_env_name = "TVOS_DEPLOYMENT_TARGET" elif effective_platform_name == "-appletvsimulator": docset_platform = "tvOS" default_deployment_target_flag_name = "-mappletvsimulator-version-min" - default_deployment_target_clang_env_name = "APPLETVOS_DEPLOYMENT_TARGET" + default_deployment_target_clang_env_name = "TVOS_DEPLOYMENT_TARGET" elif effective_platform_name == "-iphoneos": default_deployment_target_flag_name = "-miphoneos-version-min" sdk_version = env("SDK_VERSION") or "13.0" llvm_target_triple_suffix = env_or_empty("LLVM_TARGET_TRIPLE_SUFFIX") -llvm_target_triple_os_version = "ios{}".format(sdk_version) +# The OS part of the triple ("ios", "tvos", ...) comes from Xcode's LLVM_TARGET_TRIPLE_OS_VERSION +# (e.g. "tvos16.0"); its version is the deployment target, so the SDK version is substituted instead. +llvm_target_triple_os = re.sub(r"[0-9.]+$", "", env_or_empty("LLVM_TARGET_TRIPLE_OS_VERSION")) or "ios" +llvm_target_triple_os_version = "{}{}".format(llvm_target_triple_os, sdk_version) # env("LLVM_TARGET_TRIPLE_OS_VERSION") is the deployment target, so doesn't have all APIs # usually it's ios9.0 for NativeScript projects llvm_target_triple_vendor = env("LLVM_TARGET_TRIPLE_VENDOR") or "apple" diff --git a/package.json b/package.json index 93c85faa1..6f98794b7 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "open-xcode": "./tools/update-xcode-env.sh && open v8ios.xcodeproj", "build-ios": "node prepare-target ios && ./build_all_ios.sh", "build-vision": "node prepare-target visionos && ./build_all_vision.sh", + "build-tvos": "node prepare-target tvos && ./build_all_tvos.sh", "setup-ci": "./build_metadata_generator.sh", "update-version": "./update_version.sh", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", diff --git a/prepare-target.js b/prepare-target.js index 45d816c26..82437947d 100644 --- a/prepare-target.js +++ b/prepare-target.js @@ -2,12 +2,13 @@ const path = require('path'); const fs = require('fs'); const cmdArgs = process.argv.slice(2); -const target = cmdArgs[0]; // ios or visionos +const target = cmdArgs[0]; // ios, visionos or tvos const packagePath = path.join('package.json'); const packageJson = JSON.parse(fs.readFileSync(packagePath)); packageJson.name = `@nativescript/${target}`; -packageJson.description = `NativeScript Runtime for ${target === 'ios' ? 'iOS' : 'visionOS'}`; +const names = { ios: 'iOS', visionos: 'visionOS', tvos: 'tvOS' }; +packageJson.description = `NativeScript Runtime for ${names[target] || target}`; fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2)); \ No newline at end of file diff --git a/prepare_dSYMs.sh b/prepare_dSYMs.sh index 8c7e64294..2312fb691 100755 --- a/prepare_dSYMs.sh +++ b/prepare_dSYMs.sh @@ -5,7 +5,8 @@ OUTFOLDER="./dist/dSYMs" mkdir -p "$OUTFOLDER" -for framework_path in ./dist/NativeScript.xcframework/ios*; do +for framework_path in ./dist/NativeScript.xcframework/*/; do + framework_path="${framework_path%/}" DSYMPATH="$framework_path/dSYMs/NativeScript.framework.dSYM" if [ -d "$DSYMPATH" ]; then OUTPATH="$OUTFOLDER/$(basename $framework_path)NativeScript.framework.dSYM" diff --git a/project-template-tvos/__PROJECT_NAME__.xcodeproj/project.pbxproj b/project-template-tvos/__PROJECT_NAME__.xcodeproj/project.pbxproj new file mode 100644 index 000000000..e5b3d395b --- /dev/null +++ b/project-template-tvos/__PROJECT_NAME__.xcodeproj/project.pbxproj @@ -0,0 +1,532 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + AA10000000000000000000C1 /* NativeScriptTvOS in Frameworks */ = {isa = PBXBuildFile; productRef = AA10000000000000000000B1 /* NativeScriptTvOS */; }; + 858B842D18CA22B800AB12DE /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 858B833A18CA111C00AB12DE /* InfoPlist.strings */; }; + AAA0AADB2A54B96B00EE55A4 /* NativeScriptStart.m in Sources */ = {isa = PBXBuildFile; fileRef = AAA0AADA2A54B96B00EE55A4 /* NativeScriptStart.m */; }; + CD45EE7C18DC2D5800FB50C0 /* app in Resources */ = {isa = PBXBuildFile; fileRef = CD45EE7A18DC2D5800FB50C0 /* app */; }; + CD62955D1BB2678900AE3A93 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = CD62955C1BB2678900AE3A93 /* main.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 85F5BDFC1A9363BE006B9701 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 391174B521F1D7BF00BA2583 /* nativescript-build.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = "nativescript-build.xcconfig"; path = "internal/nativescript-build.xcconfig"; sourceTree = SOURCE_ROOT; }; + 391174B721F1D99900BA2583 /* plugins-release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "plugins-release.xcconfig"; sourceTree = SOURCE_ROOT; }; + 391174B821F1D99900BA2583 /* plugins-debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "plugins-debug.xcconfig"; sourceTree = SOURCE_ROOT; }; + 39940D8122C4E84C0050DDE1 /* __PROJECT_NAME__.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = __PROJECT_NAME__.entitlements; sourceTree = ""; }; + 42C751E2232B769100186695 /* nativescript-pre-link */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "nativescript-pre-link"; path = "internal/nativescript-pre-link"; sourceTree = SOURCE_ROOT; }; + 42C751E3232B769100186695 /* strip-dynamic-framework-architectures.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; name = "strip-dynamic-framework-architectures.sh"; path = "internal/strip-dynamic-framework-architectures.sh"; sourceTree = SOURCE_ROOT; }; + 42C751E4232B769100186695 /* nsld.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; name = nsld.sh; path = internal/nsld.sh; sourceTree = SOURCE_ROOT; }; + 42C751E5232B769100186695 /* nativescript-post-build */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "nativescript-post-build"; path = "internal/nativescript-post-build"; sourceTree = SOURCE_ROOT; }; + 42C751E6232B769100186695 /* nativescript-pre-build */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "nativescript-pre-build"; path = "internal/nativescript-pre-build"; sourceTree = SOURCE_ROOT; }; + 858B832E18CA111C00AB12DE /* __PROJECT_NAME__.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = __PROJECT_NAME__.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 858B833918CA111C00AB12DE /* __PROJECT_NAME__-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "__PROJECT_NAME__-Info.plist"; sourceTree = ""; }; + 858B833B18CA111C00AB12DE /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 858B833F18CA111C00AB12DE /* __PROJECT_NAME__-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "__PROJECT_NAME__-Prefix.pch"; sourceTree = ""; }; + 858B843318CA22B800AB12DE /* __PROJECT_NAME__.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = __PROJECT_NAME__.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 930F05DB22671AE300D269DC /* Swift-ObjC-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "Swift-ObjC-Bridging-Header.h"; path = "internal/Swift-ObjC-Bridging-Header.h"; sourceTree = SOURCE_ROOT; }; + AA5E89E22B5C318D008AFA28 /* macros.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = macros.h; path = internal/macros.h; sourceTree = SOURCE_ROOT; }; + AAA0AADA2A54B96B00EE55A4 /* NativeScriptStart.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = NativeScriptStart.m; path = internal/NativeScriptStart.m; sourceTree = SOURCE_ROOT; }; + AAA0AADC2A54B97E00EE55A4 /* NativeScriptStart.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = NativeScriptStart.h; path = internal/NativeScriptStart.h; sourceTree = SOURCE_ROOT; }; + CD45EE7A18DC2D5800FB50C0 /* app */ = {isa = PBXFileReference; lastKnownFileType = folder; path = app; sourceTree = ""; }; + CD62955C1BB2678900AE3A93 /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = internal/main.m; sourceTree = SOURCE_ROOT; }; + CDD59A261BB43B5D00EC2671 /* build-debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "build-debug.xcconfig"; sourceTree = ""; }; + CDD59A271BB43B5D00EC2671 /* build-release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "build-release.xcconfig"; sourceTree = ""; }; + CDF4743E1BA4855C0087EA85 /* build.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = build.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 858B83F418CA22B800AB12DE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + AA10000000000000000000C1 /* NativeScriptTvOS in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 39940D8E22C4EBCC0050DDE1 /* Sources */ = { + isa = PBXGroup; + children = ( + AA5E89E22B5C318D008AFA28 /* macros.h */, + CD62955C1BB2678900AE3A93 /* main.m */, + AAA0AADA2A54B96B00EE55A4 /* NativeScriptStart.m */, + AAA0AADC2A54B97E00EE55A4 /* NativeScriptStart.h */, + 858B833F18CA111C00AB12DE /* __PROJECT_NAME__-Prefix.pch */, + ); + name = Sources; + sourceTree = ""; + }; + 42C751E1232B767700186695 /* Build Scripts */ = { + isa = PBXGroup; + children = ( + 42C751E5232B769100186695 /* nativescript-post-build */, + 42C751E6232B769100186695 /* nativescript-pre-build */, + 42C751E2232B769100186695 /* nativescript-pre-link */, + 42C751E4232B769100186695 /* nsld.sh */, + 42C751E3232B769100186695 /* strip-dynamic-framework-architectures.sh */, + ); + name = "Build Scripts"; + sourceTree = ""; + }; + 858B832518CA111C00AB12DE = { + isa = PBXGroup; + children = ( + E070579D1B39A9D000214BF1 /* Resources */, + 858B833718CA111C00AB12DE /* __PROJECT_NAME__ */, + 858B833018CA111C00AB12DE /* Frameworks */, + 858B832F18CA111C00AB12DE /* Products */, + ); + sourceTree = ""; + }; + 858B832F18CA111C00AB12DE /* Products */ = { + isa = PBXGroup; + children = ( + 858B832E18CA111C00AB12DE /* __PROJECT_NAME__.app */, + 858B843318CA22B800AB12DE /* __PROJECT_NAME__.app */, + ); + name = Products; + sourceTree = ""; + }; + 858B833018CA111C00AB12DE /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + 858B833718CA111C00AB12DE /* __PROJECT_NAME__ */ = { + isa = PBXGroup; + children = ( + 39940D8E22C4EBCC0050DDE1 /* Sources */, + 39940D8122C4E84C0050DDE1 /* __PROJECT_NAME__.entitlements */, + CD45EE7A18DC2D5800FB50C0 /* app */, + 858B833818CA111C00AB12DE /* Supporting Files */, + ); + path = __PROJECT_NAME__; + sourceTree = ""; + }; + 858B833818CA111C00AB12DE /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 42C751E1232B767700186695 /* Build Scripts */, + 391174B821F1D99900BA2583 /* plugins-debug.xcconfig */, + 391174B721F1D99900BA2583 /* plugins-release.xcconfig */, + CDF4743E1BA4855C0087EA85 /* build.xcconfig */, + CDD59A261BB43B5D00EC2671 /* build-debug.xcconfig */, + CDD59A271BB43B5D00EC2671 /* build-release.xcconfig */, + 391174B521F1D7BF00BA2583 /* nativescript-build.xcconfig */, + 858B833918CA111C00AB12DE /* __PROJECT_NAME__-Info.plist */, + 858B833A18CA111C00AB12DE /* InfoPlist.strings */, + 930F05DB22671AE300D269DC /* Swift-ObjC-Bridging-Header.h */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + E070579D1B39A9D000214BF1 /* Resources */ = { + isa = PBXGroup; + children = ( + ); + name = Resources; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 858B83EF18CA22B800AB12DE /* __PROJECT_NAME__ */ = { + isa = PBXNativeTarget; + buildConfigurationList = 858B843018CA22B800AB12DE /* Build configuration list for PBXNativeTarget "__PROJECT_NAME__" */; + buildPhases = ( + C97FD7AC1ADE5369004DB2A4 /* NativeScript PreBuild */, + 858B83F218CA22B800AB12DE /* Sources */, + CD62955B1BB2651D00AE3A93 /* NativeScript PreLink */, + 858B83F418CA22B800AB12DE /* Frameworks */, + 858B842C18CA22B800AB12DE /* Resources */, + 85F5BDFC1A9363BE006B9701 /* Embed Frameworks */, + CD3EAD351B05FF060042DBFC /* NativeScript PostBuild */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = __PROJECT_NAME__; + packageProductDependencies = ( + AA10000000000000000000B1 /* NativeScriptTvOS */, + ); + productName = JDBridgeApp; + productReference = 858B843318CA22B800AB12DE /* __PROJECT_NAME__.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 858B832618CA111C00AB12DE /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + CLASSPREFIX = NSC; + LastUpgradeCheck = 1500; + ORGANIZATIONNAME = NativeScript; + TargetAttributes = { + 858B83EF18CA22B800AB12DE = { + LastSwiftMigration = 1520; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 858B832918CA111C00AB12DE /* Build configuration list for PBXProject "__PROJECT_NAME__" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 858B832518CA111C00AB12DE; + packageReferences = ( + AA10000000000000000000A1 /* XCRemoteSwiftPackageReference "ios-spm" */, + ); + productRefGroup = 858B832F18CA111C00AB12DE /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 858B83EF18CA22B800AB12DE /* __PROJECT_NAME__ */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 858B842C18CA22B800AB12DE /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CD45EE7C18DC2D5800FB50C0 /* app in Resources */, + 858B842D18CA22B800AB12DE /* InfoPlist.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + C97FD7AC1ADE5369004DB2A4 /* NativeScript PreBuild */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "NativeScript PreBuild"; + outputPaths = ( + "$CONFIGURATION_BUILD_DIR/metadata-arm64.bin", + "$CONFIGURATION_BUILD_DIR/metadata-arm64e.bin", + "$CONFIGURATION_BUILD_DIR/metadata-i386.bin", + "$CONFIGURATION_BUILD_DIR/metadata-x86_64.bin", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$SRCROOT/internal/nativescript-pre-build\"\n"; + showEnvVarsInLog = 0; + }; + CD3EAD351B05FF060042DBFC /* NativeScript PostBuild */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "NativeScript PostBuild"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$SRCROOT/internal/nativescript-post-build\"\n"; + showEnvVarsInLog = 0; + }; + CD62955B1BB2651D00AE3A93 /* NativeScript PreLink */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "NativeScript PreLink"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$SRCROOT/internal/nativescript-pre-link\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 858B83F218CA22B800AB12DE /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CD62955D1BB2678900AE3A93 /* main.m in Sources */, + AAA0AADB2A54B96B00EE55A4 /* NativeScriptStart.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 858B833A18CA111C00AB12DE /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 858B833B18CA111C00AB12DE /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 858B835818CA111C00AB12DE /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DERIVE_UIKITFORMAC_PRODUCT_BUNDLE_IDENTIFIER = YES; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = appletvos; + STRIP_SWIFT_SYMBOLS = NO; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/internal/Swift-ObjC-Bridging-Header.h"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 13.0; + }; + name = Debug; + }; + 858B835918CA111C00AB12DE /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD)"; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + DERIVE_UIKITFORMAC_PRODUCT_BUNDLE_IDENTIFIER = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + SDKROOT = appletvos; + STRIP_SWIFT_SYMBOLS = NO; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_OBJC_BRIDGING_HEADER = "$SRCROOT/internal/Swift-ObjC-Bridging-Header.h"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 13.0; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 858B843118CA22B800AB12DE /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CDD59A261BB43B5D00EC2671 /* build-debug.xcconfig */; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_ENTITLEMENTS = __PROJECT_NAME__/__PROJECT_NAME__.entitlements; + ENABLE_BITCODE = NO; + "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "__PROJECT_NAME__/__PROJECT_NAME__-Prefix.pch"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "DEBUG=1", + "NS_SWIFTUI_BOOT=$(NS_SWIFTUI_BOOT:default=0)", + ); + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"$SRCROOT/internal/Swift-Modules\"", + "\"$(SRCROOT)/internal\"", + ); + INFOPLIST_FILE = "$(SRCROOT)/__PROJECT_NAME__/__PROJECT_NAME__-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_NAME = __PROJECT_NAME__; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator"; + SUPPORTS_MACCATALYST = NO; + TARGETED_DEVICE_FAMILY = 3; + WRAPPER_EXTENSION = app; + }; + name = Debug; + }; + 858B843218CA22B800AB12DE /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CDD59A271BB43B5D00EC2671 /* build-release.xcconfig */; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD)"; + CODE_SIGN_ENTITLEMENTS = __PROJECT_NAME__/__PROJECT_NAME__.entitlements; + ENABLE_BITCODE = NO; + "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "__PROJECT_NAME__/__PROJECT_NAME__-Prefix.pch"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "NS_SWIFTUI_BOOT=$(NS_SWIFTUI_BOOT:default=0)", + ); + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "$SRCROOT/internal/Swift-Modules", + "$(SRCROOT)/internal", + ); + INFOPLIST_FILE = "$(SRCROOT)/__PROJECT_NAME__/__PROJECT_NAME__-Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_NAME = __PROJECT_NAME__; + SUPPORTED_PLATFORMS = "appletvos appletvsimulator"; + SUPPORTS_MACCATALYST = NO; + TARGETED_DEVICE_FAMILY = 3; + WRAPPER_EXTENSION = app; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 858B832918CA111C00AB12DE /* Build configuration list for PBXProject "__PROJECT_NAME__" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 858B835818CA111C00AB12DE /* Debug */, + 858B835918CA111C00AB12DE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 858B843018CA22B800AB12DE /* Build configuration list for PBXNativeTarget "__PROJECT_NAME__" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 858B843118CA22B800AB12DE /* Debug */, + 858B843218CA22B800AB12DE /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + AA10000000000000000000A1 /* XCRemoteSwiftPackageReference "ios-spm" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/NativeScript/ios-spm.git"; + requirement = { + kind = exactVersion; + version = "__NS_RUNTIME_VERSION__"; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + AA10000000000000000000B1 /* NativeScriptTvOS */ = { + isa = XCSwiftPackageProductDependency; + package = AA10000000000000000000A1 /* XCRemoteSwiftPackageReference "ios-spm" */; + productName = NativeScriptTvOS; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 858B832618CA111C00AB12DE /* Project object */; +} diff --git a/project-template-tvos/__PROJECT_NAME__.xcodeproj/xcshareddata/xcschemes/__PROJECT_NAME__.xcscheme b/project-template-tvos/__PROJECT_NAME__.xcodeproj/xcshareddata/xcschemes/__PROJECT_NAME__.xcscheme new file mode 100644 index 000000000..ecbb26ff8 --- /dev/null +++ b/project-template-tvos/__PROJECT_NAME__.xcodeproj/xcshareddata/xcschemes/__PROJECT_NAME__.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/project-template-tvos/__PROJECT_NAME__/__PROJECT_NAME__-Prefix.pch b/project-template-tvos/__PROJECT_NAME__/__PROJECT_NAME__-Prefix.pch new file mode 100644 index 000000000..6759b6c7f --- /dev/null +++ b/project-template-tvos/__PROJECT_NAME__/__PROJECT_NAME__-Prefix.pch @@ -0,0 +1,12 @@ +// +// Prefix header +// +// The contents of this file are implicitly included at the beginning of every source file. +// + +#import + +#ifdef __OBJC__ + #import + #import +#endif diff --git a/project-template-tvos/__PROJECT_NAME__/__PROJECT_NAME__.entitlements b/project-template-tvos/__PROJECT_NAME__/__PROJECT_NAME__.entitlements new file mode 100644 index 000000000..f9105be8a --- /dev/null +++ b/project-template-tvos/__PROJECT_NAME__/__PROJECT_NAME__.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.network.client + + + diff --git a/project-template-tvos/__PROJECT_NAME__/build-debug.xcconfig b/project-template-tvos/__PROJECT_NAME__/build-debug.xcconfig new file mode 100644 index 000000000..4526c0226 --- /dev/null +++ b/project-template-tvos/__PROJECT_NAME__/build-debug.xcconfig @@ -0,0 +1,3 @@ +// @COCOAPODS_SILENCE_WARNINGS@ // +#include "build.xcconfig" +#include "../plugins-debug.xcconfig" diff --git a/project-template-tvos/__PROJECT_NAME__/build-release.xcconfig b/project-template-tvos/__PROJECT_NAME__/build-release.xcconfig new file mode 100644 index 000000000..f65f3e98e --- /dev/null +++ b/project-template-tvos/__PROJECT_NAME__/build-release.xcconfig @@ -0,0 +1,3 @@ +// @COCOAPODS_SILENCE_WARNINGS@ // +#include "build.xcconfig" +#include "../plugins-release.xcconfig" diff --git a/project-template-tvos/__PROJECT_NAME__/build.xcconfig b/project-template-tvos/__PROJECT_NAME__/build.xcconfig new file mode 100644 index 000000000..65686340d --- /dev/null +++ b/project-template-tvos/__PROJECT_NAME__/build.xcconfig @@ -0,0 +1,3 @@ +#include "../internal/nativescript-build.xcconfig" +CLANG_ENABLE_MODULES = NO; +CLANG_MODULES_AUTOLINK = NO; diff --git a/project-template-tvos/__PROJECT_NAME__/en.lproj/InfoPlist.strings b/project-template-tvos/__PROJECT_NAME__/en.lproj/InfoPlist.strings new file mode 100644 index 000000000..477b28ff8 --- /dev/null +++ b/project-template-tvos/__PROJECT_NAME__/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/project-template-tvos/internal/NativeScriptStart.h b/project-template-tvos/internal/NativeScriptStart.h new file mode 100644 index 000000000..d55d06934 --- /dev/null +++ b/project-template-tvos/internal/NativeScriptStart.h @@ -0,0 +1,19 @@ +// +// NativeScriptStart.h +// +// Created by Team nStudio on 7/5/23. +// Copyright © 2023 NativeScript. All rights reserved. +// + +#ifndef NativeScriptStart_h +#define NativeScriptStart_h + + +#endif /* NativeScriptStart_h */ + +@interface NativeScriptStart : NSObject + ++ (void)setup; ++ (void)boot; + +@end diff --git a/project-template-tvos/internal/NativeScriptStart.m b/project-template-tvos/internal/NativeScriptStart.m new file mode 100644 index 000000000..563251a7f --- /dev/null +++ b/project-template-tvos/internal/NativeScriptStart.m @@ -0,0 +1,73 @@ +// +// NativeScriptStart.m +// +// Created by Team nStudio on 7/5/23. +// Copyright © 2023 NativeScript. All rights reserved. +// + +#import +#import +#import + +#ifdef DEBUG +#include +#include +#include "macros.h" +#endif + +extern char startOfMetadataSection __asm("section$start$__DATA$__TNSMetadata"); +NativeScript* nativescriptStart; + +@implementation NativeScriptStart + ++(void)setup{ + @autoreleasepool { + NSString* baseDir = [[NSBundle mainBundle] resourcePath]; + + #ifdef DEBUG + int refreshRequestSubscription; + notify_register_dispatch(NOTIFICATION("RefreshRequest"), &refreshRequestSubscription, dispatch_get_main_queue(), ^(int token) { + notify_post(NOTIFICATION("AppRefreshStarted")); + bool success = [nativescriptStart liveSync]; + if (success) { + notify_post(NOTIFICATION("AppRefreshSucceeded")); + } else { + NSLog(@"__onLiveSync call failed"); + notify_post(NOTIFICATION("AppRefreshFailed")); + } + }); + + TNSInitializeLiveSync(); + if (getenv("TNSBaseDir")) { + baseDir = @(getenv("TNSBaseDir")); + } + #endif + + void* metadataPtr = &startOfMetadataSection; + + bool isDebug = + #ifdef DEBUG + true; + #else + false; + #endif + + Config* config = [[Config alloc] init]; + config.IsDebug = isDebug; + config.LogToSystemConsole = isDebug; + config.MetadataPtr = metadataPtr; + config.BaseDir = baseDir; +// config.ArgumentsCount = argc; +// config.Arguments = argv; + + nativescriptStart = [[NativeScript alloc] initWithConfig:config]; + + } + +} ++(void)boot{ + [nativescriptStart runMainApplication]; +} +@end + + diff --git a/project-template-tvos/internal/README.md b/project-template-tvos/internal/README.md new file mode 100644 index 000000000..cab084cb9 --- /dev/null +++ b/project-template-tvos/internal/README.md @@ -0,0 +1,2 @@ +The contents of this folder is specific for the current version of the runtime. +It will be wiped and replaced on each platform update. diff --git a/project-template-tvos/internal/Swift-ObjC-Bridging-Header.h b/project-template-tvos/internal/Swift-ObjC-Bridging-Header.h new file mode 100644 index 000000000..5683610d8 --- /dev/null +++ b/project-template-tvos/internal/Swift-ObjC-Bridging-Header.h @@ -0,0 +1,18 @@ +// +// Swift-ObjC-Bridging-Header.h +// __PROJECT_NAME__ +// +// Created by Teodor Dermendzhiev on 4/17/19. +// Copyright © 2019 Progress. All rights reserved. +// + +// Intentionally left blank. Needed for the generation of all Swift classes in +// the *-Swift.h*. +// https://developer.apple.com/documentation/swift/imported_c_and_objective-c_apis/importing_objective-c_into_swift + +#import "TargetConditionals.h" + +#ifndef TARGET_OS_WATCH + #import "NativeScript/NativeScript.h" + #import "NativeScriptStart.h" +#endif diff --git a/project-template-tvos/internal/macros.h b/project-template-tvos/internal/macros.h new file mode 100644 index 000000000..030820957 --- /dev/null +++ b/project-template-tvos/internal/macros.h @@ -0,0 +1,17 @@ +// +// macros.h +// sampleng +// +// Created by Team nStudio on 1/19/24. +// Copyright © 2024 NativeScript. All rights reserved. +// + +#ifndef macros_h +#define macros_h + +#define NOTIFICATION(name) \ + [[NSString stringWithFormat:@"%@:NativeScript.Debug.%s", \ + [[NSBundle mainBundle] bundleIdentifier], name] UTF8String] + + +#endif /* macros_h */ diff --git a/project-template-tvos/internal/main.m b/project-template-tvos/internal/main.m new file mode 100644 index 000000000..f9da2556b --- /dev/null +++ b/project-template-tvos/internal/main.m @@ -0,0 +1,67 @@ +// +// Any changes in this file will be removed after you update your platform! +// +#import +#import + +#ifdef DEBUG +#include +#include +#include "macros.h" +#endif + +#if NS_SWIFTUI_BOOT == 0 +// Users can disable Obj-C bootstrapping by defining NS_SWIFTUI_BOOT = 1 in their project's build.xcconfig file +// This requires a NativeScriptApp.swift to be setup in the project and embedding enabled in @nativescript/core bootstrap + +extern char startOfMetadataSection __asm("section$start$__DATA$__TNSMetadata"); +NativeScript* nativescript; + +int main(int argc, char *argv[]) { + @autoreleasepool { + NSString* baseDir = [[NSBundle mainBundle] resourcePath]; + +#ifdef DEBUG + int refreshRequestSubscription; + notify_register_dispatch(NOTIFICATION("RefreshRequest"), &refreshRequestSubscription, dispatch_get_main_queue(), ^(int token) { + notify_post(NOTIFICATION("AppRefreshStarted")); + bool success = [nativescript liveSync]; + if (success) { + notify_post(NOTIFICATION("AppRefreshSucceeded")); + } else { + NSLog(@"__onLiveSync call failed"); + notify_post(NOTIFICATION("AppRefreshFailed")); + } + }); + + TNSInitializeLiveSync(); + if (getenv("TNSBaseDir")) { + baseDir = @(getenv("TNSBaseDir")); + } +#endif + + void* metadataPtr = &startOfMetadataSection; + + bool isDebug = +#ifdef DEBUG + true; +#else + false; +#endif + + Config* config = [[Config alloc] init]; + config.IsDebug = isDebug; + config.LogToSystemConsole = isDebug; + config.MetadataPtr = metadataPtr; + config.BaseDir = baseDir; + config.ArgumentsCount = argc; + config.Arguments = argv; + + nativescript = [[NativeScript alloc] initWithConfig:config]; + [nativescript runMainApplication]; + + return 0; + } +} + +#endif diff --git a/project-template-tvos/internal/nativescript-build.xcconfig b/project-template-tvos/internal/nativescript-build.xcconfig new file mode 100644 index 000000000..701b845b5 --- /dev/null +++ b/project-template-tvos/internal/nativescript-build.xcconfig @@ -0,0 +1,32 @@ +// * NativeScript build related flags +// * Add [sdk=*] after each one to avoid conflict with CocoaPods flags +// * NativeScript + TKLiveSync are provided by the SwiftPM "NativeScriptTvOS" product +// * (github.com/NativeScript/ios-spm); SwiftPM links/embeds them automatically. The +// * explicit -framework NativeScript / -framework TKLiveSync / -F internal are omitted. +// * SwiftPM stages the resolved framework slice into CONFIGURATION_BUILD_DIR (below), +// * which is where the metadata generator finds it. +OTHER_LDFLAGS[sdk=*] = $(inherited) -ObjC -sectcreate __DATA __TNSMetadata "$(CONFIGURATION_BUILD_DIR)/metadata-$(CURRENT_ARCH).bin" -licucore -lz -lc++ -framework Foundation -framework UIKit -framework CoreGraphics -framework CoreServices -framework Security + +// We need to add CONFIGURATION_BUILD_DIR here so that we can explicitly quote-escape any paths in it, because the implicitly added path by Xcode is not always escaped +FRAMEWORK_SEARCH_PATHS[sdk=*] = $(inherited) "$(SRCROOT)/internal/" "$(CONFIGURATION_BUILD_DIR)" + +// Launch custom linker script in order to generate metadata binary file and link it in the executable +LD = $SRCROOT/internal/nsld.sh +LDPLUSPLUS = $SRCROOT/internal/nsld.sh + +// * Uncomment this setting to generate TypeScript declarations for the tvOS SDK and all linked libraries. +// * They will be generated on each build, so you can find them after running "ns build tvos" in "YOUR_APP/platforms/tvos". +// NS_TYPESCRIPT_DECLARATIONS_PATH = $(SRCROOT)/typescript-declarations + +// * Uncomment this setting to generate human readable metadata for the tvOS SDK and all linked libraries. +// * You can learn more about NativeScript metadata in the docs: http://docs.nativescript.org/runtimes/ios/Overview#metadata +// * It will be generated on each build, so you can find it after running "ns build tvos" in "YOUR_APP/platforms/tvos". +// NS_DEBUG_METADATA_PATH = $(SRCROOT)/debug-metadata + +// The runtime ships arm64-only tvOS slices (there is no x86_64 tvOS simulator build). +EXCLUDED_ARCHS[sdk=appletvsimulator*] = i386 x86_64 armv7 armv7s armv8 +EXCLUDED_ARCHS[sdk=appletvos*] = i386 x86_64 armv7 armv7s armv8 +VALIDATE_WORKSPACE = YES + +// Apple TV is the only tvOS device family +TARGETED_DEVICE_FAMILY = 3 diff --git a/project-template-tvos/internal/nativescript-post-build b/project-template-tvos/internal/nativescript-post-build new file mode 100755 index 000000000..a9ed8f914 --- /dev/null +++ b/project-template-tvos/internal/nativescript-post-build @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -e + +pushd "$SRCROOT/internal" +./strip-dynamic-framework-architectures.sh +popd diff --git a/project-template-tvos/internal/nativescript-pre-build b/project-template-tvos/internal/nativescript-pre-build new file mode 100755 index 000000000..2216ef798 --- /dev/null +++ b/project-template-tvos/internal/nativescript-pre-build @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +# Ignore errors while inserting environment variables (some could be readonly) +(echo "set +e"; echo "set +o posix"; export; echo "set -o posix"; echo "set -e";) > ./.build_env_vars.sh + +# create temporary empty metadata files for XCode LD/LDPLUSPLUS flag analysis +for arch in $VALID_ARCHS +do : + touch "$CONFIGURATION_BUILD_DIR/metadata-$arch.bin" +done diff --git a/project-template-tvos/internal/nativescript-pre-link b/project-template-tvos/internal/nativescript-pre-link new file mode 100755 index 000000000..26e65ac19 --- /dev/null +++ b/project-template-tvos/internal/nativescript-pre-link @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +set -e \ No newline at end of file diff --git a/project-template-tvos/internal/nsld.sh b/project-template-tvos/internal/nsld.sh new file mode 100755 index 000000000..c61a8c094 --- /dev/null +++ b/project-template-tvos/internal/nsld.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +source ./.build_env_vars.sh + +function getArch() { + while [[ $# -gt 0 ]] + do + case $1 in + -arch) + printf $2 + return + ;; + -target) + printf `echo $2 | cut -f1 -d'-'` + return + ;; + esac + shift + done +} + +# Workaround for ARCH being set to `undefined_arch` here. Extract it from command line arguments. +TARGET_ARCH=$(getArch "$@") + +if [ -z "$TARGET_ARCH" ]; then + printf '%s\n' "NSLD: Unable to determine target architecture from arguments: $*" >&2 + exit 1 +fi + +# Use per-architecture directory to avoid race conditions with parallel linker invocations +MODULES_DIR="$SRCROOT/internal/Swift-Modules-$TARGET_ARCH" + +function DELETE_SWIFT_MODULES_DIR() { + rm -rf "$MODULES_DIR" +} + +function GEN_MODULEMAP() { + ARCH_ARG=$1 + SWIFT_HEADER_DIR=$PER_VARIANT_OBJECT_FILE_DIR/$ARCH_ARG + + DELETE_SWIFT_MODULES_DIR + if [ -d "$SWIFT_HEADER_DIR" ]; then + HEADER_PATH=$(find "$SWIFT_HEADER_DIR" -name '*-Swift.h' 2>/dev/null) + if [ -n "$HEADER_PATH" ]; then + mkdir -p "$MODULES_DIR" + CONTENT="module nsswiftsupport { \n header \"$HEADER_PATH\" \n export * \n}" + printf "$CONTENT" > "$MODULES_DIR/module.modulemap" + else + echo "NSLD: Swift bridging header '*-Swift.h' not found under '$SWIFT_HEADER_DIR'" + fi + else + echo "NSLD: Directory for Swift headers ($SWIFT_HEADER_DIR) not found." + fi +} + +function GEN_METADATA() { + TARGET_ARCH=$1 + set -e + cpu_arch=$(uname -m) + pushd "$SRCROOT/internal/metadata-generator-${cpu_arch}/bin" + ./build-step-metadata-generator.py $TARGET_ARCH + popd +} + +GEN_MODULEMAP $TARGET_ARCH +export OTHER_CFLAGS="$OTHER_CFLAGS -fmodule-map-file=\"$MODULES_DIR/module.modulemap\"" +printf "Generating metadata..." +GEN_METADATA $TARGET_ARCH +DELETE_SWIFT_MODULES_DIR + +function resolve_clang() { + # 1) If NS_LD is set and executable, honor it. + if [[ -n "$NS_LD" && -x "$NS_LD" ]]; then + echo "$NS_LD" + return 0 + fi + + # 2) TOOLCHAIN_DIR (if provided) + if [[ -n "$TOOLCHAIN_DIR" && -x "$TOOLCHAIN_DIR/usr/bin/clang" ]]; then + echo "$TOOLCHAIN_DIR/usr/bin/clang" + return 0 + fi + + # 3) Xcode's DT_TOOLCHAIN_DIR (provided by xcodebuild) + if [[ -n "$DT_TOOLCHAIN_DIR" && -x "$DT_TOOLCHAIN_DIR/usr/bin/clang" ]]; then + echo "$DT_TOOLCHAIN_DIR/usr/bin/clang" + return 0 + fi + + # 4) xcrun lookup (most reliable within Xcode build env) + local xcrun_clang + xcrun_clang=$(xcrun --find clang 2>/dev/null) || true + if [[ -n "$xcrun_clang" && -x "$xcrun_clang" ]]; then + echo "$xcrun_clang" + return 0 + fi + + # 5) Xcode default toolchain from xcode-select + local xcode_path + xcode_path=$(xcode-select -p 2>/dev/null) || true + if [[ -n "$xcode_path" && -x "$xcode_path/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang" ]]; then + echo "$xcode_path/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang" + return 0 + fi + + # 6) System fallback + if [[ -x "/usr/bin/clang" ]]; then + echo "/usr/bin/clang" + return 0 + fi + + return 1 +} + +CLANG_PATH=$(resolve_clang) +if [[ -z "$CLANG_PATH" ]]; then + echo "NSLD: ERROR: Could not locate a usable clang. TOOLCHAIN_DIR='${TOOLCHAIN_DIR}' DT_TOOLCHAIN_DIR='${DT_TOOLCHAIN_DIR}'." + exit 1 +fi + +# For visibility downstream, set NS_LD to the resolved path and invoke. +NS_LD="$CLANG_PATH" +"$NS_LD" "$@" diff --git a/project-template-tvos/internal/strip-dynamic-framework-architectures.sh b/project-template-tvos/internal/strip-dynamic-framework-architectures.sh new file mode 100755 index 000000000..1679d1a7b --- /dev/null +++ b/project-template-tvos/internal/strip-dynamic-framework-architectures.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash + +################################################################################ +# +# Copyright 2015 Realm Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +################################################################################ + +# This script strips all non-valid architectures from dynamic libraries in +# the application's `Frameworks` directory. +# +# The following environment variables are required: +# +# BUILT_PRODUCTS_DIR +# FRAMEWORKS_FOLDER_PATH +# VALID_ARCHS +# EXPANDED_CODE_SIGN_IDENTITY + + +# Signs a framework with the provided identity +code_sign() { + # Use the current code_sign_identitiy + echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" + echo "/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} --preserve-metadata=identifier,entitlements $1" + /usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} --preserve-metadata=identifier,entitlements "$1" +} + +if [ ! -e "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}" ]; then + exit 0 +fi + +echo "Stripping frameworks" +cd "${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}" + +for file in $(find . -type f); do + # Skip non-dynamic libraries + if ! [[ "$(file "$file")" == *"dynamically linked shared library"* ]]; then + continue + fi + # Get architectures for current file + archs="$(lipo -info "${file}" | rev | cut -d ':' -f1 | rev)" + stripped="" + for arch in $archs; do + if ! [[ "${VALID_ARCHS}" == *"$arch"* ]]; then + # Strip non-valid architectures in-place + lipo -remove "$arch" -output "$file" "$file" || exit 1 + stripped="$stripped $arch" + fi + done + if [[ "$stripped" != "" ]]; then + echo "Stripped $file of architectures:$stripped" + if [ "${CODE_SIGNING_ALLOWED}" == "YES" ]; then + code_sign "${file}" + fi + fi +done diff --git a/scripts/generate-spm-manifest.mjs b/scripts/generate-spm-manifest.mjs index 8619c4bd4..b18afbe95 100644 --- a/scripts/generate-spm-manifest.mjs +++ b/scripts/generate-spm-manifest.mjs @@ -3,16 +3,16 @@ // // A binaryTarget whose Release asset was never uploaded breaks resolution for // every consumer of that version (SwiftPM eagerly downloads every binaryTarget -// in a resolved manifest). Here, the visionOS product/targets are emitted only -// when the visionOS checksums are provided. +// in a resolved manifest). Here, the visionOS and tvOS products/targets are +// each emitted only when that platform's checksums are provided. // // Checksums come from the KEY=sha256 env files produced by // build_spm_artifacts.sh (one file per platform), passed individually via // --checksums and/or collected from a directory via --checksums-dir. The iOS -// checksums are always required. The visionOS checksums are additionally -// required when the release channel is anything but "next" (--channel), or -// when --strict is passed — so a real release can never silently ship an -// iOS-only manifest. +// checksums are always required. The visionOS and tvOS checksums are +// additionally required when the release channel is anything but "next" +// (--channel), or when --strict is passed — so a real release can never +// silently ship a manifest missing a platform its build matrix produced. import fs from "node:fs"; import path from "node:path"; import { parseArgs } from "node:util"; @@ -28,7 +28,7 @@ const USAGE = `Usage: node scripts/generate-spm-manifest.mjs --package .env name. +const OPTIONAL_PLATFORMS = [ + { + name: "visionOS", + slug: "visionos", + keys: ["NS_CHECKSUM_NATIVESCRIPT_VISIONOS", "NS_CHECKSUM_TKLIVESYNC_VISIONOS"], + platform: ".visionOS(.v1)", + product: "NativeScriptVisionOS", + targets: ["NativeScriptVisionOS", "TKLiveSyncVisionOS"], + comment: "visionOS family (xros + xrsimulator)", + }, + { + name: "tvOS", + slug: "tvos", + keys: ["NS_CHECKSUM_NATIVESCRIPT_TVOS", "NS_CHECKSUM_TKLIVESYNC_TVOS"], + platform: ".tvOS(.v13)", + product: "NativeScriptTvOS", + targets: ["NativeScriptTvOS", "TKLiveSyncTvOS"], + comment: "tvOS family (appletvos + appletvsimulator)", + }, ]; -const KNOWN_KEYS = new Set([...IOS_KEYS, ...VISION_KEYS]); +const KNOWN_KEYS = new Set([ + ...IOS_KEYS, + ...OPTIONAL_PLATFORMS.flatMap((p) => p.keys), +]); // A binaryTarget checksum must be a 64-char lowercase hex SHA-256. Reject // anything else now (empty/truncated/uppercase) so we can't emit a manifest @@ -131,40 +153,48 @@ if (missingIos.length) { process.exit(1); } -const presentVision = VISION_KEYS.filter((k) => k in checksums); -if (presentVision.length !== 0 && presentVision.length !== VISION_KEYS.length) { - const missing = VISION_KEYS.filter((k) => !(k in checksums)); - console.error( - `ERROR: partial visionOS checksums — have ${presentVision.join(", ")} but missing ${missing.join(", ")}` - ); - process.exit(1); -} -const includeVision = presentVision.length === VISION_KEYS.length; -if (opts.strict && !includeVision) { - console.error( - "ERROR: this channel requires the visionOS checksums (a non-next release must ship the full manifest)" - ); - process.exit(1); +const included = []; +for (const p of OPTIONAL_PLATFORMS) { + const present = p.keys.filter((k) => k in checksums); + if (present.length !== 0 && present.length !== p.keys.length) { + const missing = p.keys.filter((k) => !(k in checksums)); + console.error( + `ERROR: partial ${p.name} checksums — have ${present.join(", ")} but missing ${missing.join(", ")}` + ); + process.exit(1); + } + if (present.length === p.keys.length) { + included.push(p); + } else if (opts.strict) { + console.error( + `ERROR: this channel requires the ${p.name} checksums (a non-next release must ship the full manifest)` + ); + process.exit(1); + } } -const visionPlatform = includeVision ? `\n .visionOS(.v1),` : ""; -const visionProduct = includeVision - ? `\n // visionOS family (xros + xrsimulator) - .library(name: "NativeScriptVisionOS", targets: ["NativeScriptVisionOS", "TKLiveSyncVisionOS"]),` - : ""; -const visionTargets = includeVision - ? ` +const extraPlatforms = included.map((p) => `\n ${p.platform},`).join(""); +const extraProducts = included + .map( + (p) => `\n // ${p.comment} + .library(name: "${p.product}", targets: [${p.targets.map((t) => `"${t}"`).join(", ")}]),` + ) + .join(""); +const extraTargets = included + .map( + (p) => ` .binaryTarget( - name: "NativeScriptVisionOS", - url: "\\(releaseBase)/NativeScript.visionos.xcframework.zip", - checksum: "${checksums.NS_CHECKSUM_NATIVESCRIPT_VISIONOS}" + name: "${p.targets[0]}", + url: "\\(releaseBase)/NativeScript.${p.slug}.xcframework.zip", + checksum: "${checksums[p.keys[0]]}" ), .binaryTarget( - name: "TKLiveSyncVisionOS", - url: "\\(releaseBase)/TKLiveSync.visionos.xcframework.zip", - checksum: "${checksums.NS_CHECKSUM_TKLIVESYNC_VISIONOS}" + name: "${p.targets[1]}", + url: "\\(releaseBase)/TKLiveSync.${p.slug}.xcframework.zip", + checksum: "${checksums[p.keys[1]]}" ),` - : ""; + ) + .join(""); const manifest = `// swift-tools-version: 5.10 // The swift-tools-version declares the minimum version of Swift required to build this package. @@ -173,9 +203,9 @@ const manifest = `// swift-tools-version: 5.10 // Emitted per release by scripts/generate-spm-manifest.mjs in // github.com/NativeScript/ios. The target set mirrors the assets the release // publishes: the rolling "next" channel builds iOS only, so its manifests omit -// the visionOS product/targets (SwiftPM eagerly downloads every binaryTarget -// in a resolved manifest, and a target without an uploaded asset would break -// resolution for every consumer of that version). +// the visionOS and tvOS products/targets (SwiftPM eagerly downloads every +// binaryTarget in a resolved manifest, and a target without an uploaded asset +// would break resolution for every consumer of that version). // // Copyright OpenJS Foundation and other contributors, https://openjsf.org // @@ -206,13 +236,13 @@ let package = Package( name: "NativeScriptSDK", platforms: [ .iOS(.v13), - .macCatalyst(.v13),${visionPlatform} + .macCatalyst(.v13),${extraPlatforms} ], products: [ // iOS family (iphoneos + iphonesimulator + Mac Catalyst) .library(name: "NativeScript", targets: ["NativeScript", "TKLiveSync"]), // Backwards-compatible alias for the historical product name. - .library(name: "NativeScriptSDK", targets: ["NativeScript", "TKLiveSync"]),${visionProduct} + .library(name: "NativeScriptSDK", targets: ["NativeScript", "TKLiveSync"]),${extraProducts} ], dependencies: [], targets: [ @@ -225,7 +255,7 @@ let package = Package( name: "TKLiveSync", url: "\\(releaseBase)/TKLiveSync.xcframework.zip", checksum: "${checksums.NS_CHECKSUM_TKLIVESYNC_IOS}" - ),${visionTargets} + ),${extraTargets} ] ) `; @@ -234,4 +264,6 @@ fs.writeFileSync(opts.package, manifest); console.log(`Generated ${opts.package}`); console.log(` version: ${opts.version}`); -console.log(` targets: iOS${includeVision ? " + visionOS" : " only (no visionOS checksums provided)"}`); +console.log( + ` targets: iOS${included.length ? " + " + included.map((p) => p.name).join(" + ") : " only (no visionOS/tvOS checksums provided)"}` +); diff --git a/scripts/generate-spm-probe.mjs b/scripts/generate-spm-probe.mjs index 685f6c536..2b92bd1ec 100644 --- a/scripts/generate-spm-probe.mjs +++ b/scripts/generate-spm-probe.mjs @@ -9,7 +9,7 @@ // released manifest's target set is channel-shaped (see // generate-spm-manifest.mjs), the SAME probe verifies every channel: "next" // manifests only declare the iOS artifacts, real releases also declare (and -// therefore also verify) the visionOS ones. +// therefore also verify) the visionOS and tvOS ones. // // --assert-release-manifest additionally fetches the released manifest and // asserts it pins nsVersion to this exact version. This guards against the tag diff --git a/scripts/install-tvos-v8.sh b/scripts/install-tvos-v8.sh new file mode 100755 index 000000000..8d53b5891 --- /dev/null +++ b/scripts/install-tvos-v8.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Install source-built tvOS artifacts from NativeScript/v8-buildscripts. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +BUILD="${1:?Usage: scripts/install-tvos-v8.sh /path/to/v8-buildscripts}" +BUILD="$(cd "$BUILD" && pwd)" +VERSION="$(sed -E 's/^v8-(.*)-[0-9]+$/\1/' "$ROOT/V8_RELEASE")" +for variant in arm64-tvdevice arm64-tvsimulator; do + test "$(cat "$BUILD/dist/ios-$variant/V8_VERSION")" = "$VERSION" || { + echo "V8 version mismatch for $variant; expected $VERSION" >&2; exit 1; + } + test -f "$BUILD/dist/ios-$variant/lib/libv8_base_without_compiler.a" +done +for pair in arm64-tvdevice:arm64-appletvos arm64-tvsimulator:arm64-appletvsimulator; do + src="$BUILD/dist/ios-${pair%%:*}" + dest="$ROOT/NativeScript/lib/${pair#*:}" + mkdir -p "$dest" + find "$dest" -maxdepth 1 -name '*.a' ! -name libffi.a -delete + cp "$src/lib/"*.a "$dest/" +done +# Headers and inspector sources must come from the same V8 build as the archives. +cp -R "$BUILD/dist/ios-arm64-tvdevice/include/." "$ROOT/NativeScript/include/" +python3 "$ROOT/tools/v8/vendor_inspector_sources.py" \ + --v8-dir "$BUILD/.v8/v8" \ + --gen-dir "$BUILD/.v8/v8/out.gn/arm64-tvdevice-release/gen" \ + --dest "$ROOT/NativeScript/inspector" +echo "$VERSION" > "$ROOT/NativeScript/lib/.tvos-v8-version" diff --git a/scripts/resolve-release.mjs b/scripts/resolve-release.mjs index fb7a8dc96..48031c355 100644 --- a/scripts/resolve-release.mjs +++ b/scripts/resolve-release.mjs @@ -10,10 +10,10 @@ // * push to main / dispatch w/o input -> rolling "next" prerelease // (get-next-version.js) // -// The build matrix: ios always builds/publishes; visionos only for real -// releases (npm tag != "next"), never for the rolling next channel. Each entry -// carries `target` (package identity, @nativescript/) and `script` -// (the npm build script; note the vision script is build-vision). +// The build matrix: ios always builds/publishes; visionos and tvos only for +// real releases (npm tag != "next"), never for the rolling next channel. Each +// entry carries `target` (package identity, @nativescript/) and +// `script` (the npm build script; note the vision script is build-vision). // // GITHUB_REF and GITHUB_OUTPUT are read from the environment (GitHub's // contract); the workflow_dispatch version input is a real argument. @@ -80,6 +80,7 @@ const tag = runScript("get-npm-tag.js", ["--version", version]); const targets = [{ target: "ios", script: "build-ios" }]; if (tag !== "next") { targets.push({ target: "visionos", script: "build-vision" }); + targets.push({ target: "tvos", script: "build-tvos" }); } const matrix = JSON.stringify({ include: targets }); diff --git a/spm-templates/local-spm-tvos/Package.swift b/spm-templates/local-spm-tvos/Package.swift new file mode 100644 index 000000000..ee8a9c0de --- /dev/null +++ b/spm-templates/local-spm-tvos/Package.swift @@ -0,0 +1,26 @@ +// swift-tools-version: 5.10 +// Local SwiftPM package embedded in @nativescript/tvos packages built with +// NS_SPM_MODE=embedded (the default outside the release pipeline). Product and +// target names mirror the tvOS half of the released +// github.com/NativeScript/ios-spm manifest (see generate-spm-manifest.mjs), +// but the binary targets point at the xcframework zips packed next to this +// manifest, so the npm package is fully self-contained and portable. +// +// The frameworks are zipped because npm strips symlinks; SwiftPM extracts +// local zip binary targets itself. +import PackageDescription + +let package = Package( + name: "NativeScriptSDK", + platforms: [ + .tvOS(.v13), + ], + products: [ + // tvOS family (appletvos + appletvsimulator) + .library(name: "NativeScriptTvOS", targets: ["NativeScriptTvOS", "TKLiveSyncTvOS"]), + ], + targets: [ + .binaryTarget(name: "NativeScriptTvOS", path: "NativeScript.tvos.xcframework.zip"), + .binaryTarget(name: "TKLiveSyncTvOS", path: "TKLiveSync.tvos.xcframework.zip"), + ] +) diff --git a/tests/spm/manifest.py b/tests/spm/manifest.py new file mode 100755 index 000000000..9d0e3c251 --- /dev/null +++ b/tests/spm/manifest.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""generate-spm-manifest.mjs emits each optional platform whole, or not at all. + +Runs the generator with synthetic checksum files the way the release +workflow does (--checksums-dir over the merged spm-artifacts download) and +checks the channel rules: "next" ships iOS only, a real release must carry +every platform its build matrix produces, and a half-present platform is +rejected rather than shipped. +""" +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[2] +GENERATOR = ROOT / 'scripts/generate-spm-manifest.mjs' +KEYS = { + 'ios': ['NS_CHECKSUM_NATIVESCRIPT_IOS', 'NS_CHECKSUM_TKLIVESYNC_IOS'], + 'visionos': ['NS_CHECKSUM_NATIVESCRIPT_VISIONOS', 'NS_CHECKSUM_TKLIVESYNC_VISIONOS'], + 'tvos': ['NS_CHECKSUM_NATIVESCRIPT_TVOS', 'NS_CHECKSUM_TKLIVESYNC_TVOS'], +} +TVOS_FRAGMENTS = [ + '.tvOS(.v13),', + '.library(name: "NativeScriptTvOS", targets: ["NativeScriptTvOS", "TKLiveSyncTvOS"])', + 'name: "NativeScriptTvOS",\n url: "\\(releaseBase)/NativeScript.tvos.xcframework.zip"', + 'name: "TKLiveSyncTvOS",\n url: "\\(releaseBase)/TKLiveSync.tvos.xcframework.zip"', +] +VISION_FRAGMENTS = ['.visionOS(.v1),', '.library(name: "NativeScriptVisionOS"', 'NativeScript.visionos.xcframework.zip'] + + +def checksum(key): + return format(abs(hash(key)) % (1 << 64), '016x') * 4 + + +class Manifest(unittest.TestCase): + def generate(self, platforms, channel, keys=None): + with tempfile.TemporaryDirectory() as tmp: + checksums = Path(tmp) / 'spm-artifacts' + checksums.mkdir() + for platform in platforms: + lines = [f'{k}={checksum(k)}' for k in (keys or KEYS)[platform]] + (checksums / f'checksums-{platform}.env').write_text('\n'.join(lines) + '\n') + manifest = Path(tmp) / 'Package.swift' + result = subprocess.run( + ['node', GENERATOR, '--package', manifest, '--version', '9.9.9', '--checksums-dir', checksums, '--channel', channel], + capture_output=True, text=True) + return result, manifest.read_text() if manifest.exists() else '' + + def test_release_carries_every_platform(self): + result, manifest = self.generate(['ios', 'visionos', 'tvos'], 'latest') + self.assertEqual(result.returncode, 0, result.stderr) + for fragment in TVOS_FRAGMENTS + VISION_FRAGMENTS: + self.assertIn(fragment, manifest) + for key in KEYS['tvos']: + self.assertIn(checksum(key), manifest) + self.assertIn('let nsVersion = "9.9.9"', manifest) + self.assertLess(manifest.index('.visionOS(.v1)'), manifest.index('.tvOS(.v13)')) + self.dump_package(manifest) + + def test_next_channel_is_ios_only(self): + result, manifest = self.generate(['ios'], 'next') + self.assertEqual(result.returncode, 0, result.stderr) + for fragment in TVOS_FRAGMENTS + VISION_FRAGMENTS: + self.assertNotIn(fragment, manifest) + self.dump_package(manifest) + + def test_release_without_tvos_is_rejected(self): + result, _ = self.generate(['ios', 'visionos'], 'latest') + self.assertEqual(result.returncode, 1) + self.assertIn('requires the tvOS checksums', result.stderr) + + def test_release_without_visionos_is_rejected(self): + result, _ = self.generate(['ios', 'tvos'], 'latest') + self.assertEqual(result.returncode, 1) + self.assertIn('requires the visionOS checksums', result.stderr) + + def test_partial_tvos_is_rejected_on_every_channel(self): + partial = dict(KEYS, tvos=KEYS['tvos'][:1]) + for channel in ['next', 'latest']: + result, _ = self.generate(['ios', 'visionos', 'tvos'], channel, keys=partial) + self.assertEqual(result.returncode, 1, channel) + self.assertIn('partial tvOS checksums', result.stderr) + + def dump_package(self, manifest): + """The emitted Swift must load as a package manifest, not merely look like one.""" + if 'CI' not in os.environ and not os.environ.get('SPM_DUMP_PACKAGE'): + return + with tempfile.TemporaryDirectory() as tmp: + (Path(tmp) / 'Package.swift').write_text(manifest) + subprocess.run(['swift', 'package', 'dump-package', '--package-path', tmp], check=True, capture_output=True) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/tvos/template.py b/tests/tvos/template.py new file mode 100755 index 000000000..54736ed6f --- /dev/null +++ b/tests/tvos/template.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +"""project-template-tvos is the iOS template plus an enumerated tvOS delta. + +Checks that the checked-in tvOS template parses with Apple's plist reader, +differs from project-template-ios only in the settings tvOS needs, agrees with +the local SwiftPM package it is bundled with, and comes out of both packaging +stamps (released ios-spm pin / embedded local package) still parseable. Set +TVOS_REVIEW_CLI to a nativescript-cli checkout to also parse the stamped +projects with the CLI's own pbxproj parser. +""" +import json +import os +from pathlib import Path +import re +import shutil +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[2] +IOS = ROOT / 'project-template-ios' +TVOS = ROOT / 'project-template-tvos' +LOCAL_SPM = ROOT / 'spm-templates/local-spm-tvos/Package.swift' +PBXPROJ = '__PROJECT_NAME__.xcodeproj/project.pbxproj' +XCCONFIG = 'internal/nativescript-build.xcconfig' +PRODUCT = 'NativeScriptTvOS' +PLATFORMS = 'appletvos appletvsimulator' +CLI = os.environ.get('TVOS_REVIEW_CLI') + + +def template_files(template): + """Relative paths of the template's files, minus per-machine leftovers.""" + files = set() + for path in template.rglob('*'): + rel = path.relative_to(template) + if not path.is_file() or any(part.startswith('.') or part == 'project.xcworkspace' for part in rel.parts): + continue + files.add(rel.as_posix()) + return files + + +def read_pbxproj(path): + return json.loads(subprocess.check_output(['plutil', '-convert', 'json', '-o', '-', path])) + + +def as_tvos(obj): + """The tvOS counterpart of an iOS template object: the complete allowed delta.""" + obj = json.loads(json.dumps(obj)) + if obj.get('isa') == 'XCSwiftPackageProductDependency' and obj.get('productName') == 'NativeScript': + obj['productName'] = PRODUCT + if obj.get('isa') == 'XCBuildConfiguration': + settings = obj['buildSettings'] + if settings.pop('IPHONEOS_DEPLOYMENT_TARGET', None) is not None: + settings['TVOS_DEPLOYMENT_TARGET'] = '13.0' + if 'SDKROOT' in settings: + settings['SDKROOT'] = 'appletvos' + settings.pop('SUPPORTS_UIKITFORMAC', None) + settings['SUPPORTED_PLATFORMS'] = PLATFORMS + settings['SUPPORTS_MACCATALYST'] = 'NO' + settings['TARGETED_DEVICE_FAMILY'] = '3' + return obj + + +SETTING = re.compile(r'^\s*([A-Za-z_][A-Za-z0-9_]*(?:\[[^\]]*\])?)\s*=\s*(.*?)\s*$') + + +def xcconfig_settings(path): + settings = {} + for line in path.read_text().splitlines(): + match = SETTING.match(line) + if match and not line.lstrip().startswith('//'): + settings[match.group(1)] = match.group(2) + return settings + + +def objects_of(pbxproj, isa): + return [o for o in pbxproj['objects'].values() if o['isa'] == isa] + + +def parse_with_cli(project): + subprocess.run(['node', '-e', ''' +const assert = require('node:assert/strict'); +const xcode = require(require.resolve('nativescript-dev-xcode', { paths: [process.argv[1]] })); +const project = xcode.project(process.argv[2]); project.parseSync(); +const target = project.getFirstTarget(); assert(target.uuid); +assert(project.pbxFrameworksBuildPhaseObj(target.uuid)); +''', CLI, str(project)], check=True) + + +class Template(unittest.TestCase): + def test_files_match_ios_template(self): + self.assertEqual(template_files(TVOS), template_files(IOS)) + for rel in sorted(template_files(IOS) - {PBXPROJ, XCCONFIG}): + self.assertEqual((TVOS / rel).read_bytes(), (IOS / rel).read_bytes(), rel) + + def test_project_is_ios_project_with_tvos_settings(self): + ios, tvos = read_pbxproj(IOS / PBXPROJ), read_pbxproj(TVOS / PBXPROJ) + self.assertEqual(tvos['rootObject'], ios['rootObject']) + self.assertEqual(set(tvos['objects']), set(ios['objects'])) + for identifier, obj in ios['objects'].items(): + self.assertEqual(tvos['objects'][identifier], as_tvos(obj), identifier) + for config in objects_of(tvos, 'XCBuildConfiguration'): + settings = config['buildSettings'] + self.assertEqual(settings['TARGETED_DEVICE_FAMILY'], '3') + self.assertEqual(settings['SUPPORTED_PLATFORMS'], PLATFORMS) + self.assertNotIn('IPHONEOS_DEPLOYMENT_TARGET', settings) + self.assertEqual([o['productName'] for o in objects_of(tvos, 'XCSwiftPackageProductDependency')], [PRODUCT]) + text = (TVOS / PBXPROJ).read_text() + self.assertNotIn('/* NativeScript */', text) + self.assertNotIn('/* NativeScript in Frameworks */', text) + self.assertIn('XCRemoteSwiftPackageReference "ios-spm"', text) + self.assertIn('__NS_RUNTIME_VERSION__', text) + + def test_build_xcconfig_keeps_linker_setup_in_step(self): + ios, tvos = xcconfig_settings(IOS / XCCONFIG), xcconfig_settings(TVOS / XCCONFIG) + for key in ['OTHER_LDFLAGS[sdk=*]', 'FRAMEWORK_SEARCH_PATHS[sdk=*]', 'LD', 'LDPLUSPLUS', 'VALIDATE_WORKSPACE']: + self.assertEqual(tvos[key], ios[key], key) + self.assertEqual(tvos['TARGETED_DEVICE_FAMILY'], '3') + for sdk in ['appletvos', 'appletvsimulator']: + excluded = tvos[f'EXCLUDED_ARCHS[sdk={sdk}*]'].split() + self.assertIn('x86_64', excluded) + self.assertIn('i386', excluded) + self.assertNotIn('arm64', excluded) + self.assertFalse([k for k in tvos if 'iphone' in k or 'macosx' in k], 'iOS-only settings in the tvOS xcconfig') + + def test_local_spm_package_matches_template_product(self): + manifest = LOCAL_SPM.read_text() + self.assertIn(f'.library(name: "{PRODUCT}", targets: ["NativeScriptTvOS", "TKLiveSyncTvOS"])', manifest) + self.assertIn('.binaryTarget(name: "NativeScriptTvOS", path: "NativeScript.tvos.xcframework.zip")', manifest) + self.assertIn('.binaryTarget(name: "TKLiveSyncTvOS", path: "TKLiveSync.tvos.xcframework.zip")', manifest) + self.assertIn('.tvOS(.v13)', manifest) + + def test_remote_stamp(self): + with tempfile.TemporaryDirectory() as tmp: + template = Path(tmp) / 'framework' + shutil.copytree(TVOS, template) + project = template / PBXPROJ + subprocess.run(['node', ROOT / 'scripts/stamp-template-version.mjs', project, '9.9.9'], check=True) + self.assertNotIn('__NS_RUNTIME_VERSION__', project.read_text()) + stamped = read_pbxproj(project) + [reference] = objects_of(stamped, 'XCRemoteSwiftPackageReference') + self.assertIn('NativeScript/ios-spm', reference['repositoryURL']) + self.assertEqual(reference['requirement'], {'kind': 'exactVersion', 'version': '9.9.9'}) + if CLI: + parse_with_cli(project) + + def test_embedded_stamp(self): + with tempfile.TemporaryDirectory() as tmp: + template = Path(tmp) / 'framework' + shutil.copytree(TVOS, template) + project = template / PBXPROJ + local = template / 'internal/local-spm' + local.mkdir() + shutil.copy2(LOCAL_SPM, local) + subprocess.run(['node', ROOT / 'scripts/stamp-template-local-spm.mjs', project, 'internal/local-spm', '--package-dir', local], check=True) + stamped = read_pbxproj(project) + self.assertEqual(objects_of(stamped, 'XCRemoteSwiftPackageReference'), []) + [reference] = objects_of(stamped, 'XCLocalSwiftPackageReference') + self.assertEqual(reference['relativePath'], 'internal/local-spm') + [dependency] = objects_of(stamped, 'XCSwiftPackageProductDependency') + self.assertEqual(stamped['objects'][dependency['package']], reference) + self.assertEqual(dependency['productName'], PRODUCT) + if CLI: + parse_with_cli(project) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/tvos-review/README.md b/tools/tvos-review/README.md new file mode 100644 index 000000000..94c4172ef --- /dev/null +++ b/tools/tvos-review/README.md @@ -0,0 +1,53 @@ +# Build and test the tvOS review stack + +This builds an isolated `org.nativescript.tvosreview` app from the pinned contribution branches. It does not require the author's app, framework bundles, npm cache, signing team, or local Rust paths. V8 and metadata generation are built from source. npm and Cargo dependencies still require network access; the app's resolved npm lockfile is retained in the workspace. + +## Prerequisites + +Use an Apple Silicon Mac with Xcode and its iOS and tvOS SDKs installed and selected with `xcode-select`. The validation host uses Xcode 26.6, Node 22.23.2, Python 3, CocoaPods 1.17, cbindgen, and Rust nightly 2026-09-07. Install `rustup`, `cbindgen`, CMake, jq, and CocoaPods before starting. The script installs the pinned Rust toolchain and `rust-src`. Python must trust the HTTPS certificate chain; installing `certifi` lets the script use its CA bundle. + +The host package/test steps pin npm 10.9.8 inside `host-tooling`, because upstream CLI fixtures still rely on npm 10 behavior. The app setup installs npm 12.0.2 inside the workspace because npm 10 crashes while resolving this peer-dependency graph. Node must meet its requirement, 22.22.2 or newer in the Node 22 series. Local review tarballs use a stable version with `+review.` build metadata so npm peer ranges accept upstream prerelease sources. + +Make `pod` and its matching Ruby visible on PATH. If CocoaPods uses a separate GEM_HOME, export that environment too. Do not copy a machine-specific Cellar path from another Mac. + +Allow several hours and at least 100 GB free space for the first build. V8 compiles separately for device and simulator. The device deployment target is tvOS 13.0; Xcode raises the arm64 simulator minimum to tvOS 14.0. No Apple signing account is needed for the simulator workflow. + +## Run + +From the runtime PR checkout, choose an empty directory outside the checkout: + +```sh +python3 tools/tvos-review/review.py doctor --workspace /path/to/tvos-review +python3 tools/tvos-review/review.py all --workspace /path/to/tvos-review \ + --simulator YOUR_APPLE_TV_SIMULATOR_UDID --jobs 6 +``` + +Find a simulator with `xcrun simctl list devices available`. The `test` step runs Core and CLI host suites, the test runner suite, and `ns test tvos` on the selected simulator. The fixture injects a Foundation text-WebSocket transport through the test runner's `createSocket` option. The default `@valor/nativescript-websockets` binary currently lacks tvOS slices, so an unmodified `ns test init` scaffold still needs a compatible transport. + +The six app tests exercise platform detection, writable cache storage, Canvas 2D pixels and Unicode metrics, ImageData ownership across garbage collection, WebGL typed buffers and readback, WebGPU compute readback, and query cleanup. + +The `checkout`, `runtime`, `packages`, `canvas`, `helpers`, `app`, and `test` steps can also be selected individually. Resume after a failure by running the failed step after correcting its cause. Preserve the failed app directory for comparison; `app` deliberately refuses to overwrite an existing app. Use another empty workspace for a complete clean rerun. + +`revisions.json` pins companion repositories. The runtime is pinned to the checkout containing this script, so that commit must be published before `checkout`. `canvas-extras.json` lists independent Canvas fixes to apply on top of its tvOS branch. The script writes the resolved revisions and every command's output under the workspace. It adds a workspace-only Cargo override pointing to the pinned rust-skia fork until that dependency is available upstream. + +## Device and distribution checks + +Physical devices need your own signing team and provisioning. Use the generated app and the workspace CLI: + +```sh +cd /path/to/tvos-review/app +node ../cli/bin/tns run tvos --device YOUR_APPLE_TV_UDID --team-id YOUR_TEAM --no-local-cli +node ../cli/bin/tns build tvos --release --for-appstore --team-id YOUR_TEAM --no-local-cli +``` + +With an explicit paired Apple TV identifier (CoreDevice UUID, hardware UDID, or unique device name), `ns run tvos` uses Apple's `devicectl` transport. Find identifiers with `xcrun devicectl list devices`. Simulator identifiers continue through the simulator transport. The network-paired device path builds an IPA, installs it and launches the app. Use `--no-watch` for a single deployment; the default watcher performs a full rebuild/install on source or resource changes. HMR, debugger attachment and console streaming are not implemented for this transport. `ns device` still uses the existing discovery backend. + +An asleep Apple TV cannot be launched by this workflow; the command reports the launch failure. A simulator pass does not establish physical controller, audio, haptics, frame pacing, or App Store acceptance. The generated fixture is a test app, not a submission-ready product. + +## Template regression + +```sh +TVOS_REVIEW_CLI=/path/to/tvos-review/cli python3 tests/tvos/template.py +``` + +This verifies that `project-template-tvos` is the current upstream iOS template plus the tvOS build settings, agrees with the bundled local runtime package, survives both packaging stamps (the released ios-spm pin and the embedded local package), and parses with the actual CLI project parser. Run it again when upstream changes the iOS template. diff --git a/tools/tvos-review/app.py b/tools/tvos-review/app.py new file mode 100644 index 000000000..e52784a3f --- /dev/null +++ b/tools/tvos-review/app.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +import json +import os +from pathlib import Path +import plistlib +import shutil +import subprocess +import sys + +work = Path(sys.argv[1]).resolve() +if not (work / '.tvos-review-workspace').is_file(): + raise RuntimeError('Use review.py to initialize an isolated reviewer workspace first.') +here = Path(__file__).resolve().parent +# npm 10 crashes in peer resolution for this dependency graph. Keep the +# tested npm version local to the reviewer workspace, including CLI child calls. +tooling = work / 'tooling' +if not (tooling / 'node_modules/.bin/npm').exists(): + subprocess.run(['npm', 'install', '--prefix', str(tooling), '--ignore-scripts', '--no-audit', '--no-fund', 'npm@12.0.2'], check=True) +os.environ['PATH'] = str(tooling / 'node_modules/.bin') + ':' + os.environ['PATH'] +packages = work / 'packages' +packages.mkdir(exist_ok=True) + +def merge(source, dest): + dest.mkdir(parents=True, exist_ok=True) + old = plistlib.loads((dest / 'Info.plist').read_bytes()) if (dest / 'Info.plist').exists() else {'CFBundlePackageType': 'XFWK', 'XCFrameworkFormatVersion': '1.0', 'AvailableLibraries': []} + new = plistlib.loads((source / 'Info.plist').read_bytes()) + ids = {item['LibraryIdentifier'] for item in new['AvailableLibraries']} + old['AvailableLibraries'] = [item for item in old['AvailableLibraries'] if item['LibraryIdentifier'] not in ids] + new['AvailableLibraries'] + for item in new['AvailableLibraries']: + identifier = item['LibraryIdentifier'] + shutil.copytree(source / identifier, dest / identifier, dirs_exist_ok=True, symlinks=True) + (dest / 'Info.plist').write_bytes(plistlib.dumps(old)) + +def pack(name, source): + dest = packages / name + if dest.exists(): + shutil.rmtree(dest) + shutil.copytree(source, dest, symlinks=True) + if name == 'core': + for framework in ['TNSWidgets', 'NSCWinterTC']: + merge(work / ('artifacts/' + framework + '.xcframework'), dest / ('platforms/ios/' + framework + '.xcframework')) + if name == 'canvas': + # Include the exact native bridge sources being reviewed, even if Nx + # considers its TypeScript output up to date. + shutil.copytree(work / 'canvas/packages/canvas/platforms/ios/src', dest / 'platforms/ios/src', dirs_exist_ok=True) + shutil.copy2(work / 'canvas/packages/canvas/platforms/ios/build.xcconfig', dest / 'platforms/ios/build.xcconfig') + shutil.copy2(work / 'canvas/nativescript-v8/Package.swift', dest / 'platforms/ios/NativeScriptV8/Package.swift') + merge(work / 'artifacts/CanvasNative.xcframework', dest / 'platforms/ios/CanvasNative.xcframework') + # npm peer ranges reject upstream prerelease versions. Give only these + # local review artifacts a stable SemVer with the source revision attached. + manifest_path = dest / 'package.json' + manifest = json.loads(manifest_path.read_text()) + repository = {'webpack': 'core'}.get(name, name) + revision = subprocess.check_output(['git', 'rev-parse', '--short=12', 'HEAD'], cwd=work / repository, text=True).strip() + manifest['version'] = manifest['version'].split('-')[0].split('+')[0] + '+review.' + revision + manifest_path.write_text(json.dumps(manifest, indent=2) + '\n') + output = json.loads(subprocess.check_output(['npm', 'pack', '--json', '--pack-destination', str(packages)], cwd=dest, text=True)) + entry = output[0] if isinstance(output, list) else next(iter(output.values())) + return '../packages/' + entry['filename'] + +core = pack('core', work / 'core/dist/packages/core') +webpack = pack('webpack', work / 'core/dist/packages/webpack5') +canvas = pack('canvas', work / 'canvas/dist/packages/canvas') +app = work / 'app' +if app.exists(): + raise RuntimeError('App already exists. Keep it for comparison; use a new workspace for another clean installation.') +shutil.copytree(here / 'app', app) +shutil.copy2(work / 'canvas/tools/tests/webgpu-map-progress.spec.js', app / 'app/webgpu-map-progress.spec.js') +shutil.copy2(work / 'canvas/tools/tests/imagedata-ownership.js', app / 'app/imagedata-ownership.js') +runtime = next((work / 'runtime/dist').glob('nativescript-tvos-*.tgz')) +runner = next((work / 'test-runner').glob('nativescript-unit-test-runner-*.tgz')) +manifest = { + 'name': 'tvosreview', 'version': '1.0.0', 'private': True, + 'dependencies': {'@nativescript/core': 'file:' + core, '@nativescript/canvas': 'file:' + canvas, '@nativescript/unit-test-runner': 'file:../test-runner/' + runner.name}, + 'devDependencies': {'@nativescript/webpack': 'file:' + webpack, 'typescript': '5.8.3', 'vitest': '4.1.10', '@vitest/runner': '4.1.10', 'vite': '8.2.1'}, + 'nativescript': {'id': 'org.nativescript.tvosreview'}, +} +(app / 'package.json').write_text(json.dumps(manifest, indent=2) + '\n') +subprocess.run(['npm', 'install', '--package-lock-only', '--ignore-scripts'], cwd=app, check=True) +subprocess.run(['npm', 'ci'], cwd=app, check=True) +cli = ['node', str(work / 'cli/bin/tns')] +subprocess.run(cli + ['platform', 'add', 'tvos', '--frameworkPath', str(runtime), '--no-local-cli'], cwd=app, check=True) +subprocess.run(cli + ['build', 'tvos', '--emulator', '--release', '--env.production', '--no-hmr', '--no-local-cli'], cwd=app, check=True) diff --git a/tools/tvos-review/app/App_Resources/tvOS/Info.plist b/tools/tvos-review/app/App_Resources/tvOS/Info.plist new file mode 100644 index 000000000..6badea08f --- /dev/null +++ b/tools/tvos-review/app/App_Resources/tvOS/Info.plist @@ -0,0 +1,15 @@ + + + +CFBundleDisplayNametvOS Review +CFBundleIdentifier$(PRODUCT_BUNDLE_IDENTIFIER) +CFBundleExecutable$(EXECUTABLE_NAME) +CFBundleName$(PRODUCT_NAME) +CFBundlePackageTypeAPPL +CFBundleShortVersionString1.0 +CFBundleVersion1 +LSRequiresIPhoneOS +UIDeviceFamily3 +NSAppTransportSecurityNSAllowsLocalNetworking +NSLocalNetworkUsageDescriptionConnect to the local test runner. + diff --git a/tools/tvos-review/app/App_Resources/tvOS/build.xcconfig b/tools/tvos-review/app/App_Resources/tvOS/build.xcconfig new file mode 100644 index 000000000..34083c2f0 --- /dev/null +++ b/tools/tvos-review/app/App_Resources/tvOS/build.xcconfig @@ -0,0 +1,4 @@ +TVOS_DEPLOYMENT_TARGET = 13.0 +TARGETED_DEVICE_FAMILY = 3 +ASSETCATALOG_COMPILER_APPICON_NAME = +CODE_SIGN_STYLE = Automatic diff --git a/tools/tvos-review/app/app/app.js b/tools/tvos-review/app/app/app.js new file mode 100644 index 000000000..306c59980 --- /dev/null +++ b/tools/tvos-review/app/app/app.js @@ -0,0 +1,39 @@ +import { Application, StackLayout, Label, knownFolders, isTvOS, Device } from '@nativescript/core'; +import { Canvas } from '@nativescript/canvas'; +Application.run({ create() { + const page = new StackLayout(); + page.backgroundColor = '#091325'; + const status = new Label(); + status.text = 'tvOS runtime review'; + status.color = '#ffffff'; + status.fontSize = 36; + status.margin = 40; + page.addChild(status); + const canvas = new Canvas(); + canvas.marginLeft = 75; + canvas.marginRight = 75; + canvas.width = 640; + canvas.height = 360; + canvas.on('ready', () => { + try { + if (!isTvOS || Device.os !== 'tvOS') throw new Error('Incorrect platform identity'); + const context = canvas.getContext('2d'); + context.fillStyle = '#00e5ff'; + context.fillRect(0, 0, 640, 360); + context.fillStyle = '#091325'; + context.font = '32px sans-serif'; + context.fillText('Canvas + V8 14.9', 240, 80); + const sample = context.getImageData(10, 10, 1, 1).data; + if (sample[1] < 200 || sample[2] < 200) throw new Error('Canvas pixel check failed'); + const result = { passed: true, platform: Device.os, pixels: Array.from(sample), runtime: '9.1.0' }; + knownFolders.temp().getFile('tvos-review.json').writeTextSync(JSON.stringify(result)); + status.text = 'PASS: platform, Canvas and pixel readback'; + console.log('TVOS_REVIEW_PASS ' + JSON.stringify(result)); + } catch (error) { + status.text = 'FAIL: ' + error.message; + console.error('TVOS_REVIEW_FAIL ' + error.stack); + } + }); + page.addChild(canvas); + return page; +}}); diff --git a/tools/tvos-review/app/app/package.json b/tools/tvos-review/app/app/package.json new file mode 100644 index 000000000..5796c517b --- /dev/null +++ b/tools/tvos-review/app/app/package.json @@ -0,0 +1 @@ +{"main":"app.js"} diff --git a/tools/tvos-review/app/app/review-socket.js b/tools/tvos-review/app/app/review-socket.js new file mode 100644 index 000000000..0418dd396 --- /dev/null +++ b/tools/tvos-review/app/app/review-socket.js @@ -0,0 +1,69 @@ +// The reviewer only needs text messages. Use Foundation directly because the +// default WebSocket plugin's published XCFramework has no tvOS slice. +const Delegate = NSObject.extend({ + URLSessionWebSocketTaskDidOpenWithProtocol(session, task, protocol) { + const socket = this.socket; + if (socket.readyState !== 0) return; + socket.readyState = 1; + socket.onopen?.(); + socket.receive(); + }, + URLSessionWebSocketTaskDidCloseWithCloseCodeReason(session, task, code, reason) { + this.socket.closed(); + }, + URLSessionTaskDidCompleteWithError(session, task, error) { + const socket = this.socket; + if (error && socket.readyState !== 3 && !socket.finishedNormally()) socket.onerror?.({ message: error.localizedDescription }); + socket.closed(); + }, +}, { protocols: [NSURLSessionWebSocketDelegate] }); + +export function createReviewSocket(url) { + const socket = { + readyState: 0, + onopen: null, + onmessage: null, + onerror: null, + onclose: null, + finishedNormally() { + return Number(this.task.valueForKey('closeCode')) === 1000; + }, + receive() { + if (this.readyState !== 1) return; + NSURLSessionWebSocketTask.prototype.receiveMessageWithCompletionHandler.call(this.task, (message, error) => { + if (this.readyState !== 1) return; + if (error) { + if (this.finishedNormally()) { this.closed(); return; } + this.onerror?.({ message: error.localizedDescription }); + this.close(); + return; + } + this.onmessage?.({ data: message.string }); + this.receive(); + }); + }, + send(text) { + if (this.readyState !== 1) throw new Error('Review socket is not open'); + NSURLSessionWebSocketTask.prototype.sendMessageCompletionHandler.call(this.task, NSURLSessionWebSocketMessage.alloc().initWithString(text), error => { + if (error && this.readyState === 1) this.onerror?.({ message: error.localizedDescription }); + }); + }, + closed() { + if (this.readyState === 3) return; + this.readyState = 3; + this.session.invalidateAndCancel(); + this.onclose?.(); + }, + close() { + NSURLSessionWebSocketTask.prototype.cancelWithCloseCodeReason.call(this.task, 1000, null); + this.closed(); + }, + }; + socket.delegate = Delegate.new(); + socket.delegate.socket = socket; + socket.session = NSURLSession.sessionWithConfigurationDelegateDelegateQueue( + NSURLSessionConfiguration.defaultSessionConfiguration, socket.delegate, NSOperationQueue.mainQueue); + socket.task = socket.session.webSocketTaskWithURL(NSURL.URLWithString(url)); + socket.task.resume(); + return socket; +} diff --git a/tools/tvos-review/app/app/review.spec.js b/tools/tvos-review/app/app/review.spec.js new file mode 100644 index 000000000..fdb8c2d82 --- /dev/null +++ b/tools/tvos-review/app/app/review.spec.js @@ -0,0 +1,64 @@ +import { describe, it, expect } from 'vitest'; +import { Device, isTvOS, knownFolders } from '@nativescript/core'; +import { Canvas, ImageData, GPU, GPUBufferUsage, GPUMapMode } from '@nativescript/canvas'; +import { mount } from '@nativescript/unit-test-runner/testing'; + +import { checkImageDataOwnership } from './imagedata-ownership'; + +async function surface(type) { + const canvas = new Canvas(); + canvas.width = 160; + canvas.height = 120; + const ready = new Promise(resolve => canvas.once('ready', resolve)); + await mount(canvas); + await ready; + return canvas.getContext(type); +} + +describe('tvOS integration', () => { + it('reports tvOS and writes to the cache', () => { + expect(isTvOS).toBe(true); + expect(Device.os).toBe('tvOS'); + const file = knownFolders.temp().getFile('review-write.txt'); + file.writeTextSync('tvOS'); + expect(file.readTextSync()).toBe('tvOS'); + }); + + it('round-trips Canvas pixels and Unicode text metrics', async () => { + const context = await surface('2d'); + context.fillStyle = '#00e5ff'; + context.fillRect(0, 0, 100, 100); + expect(Array.from(context.getImageData(10, 10, 1, 1).data)).toEqual([0, 229, 255, 255]); + context.font = '24px sans-serif'; + expect(context.measureText('Canvas é 🕹').width).toBeGreaterThan(0); + context.fillText('Canvas é 🕹', 5, 30); + }); + + it('keeps ImageData pixel views alive across garbage collection', async () => { + expect(await checkImageDataOwnership(ImageData)).toEqual({ allocations: 4096, retainedViews: 64 }); + }); + + it('uses WebGL typed buffers and reads a cleared pixel', async () => { + const gl = await surface('webgl'); + const buffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 1, 2, 3]), gl.STATIC_DRAW); + gl.bufferSubData(gl.ARRAY_BUFFER, 0, new Float32Array([4, 5])); + expect(gl.getBufferParameter(gl.ARRAY_BUFFER, gl.BUFFER_SIZE)).toBe(16); + gl.clearColor(1, 0, 0, 1); + gl.clear(gl.COLOR_BUFFER_BIT); + const pixel = new Uint8Array(4); + gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel); + expect(Array.from(pixel)).toEqual([255, 0, 0, 255]); + expect(gl.getError()).toBe(gl.NO_ERROR); + gl.deleteBuffer(buffer); + }); + + it('destroys WebGPU query resources', async () => { + const adapter = await new GPU().requestAdapter(); + const device = await adapter.requestDevice(); + const query = device.createQuerySet({ type: 'occlusion', count: 1 }); + query.destroy(); + device.destroy(); + }); +}); diff --git a/tools/tvos-review/app/app/test.js b/tools/tvos-review/app/app/test.js new file mode 100644 index 000000000..66307c4ad --- /dev/null +++ b/tools/tvos-review/app/app/test.js @@ -0,0 +1,7 @@ +import { createReviewSocket } from './review-socket'; +import { Application } from '@nativescript/core'; +import { NativeScriptVitestCoordinator, createWebpackTestRegistry } from '@nativescript/unit-test-runner/runtime'; +import { createVitestHostPage } from '@nativescript/unit-test-runner/testing'; +const coordinator = new NativeScriptVitestCoordinator({ createSocket: createReviewSocket, registry: createWebpackTestRegistry(require.context('./', true, /\.spec\.js$/)) }); +void coordinator.start(); +Application.run({ create: () => createVitestHostPage(coordinator) }); diff --git a/tools/tvos-review/app/nativescript.config.ts b/tools/tvos-review/app/nativescript.config.ts new file mode 100644 index 000000000..64c7923fd --- /dev/null +++ b/tools/tvos-review/app/nativescript.config.ts @@ -0,0 +1,11 @@ +export default { + id: 'org.nativescript.tvosreview', + projectName: 'TVOSReview', + appPath: 'app', + main: 'app/app.js', + appResourcesPath: 'App_Resources', + tvos: { + discardUncaughtJsExceptions: false, + SPMPackages: [{ name: 'FontManager', libs: ['FontManager'], path: '../font-manager/packages/font-manager/src-native/ios/FontManager' }], + }, +}; diff --git a/tools/tvos-review/app/tsconfig.json b/tools/tvos-review/app/tsconfig.json new file mode 100644 index 000000000..f25f7bb60 --- /dev/null +++ b/tools/tvos-review/app/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "node", + "allowJs": true, + "skipLibCheck": true + }, + "include": ["app/**/*.js"] +} diff --git a/tools/tvos-review/app/vitest.config.mts b/tools/tvos-review/app/vitest.config.mts new file mode 100644 index 000000000..641970b1c --- /dev/null +++ b/tools/tvos-review/app/vitest.config.mts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vitest/config'; +import { nativeScript } from '@nativescript/unit-test-runner'; +import path from 'node:path'; +const device = process.env.NS_DEVICE; +export default defineConfig({ + plugins: [nativeScript({ + platform: process.env.NS_PLATFORM || 'tvos', + device, + connectTimeout: 300000, + launchCommand: { + command: process.execPath, + args: [path.resolve('../cli/bin/tns'), 'run', 'tvos', '--no-local-cli', '--no-hmr', '--env.unitTesting', '--env.testRunnerPort=17878', ...(device ? ['--device', device] : [])], + }, + })], + test: { testTimeout: 60000 }, +}); diff --git a/tools/tvos-review/app/webpack.config.js b/tools/tvos-review/app/webpack.config.js new file mode 100644 index 000000000..63e90a901 --- /dev/null +++ b/tools/tvos-review/app/webpack.config.js @@ -0,0 +1,5 @@ +const webpack = require('@nativescript/webpack'); +module.exports = (env) => { + webpack.init(env); + return webpack.resolveConfig(); +}; diff --git a/tools/tvos-review/audit-binaries.py b/tools/tvos-review/audit-binaries.py new file mode 100644 index 000000000..487cae781 --- /dev/null +++ b/tools/tvos-review/audit-binaries.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""Reject mislabeled or too-new tvOS Mach-O binaries in a framework/app tree.""" +import argparse +import json +from pathlib import Path +import re +import subprocess +import sys + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('tree', type=Path) +parser.add_argument('--platform', choices=['TVOS', 'TVOSSIMULATOR'], required=True) +parser.add_argument('--minimum', help='Maximum minimum OS version; defaults to 13.0 device / 14.0 simulator') +args = parser.parse_args() +args.minimum = args.minimum or ('14.0' if args.platform == 'TVOSSIMULATOR' else '13.0') +if not args.tree.is_dir(): + parser.error(f'Directory does not exist: {args.tree}') +def version(text): + return tuple(int(n) for n in (text.split('.') + ['0', '0'])[:3]) +records = [] +for path in sorted(args.tree.rglob('*')): + if not path.is_file() or path.is_symlink(): + continue + with path.open('rb') as file: + magic = file.read(4) + if magic not in [b'\xcf\xfa\xed\xfe', b'\xfe\xed\xfa\xcf', b'\xca\xfe\xba\xbe', b'\xbe\xba\xfe\xca']: + continue + output = subprocess.check_output(['xcrun', 'vtool', '-show-build', path], text=True) + platforms = re.findall(r'^\s+platform (\S+)', output, re.M) + minima = re.findall(r'^\s+minos (\S+)', output, re.M) + valid = bool(platforms and minima) and all(p == args.platform for p in platforms) and all(version(v) <= version(args.minimum) for v in minima) + records.append({'file': str(path.relative_to(args.tree)), 'platforms': platforms, 'minimumOS': minima, 'passed': valid}) +print(json.dumps(records, indent=2)) +sys.exit(not records or any(not record['passed'] for record in records)) diff --git a/tools/tvos-review/canvas-extras.json b/tools/tvos-review/canvas-extras.json new file mode 100644 index 000000000..912cc8447 --- /dev/null +++ b/tools/tvos-review/canvas-extras.json @@ -0,0 +1,7 @@ +[ + "cfe0b9de68ee97c3b51cc59cd530f9c2455af664", + "b7eab94b94f931ac86dade7f0a84afdd020c5143", + "ed84b43758e24751f2bc60a9a9a888ed944a1776", + "1bd9e03474e53ecf45d3f38e05db3847f7439b48", + "7c04d5aee23f72ffaebcb696b3524cd4a8f19015" +] diff --git a/tools/tvos-review/helpers.py b/tools/tvos-review/helpers.py new file mode 100644 index 000000000..9279e4d90 --- /dev/null +++ b/tools/tvos-review/helpers.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +import pathlib +import subprocess +import sys +work = pathlib.Path(sys.argv[1]).resolve() +if not (work / '.tvos-review-workspace').is_file(): + raise RuntimeError('Use review.py to initialize an isolated reviewer workspace first.') +artifacts = work / 'artifacts' +artifacts.mkdir(exist_ok=True) +for relative, name in [('packages/ui-mobile-base/ios/TNSWidgets', 'TNSWidgets'), ('packages/winter-tc/ios/NSCWinterTC', 'NSCWinterTC')]: + directory = work / 'core' / relative + output = work / 'build' / name + for sdk, platform in [('appletvos', 'tvOS'), ('appletvsimulator', 'tvOS Simulator')]: + subprocess.run(['xcodebuild', '-project', str(directory / (name + '.xcodeproj')), '-scheme', name, '-sdk', sdk, '-configuration', 'Release', '-destination', 'generic/platform=' + platform, 'build', 'BUILD_DIR=' + str(output), 'SKIP_INSTALL=NO', 'BUILD_LIBRARY_FOR_DISTRIBUTION=YES', 'CODE_SIGNING_ALLOWED=NO', '-quiet'], check=True) + subprocess.run(['xcodebuild', '-create-xcframework', '-framework', str(output / ('Release-appletvos/' + name + '.framework')), '-framework', str(output / ('Release-appletvsimulator/' + name + '.framework')), '-output', str(artifacts / (name + '.xcframework'))], check=True) diff --git a/tools/tvos-review/review.py b/tools/tvos-review/review.py new file mode 100644 index 000000000..8f7b4cfd8 --- /dev/null +++ b/tools/tvos-review/review.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +"""Build the tvOS review stack in an isolated, revision-pinned workspace.""" +import argparse +import json +import os +import plistlib +from pathlib import Path +import shutil +import subprocess +import sys + +HERE = Path(__file__).resolve().parent +RUNTIME = HERE.parents[1] +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('step', choices=['doctor', 'checkout', 'runtime', 'packages', 'canvas', 'helpers', 'app', 'test', 'all']) +parser.add_argument('--workspace', type=Path, required=True) +parser.add_argument('--simulator', help='Apple TV simulator UDID, required for test') +parser.add_argument('--jobs', type=int, default=6) +args = parser.parse_args() +work = args.workspace.expanduser().resolve() +if work == RUNTIME or RUNTIME in work.parents: + parser.error('Use a workspace outside the runtime checkout.') +work.mkdir(parents=True, exist_ok=True) +marker = work / '.tvos-review-workspace' +if not marker.exists() and any(work.iterdir()): + parser.error('The initial workspace must be empty.') +marker.touch() +logs = work / 'logs' +logs.mkdir(exist_ok=True) +env = dict(os.environ, NX_DAEMON='false', NX_NO_CLOUD='true', DEPOT_TOOLS_UPDATE='0', CARGO_BUILD_JOBS=str(args.jobs)) +env['PATH'] = str(work / 'host-tooling/node_modules/.bin') + ':' + str(work / 'depot_tools/.cipd_bin') + ':' + str(work / 'depot_tools') + ':' + str(Path.home() / '.cargo/bin') + ':' + env['PATH'] +if not env.get('SSL_CERT_FILE'): + try: + import certifi + env['SSL_CERT_FILE'] = certifi.where() + except ImportError: + pass + +def run(label, cmd, cwd=work, extra=None): + print(label, flush=True) + with (logs / (label + '.log')).open('w') as log: + result = subprocess.run(list(map(str, cmd)), cwd=cwd, env=dict(env, **(extra or {})), stdout=log, stderr=subprocess.STDOUT) + if result.returncode: + raise RuntimeError(f'{label} failed ({result.returncode}); see {log.name}') + +def doctor(): + if sys.platform != 'darwin': + raise RuntimeError('The native build requires macOS.') + for command in ['git', 'node', 'npm', 'python3', 'swift', 'xcodebuild', 'rustup', 'cargo', 'cbindgen', 'pod', 'cmake', 'jq']: + if not shutil.which(command, path=env['PATH']): + raise RuntimeError(f'Missing prerequisite: {command}. See README.md.') + for sdk in ['iphoneos', 'iphonesimulator', 'appletvos', 'appletvsimulator']: + run('sdk-' + sdk, ['xcrun', '--sdk', sdk, '--show-sdk-version']) + run('node-version', ['node', '--version']) + run('pods-version', ['pod', '--version']) + run('xcode-version', ['xcodebuild', '-version']) + print('Prerequisites found. Physical signing is configured separately.', flush=True) + +def checkout(): + manifest = json.loads((HERE / 'revisions.json').read_text()) + # The runtime revision is exactly the checkout providing this script. + manifest['runtime'] = {'url': 'https://github.com/LorenzGit/ios.git', 'rev': subprocess.check_output(['git', '-C', str(RUNTIME), 'rev-parse', 'HEAD'], text=True).strip()} + for name, spec in manifest.items(): + dest = work / name + created = not dest.exists() + if created: + run('clone-' + name, ['git', 'clone', '--filter=blob:none', '--no-checkout', spec['url'], dest]) + current = subprocess.run(['git', '-C', str(dest), 'status', '--porcelain'], capture_output=True, text=True) + if not created and current.stdout.strip(): + raise RuntimeError(f'{dest} has local changes; use a new workspace instead of overwriting them') + run('fetch-' + name, ['git', 'fetch', '--no-tags', 'origin', spec['rev']], dest) + run('checkout-' + name, ['git', 'checkout', '--detach', spec['rev']], dest) + run('depot-bootstrap', [work / 'depot_tools/ensure_bootstrap']) + run('depot-gclient', ['gclient', '--version']) + run('depot-ninja', ['ninja', '--version']) + (work / 'resolved-revisions.json').write_text(json.dumps(manifest, indent=2) + '\n') + for revision in json.loads((HERE / 'canvas-extras.json').read_text()): + run('fetch-canvas-' + revision[:8], ['git', 'fetch', 'origin', revision], work / 'canvas') + run('apply-canvas-' + revision[:8], ['git', 'cherry-pick', revision], work / 'canvas') + cargo = work / 'canvas/Cargo.toml' + skia = manifest['rust-skia'] + cargo.write_text(cargo.read_text() + '\n[patch."https://github.com/triniwiz/rust-skia"]\n' + '\n'.join(f'{name} = {{ git = "{skia["url"].removesuffix(".git")}", rev = "{skia["rev"]}" }}' for name in ['skia-safe', 'skia-bindings']) + '\n') + +def runtime(): + v8 = work / 'v8-buildscripts' + run('v8-fetch', ['scripts/matrix/fetch.sh', '--platform', 'ios', '--no-history'], v8) + for variant in ['arm64-tvdevice', 'arm64-tvsimulator']: + run('v8-' + variant, ['scripts/matrix/build-ios.sh', '--variant', variant, '--', '-j' + str(args.jobs)], v8) + run('runtime-install', ['npm', 'install', '--ignore-scripts'], work / 'runtime') + run('runtime-build', ['./build_all_tvos.sh'], work / 'runtime', {'V8_TVOS_BUILD': str(v8)}) + for name in ['NativeScript', 'TKLiveSync']: + framework = work / 'runtime/dist' / (name + '.xcframework') + for library in plistlib.loads((framework / 'Info.plist').read_bytes())['AvailableLibraries']: + identifier = library['LibraryIdentifier'] + platform = 'TVOSSIMULATOR' if library.get('SupportedPlatformVariant') == 'simulator' else 'TVOS' + run('audit-' + name + '-' + identifier, [sys.executable, HERE / 'audit-binaries.py', framework / identifier, '--platform', platform]) + +def host_npm(): + # CLI host fixtures still exercise npm 10 semantics. App dependency + # resolution separately uses npm 12 (see app.py). + tooling = work / 'host-tooling' + if not (tooling / 'node_modules/.bin/npm').exists(): + run('host-npm', ['npm', 'install', '--prefix', tooling, '--ignore-scripts', '--no-audit', '--no-fund', 'npm@10.9.8']) + +def packages(): + host_npm() + for name in ['core', 'canvas', 'cli', 'simulator', 'test-runner']: + cwd = work / name + run(name + '-install', ['npm', 'ci' if (cwd / 'package-lock.json').exists() else 'install', '--ignore-scripts'], cwd) + run('core-ts-patch', ['npx', 'ts-patch', 'install'], work / 'core') + run('core-build', ['npx', 'nx', 'run-many', '--target=build', '--projects=core,webpack5'], work / 'core') + run('canvas-build', ['npx', 'nx', 'run', 'canvas:build.all'], work / 'canvas') + run('test-runner-pack', ['npm', 'pack'], work / 'test-runner') + run('simulator-pack', ['npm', 'pack'], work / 'simulator') + simulator = next((work / 'simulator').glob('ios-sim-portable-*.tgz')) + run('cli-simulator-install', ['npm', 'install', '--no-save', simulator], work / 'cli') + run('cli-build', ['npm', 'run', 'build'], work / 'cli') + +def canvas(): + cwd = work / 'canvas' + run('rust-toolchain', ['rustup', 'toolchain', 'install', 'nightly-2026-09-07', '--profile', 'minimal', '--component', 'rust-src']) + env['CANVAS_RUST_TOOLCHAIN'] = 'nightly-2026-09-07' + env['RUSTFLAGS'] = '-Zlocation-detail=none -Zunstable-options -Cpanic=immediate-abort' + # The Xcode prebuild phase compiles Rust with the selected SDK and toolchain. + native = cwd / 'packages/canvas/src-native/canvas-ios' + for sdk, platform in [('appletvos', 'tvOS'), ('appletvsimulator', 'tvOS Simulator')]: + run('canvas-native-' + sdk, ['xcodebuild', '-project', 'CanvasNative.xcodeproj', '-scheme', 'CanvasNative', '-sdk', sdk, '-destination', 'generic/platform=' + platform, '-configuration', 'Release', 'build', 'BUILD_DIR=' + str(native / 'dist-review'), 'ARCHS=arm64', 'ONLY_ACTIVE_ARCH=NO', 'SKIP_INSTALL=NO', 'BUILD_LIBRARY_FOR_DISTRIBUTION=YES', 'CODE_SIGNING_ALLOWED=NO', '-quiet'], native) + output = work / 'artifacts/CanvasNative.xcframework' + output.parent.mkdir(exist_ok=True) + if output.exists(): + shutil.rmtree(output) + run('canvas-xcframework', ['xcodebuild', '-create-xcframework', '-framework', native / 'dist-review/Release-appletvos/CanvasNative.framework', '-framework', native / 'dist-review/Release-appletvsimulator/CanvasNative.framework', '-output', output]) + +def helpers(): + run('helpers', [sys.executable, HERE / 'helpers.py', work]) + +def app(): + run('app', [sys.executable, HERE / 'app.py', work]) + +def test(): + host_npm() + if not args.simulator: + parser.error('--simulator is required for test') + run('core-tests', ['npx', 'nx', 'run', 'core:test'], work / 'core') + run('template-tests', [sys.executable, work / 'runtime/tests/tvos/template.py'], extra={'TVOS_REVIEW_CLI': str(work / 'cli')}) + run('runner-tests', ['npm', 'test'], work / 'test-runner') + run('webpack-tests', ['npx', 'nx', 'run', 'webpack5:test', '--runInBand'], work / 'core') + run('cli-tests', ['npm', 'test'], work / 'cli') + run('app-tests', ['node', work / 'cli/bin/tns', 'test', 'tvos', '--device', args.simulator, '--no-local-cli'], work / 'app', extra={'PATH': str(work / 'tooling/node_modules/.bin') + ':' + env['PATH']}) + +steps = ['doctor', 'checkout', 'runtime', 'packages', 'canvas', 'helpers', 'app', 'test'] if args.step == 'all' else [args.step] +for step in steps: + globals()[step]() diff --git a/tools/tvos-review/revisions.json b/tools/tvos-review/revisions.json new file mode 100644 index 000000000..40dbc627c --- /dev/null +++ b/tools/tvos-review/revisions.json @@ -0,0 +1,38 @@ +{ + "core": { + "url": "https://github.com/LorenzGit/NativeScript.git", + "rev": "df7776ba8a9a32900b4d1ac7c8718b3eaa3782d7" + }, + "canvas": { + "url": "https://github.com/LorenzGit/canvas.git", + "rev": "1dffcabf119046d7bea2c402150be24f0982ff1a" + }, + "cli": { + "url": "https://github.com/LorenzGit/nativescript-cli.git", + "rev": "56c3a3b0ea8df1ac1e347278ab866a5e7b1cc3b7" + }, + "simulator": { + "url": "https://github.com/LorenzGit/ios-sim-portable.git", + "rev": "5543c967a620450766136597d8b14bab371f1efc" + }, + "font-manager": { + "url": "https://github.com/LorenzGit/font-manager.git", + "rev": "44fdb034a64a91107ac0b8831bc9668e1f823475" + }, + "rust-skia": { + "url": "https://github.com/LorenzGit/rust-skia.git", + "rev": "6dd40c1c5aff99fcc7d0f5f303bda460d6445230" + }, + "v8-buildscripts": { + "url": "https://github.com/LorenzGit/v8-buildscripts.git", + "rev": "36a75935c032f8ce0715e05ef112a42350d8dce1" + }, + "test-runner": { + "url": "https://github.com/LorenzGit/nativescript-unit-test-runner.git", + "rev": "a2934e4cfff0d32d099ee82870e430ecd5cc1b4e" + }, + "depot_tools": { + "url": "https://chromium.googlesource.com/chromium/tools/depot_tools.git", + "rev": "2cea9cdd785391335d448773124631cb711225e2" + } +} diff --git a/v8ios.xcodeproj/project.pbxproj b/v8ios.xcodeproj/project.pbxproj index 7c53cc0fb..fcc879ad5 100644 --- a/v8ios.xcodeproj/project.pbxproj +++ b/v8ios.xcodeproj/project.pbxproj @@ -2440,6 +2440,7 @@ INFOPLIST_FILE = TKLiveSync/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; + TVOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2454,11 +2455,11 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = ""; SKIP_INSTALL = YES; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator appletvos appletvsimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - TARGETED_DEVICE_FAMILY = "1,2,7"; + TARGETED_DEVICE_FAMILY = "1,2,3,7"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; @@ -2478,6 +2479,7 @@ INFOPLIST_FILE = TKLiveSync/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; + TVOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2492,11 +2494,11 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = ""; SKIP_INSTALL = YES; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator appletvos appletvsimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - TARGETED_DEVICE_FAMILY = "1,2,7"; + TARGETED_DEVICE_FAMILY = "1,2,3,7"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; @@ -2610,6 +2612,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; + TVOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -2667,6 +2670,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; + TVOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_CFLAGS = ""; @@ -2817,6 +2821,7 @@ ); INFOPLIST_FILE = TestRunner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; + TVOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2845,11 +2850,11 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = iphoneos; STRIPFLAGS = "-s $(PROJECT_DIR)/TestFixtures/exported-symbols.txt"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator appletvos appletvsimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - TARGETED_DEVICE_FAMILY = "1,7"; + TARGETED_DEVICE_FAMILY = "1,3,7"; USER_HEADER_SEARCH_PATHS = ""; }; name = Debug; @@ -2873,6 +2878,7 @@ ); INFOPLIST_FILE = TestRunner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; + TVOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2901,11 +2907,11 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = iphoneos; STRIPFLAGS = "-s $(PROJECT_DIR)/TestFixtures/exported-symbols.txt"; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator appletvos appletvsimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - TARGETED_DEVICE_FAMILY = "1,7"; + TARGETED_DEVICE_FAMILY = "1,3,7"; USER_HEADER_SEARCH_PATHS = ""; }; name = Release; @@ -2926,6 +2932,8 @@ EXCLUDED_ARCHS = ""; "EXCLUDED_ARCHS[sdk=xros*]" = x86_64; "EXCLUDED_ARCHS[sdk=xrsimulator*]" = x86_64; + "EXCLUDED_ARCHS[sdk=appletvsimulator*]" = x86_64; + TVOS_DEPLOYMENT_TARGET = 13.0; GCC_C_LANGUAGE_STANDARD = c11; GCC_PREFIX_HEADER = "NativeScript/NativeScript-Prefix.pch"; GCC_PREPROCESSOR_DEFINITIONS = "DEBUG=1"; @@ -2948,6 +2956,7 @@ "-Wall", "-Werror", "-Wno-documentation", + "-Wno-error=deprecated-declarations", "-fembed-bitcode", ); OTHER_LDFLAGS = ( @@ -2990,11 +2999,11 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = ""; SKIP_INSTALL = YES; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator appletvos appletvsimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - TARGETED_DEVICE_FAMILY = "1,2,7"; + TARGETED_DEVICE_FAMILY = "1,2,3,7"; USER_HEADER_SEARCH_PATHS = ( "$(SRCROOT)/NativeScript", "$(SRCROOT)/NativeScript/include", @@ -3026,6 +3035,8 @@ EXCLUDED_ARCHS = ""; "EXCLUDED_ARCHS[sdk=xros*]" = x86_64; "EXCLUDED_ARCHS[sdk=xrsimulator*]" = x86_64; + "EXCLUDED_ARCHS[sdk=appletvsimulator*]" = x86_64; + TVOS_DEPLOYMENT_TARGET = 13.0; GCC_C_LANGUAGE_STANDARD = c11; GCC_PREFIX_HEADER = "NativeScript/NativeScript-Prefix.pch"; GCC_TREAT_WARNINGS_AS_ERRORS = NO; @@ -3046,6 +3057,7 @@ "-Wall", "-Werror", "-Wno-documentation", + "-Wno-error=deprecated-declarations", "-fembed-bitcode", ); OTHER_LDFLAGS = ( @@ -3088,11 +3100,11 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = ""; SKIP_INSTALL = YES; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator appletvos appletvsimulator"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - TARGETED_DEVICE_FAMILY = "1,2,7"; + TARGETED_DEVICE_FAMILY = "1,2,3,7"; USER_HEADER_SEARCH_PATHS = ( "$(SRCROOT)/NativeScript", "$(SRCROOT)/NativeScript/include",