454 lines
12 KiB
JavaScript
454 lines
12 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @format
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const child_process = require('child_process');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const findXcodeProject = require('./findXcodeProject');
|
|
const findReactNativeScripts = require('../util/findReactNativeScripts');
|
|
const parseIOSDevicesList = require('./parseIOSDevicesList');
|
|
const findMatchingSimulator = require('./findMatchingSimulator');
|
|
const getBuildPath = function(configuration = 'Debug', appName, isDevice) {
|
|
let device;
|
|
|
|
if (isDevice) {
|
|
device = 'iphoneos';
|
|
} else if (appName.toLowerCase().includes('tvos')) {
|
|
device = 'appletvsimulator';
|
|
} else {
|
|
device = 'iphonesimulator';
|
|
}
|
|
|
|
return `build/Build/Products/${configuration}-${device}/${appName}.app`;
|
|
};
|
|
const xcprettyAvailable = function() {
|
|
try {
|
|
child_process.execSync('xcpretty --version', {
|
|
stdio: [0, 'pipe', 'ignore'],
|
|
});
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
function runIOS(argv, config, args) {
|
|
if (!fs.existsSync(args.projectPath)) {
|
|
const reactNativeScriptsPath = findReactNativeScripts();
|
|
if (reactNativeScriptsPath) {
|
|
child_process.spawnSync(
|
|
reactNativeScriptsPath,
|
|
['ios'].concat(process.argv.slice(1)),
|
|
{stdio: 'inherit'},
|
|
);
|
|
return;
|
|
} else {
|
|
throw new Error(
|
|
'iOS project folder not found. Are you sure this is a React Native project?',
|
|
);
|
|
}
|
|
}
|
|
process.chdir(args.projectPath);
|
|
const xcodeProject = findXcodeProject(fs.readdirSync('.'));
|
|
if (!xcodeProject) {
|
|
throw new Error('Could not find Xcode project files in ios folder');
|
|
}
|
|
|
|
const inferredSchemeName = path.basename(
|
|
xcodeProject.name,
|
|
path.extname(xcodeProject.name),
|
|
);
|
|
const scheme = args.scheme || inferredSchemeName;
|
|
console.log(
|
|
`Found Xcode ${xcodeProject.isWorkspace ? 'workspace' : 'project'} ${
|
|
xcodeProject.name
|
|
}`,
|
|
);
|
|
const devices = parseIOSDevicesList(
|
|
child_process.execFileSync('xcrun', ['instruments', '-s'], {
|
|
encoding: 'utf8',
|
|
}),
|
|
);
|
|
if (args.device) {
|
|
const selectedDevice = matchingDevice(devices, args.device);
|
|
if (selectedDevice) {
|
|
return runOnDevice(
|
|
selectedDevice,
|
|
scheme,
|
|
xcodeProject,
|
|
args.configuration,
|
|
args.packager,
|
|
args.verbose,
|
|
args.port,
|
|
);
|
|
} else {
|
|
if (devices && devices.length > 0) {
|
|
console.log(
|
|
'Could not find device with the name: "' + args.device + '".',
|
|
);
|
|
console.log('Choose one of the following:');
|
|
printFoundDevices(devices);
|
|
} else {
|
|
console.log('No iOS devices connected.');
|
|
}
|
|
}
|
|
} else if (args.udid) {
|
|
return runOnDeviceByUdid(args, scheme, xcodeProject, devices);
|
|
} else {
|
|
return runOnSimulator(xcodeProject, args, scheme);
|
|
}
|
|
}
|
|
|
|
function runOnDeviceByUdid(args, scheme, xcodeProject, devices) {
|
|
const selectedDevice = matchingDeviceByUdid(devices, args.udid);
|
|
if (selectedDevice) {
|
|
return runOnDevice(
|
|
selectedDevice,
|
|
scheme,
|
|
xcodeProject,
|
|
args.configuration,
|
|
args.packager,
|
|
args.verbose,
|
|
args.port,
|
|
);
|
|
} else {
|
|
if (devices && devices.length > 0) {
|
|
console.log('Could not find device with the udid: "' + args.udid + '".');
|
|
console.log('Choose one of the following:');
|
|
printFoundDevices(devices);
|
|
} else {
|
|
console.log('No iOS devices connected.');
|
|
}
|
|
}
|
|
}
|
|
|
|
function runOnSimulator(xcodeProject, args, scheme) {
|
|
return new Promise(resolve => {
|
|
try {
|
|
var simulators = JSON.parse(
|
|
child_process.execFileSync(
|
|
'xcrun',
|
|
['simctl', 'list', '--json', 'devices'],
|
|
{encoding: 'utf8'},
|
|
),
|
|
);
|
|
} catch (e) {
|
|
throw new Error('Could not parse the simulator list output');
|
|
}
|
|
|
|
const selectedSimulator = findMatchingSimulator(simulators, args.simulator);
|
|
if (!selectedSimulator) {
|
|
throw new Error(`Could not find ${args.simulator} simulator`);
|
|
}
|
|
|
|
/**
|
|
* Booting simulator through `xcrun simctl boot` will boot it in the `headless` mode
|
|
* (running in the background).
|
|
*
|
|
* In order for user to see the app and the simulator itself, we have to make sure
|
|
* that the Simulator.app is running.
|
|
*
|
|
* We also pass it `-CurrentDeviceUDID` so that when we launch it for the first time,
|
|
* it will not boot the "default" device, but the one we set. If the app is already running,
|
|
* this flag has no effect.
|
|
*/
|
|
const activeDeveloperDir = child_process
|
|
.execFileSync('xcode-select', ['-p'], {encoding: 'utf8'})
|
|
.trim();
|
|
child_process.execFileSync('open', [
|
|
`${activeDeveloperDir}/Applications/Simulator.app`,
|
|
'--args',
|
|
'-CurrentDeviceUDID',
|
|
selectedSimulator.udid,
|
|
]);
|
|
|
|
if (!selectedSimulator.booted) {
|
|
const simulatorFullName = formattedDeviceName(selectedSimulator);
|
|
console.log(`Launching ${simulatorFullName}...`);
|
|
try {
|
|
child_process.spawnSync('xcrun', [
|
|
'instruments',
|
|
'-w',
|
|
selectedSimulator.udid,
|
|
]);
|
|
} catch (e) {
|
|
// instruments always fail with 255 because it expects more arguments,
|
|
// but we want it to only launch the simulator
|
|
}
|
|
}
|
|
|
|
buildProject(
|
|
xcodeProject,
|
|
selectedSimulator.udid,
|
|
scheme,
|
|
args.configuration,
|
|
args.packager,
|
|
args.verbose,
|
|
args.port,
|
|
).then(appName => resolve({udid: selectedSimulator.udid, appName}));
|
|
}).then(({udid, appName}) => {
|
|
if (!appName) {
|
|
appName = scheme;
|
|
}
|
|
let appPath = getBuildPath(args.configuration, appName);
|
|
console.log(`Installing ${appPath}`);
|
|
child_process.spawnSync('xcrun', ['simctl', 'install', udid, appPath], {
|
|
stdio: 'inherit',
|
|
});
|
|
|
|
const bundleID = child_process
|
|
.execFileSync(
|
|
'/usr/libexec/PlistBuddy',
|
|
['-c', 'Print:CFBundleIdentifier', path.join(appPath, 'Info.plist')],
|
|
{encoding: 'utf8'},
|
|
)
|
|
.trim();
|
|
|
|
console.log(`Launching ${bundleID}`);
|
|
child_process.spawnSync('xcrun', ['simctl', 'launch', udid, bundleID], {
|
|
stdio: 'inherit',
|
|
});
|
|
});
|
|
}
|
|
|
|
function runOnDevice(
|
|
selectedDevice,
|
|
scheme,
|
|
xcodeProject,
|
|
configuration,
|
|
launchPackager,
|
|
verbose,
|
|
port,
|
|
) {
|
|
return buildProject(
|
|
xcodeProject,
|
|
selectedDevice.udid,
|
|
scheme,
|
|
configuration,
|
|
launchPackager,
|
|
verbose,
|
|
port,
|
|
).then(appName => {
|
|
if (!appName) {
|
|
appName = scheme;
|
|
}
|
|
const iosDeployInstallArgs = [
|
|
'--bundle',
|
|
getBuildPath(configuration, appName, true),
|
|
'--id',
|
|
selectedDevice.udid,
|
|
'--justlaunch',
|
|
];
|
|
console.log(
|
|
`installing and launching your app on ${selectedDevice.name}...`,
|
|
);
|
|
const iosDeployOutput = child_process.spawnSync(
|
|
'ios-deploy',
|
|
iosDeployInstallArgs,
|
|
{encoding: 'utf8'},
|
|
);
|
|
if (iosDeployOutput.error) {
|
|
console.log('');
|
|
console.log('** INSTALLATION FAILED **');
|
|
console.log('Make sure you have ios-deploy installed globally.');
|
|
console.log('(e.g "npm install -g ios-deploy")');
|
|
} else {
|
|
console.log('** INSTALLATION SUCCEEDED **');
|
|
}
|
|
});
|
|
}
|
|
|
|
function buildProject(
|
|
xcodeProject,
|
|
udid,
|
|
scheme,
|
|
configuration = 'Debug',
|
|
launchPackager = false,
|
|
verbose,
|
|
port,
|
|
) {
|
|
return new Promise((resolve, reject) => {
|
|
var xcodebuildArgs = [
|
|
xcodeProject.isWorkspace ? '-workspace' : '-project',
|
|
xcodeProject.name,
|
|
'-configuration',
|
|
configuration,
|
|
'-scheme',
|
|
scheme,
|
|
'-destination',
|
|
`id=${udid}`,
|
|
'-derivedDataPath',
|
|
'build',
|
|
];
|
|
console.log(`Building using "xcodebuild ${xcodebuildArgs.join(' ')}"`);
|
|
let xcpretty;
|
|
if (!verbose) {
|
|
xcpretty =
|
|
xcprettyAvailable() &&
|
|
child_process.spawn('xcpretty', [], {
|
|
stdio: ['pipe', process.stdout, process.stderr],
|
|
});
|
|
}
|
|
const buildProcess = child_process.spawn(
|
|
'xcodebuild',
|
|
xcodebuildArgs,
|
|
getProcessOptions(launchPackager, port),
|
|
);
|
|
let buildOutput = '';
|
|
buildProcess.stdout.on('data', function(data) {
|
|
buildOutput += data.toString();
|
|
if (xcpretty) {
|
|
xcpretty.stdin.write(data);
|
|
} else {
|
|
console.log(data.toString());
|
|
}
|
|
});
|
|
buildProcess.stderr.on('data', function(data) {
|
|
console.error(data.toString());
|
|
});
|
|
buildProcess.on('close', function(code) {
|
|
if (xcpretty) {
|
|
xcpretty.stdin.end();
|
|
}
|
|
//FULL_PRODUCT_NAME is the actual file name of the app, which actually comes from the Product Name in the build config, which does not necessary match a scheme name, example output line: export FULL_PRODUCT_NAME="Super App Dev.app"
|
|
let productNameMatch = /export FULL_PRODUCT_NAME="?(.+).app"?$/m.exec(
|
|
buildOutput,
|
|
);
|
|
if (
|
|
productNameMatch &&
|
|
productNameMatch.length &&
|
|
productNameMatch.length > 1
|
|
) {
|
|
return resolve(productNameMatch[1]); //0 is the full match, 1 is the app name
|
|
}
|
|
return buildProcess.error ? reject(buildProcess.error) : resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
function matchingDevice(devices, deviceName) {
|
|
if (deviceName === true && devices.length === 1) {
|
|
console.log(
|
|
`Using first available device ${
|
|
devices[0].name
|
|
} due to lack of name supplied.`,
|
|
);
|
|
return devices[0];
|
|
}
|
|
for (let i = devices.length - 1; i >= 0; i--) {
|
|
if (
|
|
devices[i].name === deviceName ||
|
|
formattedDeviceName(devices[i]) === deviceName
|
|
) {
|
|
return devices[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
function matchingDeviceByUdid(devices, udid) {
|
|
for (let i = devices.length - 1; i >= 0; i--) {
|
|
if (devices[i].udid === udid) {
|
|
return devices[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
function formattedDeviceName(simulator) {
|
|
return `${simulator.name} (${simulator.version})`;
|
|
}
|
|
|
|
function printFoundDevices(devices) {
|
|
for (let i = devices.length - 1; i >= 0; i--) {
|
|
console.log(devices[i].name + ' Udid: ' + devices[i].udid);
|
|
}
|
|
}
|
|
|
|
function getProcessOptions(launchPackager, port) {
|
|
if (launchPackager) {
|
|
return {
|
|
env: {...process.env, RCT_METRO_PORT: port},
|
|
};
|
|
}
|
|
|
|
return {
|
|
env: {...process.env, RCT_NO_LAUNCH_PACKAGER: true},
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
name: 'run-ios',
|
|
description: 'builds your app and starts it on iOS simulator',
|
|
func: runIOS,
|
|
examples: [
|
|
{
|
|
desc: 'Run on a different simulator, e.g. iPhone 5',
|
|
cmd: 'react-native run-ios --simulator "iPhone 5"',
|
|
},
|
|
{
|
|
desc: 'Pass a non-standard location of iOS directory',
|
|
cmd: 'react-native run-ios --project-path "./app/ios"',
|
|
},
|
|
{
|
|
desc: "Run on a connected device, e.g. Max's iPhone",
|
|
cmd: 'react-native run-ios --device "Max\'s iPhone"',
|
|
},
|
|
{
|
|
desc: 'Run on the AppleTV simulator',
|
|
cmd:
|
|
'react-native run-ios --simulator "Apple TV" --scheme "helloworld-tvOS"',
|
|
},
|
|
],
|
|
options: [
|
|
{
|
|
command: '--simulator [string]',
|
|
description: 'Explicitly set simulator to use',
|
|
default: 'iPhone X',
|
|
},
|
|
{
|
|
command: '--configuration [string]',
|
|
description: 'Explicitly set the scheme configuration to use',
|
|
},
|
|
{
|
|
command: '--scheme [string]',
|
|
description: 'Explicitly set Xcode scheme to use',
|
|
},
|
|
{
|
|
command: '--project-path [string]',
|
|
description:
|
|
'Path relative to project root where the Xcode project ' +
|
|
"(.xcodeproj) lives. The default is 'ios'.",
|
|
default: 'ios',
|
|
},
|
|
{
|
|
command: '--device [string]',
|
|
description:
|
|
'Explicitly set device to use by name. The value is not required if you have a single device connected.',
|
|
},
|
|
{
|
|
command: '--udid [string]',
|
|
description: 'Explicitly set device to use by udid',
|
|
},
|
|
{
|
|
command: '--no-packager',
|
|
description: 'Do not launch packager while building',
|
|
},
|
|
{
|
|
command: '--verbose',
|
|
description: 'Do not use xcpretty even if installed',
|
|
},
|
|
{
|
|
command: '--port [number]',
|
|
default: process.env.RCT_METRO_PORT || 8081,
|
|
parse: (val: string) => Number(val),
|
|
},
|
|
],
|
|
};
|