ReviewOS

also looking at this

craft-native/craft

fix(native): preserve workspace sources and native overlay lifecycle

#384
Merged chris wants to merge codex/windows-zig-cache-diagnostics into main
20 files +500 -227

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

packages/android/README.mdmodified+6-0
Changes to packages/android/README.md
@@ -90,6 +90,7 @@ const device = await createAndroidEmulator({
9090 avdmanager: '/ci/pantry/.bin/avdmanager',
9191 image: 'system-images;android-35;google_apis_playstore;x86_64',
9292 acceleration: 'software',
93 memoryMb: 4096,
9394 artifactsDirectory: '/ci/artifacts/android',
9495 acceptLicenses: true,
9596})
@@ -106,6 +107,11 @@ the Android emulator's explicit `-accel off` mode on x86_64 hosts; it provides
106107functional test evidence, with no claim about hardware performance. The public
107108helper defaults to a ten-minute boot budget, and hardware mode cannot exceed
108109it. Software callers may explicitly set `bootTimeoutMs` up to twenty minutes.
110`memoryMb` selects the requested guest RAM, defaulting to 2048 MB. It accepts
111the emulator's documented [1536–8192 MB range](https://developer.android.com/studio/run/emulator-commandline#startup-options).
112The stock Play software verifier requests 4096 MB after retained cold boots
113showed low-memory/process-death churn and a SystemUI ANR. It keeps the same
114image, two virtual CPUs, readiness checks and twenty-minute deadline.
109115The stock Play software CI verifier uses that cold-provisioning profile: the
110116API 35 image took 943,520 ms in the retained diagnostic run
111117[39113277828](https://github.com/craft-native/craft/actions/runs/39113277828).
packages/android/src/emulator.tsmodified+7-2
Changes to packages/android/src/emulator.ts
@@ -14,6 +14,8 @@ export interface AndroidEmulatorOptions {
1414 acceptLicenses?: boolean
1515 /** Defaults to 10 minutes; hardware is capped there, software cold provisioning at 20 minutes. */
1616 bootTimeoutMs?: number
17 /** Requested guest RAM, 1536–8192 MB (Android emulator limits); defaults to 2048. */
18 memoryMb?: number
1719 env?: Record<string, string | undefined>
1820}
1921
@@ -40,6 +42,9 @@ export async function createAndroidEmulator(options: AndroidEmulatorOptions): Pr
4042 const maximumBootTimeoutMs = options.acceleration === 'software' ? 1_200_000 : 600_000
4143 if (!Number.isSafeInteger(bootTimeoutMs) || bootTimeoutMs < 1000 || bootTimeoutMs > maximumBootTimeoutMs)
4244 throw new Error(`Android bootTimeoutMs must be an integer between 1000 and ${maximumBootTimeoutMs} milliseconds`)
45 const memoryMb = options.memoryMb ?? 2048
46 if (!Number.isSafeInteger(memoryMb) || memoryMb < 1536 || memoryMb > 8192)
47 throw new Error('Android memoryMb must be an integer between 1536 and 8192 megabytes')
4348 const port = options.port ?? 5558
4449 if (!Number.isInteger(port) || port < 5554 || port > 5682 || port % 2 !== 0) throw new Error('Android console ports must be even and between 5554 and 5682')
4550 const sdk = resolve(options.sdkRoot)
@@ -128,12 +133,12 @@ export async function createAndroidEmulator(options: AndroidEmulatorOptions): Pr
128133 const hostCpus = cpus()
129134 cpuStart = hostCpus.reduce((total, cpu) => total + Object.values(cpu.times).reduce((sum, value) => sum + value, 0), 0)
130135 idleStart = hostCpus.reduce((total, cpu) => total + cpu.times.idle, 0)
131 child = Bun.spawn([executable, '-avd', name, '-port', String(port), '-no-window', '-no-snapshot', '-no-audio', '-no-boot-anim', '-gpu', 'software', '-memory', '2048', '-cores', '2', '-accel', options.acceleration === 'software' ? 'off' : 'on'], { env, stdout: log, stderr: log })
136 child = Bun.spawn([executable, '-avd', name, '-port', String(port), '-no-window', '-no-snapshot', '-no-audio', '-no-boot-anim', '-gpu', 'software', '-memory', String(memoryMb), '-cores', '2', '-accel', options.acceleration === 'software' ? 'off' : 'on'], { env, stdout: log, stderr: log })
132137 identity = {
133138 image: options.image, acceleration: options.acceleration, accelerationProbeExitCode: acceleration.code,
134139 bootTimeoutMs, bootStartedAt: new Date(bootStartedAt).toISOString(),
135140 host: { cpuModel: hostCpus[0]?.model, logicalCpus: hostCpus.length, availableParallelism: availableParallelism(), memoryBytes: totalmem() },
136 virtualCpus: 2, memoryMb: 2048, serial, avdDirectory: env.ANDROID_AVD_HOME, emulatorPid: child.pid,
141 virtualCpus: 2, memoryMb, serial, avdDirectory: env.ANDROID_AVD_HOME, emulatorPid: child.pid,
137142 }
138143 await Bun.write(join(artifacts, 'android-emulator.json'), JSON.stringify(identity, null, 2) + '\n')
139144 const deadline = bootStartedAt + bootTimeoutMs
packages/typescript/src/__tests__/android-emulator.test.tsmodified+26-0
Changes to packages/typescript/src/__tests__/android-emulator.test.ts
@@ -137,6 +137,19 @@ test('an offline guest that never connected still fails its deadline and is disp
137137 finally { rmSync(root, { recursive: true, force: true }) }
138138}, 10_000)
139139
140test('explicit guest memory reaches the emulator process and its retained identity', async () => {
141 const { root, options } = fixture(true)
142 let session: Awaited<ReturnType<typeof createAndroidEmulator>> | undefined
143 try {
144 session = await createAndroidEmulator({ ...options, memoryMb: 4096 })
145 expect(readFileSync(join(options.sdkRoot, 'calls'), 'utf8')).toContain('-memory 4096')
146 const identity = JSON.parse(readFileSync(join(options.artifactsDirectory, 'android-emulator.json'), 'utf8'))
147 expect(identity.memoryMb).toBe(4096)
148 expect(identity.virtualCpus).toBe(2)
149 }
150 finally { await session?.dispose(); rmSync(root, { recursive: true, force: true }) }
151}, 10_000)
152
140153test('invalid boot budgets fail before starting tools or creating owned device data', async () => {
141154 const { root, options } = fixture(true)
142155 try {
@@ -153,6 +166,18 @@ test('invalid boot budgets fail before starting tools or creating owned device d
153166 finally { rmSync(root, { recursive: true, force: true }) }
154167})
155168
169test('invalid guest memory fails before starting tools or creating owned device data', async () => {
170 const { root, options } = fixture(true)
171 try {
172 for (const memoryMb of [NaN, Infinity, -Infinity, 0, 1535, 8193, 4096.5]) {
173 await expect(createAndroidEmulator({ ...options, memoryMb })).rejects.toThrow('memoryMb')
174 expect(existsSync(join(options.sdkRoot, 'calls'))).toBe(false)
175 expect(existsSync(options.temporaryDirectory!)).toBe(false)
176 }
177 }
178 finally { rmSync(root, { recursive: true, force: true }) }
179})
180
156181test('boot completion alone does not admit a device before SettingsProvider is usable', async () => {
157182 const { root, options } = fixture(true, true)
158183 let session: Awaited<ReturnType<typeof createAndroidEmulator>> | undefined
@@ -266,6 +291,7 @@ test('the SDK process fixture records hardware mode and reaps only its owned pro
266291 session = await createAndroidEmulator(options)
267292 const identity = JSON.parse(readFileSync(join(options.artifactsDirectory, 'android-emulator.json'), 'utf8'))
268293 expect(identity.acceleration).toBe('hardware')
294 expect(identity.memoryMb).toBe(2048)
269295 expect(identity.bootedAt).toBeString()
270296 expect(identity.bootTimeoutMs).toBe(42_000)
271297 expect(identity.bootElapsedMs).toBeGreaterThanOrEqual(0)
scripts/verify-android-emulator.tsmodified+11-3
Changes to scripts/verify-android-emulator.ts
@@ -14,6 +14,10 @@ const diagnostic = args.length === 1
1414// measured API 35 image took 943520 ms; this explicit software-only profile
1515// leaves the public helper default and hardware acceptance at 600000 ms.
1616const bootTimeoutMs = 1_200_000
17// Both retained 2 GB-profile failures show low-memory/process-death churn and
18// a SystemUI ANR. Give the unmodified stock image an explicit 4 GB guest;
19// readiness, software acceleration, CPU count and boot deadline stay intact.
20const memoryMb = 4096
1721const expectedFingerprint = diagnostic ? process.env.CRAFT_ANDROID_DIAGNOSTIC_FINGERPRINT : undefined
1822if (diagnostic && !expectedFingerprint)
1923 throw new Error('The cold-provisioning diagnostic requires the failed stock image fingerprint')
@@ -35,7 +39,7 @@ let session: Awaited<ReturnType<typeof createAndroidEmulator>> | undefined
3539const proof: Record<string, unknown> = {
3640 purpose: diagnostic ? 'software-cold-provisioning-diagnostic-not-release-acceptance' : 'software-emulator-acceptance',
3741 platform: process.platform, architecture: process.arch, acceleration: 'software', sdkmanager, avdmanager, javaHome,
38 provisioningProfile: 'stock-play-software-cold', bootTimeoutMs,
42 provisioningProfile: 'stock-play-software-cold', bootTimeoutMs, memoryMb,
3943 expectedFingerprint, runnerImage: process.env.ImageOS, runnerImageVersion: process.env.ImageVersion,
4044}
4145
@@ -44,7 +48,7 @@ try {
4448 sdkRoot: join(owned, 'sdk'), sdkmanager, avdmanager,
4549 image: 'system-images;android-35;google_apis_playstore;x86_64',
4650 acceleration: 'software', artifactsDirectory: artifacts,
47 bootTimeoutMs,
51 bootTimeoutMs, memoryMb,
4852 temporaryDirectory: owned, acceptLicenses: true,
4953 env: { JAVA_HOME: javaHome },
5054 })
@@ -63,11 +67,15 @@ try {
6367 const abi = await adb(['shell', 'getprop', 'ro.product.cpu.abi'])
6468 const playStore = await adb(['shell', 'pm', 'path', 'com.android.vending'])
6569 const fingerprint = await adb(['shell', 'getprop', 'ro.build.fingerprint'])
70 const meminfo = await adb(['shell', 'cat', '/proc/meminfo'])
71 const guestMemTotalKb = Number(/^MemTotal:\s+(\d+)\s+kB$/m.exec(meminfo)?.[1])
6672 if (api !== '35' || abi !== 'x86_64' || !/^package:\/\S+/.test(playStore) || !fingerprint)
6773 throw new Error('The owned emulator must run the requested stock Play image')
74 if (!Number.isSafeInteger(guestMemTotalKb) || guestMemTotalKb <= 0)
75 throw new Error('The owned emulator must report its actual guest memory')
6876 if (diagnostic && fingerprint !== expectedFingerprint)
6977 throw new Error('The stock Play image changed; this diagnostic cannot explain the retained cold-boot failures')
70 Object.assign(proof, { serial: session.serial, api, abi, playStore, fingerprint, verifiedAt: new Date().toISOString() })
78 Object.assign(proof, { serial: session.serial, api, abi, playStore, fingerprint, guestMemTotalKb, verifiedAt: new Date().toISOString() })
7179}
7280finally {
7381 try {