Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This is the log of notable changes to EAS CLI and related packages.

### 🎉 New features

- [eas-cli] Add `--system-image-package` to `eas simulator` for selecting a validated x86_64 Android emulator system image. ([#4317](https://github.com/expo/eas-cli/pull/4317) by [@szdziedzic](https://github.com/szdziedzic))
- [eas-cli] Update workflow run logs in real time, instead of every 10 seconds. ([#4228](https://github.com/expo/eas-cli/pull/4228) by [@AHGIJMKLKKZNPJKQR](https://github.com/AHGIJMKLKKZNPJKQR))
- [build-tools] Add `expo-device-hub` web previews to Android web-preview-only, Agent Device, Argent, and Appium remote sessions. ([#4304](https://github.com/expo/eas-cli/pull/4304) by [@krystofwoldrich](https://github.com/krystofwoldrich))
- [eas-cli] Add `eas workflow:ssh <workflow-job-id> [command...]` to open an ssh session on the worker running a workflow job. ([#4032](https://github.com/expo/eas-cli/pull/4032) by [@gwdp](https://github.com/gwdp))
Expand Down
45 changes: 30 additions & 15 deletions packages/eas-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2556,10 +2556,11 @@ _See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/exp

```
USAGE
$ eas sim [-p android|ios] [--name <value>] [--device <value>] [--build-id <value> |
--application-archive-url <value> | --expo-go] [--sdk-version <value>] [--launch-arg <value>...] [--open-url
<value>] [--type agent-device|appium|argent|web-preview-only] [--package-version <value>] [--max-duration-minutes
<value>] [--max-idle-time-minutes <value>] [--force] [--out-config-type env|dotenv] [--json] [--non-interactive]
$ eas sim [-p android|ios] [--name <value>] [--device <value>] [--system-image-package <value>]
[--build-id <value> | --application-archive-url <value> | --expo-go] [--sdk-version <value>] [--launch-arg
<value>...] [--open-url <value>] [--type agent-device|appium|argent|web-preview-only]
[--package-version <value>] [--max-duration-minutes <value>] [--max-idle-time-minutes <value>] [--force]
[--out-config-type env|dotenv] [--json] [--non-interactive]

FLAGS
-p, --platform=<option> Device platform
Expand All @@ -2569,7 +2570,7 @@ FLAGS
--build-id=<value> EAS Build to install and launch before the simulator session is ready.
--device=<value> 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.
"pixel_9"). Defaults to a device chosen by the runner.
--expo-go Install and launch Expo Go matching the current project's Expo SDK before the
simulator session is ready.
--[no-]force [default: true] Create a new simulator session even when an existing simulator
Expand All @@ -2596,6 +2597,10 @@ FLAGS
Defaults to "latest" when omitted.
--sdk-version=<value> Expo SDK version used to select Expo Go when --expo-go is passed. Defaults to
the current project SDK.
--system-image-package=<value> Android SDK system image package to use for the emulator (e.g.
"system-images;android-36;google_apis_playstore;x86_64"). Only supported for
Android and must use the x86_64 architecture required by EAS
nested-virtualization runners.
--type=<option> [default: agent-device] Type of simulator session to create. All session types
include a web preview. agent-device, appium, and argent also include an
automation interface; web-preview-only includes no automation interface.
Expand Down Expand Up @@ -2723,10 +2728,11 @@ ALIASES

```
USAGE
$ eas sim:start [-p android|ios] [--name <value>] [--device <value>] [--build-id <value> |
--application-archive-url <value> | --expo-go] [--sdk-version <value>] [--launch-arg <value>...] [--open-url
<value>] [--type agent-device|appium|argent|web-preview-only] [--package-version <value>] [--max-duration-minutes
<value>] [--max-idle-time-minutes <value>] [--force] [--out-config-type env|dotenv] [--json] [--non-interactive]
$ eas sim:start [-p android|ios] [--name <value>] [--device <value>] [--system-image-package <value>]
[--build-id <value> | --application-archive-url <value> | --expo-go] [--sdk-version <value>] [--launch-arg
<value>...] [--open-url <value>] [--type agent-device|appium|argent|web-preview-only]
[--package-version <value>] [--max-duration-minutes <value>] [--max-idle-time-minutes <value>] [--force]
[--out-config-type env|dotenv] [--json] [--non-interactive]

FLAGS
-p, --platform=<option> Device platform
Expand All @@ -2736,7 +2742,7 @@ FLAGS
--build-id=<value> EAS Build to install and launch before the simulator session is ready.
--device=<value> 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.
"pixel_9"). Defaults to a device chosen by the runner.
--expo-go Install and launch Expo Go matching the current project's Expo SDK before the
simulator session is ready.
--[no-]force [default: true] Create a new simulator session even when an existing simulator
Expand All @@ -2763,6 +2769,10 @@ FLAGS
Defaults to "latest" when omitted.
--sdk-version=<value> Expo SDK version used to select Expo Go when --expo-go is passed. Defaults to
the current project SDK.
--system-image-package=<value> Android SDK system image package to use for the emulator (e.g.
"system-images;android-36;google_apis_playstore;x86_64"). Only supported for
Android and must use the x86_64 architecture required by EAS
nested-virtualization runners.
--type=<option> [default: agent-device] Type of simulator session to create. All session types
include a web preview. agent-device, appium, and argent also include an
automation interface; web-preview-only includes no automation interface.
Expand Down Expand Up @@ -2803,10 +2813,11 @@ ALIASES

```
USAGE
$ eas simulator:start [-p android|ios] [--name <value>] [--device <value>] [--build-id <value> |
--application-archive-url <value> | --expo-go] [--sdk-version <value>] [--launch-arg <value>...] [--open-url
<value>] [--type agent-device|appium|argent|web-preview-only] [--package-version <value>] [--max-duration-minutes
<value>] [--max-idle-time-minutes <value>] [--force] [--out-config-type env|dotenv] [--json] [--non-interactive]
$ eas simulator:start [-p android|ios] [--name <value>] [--device <value>] [--system-image-package <value>]
[--build-id <value> | --application-archive-url <value> | --expo-go] [--sdk-version <value>] [--launch-arg
<value>...] [--open-url <value>] [--type agent-device|appium|argent|web-preview-only]
[--package-version <value>] [--max-duration-minutes <value>] [--max-idle-time-minutes <value>] [--force]
[--out-config-type env|dotenv] [--json] [--non-interactive]

FLAGS
-p, --platform=<option> Device platform
Expand All @@ -2816,7 +2827,7 @@ FLAGS
--build-id=<value> EAS Build to install and launch before the simulator session is ready.
--device=<value> 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.
"pixel_9"). Defaults to a device chosen by the runner.
--expo-go Install and launch Expo Go matching the current project's Expo SDK before the
simulator session is ready.
--[no-]force [default: true] Create a new simulator session even when an existing simulator
Expand All @@ -2843,6 +2854,10 @@ FLAGS
Defaults to "latest" when omitted.
--sdk-version=<value> Expo SDK version used to select Expo Go when --expo-go is passed. Defaults to
the current project SDK.
--system-image-package=<value> Android SDK system image package to use for the emulator (e.g.
"system-images;android-36;google_apis_playstore;x86_64"). Only supported for
Android and must use the x86_64 architecture required by EAS
nested-virtualization runners.
--type=<option> [default: agent-device] Type of simulator session to create. All session types
include a web preview. agent-device, appium, and argent also include an
automation interface; web-preview-only includes no automation interface.
Expand Down
116 changes: 112 additions & 4 deletions packages/eas-cli/src/commands/simulator/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ describe(Simulator, () => {
);
});

it('forwards --device to the create mutation as deviceIdentifier', async () => {
it('forwards an iOS --device through the iOS-specific input', async () => {
const { command } = createCommand([
'--platform',
'ios',
Expand All @@ -520,7 +520,7 @@ describe(Simulator, () => {

expect(mockCreateDeviceRunSessionAsync).toHaveBeenCalledWith(
graphqlClient,
expect.objectContaining({ deviceIdentifier: 'iPhone 16 Pro' })
expect.objectContaining({ ios: { deviceIdentifier: 'iPhone 16 Pro' } })
);
});

Expand All @@ -536,18 +536,126 @@ describe(Simulator, () => {

expect(mockCreateDeviceRunSessionAsync).toHaveBeenCalledWith(
graphqlClient,
expect.objectContaining({ deviceIdentifier: 'iPhone 16 Pro' })
expect.objectContaining({ ios: { deviceIdentifier: 'iPhone 16 Pro' } })
);
});

it('omits a blank --device', async () => {
const { command } = createCommand(['--platform', 'ios', '--non-interactive', '--device', ' ']);
await command.runAsync();

expect(mockCreateDeviceRunSessionAsync.mock.calls[0][1]).not.toHaveProperty('ios');
expect(mockCreateDeviceRunSessionAsync.mock.calls[0][1]).not.toHaveProperty('android');
});

it('forwards Android device and system image options through the Android-specific input', async () => {
const { command } = createCommand([
'--platform',
'android',
'--non-interactive',
'--device',
' pixel_8 ',
'--system-image-package',
' system-images;android-36;google_apis_playstore;x86_64 ',
]);
await command.runAsync();

expect(mockCreateDeviceRunSessionAsync).toHaveBeenCalledWith(
graphqlClient,
expect.objectContaining({ deviceIdentifier: undefined })
expect.objectContaining({
android: {
deviceIdentifier: 'pixel_8',
systemImagePackage: 'system-images;android-36;google_apis_playstore;x86_64',
},
})
);
expect(mockCreateDeviceRunSessionAsync.mock.calls[0][1]).not.toHaveProperty('deviceIdentifier');
});

it('forwards --system-image-package without requiring --device', async () => {
const { command } = createCommand([
'--platform',
'android',
'--non-interactive',
'--system-image-package',
'system-images;android-36-ext1;google_apis_ps16k;x86_64',
]);
await command.runAsync();

expect(mockCreateDeviceRunSessionAsync).toHaveBeenCalledWith(
graphqlClient,
expect.objectContaining({
android: {
systemImagePackage: 'system-images;android-36-ext1;google_apis_ps16k;x86_64',
},
})
);
});

it('rejects --system-image-package for iOS', async () => {
const { command } = createCommand([
'--platform',
'ios',
'--non-interactive',
'--system-image-package',
'system-images;android-36;google_apis_playstore;x86_64',
]);

await expect(command.runAsync()).rejects.toThrow(
'The --system-image-package flag is only supported for Android simulator sessions.'
);
expect(mockCreateDeviceRunSessionAsync).not.toHaveBeenCalled();
});

it('rejects a blank --system-image-package', async () => {
const { command } = createCommand([
'--platform',
'android',
'--non-interactive',
'--system-image-package',
' ',
]);

await expect(command.runAsync()).rejects.toThrow(
'The --system-image-package value cannot be blank.'
);
expect(mockCreateDeviceRunSessionAsync).not.toHaveBeenCalled();
});

it.each([
'system-images;android-36;google_apis_playstore',
'platforms;android-36;google_apis_playstore;x86_64',
'system-images;36;google_apis_playstore;x86_64',
'system-images;android-36;google apis;x86_64',
'system-images;android-36;google_apis_playstore;x86_64;extra',
])('rejects malformed --system-image-package %s', async systemImagePackage => {
const { command } = createCommand([
'--platform',
'android',
'--non-interactive',
'--system-image-package',
systemImagePackage,
]);

await expect(command.runAsync()).rejects.toThrow(
'Expected an Android SDK package coordinate in the form'
);
expect(mockCreateDeviceRunSessionAsync).not.toHaveBeenCalled();
});

it('rejects a non-x86_64 Android system image', async () => {
const { command } = createCommand([
'--platform',
'android',
'--non-interactive',
'--system-image-package',
'system-images;android-36;google_apis_playstore;arm64-v8a',
]);

await expect(command.runAsync()).rejects.toThrow(
'must be x86_64 because EAS Android simulator sessions use x86_64 nested-virtualization runners'
);
expect(mockCreateDeviceRunSessionAsync).not.toHaveBeenCalled();
});

it('omits resourceClass when --resource-class is not set', async () => {
Expand Down
67 changes: 65 additions & 2 deletions packages/eas-cli/src/commands/simulator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ const APP_PLATFORM_BY_FLAG_VALUE: Record<PlatformFlagValue, AppPlatform> = {
android: AppPlatform.Android,
ios: AppPlatform.Ios,
};
const ANDROID_SYSTEM_IMAGE_PACKAGE_EXAMPLE =
'system-images;android-36;google_apis_playstore;x86_64';

export default class Simulator extends EasCommand {
static override hidden = true;
Expand All @@ -73,7 +75,12 @@ export default class Simulator extends EasCommand {
}),
device: Flags.string({
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.',
'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_9"). Defaults to a device chosen by the runner.',
}),
'system-image-package': Flags.string({
description:
`Android SDK system image package to use for the emulator (e.g. "${ANDROID_SYSTEM_IMAGE_PACKAGE_EXAMPLE}"). ` +
'Only supported for Android and must use the x86_64 architecture required by EAS nested-virtualization runners.',
}),
'build-id': Flags.string({
description: 'EAS Build to install and launch before the simulator session is ready.',
Expand Down Expand Up @@ -212,6 +219,10 @@ export default class Simulator extends EasCommand {
}

const platform = await resolvePlatformAsync(flags.platform, nonInteractive);
const androidSystemImagePackage = resolveAndroidSystemImagePackage(
flags['system-image-package'],
platform
);
const expoGoSdkVersion = flags['expo-go']
? await resolveExpoGoSdkVersionAsync({ projectDir, sdkVersion: sdkVersionFromFlag })
: undefined;
Expand All @@ -236,7 +247,17 @@ export default class Simulator extends EasCommand {
platform,
type: DEVICE_RUN_SESSION_TYPE_BY_FLAG_VALUE[flags.type],
packageVersion: flags['package-version'],
deviceIdentifier,
...(platform === AppPlatform.Android && (deviceIdentifier || androidSystemImagePackage)
? {
android: {
...(deviceIdentifier ? { deviceIdentifier } : {}),
...(androidSystemImagePackage
? { systemImagePackage: androidSystemImagePackage }
: {}),
},
}
: {}),
...(platform === AppPlatform.Ios && deviceIdentifier ? { ios: { deviceIdentifier } } : {}),
...(buildId ? { buildId } : {}),
...(applicationArchiveUrlFromFlag
? { applicationArchiveUrl: applicationArchiveUrlFromFlag }
Expand Down Expand Up @@ -395,6 +416,48 @@ export default class Simulator extends EasCommand {
}
}

function resolveAndroidSystemImagePackage(
rawSystemImagePackage: string | undefined,
platform: AppPlatform
): string | undefined {
if (rawSystemImagePackage === undefined) {
return undefined;
}

const systemImagePackage = rawSystemImagePackage.trim();
if (!systemImagePackage) {
throw new EasCommandError(
`The --system-image-package value cannot be blank. Pass a package such as "${ANDROID_SYSTEM_IMAGE_PACKAGE_EXAMPLE}", or omit the flag to use the default.`
);
}
if (platform !== AppPlatform.Android) {
throw new EasCommandError(
'The --system-image-package flag is only supported for Android simulator sessions. Use --platform android or omit the flag.'
);
}

const [namespace, androidVersion, imageFlavor, architecture, ...extraSegments] =
systemImagePackage.split(';');
const isValidCoordinate =
namespace === 'system-images' &&
/^android-[A-Za-z0-9_.-]+$/.test(androidVersion ?? '') &&
/^[A-Za-z0-9_.-]+$/.test(imageFlavor ?? '') &&
architecture !== undefined &&
extraSegments.length === 0;
if (!isValidCoordinate) {
throw new EasCommandError(
`Invalid --system-image-package "${systemImagePackage}". Expected an Android SDK package coordinate in the form "system-images;android-<API>;image-flavor;x86_64", for example "${ANDROID_SYSTEM_IMAGE_PACKAGE_EXAMPLE}".`
);
}
if (architecture !== 'x86_64') {
throw new EasCommandError(
`The --system-image-package architecture must be x86_64 because EAS Android simulator sessions use x86_64 nested-virtualization runners; received "${architecture}".`
);
}

return systemImagePackage;
}

async function resolvePlatformAsync(
platform: PlatformFlagValue | undefined,
nonInteractive: boolean
Expand Down
Loading
Loading