@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,82 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import crypto from 'node:crypto'
6
+
7
+ const args = process.argv.slice(2)
8
+
9
+ function value(name) {
10
+ const index = args.indexOf(name)
11
+ return index >= 0 ? args[index + 1] || '' : ''
12
+ }
13
+
14
+ const graphPath = path.resolve(value('--graph') || '')
15
+ const referencePath = path.resolve(value('--reference') || '')
16
+ if (!value('--graph') || !value('--reference')) {
17
+ process.stderr.write('usage: check-module-graph-freshness.mjs --graph <graph.json> --reference <stamp>\n')
18
+ process.exit(1)
19
+ }
20
+ if (!fs.existsSync(graphPath) || !fs.existsSync(referencePath)) process.exit(0)
21
+
22
+ const graph = JSON.parse(fs.readFileSync(graphPath, 'utf8'))
23
+ const graphDir = path.dirname(graphPath)
24
+ const referenceMtime = fs.statSync(referencePath).mtimeMs
25
+ const visited = new Set()
26
+
27
+ function realInputFor(module) {
28
+ if (module?.assetSource && path.isAbsolute(module.assetSource)) return module.assetSource
29
+ const moduleFile = module?.file
30
+ if (!moduleFile || !path.isAbsolute(moduleFile)) return ''
31
+ if (fs.existsSync(moduleFile)) return moduleFile
32
+ // The native asset Vite plugin represents binary imports as virtual sibling
33
+ // modules. The graph records the virtual id; the real binary remains the
34
+ // generation input whose content/mtime must invalidate generated C++.
35
+ const assetStubSuffix = '.geaassetstub.js'
36
+ if (moduleFile.endsWith(assetStubSuffix)) {
37
+ return moduleFile.slice(0, -assetStubSuffix.length)
38
+ }
39
+ return moduleFile
40
+ }
41
+
42
+ for (const module of Array.isArray(graph.modules) ? graph.modules : []) {
43
+ const input = realInputFor(module)
44
+ if (!input || visited.has(input)) continue
45
+ visited.add(input)
46
+ if (!fs.existsSync(input)) {
47
+ process.stdout.write(`missing:${input}\n`)
48
+ process.exit(0)
49
+ }
50
+ const stat = fs.statSync(input)
51
+ if (stat.mtimeMs > referenceMtime) {
52
+ process.stdout.write(`newer:${input}\n`)
53
+ process.exit(0)
54
+ }
55
+
56
+ // Static assets are represented in the compiler graph by synthetic JS
57
+ // modules, so their originalSource snapshot contains the URL-export module,
58
+ // not the binary bytes. Compare the content digest captured in the graph to
59
+ // catch restored/backdated binary changes exactly.
60
+ if (module.assetSource && typeof module.assetSha256 === 'string') {
61
+ const currentSha256 = crypto.createHash('sha256').update(fs.readFileSync(input)).digest('hex')
62
+ if (currentSha256 !== module.assetSha256) {
63
+ process.stdout.write(`content:${input}\n`)
64
+ process.exit(0)
65
+ }
66
+ continue
67
+ }
68
+
69
+ // Most module-graph original snapshots are byte-for-byte source copies.
70
+ // Comparing those catches checkouts/restores that preserve or backdate
71
+ // mtimes. Some Vite plugins intentionally rewrite their "original" snapshot;
72
+ // detect that baseline and leave those modules on the mtime path above.
73
+ if (!module.originalSource) continue
74
+ const snapshot = path.resolve(graphDir, module.originalSource)
75
+ if (!fs.existsSync(snapshot)) continue
76
+ const current = fs.readFileSync(input)
77
+ const captured = fs.readFileSync(snapshot)
78
+ if (current.length === captured.length && !current.equals(captured)) {
79
+ process.stdout.write(`content:${input}\n`)
80
+ process.exit(0)
81
+ }
82
+ }
@@ -0,0 +1,32 @@
1
+ import { createHash } from 'node:crypto'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ // These are the compiler's shipped modules and runtime sources, not the native
7
+ // executables, PCHs, package archives, or logs a tool may have left in dist.
8
+ export function compilerInputFingerprint(root) {
9
+ const files = []
10
+ const visit = (directory, accepts) => {
11
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
12
+ const file = path.join(directory, entry.name)
13
+ if (entry.isDirectory()) visit(file, accepts)
14
+ else if (entry.isFile() && accepts(entry.name)) files.push(file)
15
+ }
16
+ }
17
+ visit(path.join(root, 'dist'), name => name.endsWith('.js') || name.endsWith('.d.ts'))
18
+ // cli-emit copies these from source, so hashing only dist could reuse stale
19
+ // runtime output after a header change. Include new runtime files as well.
20
+ visit(path.join(root, 'src/targets/cpp/runtime'), () => true)
21
+ files.push(path.join(root, 'package.json'))
22
+ const hash = createHash('sha256').update('gea-compiler-inputs-v1\0')
23
+ for (const file of files.sort()) {
24
+ hash.update(path.relative(root, file)).update('\0').update(fs.readFileSync(file)).update('\0')
25
+ }
26
+ return hash.digest('hex')
27
+ }
28
+
29
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
30
+ if (!process.argv[2]) throw new Error('Usage: compiler-input-fingerprint.mjs <compiler-root>')
31
+ process.stdout.write(compilerInputFingerprint(path.resolve(process.argv[2])))
32
+ }
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+
6
+ function fail(message) {
7
+ process.stderr.write(`${message}\n`)
8
+ process.exit(1)
9
+ }
10
+
11
+ function readOption(name) {
12
+ const index = process.argv.indexOf(name)
13
+ return index >= 0 ? process.argv[index + 1] : undefined
14
+ }
15
+
16
+ function hasFlag(name) {
17
+ return process.argv.includes(name)
18
+ }
19
+
20
+ function isNonEmptyFile(filePath) {
21
+ try {
22
+ const stats = fs.statSync(filePath)
23
+ return stats.isFile() && stats.size > 0
24
+ } catch {
25
+ return false
26
+ }
27
+ }
28
+
29
+ const outDirOption = readOption('--out-dir')
30
+ if (!outDirOption) fail('usage: finalize-apple-native-output.mjs --out-dir <generated-dir>')
31
+
32
+ const outDir = path.resolve(outDirOption)
33
+ const checkOnly = hasFlag('--check-only')
34
+ const expectedState = readOption('--expected-state')
35
+ if (expectedState !== undefined && expectedState !== '0' && expectedState !== '1') {
36
+ fail('--expected-state must be 0 or 1')
37
+ }
38
+ const supportPath = path.join(outDir, 'generated_support.hpp')
39
+ const metadataPath = path.join(outDir, 'gea-apple-metadata.json')
40
+ const bridgeDir = path.join(outDir, 'gea', 'apple')
41
+ const bridgeHeader = path.join(bridgeDir, 'native_bridge.h')
42
+ const bridgeSource = path.join(bridgeDir, 'native_bridge.mm')
43
+ const bridgeInclude = '#include "gea/apple/native_bridge.h"'
44
+ if (!isNonEmptyFile(supportPath)) {
45
+ fail(`generation did not produce a non-empty generated_support.hpp: ${supportPath}`)
46
+ }
47
+ const support = fs.readFileSync(supportPath, 'utf8')
48
+
49
+ let state
50
+ if (support.includes(bridgeInclude)) {
51
+ const missing = [bridgeHeader, bridgeSource].filter((file) => !isNonEmptyFile(file))
52
+ if (missing.length > 0) {
53
+ fail(`generated_support.hpp includes the Apple native bridge, but generation did not produce non-empty files: ${missing.join(', ')}`)
54
+ }
55
+ state = '1'
56
+ } else {
57
+ state = '0'
58
+ }
59
+
60
+ if (expectedState !== undefined && state !== expectedState) {
61
+ fail(`generated Apple-native state ${state} does not match expected state ${expectedState}`)
62
+ }
63
+
64
+ if (checkOnly) {
65
+ if (state === '0') {
66
+ const stale = [metadataPath, bridgeDir].filter((file) => fs.existsSync(file))
67
+ if (stale.length > 0) fail(`cached non-Apple output retains stale Apple-native files: ${stale.join(', ')}`)
68
+ }
69
+ } else if (state === '0') {
70
+ // The compiler does not own stale plugin output. Remove it only after a
71
+ // successful generation whose transformed support no longer references the
72
+ // bridge; failed generations therefore retain their last complete tree.
73
+ fs.rmSync(bridgeDir, { recursive: true, force: true })
74
+ fs.rmSync(metadataPath, { force: true })
75
+ }
76
+
77
+ process.stdout.write(state)
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ // Per-resident-app entry.cpp generator (macOS-flavoured).
3
+ // Mirrors the template in targets/esp32-s3-touch-amoled-2.06/main/CMakeLists.txt
4
+ // but drops ESP-only host integration. Each entry.cpp exposes
5
+ // <prefix>_init/_frame/_settings_toggle/_mirror_*/_lookup_font*/_ble_* so the
6
+ // generated registry can populate a ResidentApp slot for this app.
7
+
8
+ import { writeFileSync } from 'node:fs'
9
+
10
+ const [, , prefix, outPath] = process.argv
11
+ if (!prefix || !outPath) {
12
+ console.error('Usage: generate-resident-entry.mjs <prefix> <out-cpp>')
13
+ process.exit(1)
14
+ }
15
+
16
+ const code = `// Auto-generated by targets/macos/generate-resident-entry.mjs. Do not edit.
17
+ #include "app.h"
18
+ #include "gea/embedded.h"
19
+ #include "graphics/font.h"
20
+ #include "host/timers.h"
21
+ #include "host/websocket.h"
22
+ #include "host/rtc.h"
23
+ #include "ui/document.h"
24
+ #include "ui/style.h"
25
+
26
+ extern void ${prefix}_top_level();
27
+ extern void ${prefix}_register_styles();
28
+ extern void gea_cpp_clear_microtasks();
29
+
30
+ namespace gea::framework::app::generated::${prefix} {
31
+ void drainMicrotasks();
32
+ } // namespace gea::framework::app::generated::${prefix}
33
+
34
+ void __attribute__((weak)) ${prefix}_register_styles() {}
35
+
36
+ void ${prefix}_init(int w, int h, double devicePixelRatio)
37
+ {
38
+ \tgea::embedded::ui::Document::setPreferredMountSize(w, h);
39
+ \tgea::embedded::ui::setViewportMetrics(w, h, devicePixelRatio);
40
+ \tgea::host::resetAnimationFrameCallbacks();
41
+ \tgea::host::resetScheduledTimers();
42
+ \tgea_cpp_clear_microtasks();
43
+ \tgea::embedded::ui::StyleSheet::instance().clear();
44
+ \t${prefix}_register_styles();
45
+ \tgea::embedded::ui::Document::instance().clear();
46
+ \t${prefix}_top_level();
47
+ \tgea::embedded::ui::Document::instance().refreshMountedIfDirty();
48
+ }
49
+
50
+ void ${prefix}_frame(int timestamp_ms)
51
+ {
52
+ \t// Renderer callbacks create many short-lived native references. Keep the
53
+ \t// cycle collector out of those callbacks so their stack-owned references
54
+ \t// unwind before the collector traces the live graph.
55
+ \t{
56
+ \t\tgea::CycleCollectionDeferral deferCycleCollection;
57
+ \t\tgea::framework::app::generated::${prefix}::drainMicrotasks();
58
+ \t\tgea::host::runAnimationFrameCallbacks(static_cast<double>(timestamp_ms));
59
+ \t\tgea::host::websocket::runCallbacks();
60
+ \t\tgea::host::rtc::runCallbacks();
61
+ \t\tgea::embedded::ui::Document::instance().frame(timestamp_ms);
62
+ \t\tgea::embedded::ui::Document::instance().refreshMountedIfDirty();
63
+ \t}
64
+ \tgea::collectCyclesIfNeeded();
65
+ }
66
+
67
+ void ${prefix}_settings_toggle() {}
68
+ int ${prefix}_mirror_begin_snapshot() { return 0; }
69
+ int ${prefix}_mirror_begin_diff() { return 0; }
70
+ int ${prefix}_mirror_next_record(unsigned char *, int) { return 0; }
71
+ void ${prefix}_mirror_clear_dirty() {}
72
+ void ${prefix}_ble_connected() {}
73
+ void ${prefix}_ble_disconnected() {}
74
+ void ${prefix}_ble_bound() {}
75
+ // Note: <prefix>_lookup_font, lookup_font_for_family, lookup_font_family
76
+ // come from each app's gea_embedded_font_generated.cpp — don't emit
77
+ // duplicates here or the link fails with multiple-definition errors.
78
+ `
79
+
80
+ writeFileSync(outPath, code)
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ // Generates the macOS resident-apps registry .cpp. Mirrors the ESP32
3
+ // generator (scripts/generate-resident-app-registry.mjs) but drops the
4
+ // FreeRTOS / ESP_LOG machinery — macOS uses std::mutex for pending-launch
5
+ // sync and just leaves the swap to be drained from the AppKit main tick.
6
+ //
7
+ // Usage: generate-resident-registry.mjs <out-cpp> <launcher-id> <app-id...>
8
+ // First app-id is treated as the boot/active app.
9
+
10
+ import { writeFileSync } from 'node:fs'
11
+
12
+ const [, , outPath, launcherId, ...appIds] = process.argv
13
+ if (!outPath || !launcherId || appIds.length === 0) {
14
+ console.error('Usage: generate-resident-registry.mjs <out-cpp> <launcher-id> <app-id...>')
15
+ process.exit(1)
16
+ }
17
+
18
+ const safe = (id) => id.replace(/[^A-Za-z0-9_]/g, '_')
19
+ const prefix = (id) => `gea_resident_${safe(id)}`
20
+ const quote = (s) => s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
21
+
22
+ const lines = []
23
+ lines.push('// Auto-generated by targets/macos/generate-resident-registry.mjs.')
24
+ lines.push('// Do not edit.')
25
+ lines.push('#include "app.h"')
26
+ lines.push('#include "apps.h"')
27
+ lines.push('#include "resident_apps.h"')
28
+ lines.push('#include "graphics/font.h"')
29
+ lines.push('')
30
+ lines.push('#include <cstdio>')
31
+ lines.push('#include <cstring>')
32
+ lines.push('#include <mutex>')
33
+ lines.push('')
34
+
35
+ for (const id of appIds) {
36
+ const p = prefix(id)
37
+ lines.push(`extern void ${p}_init(int w, int h, double devicePixelRatio);`)
38
+ lines.push(`extern void ${p}_frame(int timestamp_ms);`)
39
+ lines.push(`extern void ${p}_settings_toggle();`)
40
+ }
41
+ lines.push('')
42
+
43
+ lines.push('namespace gea::framework::apps {')
44
+ lines.push('namespace {')
45
+ lines.push(`constexpr const char *kLauncherAppId = "${quote(launcherId)}";`)
46
+ lines.push('std::mutex &pendingLock() { static std::mutex m; return m; }')
47
+ lines.push('char pendingLaunchAppId[64] = "";')
48
+ lines.push('bool pendingValid = false;')
49
+ lines.push('')
50
+ lines.push('const ResidentApp residentApps[] = {')
51
+ for (const id of appIds) {
52
+ const p = prefix(id)
53
+ lines.push('\t{')
54
+ lines.push(`\t\t.id = "${quote(id)}",`)
55
+ lines.push(`\t\t.displayFlushChunkRows = 0,`)
56
+ lines.push(`\t\t.displayFlushQueueDepth = 0,`)
57
+ lines.push(`\t\t.init = ${p}_init,`)
58
+ lines.push(`\t\t.frame = ${p}_frame,`)
59
+ lines.push(`\t\t.settingsToggle = ${p}_settings_toggle,`)
60
+ lines.push('\t},')
61
+ }
62
+ lines.push('};')
63
+ lines.push(`constexpr int kResidentAppCount = ${appIds.length};`)
64
+ lines.push('const ResidentApp *activeApp = &residentApps[0];')
65
+ lines.push('')
66
+ lines.push('const ResidentApp *findApp(const char *appId)')
67
+ lines.push('{')
68
+ lines.push("\tif (!appId || appId[0] == '\\0') return nullptr;")
69
+ lines.push('\tfor (int i = 0; i < kResidentAppCount; i++) {')
70
+ lines.push('\t\tif (std::strcmp(residentApps[i].id, appId) == 0) return &residentApps[i];')
71
+ lines.push('\t}')
72
+ lines.push('\treturn nullptr;')
73
+ lines.push('}')
74
+ lines.push('} // namespace')
75
+ lines.push('')
76
+ lines.push('namespace generated {')
77
+ lines.push('const ResidentApp *activeResidentApp() { return activeApp; }')
78
+ lines.push('} // namespace generated')
79
+ lines.push('')
80
+ lines.push('bool ResidentApps::isEnabled() { return kResidentAppCount > 0; }')
81
+ lines.push('const char *ResidentApps::activeId() { return activeApp ? activeApp->id : nullptr; }')
82
+ lines.push('')
83
+ lines.push('bool ResidentApps::select(const char *appId)')
84
+ lines.push('{')
85
+ lines.push('\tconst ResidentApp *next = findApp(appId);')
86
+ lines.push('\tif (!next) return false;')
87
+ lines.push('\tactiveApp = next;')
88
+ lines.push('\treturn true;')
89
+ lines.push('}')
90
+ lines.push('')
91
+ lines.push('bool ResidentApps::requestLaunch(const char *appId)')
92
+ lines.push('{')
93
+ lines.push('\tif (!findApp(appId)) return false;')
94
+ lines.push('\tstd::lock_guard guard(pendingLock());')
95
+ lines.push('\tstd::snprintf(pendingLaunchAppId, sizeof(pendingLaunchAppId), "%s", appId);')
96
+ lines.push('\tpendingValid = true;')
97
+ lines.push('\treturn true;')
98
+ lines.push('}')
99
+ lines.push('')
100
+ lines.push('bool ResidentApps::consumeLaunch(char *dst, std::size_t cap)')
101
+ lines.push('{')
102
+ lines.push('\tstd::lock_guard guard(pendingLock());')
103
+ lines.push('\tif (!pendingValid) return false;')
104
+ lines.push('\tif (dst && cap > 0) std::snprintf(dst, cap, "%s", pendingLaunchAppId);')
105
+ lines.push('\tpendingValid = false;')
106
+ lines.push("\tpendingLaunchAppId[0] = '\\0';")
107
+ lines.push('\treturn true;')
108
+ lines.push('}')
109
+ lines.push('')
110
+ lines.push('bool ResidentApps::returnToLauncher(const char *trigger)')
111
+ lines.push('{')
112
+ lines.push('\t(void)trigger;')
113
+ lines.push('\tif (!activeApp || std::strcmp(activeApp->id, kLauncherAppId) == 0) return false;')
114
+ lines.push('\treturn ResidentApps::requestLaunch(kLauncherAppId);')
115
+ lines.push('}')
116
+ lines.push('')
117
+ lines.push('} // namespace gea::framework::apps')
118
+ lines.push('')
119
+ // Application::init / frame / toggleSettings are normally provided by
120
+ // gea_app_entry.cpp (single-app mode). In multi-app mode the launcher's
121
+ // macOS build drops gea_app_entry.cpp and emits this version instead, so
122
+ // Application::init dispatches to whichever resident is currently active.
123
+ lines.push('namespace gea::framework::app {')
124
+ lines.push('void Application::init(int width, int height, double devicePixelRatio)')
125
+ lines.push('{')
126
+ lines.push('\tconst auto *active = gea::framework::apps::generated::activeResidentApp();')
127
+ lines.push('\tif (active && active->init) active->init(width, height, devicePixelRatio);')
128
+ lines.push('}')
129
+ lines.push('void Application::frame(int timestampMs)')
130
+ lines.push('{')
131
+ lines.push('\tconst auto *active = gea::framework::apps::generated::activeResidentApp();')
132
+ lines.push('\tif (active && active->frame) active->frame(timestampMs);')
133
+ lines.push('}')
134
+ lines.push('void Application::toggleSettings()')
135
+ lines.push('{')
136
+ lines.push('\tconst auto *active = gea::framework::apps::generated::activeResidentApp();')
137
+ lines.push('\tif (active && active->settingsToggle) active->settingsToggle();')
138
+ lines.push('}')
139
+ lines.push('} // namespace gea::framework::app')
140
+
141
+ writeFileSync(outPath, lines.join('\n') + '\n')
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # Optional workspace-wide resource lock for compiler-heavy native builds.
4
+ #
5
+ # Normal builds remain concurrent. Set GEA_SERIALIZE_HEAVY_BUILDS=1 only when
6
+ # an uncontaminated benchmark or a memory-constrained machine needs global
7
+ # serialization. Target/output correctness locks remain independent of this.
8
+
9
+ GEA_HEAVY_BUILD_LOCK_HELD="${GEA_HEAVY_BUILD_LOCK_HELD:-0}"
10
+ GEA_HEAVY_BUILD_LOCK_ACTIVE_PATH="${GEA_HEAVY_BUILD_LOCK_ACTIVE_PATH:-}"
11
+ GEA_HEAVY_BUILD_LOCK_TOKEN="${GEA_HEAVY_BUILD_LOCK_TOKEN:-}"
12
+
13
+ gea_heavy_build_lock_field() {
14
+ local lock_path="$1"
15
+ local field="$2"
16
+ sed -n "s/^${field}=//p" "$lock_path" 2>/dev/null | head -n 1
17
+ }
18
+
19
+ gea_heavy_build_process_start() {
20
+ local pid="$1"
21
+ ps -p "$pid" -o lstart= 2>/dev/null | sed 's/^[[:space:]]*//; s/[[:space:]]*$//'
22
+ }
23
+
24
+ gea_heavy_build_lock_owner_is_live() {
25
+ local lock_path="$1"
26
+ local owner_pid=""
27
+ local recorded_start=""
28
+ local current_start=""
29
+
30
+ owner_pid="$(gea_heavy_build_lock_field "$lock_path" pid)"
31
+ case "$owner_pid" in
32
+ ''|*[!0-9]*) return 1 ;;
33
+ esac
34
+ if ! kill -0 "$owner_pid" 2>/dev/null; then
35
+ return 1
36
+ fi
37
+
38
+ # A PID can be reused after a crashed owner. Match the process start time
39
+ # when both the recorded and current platform expose it.
40
+ recorded_start="$(gea_heavy_build_lock_field "$lock_path" process_start)"
41
+ current_start="$(gea_heavy_build_process_start "$owner_pid")"
42
+ if [ -n "$recorded_start" ] && [ -n "$current_start" ] && [ "$recorded_start" != "$current_start" ]; then
43
+ return 1
44
+ fi
45
+ return 0
46
+ }
47
+
48
+ gea_report_heavy_build_lock_owner() {
49
+ local lock_path="$1"
50
+ local owner_pid=""
51
+ local owner_kind=""
52
+ local owner_label=""
53
+ local owner_started=""
54
+
55
+ owner_pid="$(gea_heavy_build_lock_field "$lock_path" pid)"
56
+ owner_kind="$(gea_heavy_build_lock_field "$lock_path" kind)"
57
+ owner_label="$(gea_heavy_build_lock_field "$lock_path" label)"
58
+ owner_started="$(gea_heavy_build_lock_field "$lock_path" started_at)"
59
+ echo "Another heavyweight build is already using this workspace:" >&2
60
+ echo " kind: ${owner_kind:-unknown}" >&2
61
+ echo " target: ${owner_label:-unknown}" >&2
62
+ echo " pid: ${owner_pid:-unknown}" >&2
63
+ echo " started: ${owner_started:-unknown}" >&2
64
+ echo "Unset GEA_SERIALIZE_HEAVY_BUILDS or set GEA_ALLOW_CONCURRENT_HEAVY_BUILDS=1 to run concurrently." >&2
65
+ }
66
+
67
+ gea_acquire_heavy_build_lock() {
68
+ local lock_path="$1"
69
+ local kind="${2:-unknown}"
70
+ local label="${3:-unknown}"
71
+ local lock_parent=""
72
+ local candidate=""
73
+ local recovery_dir=""
74
+ local token=""
75
+ local process_start=""
76
+
77
+ # Concurrency is the normal developer workflow. The workspace-wide lock is
78
+ # deliberately opt-in for controlled benchmarks and low-memory machines.
79
+ if [ "${GEA_SERIALIZE_HEAVY_BUILDS:-0}" != "1" ]; then
80
+ return 0
81
+ fi
82
+ if [ "${GEA_ALLOW_CONCURRENT_HEAVY_BUILDS:-0}" = "1" ]; then
83
+ echo "Heavy-build resource lock overridden for $kind '$label'." >&2
84
+ return 0
85
+ fi
86
+ if [ "$GEA_HEAVY_BUILD_LOCK_HELD" = "1" ]; then
87
+ echo "Heavy-build resource lock is already held by this process." >&2
88
+ return 1
89
+ fi
90
+
91
+ lock_parent="$(dirname "$lock_path")"
92
+ if [ ! -d "$lock_parent" ]; then
93
+ echo "Heavy-build lock parent does not exist: $lock_parent" >&2
94
+ return 1
95
+ fi
96
+
97
+ token="$$.$(date +%s).${RANDOM:-0}"
98
+ candidate="$(mktemp "${lock_path}.candidate.XXXXXX")" || return 1
99
+ recovery_dir="${lock_path}.recovery"
100
+ process_start="$(gea_heavy_build_process_start "$$")"
101
+ kind="${kind//$'\n'/ }"
102
+ label="${label//$'\n'/ }"
103
+ (
104
+ umask 077
105
+ {
106
+ printf 'pid=%s\n' "$$"
107
+ printf 'token=%s\n' "$token"
108
+ printf 'process_start=%s\n' "$process_start"
109
+ printf 'kind=%s\n' "$kind"
110
+ printf 'label=%s\n' "$label"
111
+ printf 'started_at=%s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
112
+ printf 'host=%s\n' "$(hostname 2>/dev/null || echo unknown)"
113
+ printf 'command=%s\n' "$0"
114
+ } > "$candidate"
115
+ )
116
+
117
+ # `ln` installs a fully-written file atomically, avoiding mkdir+pid's small
118
+ # window where a contender can mistake a new lock for stale state.
119
+ if [ ! -d "$recovery_dir" ] && ln "$candidate" "$lock_path" 2>/dev/null; then
120
+ rm -f "$candidate"
121
+ GEA_HEAVY_BUILD_LOCK_HELD=1
122
+ GEA_HEAVY_BUILD_LOCK_ACTIVE_PATH="$lock_path"
123
+ GEA_HEAVY_BUILD_LOCK_TOKEN="$token"
124
+ return 0
125
+ fi
126
+
127
+ if gea_heavy_build_lock_owner_is_live "$lock_path"; then
128
+ rm -f "$candidate"
129
+ gea_report_heavy_build_lock_owner "$lock_path"
130
+ return 1
131
+ fi
132
+
133
+ # Serialize stale-owner recovery. Re-read after winning recovery ownership,
134
+ # because another contender may have replaced the stale file first.
135
+ if ! mkdir "$recovery_dir" 2>/dev/null; then
136
+ rm -f "$candidate"
137
+ echo "Heavy-build lock ownership is changing; retry the build." >&2
138
+ return 1
139
+ fi
140
+ if gea_heavy_build_lock_owner_is_live "$lock_path"; then
141
+ rmdir "$recovery_dir" 2>/dev/null || true
142
+ rm -f "$candidate"
143
+ gea_report_heavy_build_lock_owner "$lock_path"
144
+ return 1
145
+ fi
146
+
147
+ rm -f "$lock_path"
148
+ if ! ln "$candidate" "$lock_path" 2>/dev/null; then
149
+ rmdir "$recovery_dir" 2>/dev/null || true
150
+ rm -f "$candidate"
151
+ echo "Could not recover the stale heavy-build lock; retry the build." >&2
152
+ return 1
153
+ fi
154
+ rmdir "$recovery_dir" 2>/dev/null || true
155
+ rm -f "$candidate"
156
+ GEA_HEAVY_BUILD_LOCK_HELD=1
157
+ GEA_HEAVY_BUILD_LOCK_ACTIVE_PATH="$lock_path"
158
+ GEA_HEAVY_BUILD_LOCK_TOKEN="$token"
159
+ }
160
+
161
+ gea_release_heavy_build_lock() {
162
+ local owner_pid=""
163
+ local owner_token=""
164
+
165
+ if [ "$GEA_HEAVY_BUILD_LOCK_HELD" != "1" ] || [ -z "$GEA_HEAVY_BUILD_LOCK_ACTIVE_PATH" ]; then
166
+ return 0
167
+ fi
168
+ owner_pid="$(gea_heavy_build_lock_field "$GEA_HEAVY_BUILD_LOCK_ACTIVE_PATH" pid)"
169
+ owner_token="$(gea_heavy_build_lock_field "$GEA_HEAVY_BUILD_LOCK_ACTIVE_PATH" token)"
170
+ if [ "$owner_pid" = "$$" ] && [ "$owner_token" = "$GEA_HEAVY_BUILD_LOCK_TOKEN" ]; then
171
+ rm -f "$GEA_HEAVY_BUILD_LOCK_ACTIVE_PATH"
172
+ fi
173
+ GEA_HEAVY_BUILD_LOCK_HELD=0
174
+ GEA_HEAVY_BUILD_LOCK_ACTIVE_PATH=""
175
+ GEA_HEAVY_BUILD_LOCK_TOKEN=""
176
+ }
@@ -0,0 +1,9 @@
1
+ #pragma once
2
+
3
+ #ifdef __OBJC__
4
+ #import <AppKit/AppKit.h>
5
+
6
+ @interface GeaCanvasView : NSView
7
+ @property(nonatomic, assign) int nodeId;
8
+ @end
9
+ #endif