Skip to content

Commit

Permalink
[menu-bar][cli] Add support for iOS internal distribution apps (#79)
Browse files Browse the repository at this point in the history
* [menu-bar][cli] Add support for iOS internal distribution apps

* Add changelog entry
  • Loading branch information
gabrieldonadel authored Oct 14, 2023
1 parent de91e1e commit 6bab397
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 27 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- Added Projects section to the menu bar. ([#46](https://github.com/expo/orbit/pull/46), [#59](https://github.com/expo/orbit/pull/59) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- Added support for login to Expo. ([#41](https://github.com/expo/orbit/pull/41), [#43](https://github.com/expo/orbit/pull/43), [#44](https://github.com/expo/orbit/pull/44), [#45](https://github.com/expo/orbit/pull/45), [#62](https://github.com/expo/orbit/pull/62), [#67](https://github.com/expo/orbit/pull/67) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- Focus simulator/emulator window when launching an app. ([#75](https://github.com/expo/orbit/pull/75) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- Add support for running iOS internal distribution apps on real devices. ([#79](https://github.com/expo/orbit/pull/79) by [@gabrieldonadel](https://github.com/gabrieldonadel))

### 🐛 Bug fixes

Expand Down
23 changes: 20 additions & 3 deletions apps/cli/src/commands/InstallAndLaunchApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
Emulator,
Simulator,
extractAppFromLocalArchiveAsync,
AppleDevice,
} from "eas-shared";
import { Platform } from "common-types/build/cli-commands";

Expand All @@ -27,9 +28,25 @@ export async function installAndLaunchAppAsync(
}

async function installAndLaunchIOSAppAsync(appPath: string, deviceId: string) {
const bundleIdentifier = await Simulator.getAppBundleIdentifierAsync(appPath);
await Simulator.installAppAsync(deviceId, appPath);
await Simulator.launchAppAsync(deviceId, bundleIdentifier);
if (
(await Simulator.getAvailableIosSimulatorsListAsync()).find(
({ udid }) => udid === deviceId
)
) {
const bundleIdentifier =
await Simulator.getAppBundleIdentifierAsync(appPath);
await Simulator.installAppAsync(deviceId, appPath);
await Simulator.launchAppAsync(deviceId, bundleIdentifier);
return;
}

const appId = await AppleDevice.getBundleIdentifierForBinaryAsync(appPath);
await AppleDevice.installOnDeviceAsync({
bundleIdentifier: appId,
bundle: appPath,
appDeltaDirectory: AppleDevice.getAppDeltaDirectory(appId),
udid: deviceId,
});
}

async function installAndLaunchAndroidAppAsync(
Expand Down
34 changes: 19 additions & 15 deletions apps/menu-bar/src/popover/Core.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,22 +167,26 @@ function Core(props: Props) {
try {
await installAndLaunchAppAsync({ appPath: localFilePath, deviceId });
} catch (error) {
if (
error instanceof InternalError &&
error.code === 'MULTIPLE_APPS_IN_TARBALL' &&
error.details
) {
const { apps } = error.details as MultipleAppsInTarballErrorDetails;
const selectedAppNameIndex = await MenuBarModule.showMultiOptionAlert(
'Multiple apps where detected in the tarball',
'Select which app to run:',
apps.map((app) => app.name)
);
if (error instanceof InternalError) {
if (error.code === 'MULTIPLE_APPS_IN_TARBALL' && error.details) {
const { apps } = error.details as MultipleAppsInTarballErrorDetails;
const selectedAppNameIndex = await MenuBarModule.showMultiOptionAlert(
'Multiple apps where detected in the tarball',
'Select which app to run:',
apps.map((app) => app.name)
);

await installAndLaunchAppAsync({
appPath: apps[selectedAppNameIndex].path,
deviceId,
});
await installAndLaunchAppAsync({
appPath: apps[selectedAppNameIndex].path,
deviceId,
});
}
if (error.code === 'APPLE_DEVICE_LOCKED') {
Alert.alert(
'Please unlock your device and open the app manually',
'We were unable to launch your app because the device is currently locked.'
);
}
} else {
throw error;
}
Expand Down
1 change: 1 addition & 0 deletions packages/common-types/src/InternalError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default class InternalError extends Error {
}

export type InternalErrorCode =
| "APPLE_DEVICE_LOCKED"
| "INVALID_VERSION"
| "MULTIPLE_APPS_IN_TARBALL"
| "XCODE_COMMAND_LINE_TOOLS_NOT_INSTALLED"
Expand Down
59 changes: 57 additions & 2 deletions packages/eas-shared/src/run/ios/appleDevice/AppleDevice.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from "fs";
import path from "path";
import { AppleConnectedDevice } from "common-types/build/devices";
import debug from "debug";

import { ClientManager } from "./ClientManager";
import { XcodeDeveloperDiskImagePrerequisite } from "./XcodeDeveloperDiskImagePrerequisite";
Expand All @@ -13,7 +14,9 @@ import { UsbmuxdClient } from "./client/UsbmuxdClient";
import { AFC_STATUS, AFCError } from "./protocol/AFCProtocol";
import { delayAsync } from "../../../utils/delayAsync";
import { CommandError } from "../../../utils/errors";
import { parseBinaryPlistAsync } from "../../../utils/parseBinaryPlistAsync";
import { installExitHooks } from "../../../utils/exit";
import { xcrunAsync } from "../xcrun";

/** @returns a list of connected Apple devices. */
export async function getConnectedDevicesAsync(): Promise<
Expand Down Expand Up @@ -115,9 +118,10 @@ export async function runOnDevice({
await delayAsync(200);
const debugServerClient = await launchApp(clientManager, {
appInfo,
bundleId,
detach: !waitForApp,
});
if (waitForApp) {
if (waitForApp && debugServerClient) {
installExitHooks(async () => {
// causes continue() to return
debugServerClient.halt();
Expand Down Expand Up @@ -191,7 +195,7 @@ async function uploadApp(
await afcClient.uploadDirectory(appBinaryPath, destinationPath);
}

async function launchApp(
async function launchAppWithUsbmux(
clientManager: ClientManager,
{ appInfo, detach }: { appInfo: IPLookupResult[string]; detach?: boolean }
) {
Expand Down Expand Up @@ -229,3 +233,54 @@ async function launchApp(
}
throw new CommandError("Unable to launch app, number of tries exceeded");
}

async function launchAppWithDeviceCtl(deviceId: string, bundleId: string) {
await xcrunAsync([
"devicectl",
"device",
"process",
"launch",
"--device",
deviceId,
bundleId,
]);
}

/**
* iOS 17 introduces a new protocol called RemoteXPC.
* This is not yet implemented, so we fallback to devicectl.
*
* @see https://github.com/doronz88/pymobiledevice3/blob/master/misc/RemoteXPC.md#process-remoted
*/
async function launchApp(
clientManager: ClientManager,
{
bundleId,
appInfo,
detach,
}: { bundleId: string; appInfo: IPLookupResult[string]; detach?: boolean }
) {
try {
return await launchAppWithUsbmux(clientManager, { appInfo, detach });
} catch (error) {
debug(
`Failed to launch app with Usbmuxd, falling back to xcrun... ${error}`
);

// Get the device UDID and close the connection, to allow `xcrun devicectl` to connect
const deviceId = clientManager.device.Properties.SerialNumber;
clientManager.end();

// Fallback to devicectl for iOS 17 support
return await launchAppWithDeviceCtl(deviceId, bundleId);
}
}

export async function getBundleIdentifierForBinaryAsync(
binaryPath: string
): Promise<string> {
const builtInfoPlistPath = path.join(binaryPath, "Info.plist");
const { CFBundleIdentifier } =
await parseBinaryPlistAsync(builtInfoPlistPath);
return CFBundleIdentifier;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import path from "path";
import * as AppleDevice from "./AppleDevice";
import { ora } from "../../../ora";
import { ensureDirectory } from "../../../utils/dir";
import { CommandError } from "../../../utils/errors";
import { InternalError } from "common-types";

/** Get the app_delta folder for faster subsequent rebuilds on devices. */
export function getAppDeltaDirectory(bundleId: string): string {
Expand All @@ -26,10 +26,8 @@ export async function installOnDeviceAsync(props: {
bundleIdentifier: string;
appDeltaDirectory: string;
udid: string;
deviceName: string;
}): Promise<void> {
const { bundle, bundleIdentifier, appDeltaDirectory, udid, deviceName } =
props;
const { bundle, bundleIdentifier, appDeltaDirectory, udid } = props;
let indicator: Ora | undefined;

try {
Expand Down Expand Up @@ -65,8 +63,9 @@ export async function installOnDeviceAsync(props: {
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} on ${deviceName} because the device is locked.`
throw new InternalError(
"APPLE_DEVICE_LOCKED",
`Unable to launch ${appName} because the device is locked. Please launch the app manually.`
);
}
throw error;
Expand Down
12 changes: 11 additions & 1 deletion packages/eas-shared/src/run/ios/device.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import { getConnectedDevicesAsync } from "./appleDevice/AppleDevice";
import {
getConnectedDevicesAsync,
getBundleIdentifierForBinaryAsync,
} from "./appleDevice/AppleDevice";
import {
getAppDeltaDirectory,
installOnDeviceAsync,
} from "./appleDevice/installOnDeviceAsync";

const AppleDevice = {
getConnectedDevicesAsync,
getAppDeltaDirectory,
installOnDeviceAsync,
getBundleIdentifierForBinaryAsync,
};

export default AppleDevice;
5 changes: 5 additions & 0 deletions packages/eas-shared/src/run/ios/xcrun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ function throwXcrunError(e: any): never {
"sudo xcode-select -s /Applications/Xcode.app"
)} and try again.`
);
} else if (e.stderr?.match(/the device was not, or could not be, unlocked/)) {
throw new InternalError(
"APPLE_DEVICE_LOCKED",
"Device is currently locked."
);
}

if (Array.isArray(e.output)) {
Expand Down

0 comments on commit 6bab397

Please sign in to comment.