Skip to content

Commit eadcba0

Browse files
committed
feat(cordova): use native-run by default
native-run is a utility built by Ionic for running native binaries on iOS and Android devices and simulators/emulators. See the GitHub repo: https://github.com/ionic-team/native-run native-run only works for iOS and Android. If a different platform is used, Cordova is used to run the app. BREAKING CHANGE: `ionic cordova run/emulate` no longer use Cordova to deploy apps by default. `cordova run`, which builds and deploys the app, is no longer used. Instead, `cordova build` is used to build the app and then it is deployed to devices using `native-run`. The old behavior is available by specifying `--no-native-run`. When using Cordova to run, unless you manually forward ports, you will need to also specify `--address=0.0.0.0` (or any host accessible externally).
1 parent 5ad11ef commit eadcba0

5 files changed

Lines changed: 214 additions & 160 deletions

File tree

packages/ionic/src/commands/cordova/base.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { input, strong, weak } from '../../lib/color';
99
import { Command } from '../../lib/command';
1010
import { FatalException } from '../../lib/errors';
1111
import { runCommand } from '../../lib/executor';
12+
import { pkgManagerArgs } from '../../lib/utils/npm';
1213

1314
export const CORDOVA_COMPILE_OPTIONS: CommandMetadataOption[] = [
1415
{
@@ -55,7 +56,7 @@ export const CORDOVA_RUN_OPTIONS: ReadonlyArray<CommandMetadataOption> = [
5556
summary: `Deploy build to a device (use ${input('--list')} to see all)`,
5657
type: String,
5758
groups: [MetadataGroup.ADVANCED, 'cordova', 'cordova-cli', 'native-run'],
58-
hint: weak('[cordova]'),
59+
hint: weak('[cordova/native-run]'),
5960
},
6061
];
6162

@@ -134,17 +135,15 @@ export abstract class CordovaCommand extends Command {
134135
throw new FatalException('Cannot use Cordova outside a project directory.');
135136
}
136137

137-
const { pkgManagerArgs } = await import('../../lib/utils/npm');
138-
139138
try {
140139
await this.env.shell.run('cordova', argList, { fatalOnNotFound, truncateErrorOutput, cwd: this.integration.root, ...options });
141140
} catch (e) {
142141
if (e instanceof SubprocessError) {
143142
if (e.code === ERROR_COMMAND_NOT_FOUND) {
144-
const cdvInstallArgs = await pkgManagerArgs(this.env.config.get('npmClient'), { command: 'install', pkg: 'cordova', global: true });
143+
const installArgs = await pkgManagerArgs(this.env.config.get('npmClient'), { command: 'install', pkg: 'cordova', global: true });
145144
throw new FatalException(
146145
`The Cordova CLI was not found on your PATH. Please install Cordova globally:\n` +
147-
`${input(cdvInstallArgs.join(' '))}\n`
146+
`${input(installArgs.join(' '))}\n`
148147
);
149148
}
150149

Lines changed: 85 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,43 @@
1-
import { Footnote, LOGGER_LEVELS, MetadataGroup, createPrefixedFormatter } from '@ionic/cli-framework';
2-
import { onBeforeExit, processExit, sleepForever } from '@ionic/utils-process';
3-
import { ERROR_COMMAND_NOT_FOUND, SubprocessError } from '@ionic/utils-subprocess';
4-
import * as path from 'path';
1+
import { Footnote, MetadataGroup } from '@ionic/cli-framework';
2+
import { onBeforeExit, sleepForever } from '@ionic/utils-process';
53
import * as url from 'url';
64

75
import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandMetadataOption, CommandPreRun, IShellRunOptions } from '../../definitions';
86
import { COMMON_BUILD_COMMAND_OPTIONS, build } from '../../lib/build';
97
import { input, strong, weak } from '../../lib/color';
108
import { FatalException } from '../../lib/errors';
119
import { loadConfigXml } from '../../lib/integrations/cordova/config';
10+
import { getPackagePath } from '../../lib/integrations/cordova/project';
1211
import { filterArgumentsForCordova, generateOptionsForCordovaBuild } from '../../lib/integrations/cordova/utils';
12+
import { SUPPORTED_PLATFORMS, createNativeRunArgs, createNativeRunListArgs, runNativeRun } from '../../lib/native-run';
1313
import { COMMON_SERVE_COMMAND_OPTIONS, LOCAL_ADDRESSES, serve } from '../../lib/serve';
14-
import { createDefaultLoggerHandlers } from '../../lib/utils/logger';
15-
import { pkgManagerArgs } from '../../lib/utils/npm';
14+
import { createPrefixedWriteStream } from '../../lib/utils/logger';
1615

1716
import { CORDOVA_BUILD_EXAMPLE_COMMANDS, CORDOVA_RUN_OPTIONS, CordovaCommand } from './base';
1817

19-
const CORDOVA_ANDROID_PACKAGE_PATH = 'platforms/android/app/build/outputs/apk/';
20-
const CORDOVA_IOS_SIMULATOR_PACKAGE_PATH = 'platforms/ios/build/emulator';
21-
const CORDOVA_IOS_DEVICE_PACKAGE_PATH = 'platforms/ios/build/device';
22-
2318
const NATIVE_RUN_OPTIONS: ReadonlyArray<CommandMetadataOption> = [
2419
{
2520
name: 'native-run',
26-
summary: `Use ${input('native-run')} instead of Cordova for running the app`,
21+
summary: `Do not use ${input('native-run')} to run the app; use Cordova instead`,
2722
type: Boolean,
28-
groups: [MetadataGroup.EXPERIMENTAL, 'native-run'],
23+
default: true,
24+
groups: ['native-run'],
2925
hint: weak('[native-run]'),
3026
},
3127
{
3228
name: 'connect',
3329
summary: 'Do not tie the running app to the process',
3430
type: Boolean,
3531
default: true,
36-
groups: [MetadataGroup.EXPERIMENTAL, 'native-run'],
37-
hint: weak('[native-run]'),
32+
groups: ['native-run'],
33+
hint: weak('[native-run] (--livereload)'),
3834
},
3935
{
4036
name: 'json',
41-
summary: `Output ${input('--list')} targets in JSON`,
37+
summary: `Output targets in JSON`,
4238
type: Boolean,
43-
groups: [MetadataGroup.EXPERIMENTAL, 'native-run'],
44-
hint: weak('[native-run]'),
39+
groups: [MetadataGroup.ADVANCED, 'native-run'],
40+
hint: weak('[native-run] (--list)'),
4541
},
4642
];
4743

@@ -128,13 +124,15 @@ export class RunCommand extends CordovaCommand implements CommandPreRun {
128124
type: 'project',
129125
summary: 'Run an Ionic project on a connected device',
130126
description: `
131-
Like running ${input('cordova run')} or ${input('cordova emulate')} directly, but performs ${input('ionic build')} before deploying to the device or emulator. Optionally specify the ${input('--livereload')} option to use the dev server from ${input('ionic serve')} for livereload functionality.
127+
Build your app and deploy it to devices and emulators using this command. Optionally specify the ${input('--livereload')} option to use the dev server from ${input('ionic serve')} for livereload functionality.
132128
133-
For Android and iOS, you can setup Remote Debugging on your device with browser development tools using these docs[^remote-debugging-docs].
129+
This command will first use ${input('ionic build')} to build web assets (or ${input('ionic serve')} with the ${input('--livereload')} option). Then, ${input('cordova build')} is used to compile and prepare your app. Finally, the ${input('native-run')} utility[^native-run-repo] is used to run your app on a device. To use Cordova for this process instead, use the ${input('--no-native-run')} option.
130+
131+
If you have multiple devices and emulators, you can target a specific one with the ${input('--target')} option. You can list targets with ${input('--list')}.
134132
135-
Just like with ${input('ionic cordova build')}, you can pass additional options to the Cordova CLI using the ${input('--')} separator. To pass additional options to the dev server, consider using ${input('ionic serve')} and the ${input('--livereload-url')} option.
133+
For Android and iOS, you can setup Remote Debugging on your device with browser development tools using these docs[^remote-debugging-docs].
136134
137-
With the experimental ${input('--native-run')} flag, this command will first use Cordova to build your app, and then it will run it on a device using the ${input('native-run')} utility[^native-run-repo] instead of Cordova.
135+
Just like with ${input('ionic cordova build')}, you can pass additional options to the Cordova CLI using the ${input('--')} separator. To pass additional options to the dev server, consider using ${input('ionic serve')} separately and using the ${input('--livereload-url')} option.
138136
`,
139137
footnotes,
140138
exampleCommands,
@@ -181,7 +179,7 @@ With the experimental ${input('--native-run')} flag, this command will first use
181179

182180
if (options['native-run']) {
183181
const args = createNativeRunListArgs(inputs, options);
184-
await this.nativeRun(args);
182+
await this.runNativeRun(args);
185183
} else {
186184
const args = filterArgumentsForCordova(metadata, options);
187185
await this.runCordova(['run', ...args.slice(1)], {});
@@ -191,180 +189,113 @@ With the experimental ${input('--native-run')} flag, this command will first use
191189
}
192190

193191
if (!inputs[0]) {
194-
const platform = await this.env.prompt({
192+
const p = await this.env.prompt({
195193
type: 'input',
196194
name: 'platform',
197195
message: `What platform would you like to run (${['android', 'ios'].map(v => input(v)).join(', ')}):`,
198196
});
199197

200-
inputs[0] = platform.trim();
198+
inputs[0] = p.trim();
201199
}
202200

203-
await this.checkForPlatformInstallation(inputs[0]);
201+
const [ platform ] = inputs;
202+
203+
if (options['native-run'] && !SUPPORTED_PLATFORMS.includes(platform)) {
204+
this.env.log.warn(`${input(platform)} is not supported by ${input('native-run')}. Using Cordova to run the app.`);
205+
options['native-run'] = false;
206+
}
207+
208+
await this.checkForPlatformInstallation(platform);
204209
}
205210

206211
async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise<void> {
212+
if (options['livereload']) {
213+
await this.runServeDeploy(inputs, options);
214+
} else {
215+
await this.runBuildDeploy(inputs, options);
216+
}
217+
}
218+
219+
async runServeDeploy(inputs: CommandLineInputs, options: CommandLineOptions) {
207220
if (!this.project) {
208221
throw new FatalException(`Cannot run ${input('ionic cordova run/emulate')} outside a project directory.`);
209222
}
210223

224+
const conf = await loadConfigXml(this.integration);
211225
const metadata = await this.getMetadata();
212226

213-
if (options['livereload']) {
214-
let livereloadUrl = options['livereload-url'] ? String(options['livereload-url']) : undefined;
215-
216-
if (!livereloadUrl) {
217-
// TODO: use runner directly
218-
const details = await serve({ flags: this.env.flags, config: this.env.config, log: this.env.log, prompt: this.env.prompt, shell: this.env.shell, project: this.project }, inputs, generateOptionsForCordovaBuild(metadata, inputs, options));
227+
let livereloadUrl = options['livereload-url'] ? String(options['livereload-url']) : undefined;
219228

220-
if (details.externallyAccessible === false && !options['native-run']) {
221-
const extra = LOCAL_ADDRESSES.includes(details.externalAddress) ? '\nEnsure you have proper port forwarding setup from your device to your computer.' : '';
222-
this.env.log.warn(`Your device or emulator may not be able to access ${strong(details.externalAddress)}.${extra}\n\n`);
223-
}
229+
if (!livereloadUrl) {
230+
// TODO: use runner directly
231+
const details = await serve({ flags: this.env.flags, config: this.env.config, log: this.env.log, prompt: this.env.prompt, shell: this.env.shell, project: this.project }, inputs, generateOptionsForCordovaBuild(metadata, inputs, options));
224232

225-
livereloadUrl = `${details.protocol || 'http'}://${options['native-run'] ? details.localAddress : details.externalAddress}:${details.port}`;
233+
if (details.externallyAccessible === false && !options['native-run']) {
234+
const extra = LOCAL_ADDRESSES.includes(details.externalAddress) ? '\nEnsure you have proper port forwarding setup from your device to your computer.' : '';
235+
this.env.log.warn(`Your device or emulator may not be able to access ${strong(details.externalAddress)}.${extra}\n\n`);
226236
}
227237

228-
const conf = await loadConfigXml(this.integration);
229-
230-
onBeforeExit(async () => {
231-
conf.resetContentSrc();
232-
await conf.save();
233-
});
238+
livereloadUrl = `${details.protocol || 'http'}://${options['native-run'] ? details.localAddress : details.externalAddress}:${details.port}`;
239+
}
234240

235-
conf.writeContentSrc(livereloadUrl);
241+
onBeforeExit(async () => {
242+
conf.resetContentSrc();
236243
await conf.save();
244+
});
237245

238-
const cordovalog = this.env.log.clone();
239-
cordovalog.handlers = createDefaultLoggerHandlers(createPrefixedFormatter(`${weak(`[cordova]`)} `));
240-
const cordovalogws = cordovalog.createWriteStream(LOGGER_LEVELS.INFO);
246+
conf.writeContentSrc(livereloadUrl);
247+
await conf.save();
241248

242-
if (options['native-run']) {
243-
// hack to do just Cordova build instead
244-
metadata.name = 'build';
249+
const cordovalogws = createPrefixedWriteStream(this.env.log, weak(`[cordova]`));
245250

246-
const buildOpts: IShellRunOptions = { stream: cordovalogws };
247-
// ignore very verbose compiler output unless --verbose (still pipe stderr)
248-
if (!options['verbose']) {
249-
buildOpts.stdio = ['ignore', 'ignore', 'pipe'];
250-
}
251-
await this.runCordova(filterArgumentsForCordova(metadata, options), buildOpts);
251+
if (options['native-run']) {
252+
const [ platform ] = inputs;
253+
const packagePath = await getPackagePath(conf.getProjectInfo().name, platform, options['emulator'] as boolean);
254+
const { port: portForward } = url.parse(livereloadUrl);
252255

253-
const platform = inputs[0];
254-
const packagePath = getPackagePath(conf.getProjectInfo().name, platform, options['emulator'] as boolean);
255-
const nativeRunArgs = createNativeRunArgs(packagePath, platform, livereloadUrl, options);
256-
await this.nativeRun(nativeRunArgs);
257-
} else {
258-
await this.runCordova(filterArgumentsForCordova(metadata, options), { stream: cordovalogws });
259-
await sleepForever();
260-
}
261-
} else {
262-
if (options.build) {
263-
// TODO: use runner directly
264-
await build({ config: this.env.config, log: this.env.log, shell: this.env.shell, prompt: this.env.prompt, project: this.project }, inputs, generateOptionsForCordovaBuild(metadata, inputs, options));
256+
const buildOpts: IShellRunOptions = { stream: cordovalogws };
257+
// ignore very verbose compiler output unless --verbose (still pipe stderr)
258+
if (!options['verbose']) {
259+
buildOpts.stdio = ['ignore', 'ignore', 'pipe'];
265260
}
266261

267-
await this.runCordova(filterArgumentsForCordova(metadata, options));
262+
await this.runCordova(filterArgumentsForCordova({ ...metadata, name: 'build' }, options), buildOpts);
263+
await this.runNativeRun(createNativeRunArgs({ packagePath, platform, portForward }, options));
264+
} else {
265+
await this.runCordova(filterArgumentsForCordova(metadata, options), { stream: cordovalogws });
266+
await sleepForever();
268267
}
269268
}
270269

271-
protected async nativeRun(args: ReadonlyArray<string>): Promise<void> {
270+
async runBuildDeploy(inputs: CommandLineInputs, options: CommandLineOptions) {
272271
if (!this.project) {
273272
throw new FatalException(`Cannot run ${input('ionic cordova run/emulate')} outside a project directory.`);
274273
}
275274

276-
let ws: NodeJS.WritableStream | undefined;
275+
const conf = await loadConfigXml(this.integration);
276+
const metadata = await this.getMetadata();
277277

278-
if (!args.includes('--list')) {
279-
const log = this.env.log.clone();
280-
log.handlers = createDefaultLoggerHandlers(createPrefixedFormatter(weak(`[native-run]`)));
281-
ws = log.createWriteStream(LOGGER_LEVELS.INFO);
278+
if (options.build) {
279+
// TODO: use runner directly
280+
await build({ config: this.env.config, log: this.env.log, shell: this.env.shell, prompt: this.env.prompt, project: this.project }, inputs, generateOptionsForCordovaBuild(metadata, inputs, options));
282281
}
283282

284-
try {
285-
await this.env.shell.run('native-run', args, { showCommand: !args.includes('--json'), fatalOnNotFound: false, cwd: this.project.directory, stream: ws });
286-
} catch (e) {
287-
if (e instanceof SubprocessError && e.code === ERROR_COMMAND_NOT_FOUND) {
288-
const cdvInstallArgs = await pkgManagerArgs(this.env.config.get('npmClient'), { command: 'install', pkg: 'native-run', global: true });
289-
throw new FatalException(
290-
`${input('native-run')} was not found on your PATH. Please install it globally:\n` +
291-
`${input(cdvInstallArgs.join(' '))}\n`
292-
);
293-
}
283+
if (options['native-run']) {
284+
const [ platform ] = inputs;
285+
const packagePath = await getPackagePath(conf.getProjectInfo().name, platform, options['emulator'] as boolean);
294286

295-
throw e;
296-
}
297-
298-
// If we connect the `native-run` process to the running app, then we
299-
// should also connect the Ionic CLI with the running `native-run` process.
300-
// This will exit the Ionic CLI when `native-run` exits.
301-
if (args.includes('--connect')) {
302-
processExit(0); // tslint:disable-line:no-floating-promises
287+
await this.runCordova(filterArgumentsForCordova({ ...metadata, name: 'build' }, options));
288+
await this.runNativeRun(createNativeRunArgs({ packagePath, platform }, { ...options, connect: false }));
289+
} else {
290+
await this.runCordova(filterArgumentsForCordova(metadata, options));
303291
}
304292
}
305-
}
306-
307-
function createNativeRunArgs(packagePath: string, platform: string, livereloadUrl: string, options: CommandLineOptions): string[] {
308-
const opts = [platform, '--app', packagePath];
309-
const target = options['target'] ? String(options['target']) : undefined;
310-
311-
if (target) {
312-
opts.push('--target', target);
313-
} else if (options['emulator']) {
314-
opts.push('--virtual');
315-
}
316293

317-
if (options['connect']) {
318-
opts.push('--connect');
319-
}
320-
321-
if (!options['livereload-url']) {
322-
const { port } = url.parse(livereloadUrl);
323-
opts.push('--forward', `${port}:${port}`);
324-
}
325-
326-
if (options['json']) {
327-
opts.push('--json');
328-
}
329-
330-
if (options['verbose']) {
331-
opts.push('--verbose');
332-
}
333-
334-
return opts;
335-
}
336-
337-
function createNativeRunListArgs(inputs: string[], options: CommandLineOptions): string[] {
338-
const args = [];
339-
if (inputs[0]) {
340-
args.push(inputs[0]);
341-
}
342-
args.push('--list');
343-
if (options['json']) {
344-
args.push('--json');
345-
}
346-
if (options['device']) {
347-
args.push('--device');
348-
}
349-
if (options['emulator']) {
350-
args.push('--virtual');
351-
}
352-
if (options['json']) {
353-
args.push('--json');
354-
}
355-
356-
return args;
357-
}
294+
protected async runNativeRun(args: ReadonlyArray<string>): Promise<void> {
295+
if (!this.project) {
296+
throw new FatalException(`Cannot run ${input('ionic cordova run/emulate')} outside a project directory.`);
297+
}
358298

359-
function getPackagePath(appName: string, platform: string, emulator: boolean) {
360-
if (platform === 'android') {
361-
// TODO: don't hardcode this/support multiple build paths (ex: multiple arch builds)
362-
// use app/build/outputs/apk/debug/output.json?
363-
return path.join(CORDOVA_ANDROID_PACKAGE_PATH, 'debug', 'app-debug.apk');
364-
}
365-
if (platform === 'ios' && emulator) {
366-
return path.join(CORDOVA_IOS_SIMULATOR_PACKAGE_PATH, `${appName}.app`);
367-
} else {
368-
return path.join(CORDOVA_IOS_DEVICE_PACKAGE_PATH, `${appName}.ipa`);
299+
await runNativeRun(this.env, args, { cwd: this.project.directory });
369300
}
370301
}

0 commit comments

Comments
 (0)