also looking at this
fix(native): preserve workspace sources and native overlay lifecycle
#384
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.
| @@ -109,9 +109,13 @@ helper defaults to a ten-minute boot budget, and hardware mode cannot exceed | ||
| 109 | 109 | it. Software callers may explicitly set `bootTimeoutMs` up to twenty minutes. |
| 110 | 110 | `memoryMb` selects the requested guest RAM, defaulting to 2048 MB. It accepts |
| 111 | 111 | the 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 | |
| 113 | from 1 through 8. The stock Play software verifier requests four guest CPUs: | |
| 114 | the four-thread Intel runners used for this acceptance profile otherwise starve | |
| 115 | SystemUI during TCG startup and surface a real platform ANR. | |
| 112 | 116 | The stock Play software verifier requests 4096 MB after retained cold boots |
| 113 | 117 | showed low-memory/process-death churn and a SystemUI ANR. It keeps the same |
| 114 | image, two virtual CPUs, readiness checks and twenty-minute deadline. | |
| 118 | image, readiness checks and twenty-minute deadline. | |
| 115 | 119 | The stock Play software CI verifier uses that cold-provisioning profile: the |
| 116 | 120 | API 35 image took 943,520 ms in the retained diagnostic run |
| 117 | 121 | [39113277828](https://github.com/craft-native/craft/actions/runs/39113277828). |
| @@ -16,6 +16,8 @@ export interface AndroidEmulatorOptions { | ||
| 16 | 16 | bootTimeoutMs?: number |
| 17 | 17 | /** Requested guest RAM, 1536–8192 MB (Android emulator limits); defaults to 2048. */ |
| 18 | 18 | memoryMb?: number |
| 19 | /** Requested guest CPU count, 1–8; defaults to 2. */ | |
| 20 | virtualCpus?: number | |
| 19 | 21 | env?: Record<string, string | undefined> |
| 20 | 22 | } |
| 21 | 23 | |
| @@ -45,6 +47,9 @@ export async function createAndroidEmulator(options: AndroidEmulatorOptions): Pr | ||
| 45 | 47 | const memoryMb = options.memoryMb ?? 2048 |
| 46 | 48 | if (!Number.isSafeInteger(memoryMb) || memoryMb < 1536 || memoryMb > 8192) |
| 47 | 49 | 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') | |
| 48 | 53 | const port = options.port ?? 5558 |
| 49 | 54 | 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') |
| 50 | 55 | const sdk = resolve(options.sdkRoot) |
| @@ -133,12 +138,12 @@ export async function createAndroidEmulator(options: AndroidEmulatorOptions): Pr | ||
| 133 | 138 | const hostCpus = cpus() |
| 134 | 139 | cpuStart = hostCpus.reduce((total, cpu) => total + Object.values(cpu.times).reduce((sum, value) => sum + value, 0), 0) |
| 135 | 140 | 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 }) | |
| 137 | 142 | identity = { |
| 138 | 143 | image: options.image, acceleration: options.acceleration, accelerationProbeExitCode: acceleration.code, |
| 139 | 144 | bootTimeoutMs, bootStartedAt: new Date(bootStartedAt).toISOString(), |
| 140 | 145 | 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, | |
| 142 | 147 | } |
| 143 | 148 | await Bun.write(join(artifacts, 'android-emulator.json'), JSON.stringify(identity, null, 2) + '\n') |
| 144 | 149 | const deadline = bootStartedAt + bootTimeoutMs |
| @@ -150,6 +150,18 @@ test('explicit guest memory reaches the emulator process and its retained identi | ||
| 150 | 150 | finally { await session?.dispose(); rmSync(root, { recursive: true, force: true }) } |
| 151 | 151 | }, 10_000) |
| 152 | 152 | |
| 153 | test('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 | ||
| 153 | 165 | test('invalid boot budgets fail before starting tools or creating owned device data', async () => { |
| 154 | 166 | const { root, options } = fixture(true) |
| 155 | 167 | try { |
| @@ -178,6 +190,18 @@ test('invalid guest memory fails before starting tools or creating owned device | ||
| 178 | 190 | finally { rmSync(root, { recursive: true, force: true }) } |
| 179 | 191 | }) |
| 180 | 192 | |
| 193 | test('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 | ||
| 181 | 205 | test('boot completion alone does not admit a device before SettingsProvider is usable', async () => { |
| 182 | 206 | const { root, options } = fixture(true, true) |
| 183 | 207 | let session: Awaited<ReturnType<typeof createAndroidEmulator>> | undefined |
| @@ -16,8 +16,13 @@ const diagnostic = args.length === 1 | ||
| 16 | 16 | const bootTimeoutMs = 1_200_000 |
| 17 | 17 | // Both retained 2 GB-profile failures show low-memory/process-death churn and |
| 18 | 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. | |
| 19 | // readiness, software acceleration and the boot deadline stay intact. | |
| 20 | 20 | const 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. | |
| 25 | const virtualCpus = 4 | |
| 21 | 26 | const expectedFingerprint = diagnostic ? process.env.CRAFT_ANDROID_DIAGNOSTIC_FINGERPRINT : undefined |
| 22 | 27 | if (diagnostic && !expectedFingerprint) |
| 23 | 28 | throw new Error('The cold-provisioning diagnostic requires the failed stock image fingerprint') |
| @@ -39,7 +44,7 @@ let session: Awaited<ReturnType<typeof createAndroidEmulator>> | undefined | ||
| 39 | 44 | const proof: Record<string, unknown> = { |
| 40 | 45 | purpose: diagnostic ? 'software-cold-provisioning-diagnostic-not-release-acceptance' : 'software-emulator-acceptance', |
| 41 | 46 | 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, | |
| 43 | 48 | expectedFingerprint, runnerImage: process.env.ImageOS, runnerImageVersion: process.env.ImageVersion, |
| 44 | 49 | } |
| 45 | 50 | |
| @@ -48,7 +53,7 @@ try { | ||
| 48 | 53 | sdkRoot: join(owned, 'sdk'), sdkmanager, avdmanager, |
| 49 | 54 | image: 'system-images;android-35;google_apis_playstore;x86_64', |
| 50 | 55 | acceleration: 'software', artifactsDirectory: artifacts, |
| 51 | bootTimeoutMs, memoryMb, | |
| 56 | bootTimeoutMs, memoryMb, virtualCpus, | |
| 52 | 57 | temporaryDirectory: owned, acceptLicenses: true, |
| 53 | 58 | env: { JAVA_HOME: javaHome }, |
| 54 | 59 | }) |