Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[menu-bar][cli] Add support for iOS internal distribution apps #79

Merged
merged 2 commits into from
Oct 14, 2023
Merged
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 @@ -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) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on expo/expo#24635

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