From a7054c14d89cd6b21f1a29b84d37e661b3e87b96 Mon Sep 17 00:00:00 2001 From: Gabriel Donadel Dall'Agnol Date: Mon, 27 May 2024 10:24:34 -0300 Subject: [PATCH] [menu-bar][cli] Automatically select correct iOS device depending on app type (#200) * [menu-bar][cli] Automatically select correct iOS device depending on app type * Add changelog entry --- CHANGELOG.md | 1 + apps/cli/src/commands/DetectIOSAppType.ts | 11 +++++ apps/cli/src/index.ts | 6 +++ .../src/commands/detectIOSAppTypeAsync'.ts | 11 +++++ apps/menu-bar/src/popover/Core.tsx | 25 ++++++++--- .../ios/appleDevice/installOnDeviceAsync.ts | 8 ++++ packages/eas-shared/src/run/ios/devicectl.ts | 41 +++++++++++++++++++ 7 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 apps/cli/src/commands/DetectIOSAppType.ts create mode 100644 apps/menu-bar/src/commands/detectIOSAppTypeAsync'.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 992f9d01..842815d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ### 🎉 New features - Add experimental support for Windows and Linux. ([#152](https://github.com/expo/orbit/pull/152), [#157](https://github.com/expo/orbit/pull/157), [#158](https://github.com/expo/orbit/pull/158), [#160](https://github.com/expo/orbit/pull/160), [#161](https://github.com/expo/orbit/pull/161), [#165](https://github.com/expo/orbit/pull/165), [#170](https://github.com/expo/orbit/pull/170), [#171](https://github.com/expo/orbit/pull/171), [#172](https://github.com/expo/orbit/pull/172), [#173](https://github.com/expo/orbit/pull/173), [#174](https://github.com/expo/orbit/pull/174), [#175](https://github.com/expo/orbit/pull/175), [#177](https://github.com/expo/orbit/pull/177), [#178](https://github.com/expo/orbit/pull/178), [#180](https://github.com/expo/orbit/pull/180), [#181](https://github.com/expo/orbit/pull/181), [#182](https://github.com/expo/orbit/pull/182), [#185](https://github.com/expo/orbit/pull/185), [#191](https://github.com/expo/orbit/pull/191) by [@gabrieldonadel](https://github.com/gabrieldonadel)) +- Automatically select the correct iOS device depending on app type. ([#200](https://github.com/expo/orbit/pull/200) by [@gabrieldonadel](https://github.com/gabrieldonadel)) ### 🐛 Bug fixes diff --git a/apps/cli/src/commands/DetectIOSAppType.ts b/apps/cli/src/commands/DetectIOSAppType.ts new file mode 100644 index 00000000..34dcabf2 --- /dev/null +++ b/apps/cli/src/commands/DetectIOSAppType.ts @@ -0,0 +1,11 @@ +import { extractAppFromLocalArchiveAsync, detectIOSAppType } from 'eas-shared'; + +export async function detectIOSAppTypeAsync(appPath: string) { + if (!appPath.endsWith('.app') && !appPath.endsWith('.ipa')) { + appPath = await extractAppFromLocalArchiveAsync(appPath); + } + + const appType = await detectIOSAppType(appPath); + + return appType; +} diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 3612ab9a..d4348bd4 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -7,6 +7,7 @@ import { installAndLaunchAppAsync } from './commands/InstallAndLaunchApp'; import { launchSnackAsync } from './commands/LaunchSnack'; import { checkToolsAsync } from './commands/CheckTools'; import { setSessionAsync } from './commands/SetSession'; +import { detectIOSAppTypeAsync } from './commands/DetectIOSAppType'; import { returnLoggerMiddleware } from './utils'; const program = new Command(); @@ -66,6 +67,11 @@ program .argument('', 'Session secret') .action(returnLoggerMiddleware(setSessionAsync)); +program + .command('detect-ios-app-type') + .argument('', 'Local path of the app') + .action(returnLoggerMiddleware(detectIOSAppTypeAsync)); + if (process.argv.length < 3) { program.help(); } diff --git a/apps/menu-bar/src/commands/detectIOSAppTypeAsync'.ts b/apps/menu-bar/src/commands/detectIOSAppTypeAsync'.ts new file mode 100644 index 00000000..4b714c34 --- /dev/null +++ b/apps/menu-bar/src/commands/detectIOSAppTypeAsync'.ts @@ -0,0 +1,11 @@ +import { Device } from 'common-types/build/devices'; + +import MenuBarModule from '../modules/MenuBarModule'; + +export const detectIOSAppTypeAsync = async (appPath: string) => { + return (await MenuBarModule.runCli( + 'detect-ios-app-type', + [appPath], + console.log + )) as Device['deviceType']; +}; diff --git a/apps/menu-bar/src/popover/Core.tsx b/apps/menu-bar/src/popover/Core.tsx index c3b493bf..25ddf7f7 100644 --- a/apps/menu-bar/src/popover/Core.tsx +++ b/apps/menu-bar/src/popover/Core.tsx @@ -13,6 +13,7 @@ import { SECTION_HEADER_HEIGHT } from './SectionHeader'; import { Analytics, Event } from '../analytics'; import { withApolloProvider } from '../api/ApolloClient'; import { bootDeviceAsync } from '../commands/bootDeviceAsync'; +import { detectIOSAppTypeAsync } from "../commands/detectIOSAppTypeAsync'"; import { downloadBuildAsync } from '../commands/downloadBuildAsync'; import { installAndLaunchAppAsync } from '../commands/installAndLaunchAppAsync'; import { launchSnackAsync } from '../commands/launchSnackAsync'; @@ -146,21 +147,29 @@ function Core(props: Props) { ); const getDeviceByPlatform = useCallback( - (platform: 'android' | 'ios') => { + (platform: 'android' | 'ios', deviceType?: Device['deviceType']) => { const devices = devicesPerPlatform[platform].devices; const selectedDevicesId = selectedDevicesIds[platform]; if (selectedDevicesId && devices.has(selectedDevicesId)) { - return devices.get(selectedDevicesId); + const device = devices.get(selectedDevicesId); + if (!deviceType || device?.deviceType === deviceType) { + return devices.get(selectedDevicesId); + } } for (const device of devices.values()) { - if (isVirtualDevice(device) && device.state === 'Booted') { + if ( + (deviceType === 'device' && device.deviceType === deviceType) || + (deviceType !== 'device' && isVirtualDevice(device) && device.state === 'Booted') + ) { setSelectedDevicesIds((prev) => ({ ...prev, [platform]: getDeviceId(device) })); return device; } } - const [firstDevice] = devices.values(); + const firstDevice = [...devices.values()].find( + (d) => !deviceType || d.deviceType === deviceType + ); if (!firstDevice) { return; } @@ -302,7 +311,13 @@ function Core(props: Props) { } const platform = getPlatformFromURI(appURI); - const device = getDeviceByPlatform(platform); + + let appType: Device['deviceType'] | undefined; + if (platform === 'ios') { + appType = await detectIOSAppTypeAsync(localFilePath); + } + + const device = getDeviceByPlatform(platform, appType); if (!device) { Alert.alert( `You don't have any ${platform} device available to run this build, please make sure your environment is configured correctly and try again.` diff --git a/packages/eas-shared/src/run/ios/appleDevice/installOnDeviceAsync.ts b/packages/eas-shared/src/run/ios/appleDevice/installOnDeviceAsync.ts index 87fefb77..9121165b 100644 --- a/packages/eas-shared/src/run/ios/appleDevice/installOnDeviceAsync.ts +++ b/packages/eas-shared/src/run/ios/appleDevice/installOnDeviceAsync.ts @@ -2,6 +2,7 @@ import os from 'os'; import path from 'path'; import * as AppleDevice from './AppleDevice'; +import * as devicectl from '../devicectl'; import { ensureDirectory } from '../../../utils/dir'; import { InternalError } from 'common-types'; @@ -39,6 +40,13 @@ export async function installOnDeviceAsync(props: { }, }); } catch (error: any) { + if (error.code === 'APPLE_DEVICE_USBMUXD') { + // Usbmux can only find wireless devices if they are unlocked. Fallback on much slower devicectl. + if (devicectl.hasDevicectlEverBeenInstalled()) { + return await devicectl.installAndLaunchAppAsync({ bundle, bundleIdentifier, udid }); + } + } + if (error.code === 'APPLE_DEVICE_LOCKED') { // Get the app name from the binary path. const appName = path.basename(bundle).split('.')[0] ?? 'app'; diff --git a/packages/eas-shared/src/run/ios/devicectl.ts b/packages/eas-shared/src/run/ios/devicectl.ts index 9762fa3a..41d4fe58 100644 --- a/packages/eas-shared/src/run/ios/devicectl.ts +++ b/packages/eas-shared/src/run/ios/devicectl.ts @@ -239,3 +239,44 @@ function isDevicectlInstalled() { return false; } } + +/** + * Wraps the apple device method for installing and running an app + */ +export async function installAndLaunchAppAsync(props: { + bundle: string; + bundleIdentifier: string; + udid: string; +}): Promise { + const { bundle, bundleIdentifier, udid } = props; + + await installAppWithDeviceCtlAsync(udid, bundle); + + async function launchAppOptionally() { + try { + await launchAppWithDeviceCtl(udid, bundleIdentifier); + } catch (error: any) { + if (error.code === 'APPLE_DEVICE_LOCKED') { + // Get the app name from the binary path. + const appName = path.basename(bundle).split('.')[0] ?? 'app'; + throw new CommandError(`Cannot launch ${appName} because the device is locked.`); + } + if (error.message.includes('BSErrorCodeDescription = fairplay')) { + throw new CommandError( + `Unable to launch app due to a FairPlay failure. Ensure this device is included in your provisioning profile` + ); + } + + throw error; + } + } + + await launchAppOptionally(); +} + +async function installAppWithDeviceCtlAsync( + uuid: string, + bundleIdOrAppPath: string +): Promise { + await xcrunAsync(['devicectl', 'device', 'install', 'app', '--device', uuid, bundleIdOrAppPath]); +}