diff --git a/packages/@ionic/cli/src/definitions.ts b/packages/@ionic/cli/src/definitions.ts index 4b19d30827..e5596ab9c0 100644 --- a/packages/@ionic/cli/src/definitions.ts +++ b/packages/@ionic/cli/src/definitions.ts @@ -53,13 +53,22 @@ export interface CordovaPackageJson extends PackageJson { }; } -export interface CordovaAndroidBuildOutputEntry { +export interface LegacyAndroidBuildOutputEntry { outputType: { type: string; }; path: string; } +export interface AndroidBuildOutput { + artifactType: { + type: string; + }; + elements: { + outputFile: string; + }[]; +} + export interface Runner { run(options: T): Promise; } diff --git a/packages/@ionic/cli/src/guards.ts b/packages/@ionic/cli/src/guards.ts index 09a79c3259..0d7b9364e8 100644 --- a/packages/@ionic/cli/src/guards.ts +++ b/packages/@ionic/cli/src/guards.ts @@ -2,12 +2,12 @@ import { APIResponse, APIResponseError, APIResponseSuccess, + AndroidBuildOutput, App, AppAssociation, BitbucketCloudRepoAssociation, BitbucketServerRepoAssociation, CommandPreRun, - CordovaAndroidBuildOutputEntry, CordovaPackageJson, ExitCodeException, GithubBranch, @@ -17,6 +17,7 @@ import { IMultiProjectConfig, IProjectConfig, IntegrationName, + LegacyAndroidBuildOutputEntry, Login, OpenIdToken, Org, @@ -55,7 +56,7 @@ export function isCordovaPackageJson(obj: any): obj is CordovaPackageJson { typeof obj.cordova.plugins === 'object'; } -export function isCordovaAndroidBuildOutputFile(obj: any): obj is CordovaAndroidBuildOutputEntry[] { +export function isLegacyAndroidBuildOutputFile(obj: any): obj is LegacyAndroidBuildOutputEntry[] { if (!Array.isArray(obj)) { return false; } @@ -70,6 +71,13 @@ export function isCordovaAndroidBuildOutputFile(obj: any): obj is CordovaAndroid && typeof obj[0].outputType.type === 'string'; } +export function isAndroidBuildOutputFile(obj: any): obj is AndroidBuildOutput { + return obj && + typeof obj.artifactType === 'object' && + typeof obj.artifactType.type === 'string' && + Array.isArray(obj.elements); +} + export function isExitCodeException(err: any): err is ExitCodeException { return err && typeof err.exitCode === 'number' && err.exitCode >= 0 && err.exitCode <= 255; } diff --git a/packages/@ionic/cli/src/lib/integrations/cordova/__tests__/project.ts b/packages/@ionic/cli/src/lib/integrations/cordova/__tests__/project.ts index 2e4f57bdde..88c2dfd73b 100644 --- a/packages/@ionic/cli/src/lib/integrations/cordova/__tests__/project.ts +++ b/packages/@ionic/cli/src/lib/integrations/cordova/__tests__/project.ts @@ -29,6 +29,96 @@ describe('@ionic/cli', () => { }); + describe('getAndroidBuildOutputJson', () => { + + it('should throw for fs error', () => { + spyOn(fsSpy, 'readJson').and.callFake(async () => { throw new Error('error') }); + + const p = project.getAndroidBuildOutputJson('/path/to/output.json'); + expect(p).rejects.toThrowError('Could not parse build output file'); + }); + + it('should throw for unrecognized format', () => { + spyOn(fsSpy, 'readJson').and.callFake(async () => ({ foo: 'bar' })); + + const p = project.getAndroidBuildOutputJson('/path/to/output.json'); + expect(p).rejects.toThrowError('Could not parse build output file'); + }); + + it('should parse legacy output.json', () => { + const file = [ + { + outputType: { + type: 'APK', + }, + path: 'app-debug.apk', + }, + ]; + + spyOn(fsSpy, 'readJson').and.callFake(async () => file); + + const p = project.getAndroidBuildOutputJson('/path/to/output.json'); + expect(p).resolves.toEqual(file); + }); + + it('should parse output.json', () => { + const file = { + artifactType: { + type: 'APK', + }, + elements: [ + { + outputFile: 'app-debug.apk', + } + ], + }; + + spyOn(fsSpy, 'readJson').and.callFake(async () => file); + + const p = project.getAndroidBuildOutputJson('/path/to/output.json'); + expect(p).resolves.toEqual(file); + }); + + }); + + describe('getAndroidPackageFilePath', () => { + + it('should get file path from legacy output.json', () => { + const file = [ + { + outputType: { + type: 'APK', + }, + path: 'foo-debug.apk', + }, + ]; + + spyOn(fsSpy, 'readJson').and.callFake(async () => file); + + const p = project.getAndroidPackageFilePath('/path/to/proj', { release: false }); + expect(p).resolves.toEqual('platforms/android/app/build/outputs/apk/debug/foo-debug.apk'); + }); + + it('should get file path from output.json', () => { + const file = { + artifactType: { + type: 'APK', + }, + elements: [ + { + outputFile: 'bar-debug.apk', + } + ], + }; + + spyOn(fsSpy, 'readJson').and.callFake(async () => file); + + const p = project.getAndroidPackageFilePath('/path/to/proj', { release: false }); + expect(p).resolves.toEqual('platforms/android/app/build/outputs/apk/debug/bar-debug.apk'); + }); + + }); + }); }); diff --git a/packages/@ionic/cli/src/lib/integrations/cordova/project.ts b/packages/@ionic/cli/src/lib/integrations/cordova/project.ts index c49cc11ac7..795d3b9501 100644 --- a/packages/@ionic/cli/src/lib/integrations/cordova/project.ts +++ b/packages/@ionic/cli/src/lib/integrations/cordova/project.ts @@ -3,8 +3,8 @@ import { readJson, readdirSafe, statSafe } from '@ionic/utils-fs'; import * as Debug from 'debug'; import * as path from 'path'; -import { CordovaAndroidBuildOutputEntry } from '../../../definitions'; -import { isCordovaAndroidBuildOutputFile } from '../../../guards'; +import { AndroidBuildOutput, LegacyAndroidBuildOutputEntry } from '../../../definitions'; +import { isAndroidBuildOutputFile, isLegacyAndroidBuildOutputFile } from '../../../guards'; import { input } from '../../color'; import { FatalException } from '../../errors'; @@ -25,11 +25,13 @@ export async function getPlatforms(projectDir: string): Promise { return platforms; } -export async function getAndroidBuildOutputJson(p: string): Promise { +export async function getAndroidBuildOutputJson(p: string): Promise { try { const json = await readJson(p); - if (isCordovaAndroidBuildOutputFile(json)) { + if (isAndroidBuildOutputFile(json)) { + return json; + } else if (isLegacyAndroidBuildOutputFile(json)) { return json; } else { debug('Output file does not match expected format: %O', json); @@ -41,6 +43,19 @@ export async function getAndroidBuildOutputJson(p: string): Promise { + const outputPath = path.resolve(root, CORDOVA_ANDROID_PACKAGE_PATH, release ? 'release' : 'debug'); + const outputJsonPath = path.resolve(outputPath, 'output.json'); + const outputJson = await getAndroidBuildOutputJson(outputJsonPath); + + const p = 'elements' in outputJson + ? outputJson.elements[0].outputFile + : outputJson[0].path; + + // TODO: handle multiple files from output.json, prompt to select? + return path.relative(root, path.resolve(outputPath, p)); +} + export interface GetPackagePathOptions { emulator?: boolean; release?: boolean; @@ -51,12 +66,7 @@ export interface GetPackagePathOptions { */ export async function getPackagePath(root: string, appName: string, platform: string, { emulator = false, release = false }: GetPackagePathOptions = {}): Promise { if (platform === 'android') { - const outputPath = path.resolve(root, CORDOVA_ANDROID_PACKAGE_PATH, release ? 'release' : 'debug'); - const outputJsonPath = path.resolve(outputPath, 'output.json'); - const outputJson = await getAndroidBuildOutputJson(outputJsonPath); - - // TODO: handle multiple files from output.json, prompt to select? - return path.relative(root, path.resolve(outputPath, outputJson[0].path)); + return getAndroidPackageFilePath(root, { emulator, release }); } else if (platform === 'ios') { if (emulator) { return path.join(CORDOVA_IOS_SIMULATOR_PACKAGE_PATH, `${appName}.app`);