@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.
- package/LICENSE +202 -0
- package/README.md +54 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.js +3076 -0
- package/generated/AVFoundation.d.ts +112 -0
- package/generated/ActivityKit.d.ts +7 -0
- package/generated/AppKit.d.ts +268 -0
- package/generated/CoreGraphics.d.ts +18 -0
- package/generated/CoreLocation.d.ts +28 -0
- package/generated/CoreMedia.d.ts +8 -0
- package/generated/Dispatch.d.ts +2 -0
- package/generated/Foundation.d.ts +13 -0
- package/generated/MapKit.d.ts +28 -0
- package/generated/Metal.d.ts +91 -0
- package/generated/MetalKit.d.ts +22 -0
- package/generated/Photos.d.ts +5 -0
- package/generated/QuartzCore.d.ts +12 -0
- package/generated/SwiftData.d.ts +11 -0
- package/generated/UIKit.d.ts +187 -0
- package/package.json +123 -0
- package/runtime/AVFoundation.js +136 -0
- package/runtime/ActivityKit.js +18 -0
- package/runtime/AppKit.js +313 -0
- package/runtime/CoreGraphics.js +15 -0
- package/runtime/CoreLocation.js +30 -0
- package/runtime/CoreMedia.js +7 -0
- package/runtime/Dispatch.js +11 -0
- package/runtime/Foundation.js +21 -0
- package/runtime/MapKit.js +22 -0
- package/runtime/Metal.js +126 -0
- package/runtime/MetalKit.js +15 -0
- package/runtime/Photos.js +9 -0
- package/runtime/QuartzCore.js +9 -0
- package/runtime/SwiftData.js +29 -0
- package/runtime/UIKit.js +242 -0
- package/runtime/empty.js +1 -0
- package/targets/ios/Info.plist.in +25 -0
- package/targets/ios/README.md +46 -0
- package/targets/ios/build-ios.sh +395 -0
- package/targets/ios/detect-development-team.mjs +109 -0
- package/targets/ios/generate-resident-entry.mjs +67 -0
- package/targets/ios/generate-resident-registry.mjs +162 -0
- package/targets/ios/generate-xcode-project.mjs +520 -0
- package/targets/ios/main/canvas_view.h +7 -0
- package/targets/ios/main/canvas_view.mm +94 -0
- package/targets/ios/main/font_registry.h +17 -0
- package/targets/ios/main/font_registry.mm +129 -0
- package/targets/ios/main/image_bridge.h +13 -0
- package/targets/ios/main/image_bridge.mm +53 -0
- package/targets/ios/main/ios_apps.c +28 -0
- package/targets/ios/main/ios_camera.mm +446 -0
- package/targets/ios/main/ios_display.mm +344 -0
- package/targets/ios/main/ios_main.mm +811 -0
- package/targets/ios/main/ios_memory.cpp +55 -0
- package/targets/ios/main/ios_renderer.h +20 -0
- package/targets/ios/main/ios_renderer.mm +27 -0
- package/targets/ios/main/ios_root_background.cpp +20 -0
- package/targets/ios/main/ios_root_background.h +11 -0
- package/targets/ios/main/ios_sensors.mm +244 -0
- package/targets/ios/main/ios_timers.mm +53 -0
- package/targets/ios/main/renderer/ios_renderer_internal.h +65 -0
- package/targets/ios/main/renderer/ios_renderer_support.mm +111 -0
- package/targets/ios/main/renderer/native_button.mm +179 -0
- package/targets/ios/main/renderer/native_label.mm +161 -0
- package/targets/ios/main/renderer/native_scroll_container.mm +279 -0
- package/targets/ios/main/renderer/view_reconciler.mm +274 -0
- package/targets/macos/Info.plist.in +26 -0
- package/targets/macos/README.md +131 -0
- package/targets/macos/build-child-lifecycle.sh +119 -0
- package/targets/macos/build-macos.sh +2156 -0
- package/targets/macos/check-module-graph-freshness.mjs +82 -0
- package/targets/macos/compiler-input-fingerprint.mjs +32 -0
- package/targets/macos/finalize-apple-native-output.mjs +77 -0
- package/targets/macos/generate-resident-entry.mjs +80 -0
- package/targets/macos/generate-resident-registry.mjs +141 -0
- package/targets/macos/heavy-build-lock.sh +176 -0
- package/targets/macos/main/canvas_view.h +9 -0
- package/targets/macos/main/canvas_view.mm +176 -0
- package/targets/macos/main/color_convert.h +18 -0
- package/targets/macos/main/color_convert.mm +39 -0
- package/targets/macos/main/font_registry.h +35 -0
- package/targets/macos/main/font_registry.mm +350 -0
- package/targets/macos/main/image_bridge.h +16 -0
- package/targets/macos/main/image_bridge.mm +52 -0
- package/targets/macos/main/macos_app_platform.mm +95 -0
- package/targets/macos/main/macos_apps.c +31 -0
- package/targets/macos/main/macos_device_control.mm +56 -0
- package/targets/macos/main/macos_display.mm +484 -0
- package/targets/macos/main/macos_host_device_control.mm +95 -0
- package/targets/macos/main/macos_main.mm +839 -0
- package/targets/macos/main/macos_memory.cpp +31 -0
- package/targets/macos/main/macos_native_shell.h +31 -0
- package/targets/macos/main/macos_native_shell.mm +396 -0
- package/targets/macos/main/macos_network.mm +253 -0
- package/targets/macos/main/macos_renderer.h +39 -0
- package/targets/macos/main/macos_renderer.mm +1969 -0
- package/targets/macos/main/macos_sensors.cpp +148 -0
- package/targets/macos/main/macos_smoke_app.cpp +65 -0
- package/targets/macos/main/macos_timers.mm +52 -0
- package/targets/macos/main/press_bridge.h +21 -0
- package/targets/macos/main/press_bridge.mm +72 -0
- package/targets/macos/main/thermalright_hid_display.h +14 -0
- package/targets/macos/main/thermalright_hid_display.mm +995 -0
- package/targets/macos/native-artifact-provenance.mjs +421 -0
- package/targets/macos/order-compile-edges.mjs +64 -0
- package/targets/macos/preflight-hard-muted-artifact.mjs +93 -0
- package/targets/macos/prune-generated-modules.mjs +147 -0
- package/targets/macos/sync-resources.mjs +153 -0
- package/targets/shared/analyze-audio-capture.mjs +131 -0
- package/targets/shared/apple_audio.mm +1194 -0
- package/targets/shared/apple_audio_selftest.mm +602 -0
- package/targets/shared/apple_audio_testing.h +34 -0
- 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
|
+
}
|