UNPKG

9.76 kBJavaScriptView Raw
1/*
2 Licensed to the Apache Software Foundation (ASF) under one
3 or more contributor license agreements. See the NOTICE file
4 distributed with this work for additional information
5 regarding copyright ownership. The ASF licenses this file
6 to you under the Apache License, Version 2.0 (the
7 "License"); you may not use this file except in compliance
8 with the License. You may obtain a copy of the License at
9
10 http://www.apache.org/licenses/LICENSE-2.0
11
12 Unless required by applicable law or agreed to in writing,
13 software distributed under the License is distributed on an
14 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 KIND, either express or implied. See the License for the
16 specific language governing permissions and limitations
17 under the License.
18*/
19
20const path = require('path');
21const build = require('./build');
22const execa = require('execa');
23const { CordovaError, events } = require('cordova-common');
24const check_reqs = require('./check_reqs');
25const fs = require('fs-extra');
26
27/** @returns {Promise<void>} */
28module.exports.run = function (runOptions) {
29 const projectPath = this.root;
30
31 // Validate args
32 if (runOptions.device && runOptions.emulator) {
33 return Promise.reject(new CordovaError('Only one of "device"/"emulator" options should be specified'));
34 }
35
36 // support for CB-8168 `cordova/run --list`
37 if (runOptions.list) {
38 if (runOptions.device) return module.exports.listDevices();
39 if (runOptions.emulator) return module.exports.listEmulators();
40 // if no --device or --emulator flag is specified, list both devices and emulators
41 return module.exports.listDevices().then(() => module.exports.listEmulators());
42 }
43
44 let useDevice = !!runOptions.device;
45 const configuration = runOptions.release ? 'Release' : 'Debug';
46
47 return require('./listDevices').run()
48 .then(devices => {
49 if (devices.length > 0 && !(runOptions.emulator)) {
50 useDevice = true;
51 // we also explicitly set device flag in options as we pass
52 // those parameters to other api (build as an example)
53 runOptions.device = true;
54 return check_reqs.check_ios_deploy();
55 }
56 }).then(() => {
57 if (!runOptions.nobuild) {
58 return build.run(runOptions);
59 } else {
60 return Promise.resolve();
61 }
62 }).then(() => build.findXCodeProjectIn(projectPath))
63 .then(projectName => {
64 let appPath = path.join(projectPath, 'build', `${configuration}-iphonesimulator`, `${projectName}.app`);
65 const buildOutputDir = path.join(projectPath, 'build', `${configuration}-iphoneos`);
66
67 // select command to run and arguments depending whether
68 // we're running on device/emulator
69 if (useDevice) {
70 return module.exports.checkDeviceConnected()
71 .then(() => {
72 // Unpack IPA
73 const ipafile = path.join(buildOutputDir, `${projectName}.ipa`);
74
75 // unpack the existing platform/ios/build/device/appname.ipa (zipfile), will create a Payload folder
76 return execa('unzip', ['-o', '-qq', ipafile], { cwd: buildOutputDir, stdio: 'inherit' });
77 })
78 .then(() => {
79 // Uncompress IPA (zip file)
80 const appFileInflated = path.join(buildOutputDir, 'Payload', `${projectName}.app`);
81 const appFile = path.join(buildOutputDir, `${projectName}.app`);
82 const payloadFolder = path.join(buildOutputDir, 'Payload');
83
84 // delete the existing platform/ios/build/device/appname.app
85 fs.removeSync(appFile);
86 // move the platform/ios/build/device/Payload/appname.app to parent
87 fs.moveSync(appFileInflated, appFile);
88 // delete the platform/ios/build/device/Payload folder
89 fs.removeSync(payloadFolder);
90
91 return null;
92 })
93 .then(
94 () => {
95 appPath = path.join(projectPath, 'build', `${configuration}-iphoneos`, `${projectName}.app`);
96 let extraArgs = [];
97 if (runOptions.argv) {
98 // argv.slice(2) removes node and run.js, filterSupportedArgs removes the run.js args
99 extraArgs = module.exports.filterSupportedArgs(runOptions.argv.slice(2));
100 }
101 return module.exports.deployToDevice(appPath, runOptions.target, extraArgs);
102 },
103 // if device connection check failed use emulator then
104 () => module.exports.deployToSim(appPath, runOptions.target)
105 );
106 } else {
107 return module.exports.deployToSim(appPath, runOptions.target);
108 }
109 })
110 .then(() => {}); // resolve to undefined
111};
112
113module.exports.filterSupportedArgs = filterSupportedArgs;
114module.exports.checkDeviceConnected = checkDeviceConnected;
115module.exports.deployToDevice = deployToDevice;
116module.exports.deployToSim = deployToSim;
117module.exports.startSim = startSim;
118module.exports.listDevices = listDevices;
119module.exports.listEmulators = listEmulators;
120
121/**
122 * Filters the args array and removes supported args for the 'run' command.
123 *
124 * @return {Array} array with unsupported args for the 'run' command
125 */
126function filterSupportedArgs (args) {
127 const filtered = [];
128 const sargs = ['--device', '--emulator', '--nobuild', '--list', '--target', '--debug', '--release'];
129 const re = new RegExp(sargs.join('|'));
130
131 args.forEach(element => {
132 // supported args not found, we add
133 // we do a regex search because --target can be "--target=XXX"
134 if (element.search(re) === -1) {
135 filtered.push(element);
136 }
137 }, this);
138
139 return filtered;
140}
141
142/**
143 * Checks if any iOS device is connected
144 * @return {Promise} Fullfilled when any device is connected, rejected otherwise
145 */
146function checkDeviceConnected () {
147 return execa('ios-deploy', ['-c', '-t', '1'], { stdio: 'inherit' });
148}
149
150/**
151 * Deploy specified app package to connected device
152 * using ios-deploy command
153 * @param {String} appPath Path to application package
154 * @return {Promise} Resolves when deploy succeeds otherwise rejects
155 */
156function deployToDevice (appPath, target, extraArgs) {
157 events.emit('log', 'Deploying to device');
158 const args = ['--justlaunch', '-d', '-b', appPath];
159 if (target) {
160 args.push('-i', target);
161 } else {
162 args.push('--no-wifi');
163 }
164 return execa('ios-deploy', args.concat(extraArgs), { stdio: 'inherit' });
165}
166
167/**
168 * Deploy specified app package to ios-sim simulator
169 * @param {String} appPath Path to application package
170 * @param {String} target Target device type
171 * @return {Promise} Resolves when deploy succeeds otherwise rejects
172 */
173async function deployToSim (appPath, target) {
174 events.emit('log', 'Deploying to simulator');
175
176 if (!target) {
177 // Select target device for emulator (preferring iPhone Emulators)
178 const emulators = await require('./listEmulatorImages').run();
179 const iPhoneEmus = emulators.filter(emulator => emulator.startsWith('iPhone'));
180 target = iPhoneEmus.concat(emulators)[0];
181 events.emit('log', `No target specified for emulator. Deploying to "${target}" simulator.`);
182 }
183
184 return startSim(appPath, target);
185}
186
187function startSim (appPath, target) {
188 const projectPath = path.join(path.dirname(appPath), '../..');
189 const logPath = path.join(projectPath, 'cordova/console.log');
190 const deviceTypeId = `com.apple.CoreSimulator.SimDeviceType.${target}`;
191
192 const subprocess = execa(
193 require.resolve('ios-sim/bin/ios-sim'),
194 ['launch', appPath, '--devicetypeid', deviceTypeId, '--log', logPath, '--exit'],
195 { cwd: projectPath }
196 );
197
198 // FIXME: data emitted is not necessarily a complete line
199 subprocess.stderr.on('data', data => {
200 events.emit('error', `[ios-sim] ${data}`);
201 });
202 subprocess.stdout.on('data', data => {
203 events.emit('log', `[ios-sim] ${data}`);
204 });
205
206 subprocess.then(() => {
207 events.emit('log', 'Simulator successfully started via `ios-sim`.');
208 });
209
210 return subprocess;
211}
212
213function listDevices () {
214 return require('./listDevices').run()
215 .then(devices => {
216 events.emit('log', 'Available iOS Devices:');
217 devices.forEach(device => {
218 events.emit('log', `\t${device}`);
219 });
220 });
221}
222
223function listEmulators () {
224 return require('./listEmulatorImages').run()
225 .then(emulators => {
226 events.emit('log', 'Available iOS Simulators:');
227 emulators.forEach(emulator => {
228 events.emit('log', `\t${emulator}`);
229 });
230 });
231}
232
233module.exports.runListDevices = async function (options = {}) {
234 const { options: cliArgs = {} } = options;
235
236 if (cliArgs?.device) {
237 await module.exports.listDevices.call(this);
238 } else if (cliArgs?.emulator) {
239 await module.exports.listEmulators.call(this);
240 } else {
241 await module.exports.listDevices.call(this);
242 await module.exports.listEmulators.call(this);
243 }
244
245 return true;
246};