Skip to content

Commit 9ef53ad

Browse files
tlancinaimhoffd
authored andcommitted
feat(cordova): add --native-run option to Cordova run (#3757)
This hidden option offers an alternative to the "deploying to a device" part of `ionic cordova run`. If specified, it will no longer use `cordova run`, but preform a `cordova build` and then use `native-run` to deploy the apk or ipa to a device or virtual device.
1 parent facc96b commit 9ef53ad

File tree

2 files changed

+162
-18
lines changed

2 files changed

+162
-18
lines changed

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

Lines changed: 158 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
1-
import { LOGGER_LEVELS, OptionGroup, createPrefixedFormatter } from '@ionic/cli-framework';
2-
import { onBeforeExit, sleepForever } from '@ionic/cli-framework/utils/process';
1+
import { ERROR_SHELL_COMMAND_NOT_FOUND, LOGGER_LEVELS, OptionGroup, ShellCommandError, createPrefixedFormatter } from '@ionic/cli-framework';
2+
import { onBeforeExit, processExit, sleepForever } from '@ionic/cli-framework/utils/process';
33
import chalk from 'chalk';
4+
import * as path from 'path';
45

5-
import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandMetadataOption, CommandPreRun } from '../../definitions';
6+
import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandMetadataOption, CommandPreRun, IShellRunOptions } from '../../definitions';
67
import { COMMON_BUILD_COMMAND_OPTIONS, build } from '../../lib/build';
78
import { FatalException } from '../../lib/errors';
89
import { loadConfigXml } from '../../lib/integrations/cordova/config';
910
import { filterArgumentsForCordova, generateOptionsForCordovaBuild } from '../../lib/integrations/cordova/utils';
1011
import { COMMON_SERVE_COMMAND_OPTIONS, LOCAL_ADDRESSES, serve } from '../../lib/serve';
1112
import { createDefaultLoggerHandlers } from '../../lib/utils/logger';
13+
import { pkgManagerArgs } from '../../lib/utils/npm';
1214

1315
import { CORDOVA_BUILD_EXAMPLE_COMMANDS, CordovaCommand } from './base';
1416

17+
const CORDOVA_ANDROID_PACKAGE_PATH = 'platforms/android/app/build/outputs/apk/';
18+
const CORDOVA_IOS_SIMULATOR_PACKAGE_PATH = 'platforms/ios/build/emulator';
19+
const CORDOVA_IOS_DEVICE_PACKAGE_PATH = 'platforms/ios/build/device';
20+
1521
const CORDOVA_RUN_OPTIONS: ReadonlyArray<CommandMetadataOption> = [
1622
{
1723
name: 'debug',
@@ -31,21 +37,21 @@ const CORDOVA_RUN_OPTIONS: ReadonlyArray<CommandMetadataOption> = [
3137
name: 'device',
3238
summary: 'Deploy build to a device',
3339
type: Boolean,
34-
groups: ['cordova'],
40+
groups: ['cordova', 'native-run'],
3541
hint: chalk.dim('[cordova]'),
3642
},
3743
{
3844
name: 'emulator',
3945
summary: 'Deploy build to an emulator',
4046
type: Boolean,
41-
groups: ['cordova'],
47+
groups: ['cordova', 'native-run'],
4248
hint: chalk.dim('[cordova]'),
4349
},
4450
{
4551
name: 'target',
4652
summary: `Deploy build to a device (use ${chalk.green('--list')} to see all)`,
4753
type: String,
48-
groups: [OptionGroup.Advanced, 'cordova'],
54+
groups: [OptionGroup.Advanced, 'cordova', 'native-run'],
4955
hint: chalk.dim('[cordova]'),
5056
},
5157
{
@@ -57,6 +63,31 @@ const CORDOVA_RUN_OPTIONS: ReadonlyArray<CommandMetadataOption> = [
5763
},
5864
];
5965

66+
const NATIVE_RUN_OPTIONS: ReadonlyArray<CommandMetadataOption> = [
67+
{
68+
name: 'native-run',
69+
summary: `Use ${chalk.green('native-run')} instead of Cordova for running the app`,
70+
type: Boolean,
71+
groups: [OptionGroup.Hidden, 'native-run'],
72+
hint: chalk.dim('[native-run]'),
73+
},
74+
{
75+
name: 'connect',
76+
summary: 'Do not tie the running app to the process',
77+
type: Boolean,
78+
default: true,
79+
groups: [OptionGroup.Hidden, 'native-run'],
80+
hint: chalk.dim('[native-run]'),
81+
},
82+
{
83+
name: 'json',
84+
summary: `Output ${chalk.green('--list')} targets in JSON`,
85+
type: Boolean,
86+
groups: [OptionGroup.Hidden, 'native-run'],
87+
hint: chalk.dim('[native-run]'),
88+
},
89+
];
90+
6091
export class RunCommand extends CordovaCommand implements CommandPreRun {
6192
async getMetadata(): Promise<CommandMetadata> {
6293
let groups: string[] = [];
@@ -69,9 +100,9 @@ export class RunCommand extends CordovaCommand implements CommandPreRun {
69100
const options: CommandMetadataOption[] = [
70101
{
71102
name: 'list',
72-
summary: 'List all available Cordova targets',
103+
summary: 'List all available targets',
73104
type: Boolean,
74-
groups: ['cordova'],
105+
groups: ['cordova', 'native-run'],
75106
},
76107
// Build Options
77108
{
@@ -114,6 +145,9 @@ export class RunCommand extends CordovaCommand implements CommandPreRun {
114145
// Cordova Options
115146
options.push(...CORDOVA_RUN_OPTIONS);
116147

148+
// `native-run` Options
149+
options.push(...NATIVE_RUN_OPTIONS);
150+
117151
return {
118152
name: 'run',
119153
type: 'project',
@@ -168,9 +202,13 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/docs/developer-re
168202
options['emulator'] = true;
169203
}
170204
}
171-
172-
const args = filterArgumentsForCordova(metadata, options);
173-
await this.runCordova(['run', ...args.slice(1)], {});
205+
if (options['native-run']) {
206+
const args = createNativeRunListArgs(inputs, options);
207+
await this.nativeRun(args);
208+
} else {
209+
const args = filterArgumentsForCordova(metadata, options);
210+
await this.runCordova(['run', ...args.slice(1)], {});
211+
}
174212
throw new FatalException('', 0);
175213
}
176214

@@ -196,7 +234,6 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/docs/developer-re
196234

197235
if (options['livereload']) {
198236
let livereloadUrl = options['livereload-url'] ? String(options['livereload-url']) : undefined;
199-
200237
if (!livereloadUrl) {
201238
// TODO: use runner directly
202239
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));
@@ -223,8 +260,25 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/docs/developer-re
223260
cordovalog.handlers = createDefaultLoggerHandlers(createPrefixedFormatter(`${chalk.dim(`[cordova]`)} `));
224261
const cordovalogws = cordovalog.createWriteStream(LOGGER_LEVELS.INFO);
225262

226-
await this.runCordova(filterArgumentsForCordova(metadata, options), { stream: cordovalogws });
227-
await sleepForever();
263+
if (options['native-run']) {
264+
// hack to do just Cordova build instead
265+
metadata.name = 'build';
266+
267+
const buildOpts: IShellRunOptions = { stream: cordovalogws };
268+
// ignore very verbose compiler output unless --verbose (still pipe stderr)
269+
if (!options['verbose']) {
270+
buildOpts.stdio = ['ignore', 'ignore', 'pipe'];
271+
}
272+
await this.runCordova(filterArgumentsForCordova(metadata, options), buildOpts);
273+
274+
const platform = inputs[0];
275+
const packagePath = getPackagePath(conf.getProjectInfo().name, platform, options['emulator'] as boolean);
276+
const nativeRunArgs = createNativeRunArgs(packagePath, platform, options);
277+
await this.nativeRun(nativeRunArgs);
278+
} else {
279+
await this.runCordova(filterArgumentsForCordova(metadata, options), { stream: cordovalogws });
280+
await sleepForever();
281+
}
228282
} else {
229283
if (options.build) {
230284
// TODO: use runner directly
@@ -234,4 +288,94 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/docs/developer-re
234288
await this.runCordova(filterArgumentsForCordova(metadata, options));
235289
}
236290
}
291+
292+
protected async nativeRun(args: ReadonlyArray<string>): Promise<void> {
293+
if (!this.project) {
294+
throw new FatalException(`Cannot run ${chalk.green('ionic cordova run/emulate')} outside a project directory.`);
295+
}
296+
297+
let ws: NodeJS.WritableStream | undefined;
298+
299+
if (!args.includes('--list')) {
300+
const log = this.env.log.clone();
301+
log.handlers = createDefaultLoggerHandlers(createPrefixedFormatter(chalk.dim(`[native-run]`)));
302+
ws = log.createWriteStream(LOGGER_LEVELS.INFO);
303+
}
304+
305+
try {
306+
await this.env.shell.run('native-run', args, { showCommand: !args.includes('--json'), fatalOnNotFound: false, cwd: this.project.directory, stream: ws });
307+
} catch (e) {
308+
if (e instanceof ShellCommandError && e.code === ERROR_SHELL_COMMAND_NOT_FOUND) {
309+
const cdvInstallArgs = await pkgManagerArgs(this.env.config.get('npmClient'), { command: 'install', pkg: 'native-run', global: true });
310+
throw new FatalException(
311+
`${chalk.green('native-run')} was not found on your PATH. Please install it globally:\n` +
312+
`${chalk.green(cdvInstallArgs.join(' '))}\n`
313+
);
314+
}
315+
316+
throw e;
317+
}
318+
319+
// If we connect the `native-run` process to the running app, then we
320+
// should also connect the Ionic CLI with the running `native-run` process.
321+
// This will exit the Ionic CLI when `native-run` exits.
322+
if (args.includes('--connect')) {
323+
processExit(0); // tslint:disable-line:no-floating-promises
324+
}
325+
}
326+
}
327+
328+
function createNativeRunArgs(packagePath: string, platform: string, options: CommandLineOptions): string[] {
329+
const opts = [platform, '--app', packagePath];
330+
const target = options['target'] as string;
331+
if (target) {
332+
opts.push('--target', target);
333+
} else if (options['emulator']) {
334+
opts.push('--virtual');
335+
}
336+
337+
if (options['connect']) {
338+
opts.push('--connect');
339+
}
340+
341+
if (options['json']) {
342+
opts.push('--json');
343+
}
344+
345+
return opts;
346+
}
347+
348+
function createNativeRunListArgs(inputs: string[], options: CommandLineOptions): string[] {
349+
const args = [];
350+
if (inputs[0]) {
351+
args.push(inputs[0]);
352+
}
353+
args.push('--list');
354+
if (options['json']) {
355+
args.push('--json');
356+
}
357+
if (options['device']) {
358+
args.push('--device');
359+
}
360+
if (options['emulator']) {
361+
args.push('--virtual');
362+
}
363+
if (options['json']) {
364+
args.push('--json');
365+
}
366+
367+
return args;
368+
}
369+
370+
function getPackagePath(appName: string, platform: string, emulator: boolean) {
371+
if (platform === 'android') {
372+
// TODO: don't hardcode this/support multiple build paths (ex: multiple arch builds)
373+
// use app/build/outputs/apk/debug/output.json?
374+
return path.join(CORDOVA_ANDROID_PACKAGE_PATH, 'debug', 'app-debug.apk');
375+
}
376+
if (platform === 'ios' && emulator) {
377+
return path.join(CORDOVA_IOS_SIMULATOR_PACKAGE_PATH, `${appName}.app`);
378+
} else {
379+
return path.join(CORDOVA_IOS_DEVICE_PACKAGE_PATH, `${appName}.ipa`);
380+
}
237381
}

packages/ionic/src/definitions.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -350,10 +350,10 @@ export interface IShellRunOptions extends IShellOutputOptions {
350350
export interface IShell {
351351
alterPath: (path: string) => string;
352352

353-
run(command: string, args: string[], options: IShellRunOptions): Promise<void>;
354-
output(command: string, args: string[], options: IShellOutputOptions): Promise<string>;
355-
spawn(command: string, args: string[], options: IShellSpawnOptions): Promise<ChildProcess>;
356-
cmdinfo(cmd: string, args?: string[], options?: ShellCommandOptions): Promise<string | undefined>;
353+
run(command: string, args: ReadonlyArray<string>, options: IShellRunOptions): Promise<void>;
354+
output(command: string, args: ReadonlyArray<string>, options: IShellOutputOptions): Promise<string>;
355+
spawn(command: string, args: ReadonlyArray<string>, options: IShellSpawnOptions): Promise<ChildProcess>;
356+
cmdinfo(cmd: string, args?: ReadonlyArray<string>, options?: ShellCommandOptions): Promise<string | undefined>;
357357
}
358358

359359
export interface ITelemetry {

0 commit comments

Comments
 (0)