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+5-1
Changes to packages/android/README.md
@@ -109,9 +109,13 @@ helper defaults to a ten-minute boot budget, and hardware mode cannot exceed
109109it. Software callers may explicitly set `bootTimeoutMs` up to twenty minutes.
110110`memoryMb` selects the requested guest RAM, defaulting to 2048 MB. It accepts
111111the emulator's documented [1536–8192 MB range](https://developer.android.com/studio/run/emulator-commandline#startup-options).
112`virtualCpus` selects the guest CPU count, defaulting to 2 and accepting values
113from 1 through 8. The stock Play software verifier requests four guest CPUs:
114the four-thread Intel runners used for this acceptance profile otherwise starve
115SystemUI during TCG startup and surface a real platform ANR.
112116The stock Play software verifier requests 4096 MB after retained cold boots
113117showed low-memory/process-death churn and a SystemUI ANR. It keeps the same
114image, two virtual CPUs, readiness checks and twenty-minute deadline.
118image, readiness checks and twenty-minute deadline.
115119The stock Play software CI verifier uses that cold-provisioning profile: the
116120API 35 image took 943,520 ms in the retained diagnostic run
117121[39113277828](https://github.com/craft-native/craft/actions/runs/39113277828).
packages/android/src/emulator.tsmodified+7-2
Changes to packages/android/src/emulator.ts
@@ -16,6 +16,8 @@ export interface AndroidEmulatorOptions {
1616 bootTimeoutMs?: number
1717 /** Requested guest RAM, 1536–8192 MB (Android emulator limits); defaults to 2048. */
1818 memoryMb?: number
19 /** Requested guest CPU count, 1–8; defaults to 2. */
20 virtualCpus?: number
1921 env?: Record<string, string | undefined>
2022}
2123
@@ -45,6 +47,9 @@ export async function createAndroidEmulator(options: AndroidEmulatorOptions): Pr
4547 const memoryMb = options.memoryMb ?? 2048
4648 if (!Number.isSafeInteger(memoryMb) || memoryMb < 1536 || memoryMb > 8192)
4749 throw new Error('Android memoryMb must be an integer between 1536 and 8192 megabytes')
50 const virtualCpus = options.virtualCpus ?? 2
51 if (!Number.isSafeInteger(virtualCpus) || virtualCpus < 1 || virtualCpus > 8)
52 throw new Error('Android virtualCpus must be an integer between 1 and 8')
4853 const port = options.port ?? 5558
4954 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')
5055 const sdk = resolve(options.sdkRoot)
@@ -133,12 +138,12 @@ export async function createAndroidEmulator(options: AndroidEmulatorOptions): Pr
133138 const hostCpus = cpus()
134139 cpuStart = hostCpus.reduce((total, cpu) => total + Object.values(cpu.times).reduce((sum, value) => sum + value, 0), 0)
135140 idleStart = hostCpus.reduce((total, cpu) => total + cpu.times.idle, 0)
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 })
141 child = Bun.spawn([executable, '-avd', name, '-port', String(port), '-no-window', '-no-snapshot', '-no-audio', '-no-boot-anim', '-gpu', 'software', '-memory', String(memoryMb), '-cores', String(virtualCpus), '-accel', options.acceleration === 'software' ? 'off' : 'on'], { env, stdout: log, stderr: log })
137142 identity = {
138143 image: options.image, acceleration: options.acceleration, accelerationProbeExitCode: acceleration.code,
139144 bootTimeoutMs, bootStartedAt: new Date(bootStartedAt).toISOString(),
140145 host: { cpuModel: hostCpus[0]?.model, logicalCpus: hostCpus.length, availableParallelism: availableParallelism(), memoryBytes: totalmem() },
141 virtualCpus: 2, memoryMb, serial, avdDirectory: env.ANDROID_AVD_HOME, emulatorPid: child.pid,
146 virtualCpus, memoryMb, serial, avdDirectory: env.ANDROID_AVD_HOME, emulatorPid: child.pid,
142147 }
143148 await Bun.write(join(artifacts, 'android-emulator.json'), JSON.stringify(identity, null, 2) + '\n')
144149 const deadline = bootStartedAt + bootTimeoutMs
packages/typescript/src/__tests__/android-emulator.test.tsmodified+24-0
Changes to packages/typescript/src/__tests__/android-emulator.test.ts
@@ -150,6 +150,18 @@ test('explicit guest memory reaches the emulator process and its retained identi
150150 finally { await session?.dispose(); rmSync(root, { recursive: true, force: true }) }
151151}, 10_000)
152152
153test('explicit guest CPU count reaches the emulator process and its retained identity', async () => {
154 const { root, options } = fixture(true)
155 let session: Awaited<ReturnType<typeof createAndroidEmulator>> | undefined
156 try {
157 session = await createAndroidEmulator({ ...options, virtualCpus: 4 })
158 expect(readFileSync(join(options.sdkRoot, 'calls'), 'utf8')).toContain('-cores 4')
159 const identity = JSON.parse(readFileSync(join(options.artifactsDirectory, 'android-emulator.json'), 'utf8'))
160 expect(identity.virtualCpus).toBe(4)
161 }
162 finally { await session?.dispose(); rmSync(root, { recursive: true, force: true }) }
163}, 10_000)
164
153165test('invalid boot budgets fail before starting tools or creating owned device data', async () => {
154166 const { root, options } = fixture(true)
155167 try {
@@ -178,6 +190,18 @@ test('invalid guest memory fails before starting tools or creating owned device
178190 finally { rmSync(root, { recursive: true, force: true }) }
179191})
180192
193test('invalid guest CPU counts fail before starting tools or creating owned device data', async () => {
194 const { root, options } = fixture(true)
195 try {
196 for (const virtualCpus of [NaN, Infinity, -Infinity, 0, 0.5, 9, 8.5]) {
197 await expect(createAndroidEmulator({ ...options, virtualCpus })).rejects.toThrow('virtualCpus')
198 expect(existsSync(join(options.sdkRoot, 'calls'))).toBe(false)
199 expect(existsSync(options.temporaryDirectory!)).toBe(false)
200 }
201 }
202 finally { rmSync(root, { recursive: true, force: true }) }
203})
204
181205test('boot completion alone does not admit a device before SettingsProvider is usable', async () => {
182206 const { root, options } = fixture(true, true)
183207 let session: Awaited<ReturnType<typeof createAndroidEmulator>> | undefined
scripts/verify-android-emulator.tsmodified+8-3
Changes to scripts/verify-android-emulator.ts
@@ -16,8 +16,13 @@ const diagnostic = args.length === 1
1616const bootTimeoutMs = 1_200_000
1717// Both retained 2 GB-profile failures show low-memory/process-death churn and
1818// a SystemUI ANR. Give the unmodified stock image an explicit 4 GB guest;
19// readiness, software acceleration, CPU count and boot deadline stay intact.
19// readiness, software acceleration and the boot deadline stay intact.
2020const memoryMb = 4096
21// Intel software-emulation runners have the same four host threads as the
22// passing AMD profile but starve SystemUI with the helper's two-CPU default.
23// Keep the public helper default unchanged while giving this cold profile all
24// four guest CPUs so startup ANRs remain a real failure rather than a timeout.
25const virtualCpus = 4
2126const expectedFingerprint = diagnostic ? process.env.CRAFT_ANDROID_DIAGNOSTIC_FINGERPRINT : undefined
2227if (diagnostic && !expectedFingerprint)
2328 throw new Error('The cold-provisioning diagnostic requires the failed stock image fingerprint')
@@ -39,7 +44,7 @@ let session: Awaited<ReturnType<typeof createAndroidEmulator>> | undefined
3944const proof: Record<string, unknown> = {
4045 purpose: diagnostic ? 'software-cold-provisioning-diagnostic-not-release-acceptance' : 'software-emulator-acceptance',
4146 platform: process.platform, architecture: process.arch, acceleration: 'software', sdkmanager, avdmanager, javaHome,
42 provisioningProfile: 'stock-play-software-cold', bootTimeoutMs, memoryMb,
47 provisioningProfile: 'stock-play-software-cold', bootTimeoutMs, memoryMb, virtualCpus,
4348 expectedFingerprint, runnerImage: process.env.ImageOS, runnerImageVersion: process.env.ImageVersion,
4449}
4550
@@ -48,7 +53,7 @@ try {
4853 sdkRoot: join(owned, 'sdk'), sdkmanager, avdmanager,
4954 image: 'system-images;android-35;google_apis_playstore;x86_64',
5055 acceleration: 'software', artifactsDirectory: artifacts,
51 bootTimeoutMs, memoryMb,
56 bootTimeoutMs, memoryMb, virtualCpus,
5257 temporaryDirectory: owned, acceptLicenses: true,
5358 env: { JAVA_HOME: javaHome },
5459 })