@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,2156 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ MACOS_TIMINGS="${GEA_MACOS_TIMINGS:-0}"
5
+ MACOS_TIMING_START_US=0
6
+ MACOS_TIMING_LAST_US=0
7
+ timing_now_us() {
8
+ perl -MTime::HiRes=time -e 'printf "%.0f", time() * 1000000'
9
+ }
10
+ timing_mark() {
11
+ [[ "$MACOS_TIMINGS" == "1" ]] || return 0
12
+ local label="$1"
13
+ local now delta total
14
+ now="$(timing_now_us)"
15
+ delta=$((now - MACOS_TIMING_LAST_US))
16
+ total=$((now - MACOS_TIMING_START_US))
17
+ printf '[macos timing] %-18s %d.%03ds (total %d.%03ds)\n' \
18
+ "$label" $((delta / 1000000)) $(((delta / 1000) % 1000)) \
19
+ $((total / 1000000)) $(((total / 1000) % 1000)) >&2
20
+ MACOS_TIMING_LAST_US="$now"
21
+ }
22
+ if [[ "$MACOS_TIMINGS" == "1" ]]; then
23
+ MACOS_TIMING_START_US="$(timing_now_us)"
24
+ MACOS_TIMING_LAST_US="$MACOS_TIMING_START_US"
25
+ fi
26
+
27
+ MACOS_PIPELINE_CACHE="${GEA_MACOS_PIPELINE_CACHE:-1}"
28
+ case "$MACOS_PIPELINE_CACHE" in
29
+ 0|1) ;;
30
+ *) echo "GEA_MACOS_PIPELINE_CACHE must be 0 or 1" >&2; exit 1 ;;
31
+ esac
32
+
33
+ replace_content_stable() {
34
+ local destination="$1"
35
+ local temporary="$2"
36
+ if [[ "$MACOS_PIPELINE_CACHE" == "1" && -f "$destination" ]] && cmp -s "$temporary" "$destination"; then
37
+ rm -f "$temporary"
38
+ return 1
39
+ fi
40
+ mkdir -p "$(dirname "$destination")"
41
+ mv "$temporary" "$destination"
42
+ return 0
43
+ }
44
+
45
+ configure_macos_output_namespace() {
46
+ local app_id="$1"
47
+ local output_tag="$2"
48
+ if [[ -z "$output_tag" ]]; then
49
+ MACOS_OUTPUT_SUBPATH="$app_id"
50
+ MACOS_BUILD_LABEL="$app_id"
51
+ return 0
52
+ fi
53
+ if [[ ! "$output_tag" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || (( ${#output_tag} > 64 )); then
54
+ echo "Invalid macOS output tag '$output_tag' (use 1-64 characters: leading alphanumeric, then alphanumeric/._-)" >&2
55
+ return 1
56
+ fi
57
+ # Keep app ids as siblings inside a tag. macos_app_platform.mm walks from
58
+ # <namespace>/<app>/<Name>.app to <namespace> and appends the target app id.
59
+ MACOS_OUTPUT_SUBPATH=".namespaces/$output_tag/$app_id"
60
+ MACOS_BUILD_LABEL="$app_id@$output_tag"
61
+ }
62
+
63
+ ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
64
+ BUILD_INVOCATION_CWD="$(pwd -P)"
65
+ # This script IS @geastack/apple, so the generator must bind against this
66
+ # working copy rather than whatever installed copy it would resolve from the app.
67
+ export GEA_APPLE_ROOT="$ROOT_DIR"
68
+ MACOS_HARD_MUTE_GATE="${GEA_MACOS_HARD_MUTE_GATE:-}"
69
+ MACOS_HARD_MUTE_SOURCE="${GEA_MACOS_HARD_MUTE_SOURCE:-}"
70
+ if [[ -n "$MACOS_HARD_MUTE_GATE" && -z "$MACOS_HARD_MUTE_SOURCE" ]] || \
71
+ [[ -z "$MACOS_HARD_MUTE_GATE" && -n "$MACOS_HARD_MUTE_SOURCE" ]]; then
72
+ echo "GEA_MACOS_HARD_MUTE_GATE and GEA_MACOS_HARD_MUTE_SOURCE must be set together" >&2
73
+ exit 2
74
+ fi
75
+ APP_ID="hello"
76
+ if (( $# > 0 )) && [[ "$1" != --* ]]; then
77
+ APP_ID="$1"
78
+ shift
79
+ fi
80
+ MACOS_OUTPUT_TAG="${GEA_MACOS_OUTPUT_TAG:-}"
81
+ while (( $# > 0 )); do
82
+ case "$1" in
83
+ --output-tag)
84
+ if (( $# < 2 )) || [[ -z "$2" ]]; then
85
+ echo "--output-tag requires a non-empty value" >&2
86
+ exit 2
87
+ fi
88
+ MACOS_OUTPUT_TAG="$2"
89
+ shift 2
90
+ ;;
91
+ --output-tag=*)
92
+ MACOS_OUTPUT_TAG="${1#--output-tag=}"
93
+ if [[ -z "$MACOS_OUTPUT_TAG" ]]; then
94
+ echo "--output-tag requires a non-empty value" >&2
95
+ exit 2
96
+ fi
97
+ shift
98
+ ;;
99
+ *)
100
+ echo "Unknown macOS build argument: $1" >&2
101
+ exit 2
102
+ ;;
103
+ esac
104
+ done
105
+ MACOS_OUTPUT_SUBPATH=""
106
+ MACOS_BUILD_LABEL=""
107
+ configure_macos_output_namespace "$APP_ID" "$MACOS_OUTPUT_TAG" || exit 2
108
+ # shellcheck source=/dev/null
109
+ source "$ROOT_DIR/targets/macos/build-child-lifecycle.sh"
110
+
111
+ # Resolve the gea framework through npm plus the shared manifest. `apple` is a
112
+ # native npm package whose @geastack/* deps npm installs into node_modules.
113
+ # Framework C/C++ sources + include roots come from @geastack/core/gea_sources.sh
114
+ # (shared with web/esp32).
115
+ # Walk the node_modules chain rather than looking one level down. This target
116
+ # ships inside @geastack/apple, so when an app installs it the siblings it needs
117
+ # are hoisted to the APP's node_modules, one or more levels above this package,
118
+ # and a linked checkout has them under its own instead. One resolver answers
119
+ # both without a layout assumption.
120
+ # Two starting points, the APP first. ROOT_DIR is this script's own location
121
+ # with every symlink already resolved by `cd`, so an app that depends on
122
+ # @geastack/apple through a LINK -- a checkout of this repository beside the
123
+ # app, which is how a three.js app installs it -- sends the walk back into this
124
+ # repository, where the sibling packages are not under any node_modules and
125
+ # the chain up to the app's own is gone. The invocation directory IS the app
126
+ # (`gea build` spawns this script with the app root as cwd), and that is where
127
+ # npm put the siblings, so ask there first and keep ROOT_DIR for an app that
128
+ # genuinely contains this package.
129
+ resolve_gea_package_from() {
130
+ node -e '
131
+ const fs = require("node:fs")
132
+ const path = require("node:path")
133
+ let dir = process.argv[1]
134
+ while (true) {
135
+ const candidate = path.join(dir, "node_modules", process.argv[2])
136
+ if (fs.existsSync(path.join(candidate, "package.json"))) {
137
+ process.stdout.write(fs.realpathSync(candidate))
138
+ break
139
+ }
140
+ const parent = path.dirname(dir)
141
+ if (parent === dir) break
142
+ dir = parent
143
+ }
144
+ ' "$1" "$2" 2>/dev/null || true
145
+ }
146
+ resolve_gea_package() {
147
+ local start found
148
+ for start in "${2:-$BUILD_INVOCATION_CWD}" "${2:-$ROOT_DIR}"; do
149
+ found="$(resolve_gea_package_from "$start" "$1")"
150
+ if [[ -n "$found" ]]; then
151
+ printf '%s' "$found"
152
+ return 0
153
+ fi
154
+ done
155
+ }
156
+ GEA_CORE="$(resolve_gea_package @geastack/core)"
157
+ GEA_APPLE_NATIVE_PLUGIN="$(resolve_gea_package @geastack/geatsc-plugin-apple-native)"
158
+ # An app that installs @geastack/apple gets the plugin from node_modules; inside
159
+ # this repository it is a sibling package that nothing links, so name it there.
160
+ if [[ -z "$GEA_APPLE_NATIVE_PLUGIN" && -d "$ROOT_DIR/../geatsc-plugin-apple-native" ]]; then
161
+ GEA_APPLE_NATIVE_PLUGIN="$(cd "$ROOT_DIR/../geatsc-plugin-apple-native" && pwd)"
162
+ fi
163
+ [ -n "$GEA_APPLE_NATIVE_PLUGIN" ] && [ -f "$GEA_APPLE_NATIVE_PLUGIN/dist/index.js" ] || { echo "Cannot resolve @geastack/geatsc-plugin-apple-native" >&2; exit 1; }
164
+ GEA_COMPILER="$(resolve_gea_package @geastack/compiler)"
165
+ GEA_PLUGIN="$(resolve_gea_package @geastack/geatsc-plugin-gea)"
166
+ [ -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; }
167
+ GEA_HOST_DIR="${GEA_HOST_DIR:-$GEA_CORE/../host}"
168
+ GEA_ENGINE_DIR="${GEA_ENGINE_DIR:-$GEA_CORE/../engine}"
169
+ GEA_ELEMENTS_DIR="${GEA_ELEMENTS_DIR:-$GEA_CORE/../elements}"
170
+ GEA_GEAOS_PACKAGE_DIR="${GEA_GEAOS_PACKAGE_DIR:-$GEA_CORE/../geaos}"
171
+ GEA_ENGINE="$GEA_ENGINE_DIR"
172
+ GEA_GEAOS="$GEA_GEAOS_PACKAGE_DIR"
173
+ # The source manifest reads these from the ENVIRONMENT: gea_sources.sh is now a
174
+ # front end for gea_sources.mjs, which CMake has to read on Windows where there
175
+ # is no bash, so it runs as a child process. A plain shell variable never
176
+ # reaches it, and the include roots come back empty -- every framework header
177
+ # (app.h, display.h, pixel.h) then goes missing at the first compile.
178
+ export GEA_CORE GEA_HOST_DIR GEA_ENGINE_DIR GEA_ELEMENTS_DIR GEA_GEAOS_PACKAGE_DIR
179
+ # Shared component CSS (and the @font-face files it references). This was one
180
+ # `lib/gea-embedded/components` tree before the split; each package that ships
181
+ # components now carries its own, so both have to be named or the bundle
182
+ # silently loses whichever is missed -- the walk below and sync-resources both
183
+ # skip a directory that is not there without saying so.
184
+ GEA_SHARED_COMPONENT_DIRS=("$GEA_ENGINE_DIR/components" "$GEA_ELEMENTS_DIR/components")
185
+ # The project is the directory this script was started in. `gea build` spawns it
186
+ # with the app's own folder as cwd, and a direct run is made from the app folder
187
+ # too, so there is nothing to pass and nothing to guess: BUILD_INVOCATION_CWD
188
+ # (captured above, before anything can move) is the whole answer. Every nested
189
+ # `gea` call inherits the same cwd and resolves the same project, which is why
190
+ # none of them carry --project.
191
+ # shellcheck source=/dev/null
192
+ source "$GEA_CORE/gea_sources.sh"
193
+ # shellcheck source=/dev/null
194
+ source "$ROOT_DIR/targets/macos/heavy-build-lock.sh"
195
+ # Prefer an explicit override, then the local package, then gea on PATH.
196
+ GEA_CLI="${GEA_CLI_BIN:-}"
197
+ if [[ -z "$GEA_CLI" ]]; then
198
+ GEA_CLI="$(node -e 'process.stdout.write(require("path").join(process.argv[1], "bin", "gea.mjs"))' "$(resolve_gea_package @geastack/cli)" 2>/dev/null || true)"
199
+ if [[ ! -f "$GEA_CLI" ]]; then
200
+ GEA_CLI="$(command -v gea || true)"
201
+ fi
202
+ fi
203
+ [ -n "$GEA_CLI" ] && [ -f "$GEA_CLI" ] || { echo "Cannot locate GeaStack CLI — set GEA_CLI_BIN to gea.mjs, install @geastack/cli locally, or add gea to PATH" >&2; exit 1; }
204
+ # Opt-in benchmark serialization across concurrent builds. It belongs beside the
205
+ # apps being built, not beside this package -- installed under node_modules, a
206
+ # package-relative path would put every project's lock in a different tree.
207
+ HEAVY_BUILD_LOCK_PATH="${GEA_HEAVY_BUILD_LOCK_PATH:-$BUILD_INVOCATION_CWD/.gea-heavy-build.lock}"
208
+ case "$HEAVY_BUILD_LOCK_PATH" in
209
+ /*) ;;
210
+ *) HEAVY_BUILD_LOCK_PATH="$(pwd)/$HEAVY_BUILD_LOCK_PATH" ;;
211
+ esac
212
+
213
+ APP_META="$(node "$GEA_CLI" apps inspect "$APP_ID" --format shell 2>/dev/null || true)"
214
+ if [[ -n "$APP_META" ]]; then
215
+ IFS=$'\t' read -r _APP_ROOT _APP_ENTRY _APP_RUNTIME APP_NAME < <(printf "%s\n" "$APP_META")
216
+ else
217
+ APP_NAME="$(echo "$APP_ID" | awk '{print toupper(substr($0,1,1)) substr($0,2)}')"
218
+ fi
219
+ APP_META_CACHE_IDS=("$APP_ID")
220
+ APP_META_CACHE_VALUES=("$APP_META")
221
+ RESOLVED_APP_META="$APP_META"
222
+
223
+ # Build output belongs to the PROJECT being built, never to this package.
224
+ # ROOT_DIR is @geastack/apple's own directory, which for every app that
225
+ # installs the package is inside its node_modules -- so writing the bundle,
226
+ # the objects and the generated C++ there buried the build in a directory
227
+ # nobody opens, that npm wipes on the next install, and that several projects
228
+ # sharing one hoisted copy of the package would write to at the same time.
229
+ # The invocation directory is the app the CLI resolved (its own folder), so output
230
+ # lands next to the app's source as `dist/macos/<app-id>/<Name>.app`.
231
+ # GEA_MACOS_OUTPUT_DIR overrides the root for a caller that wants it elsewhere.
232
+ MACOS_OUTPUT_ROOT="${GEA_MACOS_OUTPUT_DIR:-$BUILD_INVOCATION_CWD/dist/macos}"
233
+ case "$MACOS_OUTPUT_ROOT" in
234
+ /*) ;;
235
+ *) MACOS_OUTPUT_ROOT="$BUILD_INVOCATION_CWD/$MACOS_OUTPUT_ROOT" ;;
236
+ esac
237
+ DIST_DIR="$MACOS_OUTPUT_ROOT/$MACOS_OUTPUT_SUBPATH"
238
+ BUILD_DIR="$DIST_DIR/build"
239
+ APP_BUNDLE="$DIST_DIR/${APP_NAME}.app"
240
+ MACOS_DIR="$APP_BUNDLE/Contents/MacOS"
241
+ RES_DIR="$APP_BUNDLE/Contents/Resources"
242
+ APP_EXEC="$APP_NAME"
243
+ SHARED_UI_DIR="$GEA_ENGINE/ui"
244
+ # Generated C++ is build output too, and a sibling of the bundles rather than
245
+ # a child: the runtime walks two directories up from a .app to find its
246
+ # namespace root and appends an app id, so a dot-prefixed sibling is invisible
247
+ # to that lookup while staying inside the one directory `rm -rf` clears.
248
+ GENERATED_DIR="$MACOS_OUTPUT_ROOT/.generated/$MACOS_OUTPUT_SUBPATH"
249
+
250
+ # Ninja avoids the per-object Bash/sed/tr dependency scan on incremental and
251
+ # no-op builds. Keep the original Shell scheduler as a portable fallback and
252
+ # as an explicit comparison backend. Resolve this before generation so a bad
253
+ # override fails without doing expensive work or touching build outputs.
254
+ MACOS_GENERATOR="${GEA_MACOS_GENERATOR:-}"
255
+ if [[ -z "$MACOS_GENERATOR" ]]; then
256
+ if command -v ninja >/dev/null 2>&1; then
257
+ MACOS_GENERATOR="Ninja"
258
+ else
259
+ MACOS_GENERATOR="Shell"
260
+ fi
261
+ fi
262
+ case "$MACOS_GENERATOR" in
263
+ Ninja)
264
+ if ! command -v ninja >/dev/null 2>&1; then
265
+ echo "GEA_MACOS_GENERATOR=Ninja requested, but ninja is not installed" >&2
266
+ exit 1
267
+ fi
268
+ ;;
269
+ Shell) ;;
270
+ *)
271
+ echo "GEA_MACOS_GENERATOR must be 'Ninja' or 'Shell' (got '$MACOS_GENERATOR')" >&2
272
+ exit 1
273
+ ;;
274
+ esac
275
+
276
+ # Generated sources, PCH files, dependency files, objects, and the final app
277
+ # bundle are all output-namespace-local shared state. Two builds of the same
278
+ # app and output tag must not update those trees concurrently; different tags
279
+ # intentionally receive disjoint generated/dist trees and locks. A generator
280
+ # can otherwise rewrite a runtime source after another invocation built its
281
+ # PCH, and clang then (correctly) rejects the now-stale PCH halfway through the
282
+ # build. `mkdir` is atomic on macOS and avoids depending on the non-system
283
+ # `flock` utility. A PID-owned
284
+ # directory also lets a later build recover a lock left by SIGKILL or a crashed
285
+ # terminal session.
286
+ mkdir -p "$DIST_DIR"
287
+ BUILD_LOCK_DIR="$DIST_DIR/.build.lock"
288
+ acquire_build_lock() {
289
+ local owner=""
290
+ local empty_owner_retries=0
291
+ while true; do
292
+ if mkdir "$BUILD_LOCK_DIR" 2>/dev/null; then
293
+ if ! printf '%s\n' "$$" > "$BUILD_LOCK_DIR/pid"; then
294
+ rmdir "$BUILD_LOCK_DIR" 2>/dev/null || true
295
+ echo "Cannot record ownership for macOS build lock: $BUILD_LOCK_DIR" >&2
296
+ return 1
297
+ fi
298
+ return 0
299
+ fi
300
+ # `mkdir` can also fail because the parent is missing/unwritable or because
301
+ # a non-directory occupies the path. Those are configuration errors, not
302
+ # lock contention; never turn them into an infinite retry loop.
303
+ if [[ ! -d "$BUILD_LOCK_DIR" ]]; then
304
+ echo "Cannot create macOS build lock: $BUILD_LOCK_DIR" >&2
305
+ return 1
306
+ fi
307
+ owner="$(cat "$BUILD_LOCK_DIR/pid" 2>/dev/null || true)"
308
+ if [[ "$owner" =~ ^[0-9]+$ ]] && kill -0 "$owner" 2>/dev/null; then
309
+ echo "macOS build of '$MACOS_BUILD_LABEL' is already running (pid $owner)" >&2
310
+ return 1
311
+ fi
312
+ # There is a very small interval between atomic mkdir and writing the pid.
313
+ # Give a new owner time to finish publishing instead of deleting its lock.
314
+ if [[ -z "$owner" && "$empty_owner_retries" -lt 20 ]]; then
315
+ empty_owner_retries=$((empty_owner_retries + 1))
316
+ sleep 0.05
317
+ continue
318
+ fi
319
+ rm -rf "$BUILD_LOCK_DIR"
320
+ empty_owner_retries=0
321
+ done
322
+ }
323
+
324
+ release_build_lock() {
325
+ local owner=""
326
+ owner="$(cat "$BUILD_LOCK_DIR/pid" 2>/dev/null || true)"
327
+ if [[ "$owner" == "$$" ]]; then
328
+ rm -rf "$BUILD_LOCK_DIR"
329
+ fi
330
+ }
331
+
332
+ cleanup_build_locks() {
333
+ local status="$?"
334
+ trap - EXIT HUP INT TERM
335
+ # No writer may outlive the same-output-namespace correctness lock. Terminate
336
+ # and reap the exact generator/Ninja/compiler trees owned by this invocation
337
+ # first, then release locks in reverse acquisition order.
338
+ macos_terminate_and_reap_build_children
339
+ release_build_lock
340
+ gea_release_heavy_build_lock
341
+ return "$status"
342
+ }
343
+
344
+ handle_build_interrupt() {
345
+ local status="$1"
346
+ trap - HUP INT TERM
347
+ exit "$status"
348
+ }
349
+
350
+ # Normal builds remain concurrent because the shared helper is opt-in. When
351
+ # benchmark serialization is requested, acquire it before the namespace-local
352
+ # correctness lock. Duplicate builds of one app+tag remain impossible either way.
353
+ gea_acquire_heavy_build_lock "$HEAVY_BUILD_LOCK_PATH" macos "$MACOS_BUILD_LABEL"
354
+ trap cleanup_build_locks EXIT
355
+ trap 'handle_build_interrupt 129' HUP
356
+ trap 'handle_build_interrupt 130' INT
357
+ trap 'handle_build_interrupt 143' TERM
358
+ acquire_build_lock
359
+
360
+ # Preserve the executable and resource outputs so a true no-op can skip linking,
361
+ # icon conversion, resource copying, and codesigning. The resource synchronizer
362
+ # below owns stale-file removal for its font/config outputs. The cache-off mode
363
+ # intentionally retains the old clean-and-restage behavior for A/B benchmarks.
364
+ if [[ "$MACOS_PIPELINE_CACHE" == "0" ]]; then rm -rf "$RES_DIR"; fi
365
+ mkdir -p "$MACOS_DIR" "$RES_DIR" "$BUILD_DIR"
366
+ BUNDLE_CONTENT_CHANGED=0
367
+
368
+ INFO_PLIST_TMP="$BUILD_DIR/Info.plist.tmp.$$"
369
+ sed -e "s/@APP_EXEC@/$APP_EXEC/g" \
370
+ -e "s/@APP_ID@/$APP_ID/g" \
371
+ -e "s/@APP_NAME@/$APP_NAME/g" \
372
+ "$ROOT_DIR/targets/macos/Info.plist.in" > "$INFO_PLIST_TMP"
373
+ if replace_content_stable "$APP_BUNDLE/Contents/Info.plist" "$INFO_PLIST_TMP"; then
374
+ BUNDLE_CONTENT_CHANGED=1
375
+ fi
376
+ timing_mark bootstrap
377
+
378
+ # Resolve package metadata once per app. Bash 3.2 has no associative arrays, so
379
+ # keep parallel indexed arrays and publish the result through a global instead
380
+ # of command substitution (which would update the cache in a subshell).
381
+ resolve_app_meta() {
382
+ local id="$1"
383
+ local index
384
+ RESOLVED_APP_META=""
385
+ for ((index = 0; index < ${#APP_META_CACHE_IDS[@]}; index++)); do
386
+ if [[ "${APP_META_CACHE_IDS[$index]}" == "$id" ]]; then
387
+ RESOLVED_APP_META="${APP_META_CACHE_VALUES[$index]}"
388
+ return 0
389
+ fi
390
+ done
391
+ RESOLVED_APP_META="$(node "$GEA_CLI" apps inspect "$id" --format shell 2>/dev/null || true)"
392
+ APP_META_CACHE_IDS+=("$id")
393
+ APP_META_CACHE_VALUES+=("$RESOLVED_APP_META")
394
+ }
395
+
396
+ # Resolve an app's source directory from package.json `gea` metadata. By the
397
+ # time later bundle/resource stages call this, generate_app has populated the
398
+ # parent-shell cache for every resident.
399
+ resolve_app_dir() {
400
+ local id="$1"
401
+ local root _entry _runtime _name
402
+ resolve_app_meta "$id"
403
+ if [[ -z "$RESOLVED_APP_META" ]]; then echo ""; return; fi
404
+ IFS=$'\t' read -r root _entry _runtime _name < <(printf "%s\n" "$RESOLVED_APP_META")
405
+ echo "$root"
406
+ }
407
+
408
+ # Cheap read-only equivalent of the post-generation finalizer for no-op cache
409
+ # validation. Avoiding a Node process per resident saves meaningful traversal
410
+ # time while preserving the same ownership invariants.
411
+ apple_native_cached_output_is_current() {
412
+ local outDir="$1"
413
+ local expectedState="$2"
414
+ local supportPath="$outDir/generated_support.hpp"
415
+ local metadataPath="$outDir/gea-apple-metadata.json"
416
+ local bridgeDir="$outDir/gea/apple"
417
+ local bridgeHeader="$bridgeDir/native_bridge.h"
418
+ local bridgeSource="$bridgeDir/native_bridge.mm"
419
+ local bridgeInclude='#include "gea/apple/native_bridge.h"'
420
+ local state="0"
421
+ case "$expectedState" in 0|1) ;; *) return 1 ;; esac
422
+ [[ -f "$supportPath" && -s "$supportPath" ]] || return 1
423
+ if grep -Fq "$bridgeInclude" "$supportPath"; then
424
+ [[ -f "$bridgeHeader" && -s "$bridgeHeader" ]] || return 1
425
+ [[ -f "$bridgeSource" && -s "$bridgeSource" ]] || return 1
426
+ state="1"
427
+ elif [[ -e "$metadataPath" || -L "$metadataPath" || -e "$bridgeDir" || -L "$bridgeDir" ]]; then
428
+ return 1
429
+ fi
430
+ [[ "$state" == "$expectedState" ]]
431
+ }
432
+
433
+ find_generation_input_newer_than() {
434
+ local reference="$1"
435
+ local appDir="$2"
436
+ shift 2
437
+ local found=""
438
+ local referenceEpoch=""
439
+ local nowEpoch=""
440
+ referenceEpoch="$(stat -f %m "$reference" 2>/dev/null || stat -c %Y "$reference" 2>/dev/null || true)"
441
+ nowEpoch="$(date +%s)"
442
+ # A future-dated sentinel makes every real input appear older indefinitely
443
+ # (clock adjustment, restored build artifact, or a manual `touch -t`). Treat
444
+ # that as stale state instead of silently compiling old generated C++.
445
+ if [[ "$referenceEpoch" =~ ^[0-9]+$ ]] && (( referenceEpoch > nowEpoch + 5 )); then
446
+ echo "clock-skew:$reference"
447
+ return
448
+ fi
449
+ found=$(find "$appDir" \( -name '*.tsx' -o -name '*.ts' -o -name '*.jsx' -o -name '*.js' -o -name '*.mjs' -o -name '*.css' -o -name '*.json' \) \
450
+ -not -path '*/node_modules/*' -not -path '*/dist/*' \
451
+ -newer "$reference" -print -quit 2>/dev/null)
452
+ if [[ -n "$found" ]]; then
453
+ echo "$found"
454
+ return
455
+ fi
456
+
457
+ local inputs=(
458
+ "$GEA_CORE/scripts/build-gea-vite-geatsc.mjs"
459
+ "$GEA_CORE/scripts/gea-bundle-type-hints.mjs"
460
+ "$GEA_CORE/scripts/gea-embedded-compat-transform.mjs"
461
+ "$GEA_CORE/scripts/generate-gea-embedded-assets.mjs"
462
+ "$GEA_CORE/scripts/generate-gea-embedded-fonts.mjs"
463
+ "$ROOT_DIR/targets/macos/check-module-graph-freshness.mjs"
464
+ "$GEA_PLUGIN/dist"
465
+ "$GEA_APPLE_NATIVE_PLUGIN/dist"
466
+ "$ROOT_DIR/dist"
467
+ "$ROOT_DIR/runtime"
468
+ "$GEA_CORE/runtime.ts"
469
+ "$GEA_CORE/components"
470
+ )
471
+ local extraInput
472
+ for extraInput in "$@"; do
473
+ inputs+=("$extraInput")
474
+ done
475
+ local input
476
+ for input in "${inputs[@]}"; do
477
+ [[ -e "$input" ]] || continue
478
+ if [[ -f "$input" ]]; then
479
+ if [[ "$input" -nt "$reference" ]]; then
480
+ echo "$input"
481
+ return
482
+ fi
483
+ continue
484
+ fi
485
+ found=$(find "$input" \( -name '*.tsx' -o -name '*.ts' -o -name '*.jsx' -o -name '*.js' -o -name '*.mjs' -o -name '*.css' -o -name '*.json' -o -name '*.cpp' -o -name '*.h' \) \
486
+ -not -path '*/node_modules/*' \
487
+ -newer "$reference" -print -quit 2>/dev/null)
488
+ if [[ -n "$found" ]]; then
489
+ echo "$found"
490
+ return
491
+ fi
492
+ done
493
+ }
494
+
495
+ tree_fingerprint() {
496
+ local root="$1"
497
+ node - "$root" <<'NODE'
498
+ const crypto = require('node:crypto')
499
+ const fs = require('node:fs')
500
+ const path = require('node:path')
501
+ const root = process.argv[2]
502
+ const files = []
503
+ const walk = (dir) => {
504
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
505
+ const full = path.join(dir, entry.name)
506
+ if (entry.isDirectory()) walk(full)
507
+ else if (entry.isFile()) files.push(full)
508
+ }
509
+ }
510
+ walk(root)
511
+ files.sort()
512
+ const hash = crypto.createHash('sha256')
513
+ for (const file of files) {
514
+ hash.update(path.relative(root, file))
515
+ hash.update('\0')
516
+ hash.update(fs.readFileSync(file))
517
+ hash.update('\0')
518
+ }
519
+ process.stdout.write(hash.digest('hex'))
520
+ NODE
521
+ }
522
+
523
+ # Run the JSX → C++ pipeline for one app, optionally with a unique entry-
524
+ # symbol prefix (resident-app build).
525
+ generate_app() {
526
+ local id="$1"
527
+ local outDir="$2"
528
+ local prefix="$3" # empty = no prefix (single-app mode, top-level)
529
+ local appMeta appRoot _appName
530
+ local appDir=""
531
+ local entry="index.tsx"
532
+ local appRuntime=""
533
+ resolve_app_meta "$id"
534
+ appMeta="$RESOLVED_APP_META"
535
+ if [[ -n "$appMeta" ]]; then
536
+ IFS=$'\t' read -r appRoot entry appRuntime _appName < <(printf "%s\n" "$appMeta")
537
+ appDir="$appRoot"
538
+ fi
539
+ if [[ -z "$appDir" || ! -f "$appDir/$entry" ]]; then
540
+ echo "Skipping '$id' — no $entry under $appDir" >&2
541
+ return 1
542
+ fi
543
+ local extraArgs=()
544
+ if [[ -n "$prefix" ]]; then
545
+ extraArgs=(
546
+ --entry-symbol "${prefix}_top_level"
547
+ --cpp-prelude-symbol "${prefix}_register_styles"
548
+ --font-symbol-prefix "$prefix"
549
+ --isolate-symbols
550
+ )
551
+ fi
552
+ if [[ "${GEA_VITE_MODULE_GRAPH:-0}" == "1" || ( "$id" == "three-angle-metal" && "${GEA_THREE_MODULE_GRAPH:-1}" != "0" ) ]]; then
553
+ extraArgs+=(--module-graph-out "$outDir/module-graph")
554
+ fi
555
+ # One generated C++ unit per source module instead of one for the whole app.
556
+ # `read_geatsc_sources` already compiles every line of geatsc-sources.txt, so
557
+ # this only changes how many lines there are; see the compiler's
558
+ # docs/TRANSLATION-UNITS.md for the measured trade.
559
+ if [[ "${GEA_PER_FILE_UNITS:-0}" == "1" ]]; then
560
+ extraArgs+=(--per-file-units)
561
+ fi
562
+ if [[ "$id" == "three-angle-metal" && "${GEA_THREE_MODULE_GRAPH_ONLY:-0}" == "1" ]]; then
563
+ extraArgs+=(--module-graph-only)
564
+ fi
565
+ if [[ "$id" == "three-angle-metal" && "${GEA_THREE_MODULE_GRAPH_COMPILE:-1}" != "0" ]]; then
566
+ extraArgs+=(--compile-module-graph --allow-any)
567
+ fi
568
+ # `gea build` resolves which compiler plugins this app's packages ship and
569
+ # names them here, so no app needs a stub file re-exporting one. The appDir
570
+ # fallback is for a direct invocation of this script, not a decision.
571
+ if [[ -n "${GEA_EXTRA_GEATSC_PLUGINS:-}" ]]; then
572
+ local pluginPath
573
+ while IFS= read -r pluginPath; do
574
+ [[ -n "$pluginPath" ]] && extraArgs+=(--extra-geatsc-plugin "$pluginPath")
575
+ done <<< "${GEA_EXTRA_GEATSC_PLUGINS//:/$'\n'}"
576
+ elif [[ -f "$appDir/geatsc-plugin.mjs" ]]; then
577
+ extraArgs+=(--extra-geatsc-plugin "$appDir/geatsc-plugin.mjs")
578
+ fi
579
+ if [[ "$appRuntime" == "apple-native" ]]; then
580
+ extraArgs+=(--apple-native)
581
+ fi
582
+ local envSignatureFile="$outDir/.gea-build-env"
583
+ local compilerFingerprint
584
+ compilerFingerprint="$(node "$ROOT_DIR/targets/macos/compiler-input-fingerprint.mjs" "$GEA_COMPILER")"
585
+ local envSignature="GEA_THREE_USE_UPSTREAM=${GEA_THREE_USE_UPSTREAM:-}
586
+ GEA_THREE_REFERENCE_DEMO=${GEA_THREE_REFERENCE_DEMO:-}
587
+ GEA_THREE_MODULE_GRAPH=${GEA_THREE_MODULE_GRAPH:-1}
588
+ GEA_VITE_MODULE_GRAPH=${GEA_VITE_MODULE_GRAPH:-0}
589
+ GEA_THREE_MODULE_GRAPH_ONLY=${GEA_THREE_MODULE_GRAPH_ONLY:-0}
590
+ GEA_THREE_MODULE_GRAPH_COMPILE=${GEA_THREE_MODULE_GRAPH_COMPILE:-1}
591
+ GEA_PER_FILE_UNITS=${GEA_PER_FILE_UNITS:-0}
592
+ GEA_CPP_TRANSLATION_UNITS=${GEA_CPP_TRANSLATION_UNITS:-}
593
+ GEA_WEBGL_AUTO_INSTANCE=${GEA_WEBGL_AUTO_INSTANCE:-}
594
+ GEA_WEBGL_AUTO_INSTANCE_TEST=${GEA_WEBGL_AUTO_INSTANCE_TEST:-}
595
+ GEA_COMPILER_FINGERPRINT=${compilerFingerprint}"
596
+ # Only when an alternate compiler was named, so the default signature -- and
597
+ # therefore every existing generated directory's freshness -- is unchanged.
598
+ if [[ -n "${GEA_GEATSC_BIN:-}" ]]; then
599
+ envSignature="$envSignature
600
+ GEA_GEATSC_FINGERPRINT=$(tree_fingerprint "$(dirname "$GEA_GEATSC_BIN")")"
601
+ fi
602
+ local newer=""
603
+ local generatedNow=0
604
+ local sourceList="$outDir/geatsc-sources.txt"
605
+ local generationSentinel="$outDir/.gea-generation.stamp"
606
+ local appleStateFile="$outDir/.gea-current-apple-native"
607
+ local moduleGraphFile="$outDir/module-graph/gea-module-graph.json"
608
+ if [[ -f "$sourceList" && -f "$generationSentinel" ]]; then
609
+ local generationInputDirs=()
610
+ # Every plugin that feeds generation is a generation input: an edit to one
611
+ # must invalidate the generated C++. The paths come from the same list the
612
+ # compiler is given, so nothing here has to guess which packages ship one.
613
+ if [[ -n "${GEA_EXTRA_GEATSC_PLUGINS:-}" ]]; then
614
+ local freshnessPluginPath
615
+ while IFS= read -r freshnessPluginPath; do
616
+ [[ -n "$freshnessPluginPath" ]] && generationInputDirs+=("$(dirname "$freshnessPluginPath")")
617
+ done <<< "${GEA_EXTRA_GEATSC_PLUGINS//:/$'\n'}"
618
+ fi
619
+ newer="$(find_generation_input_newer_than "$generationSentinel" "$appDir" ${generationInputDirs[@]+"${generationInputDirs[@]}"})"
620
+ if [[ -z "$newer" && -f "$moduleGraphFile" ]]; then
621
+ newer="$(node "$ROOT_DIR/targets/macos/check-module-graph-freshness.mjs" \
622
+ --graph "$moduleGraphFile" --reference "$generationSentinel")"
623
+ fi
624
+ if [[ ! -f "$envSignatureFile" ]] || [[ "$(cat "$envSignatureFile")" != "$envSignature" ]]; then
625
+ newer="${newer:-env:three-scene-source}"
626
+ fi
627
+ if [[ ! -f "$appleStateFile" ]]; then
628
+ newer="${newer:-env:apple-native-output-ownership}"
629
+ else
630
+ local cachedAppleState
631
+ cachedAppleState="$(cat "$appleStateFile" 2>/dev/null || true)"
632
+ if [[ "$appRuntime" == "apple-native" && "$cachedAppleState" != "1" ]]; then
633
+ newer="${newer:-env:apple-native-runtime-state}"
634
+ elif ! apple_native_cached_output_is_current "$outDir" "$cachedAppleState"; then
635
+ newer="${newer:-env:apple-native-output-inconsistent}"
636
+ fi
637
+ fi
638
+ if [[ "$id" == "three-angle-metal" && "${GEA_THREE_MODULE_GRAPH_ONLY:-0}" == "1" && ! -f "$outDir/module-graph/gea-module-graph.json" ]]; then
639
+ newer="${newer:-env:three-module-graph-only}"
640
+ fi
641
+ fi
642
+ if [[ ! -f "$sourceList" || ! -f "$generationSentinel" || -n "$newer" ]]; then
643
+ echo "Generating C++ for app '$id' from $appDir (entry $entry)..."
644
+ mkdir -p "$outDir"
645
+ if ! macos_run_tracked_child node "$GEA_CORE/scripts/build-gea-vite-geatsc.mjs" \
646
+ --app-dir "$appDir" \
647
+ --entry "$entry" \
648
+ --out-dir "$outDir" \
649
+ --geatsc-bin "${GEA_GEATSC_BIN:-$GEA_COMPILER/dist/cli.js}" \
650
+ --geatsc-gea-plugin "$GEA_PLUGIN/dist/index.js" \
651
+ --apple-platform macos \
652
+ --geatsc-apple-native-plugin "$GEA_APPLE_NATIVE_PLUGIN/dist/index.js" \
653
+ "${extraArgs[@]+"${extraArgs[@]}"}"; then
654
+ if [[ "$id" == "three-angle-metal" && "${GEA_THREE_MODULE_GRAPH_ONLY:-0}" == "1" ]]; then
655
+ echo "Module graph generation failed for '$id'; native generation state is unchanged" >&2
656
+ return 1
657
+ fi
658
+ rm -f "$generationSentinel" "$appleStateFile"
659
+ echo "C++ generation failed for '$id'; invalidated freshness state" >&2
660
+ return 1
661
+ fi
662
+ if [[ "$id" == "three-angle-metal" && "${GEA_THREE_MODULE_GRAPH_ONLY:-0}" == "1" ]]; then
663
+ echo "Generated module graph only for '$id' at $outDir/module-graph"
664
+ exit 0
665
+ fi
666
+ # Derive ownership from the transformed compiler output, not the metadata
667
+ # input consumed by the plugin. The finalizer also rejects an include whose
668
+ # bridge files are missing, so an inconsistent generation never reaches PCH
669
+ # or native compilation.
670
+ local appleState
671
+ local appleFinalizeArgs=(--out-dir "$outDir")
672
+ if [[ "$appRuntime" == "apple-native" ]]; then
673
+ appleFinalizeArgs+=(--expected-state 1)
674
+ fi
675
+ if ! appleState="$(node "$ROOT_DIR/targets/macos/finalize-apple-native-output.mjs" "${appleFinalizeArgs[@]}")"; then
676
+ rm -f "$generationSentinel" "$appleStateFile"
677
+ echo "C++ generation produced inconsistent Apple-native output for '$id'; invalidated freshness state" >&2
678
+ return 1
679
+ fi
680
+ local envSignatureTmp="$envSignatureFile.tmp.$$"
681
+ local appleStateTmp="$appleStateFile.tmp.$$"
682
+ printf "%s\n" "$envSignature" > "$envSignatureTmp"
683
+ printf "%s\n" "$appleState" > "$appleStateTmp"
684
+ mv "$envSignatureTmp" "$envSignatureFile"
685
+ mv "$appleStateTmp" "$appleStateFile"
686
+ touch "$generationSentinel"
687
+ generatedNow=1
688
+ fi
689
+ if [[ "$id" == "three-angle-metal" && "${GEA_THREE_MODULE_GRAPH_ONLY:-0}" == "1" ]]; then
690
+ echo "Module graph already fresh for '$id' at $outDir/module-graph"
691
+ exit 0
692
+ fi
693
+ prune_geatsc_modules "$outDir" "$generatedNow"
694
+ }
695
+
696
+ prune_geatsc_modules() {
697
+ local outDir="$1"
698
+ local generatedNow="${2:-0}"
699
+ local sourceList="$outDir/geatsc-sources.txt"
700
+ local pruneStamp="$outDir/.gea-module-prune.stamp"
701
+ [[ "${GEA_MACOS_PRUNE_GENERATED:-1}" != "0" ]] || return 0
702
+ [[ -f "$sourceList" && -d "$outDir/modules" ]] || return 0
703
+ if [[ "$generatedNow" != "1" && -f "$outDir/.gea-owned-modules.json" && -f "$pruneStamp" && ! "$sourceList" -nt "$pruneStamp" ]]; then
704
+ return 0
705
+ fi
706
+ local pruneArgs=(--out-dir "$outDir")
707
+ if [[ "${GEA_MACOS_PRUNE_DRY_RUN:-0}" == "1" ]]; then
708
+ pruneArgs+=(--dry-run)
709
+ fi
710
+ node "$ROOT_DIR/targets/macos/prune-generated-modules.mjs" "${pruneArgs[@]}" >/dev/null
711
+ if [[ "${GEA_MACOS_PRUNE_DRY_RUN:-0}" != "1" ]]; then touch "$pruneStamp"; fi
712
+ }
713
+
714
+ refresh_geatsc_runtime_assets() {
715
+ local outDir="$1"
716
+ local runtimeDir="$GEA_COMPILER/dist/targets/cpp/runtime"
717
+ [[ -d "$outDir" && -d "$runtimeDir" ]] || return 0
718
+ local runtimeAsset
719
+ # `.def` belongs in this set: geatsc's own materializer ships every
720
+ # `.cpp`/`.h`/`.def` in the runtime dir (compiler/packages/geatsc/src/targets/
721
+ # cpp/runtime.ts), and `runtime.cpp` `#include`s
722
+ # `ecmascript_primitive_capabilities.def`. Omitting it only worked while the
723
+ # emitting compiler also wrote the runtime parts; a compiler that emits just
724
+ # its own header (geatsc) leaves the `.def` missing and the runtime PCH
725
+ # fails with "'ecmascript_primitive_capabilities.def' file not found".
726
+ for runtimeAsset in "$runtimeDir"/*.cpp "$runtimeDir"/*.h "$runtimeDir"/*.def; do
727
+ [[ -f "$runtimeAsset" ]] || continue
728
+ local dest="$outDir/$(basename "$runtimeAsset")"
729
+ if [[ ! -f "$dest" ]] || ! cmp -s "$runtimeAsset" "$dest"; then
730
+ cp "$runtimeAsset" "$dest"
731
+ fi
732
+ done
733
+ }
734
+
735
+ # Decide whether to bundle resident apps. The launcher is special: it can
736
+ # `Apps.launch('foo')` which needs an in-process app swap. For every other
737
+ # id we build standalone.
738
+ RESIDENT_IDS=()
739
+ if [[ "$APP_ID" == "app-launcher" ]]; then
740
+ # Apps explicitly disabled for the macOS resident bundle. Currently
741
+ # `dialer` uses WebRTC integration patterns (heavy template SFINAE) that
742
+ # ESP-IDF's GCC accepts but Apple clang rejects — until the codegen is
743
+ # adjusted, the dialer tile builds standalone (build-macos.sh dialer)
744
+ # but is not embedded into app-launcher.app.
745
+ MACOS_EXCLUDE_REGEX='^(dialer)$'
746
+ RESIDENT_IDS=( $(node "$GEA_CLI" apps list --target geaos) )
747
+ ORDERED_RESIDENT_IDS=(app-launcher)
748
+ for id in "${RESIDENT_IDS[@]}"; do
749
+ [[ "$id" == "app-launcher" ]] && continue
750
+ ORDERED_RESIDENT_IDS+=("$id")
751
+ done
752
+ RESIDENT_IDS=("${ORDERED_RESIDENT_IDS[@]}")
753
+ FILTERED=()
754
+ for id in "${RESIDENT_IDS[@]}"; do
755
+ if [[ "$id" =~ $MACOS_EXCLUDE_REGEX ]]; then
756
+ echo "Excluding '$id' from app-launcher resident bundle (macOS toolchain quirks)" >&2
757
+ continue
758
+ fi
759
+ FILTERED+=("$id")
760
+ done
761
+ RESIDENT_IDS=("${FILTERED[@]}")
762
+ fi
763
+
764
+ # Generate per-app C++ (single-app mode → directly to GENERATED_DIR;
765
+ # multi-app mode → each app under GENERATED_DIR/residents/<id>/).
766
+ GENERATED_GEATSC_SOURCES=()
767
+ GENERATED_FONT_CPP=""
768
+ GENERATED_ASSETS_CPP=""
769
+ RESIDENT_GEATSC_SOURCES=()
770
+ RESIDENT_FONT_CPPS=()
771
+ RESIDENT_ASSET_CPPS=()
772
+ RESIDENT_ENTRY_CPPS=()
773
+ RESIDENT_REGISTRY_CPP=""
774
+
775
+ read_geatsc_sources() {
776
+ local outDir="$1"
777
+ local sourceList="$outDir/geatsc-sources.txt"
778
+ [[ -f "$sourceList" ]] || return
779
+ while IFS= read -r src; do
780
+ [[ -n "$src" && -f "$src" ]] && printf '%s\n' "$src"
781
+ done < "$sourceList"
782
+ }
783
+
784
+ if [[ ${#RESIDENT_IDS[@]} -eq 0 ]]; then
785
+ if ! generate_app "$APP_ID" "$GENERATED_DIR" ""; then
786
+ if [[ -n "$APP_META" ]]; then
787
+ exit 1
788
+ fi
789
+ fi
790
+ refresh_geatsc_runtime_assets "$GENERATED_DIR"
791
+ while IFS= read -r src; do GENERATED_GEATSC_SOURCES+=("$src"); done < <(read_geatsc_sources "$GENERATED_DIR")
792
+ GENERATED_FONT_CPP="$GENERATED_DIR/gea_embedded_font_generated.cpp"
793
+ GENERATED_ASSETS_CPP="$GENERATED_DIR/gea_embedded_assets_generated.cpp"
794
+ else
795
+ # Track which residents successfully generated — apps without an
796
+ # index.tsx (or otherwise skipped) shouldn't show up in the registry,
797
+ # otherwise the launcher would reference symbols that nobody defines.
798
+ ACTUAL_RESIDENTS=()
799
+ for id in "${RESIDENT_IDS[@]}"; do
800
+ safe="${id//[^A-Za-z0-9_]/_}"
801
+ prefix="gea_resident_${safe}"
802
+ rDir="$GENERATED_DIR/residents/$id"
803
+ mkdir -p "$rDir"
804
+ if ! generate_app "$id" "$rDir" "$prefix"; then continue; fi
805
+ refresh_geatsc_runtime_assets "$rDir"
806
+ while IFS= read -r src; do RESIDENT_GEATSC_SOURCES+=("$src"); done < <(read_geatsc_sources "$rDir")
807
+ if [[ -f "$rDir/gea_embedded_font_generated.cpp" ]]; then
808
+ RESIDENT_FONT_CPPS+=("$rDir/gea_embedded_font_generated.cpp")
809
+ fi
810
+ if [[ -f "$rDir/gea_embedded_assets_generated.cpp" ]]; then
811
+ RESIDENT_ASSET_CPPS+=("$rDir/gea_embedded_assets_generated.cpp")
812
+ fi
813
+ entryCpp="$rDir/entry.cpp"
814
+ node "$ROOT_DIR/targets/macos/generate-resident-entry.mjs" "$prefix" "$entryCpp"
815
+ RESIDENT_ENTRY_CPPS+=("$entryCpp")
816
+ ACTUAL_RESIDENTS+=("$id")
817
+ done
818
+ RESIDENT_IDS=("${ACTUAL_RESIDENTS[@]}")
819
+ RESIDENT_REGISTRY_CPP="$GENERATED_DIR/resident_registry.cpp"
820
+ node "$ROOT_DIR/targets/macos/generate-resident-registry.mjs" \
821
+ "$RESIDENT_REGISTRY_CPP" "$APP_ID" "${RESIDENT_IDS[@]}"
822
+ fi
823
+ timing_mark generation
824
+
825
+ # Apple-native detection. geatsc emits gea/apple/*.mm (handle-table bridge +
826
+ # any delegate shims) when the app imports @geajs/apple/*. In that mode the
827
+ # generated geatsc app modules message-send to ::NSView / ::NSToolbar etc., so
828
+ # they must be compiled with -ObjC++, the bridge .mm files must be linked, and
829
+ # the macOS main compiles its AppKit installRootView entry (guarded by
830
+ # GEA_MACOS_HAS_APPLE_NATIVE_BRIDGE). Cocoa (which includes AppKit) is already
831
+ # on the link line.
832
+ APPLE_NATIVE=0
833
+ APPLE_BRIDGE_MMS=()
834
+ if [[ "$(cat "$GENERATED_DIR/.gea-current-apple-native" 2>/dev/null || true)" == "1" && -d "$GENERATED_DIR/gea/apple" ]]; then
835
+ for f in "$GENERATED_DIR/gea/apple"/*.mm; do
836
+ [[ -f "$f" ]] || continue
837
+ APPLE_NATIVE=1
838
+ APPLE_BRIDGE_MMS+=("$f")
839
+ done
840
+ fi
841
+
842
+ # host/*.cpp files (image, touch, imu, …) do `#include "gea_runtime.cpp"`
843
+ # to pull in the gea_cpp_value runtime. ESP32 writes a one-line wrapper to
844
+ # the launcher's generated dir; mirror that here so the include resolves
845
+ # via the existing -I${GENERATED_DIR}.
846
+ mkdir -p "$GENERATED_DIR"
847
+ if [[ ! -f "$GENERATED_DIR/gea_runtime.cpp" ]] || [[ "$(cat "$GENERATED_DIR/gea_runtime.cpp")" != '#include "runtime_pch.h"' ]]; then
848
+ echo '#include "runtime_pch.h"' > "$GENERATED_DIR/gea_runtime.cpp"
849
+ fi
850
+
851
+ INCLUDES=(
852
+ -I"$ROOT_DIR/targets/macos/include"
853
+ -I"$GENERATED_DIR"
854
+ )
855
+ # Framework include roots from the shared manifest (gea_sources.sh).
856
+ while IFS= read -r __inc; do INCLUDES+=("$__inc"); done < <(gea_fw_include_flags)
857
+ SINGLE_APP_DIR=""
858
+ if [[ ${#RESIDENT_IDS[@]} -eq 0 ]]; then
859
+ SINGLE_APP_DIR="$(resolve_app_dir "$APP_ID")"
860
+ if [[ -n "$SINGLE_APP_DIR" ]]; then
861
+ INCLUDES+=(-I"$SINGLE_APP_DIR" -I"$SINGLE_APP_DIR/native")
862
+ fi
863
+ fi
864
+
865
+ CXX_DEFINES=(
866
+ -DGEA_EMBEDDED_ENABLE_VIRTUAL_KEYBOARD=0
867
+ # Canvas ctx.fillText glyphs: rasterize at runtime from the bundle's
868
+ # Resources/Fonts TTFs (font_registry.mm's lookupRuntimeTtfFontForFamily).
869
+ # Windowed builds link no baked font atlas, so without this canvas text
870
+ # fell back to the builtin bitmap font and rendered garbled. Thermalright
871
+ # builds keep their baked atlas: the runtime path tries first, finds no
872
+ # registered TTF bytes there, and falls through.
873
+ -DGEA_EMBEDDED_TTF_RUNTIME_FONTS=1
874
+ )
875
+ MACOS_SHARED_RUNTIME_BUILTINS="${GEA_MACOS_SHARED_RUNTIME_BUILTINS:-0}"
876
+ case "$MACOS_SHARED_RUNTIME_BUILTINS" in
877
+ 0) ;;
878
+ 1) CXX_DEFINES+=(-DGEA_CPP_SHARED_RUNTIME_BUILTINS=1) ;;
879
+ *) echo "GEA_MACOS_SHARED_RUNTIME_BUILTINS must be 0 or 1" >&2; exit 1 ;;
880
+ esac
881
+ GEA_MACOS_OPT_LEVEL="${GEA_MACOS_OPT_LEVEL:--O2}"
882
+ if [[ "${GEA_MACOS_FAST_DEV:-0}" == "1" && "${GEA_MACOS_OPT_LEVEL:-}" == "-O2" ]]; then
883
+ GEA_MACOS_OPT_LEVEL="-O0"
884
+ fi
885
+ # Debug symbols do not affect runtime behavior, but full DWARF greatly increases
886
+ # compiler RSS, object size, and link I/O for generated Three.js modules. Keep
887
+ # the choice explicit and part of every compile/link signature.
888
+ GEA_MACOS_DEBUG_INFO_MODE="${GEA_MACOS_DEBUG_INFO:-none}"
889
+ case "$GEA_MACOS_DEBUG_INFO_MODE" in
890
+ none) GEA_MACOS_DEBUG_INFO="-g0" ;;
891
+ line-tables) GEA_MACOS_DEBUG_INFO="-gline-tables-only" ;;
892
+ full) GEA_MACOS_DEBUG_INFO="-g" ;;
893
+ *)
894
+ echo "GEA_MACOS_DEBUG_INFO must be none, line-tables, or full (got '$GEA_MACOS_DEBUG_INFO_MODE')" >&2
895
+ exit 1
896
+ ;;
897
+ esac
898
+ THERMALRIGHT_DISPLAY_TARGET=0
899
+ THERMALRIGHT_DEFAULT_WIDTH=1280
900
+ THERMALRIGHT_DEFAULT_HEIGHT=480
901
+ THERMALRIGHT_DEFAULT_PRODUCT_ID=0x5302
902
+ case "${GEA_MACOS_DISPLAY_TARGET:-}" in
903
+ thermalright|thermalright-hid|thermalright-usb|thermalright-1280x480|thermalright-hid-1280x480)
904
+ THERMALRIGHT_DISPLAY_TARGET=1
905
+ ;;
906
+ thermalright-wide|thermalright-hid-wide|thermalright-usb-wide|thermalright-1920x462|thermalright-ly-1920x462|thermalright-1920x480|thermalright-hid-1920x480)
907
+ THERMALRIGHT_DISPLAY_TARGET=1
908
+ THERMALRIGHT_DEFAULT_WIDTH=1920
909
+ THERMALRIGHT_DEFAULT_HEIGHT=462
910
+ THERMALRIGHT_DEFAULT_PRODUCT_ID=0x5408
911
+ ;;
912
+ esac
913
+ case "${GEA_MACOS_THERMALRIGHT_PRODUCT_ID:-}" in
914
+ 0x5408|0X5408|21512)
915
+ THERMALRIGHT_DEFAULT_WIDTH=1920
916
+ THERMALRIGHT_DEFAULT_HEIGHT=462
917
+ THERMALRIGHT_DEFAULT_PRODUCT_ID="${GEA_MACOS_THERMALRIGHT_PRODUCT_ID}"
918
+ ;;
919
+ 0x5302|0X5302|21250)
920
+ THERMALRIGHT_DEFAULT_WIDTH=1280
921
+ THERMALRIGHT_DEFAULT_HEIGHT=480
922
+ THERMALRIGHT_DEFAULT_PRODUCT_ID="${GEA_MACOS_THERMALRIGHT_PRODUCT_ID}"
923
+ ;;
924
+ esac
925
+ if [[ "$THERMALRIGHT_DISPLAY_TARGET" == "1" ]]; then
926
+ THERMALRIGHT_WIDTH="${GEA_MACOS_THERMALRIGHT_WIDTH:-$THERMALRIGHT_DEFAULT_WIDTH}"
927
+ THERMALRIGHT_HEIGHT="${GEA_MACOS_THERMALRIGHT_HEIGHT:-$THERMALRIGHT_DEFAULT_HEIGHT}"
928
+ THERMALRIGHT_PRODUCT_ID="${GEA_MACOS_THERMALRIGHT_PRODUCT_ID:-$THERMALRIGHT_DEFAULT_PRODUCT_ID}"
929
+ CXX_DEFINES+=(
930
+ -DGEA_MACOS_THERMALRIGHT_DISPLAY_TARGET=1
931
+ -DGEA_MACOS_THERMALRIGHT_PRODUCT_ID="$THERMALRIGHT_PRODUCT_ID"
932
+ -DGEA_EMBEDDED_DISPLAY_WIDTH="$THERMALRIGHT_WIDTH"
933
+ -DGEA_EMBEDDED_DISPLAY_HEIGHT="$THERMALRIGHT_HEIGHT"
934
+ -DGEA_EMBEDDED_DISPLAY_NATIVE_WIDTH="$THERMALRIGHT_WIDTH"
935
+ -DGEA_EMBEDDED_DISPLAY_NATIVE_HEIGHT="$THERMALRIGHT_HEIGHT"
936
+ )
937
+ fi
938
+ if [[ "$THERMALRIGHT_DISPLAY_TARGET" == "1" && ( -f "$GENERATED_FONT_CPP" || ${#RESIDENT_FONT_CPPS[@]} -gt 0 ) ]]; then
939
+ CXX_DEFINES+=(-DGEA_EMBEDDED_HAS_GENERATED_FONTS=1)
940
+ fi
941
+
942
+ C_SOURCES=(
943
+ "$ROOT_DIR/targets/macos/main/macos_apps.c"
944
+ )
945
+ # Framework C sources (AnimatedGIF) from the shared manifest.
946
+ while IFS= read -r __csrc; do C_SOURCES+=("$__csrc"); done < <(gea_fw_c_sources)
947
+
948
+ CXX_SOURCES=(
949
+ "$ROOT_DIR/targets/macos/main/macos_memory.cpp"
950
+ "$ROOT_DIR/targets/macos/main/macos_sensors.cpp"
951
+ )
952
+ # Framework C++ sources (engine/host/elements/geaos) from the shared manifest.
953
+ # Apple (AppKit-native) lacks camera/power, uses its own run loop, and has no
954
+ # resident-app/OTA/diagnostics services — exclude those framework sources (their
955
+ # gea::platform::* / service impls are esp32/web-only) so the curated set links.
956
+ while IFS= read -r __cxxsrc; do CXX_SOURCES+=("$__cxxsrc"); done < <(gea_fw_cxx_sources | grep -vE "/(host/camera|runtime|services/[a-z_]+)\.cpp$")
957
+
958
+ GENERATED_OBJCXX_SOURCES=()
959
+ if [[ ${#RESIDENT_IDS[@]} -eq 0 ]]; then
960
+ # Single-app mode: gea_app_entry.cpp provides Application::init/frame,
961
+ # which calls the one __gea_top_level emitted by geatsc. In apple-native mode
962
+ # that __gea_top_level builds the AppKit object graph and calls installRootView.
963
+ CXX_SOURCES+=("$GEA_CORE/gea_app_entry.cpp")
964
+ [[ -f "$GEA_GEAOS/resident_apps.cpp" ]] && CXX_SOURCES+=("$GEA_GEAOS/resident_apps.cpp")
965
+ if [[ ${#GENERATED_GEATSC_SOURCES[@]} -gt 0 ]]; then
966
+ # The vast majority of generated geatsc modules are pure C++: they reference
967
+ # host types (NSView, NSColor, …) as plain C++ handle-wrapper structs, and the
968
+ # ObjC-backed method bodies live in the hand-written gea/apple/*.mm bridges
969
+ # (APPLE_BRIDGE_MMS), resolved at link. Compile those with the plain C++
970
+ # toolchain against the cxx runtime PCH — apple-native included — instead of
971
+ # forcing every module through `-ObjC++ -fobjc-arc`, which made each of the
972
+ # hundreds of TUs re-parse the entire AppKit/Metal/Foundation ObjC surface
973
+ # (native_bridge.h now guards those imports behind `#ifdef __OBJC__`).
974
+ #
975
+ # A small minority emit genuine ObjC syntax inline — the callback-target
976
+ # bridge (`[[GeaAppleObjCTarget alloc] …]`, `(__bridge void *)`) that an
977
+ # AppKit event handler lowers to. Those must stay on the .mm toolchain; detect
978
+ # them by content and route only those to MM_SOURCES below.
979
+ GENERATED_OBJCXX_KEYS=$'\n'
980
+ while IFS= read -r f; do
981
+ [[ -n "$f" ]] && GENERATED_OBJCXX_KEYS+="$f"$'\n'
982
+ done < <(grep -lE '__bridge|GeaAppleObjCTarget|@selector|@autoreleasepool|objc_msgSend' \
983
+ "${GENERATED_GEATSC_SOURCES[@]}" 2>/dev/null || true)
984
+ for f in "${GENERATED_GEATSC_SOURCES[@]}"; do
985
+ if [[ "$GENERATED_OBJCXX_KEYS" == *$'\n'"$f"$'\n'* ]]; then
986
+ GENERATED_OBJCXX_SOURCES+=("$f")
987
+ else
988
+ CXX_SOURCES+=("$f")
989
+ fi
990
+ done
991
+ else
992
+ CXX_SOURCES+=("$ROOT_DIR/targets/macos/main/macos_smoke_app.cpp")
993
+ fi
994
+ # Embedded-asset registry (plain C++) is compiled in regardless of
995
+ # apple-native mode — generated app modules may be Objective-C++, this never is.
996
+ if [[ "$THERMALRIGHT_DISPLAY_TARGET" == "1" && -f "$GENERATED_FONT_CPP" ]]; then
997
+ CXX_SOURCES+=("$GENERATED_FONT_CPP")
998
+ fi
999
+ [[ -f "$GENERATED_ASSETS_CPP" ]] && CXX_SOURCES+=("$GENERATED_ASSETS_CPP")
1000
+ else
1001
+ # Multi-app mode: the generated registry provides Application::init/frame
1002
+ # AND ResidentApps::*, dispatching to whichever resident is active. Drop
1003
+ # gea_app_entry.cpp and the inert lib/.../resident_apps.cpp to avoid
1004
+ # duplicate symbols.
1005
+ CXX_SOURCES+=("$RESIDENT_REGISTRY_CPP")
1006
+ for f in "${RESIDENT_GEATSC_SOURCES[@]}"; do CXX_SOURCES+=("$f"); done
1007
+ for f in "${RESIDENT_FONT_CPPS[@]}"; do CXX_SOURCES+=("$f"); done
1008
+ for f in "${RESIDENT_ASSET_CPPS[@]}"; do CXX_SOURCES+=("$f"); done
1009
+ for f in "${RESIDENT_ENTRY_CPPS[@]}"; do CXX_SOURCES+=("$f"); done
1010
+ fi
1011
+
1012
+ MM_SOURCES=(
1013
+ # gea::platform::audio — the AVAudioEngine backend, shared verbatim with iOS.
1014
+ "$ROOT_DIR/targets/shared/apple_audio.mm"
1015
+ "$ROOT_DIR/targets/macos/main/macos_main.mm"
1016
+ "$ROOT_DIR/targets/macos/main/macos_native_shell.mm"
1017
+ "$ROOT_DIR/targets/macos/main/macos_display.mm"
1018
+ "$ROOT_DIR/targets/macos/main/macos_timers.mm"
1019
+ "$ROOT_DIR/targets/macos/main/macos_renderer.mm"
1020
+ "$ROOT_DIR/targets/macos/main/color_convert.mm"
1021
+ "$ROOT_DIR/targets/macos/main/font_registry.mm"
1022
+ "$ROOT_DIR/targets/macos/main/press_bridge.mm"
1023
+ "$ROOT_DIR/targets/macos/main/image_bridge.mm"
1024
+ "$ROOT_DIR/targets/macos/main/canvas_view.mm"
1025
+ "$ROOT_DIR/targets/macos/main/thermalright_hid_display.mm"
1026
+ "$ROOT_DIR/targets/macos/main/macos_app_platform.mm"
1027
+ "$ROOT_DIR/targets/macos/main/macos_device_control.mm"
1028
+ "$ROOT_DIR/targets/macos/main/macos_host_device_control.mm"
1029
+ "$ROOT_DIR/targets/macos/main/macos_network.mm"
1030
+ )
1031
+
1032
+ if [[ -n "$SINGLE_APP_DIR" ]]; then
1033
+ while IFS= read -r native_src; do
1034
+ [[ -n "$native_src" ]] || continue
1035
+ # nativeSourcePaths are already resolved and absolute -- an entry naming a
1036
+ # file in one of the app's packages does not live under the app root, so
1037
+ # re-joining it there would silently drop it.
1038
+ case "$native_src" in
1039
+ /*) full_src="$native_src" ;;
1040
+ *) full_src="$SINGLE_APP_DIR/$native_src" ;;
1041
+ esac
1042
+ [[ -f "$full_src" ]] || continue
1043
+ case "$full_src" in
1044
+ *.c) C_SOURCES+=("$full_src") ;;
1045
+ *.m|*.mm) MM_SOURCES+=("$full_src") ;;
1046
+ *.cc|*.cpp|*.cxx) CXX_SOURCES+=("$full_src") ;;
1047
+ esac
1048
+ done < <(node "$GEA_CLI" apps inspect "$APP_ID" --json | node -e "let data=''; process.stdin.on('data', c => data += c); process.stdin.on('end', () => { const app = JSON.parse(data); for (const src of app.nativeSourcePaths || app.nativeSources || []) console.log(src); });")
1049
+ fi
1050
+
1051
+ # Apple-native: the hand-written AppKit handle-table bridges, plus the handful of
1052
+ # generated modules that emit inline ObjC (the callback-target bridge), need the
1053
+ # .mm toolchain (-ObjC++ -fobjc-arc). Everything else compiled as plain C++ above.
1054
+ if [[ "$APPLE_NATIVE" == "1" ]]; then
1055
+ for f in ${APPLE_BRIDGE_MMS[@]+"${APPLE_BRIDGE_MMS[@]}"}; do MM_SOURCES+=("$f"); done
1056
+ for f in ${GENERATED_OBJCXX_SOURCES[@]+"${GENERATED_OBJCXX_SOURCES[@]}"}; do MM_SOURCES+=("$f"); done
1057
+ fi
1058
+
1059
+ OBJ_FILES=()
1060
+
1061
+ CCACHE_PREFIX=()
1062
+ if [[ "${GEA_MACOS_CCACHE:-1}" != "0" ]]; then
1063
+ if command -v sccache >/dev/null 2>&1; then
1064
+ CCACHE_PREFIX=(sccache)
1065
+ elif command -v ccache >/dev/null 2>&1; then
1066
+ CCACHE_PREFIX=(ccache)
1067
+ fi
1068
+ fi
1069
+ # macOS still ships Bash 3.2, where expanding an explicitly empty array under
1070
+ # `set -u` raises "unbound variable". Keep the cache prefix genuinely
1071
+ # optional so GEA_MACOS_CCACHE=0 can exercise an authoritative cold build.
1072
+ CLANG_CMD=(${CCACHE_PREFIX[@]+"${CCACHE_PREFIX[@]}"} clang)
1073
+ CLANGXX_CMD=(${CCACHE_PREFIX[@]+"${CCACHE_PREFIX[@]}"} clang++)
1074
+ MACOS_COMPILER_VERSION="$("${CLANGXX_CMD[@]}" --version 2>/dev/null | head -n 1 || true)"
1075
+
1076
+ # Mangle a source path into a unique object filename. Return through a global
1077
+ # variable so the hundreds of calls made while writing build.ninja do not each
1078
+ # fork a Bash command-substitution subshell.
1079
+ OBJ_PATH_RESULT=""
1080
+ set_obj_path() {
1081
+ local src="$1"
1082
+ local suffix="$2"
1083
+ local rel="${src#$ROOT_DIR/}"
1084
+ rel="${rel#$MACOS_OUTPUT_ROOT/}"
1085
+ rel="${rel%.cpp}"
1086
+ rel="${rel%.mm}"
1087
+ rel="${rel%.c}"
1088
+ rel="${rel//\//__}"
1089
+ OBJ_PATH_RESULT="$BUILD_DIR/${rel}${suffix}"
1090
+ }
1091
+
1092
+ compile_signature() {
1093
+ local kind="$1"
1094
+ {
1095
+ printf 'kind=%s\n' "$kind"
1096
+ printf 'opt=%s\n' "$GEA_MACOS_OPT_LEVEL"
1097
+ printf 'debug=%s\n' "$GEA_MACOS_DEBUG_INFO"
1098
+ printf 'clang='
1099
+ printf '%s ' "${CLANG_CMD[@]}"
1100
+ printf '\nclangxx='
1101
+ printf '%s ' "${CLANGXX_CMD[@]}"
1102
+ printf '\ncompiler-version=%s\n' "$MACOS_COMPILER_VERSION"
1103
+ printf 'defines='
1104
+ printf '%s ' "${CXX_DEFINES[@]}"
1105
+ printf '\nincludes='
1106
+ printf '%s ' "${INCLUDES[@]}"
1107
+ printf '\n'
1108
+ }
1109
+ }
1110
+
1111
+ depfile_has_newer_input() {
1112
+ local depfile="$1"
1113
+ local obj="$2"
1114
+ [[ -f "$depfile" ]] || return 0
1115
+ local dep
1116
+ while IFS= read -r dep; do
1117
+ [[ -z "$dep" ]] && continue
1118
+ dep="${dep//\\ / }"
1119
+ if [[ -e "$dep" && "$dep" -nt "$obj" ]]; then
1120
+ return 0
1121
+ fi
1122
+ done < <(sed -e ':again' -e '/\\$/N' -e 's/\\\n/ /' -e 'tagain' -e 's/^[^:]*: //' "$depfile" | tr ' ' '\n' | sed -e 's/\\$//' -e '/^$/d')
1123
+ return 1
1124
+ }
1125
+
1126
+ needs_compile() {
1127
+ local src="$1"
1128
+ local obj="$2"
1129
+ local sigFile="$3"
1130
+ local sig="$4"
1131
+ [[ -f "$obj" ]] || return 0
1132
+ [[ -f "$sigFile" ]] || return 0
1133
+ [[ "$(cat "$sigFile")" == "$sig" ]] || return 0
1134
+ [[ "$src" -nt "$obj" ]] && return 0
1135
+ depfile_has_newer_input "$obj.d" "$obj" && return 0
1136
+ return 1
1137
+ }
1138
+
1139
+ # macOS ships bash 3.2, where "${arr[@]}" on an EMPTY array is an unbound-
1140
+ # variable error under `set -u` — RESIDENT_GEATSC_SOURCES is only populated in
1141
+ # multi-app mode, so guard with the [@]+ idiom (same as native-test-common.sh).
1142
+ GEATSC_GENERATED_SOURCES=()
1143
+ for f in ${GENERATED_GEATSC_SOURCES[@]+"${GENERATED_GEATSC_SOURCES[@]}"}; do GEATSC_GENERATED_SOURCES+=("$f"); done
1144
+ for f in ${RESIDENT_GEATSC_SOURCES[@]+"${RESIDENT_GEATSC_SOURCES[@]}"}; do GEATSC_GENERATED_SOURCES+=("$f"); done
1145
+ GEATSC_GENERATED_SOURCE_KEYS=$'\n'
1146
+ for f in ${GEATSC_GENERATED_SOURCES[@]+"${GEATSC_GENERATED_SOURCES[@]}"}; do
1147
+ GEATSC_GENERATED_SOURCE_KEYS+="$f"$'\n'
1148
+ done
1149
+
1150
+ is_geatsc_generated_source() {
1151
+ local src="$1"
1152
+ [[ "$GEATSC_GENERATED_SOURCE_KEYS" == *$'\n'"$src"$'\n'* ]]
1153
+ }
1154
+
1155
+ timing_mark source-graph
1156
+ # Precompiled headers, two levels, one pair per generated directory.
1157
+ #
1158
+ # Level one is the RUNTIME layer: `<stem>.runtime.hpp`, the prelude the
1159
+ # compiler writes for a `--per-file-units` compile (standard headers, the
1160
+ # hosts' preambles, gea_runtime.h, the hosts' headers). It is independent of
1161
+ # the program's declarations, so it is built once per generated directory and
1162
+ # reused across every unit and every rebuild until the runtime or the flags
1163
+ # change. (`runtime_pch.h` is the previous compiler's equivalent and is honored
1164
+ # only in a directory that compiler generated.)
1165
+ #
1166
+ # Level two is the PROGRAM layer: the shared `<stem>.hpp` the same compile
1167
+ # names beside it in `geatsc-header.txt`, built ON TOP of the runtime PCH with
1168
+ # `-include-pch` (clang's chained PCH). Every unit of that directory then takes
1169
+ # `-include-pch <program>.pch`, which loads both layers, and its own
1170
+ # `#include "<stem>.hpp"` is skipped because the PCH recorded the guard. A
1171
+ # change to the program's declarations rebuilds only this small top layer; the
1172
+ # runtime layer is untouched. Without it every one of the N units re-parsed the
1173
+ # whole runtime -- the compiler's docs/TRANSLATION-UNITS.md measured that at
1174
+ # ~1.3s per unit, most of a small unit's compile.
1175
+ #
1176
+ # A `single`-layout directory of this compiler names no header and takes no
1177
+ # PCH: its one unit carries its own prelude, and nothing preamble-exact exists
1178
+ # to precompile for it.
1179
+ #
1180
+ # macOS bash 3.2 has no associative arrays, so the registry is three parallel
1181
+ # arrays indexed together: the generated directory, and the PCH a cxx / mm
1182
+ # unit from that directory is compiled against.
1183
+ PCH_DIRS=()
1184
+ PCH_CXX=()
1185
+ PCH_MM=()
1186
+
1187
+ # The two headers a per-file compile named in `geatsc-header.txt`
1188
+ # (`program=<path>` and `runtime=<path>` lines), for one generated directory.
1189
+ # The runtime layer MUST be the compiler's `<stem>.runtime.hpp`, never
1190
+ # `generated_support.hpp`: the hosts' preambles precede `gea_runtime.h` and
1191
+ # decide what it declares, and a PCH built without them declares engine
1192
+ # stand-ins that collide with the engine's own types in every unit.
1193
+ pch_sources_for_dir() {
1194
+ local dir="$1"
1195
+ local manifest="$dir/geatsc-header.txt"
1196
+ PCH_PROGRAM_SOURCE_RESULT=""
1197
+ PCH_RUNTIME_SOURCE_RESULT=""
1198
+ if [[ -f "$manifest" ]]; then
1199
+ local line
1200
+ while IFS= read -r line; do
1201
+ case "$line" in
1202
+ program=*) [[ -f "${line#program=}" ]] && PCH_PROGRAM_SOURCE_RESULT="${line#program=}" ;;
1203
+ runtime=*) [[ -f "${line#runtime=}" ]] && PCH_RUNTIME_SOURCE_RESULT="${line#runtime=}" ;;
1204
+ esac
1205
+ done < "$manifest"
1206
+ fi
1207
+ # The previous compiler's runtime umbrella, honored only where it generated
1208
+ # the directory and this compiler named nothing; a single-unit directory of
1209
+ # this compiler has no preamble-exact header to precompile and takes none.
1210
+ if [[ -z "$PCH_RUNTIME_SOURCE_RESULT" && -z "$PCH_PROGRAM_SOURCE_RESULT" && -f "$dir/runtime_pch.h" ]]; then
1211
+ PCH_RUNTIME_SOURCE_RESULT="$dir/runtime_pch.h"
1212
+ fi
1213
+ }
1214
+
1215
+ # Build one PCH from one header, optionally chained on another PCH. The
1216
+ # signature is the completion marker (see needs_compile): invalidate it before
1217
+ # touching outputs, publish the PCH/depfile atomically, the signature last, so
1218
+ # a killed clang leaves the prior PCH intact but untrusted. A chained PCH also
1219
+ # rebuilds whenever the PCH under it was rebuilt, because clang validates the
1220
+ # chain by identity, not by content.
1221
+ build_pch() {
1222
+ local kind="$1"
1223
+ local src="$2"
1224
+ local pch="$3"
1225
+ local chained="${4:-}"
1226
+ local label="$5"
1227
+ local sigFile="$pch.sig"
1228
+ local failFile="$pch.failed"
1229
+ local sig
1230
+ local pchTmp="$pch.tmp.$$"
1231
+ local depTmp="$pch.d.tmp.$$"
1232
+ local sigTmp="$sigFile.tmp.$$"
1233
+ local chainArgs=()
1234
+ [[ -n "$chained" ]] && chainArgs=(-include-pch "$chained")
1235
+ sig="$(compile_signature "pch-$kind")"$'\n'"source=$src"$'\n'"chained=$chained"
1236
+ # Retry a prior failure. The PCH includes generated runtime and host headers
1237
+ # whose contents are not part of this direct-source signature, so an exact
1238
+ # `.failed` marker can become stale while `$src` itself stays unchanged.
1239
+ if needs_compile "$src" "$pch" "$sigFile" "$sig" || { [[ -n "$chained" && "$chained" -nt "$pch" ]]; }; then
1240
+ echo "Building $label PCH ($kind)..."
1241
+ rm -f "$sigFile" "$failFile" "$pchTmp" "$depTmp" "$sigTmp"
1242
+ # A precompiled header is an optimization and nothing else: every unit
1243
+ # compiles correctly without one. So a header that will not compile
1244
+ # degrades this directory to no PCH and says so, rather than failing a
1245
+ # build that would otherwise succeed. The case that forced this: a
1246
+ # directory the previous compiler generated leaves a `runtime_pch.h`
1247
+ # behind, this compiler does not ship the `.def` files that header
1248
+ # includes, and honoring it killed every single-layout build of an app
1249
+ # whose directory still carried one.
1250
+ local status=0
1251
+ case "$kind" in
1252
+ cxx)
1253
+ macos_run_tracked_child "${CLANGXX_CMD[@]}" -std=c++20 "$GEA_MACOS_OPT_LEVEL" "${CXX_DEFINES[@]}" "${INCLUDES[@]}" \
1254
+ ${chainArgs[@]+"${chainArgs[@]}"} \
1255
+ -x c++-header -MMD -MP -MF "$depTmp" -MT "$pch" "$src" -o "$pchTmp" || status=$?
1256
+ ;;
1257
+ mm)
1258
+ macos_run_tracked_child "${CLANGXX_CMD[@]}" -std=c++20 -ObjC++ -fobjc-arc "$GEA_MACOS_OPT_LEVEL" "${CXX_DEFINES[@]}" "${INCLUDES[@]}" \
1259
+ ${chainArgs[@]+"${chainArgs[@]}"} \
1260
+ -x objective-c++-header -MMD -MP -MF "$depTmp" -MT "$pch" "$src" -o "$pchTmp" || status=$?
1261
+ ;;
1262
+ esac
1263
+ if [[ "$status" != "0" ]]; then
1264
+ rm -f "$pchTmp" "$depTmp" "$sigTmp" "$pch" "$pch.d"
1265
+ printf "%s\n" "$sig" > "$failFile"
1266
+ echo "warning: $label PCH ($kind) failed to build from $src; continuing without it -- every unit in ${src%/*} will re-parse the runtime instead." >&2
1267
+ return 1
1268
+ fi
1269
+ printf "%s\n" "$sig" > "$sigTmp"
1270
+ mv "$pchTmp" "$pch"
1271
+ mv "$depTmp" "$pch.d"
1272
+ mv "$sigTmp" "$sigFile"
1273
+ fi
1274
+ }
1275
+
1276
+ # Both levels for one generated directory and one kind; records the PCH its
1277
+ # units take (the program layer when there is one, else the runtime layer).
1278
+ build_pchs_for_dir() {
1279
+ local dir="$1"
1280
+ local kind="$2"
1281
+ local index="$3"
1282
+ [[ "${GEA_MACOS_PCH:-1}" != "0" ]] || return 0
1283
+ pch_sources_for_dir "$dir"
1284
+ local runtimeSrc="$PCH_RUNTIME_SOURCE_RESULT"
1285
+ [[ -n "$runtimeSrc" ]] || return 0
1286
+ local rel="${dir#$ROOT_DIR/}"
1287
+ rel="${rel#$MACOS_OUTPUT_ROOT/}"
1288
+ rel="${rel//\//__}"
1289
+ local runtimePch="$BUILD_DIR/${rel}.runtime_${kind}.pch"
1290
+ # No runtime layer means no chain to hang the program layer on, so the whole
1291
+ # directory compiles bare. A program layer that fails on its own still
1292
+ # leaves the runtime layer usable, which is most of the win.
1293
+ build_pch "$kind" "$runtimeSrc" "$runtimePch" "" "runtime" || return 0
1294
+ local unitPch="$runtimePch"
1295
+ if [[ -n "$PCH_PROGRAM_SOURCE_RESULT" ]]; then
1296
+ local programPch="$BUILD_DIR/${rel}.program_${kind}.pch"
1297
+ if build_pch "$kind" "$PCH_PROGRAM_SOURCE_RESULT" "$programPch" "$runtimePch" "program"; then unitPch="$programPch"; fi
1298
+ fi
1299
+ case "$kind" in
1300
+ cxx) PCH_CXX[$index]="$unitPch" ;;
1301
+ mm) PCH_MM[$index]="$unitPch" ;;
1302
+ esac
1303
+ }
1304
+
1305
+ # The PCH a generated source is compiled against, by its directory; empty when
1306
+ # none was built. `${src%/*}` is dirname without a subshell -- this runs once
1307
+ # per source on both compile paths.
1308
+ pch_for_source() {
1309
+ local kind="$1"
1310
+ local src="$2"
1311
+ local dir="${src%/*}"
1312
+ local i
1313
+ PCH_FOR_SOURCE_RESULT=""
1314
+ for i in ${PCH_DIRS[@]+"${!PCH_DIRS[@]}"}; do
1315
+ if [[ "${PCH_DIRS[$i]}" == "$dir" ]]; then
1316
+ case "$kind" in
1317
+ cxx) PCH_FOR_SOURCE_RESULT="${PCH_CXX[$i]}" ;;
1318
+ mm) PCH_FOR_SOURCE_RESULT="${PCH_MM[$i]}" ;;
1319
+ esac
1320
+ return 0
1321
+ fi
1322
+ done
1323
+ }
1324
+
1325
+ # Every directory a generated source lives in, once each, in first-seen order.
1326
+ for src in ${GEATSC_GENERATED_SOURCES[@]+"${GEATSC_GENERATED_SOURCES[@]}"}; do
1327
+ dir="${src%/*}"
1328
+ seen=0
1329
+ for known in ${PCH_DIRS[@]+"${PCH_DIRS[@]}"}; do
1330
+ if [[ "$known" == "$dir" ]]; then seen=1; break; fi
1331
+ done
1332
+ if [[ "$seen" == "0" ]]; then
1333
+ PCH_DIRS+=("$dir")
1334
+ PCH_CXX+=("")
1335
+ PCH_MM+=("")
1336
+ fi
1337
+ done
1338
+
1339
+ NEEDS_CXX_PCH=0
1340
+ NEEDS_MM_PCH=0
1341
+ for src in "${CXX_SOURCES[@]}"; do
1342
+ if is_geatsc_generated_source "$src"; then NEEDS_CXX_PCH=1; fi
1343
+ done
1344
+ # Now that generated modules compile as plain C++ against the cxx PCH, the only
1345
+ # generated sources still on the .mm toolchain are the rare ones that emit inline
1346
+ # ObjC (the callback-target bridge). Building the ~43MB mm runtime PCH costs far
1347
+ # more than it saves for a handful of TUs, so only build it when enough generated
1348
+ # .mm modules would amortize it; otherwise they compile bare (each re-parses the
1349
+ # runtime once — cheaper than the PCH build). The hand-written gea/apple/*.mm
1350
+ # bridges do not use the runtime, so they never need it.
1351
+ MM_GENERATED_COUNT=0
1352
+ for src in "${MM_SOURCES[@]}"; do
1353
+ if is_geatsc_generated_source "$src"; then MM_GENERATED_COUNT=$((MM_GENERATED_COUNT + 1)); fi
1354
+ done
1355
+ if [[ "$MM_GENERATED_COUNT" -ge "${GEA_MACOS_MM_PCH_THRESHOLD:-8}" ]]; then NEEDS_MM_PCH=1; fi
1356
+ # Generation can take minutes. Re-establish the object-directory invariant at
1357
+ # the compilation boundary so an out-of-band cleanup cannot leave the PCH and
1358
+ # object writers targeting a vanished parent directory.
1359
+ mkdir -p "$BUILD_DIR"
1360
+ if [[ "$MACOS_SHARED_RUNTIME_BUILTINS" == "1" ]]; then
1361
+ sharedRuntimePrelude=""
1362
+ for dir in ${PCH_DIRS[@]+"${PCH_DIRS[@]}"}; do
1363
+ pch_sources_for_dir "$dir"
1364
+ if [[ -n "$PCH_RUNTIME_SOURCE_RESULT" ]]; then
1365
+ sharedRuntimePrelude="$PCH_RUNTIME_SOURCE_RESULT"
1366
+ break
1367
+ fi
1368
+ done
1369
+ if [[ -z "$sharedRuntimePrelude" ]]; then
1370
+ echo "Shared runtime builtins require a generated runtime prelude (per-file or balanced layout)" >&2
1371
+ exit 1
1372
+ fi
1373
+ # Every consumer gets the same declaration-mode flag; exactly one owner is
1374
+ # linked for the entire native build, including builds with resident apps.
1375
+ # Use the real host prelude before the runtime implementation, just as the
1376
+ # generated units do, so engine/host types retain their existing ABI.
1377
+ sharedRuntimeOwner="$BUILD_DIR/gea_runtime_builtins_owner.cpp"
1378
+ sharedRuntimeOwnerTmp="$sharedRuntimeOwner.tmp.$$"
1379
+ printf '#include "%s"\n#include "%s"\n' "$sharedRuntimePrelude" \
1380
+ "$GEA_COMPILER/src/targets/cpp/runtime/gea_runtime_builtins.cpp" > "$sharedRuntimeOwnerTmp"
1381
+ replace_content_stable "$sharedRuntimeOwner" "$sharedRuntimeOwnerTmp" || true
1382
+ CXX_SOURCES+=("$sharedRuntimeOwner")
1383
+ fi
1384
+ for i in ${PCH_DIRS[@]+"${!PCH_DIRS[@]}"}; do
1385
+ if [[ "$NEEDS_CXX_PCH" == "1" ]]; then build_pchs_for_dir "${PCH_DIRS[$i]}" cxx "$i"; fi
1386
+ if [[ "$NEEDS_MM_PCH" == "1" ]]; then build_pchs_for_dir "${PCH_DIRS[$i]}" mm "$i"; fi
1387
+ done
1388
+ timing_mark pch
1389
+
1390
+ compile_c() {
1391
+ local src="$1"
1392
+ local obj
1393
+ set_obj_path "$src" .o
1394
+ obj="$OBJ_PATH_RESULT"
1395
+ "${CLANG_CMD[@]}" "$GEA_MACOS_OPT_LEVEL" ${GEA_MACOS_DEBUG_INFO} -DGEA_EMBEDDED_GIF_C_API "${INCLUDES[@]}" -c "$src" -o "$obj"
1396
+ OBJ_FILES+=("$obj")
1397
+ }
1398
+
1399
+ compile_cxx() {
1400
+ local src="$1"
1401
+ local obj
1402
+ set_obj_path "$src" .cxx.o
1403
+ obj="$OBJ_PATH_RESULT"
1404
+ "${CLANGXX_CMD[@]}" -std=c++20 "$GEA_MACOS_OPT_LEVEL" ${GEA_MACOS_DEBUG_INFO} "${CXX_DEFINES[@]}" "${INCLUDES[@]}" -x c++ -c "$src" -o "$obj"
1405
+ OBJ_FILES+=("$obj")
1406
+ }
1407
+
1408
+ compile_mm() {
1409
+ local src="$1"
1410
+ local obj
1411
+ set_obj_path "$src" .mm.o
1412
+ obj="$OBJ_PATH_RESULT"
1413
+ "${CLANGXX_CMD[@]}" -std=c++20 -ObjC++ -fobjc-arc "$GEA_MACOS_OPT_LEVEL" ${GEA_MACOS_DEBUG_INFO} "${CXX_DEFINES[@]}" "${INCLUDES[@]}" -c "$src" -o "$obj"
1414
+ OBJ_FILES+=("$obj")
1415
+ }
1416
+
1417
+ # Parallel compile. A generated -g0 module currently peaks around 2.4 GiB in
1418
+ # clang. Use eight workers on >=64 GiB machines, four on smaller hosts, and
1419
+ # always cap the result by available logical CPUs. Explicit overrides win.
1420
+ default_macos_jobs() {
1421
+ local logical memory pages page_size jobs
1422
+ logical="$(sysctl -n hw.perflevel0.logicalcpu 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)"
1423
+ memory="$(sysctl -n hw.memsize 2>/dev/null || true)"
1424
+ if ! [[ "$memory" =~ ^[0-9]+$ ]]; then
1425
+ pages="$(getconf _PHYS_PAGES 2>/dev/null || echo 0)"
1426
+ page_size="$(getconf PAGE_SIZE 2>/dev/null || echo 0)"
1427
+ if [[ "$pages" =~ ^[0-9]+$ && "$page_size" =~ ^[0-9]+$ ]]; then
1428
+ memory=$((pages * page_size))
1429
+ else
1430
+ memory=0
1431
+ fi
1432
+ fi
1433
+ jobs=4
1434
+ if (( memory >= 68719476736 )); then jobs=8; fi
1435
+ # Large generated applications have enough independent translation units to
1436
+ # keep more of an M-series Max busy. Reserve four logical CPUs for the host
1437
+ # while using the extra RAM to avoid the swap/RSS cliff seen on smaller Macs.
1438
+ if (( memory >= 103079215104 )); then jobs=12; fi
1439
+ if [[ "$logical" =~ ^[0-9]+$ ]] && (( logical > 0 && jobs > logical )); then jobs="$logical"; fi
1440
+ printf '%s\n' "$jobs"
1441
+ }
1442
+
1443
+ JOBS="${GEA_MACOS_JOBS:-${MAKEFLAGS_J:-$(default_macos_jobs)}}"
1444
+ if ! [[ "$JOBS" =~ ^[0-9]+$ ]] || (( JOBS < 1 )); then
1445
+ JOBS=1
1446
+ fi
1447
+ if [[ "$MACOS_TIMINGS" == "1" ]]; then
1448
+ printf '[macos timing] configuration generator=%s jobs=%s debug=%s opt=%s cache=%s\n' \
1449
+ "$MACOS_GENERATOR" "$JOBS" "$GEA_MACOS_DEBUG_INFO_MODE" "$GEA_MACOS_OPT_LEVEL" "$MACOS_PIPELINE_CACHE" >&2
1450
+ fi
1451
+
1452
+ write_content_stable_file() {
1453
+ local destination="$1"
1454
+ local temporary="$2"
1455
+ if [[ -f "$destination" ]] && cmp -s "$temporary" "$destination"; then
1456
+ rm -f "$temporary"
1457
+ return
1458
+ fi
1459
+ mv "$temporary" "$destination"
1460
+ }
1461
+
1462
+ NINJA_ESCAPED_PATH=""
1463
+ set_ninja_escaped_path() {
1464
+ local value="$1"
1465
+ case "$value" in
1466
+ *$'\n'*) echo "Ninja paths may not contain newlines: $value" >&2; exit 1 ;;
1467
+ esac
1468
+ value="${value//\$/\$\$}"
1469
+ value="${value// /\$ }"
1470
+ value="${value//:/\$:}"
1471
+ NINJA_ESCAPED_PATH="$value"
1472
+ }
1473
+
1474
+ NINJA_SHELL_QUOTED=""
1475
+ set_ninja_shell_quoted() {
1476
+ local value="$1"
1477
+ case "$value" in
1478
+ *"'"*|*$'\n'*) echo "Unsupported quote/newline in Ninja command argument: $value" >&2; exit 1 ;;
1479
+ esac
1480
+ value="${value//\$/\$\$}"
1481
+ NINJA_SHELL_QUOTED="'$value'"
1482
+ }
1483
+
1484
+ write_clang_response_file() {
1485
+ local destination="$1"
1486
+ shift
1487
+ local temporary="$destination.tmp.$$"
1488
+ : > "$temporary"
1489
+ local argument escaped
1490
+ for argument in "$@"; do
1491
+ [[ -n "$argument" ]] || continue
1492
+ escaped="${argument//\\/\\\\}"
1493
+ escaped="${escaped//\"/\\\"}"
1494
+ printf '"%s"\n' "$escaped" >> "$temporary"
1495
+ done
1496
+ write_content_stable_file "$destination" "$temporary"
1497
+ }
1498
+
1499
+ write_ninja_signature_file() {
1500
+ local destination="$1"
1501
+ local signature="$2"
1502
+ local temporary="$destination.tmp.$$"
1503
+ printf '%s\n' "$signature" > "$temporary"
1504
+ write_content_stable_file "$destination" "$temporary"
1505
+ }
1506
+
1507
+ write_ninja_compile_edge() {
1508
+ local output_file="$1"
1509
+ local rule="$2"
1510
+ local source_file="$3"
1511
+ local response_file="$4"
1512
+ local signature_file="$5"
1513
+ local pch_file="${6:-}"
1514
+ local escaped_output escaped_source escaped_response escaped_signature escaped_pch
1515
+ set_ninja_escaped_path "$output_file"; escaped_output="$NINJA_ESCAPED_PATH"
1516
+ set_ninja_escaped_path "$source_file"; escaped_source="$NINJA_ESCAPED_PATH"
1517
+ set_ninja_escaped_path "$response_file"; escaped_response="$NINJA_ESCAPED_PATH"
1518
+ set_ninja_escaped_path "$signature_file"; escaped_signature="$NINJA_ESCAPED_PATH"
1519
+ printf 'build %s: %s %s | %s %s' \
1520
+ "$escaped_output" \
1521
+ "$rule" \
1522
+ "$escaped_source" \
1523
+ "$escaped_response" \
1524
+ "$escaped_signature"
1525
+ if [[ -n "$pch_file" ]]; then
1526
+ set_ninja_escaped_path "$pch_file"; escaped_pch="$NINJA_ESCAPED_PATH"
1527
+ printf ' %s' "$escaped_pch"
1528
+ fi
1529
+ printf '\n'
1530
+ }
1531
+
1532
+ compile_with_ninja() {
1533
+ local ninja_file="$BUILD_DIR/build.ninja"
1534
+ local ninja_tmp="$ninja_file.tmp.$$"
1535
+ local ninja_inputs_file="$BUILD_DIR/build.ninja.inputs"
1536
+ local ninja_inputs_tmp="$ninja_inputs_file.tmp.$$"
1537
+ local c_rsp="$BUILD_DIR/ninja-c.rsp"
1538
+ local cxx_rsp="$BUILD_DIR/ninja-cxx.rsp"
1539
+ local mm_rsp="$BUILD_DIR/ninja-mm.rsp"
1540
+ local c_signature_file="$BUILD_DIR/ninja-c.signature"
1541
+ local cxx_signature_file="$BUILD_DIR/ninja-cxx.signature"
1542
+ local mm_signature_file="$BUILD_DIR/ninja-mm.signature"
1543
+ local src obj
1544
+
1545
+ # Ninja cannot safely import Shell-era depfiles into its binary dependency
1546
+ # database without running each edge. Rebuild that legacy tree once instead
1547
+ # of silently trusting objects whose flags/dependencies may differ. Every
1548
+ # subsequent build uses the compact .ninja_deps database.
1549
+ if [[ ! -f "$BUILD_DIR/.ninja_deps" ]] && \
1550
+ find "$BUILD_DIR" -name '*.o' -type f -print -quit 2>/dev/null | grep -q .; then
1551
+ echo "Migrating existing objects to Ninja dependency tracking (one-time rebuild)..." >&2
1552
+ fi
1553
+
1554
+ # Object order is also link order. Populate it even on a graph-cache hit,
1555
+ # using in-process path mangling rather than one subshell per source.
1556
+ for src in "${C_SOURCES[@]}"; do
1557
+ set_obj_path "$src" .o; OBJ_FILES+=("$OBJ_PATH_RESULT")
1558
+ done
1559
+ for src in "${CXX_SOURCES[@]}"; do
1560
+ set_obj_path "$src" .cxx.o; OBJ_FILES+=("$OBJ_PATH_RESULT")
1561
+ done
1562
+ for src in "${MM_SOURCES[@]}"; do
1563
+ set_obj_path "$src" .mm.o; OBJ_FILES+=("$OBJ_PATH_RESULT")
1564
+ done
1565
+
1566
+ # Source sizes are part of the graph: they determine the compile-edge order.
1567
+ # Compute the schedule once, then include its size/order fingerprint in the
1568
+ # graph-cache key so a regenerated module cannot leave a stale LPT schedule.
1569
+ # One Node process stats the whole source set; the no-op path still avoids the
1570
+ # much more expensive Bash graph-emission work below.
1571
+ local compile_schedule
1572
+ compile_schedule="$({
1573
+ for src in "${C_SOURCES[@]}"; do printf 'c\t%s\n' "$src"; done
1574
+ for src in "${CXX_SOURCES[@]}"; do printf 'cxx\t%s\n' "$src"; done
1575
+ for src in "${MM_SOURCES[@]}"; do printf 'mm\t%s\n' "$src"; done
1576
+ } | node "$ROOT_DIR/targets/macos/order-compile-edges.mjs")"
1577
+
1578
+ # build.ninja is a pure function of these inputs. Re-emitting its hundreds of
1579
+ # edges used to cost several seconds of Bash process churn even when Ninja
1580
+ # itself needed only milliseconds to prove the graph clean.
1581
+ {
1582
+ printf 'format=5\nuse-ninja-deps=1\n'
1583
+ printf 'clang='; printf '%s\037' "${CLANG_CMD[@]}"; printf '\n'
1584
+ printf 'clangxx='; printf '%s\037' "${CLANGXX_CMD[@]}"; printf '\n'
1585
+ printf 'c-signature=%s\n' "$COMPILE_SIGNATURE_C"
1586
+ printf 'cxx-signature=%s\n' "$COMPILE_SIGNATURE_CXX"
1587
+ printf 'mm-signature=%s\n' "$COMPILE_SIGNATURE_MM"
1588
+ for i in ${PCH_DIRS[@]+"${!PCH_DIRS[@]}"}; do
1589
+ printf 'pch=%s\t%s\t%s\n' "${PCH_DIRS[$i]}" "${PCH_CXX[$i]}" "${PCH_MM[$i]}"
1590
+ done
1591
+ for src in "${C_SOURCES[@]}"; do printf 'c=%s\n' "$src"; done
1592
+ for src in "${CXX_SOURCES[@]}"; do
1593
+ if is_geatsc_generated_source "$src"; then printf 'cxx-generated=%s\n' "$src"
1594
+ else printf 'cxx=%s\n' "$src"; fi
1595
+ done
1596
+ for src in "${MM_SOURCES[@]}"; do
1597
+ if is_geatsc_generated_source "$src"; then printf 'mm-generated=%s\n' "$src"
1598
+ else printf 'mm=%s\n' "$src"; fi
1599
+ done
1600
+ local scheduled_kind scheduled_size scheduled_source
1601
+ while IFS=$'\t' read -r scheduled_kind scheduled_size scheduled_source; do
1602
+ printf 'schedule=%s\t%s\t%s\n' "$scheduled_kind" "$scheduled_size" "$scheduled_source"
1603
+ done <<< "$compile_schedule"
1604
+ } > "$ninja_inputs_tmp"
1605
+
1606
+ if [[ "$MACOS_PIPELINE_CACHE" == "1" && -f "$ninja_file" && -f "$ninja_inputs_file" && \
1607
+ -f "$c_rsp" && -f "$cxx_rsp" && -f "$mm_rsp" && \
1608
+ -f "$c_signature_file" && -f "$cxx_signature_file" && -f "$mm_signature_file" ]] && \
1609
+ cmp -s "$ninja_inputs_tmp" "$ninja_inputs_file"; then
1610
+ rm -f "$ninja_inputs_tmp"
1611
+ if [[ "${GEA_MACOS_NINJA_DRY_RUN:-0}" == "1" ]]; then
1612
+ macos_run_tracked_child ninja -C "$BUILD_DIR" -f "$ninja_file" -d keepdepfile -n -j "$JOBS" gea_objects
1613
+ else
1614
+ macos_run_tracked_child ninja -C "$BUILD_DIR" -f "$ninja_file" -d keepdepfile -j "$JOBS" gea_objects
1615
+ fi
1616
+ return
1617
+ fi
1618
+
1619
+ write_clang_response_file "$c_rsp" \
1620
+ "$GEA_MACOS_OPT_LEVEL" "$GEA_MACOS_DEBUG_INFO" \
1621
+ -DGEA_EMBEDDED_GIF_C_API "${INCLUDES[@]}"
1622
+ write_clang_response_file "$cxx_rsp" \
1623
+ -std=c++20 "$GEA_MACOS_OPT_LEVEL" "$GEA_MACOS_DEBUG_INFO" \
1624
+ "${CXX_DEFINES[@]}" "${INCLUDES[@]}" -x c++
1625
+ write_clang_response_file "$mm_rsp" \
1626
+ -std=c++20 -ObjC++ -fobjc-arc "$GEA_MACOS_OPT_LEVEL" "$GEA_MACOS_DEBUG_INFO" \
1627
+ "${CXX_DEFINES[@]}" "${INCLUDES[@]}"
1628
+ write_ninja_signature_file "$c_signature_file" "$COMPILE_SIGNATURE_C"
1629
+ write_ninja_signature_file "$cxx_signature_file" "$COMPILE_SIGNATURE_CXX"
1630
+ write_ninja_signature_file "$mm_signature_file" "$COMPILE_SIGNATURE_MM"
1631
+
1632
+ local clang_command=""
1633
+ local clangxx_command=""
1634
+ local command_part
1635
+ for command_part in "${CLANG_CMD[@]}"; do
1636
+ set_ninja_shell_quoted "$command_part"
1637
+ clang_command+="$NINJA_SHELL_QUOTED "
1638
+ done
1639
+ for command_part in "${CLANGXX_CMD[@]}"; do
1640
+ set_ninja_shell_quoted "$command_part"
1641
+ clangxx_command+="$NINJA_SHELL_QUOTED "
1642
+ done
1643
+
1644
+ local escaped_c_rsp escaped_cxx_rsp escaped_mm_rsp
1645
+ local escaped_c_signature escaped_cxx_signature escaped_mm_signature
1646
+ set_ninja_escaped_path "$c_rsp"; escaped_c_rsp="$NINJA_ESCAPED_PATH"
1647
+ set_ninja_escaped_path "$cxx_rsp"; escaped_cxx_rsp="$NINJA_ESCAPED_PATH"
1648
+ set_ninja_escaped_path "$mm_rsp"; escaped_mm_rsp="$NINJA_ESCAPED_PATH"
1649
+ set_ninja_escaped_path "$c_signature_file"; escaped_c_signature="$NINJA_ESCAPED_PATH"
1650
+ set_ninja_escaped_path "$cxx_signature_file"; escaped_cxx_signature="$NINJA_ESCAPED_PATH"
1651
+ set_ninja_escaped_path "$mm_signature_file"; escaped_mm_signature="$NINJA_ESCAPED_PATH"
1652
+
1653
+ {
1654
+ printf 'ninja_required_version = 1.10\n\n'
1655
+ printf 'rule compile_c\n'
1656
+ printf ' command = rm -f ${out}.sig ${out}.tmp ${out}.d.tmp && %s"@$rsp" -MMD -MP -MF ${out}.d.tmp -MT $out -c $in -o ${out}.tmp && mv ${out}.tmp $out && mv ${out}.d.tmp ${out}.d && cp "$signature" ${out}.sig\n' "$clang_command"
1657
+ printf ' description = CC $in\n'
1658
+ printf ' depfile = ${out}.d\n'
1659
+ printf ' deps = gcc\n'
1660
+ printf '\n'
1661
+ printf 'rule compile_cxx\n'
1662
+ printf ' command = rm -f ${out}.sig ${out}.tmp ${out}.d.tmp && %s"@$rsp" $extra -MMD -MP -MF ${out}.d.tmp -MT $out -c $in -o ${out}.tmp && mv ${out}.tmp $out && mv ${out}.d.tmp ${out}.d && cp "$signature" ${out}.sig\n' "$clangxx_command"
1663
+ printf ' description = CXX $in\n'
1664
+ printf ' depfile = ${out}.d\n'
1665
+ printf ' deps = gcc\n'
1666
+ printf '\n'
1667
+ printf 'rule compile_mm\n'
1668
+ printf ' command = rm -f ${out}.sig ${out}.tmp ${out}.d.tmp && %s"@$rsp" $extra -MMD -MP -MF ${out}.d.tmp -MT $out -c $in -o ${out}.tmp && mv ${out}.tmp $out && mv ${out}.d.tmp ${out}.d && cp "$signature" ${out}.sig\n' "$clangxx_command"
1669
+ printf ' description = OBJCXX $in\n'
1670
+ printf ' depfile = ${out}.d\n'
1671
+ printf ' deps = gcc\n'
1672
+ printf '\n'
1673
+
1674
+ # Ninja gives equally deep ready edges ascending build-statement IDs. Emit
1675
+ # only compile edges in the deterministic LPT-like order computed above.
1676
+ # OBJ_FILES remains in its original order, preserving link order/signatures.
1677
+ local source_kind source_size src obj extra pch quoted_pch escaped_object
1678
+ while IFS=$'\t' read -r source_kind source_size src; do
1679
+ case "$source_kind" in
1680
+ c)
1681
+ set_obj_path "$src" .o; obj="$OBJ_PATH_RESULT"
1682
+ write_ninja_compile_edge "$obj" compile_c "$src" "$c_rsp" "$c_signature_file"
1683
+ printf ' rsp = %s\n' "$escaped_c_rsp"
1684
+ printf ' signature = %s\n' "$escaped_c_signature"
1685
+ ;;
1686
+ cxx)
1687
+ set_obj_path "$src" .cxx.o; obj="$OBJ_PATH_RESULT"
1688
+ extra=""
1689
+ pch=""
1690
+ if is_geatsc_generated_source "$src"; then
1691
+ extra="-Wno-parentheses-equality"
1692
+ pch_for_source cxx "$src"
1693
+ if [[ -n "$PCH_FOR_SOURCE_RESULT" ]]; then
1694
+ pch="$PCH_FOR_SOURCE_RESULT"
1695
+ set_ninja_shell_quoted "$pch"; quoted_pch="$NINJA_SHELL_QUOTED"
1696
+ extra+=" -include-pch $quoted_pch"
1697
+ fi
1698
+ fi
1699
+ write_ninja_compile_edge "$obj" compile_cxx "$src" "$cxx_rsp" "$cxx_signature_file" "$pch"
1700
+ printf ' rsp = %s\n' "$escaped_cxx_rsp"
1701
+ printf ' signature = %s\n' "$escaped_cxx_signature"
1702
+ printf ' extra = %s\n' "$extra"
1703
+ ;;
1704
+ mm)
1705
+ set_obj_path "$src" .mm.o; obj="$OBJ_PATH_RESULT"
1706
+ extra=""
1707
+ pch=""
1708
+ if is_geatsc_generated_source "$src"; then
1709
+ extra="-Wno-parentheses-equality"
1710
+ pch_for_source mm "$src"
1711
+ if [[ -n "$PCH_FOR_SOURCE_RESULT" ]]; then
1712
+ pch="$PCH_FOR_SOURCE_RESULT"
1713
+ set_ninja_shell_quoted "$pch"; quoted_pch="$NINJA_SHELL_QUOTED"
1714
+ extra+=" -include-pch $quoted_pch"
1715
+ fi
1716
+ fi
1717
+ write_ninja_compile_edge "$obj" compile_mm "$src" "$mm_rsp" "$mm_signature_file" "$pch"
1718
+ printf ' rsp = %s\n' "$escaped_mm_rsp"
1719
+ printf ' signature = %s\n' "$escaped_mm_signature"
1720
+ printf ' extra = %s\n' "$extra"
1721
+ ;;
1722
+ *)
1723
+ echo "Unsupported scheduled source kind: $source_kind" >&2
1724
+ return 1
1725
+ ;;
1726
+ esac
1727
+ done <<< "$compile_schedule"
1728
+
1729
+ printf '\nbuild gea_objects: phony'
1730
+ local object_file
1731
+ for object_file in "${OBJ_FILES[@]}"; do
1732
+ set_ninja_escaped_path "$object_file"; escaped_object="$NINJA_ESCAPED_PATH"
1733
+ printf ' %s' "$escaped_object"
1734
+ done
1735
+ printf '\ndefault gea_objects\n'
1736
+ } > "$ninja_tmp"
1737
+ write_content_stable_file "$ninja_file" "$ninja_tmp"
1738
+ write_content_stable_file "$ninja_inputs_file" "$ninja_inputs_tmp"
1739
+
1740
+ # deps=gcc normally lets Ninja remove .d files after importing them into its
1741
+ # database. Keep them so GEA_MACOS_GENERATOR=Shell can reuse Ninja objects.
1742
+ if [[ "${GEA_MACOS_NINJA_DRY_RUN:-0}" == "1" ]]; then
1743
+ macos_run_tracked_child ninja -C "$BUILD_DIR" -f "$ninja_file" -d keepdepfile -n -j "$JOBS" gea_objects
1744
+ else
1745
+ macos_run_tracked_child ninja -C "$BUILD_DIR" -f "$ninja_file" -d keepdepfile -j "$JOBS" gea_objects
1746
+ fi
1747
+ }
1748
+
1749
+ COMPILE_FAILED=0
1750
+ NINJA_DEPS_INVALIDATED=0
1751
+
1752
+ wait_for_compile_slot() {
1753
+ (( ${#PIDS[@]} < JOBS )) && return 0
1754
+ # macOS ships bash 3.2, which has neither `wait -n` (wait for ANY job) nor
1755
+ # its `-p` pid capture. Wait on the OLDEST outstanding job instead — a
1756
+ # slightly more conservative slot policy that still honours the JOBS cap and
1757
+ # keeps the pipeline saturated in practice.
1758
+ local finished_pid="${PIDS[0]}"
1759
+ if ! wait "$finished_pid"; then
1760
+ COMPILE_FAILED=1
1761
+ # Do not keep feeding the queue after the first failed TU. Let only the
1762
+ # already-running peers finish (at most JOBS-1), then return failure; with
1763
+ # `set -e` the caller exits and releases the per-app build lock.
1764
+ local peer_pid
1765
+ for peer_pid in ${PIDS[@]+"${PIDS[@]}"}; do
1766
+ [[ "$peer_pid" == "$finished_pid" ]] && continue
1767
+ wait "$peer_pid" 2>/dev/null || true
1768
+ done
1769
+ PIDS=()
1770
+ return 1
1771
+ fi
1772
+ local next=()
1773
+ local pid
1774
+ for pid in ${PIDS[@]+"${PIDS[@]}"}; do
1775
+ [[ "$pid" == "$finished_pid" ]] && continue
1776
+ next+=("$pid")
1777
+ done
1778
+ PIDS=(${next[@]+"${next[@]}"})
1779
+ }
1780
+
1781
+ wait_for_all_compiles() {
1782
+ local pid
1783
+ for pid in ${PIDS[@]+"${PIDS[@]}"}; do
1784
+ if ! wait "$pid"; then
1785
+ COMPILE_FAILED=1
1786
+ fi
1787
+ done
1788
+ PIDS=()
1789
+ if (( COMPILE_FAILED != 0 )); then
1790
+ exit 1
1791
+ fi
1792
+ }
1793
+
1794
+ queue_compile() {
1795
+ local kind="$1"
1796
+ local src="$2"
1797
+ local obj
1798
+ local sig
1799
+ local sigFile
1800
+ local objTmp
1801
+ local depTmp
1802
+ local sigTmp
1803
+ local pchArgs=()
1804
+ local generatedWarningArgs=()
1805
+ case "$kind" in
1806
+ c) set_obj_path "$src" .o; obj="$OBJ_PATH_RESULT" ;;
1807
+ cxx) set_obj_path "$src" .cxx.o; obj="$OBJ_PATH_RESULT" ;;
1808
+ mm) set_obj_path "$src" .mm.o; obj="$OBJ_PATH_RESULT" ;;
1809
+ esac
1810
+ OBJ_FILES+=("$obj")
1811
+ case "$kind" in
1812
+ c) sig="$COMPILE_SIGNATURE_C" ;;
1813
+ cxx) sig="$COMPILE_SIGNATURE_CXX" ;;
1814
+ mm) sig="$COMPILE_SIGNATURE_MM" ;;
1815
+ esac
1816
+ sigFile="$obj.sig"
1817
+ if is_geatsc_generated_source "$src"; then
1818
+ # Abbreviated generated method templates frequently compare dependent
1819
+ # values inside an extra pair of parentheses. Clang's default diagnostic
1820
+ # produces several notes per instantiation (thousands for Three.js apps),
1821
+ # adding log I/O and diagnostic work without identifying a source bug.
1822
+ generatedWarningArgs=(-Wno-parentheses-equality)
1823
+ case "$kind" in
1824
+ cxx|mm)
1825
+ pch_for_source "$kind" "$src"
1826
+ [[ -n "$PCH_FOR_SOURCE_RESULT" ]] && pchArgs=(-include-pch "$PCH_FOR_SOURCE_RESULT")
1827
+ ;;
1828
+ esac
1829
+ fi
1830
+ if ! needs_compile "$src" "$obj" "$sigFile" "$sig"; then
1831
+ return
1832
+ fi
1833
+ objTmp="$obj.tmp.$$"
1834
+ depTmp="$obj.d.tmp.$$"
1835
+ sigTmp="$sigFile.tmp.$$"
1836
+ # Keep the last successful object available until clang succeeds, but remove
1837
+ # its completion marker now so interruption can never make it look current.
1838
+ rm -f "$sigFile" "$objTmp" "$depTmp" "$sigTmp"
1839
+ # If the Shell fallback recompiles anything after a fresh Ninja build, its
1840
+ # new depfile may contain a different header set than Ninja's binary deps
1841
+ # database. Drop the database so the next Ninja invocation performs the safe
1842
+ # one-time rebuild above instead of trusting stale binary dependency edges.
1843
+ if [[ "$NINJA_DEPS_INVALIDATED" == "0" ]]; then
1844
+ rm -f "$BUILD_DIR/.ninja_deps"
1845
+ NINJA_DEPS_INVALIDATED=1
1846
+ fi
1847
+ case "$kind" in
1848
+ c)
1849
+ ( "${CLANG_CMD[@]}" "$GEA_MACOS_OPT_LEVEL" ${GEA_MACOS_DEBUG_INFO} -DGEA_EMBEDDED_GIF_C_API "${INCLUDES[@]}" \
1850
+ -MMD -MP -MF "$depTmp" -MT "$obj" -c "$src" -o "$objTmp" && \
1851
+ printf "%s\n" "$sig" > "$sigTmp" && mv "$objTmp" "$obj" && mv "$depTmp" "$obj.d" && mv "$sigTmp" "$sigFile" ) &
1852
+ ;;
1853
+ cxx)
1854
+ ( "${CLANGXX_CMD[@]}" -std=c++20 "$GEA_MACOS_OPT_LEVEL" ${GEA_MACOS_DEBUG_INFO} "${CXX_DEFINES[@]}" "${INCLUDES[@]}" \
1855
+ ${pchArgs[@]+"${pchArgs[@]}"} ${generatedWarningArgs[@]+"${generatedWarningArgs[@]}"} \
1856
+ -x c++ -MMD -MP -MF "$depTmp" -MT "$obj" -c "$src" -o "$objTmp" && \
1857
+ printf "%s\n" "$sig" > "$sigTmp" && mv "$objTmp" "$obj" && mv "$depTmp" "$obj.d" && mv "$sigTmp" "$sigFile" ) &
1858
+ ;;
1859
+ mm)
1860
+ ( "${CLANGXX_CMD[@]}" -std=c++20 -ObjC++ -fobjc-arc "$GEA_MACOS_OPT_LEVEL" ${GEA_MACOS_DEBUG_INFO} "${CXX_DEFINES[@]}" "${INCLUDES[@]}" \
1861
+ ${pchArgs[@]+"${pchArgs[@]}"} ${generatedWarningArgs[@]+"${generatedWarningArgs[@]}"} \
1862
+ -MMD -MP -MF "$depTmp" -MT "$obj" -c "$src" -o "$objTmp" && \
1863
+ printf "%s\n" "$sig" > "$sigTmp" && mv "$objTmp" "$obj" && mv "$depTmp" "$obj.d" && mv "$sigTmp" "$sigFile" ) &
1864
+ ;;
1865
+ esac
1866
+ PIDS+=($!)
1867
+ wait_for_compile_slot
1868
+ }
1869
+
1870
+ COMPILE_SIGNATURE_C="$(compile_signature c)"
1871
+ COMPILE_SIGNATURE_CXX="$(compile_signature cxx)"
1872
+ COMPILE_SIGNATURE_MM="$(compile_signature mm)"
1873
+
1874
+ if [[ "$MACOS_GENERATOR" == "Ninja" ]]; then
1875
+ compile_with_ninja
1876
+ else
1877
+ for src in "${C_SOURCES[@]}"; do queue_compile c "$src"; done
1878
+ for src in "${CXX_SOURCES[@]}"; do queue_compile cxx "$src"; done
1879
+ for src in "${MM_SOURCES[@]}"; do queue_compile mm "$src"; done
1880
+ wait_for_all_compiles
1881
+ fi
1882
+ timing_mark compile
1883
+
1884
+ # AVFoundation is unconditional: targets/shared/apple_audio.mm (the real audio
1885
+ # backend, oscillators + file/PCM playback) is built into every macOS app, not
1886
+ # just apple-native ones.
1887
+ LINK_FRAMEWORKS=(-framework Cocoa -framework QuartzCore -framework CoreText -framework IOKit -framework ImageIO -framework CoreGraphics -framework SystemConfiguration -framework AVFoundation)
1888
+ if [[ "$APPLE_NATIVE" == "1" ]]; then
1889
+ # The apple-native bridge emits Objective-C++ for every framework in the
1890
+ # metadata (UIKit + the iOS-only AVFoundation camera shims are compiled out
1891
+ # on macOS). The remaining macOS-available frameworks it references must be
1892
+ # linked. AppKit/Foundation/CoreGraphics come via Cocoa.
1893
+ LINK_FRAMEWORKS+=(
1894
+ -framework Metal -framework MetalKit -framework MapKit
1895
+ -framework CoreLocation -framework AVFoundation -framework CoreMedia
1896
+ -framework Photos
1897
+ )
1898
+ fi
1899
+
1900
+ LINK_OUTPUT="$MACOS_DIR/$APP_EXEC"
1901
+ LINK_OUTPUT_TMP="$LINK_OUTPUT.tmp.$$"
1902
+ LINK_SIGNATURE_FILE="$BUILD_DIR/link.sig"
1903
+ LINK_SIGNATURE_TMP="$LINK_SIGNATURE_FILE.tmp.$$"
1904
+ {
1905
+ printf 'compiler=%s\n' "$MACOS_COMPILER_VERSION"
1906
+ printf 'opt=%s\n' "$GEA_MACOS_OPT_LEVEL"
1907
+ printf 'debug=%s\n' "${GEA_MACOS_DEBUG_INFO:-}"
1908
+ printf 'framework=%s\n' "${LINK_FRAMEWORKS[@]}"
1909
+ printf 'object=%s\n' "${OBJ_FILES[@]}"
1910
+ } > "$LINK_SIGNATURE_TMP"
1911
+
1912
+ LINK_NEEDED=0
1913
+ if [[ ! -x "$LINK_OUTPUT" ]] || ! cmp -s "$LINK_SIGNATURE_TMP" "$LINK_SIGNATURE_FILE"; then
1914
+ LINK_NEEDED=1
1915
+ else
1916
+ for obj in "${OBJ_FILES[@]}"; do
1917
+ if [[ "$obj" -nt "$LINK_OUTPUT" ]]; then
1918
+ LINK_NEEDED=1
1919
+ break
1920
+ fi
1921
+ done
1922
+ fi
1923
+
1924
+ if (( LINK_NEEDED != 0 )); then
1925
+ # Preserve the last complete executable until the linker succeeds. Clear both
1926
+ # completion markers first so a killed link is retried and re-signed.
1927
+ rm -f "$LINK_SIGNATURE_FILE" "$BUILD_DIR/codesign.complete" "$LINK_OUTPUT_TMP"
1928
+ macos_run_tracked_child clang++ -std=c++20 -ObjC++ -fobjc-arc "$GEA_MACOS_OPT_LEVEL" ${GEA_MACOS_DEBUG_INFO} \
1929
+ "${LINK_FRAMEWORKS[@]}" \
1930
+ "${OBJ_FILES[@]}" \
1931
+ -o "$LINK_OUTPUT_TMP"
1932
+ mv "$LINK_OUTPUT_TMP" "$LINK_OUTPUT"
1933
+ mv "$LINK_SIGNATURE_TMP" "$LINK_SIGNATURE_FILE"
1934
+ else
1935
+ rm -f "$LINK_SIGNATURE_TMP"
1936
+ fi
1937
+ if (( LINK_NEEDED != 0 )); then BUNDLE_CONTENT_CHANGED=1; fi
1938
+ timing_mark link
1939
+
1940
+ # Convert the app icon only when its resolved source, manifest metadata, or the
1941
+ # icon tooling changed. Ten `sips` subprocesses made this the largest historical
1942
+ # no-op phase.
1943
+ if [[ -n "$APP_META" ]]; then
1944
+ ICON_OUTPUT="$RES_DIR/AppIcon.icns"
1945
+ ICON_INPUT_SIGNATURE_FILE="$BUILD_DIR/AppIcon.inputs"
1946
+ ICON_INPUT_SIGNATURE="$(node "$GEA_CLI" apps apple-icons "$APP_ID" --platform macos --signature)"
1947
+ ICON_NEEDED=0
1948
+ if [[ "$MACOS_PIPELINE_CACHE" == "0" || ! -f "$ICON_OUTPUT" || ! -f "$ICON_INPUT_SIGNATURE_FILE" || \
1949
+ "$(cat "$ICON_INPUT_SIGNATURE_FILE" 2>/dev/null || true)" != "$ICON_INPUT_SIGNATURE" ]]; then
1950
+ ICON_NEEDED=1
1951
+ fi
1952
+ if (( ICON_NEEDED != 0 )); then
1953
+ ICON_HASH_BEFORE="$(shasum -a 256 "$ICON_OUTPUT" 2>/dev/null | awk '{print $1}' || true)"
1954
+ node "$GEA_CLI" apps apple-icons "$APP_ID" --platform macos \
1955
+ --build-dir "$BUILD_DIR" --resources-dir "$RES_DIR" >/dev/null
1956
+ ICON_HASH_AFTER="$(shasum -a 256 "$ICON_OUTPUT" | awk '{print $1}')"
1957
+ ICON_SIGNATURE_TMP="$ICON_INPUT_SIGNATURE_FILE.tmp.$$"
1958
+ printf '%s\n' "$ICON_INPUT_SIGNATURE" > "$ICON_SIGNATURE_TMP"
1959
+ replace_content_stable "$ICON_INPUT_SIGNATURE_FILE" "$ICON_SIGNATURE_TMP" || true
1960
+ if [[ "$ICON_HASH_BEFORE" != "$ICON_HASH_AFTER" ]]; then BUNDLE_CONTENT_CHANGED=1; fi
1961
+ fi
1962
+ fi
1963
+
1964
+ # Copy any TTF / OTF referenced by @font-face in CSS — for resident-app
1965
+ # builds we walk every resident's app dir so fonts used by other tiles
1966
+ # are also bundled.
1967
+ if [[ "$MACOS_PIPELINE_CACHE" == "0" ]]; then
1968
+ FONTS_OUT="$RES_DIR/Fonts"
1969
+ mkdir -p "$FONTS_OUT"
1970
+ APP_DIRS_FOR_FONTS=()
1971
+ if [[ ${#RESIDENT_IDS[@]} -eq 0 ]]; then
1972
+ d="$(resolve_app_dir "$APP_ID")"
1973
+ [[ -n "$d" ]] && APP_DIRS_FOR_FONTS+=("$d")
1974
+ else
1975
+ for id in "${RESIDENT_IDS[@]}"; do
1976
+ d="$(resolve_app_dir "$id")"
1977
+ [[ -n "$d" ]] && APP_DIRS_FOR_FONTS+=("$d")
1978
+ done
1979
+ fi
1980
+ for d in "${APP_DIRS_FOR_FONTS[@]}"; do
1981
+ node -e "
1982
+ const fs = require('fs');
1983
+ const path = require('path');
1984
+ const appDir = process.argv[1];
1985
+ const out = process.argv[2];
1986
+ const sharedDirs = process.argv.slice(3);
1987
+ function walk(dir) {
1988
+ if (!fs.existsSync(dir)) return;
1989
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1990
+ const full = path.join(dir, entry.name);
1991
+ if (entry.isDirectory()) {
1992
+ if (entry.name === 'node_modules' || entry.name === 'dist') continue;
1993
+ walk(full);
1994
+ } else if (entry.isFile() && entry.name.endsWith('.css')) {
1995
+ const css = fs.readFileSync(full, 'utf8').replace(/\/\*[\s\S]*?\*\//g, '');
1996
+ const re = /@font-face\s*\{([^}]+)\}/gi;
1997
+ let m;
1998
+ while ((m = re.exec(css)) !== null) {
1999
+ const block = m[1];
2000
+ const srcMatch = block.match(/src\s*:\s*url\(\s*(?:\"([^\"]+)\"|'([^']+)'|([^)\"']+))\s*\)/i);
2001
+ if (!srcMatch) continue;
2002
+ const raw = (srcMatch[1] || srcMatch[2] || srcMatch[3] || '').trim();
2003
+ if (!raw || /^https?:\/\//i.test(raw) || raw.startsWith('data:')) continue;
2004
+ const clean = raw.split(/[?#]/, 1)[0];
2005
+ const resolved = path.resolve(path.dirname(full), clean);
2006
+ if (!fs.existsSync(resolved)) continue;
2007
+ const base = path.basename(resolved);
2008
+ const dest = path.join(out, base);
2009
+ if (fs.existsSync(dest)) continue;
2010
+ fs.copyFileSync(resolved, dest);
2011
+ console.log(' copied font:', base);
2012
+ }
2013
+ }
2014
+ }
2015
+ }
2016
+ walk(appDir);
2017
+ for (const dir of sharedDirs) walk(dir);
2018
+ " "$d" "$FONTS_OUT" "${GEA_SHARED_COMPONENT_DIRS[@]}"
2019
+ done
2020
+
2021
+ # Per-app window config — the macOS main reads window.json out of Resources/
2022
+ # at launch to pick up size, transparent title bar, dark appearance, etc.
2023
+ # Source path is apps/<id>/macos.json so the file lives next to the JSX
2024
+ # it configures; we copy it as window.json so the runtime path is stable
2025
+ # regardless of which app id produced it.
2026
+ APP_DIR_FOR_CONFIG="$(resolve_app_dir "$APP_ID")"
2027
+ if [[ -n "$APP_DIR_FOR_CONFIG" && -f "$APP_DIR_FOR_CONFIG/macos.json" ]]; then
2028
+ cp "$APP_DIR_FOR_CONFIG/macos.json" "$RES_DIR/window.json"
2029
+ fi
2030
+ else
2031
+ APP_DIRS_FOR_FONTS=()
2032
+ APP_DIR_FOR_CONFIG="$(resolve_app_dir "$APP_ID")"
2033
+ if [[ ${#RESIDENT_IDS[@]} -eq 0 ]]; then
2034
+ [[ -n "$SINGLE_APP_DIR" ]] && APP_DIRS_FOR_FONTS+=("$SINGLE_APP_DIR")
2035
+ else
2036
+ for id in "${RESIDENT_IDS[@]}"; do
2037
+ d="$(resolve_app_dir "$id")"
2038
+ [[ -n "$d" ]] && APP_DIRS_FOR_FONTS+=("$d")
2039
+ done
2040
+ fi
2041
+ RESOURCE_SYNC_ARGS=(
2042
+ --resources-dir "$RES_DIR"
2043
+ --manifest "$BUILD_DIR/resources-owned.json"
2044
+ --config-dir "$APP_DIR_FOR_CONFIG"
2045
+ )
2046
+ for d in "${GEA_SHARED_COMPONENT_DIRS[@]}"; do
2047
+ RESOURCE_SYNC_ARGS+=(--shared-css-dir "$d")
2048
+ done
2049
+ for d in ${APP_DIRS_FOR_FONTS[@]+"${APP_DIRS_FOR_FONTS[@]}"}; do
2050
+ RESOURCE_SYNC_ARGS+=(--app-dir "$d")
2051
+ done
2052
+ RESOURCE_SYNC_RESULT="$(node "$ROOT_DIR/targets/macos/sync-resources.mjs" "${RESOURCE_SYNC_ARGS[@]}")"
2053
+ if [[ "$RESOURCE_SYNC_RESULT" == *'changed=1'* ]]; then BUNDLE_CONTENT_CHANGED=1; fi
2054
+ fi
2055
+
2056
+ # Bundle app audio clips (src/sounds/*.{mp3,wav,ogg,m4a}) into Resources/Sounds
2057
+ # so the native audio host (native-webgl-angle/native/audio_host.mm) can decode
2058
+ # them by basename out of the app bundle. Runs in both pipeline-cache modes.
2059
+ SOUNDS_OUT="$RES_DIR/Sounds"
2060
+ SOUND_APP_DIRS=()
2061
+ if [[ ${#RESIDENT_IDS[@]} -eq 0 ]]; then
2062
+ [[ -n "$SINGLE_APP_DIR" ]] && SOUND_APP_DIRS+=("$SINGLE_APP_DIR")
2063
+ else
2064
+ for id in "${RESIDENT_IDS[@]}"; do
2065
+ d="$(resolve_app_dir "$id")"
2066
+ [[ -n "$d" ]] && SOUND_APP_DIRS+=("$d")
2067
+ done
2068
+ fi
2069
+ for d in ${SOUND_APP_DIRS[@]+"${SOUND_APP_DIRS[@]}"}; do
2070
+ srcSounds="$d/src/sounds"
2071
+ [[ -d "$srcSounds" ]] || continue
2072
+ mkdir -p "$SOUNDS_OUT"
2073
+ for f in "$srcSounds"/*.mp3 "$srcSounds"/*.wav "$srcSounds"/*.ogg "$srcSounds"/*.m4a; do
2074
+ [[ -f "$f" ]] || continue
2075
+ dest="$SOUNDS_OUT/$(basename "$f")"
2076
+ if [[ ! -f "$dest" ]] || ! cmp -s "$f" "$dest"; then
2077
+ cp "$f" "$dest"
2078
+ BUNDLE_CONTENT_CHANGED=1
2079
+ fi
2080
+ done
2081
+ done
2082
+
2083
+ if [[ "$APP_ID" == "gea-companion" ]]; then
2084
+ COMPANION_TOOLS_DIR="$RES_DIR/CompanionTools"
2085
+ # The scripts belong to the app being built, so they come from its own
2086
+ # resolved directory. Composing "<project>/gea-companion/scripts" instead
2087
+ # only found them when the project happened to be the folder above the app.
2088
+ COMPANION_SCRIPTS_DIR="$(resolve_app_dir "$APP_ID")/scripts"
2089
+ mkdir -p "$COMPANION_TOOLS_DIR"
2090
+ for d in "$COMPANION_TOOLS_DIR"/*.mjs; do
2091
+ [[ -f "$d" ]] || continue
2092
+ if [[ ! -f "$COMPANION_SCRIPTS_DIR/$(basename "$d")" ]]; then
2093
+ rm -f "$d"
2094
+ BUNDLE_CONTENT_CHANGED=1
2095
+ fi
2096
+ done
2097
+ for d in "$COMPANION_SCRIPTS_DIR/"*.mjs; do
2098
+ [[ -f "$d" ]] || continue
2099
+ RESOURCE_TMP="$COMPANION_TOOLS_DIR/$(basename "$d").tmp.$$"
2100
+ cp "$d" "$RESOURCE_TMP"
2101
+ if replace_content_stable "$COMPANION_TOOLS_DIR/$(basename "$d")" "$RESOURCE_TMP"; then BUNDLE_CONTENT_CHANGED=1; fi
2102
+ done
2103
+ fi
2104
+ timing_mark resources
2105
+
2106
+ # Ad-hoc sign only after an input changed. The completion stamp is deliberately
2107
+ # written after codesign so an interrupted resource stage is repaired next run.
2108
+ CODESIGN_STAMP="$BUILD_DIR/codesign.complete"
2109
+ CODESIGN_NEEDED="$BUNDLE_CONTENT_CHANGED"
2110
+ if [[ "$MACOS_PIPELINE_CACHE" == "0" || ! -f "$CODESIGN_STAMP" || ! -d "$APP_BUNDLE/Contents/_CodeSignature" ]]; then
2111
+ CODESIGN_NEEDED=1
2112
+ elif find "$APP_BUNDLE/Contents" -path "$APP_BUNDLE/Contents/_CodeSignature" -prune -o \
2113
+ -type f -newer "$CODESIGN_STAMP" -print -quit | grep -q .; then
2114
+ CODESIGN_NEEDED=1
2115
+ fi
2116
+ if (( CODESIGN_NEEDED != 0 )); then
2117
+ rm -f "$CODESIGN_STAMP"
2118
+ if ! codesign --force --sign - "$APP_BUNDLE"; then
2119
+ echo "Code signing failed for $APP_BUNDLE" >&2
2120
+ exit 1
2121
+ fi
2122
+ touch "$CODESIGN_STAMP"
2123
+ fi
2124
+ timing_mark codesign
2125
+
2126
+ # A hard-muted runtime verification is eligible to launch only when the
2127
+ # configured gate source can be traced through its compiler depfile to exactly
2128
+ # one linked object and the signed executable. The manifest freezes hashes and
2129
+ # repository revisions; the second invocation re-reads every input, so a stale
2130
+ # or replaced artifact fails closed without executing application code.
2131
+ HARD_MUTE_PROVENANCE_FILE="$BUILD_DIR/hard-mute-artifact-provenance.json"
2132
+ if [[ -n "$MACOS_HARD_MUTE_GATE" ]]; then
2133
+ node "$ROOT_DIR/targets/macos/preflight-hard-muted-artifact.mjs" record \
2134
+ --manifest "$HARD_MUTE_PROVENANCE_FILE" \
2135
+ --executable "$LINK_OUTPUT" \
2136
+ --link-signature "$LINK_SIGNATURE_FILE" \
2137
+ --source "$MACOS_HARD_MUTE_SOURCE" \
2138
+ --gate "$MACOS_HARD_MUTE_GATE" \
2139
+ --build-working-directory "$BUILD_INVOCATION_CWD" \
2140
+ --dependency-root "$ROOT_DIR" \
2141
+ --dependency-root "$GEA_COMPILER" \
2142
+ --dependency-root "$GEA_CORE" \
2143
+ --dependency-root "$BUILD_INVOCATION_CWD"
2144
+ node "$ROOT_DIR/targets/macos/preflight-hard-muted-artifact.mjs" verify \
2145
+ --manifest "$HARD_MUTE_PROVENANCE_FILE" \
2146
+ --executable "$LINK_OUTPUT" \
2147
+ --source "$MACOS_HARD_MUTE_SOURCE" \
2148
+ --gate "$MACOS_HARD_MUTE_GATE"
2149
+ fi
2150
+ timing_mark hard-mute-preflight
2151
+
2152
+ echo "Built $APP_BUNDLE"
2153
+ if [[ ${#RESIDENT_IDS[@]} -gt 0 ]]; then
2154
+ echo " residents bundled: ${RESIDENT_IDS[*]}"
2155
+ fi
2156
+ echo "Run: open '$APP_BUNDLE'"