410 lines
12 KiB
Plaintext
410 lines
12 KiB
Plaintext
/**
|
|
* @flow
|
|
*/
|
|
|
|
import delayAsync from 'delay-async';
|
|
import existsAsync from 'exists-async';
|
|
import glob from 'glob-promise';
|
|
import homeDir from 'home-dir';
|
|
import mkdirp from 'mkdirp';
|
|
import osascript from '@expo/osascript';
|
|
import path from 'path';
|
|
import semver from 'semver';
|
|
import spawnAsync from '@expo/spawn-async';
|
|
import rimraf from 'rimraf';
|
|
import fs from 'fs-extra';
|
|
|
|
import * as Analytics from './Analytics';
|
|
import Api from './Api';
|
|
import ErrorCode from './ErrorCode';
|
|
import Logger from './Logger';
|
|
import NotificationCode from './NotificationCode';
|
|
import * as ProjectUtils from './project/ProjectUtils';
|
|
import UserSettings from './UserSettings';
|
|
import XDLError from './XDLError';
|
|
import * as UrlUtils from './UrlUtils';
|
|
|
|
let _lastUrl = null;
|
|
|
|
const SUGGESTED_XCODE_VERSION = `8.2.0`;
|
|
const XCODE_NOT_INSTALLED_ERROR =
|
|
'Simulator not installed. Please visit https://developer.apple.com/xcode/download/ to download Xcode and the iOS simulator. If you already have the latest version of Xcode installed, you may have to run the command `sudo xcode-select -s /Applications/Xcode.app`.';
|
|
|
|
export function isPlatformSupported() {
|
|
return process.platform === 'darwin';
|
|
}
|
|
|
|
function _isLicenseOutOfDate(text) {
|
|
if (!text) {
|
|
return false;
|
|
}
|
|
|
|
let lower = text.toLowerCase();
|
|
return lower.includes('xcode') && lower.includes('license');
|
|
}
|
|
|
|
async function _xcrunAsync(args) {
|
|
try {
|
|
return await spawnAsync('xcrun', args);
|
|
} catch (e) {
|
|
if (_isLicenseOutOfDate(e.stdout) || _isLicenseOutOfDate(e.stderr)) {
|
|
throw new XDLError(
|
|
ErrorCode.XCODE_LICENSE_NOT_ACCEPTED,
|
|
'Xcode license is not accepted. Please run `sudo xcodebuild -license`.'
|
|
);
|
|
} else {
|
|
Logger.global.error(`Error running \`xcrun ${args.join(' ')}\`: ${e.stderr}`);
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Simulator installed
|
|
export async function _isSimulatorInstalledAsync() {
|
|
let result;
|
|
try {
|
|
result = (await osascript.execAsync('id of app "Simulator"')).trim();
|
|
} catch (e) {
|
|
console.error(
|
|
"Can't determine id of Simulator app; the Simulator is most likely not installed on this machine",
|
|
e
|
|
);
|
|
Logger.global.error(XCODE_NOT_INSTALLED_ERROR);
|
|
return false;
|
|
}
|
|
if (result !== 'com.apple.iphonesimulator') {
|
|
console.warn(
|
|
"Simulator is installed but is identified as '" + result + "'; don't know what that is."
|
|
);
|
|
Logger.global.error(XCODE_NOT_INSTALLED_ERROR);
|
|
return false;
|
|
}
|
|
|
|
// check xcode version
|
|
try {
|
|
const { stdout } = await spawnAsync('xcodebuild', ['-version']);
|
|
|
|
// find something that looks like a dot separated version number
|
|
let matches = stdout.match(/[\d]{1,2}\.[\d]{1,3}/);
|
|
if (matches.length === 0) {
|
|
// very unlikely
|
|
console.error('No version number found from `xcodebuild -version`.');
|
|
Logger.global.error(
|
|
'Unable to check Xcode version. Command ran successfully but no version number was found.'
|
|
);
|
|
return false;
|
|
}
|
|
|
|
// we're cheating to use the semver lib, but it expects a proper patch version which xcode doesn't have
|
|
const version = matches[0] + '.0';
|
|
|
|
if (!semver.valid(version)) {
|
|
console.error('Invalid version number found: ' + matches[0]);
|
|
return false;
|
|
}
|
|
|
|
if (semver.lt(version, SUGGESTED_XCODE_VERSION)) {
|
|
console.warn(
|
|
`Found Xcode ${version}, which is older than the recommended Xcode ${SUGGESTED_XCODE_VERSION}.`
|
|
);
|
|
}
|
|
} catch (e) {
|
|
// how would this happen? presumably if Simulator id is found then xcodebuild is installed
|
|
console.error(`Unable to check Xcode version: ${e}`);
|
|
Logger.global.error(XCODE_NOT_INSTALLED_ERROR);
|
|
return false;
|
|
}
|
|
|
|
// make sure we can run simctl
|
|
try {
|
|
await _xcrunAsync(['simctl', 'help']);
|
|
} catch (e) {
|
|
if (e.isXDLError) {
|
|
Logger.global.error(e.toString());
|
|
} else {
|
|
console.warn(`Unable to run simctl: ${e.toString()}`);
|
|
Logger.global.error(
|
|
'xcrun may not be configured correctly. Try running `sudo xcode-select --reset` and running this again.'
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Simulator opened
|
|
export async function _openSimulatorAsync() {
|
|
if (!await _isSimulatorRunningAsync()) {
|
|
Logger.global.info('Opening iOS simulator');
|
|
await spawnAsync('open', ['-a', 'Simulator']);
|
|
await _waitForSimulatorRunningAsync();
|
|
}
|
|
}
|
|
|
|
export async function _isSimulatorRunningAsync() {
|
|
let zeroMeansNo = (await osascript.execAsync(
|
|
'tell app "System Events" to count processes whose name is "Simulator"'
|
|
)).trim();
|
|
if (zeroMeansNo === '0') {
|
|
return false;
|
|
}
|
|
|
|
let bootedDevice = await _bootedSimulatorDeviceAsync();
|
|
return !!bootedDevice;
|
|
}
|
|
|
|
async function _waitForSimulatorRunningAsync() {
|
|
if (await _isSimulatorRunningAsync()) {
|
|
return true;
|
|
} else {
|
|
await delayAsync(100);
|
|
return await _waitForSimulatorRunningAsync();
|
|
}
|
|
}
|
|
|
|
async function _listSimulatorDevicesAsync() {
|
|
let infoJson = await _xcrunAsync(['simctl', 'list', 'devices', '--json']);
|
|
let info = JSON.parse(infoJson.stdout);
|
|
return info;
|
|
}
|
|
|
|
async function _bootedSimulatorDeviceAsync() {
|
|
let simulatorDeviceInfo = await _listSimulatorDevicesAsync();
|
|
for (let runtime in simulatorDeviceInfo.devices) {
|
|
let devices = simulatorDeviceInfo.devices[runtime];
|
|
for (let i = 0; i < devices.length; i++) {
|
|
let device = devices[i];
|
|
if (device.state === 'Booted') {
|
|
return device;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function _dirForSimulatorDevice(udid: string) {
|
|
return path.resolve(homeDir(), 'Library/Developer/CoreSimulator/Devices', udid);
|
|
}
|
|
|
|
export async function _quitSimulatorAsync() {
|
|
return await osascript.execAsync('tell application "Simulator" to quit');
|
|
}
|
|
|
|
// Expo installed
|
|
export async function _isExpoAppInstalledOnCurrentBootedSimulatorAsync() {
|
|
let device = await _bootedSimulatorDeviceAsync();
|
|
if (!device) {
|
|
return false;
|
|
}
|
|
let simDir = await _dirForSimulatorDevice(device.udid);
|
|
let matches = await glob(
|
|
'./data/Containers/Data/Application/*/Library/Caches/Snapshots/host.exp.Exponent',
|
|
{ cwd: simDir }
|
|
);
|
|
|
|
return matches.length > 0;
|
|
}
|
|
|
|
export async function _waitForExpoAppInstalledOnCurrentBootedSimulatorAsync() {
|
|
if (await _isExpoAppInstalledOnCurrentBootedSimulatorAsync()) {
|
|
return true;
|
|
} else {
|
|
await delayAsync(100);
|
|
return await _waitForExpoAppInstalledOnCurrentBootedSimulatorAsync();
|
|
}
|
|
}
|
|
|
|
export async function _expoVersionOnCurrentBootedSimulatorAsync() {
|
|
let device = await _bootedSimulatorDeviceAsync();
|
|
if (!device) {
|
|
return null;
|
|
}
|
|
let simDir = await _dirForSimulatorDevice(device.udid);
|
|
let matches = await glob('./data/Containers/Bundle/Application/*/Exponent-*.app', {
|
|
cwd: simDir,
|
|
});
|
|
|
|
if (matches.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
let regex = /Exponent\-([0-9\.]+)\.app/;
|
|
let regexMatch = regex.exec(matches[0]);
|
|
if (regexMatch.length < 2) {
|
|
return null;
|
|
}
|
|
|
|
return regexMatch[1];
|
|
}
|
|
|
|
export async function _checkExpoUpToDateAsync() {
|
|
let versions = await Api.versionsAsync();
|
|
let installedVersion = await _expoVersionOnCurrentBootedSimulatorAsync();
|
|
|
|
if (!installedVersion || semver.lt(installedVersion, versions.iosVersion)) {
|
|
Logger.notifications.warn(
|
|
{ code: NotificationCode.OLD_IOS_APP_VERSION },
|
|
'This version of the Expo app is out of date. Uninstall the app and run again to upgrade.'
|
|
);
|
|
}
|
|
}
|
|
|
|
export async function _downloadSimulatorAppAsync(url) {
|
|
// If specific URL given just always download it and don't use cache
|
|
if (url) {
|
|
let dir = path.join(_simulatorCacheDirectory(), `Exponent-tmp.app`);
|
|
await Api.downloadAsync(url, dir, { extract: true });
|
|
return dir;
|
|
}
|
|
|
|
let versions = await Api.versionsAsync();
|
|
let dir = path.join(_simulatorCacheDirectory(), `Exponent-${versions.iosVersion}.app`);
|
|
|
|
if (await existsAsync(dir)) {
|
|
let filesInDir = await fs.readdir(dir);
|
|
if (filesInDir.length > 0) {
|
|
return dir;
|
|
} else {
|
|
rimraf.sync(dir);
|
|
}
|
|
}
|
|
|
|
mkdirp.sync(dir);
|
|
try {
|
|
await Api.downloadAsync(versions.iosUrl, dir, { extract: true });
|
|
} catch (e) {
|
|
rimraf.sync(dir);
|
|
throw e;
|
|
}
|
|
|
|
return dir;
|
|
}
|
|
|
|
// url: Optional URL of Exponent.app tarball to download
|
|
export async function _installExpoOnSimulatorAsync(url) {
|
|
Logger.global.info(`Downloading latest version of Expo`);
|
|
Logger.notifications.info({ code: NotificationCode.START_LOADING });
|
|
let dir = await _downloadSimulatorAppAsync(url);
|
|
Logger.notifications.info({ code: NotificationCode.STOP_LOADING });
|
|
Logger.global.info('Installing Expo on iOS simulator');
|
|
Logger.notifications.info({ code: NotificationCode.START_LOADING });
|
|
let result = await _xcrunAsync(['simctl', 'install', 'booted', dir]);
|
|
Logger.notifications.info({ code: NotificationCode.STOP_LOADING });
|
|
return result;
|
|
}
|
|
|
|
export async function _uninstallExpoAppFromSimulatorAsync() {
|
|
try {
|
|
Logger.global.info('Uninstalling Expo from iOS simulator.');
|
|
await _xcrunAsync(['simctl', 'uninstall', 'booted', 'host.exp.Exponent']);
|
|
} catch (e) {
|
|
if (e.message && e.message.includes('No devices are booted.')) {
|
|
return null;
|
|
} else {
|
|
console.error(e);
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function _simulatorCacheDirectory() {
|
|
let dotExpoHomeDirectory = UserSettings.dotExpoHomeDirectory();
|
|
let dir = path.join(dotExpoHomeDirectory, 'ios-simulator-app-cache');
|
|
mkdirp.sync(dir);
|
|
return dir;
|
|
}
|
|
|
|
export async function upgradeExpoAsync(): Promise<boolean> {
|
|
if (!await _isSimulatorInstalledAsync()) {
|
|
return false;
|
|
}
|
|
|
|
await _openSimulatorAsync();
|
|
await _uninstallExpoAppFromSimulatorAsync();
|
|
let installResult = await _installExpoOnSimulatorAsync();
|
|
if (installResult.status !== 0) {
|
|
return false;
|
|
}
|
|
|
|
if (_lastUrl) {
|
|
Logger.global.info(`Opening ${_lastUrl} in Expo.`);
|
|
await _xcrunAsync(['simctl', 'openurl', 'booted', _lastUrl]);
|
|
_lastUrl = null;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Open Url
|
|
export async function _openUrlInSimulatorAsync(url: string) {
|
|
return await _xcrunAsync(['simctl', 'openurl', 'booted', url]);
|
|
}
|
|
|
|
export async function openUrlInSimulatorSafeAsync(url: string, isDetached: boolean = false) {
|
|
if (!await _isSimulatorInstalledAsync()) {
|
|
return {
|
|
success: false,
|
|
msg: 'Unable to verify Xcode and Simulator installation.',
|
|
};
|
|
}
|
|
|
|
try {
|
|
await _openSimulatorAsync();
|
|
|
|
if (!isDetached && !await _isExpoAppInstalledOnCurrentBootedSimulatorAsync()) {
|
|
await _installExpoOnSimulatorAsync();
|
|
await _waitForExpoAppInstalledOnCurrentBootedSimulatorAsync();
|
|
}
|
|
|
|
if (!isDetached) {
|
|
_lastUrl = url;
|
|
_checkExpoUpToDateAsync(); // let this run in background
|
|
}
|
|
|
|
Logger.global.info(`Opening ${url} in iOS simulator`);
|
|
await _openUrlInSimulatorAsync(url);
|
|
} catch (e) {
|
|
if (e.isXDLError) {
|
|
// Hit some internal error, don't try again.
|
|
// This includes Xcode license errors
|
|
Logger.global.error(e.message);
|
|
return {
|
|
success: false,
|
|
msg: `${e.toString()}`,
|
|
};
|
|
}
|
|
|
|
if (isDetached) {
|
|
Logger.global.error(
|
|
`Error running app. Have you installed the app already using Xcode? Since you are detached you must build manually. ${e.toString()}`
|
|
);
|
|
} else {
|
|
Logger.global.error(`Error installing or running app. ${e.toString()}`);
|
|
}
|
|
|
|
return {
|
|
success: false,
|
|
msg: `${e.toString()}`,
|
|
};
|
|
}
|
|
|
|
Analytics.logEvent('Open Url on Device', {
|
|
platform: 'ios',
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
};
|
|
}
|
|
|
|
export async function openProjectAsync(projectRoot: string) {
|
|
let projectUrl = await UrlUtils.constructManifestUrlAsync(projectRoot, {
|
|
hostType: 'localhost',
|
|
});
|
|
|
|
let { exp } = await ProjectUtils.readConfigJsonAsync(projectRoot);
|
|
|
|
await openUrlInSimulatorSafeAsync(projectUrl, !!exp.isDetached);
|
|
}
|