GT2/GT2-iOS/node_modules/xdl/build/Simulator.js.flow

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);
}