diff --git a/.changeset/simulator-system-image.md b/.changeset/simulator-system-image.md new file mode 100644 index 0000000000..2ee9a303d1 --- /dev/null +++ b/.changeset/simulator-system-image.md @@ -0,0 +1,5 @@ +--- +'eas-cli': minor +--- + +Add a `--system-image` flag to `eas simulator:start` to select the Android SDK system image for the emulator. diff --git a/packages/eas-cli/src/commands/simulator/__tests__/index.test.ts b/packages/eas-cli/src/commands/simulator/__tests__/index.test.ts index 15b2321121..56abc3de91 100644 --- a/packages/eas-cli/src/commands/simulator/__tests__/index.test.ts +++ b/packages/eas-cli/src/commands/simulator/__tests__/index.test.ts @@ -1124,6 +1124,76 @@ describe(Simulator, () => { ); }); + it('forwards --system-image in the Android create options', async () => { + const { command } = createCommand([ + '--platform', + 'android', + '--non-interactive', + '--system-image', + ' system-images;android-35-ext15;google_apis_playstore;x86_64 ', + ]); + await command.runAsync(); + + expect(mockCreateDeviceRunSessionAsync).toHaveBeenCalledWith( + graphqlClient, + expect.objectContaining({ + android: { + systemImagePackage: 'system-images;android-35-ext15;google_apis_playstore;x86_64', + }, + }) + ); + }); + + it('forwards --device and --system-image together in the Android create options', async () => { + const { command } = createCommand([ + '--platform', + 'android', + '--non-interactive', + '--device', + 'pixel_9', + '--system-image', + 'system-images;android-35-ext15;google_apis_playstore;x86_64', + ]); + await command.runAsync(); + + expect(mockCreateDeviceRunSessionAsync).toHaveBeenCalledWith( + graphqlClient, + expect.objectContaining({ + android: { + deviceIdentifier: 'pixel_9', + systemImagePackage: 'system-images;android-35-ext15;google_apis_playstore;x86_64', + }, + }) + ); + }); + + it('omits a blank --system-image', async () => { + const { command } = createCommand([ + '--platform', + 'android', + '--non-interactive', + '--system-image', + ' ', + ]); + await command.runAsync(); + + expect(mockCreateDeviceRunSessionAsync.mock.calls[0][1]).not.toHaveProperty('android'); + }); + + it('rejects --system-image for iOS before creating a session', async () => { + const { command } = createCommand([ + '--platform', + 'ios', + '--non-interactive', + '--system-image', + 'system-images;android-35-ext15;google_apis_playstore;x86_64', + ]); + await expect(command.runAsync()).rejects.toThrow( + '--system-image is only supported with --platform android.' + ); + expect(mockCreateDeviceRunSessionAsync).not.toHaveBeenCalled(); + }); + it('trims --device before sending it', async () => { const { command } = createCommand([ '--platform', diff --git a/packages/eas-cli/src/commands/simulator/index.ts b/packages/eas-cli/src/commands/simulator/index.ts index e7972570fb..129bbd505a 100644 --- a/packages/eas-cli/src/commands/simulator/index.ts +++ b/packages/eas-cli/src/commands/simulator/index.ts @@ -100,6 +100,10 @@ export default class Simulator extends EasCommand { description: 'Virtual device to start for the session. On iOS, a Simulator device name or UDID (e.g. "iPhone 16 Pro"). On Android, an AVD hardware profile id (e.g. "pixel_7"). Defaults to a device chosen by the runner.', }), + 'system-image': Flags.string({ + description: + 'Android SDK system image package to use for the emulator (e.g. "system-images;android-35-ext15;google_apis_playstore;x86_64"). Defaults to a system image chosen by the runner. Only supported with --platform android.', + }), 'build-id': Flags.string({ description: 'EAS Build to install and launch before the simulator session is ready.', exclusive: ['build-fingerprint', 'application-archive-url', 'expo-go'], @@ -248,6 +252,7 @@ export default class Simulator extends EasCommand { const name = flags.name?.trim() || undefined; const tags = flags.tag?.map(tag => tag.trim()).filter(tag => tag.length > 0); const deviceIdentifier = flags.device?.trim() || undefined; + const systemImagePackage = flags['system-image']?.trim() || undefined; const buildId = flags['build-id']?.trim() || undefined; const buildFingerprint = flags['build-fingerprint']?.trim() || undefined; const applicationArchiveUrlFromFlag = flags['application-archive-url']?.trim() || undefined; @@ -291,6 +296,9 @@ export default class Simulator extends EasCommand { const platform = await resolvePlatformAsync(flags.platform, nonInteractive); const egress = flags.egress === 'local' ? DeviceRunSessionEgress.Local : undefined; + if (systemImagePackage && platform !== AppPlatform.Android) { + throw new EasCommandError('--system-image is only supported with --platform android.'); + } let egressAllow: string[] = []; try { egressAllow = parseEgressAllowList(flags['egress-allow'] ?? []); @@ -366,11 +374,18 @@ export default class Simulator extends EasCommand { packageVersion: flags['package-version'], networkCapture: flags['network-capture'], ...(networkCaptureFields.length ? { networkCaptureFields } : {}), - ...(deviceIdentifier - ? platform === AppPlatform.Ios + ...(platform === AppPlatform.Ios + ? deviceIdentifier ? { ios: { deviceIdentifier } } - : { android: { deviceIdentifier } } - : {}), + : {} + : deviceIdentifier || systemImagePackage + ? { + android: { + ...(deviceIdentifier ? { deviceIdentifier } : {}), + ...(systemImagePackage ? { systemImagePackage } : {}), + }, + } + : {}), ...(buildId ? { buildId } : {}), ...(buildFingerprint ? { buildFingerprint } : {}), ...(applicationArchiveUrlFromFlag