@geastack/apple 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +54 -0
  3. package/dist/index.d.ts +127 -0
  4. package/dist/index.js +3076 -0
  5. package/generated/AVFoundation.d.ts +112 -0
  6. package/generated/ActivityKit.d.ts +7 -0
  7. package/generated/AppKit.d.ts +268 -0
  8. package/generated/CoreGraphics.d.ts +18 -0
  9. package/generated/CoreLocation.d.ts +28 -0
  10. package/generated/CoreMedia.d.ts +8 -0
  11. package/generated/Dispatch.d.ts +2 -0
  12. package/generated/Foundation.d.ts +13 -0
  13. package/generated/MapKit.d.ts +28 -0
  14. package/generated/Metal.d.ts +91 -0
  15. package/generated/MetalKit.d.ts +22 -0
  16. package/generated/Photos.d.ts +5 -0
  17. package/generated/QuartzCore.d.ts +12 -0
  18. package/generated/SwiftData.d.ts +11 -0
  19. package/generated/UIKit.d.ts +187 -0
  20. package/package.json +123 -0
  21. package/runtime/AVFoundation.js +136 -0
  22. package/runtime/ActivityKit.js +18 -0
  23. package/runtime/AppKit.js +313 -0
  24. package/runtime/CoreGraphics.js +15 -0
  25. package/runtime/CoreLocation.js +30 -0
  26. package/runtime/CoreMedia.js +7 -0
  27. package/runtime/Dispatch.js +11 -0
  28. package/runtime/Foundation.js +21 -0
  29. package/runtime/MapKit.js +22 -0
  30. package/runtime/Metal.js +126 -0
  31. package/runtime/MetalKit.js +15 -0
  32. package/runtime/Photos.js +9 -0
  33. package/runtime/QuartzCore.js +9 -0
  34. package/runtime/SwiftData.js +29 -0
  35. package/runtime/UIKit.js +242 -0
  36. package/runtime/empty.js +1 -0
  37. package/targets/ios/Info.plist.in +25 -0
  38. package/targets/ios/README.md +46 -0
  39. package/targets/ios/build-ios.sh +395 -0
  40. package/targets/ios/detect-development-team.mjs +109 -0
  41. package/targets/ios/generate-resident-entry.mjs +67 -0
  42. package/targets/ios/generate-resident-registry.mjs +162 -0
  43. package/targets/ios/generate-xcode-project.mjs +520 -0
  44. package/targets/ios/main/canvas_view.h +7 -0
  45. package/targets/ios/main/canvas_view.mm +94 -0
  46. package/targets/ios/main/font_registry.h +17 -0
  47. package/targets/ios/main/font_registry.mm +129 -0
  48. package/targets/ios/main/image_bridge.h +13 -0
  49. package/targets/ios/main/image_bridge.mm +53 -0
  50. package/targets/ios/main/ios_apps.c +28 -0
  51. package/targets/ios/main/ios_camera.mm +446 -0
  52. package/targets/ios/main/ios_display.mm +344 -0
  53. package/targets/ios/main/ios_main.mm +811 -0
  54. package/targets/ios/main/ios_memory.cpp +55 -0
  55. package/targets/ios/main/ios_renderer.h +20 -0
  56. package/targets/ios/main/ios_renderer.mm +27 -0
  57. package/targets/ios/main/ios_root_background.cpp +20 -0
  58. package/targets/ios/main/ios_root_background.h +11 -0
  59. package/targets/ios/main/ios_sensors.mm +244 -0
  60. package/targets/ios/main/ios_timers.mm +53 -0
  61. package/targets/ios/main/renderer/ios_renderer_internal.h +65 -0
  62. package/targets/ios/main/renderer/ios_renderer_support.mm +111 -0
  63. package/targets/ios/main/renderer/native_button.mm +179 -0
  64. package/targets/ios/main/renderer/native_label.mm +161 -0
  65. package/targets/ios/main/renderer/native_scroll_container.mm +279 -0
  66. package/targets/ios/main/renderer/view_reconciler.mm +274 -0
  67. package/targets/macos/Info.plist.in +26 -0
  68. package/targets/macos/README.md +131 -0
  69. package/targets/macos/build-child-lifecycle.sh +119 -0
  70. package/targets/macos/build-macos.sh +2156 -0
  71. package/targets/macos/check-module-graph-freshness.mjs +82 -0
  72. package/targets/macos/compiler-input-fingerprint.mjs +32 -0
  73. package/targets/macos/finalize-apple-native-output.mjs +77 -0
  74. package/targets/macos/generate-resident-entry.mjs +80 -0
  75. package/targets/macos/generate-resident-registry.mjs +141 -0
  76. package/targets/macos/heavy-build-lock.sh +176 -0
  77. package/targets/macos/main/canvas_view.h +9 -0
  78. package/targets/macos/main/canvas_view.mm +176 -0
  79. package/targets/macos/main/color_convert.h +18 -0
  80. package/targets/macos/main/color_convert.mm +39 -0
  81. package/targets/macos/main/font_registry.h +35 -0
  82. package/targets/macos/main/font_registry.mm +350 -0
  83. package/targets/macos/main/image_bridge.h +16 -0
  84. package/targets/macos/main/image_bridge.mm +52 -0
  85. package/targets/macos/main/macos_app_platform.mm +95 -0
  86. package/targets/macos/main/macos_apps.c +31 -0
  87. package/targets/macos/main/macos_device_control.mm +56 -0
  88. package/targets/macos/main/macos_display.mm +484 -0
  89. package/targets/macos/main/macos_host_device_control.mm +95 -0
  90. package/targets/macos/main/macos_main.mm +839 -0
  91. package/targets/macos/main/macos_memory.cpp +31 -0
  92. package/targets/macos/main/macos_native_shell.h +31 -0
  93. package/targets/macos/main/macos_native_shell.mm +396 -0
  94. package/targets/macos/main/macos_network.mm +253 -0
  95. package/targets/macos/main/macos_renderer.h +39 -0
  96. package/targets/macos/main/macos_renderer.mm +1969 -0
  97. package/targets/macos/main/macos_sensors.cpp +148 -0
  98. package/targets/macos/main/macos_smoke_app.cpp +65 -0
  99. package/targets/macos/main/macos_timers.mm +52 -0
  100. package/targets/macos/main/press_bridge.h +21 -0
  101. package/targets/macos/main/press_bridge.mm +72 -0
  102. package/targets/macos/main/thermalright_hid_display.h +14 -0
  103. package/targets/macos/main/thermalright_hid_display.mm +995 -0
  104. package/targets/macos/native-artifact-provenance.mjs +421 -0
  105. package/targets/macos/order-compile-edges.mjs +64 -0
  106. package/targets/macos/preflight-hard-muted-artifact.mjs +93 -0
  107. package/targets/macos/prune-generated-modules.mjs +147 -0
  108. package/targets/macos/sync-resources.mjs +153 -0
  109. package/targets/shared/analyze-audio-capture.mjs +131 -0
  110. package/targets/shared/apple_audio.mm +1194 -0
  111. package/targets/shared/apple_audio_selftest.mm +602 -0
  112. package/targets/shared/apple_audio_testing.h +34 -0
  113. package/targets/shared/build-audio-selftest.sh +50 -0
@@ -0,0 +1,395 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
5
+ IOS_DIR="$ROOT_DIR/targets/ios"
6
+
7
+ # Resolve the gea framework through npm.
8
+ GEA_CORE="$(node -e "process.stdout.write(require('fs').realpathSync('$ROOT_DIR/node_modules/@geastack/core'))" 2>/dev/null || true)"
9
+ [ -n "$GEA_CORE" ] && [ -f "$GEA_CORE/package.json" ] || { echo "Cannot resolve @geastack/core via node_modules — run 'npm install' in $ROOT_DIR" >&2; exit 1; }
10
+ GEA_COMPILER="$(node -e "process.stdout.write(require('fs').realpathSync('$ROOT_DIR/node_modules/@geastack/compiler'))" 2>/dev/null || true)"
11
+ [ -n "$GEA_COMPILER" ] && [ -f "$GEA_COMPILER/package.json" ] || { echo "Cannot resolve @geastack/compiler via node_modules — run 'npm install' in $ROOT_DIR" >&2; exit 1; }
12
+ GEA_HOST_DIR="${GEA_HOST_DIR:-$GEA_CORE/../host}"
13
+ GEA_ENGINE_DIR="${GEA_ENGINE_DIR:-$GEA_CORE/../engine}"
14
+ GEA_ELEMENTS_DIR="${GEA_ELEMENTS_DIR:-$GEA_CORE/../elements}"
15
+ GEA_GEAOS_PACKAGE_DIR="${GEA_GEAOS_PACKAGE_DIR:-$GEA_CORE/../geaos}"
16
+ # The project is the directory this script was started in -- `gea build` spawns
17
+ # it with the app's own folder as cwd, and a direct run is made from the app
18
+ # folder. Captured once, before anything can move. Every nested `gea` call
19
+ # inherits the same cwd and resolves the same project, so none carry --project.
20
+ BUILD_INVOCATION_CWD="$(pwd -P)"
21
+ # This script IS @geastack/apple, so the generator must bind against this
22
+ # working copy rather than whatever installed copy it would resolve from the app.
23
+ export GEA_APPLE_ROOT="$ROOT_DIR"
24
+ GEA_CLI="${GEA_CLI_BIN:-$(node -e "process.stdout.write(require('path').join(require('fs').realpathSync('$ROOT_DIR/node_modules/@geastack/cli'), 'bin', 'gea.mjs'))" 2>/dev/null || true)}"
25
+ [ -n "$GEA_CLI" ] && [ -f "$GEA_CLI" ] || { echo "Cannot resolve @geastack/cli via node_modules — run 'npm install' in $ROOT_DIR" >&2; exit 1; }
26
+
27
+ # Framework sources/includes from the shared manifest (iOS keeps camera — ios_camera.mm
28
+ # provides it — but has no resident-app/OTA/diagnostics services or single-app
29
+ # runtime shell, so exclude those). generate-xcode-project.mjs consumes these.
30
+ # shellcheck source=/dev/null
31
+ source "$GEA_CORE/gea_sources.sh"
32
+ export GEA_CORE
33
+ export GEA_FW_CXX_SOURCES="$(gea_fw_cxx_sources | grep -vE "/(runtime|services/[a-z_]+)\.cpp\$")"
34
+ export GEA_FW_C_SOURCES="$(gea_fw_c_sources)"
35
+ export GEA_FW_INCLUDE_DIRS="$(gea_fw_include_flags | sed "s/^-I//")"
36
+ APP_ID="${1:-bouncing-balls-jsx}"
37
+ DESTINATION="${2:-simulator}"
38
+
39
+ if [[ "$DESTINATION" != "simulator" && "$DESTINATION" != "device" ]]; then
40
+ echo "usage: targets/ios/build-ios.sh [app-id] [simulator|device]" >&2
41
+ exit 2
42
+ fi
43
+
44
+ APP_INFO="$(node "$GEA_CLI" apps inspect "$APP_ID" --format shell)" || {
45
+ echo "Unknown app id: $APP_ID" >&2
46
+ exit 1
47
+ }
48
+
49
+ IFS=$'\t' read -r APP_ROOT APP_ENTRY APP_RUNTIME APP_DISPLAY_NAME <<< "$APP_INFO"
50
+ if [[ "$APP_RUNTIME" != "gea" ]]; then
51
+ echo "iOS target only supports runtime=gea apps for now: $APP_ID is runtime=$APP_RUNTIME" >&2
52
+ exit 1
53
+ fi
54
+ APP_NAME="$(node -e "
55
+ const raw = process.argv[1] || process.argv[2];
56
+ console.log(raw.replace(/(^|[-_\\s])([a-z])/g, (_, p, c) => c.toUpperCase()).replace(/[-_\\s]+/g, ''));
57
+ " "$APP_DISPLAY_NAME" "$APP_ID")"
58
+ BUNDLE_ID="${GEA_IOS_BUNDLE_IDENTIFIER:-com.gea.${APP_ID//[^A-Za-z0-9]/-}}"
59
+ # Build output belongs to the PROJECT being built, never to this package.
60
+ # IOS_DIR lives inside @geastack/apple, which for an app that installs the
61
+ # package means node_modules -- a directory nobody opens and npm wipes on the
62
+ # next install. The invocation directory is the app the CLI resolved, so output
63
+ # sources, the Xcode project and DerivedData land next to the app's source.
64
+ # GEA_IOS_OUTPUT_DIR overrides the root for a caller that wants it elsewhere.
65
+ IOS_OUTPUT_ROOT="${GEA_IOS_OUTPUT_DIR:-$BUILD_INVOCATION_CWD/dist/ios}"
66
+ case "$IOS_OUTPUT_ROOT" in
67
+ /*) ;;
68
+ *) IOS_OUTPUT_ROOT="$(pwd -P)/$IOS_OUTPUT_ROOT" ;;
69
+ esac
70
+ GENERATED_DIR="$IOS_OUTPUT_ROOT/.generated/$APP_ID"
71
+ PROJECT_PATH="$IOS_OUTPUT_ROOT/$APP_ID/GeaIos.xcodeproj"
72
+ DERIVED_DATA="$IOS_OUTPUT_ROOT/.derived-data/$APP_ID-$DESTINATION"
73
+ CONFIGURATION="${GEA_IOS_CONFIGURATION:-Debug}"
74
+ SKIP_LAUNCH="${GEA_IOS_SKIP_LAUNCH:-0}"
75
+
76
+ mkdir -p "$GENERATED_DIR"
77
+
78
+ build_font_args=(
79
+ --font-device-pixel-ratio "${GEA_IOS_FONT_DEVICE_PIXEL_RATIOS:-2,3}"
80
+ --font-viewport-width "${GEA_IOS_FONT_VIEWPORT_WIDTHS:-750,828,1125,1170,1290}"
81
+ )
82
+
83
+ generate_app() {
84
+ local app_id="$1"
85
+ local out_dir="$2"
86
+ local prefix="${3:-}"
87
+ local info
88
+ local root
89
+ local entry
90
+ local runtime
91
+ info="$(node "$GEA_CLI" apps inspect "$app_id" --format shell)" || return 1
92
+ IFS=$'\t' read -r root entry runtime _name <<< "$info"
93
+ if [[ "$runtime" != "gea" ]]; then
94
+ echo "Skipping resident $app_id: runtime=$runtime is not supported by iOS resident build" >&2
95
+ return 1
96
+ fi
97
+ local args=(
98
+ # Which platform's SDK the bridge metadata describes. It defaults to the
99
+ # empty string, meaning "no platform filter", and the filter is not
100
+ # cosmetic: `writeAppleNativeMetadata` strips the off-platform frameworks'
101
+ # functions and members, so an unfiltered metadata declares AppKit's
102
+ # `installRootView` alongside UIKit's in an iOS app's bridge -- a free
103
+ # function no iOS target defines. `build-macos.sh` passes `macos` for the
104
+ # same reason; iOS was simply never given its half.
105
+ --apple-platform ios
106
+ # The plugin resolves itself from `@geastack/geatsc-plugin-apple-native`,
107
+ # which is not installed in this repo's node_modules -- so, as
108
+ # `build-macos.sh` already does, name the in-repo build directly.
109
+ --geatsc-apple-native-plugin "$ROOT_DIR/packages/geatsc-plugin-apple-native/dist/index.js"
110
+ --app-dir "$root"
111
+ --entry "$entry"
112
+ --out-dir "$out_dir"
113
+ "${build_font_args[@]}"
114
+ )
115
+ local native_plugin="$root/ios/geatsc-native-plugin.mjs"
116
+ if [[ -f "$native_plugin" ]]; then
117
+ args+=(--extra-geatsc-plugin "$native_plugin")
118
+ fi
119
+ if [[ -n "$prefix" ]]; then
120
+ args+=(
121
+ --entry-symbol "${prefix}_top_level"
122
+ --cpp-prelude-symbol "${prefix}_register_styles"
123
+ --font-symbol-prefix "$prefix"
124
+ --isolate-symbols
125
+ )
126
+ fi
127
+ node "$GEA_CORE/scripts/build-gea-vite-geatsc.mjs" "${args[@]}"
128
+ }
129
+
130
+ pick_simulator_device() {
131
+ local devices_json
132
+ devices_json="$(xcrun simctl list devices available --json 2>/dev/null || true)"
133
+ if [[ -z "$devices_json" ]]; then
134
+ return 0
135
+ fi
136
+ node -e "
137
+ const fs = require('fs');
138
+ const input = fs.readFileSync(0, 'utf8');
139
+ if (!input.trim()) process.exit(0);
140
+ const data = JSON.parse(input);
141
+ const devices = Object.values(data.devices || {}).flat();
142
+ const iphones = devices.filter((d) => d.isAvailable && /^iPhone/.test(d.name));
143
+ const booted = iphones.find((d) => d.state === 'Booted');
144
+ const preferred = booted || iphones[iphones.length - 1];
145
+ if (preferred) console.log(preferred.udid);
146
+ " <<< "$devices_json"
147
+ }
148
+
149
+ # Build an xcodebuild -destination string for a simulator UDID using its
150
+ # name + OS version. The "name=…,OS=…" form is resolved directly by xcodebuild,
151
+ # unlike "id=<udid>", which must match the scheme's destination enumeration —
152
+ # and that enumeration is intermittently empty when a connected real iPhone
153
+ # lacks its device-support platform, making id-matching flaky.
154
+ sim_destination_for_udid() {
155
+ local udid="$1"
156
+ local devices_json
157
+ devices_json="$(xcrun simctl list devices available --json 2>/dev/null || true)"
158
+ [[ -z "$devices_json" ]] && return 0
159
+ node -e "
160
+ const data = JSON.parse(require('fs').readFileSync(0, 'utf8'));
161
+ const want = process.argv[1];
162
+ for (const [runtime, list] of Object.entries(data.devices || {})) {
163
+ for (const d of list) {
164
+ if (d.udid === want) {
165
+ const m = runtime.match(/iOS-(\d+)-(\d+)/);
166
+ const os = m ? m[1] + '.' + m[2] : '';
167
+ process.stdout.write('platform=iOS Simulator,name=' + d.name + (os ? ',OS=' + os : ''));
168
+ process.exit(0);
169
+ }
170
+ }
171
+ }
172
+ " "$udid" <<< "$devices_json"
173
+ }
174
+
175
+ show_simulator_device() {
176
+ local udid="$1"
177
+ if [[ "${GEA_IOS_OPEN_SIMULATOR:-1}" != "1" ]]; then
178
+ return 0
179
+ fi
180
+ /usr/bin/open -a Simulator --args -CurrentDeviceUDID "$udid"
181
+ }
182
+
183
+ pick_ios_device() {
184
+ local output_json
185
+ output_json="$(mktemp "${TMPDIR:-/tmp}/gea-ios-devices.XXXXXX.json")"
186
+ if ! xcrun devicectl list devices --timeout "${GEA_IOS_DEVICECTL_TIMEOUT:-30}" --json-output "$output_json" >/dev/null; then
187
+ rm -f "$output_json"
188
+ return 0
189
+ fi
190
+ node -e "
191
+ const fs = require('fs');
192
+ const data = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
193
+ const devices = data.result?.devices || [];
194
+ const usable = devices.filter((device) => {
195
+ const hardware = device.hardwareProperties || {};
196
+ const props = device.deviceProperties || {};
197
+ const capabilities = device.capabilities || [];
198
+ const features = new Set(capabilities.map((capability) => capability.featureIdentifier));
199
+ return hardware.platform === 'iOS' &&
200
+ hardware.deviceType === 'iPhone' &&
201
+ props.developerModeStatus === 'enabled' &&
202
+ device.connectionProperties?.tunnelState === 'connected' &&
203
+ features.has('com.apple.coredevice.feature.installapp') &&
204
+ features.has('com.apple.coredevice.feature.launchapplication');
205
+ });
206
+ usable.sort((a, b) => {
207
+ const aDate = Date.parse(a.connectionProperties?.lastConnectionDate || '') || 0;
208
+ const bDate = Date.parse(b.connectionProperties?.lastConnectionDate || '') || 0;
209
+ return aDate - bDate;
210
+ });
211
+ const picked = usable[usable.length - 1];
212
+ if (picked) console.log(picked.identifier || picked.hardwareProperties?.udid || picked.deviceProperties?.name || '');
213
+ " "$output_json"
214
+ rm -f "$output_json"
215
+ }
216
+
217
+ RESIDENT_IDS=()
218
+ if [[ "$APP_ID" == "app-launcher" && "${GEA_IOS_RESIDENT_APPS:-auto}" != "none" ]]; then
219
+ if [[ -n "${GEA_IOS_RESIDENT_APPS:-}" && "${GEA_IOS_RESIDENT_APPS:-}" != "auto" ]]; then
220
+ read -r -a RESIDENT_IDS <<< "${GEA_IOS_RESIDENT_APPS//,/ }"
221
+ else
222
+ mapfile -t RESIDENT_IDS < <(
223
+ node "$GEA_CLI" apps list --target esp32 | grep -vx 'direct-psram-balls' || true
224
+ )
225
+ fi
226
+ fi
227
+
228
+ if [[ ${#RESIDENT_IDS[@]} -eq 0 ]]; then
229
+ generate_app "$APP_ID" "$GENERATED_DIR" ""
230
+ else
231
+ ACTUAL_RESIDENTS=()
232
+ for id in "${RESIDENT_IDS[@]}"; do
233
+ safe="${id//[^A-Za-z0-9_]/_}"
234
+ prefix="gea_resident_${safe}"
235
+ resident_dir="$GENERATED_DIR/residents/$id"
236
+ mkdir -p "$resident_dir"
237
+ generate_app "$id" "$resident_dir" "$prefix" || continue
238
+ node "$IOS_DIR/generate-resident-entry.mjs" "$prefix" "$resident_dir/entry.cpp"
239
+ ACTUAL_RESIDENTS+=("$id")
240
+ done
241
+ RESIDENT_IDS=("${ACTUAL_RESIDENTS[@]}")
242
+ node "$IOS_DIR/generate-resident-registry.mjs" "$GENERATED_DIR/resident_registry.cpp" "$APP_ID" "${RESIDENT_IDS[@]}"
243
+ fi
244
+
245
+ if [[ ! -f "$GENERATED_DIR/gea_runtime.cpp" ]]; then
246
+ echo "#include \"$GEA_COMPILER/dist/targets/cpp/runtime/runtime.cpp\"" > "$GENERATED_DIR/gea_runtime.cpp"
247
+ fi
248
+
249
+ GEA_CLI_BIN="$GEA_CLI" node "$IOS_DIR/generate-xcode-project.mjs" \
250
+ --app-id "$APP_ID" \
251
+ --app-name "$APP_NAME" \
252
+ --bundle-id "$BUNDLE_ID" \
253
+ --generated-dir "$GENERATED_DIR" \
254
+ --project-path "$PROJECT_PATH"
255
+
256
+ SDK="iphonesimulator"
257
+ SIMULATOR_DEVICE_ID="${GEA_IOS_SIMULATOR_UDID:-}"
258
+ XCODE_DESTINATION="${GEA_IOS_SIMULATOR_DESTINATION:-generic/platform=iOS Simulator}"
259
+ if [[ "$DESTINATION" == "simulator" ]]; then
260
+ if [[ "$SKIP_LAUNCH" == "1" ]]; then
261
+ XCODE_DESTINATION="${GEA_IOS_SIMULATOR_DESTINATION:-generic/platform=iOS Simulator}"
262
+ else
263
+ if [[ -z "$SIMULATOR_DEVICE_ID" ]]; then
264
+ SIMULATOR_DEVICE_ID="$(pick_simulator_device)"
265
+ fi
266
+ if [[ -z "$SIMULATOR_DEVICE_ID" ]]; then
267
+ echo "No available iPhone simulator found." >&2
268
+ exit 1
269
+ fi
270
+ # Resolve a name+OS destination (robust against the poisoned enumeration that
271
+ # a connected real iPhone causes); fall back to id= if the lookup fails.
272
+ XCODE_DESTINATION="${GEA_IOS_SIMULATOR_DESTINATION:-}"
273
+ if [[ -z "$XCODE_DESTINATION" ]]; then
274
+ XCODE_DESTINATION="$(sim_destination_for_udid "$SIMULATOR_DEVICE_ID")"
275
+ [[ -z "$XCODE_DESTINATION" ]] && XCODE_DESTINATION="platform=iOS Simulator,id=$SIMULATOR_DEVICE_ID"
276
+ fi
277
+ fi
278
+ fi
279
+ EXTRA_BUILD_SETTINGS=()
280
+ EXTRA_XCODEBUILD_ARGS=()
281
+ IOS_DEVICE_ID="${GEA_IOS_DEVICE_ID:-}"
282
+ if [[ "$DESTINATION" == "device" ]]; then
283
+ SDK="iphoneos"
284
+ XCODE_DESTINATION="${GEA_IOS_DEVICE_DESTINATION:-generic/platform=iOS}"
285
+ if [[ -z "$IOS_DEVICE_ID" && "$SKIP_LAUNCH" != "1" ]]; then
286
+ IOS_DEVICE_ID="$(pick_ios_device)"
287
+ if [[ -n "$IOS_DEVICE_ID" ]]; then
288
+ echo "Using iOS device $IOS_DEVICE_ID. Override with GEA_IOS_DEVICE_ID." >&2
289
+ fi
290
+ fi
291
+ if [[ -z "$IOS_DEVICE_ID" && "$SKIP_LAUNCH" != "1" ]]; then
292
+ echo "No available iPhone device found for install or launch." >&2
293
+ echo "Connect an iPhone with Developer Mode enabled, or set GEA_IOS_DEVICE_ID." >&2
294
+ exit 1
295
+ fi
296
+ DEVELOPMENT_TEAM="${GEA_IOS_DEVELOPMENT_TEAM:-}"
297
+ if [[ -z "$DEVELOPMENT_TEAM" && "$SKIP_LAUNCH" != "1" ]]; then
298
+ if DEVELOPMENT_TEAM="$(node "$IOS_DIR/detect-development-team.mjs")"; then
299
+ echo "Using Xcode development team $DEVELOPMENT_TEAM. Override with GEA_IOS_DEVELOPMENT_TEAM." >&2
300
+ else
301
+ DEVELOPMENT_TEAM=""
302
+ fi
303
+ fi
304
+ if [[ -z "$DEVELOPMENT_TEAM" ]]; then
305
+ if [[ "$SKIP_LAUNCH" != "1" ]]; then
306
+ echo "Device builds require GEA_IOS_DEVELOPMENT_TEAM for Xcode automatic signing." >&2
307
+ echo "The script also checks Xcode preferences and installed provisioning profiles, but no unambiguous team was found." >&2
308
+ echo "Example: GEA_IOS_DEVELOPMENT_TEAM=ABCDE12345 targets/ios/build-ios.sh $APP_ID device" >&2
309
+ exit 1
310
+ fi
311
+ echo "GEA_IOS_DEVELOPMENT_TEAM is not set; building an unsigned iphoneos binary." >&2
312
+ echo "Set GEA_IOS_DEVELOPMENT_TEAM to install or launch on an iPhone." >&2
313
+ EXTRA_BUILD_SETTINGS+=("CODE_SIGNING_ALLOWED=NO")
314
+ EXTRA_BUILD_SETTINGS+=("CODE_SIGNING_REQUIRED=NO")
315
+ EXTRA_BUILD_SETTINGS+=("CODE_SIGN_IDENTITY=")
316
+ else
317
+ EXTRA_BUILD_SETTINGS+=("DEVELOPMENT_TEAM=$DEVELOPMENT_TEAM")
318
+ EXTRA_BUILD_SETTINGS+=("CODE_SIGN_STYLE=Automatic")
319
+ ALLOW_PROVISIONING_UPDATES="${GEA_IOS_ALLOW_PROVISIONING_UPDATES:-}"
320
+ if [[ -z "$ALLOW_PROVISIONING_UPDATES" && "$SKIP_LAUNCH" != "1" ]]; then
321
+ ALLOW_PROVISIONING_UPDATES="1"
322
+ fi
323
+ if [[ "$ALLOW_PROVISIONING_UPDATES" == "1" ]]; then
324
+ echo "Allowing Xcode to create or update iOS provisioning profiles." >&2
325
+ echo "Set GEA_IOS_ALLOW_PROVISIONING_UPDATES=0 to disable this." >&2
326
+ EXTRA_XCODEBUILD_ARGS+=("-allowProvisioningUpdates")
327
+ fi
328
+ fi
329
+ fi
330
+
331
+ # xcodebuild's destination resolution is intermittently empty when a connected
332
+ # real iPhone lacks its device-support platform — even an explicit simulator
333
+ # destination then fails to match. Retry on that specific flake (re-settling the
334
+ # sim first); any other failure breaks out immediately.
335
+ XCODEBUILD_LOG="$(mktemp "${TMPDIR:-/tmp}/gea-ios-xcodebuild.XXXXXX.log")"
336
+ XCODEBUILD_ATTEMPTS="${GEA_IOS_BUILD_ATTEMPTS:-3}"
337
+ xcodebuild_status=1
338
+ for ((xcodebuild_attempt = 1; xcodebuild_attempt <= XCODEBUILD_ATTEMPTS; xcodebuild_attempt++)); do
339
+ set +e
340
+ xcodebuild \
341
+ ${EXTRA_XCODEBUILD_ARGS[@]+"${EXTRA_XCODEBUILD_ARGS[@]}"} \
342
+ -project "$PROJECT_PATH" \
343
+ -scheme GeaIos \
344
+ -configuration "$CONFIGURATION" \
345
+ -sdk "$SDK" \
346
+ -destination "$XCODE_DESTINATION" \
347
+ -derivedDataPath "$DERIVED_DATA" \
348
+ PRODUCT_BUNDLE_IDENTIFIER="$BUNDLE_ID" \
349
+ ${EXTRA_BUILD_SETTINGS[@]+"${EXTRA_BUILD_SETTINGS[@]}"} \
350
+ build 2>&1 | tee "$XCODEBUILD_LOG"
351
+ xcodebuild_status=${PIPESTATUS[0]}
352
+ set -e
353
+ [[ $xcodebuild_status -eq 0 ]] && break
354
+ if [[ "$DESTINATION" == "simulator" ]] && grep -q "Unable to find a destination" "$XCODEBUILD_LOG"; then
355
+ echo "xcodebuild could not resolve the simulator destination (flaky with a connected device); re-settling sim and retrying ($xcodebuild_attempt/$XCODEBUILD_ATTEMPTS)." >&2
356
+ if [[ -n "$SIMULATOR_DEVICE_ID" ]]; then
357
+ xcrun simctl boot "$SIMULATOR_DEVICE_ID" >/dev/null 2>&1 || true
358
+ xcrun simctl bootstatus "$SIMULATOR_DEVICE_ID" -b >/dev/null 2>&1 || true
359
+ fi
360
+ continue
361
+ fi
362
+ break
363
+ done
364
+ rm -f "$XCODEBUILD_LOG"
365
+ [[ $xcodebuild_status -eq 0 ]] || exit "$xcodebuild_status"
366
+
367
+ APP_PRODUCT="$DERIVED_DATA/Build/Products/$CONFIGURATION-$SDK/$APP_NAME.app"
368
+ echo "Built $APP_PRODUCT"
369
+
370
+ if [[ "$DESTINATION" == "simulator" && "$SKIP_LAUNCH" != "1" ]]; then
371
+ if [[ -z "$SIMULATOR_DEVICE_ID" ]]; then
372
+ SIMULATOR_DEVICE_ID="$(pick_simulator_device)"
373
+ fi
374
+ if [[ -z "$SIMULATOR_DEVICE_ID" ]]; then
375
+ echo "No available iPhone simulator found; build succeeded without launch." >&2
376
+ exit 0
377
+ fi
378
+ show_simulator_device "$SIMULATOR_DEVICE_ID"
379
+ xcrun simctl boot "$SIMULATOR_DEVICE_ID" >/dev/null 2>&1 || true
380
+ xcrun simctl bootstatus "$SIMULATOR_DEVICE_ID" -b >/dev/null || true
381
+ xcrun simctl install "$SIMULATOR_DEVICE_ID" "$APP_PRODUCT"
382
+ xcrun simctl launch "$SIMULATOR_DEVICE_ID" "$BUNDLE_ID"
383
+ fi
384
+
385
+ if [[ "$DESTINATION" == "device" && "$SKIP_LAUNCH" != "1" ]]; then
386
+ if [[ -z "$IOS_DEVICE_ID" ]]; then
387
+ IOS_DEVICE_ID="$(pick_ios_device)"
388
+ fi
389
+ if [[ -z "$IOS_DEVICE_ID" ]]; then
390
+ echo "No available iPhone device found; build succeeded without launch." >&2
391
+ exit 0
392
+ fi
393
+ xcrun devicectl --timeout "${GEA_IOS_DEVICECTL_TIMEOUT:-60}" device install app --device "$IOS_DEVICE_ID" "$APP_PRODUCT"
394
+ xcrun devicectl --timeout "${GEA_IOS_DEVICECTL_TIMEOUT:-60}" device process launch --terminate-existing --activate --device "$IOS_DEVICE_ID" "$BUNDLE_ID"
395
+ fi
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ import childProcess from 'node:child_process'
3
+ import fs from 'node:fs'
4
+ import os from 'node:os'
5
+ import path from 'node:path'
6
+
7
+ const teamIdPattern = /^[A-Z0-9]{10}$/
8
+
9
+ function run(command, args, input = undefined) {
10
+ const result = childProcess.spawnSync(command, args, {
11
+ encoding: 'utf8',
12
+ input,
13
+ stdio: ['pipe', 'pipe', 'ignore'],
14
+ })
15
+ return result.status === 0 ? result.stdout.trim() : ''
16
+ }
17
+
18
+ function addTeam(teams, id, name, source) {
19
+ if (!teamIdPattern.test(id)) return
20
+ const existing = teams.get(id)
21
+ teams.set(id, {
22
+ id,
23
+ name: existing?.name || name || '',
24
+ sources: [...new Set([...(existing?.sources || []), source])],
25
+ })
26
+ }
27
+
28
+ function readXcodePreferences(teams) {
29
+ const plistPath = path.join(os.homedir(), 'Library/Preferences/com.apple.dt.Xcode.plist')
30
+ if (!fs.existsSync(plistPath)) return ''
31
+
32
+ const lastSelected = run('/usr/bin/plutil', [
33
+ '-extract',
34
+ 'IDEProvisioningTeamManagerLastSelectedTeamID',
35
+ 'raw',
36
+ '-o',
37
+ '-',
38
+ plistPath,
39
+ ])
40
+
41
+ const rawTeams = run('/usr/bin/plutil', [
42
+ '-extract',
43
+ 'IDEProvisioningTeamByIdentifier',
44
+ 'json',
45
+ '-o',
46
+ '-',
47
+ plistPath,
48
+ ])
49
+
50
+ if (rawTeams) {
51
+ try {
52
+ const byAccount = JSON.parse(rawTeams)
53
+ for (const accountTeams of Object.values(byAccount)) {
54
+ if (!Array.isArray(accountTeams)) continue
55
+ for (const team of accountTeams) {
56
+ addTeam(teams, team.teamID || '', team.teamName || '', 'Xcode account')
57
+ }
58
+ }
59
+ } catch {
60
+ // Ignore malformed preference data; provisioning profiles may still help.
61
+ }
62
+ }
63
+
64
+ return teamIdPattern.test(lastSelected) ? lastSelected : ''
65
+ }
66
+
67
+ function readProvisioningProfiles(teams) {
68
+ const profileDir = path.join(
69
+ os.homedir(),
70
+ 'Library/Developer/Xcode/UserData/Provisioning Profiles',
71
+ )
72
+ if (!fs.existsSync(profileDir)) return
73
+
74
+ for (const entry of fs.readdirSync(profileDir)) {
75
+ if (!entry.endsWith('.mobileprovision') && !entry.endsWith('.provisionprofile')) continue
76
+ const profilePath = path.join(profileDir, entry)
77
+ const decoded = run('/usr/bin/security', ['cms', '-D', '-i', profilePath])
78
+ if (!decoded) continue
79
+ const id = run('/usr/bin/plutil', ['-extract', 'TeamIdentifier.0', 'raw', '-o', '-', '-'], decoded)
80
+ const name = run('/usr/bin/plutil', ['-extract', 'TeamName', 'raw', '-o', '-', '-'], decoded)
81
+ addTeam(teams, id, name, 'provisioning profile')
82
+ }
83
+ }
84
+
85
+ const teams = new Map()
86
+ const lastSelected = readXcodePreferences(teams)
87
+ readProvisioningProfiles(teams)
88
+
89
+ if (lastSelected) {
90
+ console.log(lastSelected)
91
+ process.exit(0)
92
+ }
93
+
94
+ if (teams.size === 1) {
95
+ console.log([...teams.keys()][0])
96
+ process.exit(0)
97
+ }
98
+
99
+ if (teams.size > 1) {
100
+ console.error('Multiple Xcode development teams found:')
101
+ for (const team of teams.values()) {
102
+ const label = team.name ? `${team.id} (${team.name})` : team.id
103
+ console.error(` ${label}`)
104
+ }
105
+ console.error('Set GEA_IOS_DEVELOPMENT_TEAM to choose one.')
106
+ process.exit(2)
107
+ }
108
+
109
+ process.exit(1)
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ // Per-resident-app entry.cpp generator for the iOS in-process app launcher.
3
+
4
+ import { writeFileSync } from 'node:fs'
5
+
6
+ const [, , prefix, outPath] = process.argv
7
+ if (!prefix || !outPath) {
8
+ console.error('Usage: generate-resident-entry.mjs <prefix> <out-cpp>')
9
+ process.exit(1)
10
+ }
11
+
12
+ const code = `// Auto-generated by targets/ios/generate-resident-entry.mjs. Do not edit.
13
+ #include "app.h"
14
+ #include "gea/embedded.h"
15
+ #include "graphics/font.h"
16
+ #include "host/rtc.h"
17
+ #include "host/timers.h"
18
+ #include "host/websocket.h"
19
+ #include "ui/document.h"
20
+ #include "ui/style.h"
21
+
22
+ extern void ${prefix}_top_level();
23
+ extern void ${prefix}_register_styles();
24
+ extern void gea_cpp_clear_microtasks();
25
+
26
+ namespace gea::framework::app::generated::${prefix} {
27
+ void drainMicrotasks();
28
+ } // namespace gea::framework::app::generated::${prefix}
29
+
30
+ void __attribute__((weak)) ${prefix}_register_styles() {}
31
+
32
+ void ${prefix}_init(int width, int height, double devicePixelRatio)
33
+ {
34
+ \tgea::embedded::ui::Document::setPreferredMountSize(width, height);
35
+ \tgea::embedded::ui::setViewportMetrics(width, height, devicePixelRatio);
36
+ \tgea::host::resetAnimationFrameCallbacks();
37
+ \tgea::host::resetScheduledTimers();
38
+ \tgea_cpp_clear_microtasks();
39
+ \tgea::embedded::ui::StyleSheet::instance().clear();
40
+ \t${prefix}_register_styles();
41
+ \tgea::embedded::ui::Document::instance().clear();
42
+ \t${prefix}_top_level();
43
+ \tgea::embedded::ui::Document::instance().refreshMountedIfDirty();
44
+ }
45
+
46
+ void ${prefix}_frame(int timestampMs)
47
+ {
48
+ \tgea::framework::app::generated::${prefix}::drainMicrotasks();
49
+ \tgea::host::runAnimationFrameCallbacks(static_cast<double>(timestampMs));
50
+ \tgea::framework::app::generated::${prefix}::drainMicrotasks();
51
+ \tgea::host::websocket::runCallbacks();
52
+ \tgea::host::rtc::runCallbacks();
53
+ \tgea::embedded::ui::Document::instance().frame(timestampMs);
54
+ \tgea::embedded::ui::Document::instance().refreshMountedIfDirty();
55
+ }
56
+
57
+ void ${prefix}_settings_toggle() {}
58
+ int ${prefix}_mirror_begin_snapshot() { return 0; }
59
+ int ${prefix}_mirror_begin_diff() { return 0; }
60
+ int ${prefix}_mirror_next_record(unsigned char *, int) { return 0; }
61
+ void ${prefix}_mirror_clear_dirty() {}
62
+ void ${prefix}_ble_connected() {}
63
+ void ${prefix}_ble_disconnected() {}
64
+ void ${prefix}_ble_bound() {}
65
+ `
66
+
67
+ writeFileSync(outPath, code)