diff --git a/packages/platform-android/src/__tests__/environment.test.ts b/packages/platform-android/src/__tests__/environment.test.ts index 3fb7c06..c919eff 100644 --- a/packages/platform-android/src/__tests__/environment.test.ts +++ b/packages/platform-android/src/__tests__/environment.test.ts @@ -1,11 +1,17 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; import { + ensureAndroidAdbAvailable, + ensureAndroidEmulatorAvailable, getAndroidSdkRoot, getAndroidSystemImagePackage, getDefaultUnixAndroidSdkRoot, getHostAndroidSystemImageArch, getRequiredAndroidSdkPackages, } from '../environment.js'; +import * as tools from '@react-native-harness/tools'; describe('Android environment', () => { beforeEach(() => { @@ -13,12 +19,131 @@ describe('Android environment', () => { vi.unstubAllEnvs(); }); + it('skips bootstrapping command-line tools when adb is already installed', async () => { + const sdkRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'android-sdk-')); + const adbPath = path.join(sdkRoot, 'platform-tools', 'adb'); + + fs.mkdirSync(path.dirname(adbPath), { recursive: true }); + fs.writeFileSync(adbPath, ''); + + const spawnSpy = vi.spyOn(tools, 'spawn'); + + await expect( + ensureAndroidAdbAvailable({ + env: { ANDROID_HOME: sdkRoot }, + }), + ).resolves.toBe(sdkRoot); + + expect(spawnSpy).not.toHaveBeenCalled(); + + fs.rmSync(sdkRoot, { force: true, recursive: true }); + }); + + it('installs only platform-tools when adb is missing', async () => { + const sdkRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'android-sdk-')); + const sdkManagerDirectory = path.join( + sdkRoot, + 'cmdline-tools', + 'latest', + 'bin', + ); + + fs.mkdirSync(sdkManagerDirectory, { recursive: true }); + fs.writeFileSync(path.join(sdkManagerDirectory, 'sdkmanager'), ''); + fs.writeFileSync(path.join(sdkManagerDirectory, 'avdmanager'), ''); + + const spawnSpy = vi.spyOn(tools, 'spawn').mockImplementation((async ( + command: string, + args?: readonly string[], + ) => { + if (command === 'bash' && typeof args?.[1] === 'string') { + const commandString = args[1]; + + if (commandString.includes('platform-tools')) { + const adbPath = path.join(sdkRoot, 'platform-tools', 'adb'); + fs.mkdirSync(path.dirname(adbPath), { recursive: true }); + fs.writeFileSync(adbPath, ''); + } + } + + return {} as Awaited>; + }) as typeof tools.spawn); + + await expect( + ensureAndroidAdbAvailable({ + env: { ANDROID_HOME: sdkRoot }, + }), + ).resolves.toBe(sdkRoot); + + expect(spawnSpy).toHaveBeenCalledWith( + 'bash', + ['-lc', expect.stringContaining('platform-tools')], + expect.any(Object), + ); + expect( + spawnSpy.mock.calls.some( + ([command, args]) => + command === 'bash' && + typeof args?.[1] === 'string' && + args[1].includes('platform-tools') && + args[1].includes('emulator'), + ), + ).toBe(false); + + fs.rmSync(sdkRoot, { force: true, recursive: true }); + }); + + it('installs emulator only when emulator is missing', async () => { + const sdkRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'android-sdk-')); + const sdkManagerDirectory = path.join( + sdkRoot, + 'cmdline-tools', + 'latest', + 'bin', + ); + + fs.mkdirSync(sdkManagerDirectory, { recursive: true }); + fs.writeFileSync(path.join(sdkManagerDirectory, 'sdkmanager'), ''); + fs.writeFileSync(path.join(sdkManagerDirectory, 'avdmanager'), ''); + + const spawnSpy = vi.spyOn(tools, 'spawn').mockImplementation((async ( + command: string, + args?: readonly string[], + ) => { + if (command === 'bash' && typeof args?.[1] === 'string') { + const commandString = args[1]; + + if (commandString.includes('emulator')) { + const emulatorPath = path.join(sdkRoot, 'emulator', 'emulator'); + fs.mkdirSync(path.dirname(emulatorPath), { recursive: true }); + fs.writeFileSync(emulatorPath, ''); + } + } + + return {} as Awaited>; + }) as typeof tools.spawn); + + await expect( + ensureAndroidEmulatorAvailable({ + env: { ANDROID_HOME: sdkRoot }, + }), + ).resolves.toBe(sdkRoot); + + expect(spawnSpy).toHaveBeenCalledWith( + 'bash', + ['-lc', expect.stringContaining('emulator')], + expect.any(Object), + ); + + fs.rmSync(sdkRoot, { force: true, recursive: true }); + }); + it('uses the default Unix SDK root when env vars are missing', () => { expect( getDefaultUnixAndroidSdkRoot({ platform: 'darwin', homeDirectory: '/Users/tester', - }) + }), ).toBe('/Users/tester/Library/Android/sdk'); expect( @@ -27,8 +152,8 @@ describe('Android environment', () => { { platform: 'linux', homeDirectory: '/home/tester', - } - ) + }, + ), ).toBe('/home/tester/Android/Sdk'); }); @@ -42,8 +167,8 @@ describe('Android environment', () => { { platform: 'darwin', homeDirectory: '/Users/tester', - } - ) + }, + ), ).toBe('/env/android-home'); expect( @@ -54,8 +179,8 @@ describe('Android environment', () => { { platform: 'linux', homeDirectory: '/home/tester', - } - ) + }, + ), ).toBe('/env/android-sdk-root'); }); @@ -63,10 +188,10 @@ describe('Android environment', () => { expect(getHostAndroidSystemImageArch('x64')).toBe('x86_64'); expect(getHostAndroidSystemImageArch('arm64')).toBe('arm64-v8a'); expect(getAndroidSystemImagePackage(35, 'x86_64')).toBe( - 'system-images;android-35;default;x86_64' + 'system-images;android-35;default;x86_64', ); expect(getAndroidSystemImagePackage(35, 'arm64-v8a')).toBe( - 'system-images;android-35;default;arm64-v8a' + 'system-images;android-35;default;arm64-v8a', ); }); @@ -76,7 +201,7 @@ describe('Android environment', () => { apiLevel: 34, includeEmulator: true, architecture: 'x86_64', - }) + }), ).toEqual([ 'platform-tools', 'emulator', diff --git a/packages/platform-android/src/__tests__/instance.test.ts b/packages/platform-android/src/__tests__/instance.test.ts index 1310bba..505fd4e 100644 --- a/packages/platform-android/src/__tests__/instance.test.ts +++ b/packages/platform-android/src/__tests__/instance.test.ts @@ -36,7 +36,7 @@ describe('Android platform instance', () => { const ensureAndroidEmulatorEnvironment = vi .spyOn( await import('../environment.js'), - 'ensureAndroidEmulatorEnvironment' + 'ensureAndroidEmulatorEnvironment', ) .mockResolvedValue('/tmp/android-sdk'); vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']); @@ -47,10 +47,10 @@ describe('Android platform instance', () => { vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined); vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234); vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue( - undefined + undefined, ); vi.spyOn(sharedPrefs, 'clearHarnessDebugHttpHost').mockResolvedValue( - undefined + undefined, ); vi.spyOn(adb, 'stopApp').mockResolvedValue(undefined); const stopEmulator = vi.spyOn(adb, 'stopEmulator').mockResolvedValue(); @@ -72,19 +72,19 @@ describe('Android platform instance', () => { activityName: '.MainActivity', }, harnessConfig, - init + init, ); await instance.dispose(); - expect(ensureAndroidEmulatorEnvironment).toHaveBeenCalledWith(35); + expect(ensureAndroidEmulatorEnvironment).not.toHaveBeenCalled(); expect(stopEmulator).not.toHaveBeenCalled(); }); it('creates and boots an emulator when missing and shuts it down on dispose', async () => { vi.spyOn( await import('../environment.js'), - 'ensureAndroidEmulatorEnvironment' + 'ensureAndroidEmulatorEnvironment', ).mockResolvedValue('/tmp/android-sdk'); vi.spyOn(adb, 'getDeviceIds').mockResolvedValue([]); vi.spyOn(adb, 'hasAvd').mockResolvedValue(false); @@ -98,10 +98,10 @@ describe('Android platform instance', () => { vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined); vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234); vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue( - undefined + undefined, ); vi.spyOn(sharedPrefs, 'clearHarnessDebugHttpHost').mockResolvedValue( - undefined + undefined, ); vi.spyOn(adb, 'stopApp').mockResolvedValue(undefined); const stopEmulator = vi.spyOn(adb, 'stopEmulator').mockResolvedValue(); @@ -123,7 +123,7 @@ describe('Android platform instance', () => { activityName: '.MainActivity', }, harnessConfig, - init + init, ); expect(createAvd).toHaveBeenCalledWith({ @@ -144,7 +144,7 @@ describe('Android platform instance', () => { const ensureAndroidEmulatorEnvironment = vi .spyOn( await import('../environment.js'), - 'ensureAndroidEmulatorEnvironment' + 'ensureAndroidEmulatorEnvironment', ) .mockResolvedValue('/tmp/android-sdk'); vi.spyOn(adb, 'getDeviceIds').mockResolvedValue([]); @@ -159,7 +159,7 @@ describe('Android platform instance', () => { vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined); vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234); vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue( - undefined + undefined, ); await expect( @@ -180,8 +180,8 @@ describe('Android platform instance', () => { activityName: '.MainActivity', }, harnessConfig, - init - ) + init, + ), ).resolves.toBeDefined(); expect(ensureAndroidEmulatorEnvironment).toHaveBeenCalledWith(35); @@ -193,7 +193,7 @@ describe('Android platform instance', () => { vi.stubEnv('HARNESS_AVD_CACHING', 'true'); vi.spyOn( await import('../environment.js'), - 'ensureAndroidEmulatorEnvironment' + 'ensureAndroidEmulatorEnvironment', ).mockResolvedValue('/tmp/android-sdk'); vi.spyOn(adb, 'getDeviceIds').mockResolvedValue([]); vi.spyOn(adb, 'hasAvd').mockResolvedValue(true); @@ -211,7 +211,7 @@ describe('Android platform instance', () => { vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined); vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234); vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue( - undefined + undefined, ); await expect( @@ -233,14 +233,14 @@ describe('Android platform instance', () => { activityName: '.MainActivity', }, harnessConfig, - init - ) + init, + ), ).resolves.toBeDefined(); expect(adb.startEmulator).toHaveBeenCalledTimes(1); expect(adb.startEmulator).toHaveBeenCalledWith( 'Pixel_8_API_35', - 'snapshot-reuse' + 'snapshot-reuse', ); }); @@ -248,7 +248,7 @@ describe('Android platform instance', () => { vi.stubEnv('HARNESS_AVD_CACHING', 'true'); vi.spyOn( await import('../environment.js'), - 'ensureAndroidEmulatorEnvironment' + 'ensureAndroidEmulatorEnvironment', ).mockResolvedValue('/tmp/android-sdk'); vi.spyOn(adb, 'getDeviceIds').mockResolvedValue([]); vi.spyOn(adb, 'hasAvd').mockResolvedValue(true); @@ -272,7 +272,7 @@ describe('Android platform instance', () => { vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined); vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234); vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue( - undefined + undefined, ); await expect( @@ -294,8 +294,8 @@ describe('Android platform instance', () => { activityName: '.MainActivity', }, harnessConfig, - init - ) + init, + ), ).resolves.toBeDefined(); expect(deleteAvd).toHaveBeenCalledWith('Pixel_8_API_35'); @@ -303,17 +303,17 @@ describe('Android platform instance', () => { expect(stopEmulator).toHaveBeenCalledWith('emulator-5554'); expect(waitForEmulatorDisconnect).toHaveBeenCalledWith( 'emulator-5554', - init.signal + init.signal, ); expect(adb.startEmulator).toHaveBeenNthCalledWith( 1, 'Pixel_8_API_35', - 'clean-snapshot-generation' + 'clean-snapshot-generation', ); expect(adb.startEmulator).toHaveBeenNthCalledWith( 2, 'Pixel_8_API_35', - 'snapshot-reuse' + 'snapshot-reuse', ); }); @@ -321,7 +321,7 @@ describe('Android platform instance', () => { vi.stubEnv('HARNESS_AVD_CACHING', 'true'); vi.spyOn( await import('../environment.js'), - 'ensureAndroidEmulatorEnvironment' + 'ensureAndroidEmulatorEnvironment', ).mockResolvedValue('/tmp/android-sdk'); vi.spyOn(adb, 'getDeviceIds').mockResolvedValue([]); vi.spyOn(adb, 'hasAvd').mockResolvedValue(false); @@ -339,7 +339,7 @@ describe('Android platform instance', () => { vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined); vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234); vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue( - undefined + undefined, ); await expect( @@ -361,24 +361,24 @@ describe('Android platform instance', () => { activityName: '.MainActivity', }, harnessConfig, - init - ) + init, + ), ).resolves.toBeDefined(); expect(startEmulator).toHaveBeenNthCalledWith( 1, 'Pixel_8_API_35', - 'clean-snapshot-generation' + 'clean-snapshot-generation', ); expect(stopEmulator).toHaveBeenCalledWith('emulator-5554'); expect(waitForEmulatorDisconnect).toHaveBeenCalledWith( 'emulator-5554', - init.signal + init.signal, ); expect(startEmulator).toHaveBeenNthCalledWith( 2, 'Pixel_8_API_35', - 'snapshot-reuse' + 'snapshot-reuse', ); }); @@ -388,7 +388,7 @@ describe('Android platform instance', () => { vi.stubEnv('HARNESS_APP_PATH', appPath); vi.spyOn( await import('../environment.js'), - 'ensureAndroidEmulatorEnvironment' + 'ensureAndroidEmulatorEnvironment', ).mockResolvedValue('/tmp/android-sdk'); vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']); vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35'); @@ -399,7 +399,7 @@ describe('Android platform instance', () => { vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined); vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234); vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue( - undefined + undefined, ); await expect( @@ -420,8 +420,8 @@ describe('Android platform instance', () => { activityName: '.MainActivity', }, harnessConfig, - init - ) + init, + ), ).resolves.toBeDefined(); expect(installApp).toHaveBeenCalledWith('emulator-5554', appPath); @@ -453,8 +453,8 @@ describe('Android platform instance', () => { activityName: '.MainActivity', }, harnessConfig, - init - ) + init, + ), ).rejects.toBeInstanceOf(HarnessAppPathError); }); @@ -483,8 +483,8 @@ describe('Android platform instance', () => { activityName: '.MainActivity', }, harnessConfig, - init - ) + init, + ), ).rejects.toBeInstanceOf(HarnessAppPathError); }); @@ -503,15 +503,15 @@ describe('Android platform instance', () => { activityName: '.MainActivity', }, harnessConfig, - init - ) + init, + ), ).rejects.toBeInstanceOf(HarnessEmulatorConfigError); }); it('returns a noop emulator app monitor when native crash detection is disabled', async () => { vi.spyOn( await import('../environment.js'), - 'ensureAndroidEmulatorEnvironment' + 'ensureAndroidEmulatorEnvironment', ).mockResolvedValue('/tmp/android-sdk'); vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']); vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35'); @@ -521,7 +521,7 @@ describe('Android platform instance', () => { vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined); vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234); vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue( - undefined + undefined, ); const instance = await getAndroidEmulatorPlatformInstance( @@ -541,7 +541,7 @@ describe('Android platform instance', () => { activityName: '.MainActivity', }, harnessConfigWithoutNativeCrashDetection, - init + init, ); const listener = vi.fn(); @@ -565,7 +565,7 @@ describe('Android platform instance', () => { vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined); vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234); vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue( - undefined + undefined, ); await expect( @@ -580,8 +580,8 @@ describe('Android platform instance', () => { bundleId: 'com.harnessplayground', activityName: '.MainActivity', }, - harnessConfigWithoutNativeCrashDetection - ) + harnessConfigWithoutNativeCrashDetection, + ), ).resolves.toBeDefined(); const instance = await getAndroidPhysicalDevicePlatformInstance( @@ -595,7 +595,7 @@ describe('Android platform instance', () => { bundleId: 'com.harnessplayground', activityName: '.MainActivity', }, - harnessConfigWithoutNativeCrashDetection + harnessConfigWithoutNativeCrashDetection, ); const listener = vi.fn(); diff --git a/packages/platform-android/src/__tests__/targets.test.ts b/packages/platform-android/src/__tests__/targets.test.ts new file mode 100644 index 0000000..6390023 --- /dev/null +++ b/packages/platform-android/src/__tests__/targets.test.ts @@ -0,0 +1,53 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { getRunTargets } from '../targets.js'; +import * as adb from '../adb.js'; +import * as environment from '../environment.js'; + +describe('Android target discovery', () => { + beforeEach(() => { + vi.restoreAllMocks(); + vi.unstubAllEnvs(); + }); + + it('installs adb and emulator only for discovery', async () => { + const ensureAndroidAdbAvailable = vi + .spyOn(environment, 'ensureAndroidAdbAvailable') + .mockResolvedValue('/tmp/android-sdk'); + const ensureAndroidEmulatorAvailable = vi + .spyOn(environment, 'ensureAndroidEmulatorAvailable') + .mockResolvedValue('/tmp/android-sdk'); + vi.spyOn(adb, 'getAvds').mockResolvedValue(['Pixel_8_API_35']); + vi.spyOn(adb, 'getConnectedDevices').mockResolvedValue([ + { + id: 'device-1', + manufacturer: 'Google', + model: 'Pixel 8', + }, + ]); + + await expect(getRunTargets()).resolves.toEqual([ + { + type: 'emulator', + name: 'Pixel_8_API_35', + platform: 'android', + description: 'Android emulator', + device: { + name: 'Pixel_8_API_35', + }, + }, + { + type: 'physical', + name: 'Google Pixel 8', + platform: 'android', + description: 'Physical device (device-1)', + device: { + manufacturer: 'Google', + model: 'Pixel 8', + }, + }, + ]); + + expect(ensureAndroidAdbAvailable).toHaveBeenCalledTimes(1); + expect(ensureAndroidEmulatorAvailable).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/platform-android/src/adb.ts b/packages/platform-android/src/adb.ts index 05af9c7..4c7acce 100644 --- a/packages/platform-android/src/adb.ts +++ b/packages/platform-android/src/adb.ts @@ -5,6 +5,7 @@ import type { ChildProcessByStdio } from 'node:child_process'; import { access, rm } from 'node:fs/promises'; import type { Readable } from 'node:stream'; import { + ensureAndroidEmulatorAvailable, ensureAndroidSdkPackages, getAdbBinaryPath, getAndroidSystemImagePackage, @@ -12,7 +13,6 @@ import { getEmulatorBinaryPath, getHostAndroidSystemImageArch, getRequiredAndroidSdkPackages, - getSdkManagerBinaryPath, } from './environment.js'; import { getEmulatorStartupArgs, @@ -36,14 +36,14 @@ const waitForAbort = (signal: AbortSignal): Promise => { () => { reject(signal.reason); }, - { once: true } + { once: true }, ); }); }; const waitWithSignal = async ( ms: number, - signal: AbortSignal + signal: AbortSignal, ): Promise => { if (signal.aborted) { throw signal.reason; @@ -63,7 +63,7 @@ const EMULATOR_OUTPUT_BUFFER_LIMIT = 16 * 1024; export const emulatorProcess = { startDetachedProcess: ( file: string, - args: readonly string[] + args: readonly string[], ): ChildProcessByStdio => nodeSpawn(file, args, { detached: true, @@ -74,7 +74,7 @@ export const emulatorProcess = { const appendBoundedOutput = ( output: string, chunk: string, - limit: number = EMULATOR_OUTPUT_BUFFER_LIMIT + limit: number = EMULATOR_OUTPUT_BUFFER_LIMIT, ): string => { const nextOutput = output + chunk; @@ -131,16 +131,11 @@ const formatEmulatorStartupError = ({ }; const ensureEmulatorInstalled = async (): Promise => { - const emulatorBinaryPath = getEmulatorBinaryPath(); + await ensureAndroidEmulatorAvailable(); - try { - await access(emulatorBinaryPath); - return emulatorBinaryPath; - } catch { - await spawn(getSdkManagerBinaryPath(), ['emulator']); - await access(emulatorBinaryPath); - return emulatorBinaryPath; - } + const emulatorBinaryPath = getEmulatorBinaryPath(); + await access(emulatorBinaryPath); + return emulatorBinaryPath; }; export type CreateAvdOptions = { @@ -160,7 +155,7 @@ export const getRequiredEmulatorPackages = (apiLevel: number): string[] => { }; export const verifyAndroidEmulatorSdk = async ( - apiLevel: number + apiLevel: number, ): Promise => { await ensureAndroidSdkPackages(getRequiredEmulatorPackages(apiLevel)); }; @@ -168,7 +163,7 @@ export const verifyAndroidEmulatorSdk = async ( export const getStartAppArgs = ( bundleId: string, activityName: string, - options?: AndroidAppLaunchOptions + options?: AndroidAppLaunchOptions, ): string[] => { const args = [ 'shell', @@ -197,7 +192,7 @@ export const getStartAppArgs = ( if (!Number.isSafeInteger(value)) { throw new Error( - `Android app launch option "${key}" must be a safe integer.` + `Android app launch option "${key}" must be a safe integer.`, ); } @@ -209,7 +204,7 @@ export const getStartAppArgs = ( export const isAppInstalled = async ( adbId: string, - bundleId: string + bundleId: string, ): Promise => { const { stdout } = await spawn(getAdbBinaryPath(), [ '-s', @@ -226,7 +221,7 @@ export const isAppInstalled = async ( export const reversePort = async ( adbId: string, port: number, - hostPort: number = port + hostPort: number = port, ): Promise => { await spawn(getAdbBinaryPath(), [ '-s', @@ -239,7 +234,7 @@ export const reversePort = async ( export const stopApp = async ( adbId: string, - bundleId: string + bundleId: string, ): Promise => { await spawn(getAdbBinaryPath(), [ '-s', @@ -255,7 +250,7 @@ export const startApp = async ( adbId: string, bundleId: string, activityName: string, - options?: AndroidAppLaunchOptions + options?: AndroidAppLaunchOptions, ): Promise => { await spawn(getAdbBinaryPath(), [ '-s', @@ -274,7 +269,7 @@ export const getDeviceIds = async (): Promise => { }; export const getEmulatorName = async ( - adbId: string + adbId: string, ): Promise => { const { stdout } = await spawn(getAdbBinaryPath(), [ '-s', @@ -288,7 +283,7 @@ export const getEmulatorName = async ( export const getShellProperty = async ( adbId: string, - property: string + property: string, ): Promise => { const { stdout } = await spawn(getAdbBinaryPath(), [ '-s', @@ -310,7 +305,7 @@ export type DeviceInfo = { }; export const getDeviceInfo = async ( - adbId: string + adbId: string, ): Promise => { const manufacturer = await getShellProperty(adbId, 'ro.product.manufacturer'); const model = await getShellProperty(adbId, 'ro.product.model'); @@ -336,7 +331,7 @@ export const stopEmulator = async (adbId: string): Promise => { export const installApp = async ( adbId: string, - appPath: string + appPath: string, ): Promise => { await spawn(getAdbBinaryPath(), ['-s', adbId, 'install', '-r', appPath]); }; @@ -355,7 +350,7 @@ export const createAvd = async ({ }: CreateAvdOptions): Promise => { const systemImagePackage = getAndroidSystemImagePackage( apiLevel, - getHostAndroidSystemImageArch() + getHostAndroidSystemImageArch(), ); await verifyAndroidEmulatorSdk(apiLevel); @@ -366,7 +361,7 @@ export const createAvd = async ({ await spawn('bash', [ '-lc', `printf '%s\n%s\n' 'disk.dataPartition.size=${diskSize}' 'vm.heapSize=${heapSize}' >> "${getAvdConfigPath( - name + name, )}"`, ]); }; @@ -379,7 +374,7 @@ export const deleteAvd = async (name: string): Promise => { { force: true, recursive: true, - } + }, ); await rm( `${ @@ -387,18 +382,18 @@ export const deleteAvd = async (name: string): Promise => { }/${name}.ini`, { force: true, - } + }, ); }; export const startEmulator = async ( name: string, - mode: EmulatorBootMode = 'default-boot' + mode: EmulatorBootMode = 'default-boot', ): Promise => { const emulatorBinaryPath = await ensureEmulatorInstalled(); const childProcess = emulatorProcess.startDetachedProcess( emulatorBinaryPath, - getEmulatorStartupArgs(name, mode) + getEmulatorStartupArgs(name, mode), ); let stdout = ''; @@ -434,7 +429,7 @@ export const startEmulator = async ( stdout, stderr, error, - }) + }), ); }); @@ -446,7 +441,7 @@ export const startEmulator = async ( stderr, exitCode, signal, - }) + }), ); }); }); @@ -462,7 +457,7 @@ export const startEmulator = async ( }); const observationTimeout = wait(EMULATOR_STARTUP_OBSERVATION_TIMEOUT_MS).then( - () => 'timeout' as const + () => 'timeout' as const, ); try { @@ -478,7 +473,7 @@ export const startEmulator = async ( export const waitForEmulator = async ( name: string, - signal: AbortSignal + signal: AbortSignal, ): Promise => { while (!signal.aborted) { const adbIds = await getDeviceIds(); @@ -503,7 +498,7 @@ export const waitForEmulator = async ( export const waitForEmulatorDisconnect = async ( adbId: string, - signal: AbortSignal + signal: AbortSignal, ): Promise => { while (!signal.aborted) { const adbIds = await getDeviceIds(); @@ -520,7 +515,7 @@ export const waitForEmulatorDisconnect = async ( export const waitForBoot = async ( name: string, - signal: AbortSignal + signal: AbortSignal, ): Promise => { while (!signal.aborted) { const adbIds = await getDeviceIds(); @@ -549,7 +544,7 @@ export const waitForBoot = async ( export const isAppRunning = async ( adbId: string, - bundleId: string + bundleId: string, ): Promise => { try { const { stdout } = await spawn(getAdbBinaryPath(), [ @@ -571,7 +566,7 @@ export const isAppRunning = async ( export const getAppUid = async ( adbId: string, - bundleId: string + bundleId: string, ): Promise => { const { stdout } = await spawn(getAdbBinaryPath(), [ '-s', @@ -596,7 +591,7 @@ export const getAppUid = async ( export const setHideErrorDialogs = async ( adbId: string, - hide: boolean + hide: boolean, ): Promise => { await spawn(getAdbBinaryPath(), [ '-s', diff --git a/packages/platform-android/src/environment.ts b/packages/platform-android/src/environment.ts index 62c31a5..51cb555 100644 --- a/packages/platform-android/src/environment.ts +++ b/packages/platform-android/src/environment.ts @@ -21,7 +21,7 @@ type AndroidSdkRootOptions = { }; const getConfiguredAndroidSdkRoot = ( - env: NodeJS.ProcessEnv = process.env + env: NodeJS.ProcessEnv = process.env, ): string | null => { return env.ANDROID_HOME ?? env.ANDROID_SDK_ROOT ?? null; }; @@ -42,7 +42,7 @@ export const getDefaultUnixAndroidSdkRoot = ({ }; const canBootstrapAndroidSdk = ( - platform: NodeJS.Platform = process.platform + platform: NodeJS.Platform = process.platform, ) => { return platform === 'darwin' || platform === 'linux'; }; @@ -79,8 +79,8 @@ const downloadText = async (url: string): Promise => { response.resume(); reject( new Error( - `Failed to download Android repository index from ${url} (status ${statusCode}).` - ) + `Failed to download Android repository index from ${url} (status ${statusCode}).`, + ), ); return; } @@ -102,7 +102,7 @@ const downloadText = async (url: string): Promise => { const downloadFile = async ( url: string, - destinationPath: string + destinationPath: string, ): Promise => { await new Promise((resolve, reject) => { const request = https.get(url, (response) => { @@ -122,8 +122,8 @@ const downloadFile = async ( response.resume(); reject( new Error( - `Failed to download Android command-line tools from ${url} (status ${statusCode}).` - ) + `Failed to download Android command-line tools from ${url} (status ${statusCode}).`, + ), ); return; } @@ -137,27 +137,27 @@ const downloadFile = async ( }; const getCommandLineToolsArchiveUrl = async ( - platform: NodeJS.Platform = process.platform + platform: NodeJS.Platform = process.platform, ): Promise => { const archivePlatform = platform === 'darwin' ? 'mac' : platform === 'linux' ? 'linux' : null; if (!archivePlatform) { throw new Error( - 'Automatic Android SDK bootstrap is only supported on macOS and Linux.' + 'Automatic Android SDK bootstrap is only supported on macOS and Linux.', ); } const repositoryIndex = await downloadText(ANDROID_REPOSITORY_INDEX_URL); const archivePattern = new RegExp( `commandlinetools-${archivePlatform}-(\\d+)_latest\\.zip`, - 'g' + 'g', ); const matches = [...repositoryIndex.matchAll(archivePattern)]; if (matches.length === 0) { throw new Error( - `Failed to resolve Android command-line tools archive for ${archivePlatform}.` + `Failed to resolve Android command-line tools archive for ${archivePlatform}.`, ); } @@ -173,7 +173,7 @@ const getCommandLineToolsArchiveUrl = async ( const ensureAndroidCommandLineTools = async ( sdkRoot: string, - platform: NodeJS.Platform = process.platform + platform: NodeJS.Platform = process.platform, ): Promise => { if ( (await pathExists(getSdkManagerBinaryPath(sdkRoot))) && @@ -184,19 +184,19 @@ const ensureAndroidCommandLineTools = async ( if (!canBootstrapAndroidSdk(platform)) { throw new Error( - 'Android command-line tools are missing. Set ANDROID_HOME or ANDROID_SDK_ROOT to an initialized SDK.' + 'Android command-line tools are missing. Set ANDROID_HOME or ANDROID_SDK_ROOT to an initialized SDK.', ); } androidEnvironmentLogger.info( 'Bootstrapping Android command-line tools in %s', - sdkRoot + sdkRoot, ); await mkdir(sdkRoot, { recursive: true }); const temporaryDirectory = await mkdtemp( - path.join(os.tmpdir(), 'android-cmdline-tools-') + path.join(os.tmpdir(), 'android-cmdline-tools-'), ); const archivePath = path.join(temporaryDirectory, 'cmdline-tools.zip'); const extractedPath = path.join(temporaryDirectory, 'extracted'); @@ -206,7 +206,7 @@ const ensureAndroidCommandLineTools = async ( try { await downloadFile( await getCommandLineToolsArchiveUrl(platform), - archivePath + archivePath, ); await spawn('unzip', ['-q', archivePath, '-d', extractedPath]); await rm(targetDirectory, { force: true, recursive: true }); @@ -225,7 +225,7 @@ const acceptAndroidLicenses = async (sdkRoot: string): Promise => { [ '-lc', `yes | ${quoteShell(sdkManagerBinaryPath)} --sdk_root=${quoteShell( - sdkRoot + sdkRoot, )} --licenses >/dev/null`, ], { @@ -234,13 +234,13 @@ const acceptAndroidLicenses = async (sdkRoot: string): Promise => { ANDROID_HOME: sdkRoot, ANDROID_SDK_ROOT: sdkRoot, }), - } + }, ); }; const getPackageVerificationPath = ( sdkRoot: string, - packageName: string + packageName: string, ): string | null => { if (packageName === 'platform-tools') { return getAdbBinaryPath(sdkRoot); @@ -263,7 +263,7 @@ const getPackageVerificationPath = ( const getMissingAndroidSdkPackages = async ( sdkRoot: string, - packages: readonly string[] + packages: readonly string[], ): Promise => { const missingPackages: string[] = []; @@ -284,7 +284,7 @@ const getMissingAndroidSdkPackages = async ( const installAndroidSdkPackages = async ( sdkRoot: string, - packages: readonly string[] + packages: readonly string[], ): Promise => { if (packages.length === 0) { return; @@ -297,7 +297,7 @@ const installAndroidSdkPackages = async ( androidEnvironmentLogger.info( 'Installing missing Android SDK packages: %s', - packages.join(', ') + packages.join(', '), ); await acceptAndroidLicenses(sdkRoot); @@ -306,7 +306,7 @@ const installAndroidSdkPackages = async ( [ '-lc', `yes | ${quoteShell(sdkManagerBinaryPath)} --sdk_root=${quoteShell( - sdkRoot + sdkRoot, )} ${packageArgs}`, ], { @@ -315,13 +315,13 @@ const installAndroidSdkPackages = async ( ANDROID_HOME: sdkRoot, ANDROID_SDK_ROOT: sdkRoot, }), - } + }, ); }; export const getAndroidSdkRoot = ( env: NodeJS.ProcessEnv = process.env, - options: Omit = {} + options: Omit = {}, ): string | null => { return ( getConfiguredAndroidSdkRoot(env) ?? getDefaultUnixAndroidSdkRoot(options) @@ -330,13 +330,13 @@ export const getAndroidSdkRoot = ( const getRequiredAndroidSdkRoot = ( env: NodeJS.ProcessEnv = process.env, - options: Omit = {} + options: Omit = {}, ): string => { const sdkRoot = getAndroidSdkRoot(env, options); if (!sdkRoot) { throw new Error( - 'Android SDK root is not configured. Set ANDROID_HOME or ANDROID_SDK_ROOT.' + 'Android SDK root is not configured. Set ANDROID_HOME or ANDROID_SDK_ROOT.', ); } @@ -344,7 +344,7 @@ const getRequiredAndroidSdkRoot = ( }; export const getHostAndroidSystemImageArch = ( - architecture: string = process.arch + architecture: string = process.arch, ): AndroidSystemImageArch => { switch (architecture) { case 'arm64': @@ -363,7 +363,7 @@ export const getAndroidPlatformPackage = (apiLevel: number): string => { export const getAndroidSystemImagePackage = ( apiLevel: number, - architecture: AndroidSystemImageArch = getHostAndroidSystemImageArch() + architecture: AndroidSystemImageArch = getHostAndroidSystemImageArch(), ): string => { return `system-images;android-${apiLevel};default;${architecture}`; }; @@ -393,71 +393,115 @@ export const getRequiredAndroidSdkPackages = ({ return packages; }; -export const ensureAndroidSdkPackages = async ( +const getMissingAndroidSdkPackagesForEnvironment = async ( packages: readonly string[], { env = process.env, platform = process.platform, homeDirectory = os.homedir(), - }: AndroidSdkRootOptions = {} -): Promise => { + }: AndroidSdkRootOptions = {}, +): Promise<{ sdkRoot: string; missingPackages: string[] }> => { const sdkRoot = getRequiredAndroidSdkRoot(env, { platform, homeDirectory }); await mkdir(sdkRoot, { recursive: true }); - await ensureAndroidCommandLineTools(sdkRoot, platform); - const missingPackages = await getMissingAndroidSdkPackages(sdkRoot, packages); + return { + sdkRoot, + missingPackages: await getMissingAndroidSdkPackages(sdkRoot, packages), + }; +}; - if (missingPackages.length > 0) { - await installAndroidSdkPackages(sdkRoot, missingPackages); +export const ensureAndroidSdkPackages = async ( + packages: readonly string[], + { + env = process.env, + platform = process.platform, + homeDirectory = os.homedir(), + }: AndroidSdkRootOptions = {}, +): Promise => { + const { sdkRoot, missingPackages } = + await getMissingAndroidSdkPackagesForEnvironment(packages, { + env, + platform, + homeDirectory, + }); + + if (missingPackages.length === 0) { + return sdkRoot; } + await ensureAndroidCommandLineTools(sdkRoot, platform); + + await installAndroidSdkPackages(sdkRoot, missingPackages); + const unresolvedPackages = await getMissingAndroidSdkPackages( sdkRoot, - packages + packages, ); if (unresolvedPackages.length > 0) { throw new Error( `Android SDK packages are still missing after installation: ${unresolvedPackages.join( - ', ' - )}` + ', ', + )}`, ); } return sdkRoot; }; -export const ensureAndroidDiscoveryEnvironment = async (): Promise => { - initializeAndroidProcessEnv(); +export const ensureAndroidAdbAvailable = async ( + options: AndroidSdkRootOptions = {}, +): Promise => { + return ensureAndroidSdkPackages(['platform-tools'], options); +}; +export const ensureAndroidEmulatorAvailable = async ( + options: AndroidSdkRootOptions = {}, +): Promise => { + return ensureAndroidSdkPackages(['emulator'], options); +}; + +export const ensureAndroidAvdProvisioningAvailable = async ( + apiLevel: number, + architecture: AndroidSystemImageArch = getHostAndroidSystemImageArch(), + options: AndroidSdkRootOptions = {}, +): Promise => { return ensureAndroidSdkPackages( - getRequiredAndroidSdkPackages({ includeEmulator: true }) + [ + getAndroidPlatformPackage(apiLevel), + getAndroidSystemImagePackage(apiLevel, architecture), + ], + options, ); }; +export const ensureAndroidDiscoveryEnvironment = async (): Promise => { + initializeAndroidProcessEnv(); + + return ensureAndroidAdbAvailable(); +}; + export const ensureAndroidPhysicalDeviceEnvironment = async (): Promise => { initializeAndroidProcessEnv(); - return ensureAndroidSdkPackages(getRequiredAndroidSdkPackages()); + return ensureAndroidAdbAvailable(); }; export const ensureAndroidEmulatorEnvironment = async ( - apiLevel: number + apiLevel: number, ): Promise => { initializeAndroidProcessEnv(); - return ensureAndroidSdkPackages( - getRequiredAndroidSdkPackages({ - apiLevel, - includeEmulator: true, - }) - ); + await ensureAndroidAdbAvailable(); + await ensureAndroidEmulatorAvailable(); + + return ensureAndroidAvdProvisioningAvailable(apiLevel); }; export const getAndroidProcessEnv = ( - env: NodeJS.ProcessEnv = process.env + env: NodeJS.ProcessEnv = process.env, ): NodeJS.ProcessEnv => { const sdkRoot = getAndroidSdkRoot(env); @@ -492,19 +536,19 @@ export const initializeAndroidProcessEnv = (): void => { }; export const getAdbBinaryPath = ( - sdkRoot: string = getRequiredAndroidSdkRoot() + sdkRoot: string = getRequiredAndroidSdkRoot(), ): string => path.join(sdkRoot, 'platform-tools', 'adb'); export const getEmulatorBinaryPath = ( - sdkRoot: string = getRequiredAndroidSdkRoot() + sdkRoot: string = getRequiredAndroidSdkRoot(), ): string => path.join(sdkRoot, 'emulator', 'emulator'); export const getSdkManagerBinaryPath = ( - sdkRoot: string = getRequiredAndroidSdkRoot() + sdkRoot: string = getRequiredAndroidSdkRoot(), ): string => path.join(sdkRoot, ...CMDLINE_TOOLS_PATH_SEGMENTS, 'bin', 'sdkmanager'); export const getAvdManagerBinaryPath = ( - sdkRoot: string = getRequiredAndroidSdkRoot() + sdkRoot: string = getRequiredAndroidSdkRoot(), ): string => path.join(sdkRoot, ...CMDLINE_TOOLS_PATH_SEGMENTS, 'bin', 'avdmanager'); diff --git a/packages/platform-android/src/instance.ts b/packages/platform-android/src/instance.ts index 335e9cf..5cf97ab 100644 --- a/packages/platform-android/src/instance.ts +++ b/packages/platform-android/src/instance.ts @@ -27,6 +27,7 @@ import { getDeviceName } from './utils.js'; import { createAndroidAppMonitor } from './app-monitor.js'; import { HarnessAppPathError, HarnessEmulatorConfigError } from './errors.js'; import { + ensureAndroidEmulatorAvailable, ensureAndroidEmulatorEnvironment, getHostAndroidSystemImageArch, } from './environment.js'; @@ -61,7 +62,7 @@ const getHarnessAppPath = (): string => { const configureAndroidRuntime = async ( adbId: string, config: AndroidPlatformConfig, - harnessConfig: HarnessConfig + harnessConfig: HarnessConfig, ): Promise => { const metroPort = harnessConfig.metroPort; @@ -84,6 +85,7 @@ const startAndWaitForBoot = async ({ signal: AbortSignal; mode?: Parameters[1]; }): Promise => { + await ensureAndroidEmulatorAvailable(); await adb.startEmulator(emulatorName, mode); return adb.waitForBoot(emulatorName, signal); }; @@ -137,14 +139,14 @@ const prepareCachedAvd = async ({ hasExistingAvd ? 'Recreating incompatible Android emulator %s...' : 'Creating Android emulator %s...', - emulatorName + emulatorName, ); if (hasExistingAvd && !compatibility.compatible) { androidInstanceLogger.debug( 'Android AVD %s is not reusable: %s', emulatorName, - compatibility.reason + compatibility.reason, ); await adb.deleteAvd(emulatorName); } @@ -174,7 +176,7 @@ const prepareCachedAvd = async ({ export const getAndroidEmulatorPlatformInstance = async ( config: AndroidPlatformConfig, harnessConfig: HarnessConfig, - init: HarnessPlatformInitOptions + init: HarnessPlatformInitOptions, ): Promise => { assertAndroidDeviceEmulator(config.device); const detectNativeCrashes = harnessConfig.detectNativeCrashes ?? true; @@ -192,7 +194,7 @@ export const getAndroidEmulatorPlatformInstance = async ( androidInstanceLogger.debug( 'resolved Android emulator %s with adb id %s', emulatorConfig.name, - adbId ?? 'not-found' + adbId ?? 'not-found', ); if (!adbId) { @@ -212,7 +214,7 @@ export const getAndroidEmulatorPlatformInstance = async ( logger.info('Creating Android emulator %s...', emulatorName); androidInstanceLogger.debug( 'creating Android AVD %s before startup', - emulatorConfig.name + emulatorConfig.name, ); await recreateAvd({ emulatorConfig }); } else { @@ -221,7 +223,7 @@ export const getAndroidEmulatorPlatformInstance = async ( androidInstanceLogger.debug( 'starting Android emulator %s', - emulatorConfig.name + emulatorConfig.name, ); return startAndWaitForBoot({ emulatorName: emulatorConfig.name, @@ -234,10 +236,8 @@ export const getAndroidEmulatorPlatformInstance = async ( androidInstanceLogger.debug( 'Android emulator %s connected as %s', emulatorConfig.name, - adbId + adbId, ); - } else if (emulatorConfig.avd) { - await ensureAndroidEmulatorEnvironment(emulatorConfig.avd.apiLevel); } if (!adbId) { @@ -246,7 +246,7 @@ export const getAndroidEmulatorPlatformInstance = async ( androidInstanceLogger.debug( 'waiting for Android emulator %s to finish booting', - adbId + adbId, ); const isInstalled = await adb.isAppInstalled(adbId, config.bundleId); @@ -265,7 +265,7 @@ export const getAndroidEmulatorPlatformInstance = async ( config.bundleId, config.activityName, (options as typeof config.appLaunchOptions | undefined) ?? - config.appLaunchOptions + config.appLaunchOptions, ); }, restartApp: async (options) => { @@ -275,7 +275,7 @@ export const getAndroidEmulatorPlatformInstance = async ( config.bundleId, config.activityName, (options as typeof config.appLaunchOptions | undefined) ?? - config.appLaunchOptions + config.appLaunchOptions, ); }, stopApp: async () => { @@ -311,7 +311,7 @@ export const getAndroidEmulatorPlatformInstance = async ( export const getAndroidPhysicalDevicePlatformInstance = async ( config: AndroidPlatformConfig, - harnessConfig: HarnessConfig + harnessConfig: HarnessConfig, ): Promise => { assertAndroidDevicePhysical(config.device); const detectNativeCrashes = harnessConfig.detectNativeCrashes ?? true; @@ -327,7 +327,7 @@ export const getAndroidPhysicalDevicePlatformInstance = async ( if (!isInstalled) { throw new AppNotInstalledError( config.bundleId, - getDeviceName(config.device) + getDeviceName(config.device), ); } @@ -340,7 +340,7 @@ export const getAndroidPhysicalDevicePlatformInstance = async ( config.bundleId, config.activityName, (options as typeof config.appLaunchOptions | undefined) ?? - config.appLaunchOptions + config.appLaunchOptions, ); }, restartApp: async (options) => { @@ -350,7 +350,7 @@ export const getAndroidPhysicalDevicePlatformInstance = async ( config.bundleId, config.activityName, (options as typeof config.appLaunchOptions | undefined) ?? - config.appLaunchOptions + config.appLaunchOptions, ); }, stopApp: async () => { diff --git a/packages/platform-android/src/runner.ts b/packages/platform-android/src/runner.ts index eec611a..191b090 100644 --- a/packages/platform-android/src/runner.ts +++ b/packages/platform-android/src/runner.ts @@ -13,34 +13,29 @@ import { getAndroidPhysicalDevicePlatformInstance, } from './instance.js'; import { - ensureAndroidEmulatorEnvironment, - ensureAndroidPhysicalDeviceEnvironment, + ensureAndroidAdbAvailable, initializeAndroidProcessEnv, } from './environment.js'; const getAndroidRunner = async ( config: AndroidPlatformConfig, harnessConfig: HarnessConfig, - init: HarnessPlatformInitOptions + init: HarnessPlatformInitOptions, ): Promise => { const parsedConfig = AndroidPlatformConfigSchema.parse(config); initializeAndroidProcessEnv(); - if (isAndroidDeviceEmulator(parsedConfig.device)) { - if (parsedConfig.device.avd) { - await ensureAndroidEmulatorEnvironment(parsedConfig.device.avd.apiLevel); - } + await ensureAndroidAdbAvailable(); + if (isAndroidDeviceEmulator(parsedConfig.device)) { return getAndroidEmulatorPlatformInstance( parsedConfig, harnessConfig, - init + init, ); } - await ensureAndroidPhysicalDeviceEnvironment(); - return getAndroidPhysicalDevicePlatformInstance(parsedConfig, harnessConfig); }; diff --git a/packages/platform-android/src/targets.ts b/packages/platform-android/src/targets.ts index ea4765a..975f0d5 100644 --- a/packages/platform-android/src/targets.ts +++ b/packages/platform-android/src/targets.ts @@ -1,9 +1,16 @@ import { RunTarget } from '@react-native-harness/platforms'; import * as adb from './adb.js'; -import { ensureAndroidDiscoveryEnvironment } from './environment.js'; +import { + ensureAndroidAdbAvailable, + ensureAndroidEmulatorAvailable, + initializeAndroidProcessEnv, +} from './environment.js'; export const getRunTargets = async (): Promise => { - await ensureAndroidDiscoveryEnvironment(); + initializeAndroidProcessEnv(); + + await ensureAndroidAdbAvailable(); + await ensureAndroidEmulatorAvailable(); const [avds, connectedDevices] = await Promise.all([ adb.getAvds(),