diff --git a/.github/actions/setup-android-native/action.yml b/.github/actions/setup-android-native/action.yml index 8b0c980d0..8af69d1be 100644 --- a/.github/actions/setup-android-native/action.yml +++ b/.github/actions/setup-android-native/action.yml @@ -8,7 +8,7 @@ inputs: ndk-version: description: NDK version to install via sdkmanager (must match ndkVersion in canvas build.gradle and the default in tools/scripts/build-android.sh) required: false - default: '27.3.13750724' + default: '29.0.14206865' api-level: description: Android API level used for the NDK clang wrappers required: false diff --git a/.github/workflows/build-native.yml b/.github/workflows/build-native.yml index 4d11aff56..f5747b105 100644 --- a/.github/workflows/build-native.yml +++ b/.github/workflows/build-native.yml @@ -14,7 +14,7 @@ # preserve file permissions. # # The canvas build mirrors the local flow: -# iOS: make ios && make visionos, then tools/scripts/canvas-build.sh +# iOS: make ios && make visionos && make tvos, then tools/scripts/canvas-build.sh # (what `npm run build.canvas.ios.framework.release` runs, minus nx) # Android: make android, then gradle :canvas:assembleRelease and copy the aar # @@ -49,8 +49,12 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ inputs.ref }} + ref: ${{ github.event_name == 'workflow_dispatch' && github.event.repository.default_branch || inputs.ref }} - uses: ./.github/actions/setup-android-native + - name: Fetch V8 + NativeScript runtime headers + run: ./tools/scripts/download-v8.sh + - name: Build libNativeScript.so stub for linking + run: ./tools/scripts/build-android-v8-stub.sh - name: Build Rust libraries run: make android - name: Build canvas AAR @@ -65,17 +69,23 @@ jobs: if-no-files-found: error canvas-ios: - name: Canvas iOS + visionOS + name: Canvas iOS + visionOS + tvOS runs-on: macos-15 steps: - uses: actions/checkout@v4 with: - ref: ${{ inputs.ref }} + ref: ${{ github.event_name == 'workflow_dispatch' && github.event.repository.default_branch || inputs.ref }} - uses: ./.github/actions/setup-apple-native + - name: Fetch V8 + NativeScript runtime headers + run: ./tools/scripts/download-v8.sh - name: Build Rust libraries (iOS) run: make ios - name: Build Rust libraries (visionOS) run: make visionos + # canvas-build.sh requires the tvOS static libs whenever the runner has a + # usable tvOS destination, which the macOS images do. + - name: Build Rust libraries (tvOS) + run: make tvos # same as `npm run build.canvas.ios.framework.release`, without needing node_modules - name: Build CanvasNative.xcframework working-directory: tools/scripts @@ -190,7 +200,7 @@ jobs: if-no-files-found: error audio-context-ios: - name: AudioContext iOS + visionOS + name: AudioContext iOS + visionOS + tvOS runs-on: macos-15 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/npm_release.yml b/.github/workflows/npm_release.yml index be36dcb0c..5bf2af92c 100644 --- a/.github/workflows/npm_release.yml +++ b/.github/workflows/npm_release.yml @@ -101,6 +101,15 @@ jobs: - name: Install dependencies run: npm install + # The V8 + NativeScript runtime headers under nativescript-v8/Headers are + # fetched, not committed (see .gitignore + tools/scripts/download-v8.sh), + # and the build copies that tree into each package's NativeScriptV8 + # SwiftPM shim. Without this step a fresh checkout ships a shim whose + # Headers/include holds only the tracked libffi files, and every + # consumer's iOS build dies on Common.h's `#include "include/v8.h"`. + - name: Vendor V8 + NativeScript runtime headers + run: ./tools/scripts/download-v8.sh + - name: Build packages run: npx nx run-many --target=build.all --projects="$(echo "$PACKAGES" | tr ' ' ',')" @@ -114,12 +123,21 @@ jobs: set -euo pipefail for pkg in canvas audio-context; do dir="dist/packages/$pkg/platforms/ios/NativeScriptV8" - for required in "$dir/Package.swift" "$dir/Headers" "$dir/Sources"; do - if [ ! -e "$required" ]; then - echo "error: $required missing — the NativeScriptV8 SPM shim was not packaged" >&2 + # Check real files, not just the directories: Headers/ also holds + # the committed inspector/ + libffi trees, so an `-e Headers` test + # passes even when download-v8.sh never ran and the actual V8 + # headers are absent (that shipped in 3.0.0-alpha.0). + for required in "$dir/Package.swift" "$dir/Sources/NativeScriptV8.c" \ + "$dir/Headers/include/v8.h" "$dir/Headers/include/v8config.h"; do + if [ ! -f "$required" ]; then + echo "error: $required missing — the NativeScriptV8 SPM shim was not packaged (did tools/scripts/download-v8.sh run?)" >&2 exit 1 fi done + if [ -z "$(ls -A "$dir/Headers/runtime" 2>/dev/null)" ]; then + echo "error: $dir/Headers/runtime is empty — the NativeScript runtime headers were not vendored" >&2 + exit 1 + fi # match only a real `repositoryURL:` key — the config's comments # legitimately mention the word while explaining why it's banned if grep -v -E '^[[:space:]]*(//|\*)' "dist/packages/$pkg/nativescript.config.ts" | grep -q -E 'repositoryURL[[:space:]]*:'; then diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index ac63c1e28..9af9b55c5 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -7,6 +7,9 @@ # prerelease of each package. name: Canvas Native +permissions: + contents: read + on: push: branches: @@ -15,6 +18,41 @@ on: workflow_dispatch: jobs: + # Fast, dependency-free guards. These run first because they catch the class + # of regression that merges introduce silently -- see + # tools/tests/check-v8-bridge-invariants.py. + static-checks: + name: Static Checks + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: V8 bridge invariants + run: python3 tools/tests/check-v8-bridge-invariants.py + + unit-tests: + name: Unit Tests + runs-on: macos-15 + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-test-${{ hashFiles('Cargo.toml', 'crates/*/Cargo.toml') }} + restore-keys: ${{ runner.os }}-cargo-test- + # canvas-c needs 2d (skia) and webgl to compile; gl pulls the WebGL utils + # the 2d and webgpu paths import. + # + # .cargo/config.toml adds `-C panic=abort` on the Apple host targets for + # the macOS dylib build, and libtest cannot link against that, so the + # rustflags are overridden for this run only. + - name: Rust unit tests + env: + RUSTFLAGS: '-C link-arg=-undefined -C link-arg=dynamic_lookup' + run: cargo test -p canvas-c --features 2d,webgl,gl + build-native: name: Build Native uses: ./.github/workflows/build-native.yml diff --git a/.gitignore b/.gitignore index 9f8d9b79c..c1362a9f4 100644 --- a/.gitignore +++ b/.gitignore @@ -53,4 +53,31 @@ rust-skia **/.fingerprint .build .nx/cache -.nx/workspace-data \ No newline at end of file +.nx/workspace-data + +# V8 + NativeScript runtime headers - fetched by tools/scripts/download-v8.sh +# (pinned by /V8_RELEASE and /NATIVESCRIPT_IOS_RUNTIME_REF), not committed. +# The ignore rules for nativescript-v8/Headers live in a NESTED .gitignore +# (nativescript-v8/Headers/.gitignore) instead of here: Nx's asset-copy +# handler (@nx/js CopyAssetsHandler, used by packages/canvas and +# packages/audio-context's build target to ship these headers into +# dist/.../NativeScriptV8/Headers) reads and applies the ROOT .gitignore to +# filter which asset-glob matches get copied, but does not read nested +# .gitignore files. Root-level ignore rules here silently dropped the fetched +# v8.h/v8config.h/runtime/*.h from the built npm packages even though +# download-v8.sh had vendored them - see the fix for that incident. A nested +# .gitignore keeps git ignoring them for commits while staying invisible to +# the asset copier. +/.v8-prebuilt/ +/packages/canvas/src-native/canvas-android/canvas/src/main/cpp/include/* +!/packages/canvas/src-native/canvas-android/canvas/src/main/cpp/include/canvas_native.h +!/packages/canvas/src-native/canvas-android/canvas/src/main/cpp/include/canvas_android.h +!/packages/canvas/src-native/canvas-android/canvas/src/main/cpp/include/command_log.txt +!/packages/canvas/src-native/canvas-android/canvas/src/main/cpp/include/robin_hood.h +!/packages/canvas/src-native/canvas-android/canvas/src/main/cpp/include/zip.h +!/packages/canvas/src-native/canvas-android/canvas/src/main/cpp/include/zipconf.h + +# Build-time-only libNativeScript.so stub, built from the pinned V8 release by +# tools/scripts/build-android-v8-stub.sh (see /V8_RELEASE) -- not committed, +# regenerated on demand. Real apps supply their own copy at runtime. +/packages/canvas/src-native/canvas-android/canvas/src/main/jniLibs/*/libNativeScript.so \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..1c3ea6183 --- /dev/null +++ b/.npmrc @@ -0,0 +1,11 @@ +# react-nativescript and svelte-native are demo-only devDependencies whose +# published peer ranges still cap @nativescript/core at 8.x (react-nativescript +# 5.0.0 -> ^8.3.5, svelte-native 1.0.29 -> ^8.5.9); no release of either +# supports core 9 yet. Since the root bumped to @nativescript/core ~9.1.0, +# npm's strict peer resolution fails `npm install` outright (ERESOLVE), which +# is what broke the npm Release + pull-request workflows. +# +# Nothing in this repo consumes those peers at runtime — the demos are built +# per-app by nx — so accept the mismatch here instead of pinning core back. +# Drop this once both packages publish a core 9 compatible release. +legacy-peer-deps=true diff --git a/Cargo.toml b/Cargo.toml index 75d995aa7..b1a0ab052 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,7 @@ opt-level = "z" [workspace.dependencies.wgt] package = "wgpu-types" git = "https://github.com/triniwiz/wgpu" -rev = "7e0f39f" +rev = "e5c0aa02" @@ -74,21 +74,21 @@ canvas-webgl = { path = "./crates/canvas-webgl" } canvas-svg = { path = "./crates/canvas-svg" } gl-bindings = { path = "./crates/gl-bindings" } canvas-c = { path = "./crates/canvas-c" } -skia-safe = { git = "https://github.com/triniwiz/rust-skia", rev = "d4c4011", features = ["textlayout"] } +skia-safe = { git = "https://github.com/triniwiz/rust-skia", rev = "1eafe30", features = ["textlayout"] } itertools = "0.14.0" ustr = "1.1.0" -wgpu-core = { git = "https://github.com/triniwiz/wgpu", rev = "7e0f39f", features = ["wgsl"] } -wgpu-hal = { git = "https://github.com/triniwiz/wgpu", rev = "7e0f39f" } +wgpu-core = { git = "https://github.com/triniwiz/wgpu", rev = "e5c0aa02", features = ["wgsl"] } +wgpu-hal = { git = "https://github.com/triniwiz/wgpu", rev = "e5c0aa02" } ureq = "2.10.1" jni = "0.21.1" regex-lite = "0.1.9" objc2 = { version = "0.6.3", default-features = false } objc2-core-foundation = { version = "0.3.2", default-features = false } -objc2-metal = { version = "0.3.2", default-features = false, features = ["MTLCommandBuffer", "MTLCommandQueue", "MTLDrawable", "MTLPixelFormat", "MTLTexture", "MTLDevice", "objc2-core-foundation"] } +objc2-metal = { version = "0.3.2", default-features = false, features = ["MTLCommandBuffer", "MTLCommandQueue", "MTLDrawable", "MTLPixelFormat", "MTLTexture", "MTLDevice", "MTLBlitCommandEncoder", "MTLCommandEncoder", "MTLBuffer", "MTLResource", "MTLTypes", "objc2-core-foundation"] } objc2-foundation = { version = "0.3.2", features = ["NSGeometry", "NSData", "NSAutoreleasePool", "NSDictionary", "NSObject", "NSValue", "NSString"] } objc2-quartz-core = { version = "0.3.2", default-features = false, features = ["std", "objc2-core-foundation", "CALayer", "CAMetalLayer", "objc2-metal"] } -core-foundation = "0.10.1" \ No newline at end of file +core-foundation = "0.10.1" diff --git a/Makefile b/Makefile index 0aac4b33f..66decc7cb 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ ARCHS_ANDROID = i686-linux-android x86_64-linux-android aarch64-linux-android ar XCFRAMEWORK = CanvasNative.xcframework RUST_LIB = canvasnative -all: GENERATE_HEADERS ios android +all: GENERATE_HEADERS GENERATE_V8_HEADERS ios android ios: $(XCFRAMEWORK) @@ -20,14 +20,33 @@ ios-svg: GENERATE_IOS_SVG visionos-svg: GENERATE_VISIONOS_SVG -svg: GENERATE_IOS_SVG GENERATE_VISIONOS_SVG +tvos-svg: GENERATE_TVOS_SVG + +svg: GENERATE_IOS_SVG GENERATE_VISIONOS_SVG GENERATE_TVOS_SVG android-svg: GENERATE_ANDROID_SVG +# Host-side tests. The workspace pins `-C panic=abort` for the Apple host +# targets in .cargo/config.toml (needed by the macOS dylib build) and libtest +# cannot link against that, so the rustflags are replaced for this run. +.PHONY: test +test: + python3 ./tools/tests/check-v8-bridge-invariants.py + RUSTFLAGS="-C link-arg=-undefined -C link-arg=dynamic_lookup" \ + cargo test -p canvas-c --features 2d,webgl,gl + .PHONY: GENERATE_HEADERS GENERATE_HEADERS: ./tools/scripts/build-headers.sh +.PHONY: GENERATE_V8_HEADERS +GENERATE_V8_HEADERS: + ./tools/scripts/download-v8.sh + +.PHONY: GENERATE_ANDROID_V8_STUB +GENERATE_ANDROID_V8_STUB: + ./tools/scripts/build-android-v8-stub.sh + .PHONY: $(ARCHS_IOS) $(ARCHS_IOS): %: RUSTFLAGS="-Zlocation-detail=none -Zunstable-options -Cpanic=immediate-abort" \ @@ -42,8 +61,13 @@ $(ARCHS_VISIONOS): %: cargo +nightly build -Z build-std='std,panic_abort' \ --target $@ --release -p canvas-ios +# TVOS_DEPLOYMENT_TARGET must be set explicitly: rustc defaults to 12.0 but the +# `cc` crate has no built-in tvOS default and falls back to the SDK version, so +# ring's C/asm objects come out tagged minos 26.4 and the framework link (12.0) +# warns on every one of them. iOS/visionOS need no equivalent -- cc knows those. .PHONY: $(ARCHS_TVOS) $(ARCHS_TVOS): %: + TVOS_DEPLOYMENT_TARGET=12.0 \ RUSTFLAGS="-Zlocation-detail=none -Zunstable-options -Cpanic=immediate-abort" \ cargo +nightly build -Z build-std='std,panic_abort' \ --target $@ --release -p canvas-ios @@ -73,7 +97,17 @@ $(addsuffix _svg,$(ARCHS_VISIONOS)): %_svg: .PHONY: GENERATE_VISIONOS_SVG GENERATE_VISIONOS_SVG: $(addsuffix _svg,$(ARCHS_VISIONOS)) -.PHONY: ios-svg visionos-svg svg +.PHONY: $(addsuffix _svg,$(ARCHS_TVOS)) +$(addsuffix _svg,$(ARCHS_TVOS)): %_svg: + TVOS_DEPLOYMENT_TARGET=12.0 \ + RUSTFLAGS="-Zlocation-detail=none -Zunstable-options -Cpanic=immediate-abort" \ + cargo +nightly build -Z build-std='std,panic_abort' \ + --target $* --release -p canvas-svg-ios + +.PHONY: GENERATE_TVOS_SVG +GENERATE_TVOS_SVG: $(addsuffix _svg,$(ARCHS_TVOS)) + +.PHONY: ios-svg visionos-svg tvos-svg svg .PHONY: $(addsuffix _svg,$(ARCHS_ANDROID)) $(addsuffix _svg,$(ARCHS_ANDROID)): %_svg: diff --git a/NATIVESCRIPT_IOS_RUNTIME_REF b/NATIVESCRIPT_IOS_RUNTIME_REF new file mode 100644 index 000000000..906ab599a --- /dev/null +++ b/NATIVESCRIPT_IOS_RUNTIME_REF @@ -0,0 +1 @@ +0e89474b1a203ff084b4c88df8a2aa777e5c50a0 diff --git a/V8_RELEASE b/V8_RELEASE new file mode 100644 index 000000000..474898527 --- /dev/null +++ b/V8_RELEASE @@ -0,0 +1 @@ +v8-14.9.207.39-3 diff --git a/apps/demo/nativescript.config.ts b/apps/demo/nativescript.config.ts index 89614fe1d..11e5fb0ea 100644 --- a/apps/demo/nativescript.config.ts +++ b/apps/demo/nativescript.config.ts @@ -29,6 +29,16 @@ export default { }, ], }, + tvos: { + discardUncaughtJsExceptions: false, + SPMPackages: [ + { + name: 'CanvasNative', + libs: ['NativeScriptV8'], + path: '../../nativescript-v8', + }, + ], + }, cli: { packageManager: 'npm', }, diff --git a/apps/demo/package.json b/apps/demo/package.json index 495ded45b..b6e2ee0a4 100644 --- a/apps/demo/package.json +++ b/apps/demo/package.json @@ -4,6 +4,7 @@ "license": "SEE LICENSE IN ", "repository": "", "dependencies": { + "@nativescript/audio-context": "file:../../packages/audio-context", "@nativescript/canvas": "file:../../packages/canvas", "@nativescript/canvas-babylon": "file:../../packages/canvas-babylon", "@nativescript/canvas-chartjs": "file:../../packages/canvas-chartjs", @@ -14,7 +15,7 @@ "@nativescript/canvas-polyfill": "file:../../packages/canvas-polyfill", "@nativescript/canvas-svg": "file:../../packages/canvas-svg", "@nativescript/canvas-three": "file:../../packages/canvas-three", - "@nativescript/core": "9.0.20", + "@nativescript/core": "9.1.1", "@pixi-essentials/svg": "file:../../node_modules/@pixi-essentials/svg", "babylonjs": "file:../../node_modules/babylonjs", "babylonjs-materials": "file:../../node_modules/babylonjs-materials", @@ -28,13 +29,12 @@ "three": "file:../../node_modules/three", "three-stdlib": "file:../../node_modules/three-stdlib", "vexflow": "file:../../node_modules/vexflow", - "zen-3d": "file:../../node_modules/zen-3d", - "@nativescript/audio-context": "file:../../packages/audio-context" + "zen-3d": "file:../../node_modules/zen-3d" }, "devDependencies": { - "@nativescript/android": "~8.9.0", + "@nativescript/android": "9.1.1", "@nativescript/devtools": "~0.0.1", - "@nativescript/ios": "~8.9.0", + "@nativescript/ios": "9.0.4-next.2026-08-13-31742124335", "@nativescript/visionos": "~9.0.1" } } diff --git a/apps/demo/src/app.ts b/apps/demo/src/app.ts index d269b46d7..e278afa1e 100644 --- a/apps/demo/src/app.ts +++ b/apps/demo/src/app.ts @@ -120,6 +120,7 @@ class CustomPage extends ProtocolWrapper { declare const jp, GDPerformanceMonitor, android, java, UIColor; let monitor; import { Application, path as filePath, knownFolders, Utils, path as nsPath, ImageSource, Trace, Screen, Color } from '@nativescript/core'; +import { captureLaunchArgs, launchArgs } from './launch-args'; function describeAppError(value: unknown): string { if (value == null) { @@ -175,6 +176,14 @@ Application.on('uncaughtError', (args) => { // global.process.env = {} as any; Application.on('launch', (args) => { + captureLaunchArgs(args.android); + + // Must happen before any Canvas is constructed; the constructor reads it once. + if (__ANDROID__ && launchArgs.surface !== undefined) { + Canvas.useSurface = launchArgs.surface === 'true'; + console.log(`SURFACE|useSurface=${Canvas.useSurface}`); + } + //require('@nativescript/canvas-polyfill'); if (__ANDROID__) { jp.wasabeef.takt.Takt.stock(Utils.android.getApplicationContext()).seat(jp.wasabeef.takt.Seat.TOP_CENTER).color(-65536); diff --git a/apps/demo/src/launch-args.ts b/apps/demo/src/launch-args.ts new file mode 100644 index 000000000..e039b412f --- /dev/null +++ b/apps/demo/src/launch-args.ts @@ -0,0 +1,76 @@ +/** + * Demo requested on the command line, so a run is one command instead of a tap + * at fixed screen coordinates: + * + * adb shell am start -n org.nativescript.plugindemo/com.tns.NativeScriptActivity \ + * --es demo canvas-spec --es suite 2d + * + * `--es surface true` swaps the canvas backing view from a TextureView to a + * SurfaceView, which skips a compositor copy -- the A/B for "is the extra copy + * costing us anything on this scene". + * + * xcrun simctl launch org.nativescript.plugindemo --demo=canvas-spec --suite=2d + * + * Captured at launch rather than read in the page, because the first page + * navigates during bootstrap -- before Application.android.foregroundActivity + * is assigned. + */ +export const launchArgs: { demo?: string; profile?: string; frames?: number; suite?: string; surface?: string } = {}; + +const KEYS = ['demo', 'profile', 'suite', 'surface'] as const; + +export function captureLaunchArgs(androidIntent?: any) { + try { + if (__ANDROID__) { + captureFromIntent(androidIntent); + } else if (__APPLE__) { + captureFromProcessArguments(); + } + } catch (e) { + // A launch without arguments is the normal case; nothing to do. + } +} + +function captureFromIntent(intent: any) { + if (!intent) { + return; + } + for (const key of KEYS) { + const value = intent.getStringExtra?.(key); + if (value) { + launchArgs[key] = value; + // Consume it so navigating Back does not bounce straight back in. + intent.removeExtra?.(key); + } + } + const frames = intent.getIntExtra?.('frames', 0); + if (frames) { + launchArgs.frames = frames; + intent.removeExtra?.('frames'); + } +} + +/** `--demo=canvas-spec`, as simctl and Xcode's scheme arguments pass them. */ +function captureFromProcessArguments() { + const args = NSProcessInfo.processInfo.arguments; + if (!args) { + return; + } + for (let i = 0; i < args.count; i++) { + const arg = String(args.objectAtIndex(i)); + const eq = arg.indexOf('='); + if (arg.indexOf('--') !== 0 || eq === -1) { + continue; + } + const key = arg.substring(2, eq); + const value = arg.substring(eq + 1); + if (!value) { + continue; + } + if (key === 'frames') { + launchArgs.frames = parseInt(value, 10) || undefined; + } else if ((KEYS as readonly string[]).indexOf(key) !== -1) { + launchArgs[key] = value; + } + } +} diff --git a/apps/demo/src/main-page.ts b/apps/demo/src/main-page.ts index cb9b0f43f..949a9426e 100644 --- a/apps/demo/src/main-page.ts +++ b/apps/demo/src/main-page.ts @@ -1,7 +1,17 @@ -import { EventData, Page } from '@nativescript/core'; +import { EventData, Frame, Page } from '@nativescript/core'; import { MainViewModel } from './main-view-model'; +import { launchArgs } from './launch-args'; export function navigatingTo(args: EventData) { const page = args.object; page.bindingContext = new MainViewModel(); + + const demo = launchArgs.demo; + if (demo) { + // One-shot: clear first so Back returns to this list. + launchArgs.demo = undefined; + setTimeout(() => { + Frame.topmost().navigate({ moduleName: `plugin-demos/${demo}` }); + }, 0); + } } diff --git a/apps/demo/src/main-page.xml b/apps/demo/src/main-page.xml index 1f039e500..651727419 100644 --- a/apps/demo/src/main-page.xml +++ b/apps/demo/src/main-page.xml @@ -5,6 +5,8 @@ +