Skip to content

Commit b06eb07

Browse files
committed
feat: support aab in run and deploy
1 parent 3627814 commit b06eb07

27 files changed

+1173
-617
lines changed

lib/bootstrap.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ $injector.require("androidPluginBuildService", "./services/android-plugin-build-
1414
$injector.require("gradleCommandService", "./services/android/gradle-command-service");
1515
$injector.require("gradleBuildService", "./services/android/gradle-build-service");
1616
$injector.require("gradleBuildArgsService", "./services/android/gradle-build-args-service");
17+
$injector.require("androidBundleToolService", "./services/android/android-bundle-tool-service");
1718
$injector.require("iOSEntitlementsService", "./services/ios-entitlements-service");
1819
$injector.require("iOSNativeTargetService", "./services/ios-native-target-service");
1920
$injector.require("iOSExtensionsService", "./services/ios-extensions-service");

lib/commands/deploy.ts

-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export class DeployOnDeviceCommand extends ValidatePlatformCommandBase implement
1818
private $mobileHelper: Mobile.IMobileHelper,
1919
$platformsDataService: IPlatformsDataService,
2020
private $deployCommandHelper: DeployCommandHelper,
21-
private $androidBundleValidatorHelper: IAndroidBundleValidatorHelper,
2221
private $markingModeService: IMarkingModeService,
2322
private $migrateController: IMigrateController) {
2423
super($options, $platformsDataService, $platformValidationService, $projectData);
@@ -40,7 +39,6 @@ export class DeployOnDeviceCommand extends ValidatePlatformCommandBase implement
4039
await this.$migrateController.validate({ projectDir: this.$projectData.projectDir, platforms: [platform] });
4140
}
4241

43-
this.$androidBundleValidatorHelper.validateNoAab();
4442
if (!args || !args.length || args.length > 1) {
4543
return false;
4644
}

lib/commands/run.ts

-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ export class RunCommandBase implements ICommand {
88
public platform: string;
99
constructor(
1010
private $analyticsService: IAnalyticsService,
11-
private $androidBundleValidatorHelper: IAndroidBundleValidatorHelper,
1211
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants,
1312
private $errors: IErrors,
1413
private $hostInfo: IHostInfo,
@@ -34,7 +33,6 @@ export class RunCommandBase implements ICommand {
3433
this.platform = this.$devicePlatformsConstants.Android;
3534
}
3635

37-
this.$androidBundleValidatorHelper.validateNoAab();
3836
this.$projectData.initializeProjectData();
3937
const platforms = this.platform ? [this.platform] : [this.$devicePlatformsConstants.Android, this.$devicePlatformsConstants.iOS];
4038

lib/common/child-process.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,8 @@ export class ChildProcess extends EventEmitter implements IChildProcess {
157157

158158
public async trySpawnFromCloseEvent(command: string, args: string[], options?: any, spawnFromEventOptions?: ISpawnFromEventOptions): Promise<ISpawnResult> {
159159
try {
160-
const childProcess = await this.spawnFromEvent(command, args, "close", options, spawnFromEventOptions);
161-
return childProcess;
160+
const childProcessResult = await this.spawnFromEvent(command, args, "close", options, spawnFromEventOptions);
161+
return childProcessResult;
162162
} catch (err) {
163163
this.$logger.trace(`Error from trySpawnFromCloseEvent method. More info: ${err}`);
164164
return Promise.resolve({ stderr: err && err.message ? err.message : err, stdout: null, exitCode: -1 });

lib/common/declarations.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1001,6 +1001,11 @@ interface ISysInfo {
10011001
* @return {Promise<string>} Returns the currently installed version of Xcode or null if Xcode is not installed or executed on Linux or Windows.
10021002
*/
10031003
getXcodeVersion(): Promise<string>;
1004+
/**
1005+
* Returns the currently installed Java path based on JAVA_HOME and PATH..
1006+
* @return {Promise<string>} The currently installed Java path.
1007+
*/
1008+
getJavaPath(): Promise<string>;
10041009
/**
10051010
* Returns the currently installed Cocoapods version.
10061011
* @return {Promise<string>} Returns the currently installed Cocoapods version. It will return null if Cocoapods is not installed.

lib/common/definitions/mobile.d.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -321,9 +321,9 @@ declare global {
321321
interface IDeviceApplicationManager extends NodeJS.EventEmitter {
322322
getInstalledApplications(): Promise<string[]>;
323323
isApplicationInstalled(appIdentifier: string): Promise<boolean>;
324-
installApplication(packageFilePath: string, appIdentifier?: string): Promise<void>;
324+
installApplication(packageFilePath: string, appIdentifier?: string, buildData?: IBuildData): Promise<void>;
325325
uninstallApplication(appIdentifier: string): Promise<void>;
326-
reinstallApplication(appIdentifier: string, packageFilePath: string): Promise<void>;
326+
reinstallApplication(appIdentifier: string, packageFilePath: string, buildData?: IBuildData): Promise<void>;
327327
startApplication(appData: IStartApplicationData): Promise<void>;
328328
stopApplication(appData: IApplicationData): Promise<void>;
329329
restartApplication(appData: IStartApplicationData): Promise<void>;

lib/common/mobile/android/android-application-manager.ts

+32-2
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ import { ApplicationManagerBase } from "../application-manager-base";
33
import { TARGET_FRAMEWORK_IDENTIFIERS, LiveSyncPaths } from "../../constants";
44
import { hook, sleep, regExpEscape } from "../../helpers";
55
import { cache } from "../../decorators";
6+
import { parse, join } from "path";
67

78
export class AndroidApplicationManager extends ApplicationManagerBase {
89
public PID_CHECK_INTERVAL = 100;
910
public PID_CHECK_TIMEOUT = 10000; // 10 secs
1011

1112
constructor(private adb: Mobile.IDeviceAndroidDebugBridge,
1213
private identifier: string,
14+
private $androidBundleToolService: IAndroidBundleToolService,
15+
private $fs: IFileSystem,
1316
private $options: IOptions,
1417
private $logcatHelper: Mobile.ILogcatHelper,
1518
private $androidProcessService: Mobile.IAndroidProcessService,
@@ -33,13 +36,29 @@ export class AndroidApplicationManager extends ApplicationManagerBase {
3336
}
3437

3538
@hook('install')
36-
public async installApplication(packageFilePath: string, appIdentifier?: string): Promise<void> {
39+
public async installApplication(packageFilePath: string, appIdentifier?: string, buildData?: IAndroidBuildData): Promise<void> {
3740
if (appIdentifier) {
3841
const deviceRootPath = `${LiveSyncPaths.ANDROID_TMP_DIR_NAME}/${appIdentifier}`;
3942
await this.adb.executeShellCommand(["rm", "-rf", deviceRootPath]);
4043
}
4144

42-
return this.adb.executeCommand(["install", "-r", `${packageFilePath}`]);
45+
const { dir, name, ext } = parse(packageFilePath);
46+
if (ext === ".aab") {
47+
const apksOutputPath = join(dir, name) + ".apks";
48+
if (!this.hasValidApksFile(packageFilePath, apksOutputPath)) {
49+
await this.$androidBundleToolService.buildApks({
50+
aabFilePath: packageFilePath,
51+
apksOutputPath,
52+
signingData: <IAndroidSigningData>buildData
53+
});
54+
}
55+
await this.$androidBundleToolService.installApks({
56+
apksFilePath: apksOutputPath,
57+
deviceId: this.identifier
58+
});
59+
} else {
60+
return this.adb.executeCommand(["install", "-r", `${packageFilePath}`]);
61+
}
4362
}
4463

4564
public uninstallApplication(appIdentifier: string): Promise<void> {
@@ -155,4 +174,15 @@ export class AndroidApplicationManager extends ApplicationManagerBase {
155174

156175
return new RegExp(`${regExpEscape(appIdentifier)}${packageActivitySeparator}${fullJavaClassName}`, `m`);
157176
}
177+
178+
private hasValidApksFile(aabFilaPath: string, apksFilePath: string): boolean {
179+
let isValid = false;
180+
if (this.$fs.exists(apksFilePath)) {
181+
const lastUpdatedAab = this.$fs.getFsStats(aabFilaPath).ctime.getTime();
182+
const lastUpdatedApks = this.$fs.getFsStats(apksFilePath).ctime.getTime();
183+
isValid = lastUpdatedApks >= lastUpdatedAab;
184+
}
185+
186+
return isValid;
187+
}
158188
}

lib/common/mobile/application-manager-base.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ export abstract class ApplicationManagerBase extends EventEmitter implements Mob
1818
}
1919
}
2020

21-
public async reinstallApplication(appIdentifier: string, packageFilePath: string): Promise<void> {
21+
public async reinstallApplication(appIdentifier: string, packageFilePath: string, buildData?: IBuildData): Promise<void> {
2222
const isApplicationInstalled = await this.isApplicationInstalled(appIdentifier);
2323

2424
if (isApplicationInstalled) {
2525
await this.uninstallApplication(appIdentifier);
2626
}
2727

28-
await this.installApplication(packageFilePath, appIdentifier);
28+
await this.installApplication(packageFilePath, appIdentifier, buildData);
2929
}
3030

3131
public async restartApplication(appData: Mobile.IApplicationData): Promise<void> {
@@ -83,7 +83,7 @@ export abstract class ApplicationManagerBase extends EventEmitter implements Mob
8383
}
8484
}
8585

86-
public abstract async installApplication(packageFilePath: string, appIdentifier?: string): Promise<void>;
86+
public abstract async installApplication(packageFilePath: string, appIdentifier?: string, buildData?: IBuildData): Promise<void>;
8787
public abstract async uninstallApplication(appIdentifier: string): Promise<void>;
8888
public abstract async startApplication(appData: Mobile.IApplicationData): Promise<void>;
8989
public abstract async stopApplication(appData: Mobile.IApplicationData): Promise<void>;

lib/common/test/unit-tests/android-application-manager.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { AndroidApplicationManager } from "../../mobile/android/android-application-manager";
22
import { Yok } from "../../yok";
33
import { assert } from "chai";
4-
import { CommonLoggerStub, LogcatHelperStub, AndroidProcessServiceStub, DeviceLogProviderStub, ErrorsStub } from "./stubs";
4+
import { AndroidBundleToolServiceStub, CommonLoggerStub, LogcatHelperStub, AndroidProcessServiceStub, DeviceLogProviderStub, ErrorsStub } from "./stubs";
5+
import { FileSystemStub } from "../../../../test/stubs";
56
const invalidIdentifier = "invalid.identifier";
67
const validDeviceIdentifier = "device.identifier";
78
const validIdentifier = "org.nativescript.testApp";
@@ -111,6 +112,8 @@ function createTestInjector(options?: {
111112
testInjector.register("identifier", validDeviceIdentifier);
112113
testInjector.register("logcatHelper", LogcatHelperStub);
113114
testInjector.register("androidProcessService", AndroidProcessServiceStub);
115+
testInjector.register("androidBundleToolService", AndroidBundleToolServiceStub)
116+
testInjector.register("fs", FileSystemStub);
114117
testInjector.register("httpClient", {});
115118
testInjector.register("deviceLogProvider", DeviceLogProviderStub);
116119
testInjector.register("hooksService", {});

lib/common/test/unit-tests/stubs.ts

+9
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,12 @@ export class DeviceLogProviderStub extends EventEmitter implements Mobile.IDevic
193193
this.currentDeviceProjectDirs[deviceIdentifier] = projectDir;
194194
}
195195
}
196+
197+
export class AndroidBundleToolServiceStub implements IAndroidBundleToolService {
198+
buildApks(options: IBuildApksOptions): Promise<void> {
199+
return;
200+
}
201+
installApks(options: IInstallApksOptions): Promise<void> {
202+
return;
203+
}
204+
}

lib/constants.ts

+1
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ export class BundleValidatorMessages {
288288
export class AndroidBundleValidatorMessages {
289289
public static AAB_NOT_SUPPORTED_BY_COMMNAND_MESSAGE = "This command does not support --aab (Android App Bundle) parameter.";
290290
public static NOT_SUPPORTED_RUNTIME_VERSION = "Android App Bundle (--aab) option requires NativeScript Android Runtime (tns-android) version %s and above.";
291+
public static NOT_SUPPORTED_ANDROID_VERSION = "Cannot use the Android App Bundle (--aab) option on device '%s' with Android '%s'. The --aab options is supported on Android '%s' and above.";
291292
}
292293

293294
export class AndroidAppBundleMessages {

lib/declarations.d.ts

+8
Original file line numberDiff line numberDiff line change
@@ -939,6 +939,14 @@ interface IAndroidBundleValidatorHelper {
939939
* @return {void}
940940
*/
941941
validateRuntimeVersion(projectData: IProjectData): void
942+
943+
/**
944+
* Validates that the specified device supports aab.
945+
* @param {Mobile.IDevice} device The device to be validated.
946+
* @param {IBuildData} buildData The current build data.
947+
* @return {void}
948+
*/
949+
validateDeviceApiLevel(device: Mobile.IDevice, buildData: IBuildData): void
942950
}
943951

944952
interface IOptionsTracker {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
interface IAndroidBundleToolService {
2+
buildApks(options: IBuildApksOptions): Promise<void>;
3+
installApks(options: IInstallApksOptions): Promise<void>;
4+
}
5+
6+
interface IBuildApksOptions {
7+
aabFilePath: string;
8+
apksOutputPath: string;
9+
signingData: IAndroidSigningData;
10+
}
11+
12+
interface IInstallApksOptions {
13+
apksFilePath: string;
14+
deviceId: string;
15+
}

lib/definitions/build.d.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@ interface IiOSBuildData extends IBuildData {
1717
iCloudContainerEnvironment: string;
1818
}
1919

20-
interface IAndroidBuildData extends IBuildData {
20+
interface IAndroidBuildData extends IBuildData, IAndroidSigningData {
21+
androidBundle: boolean;
22+
}
23+
24+
interface IAndroidSigningData {
2125
keyStoreAlias: string;
2226
keyStorePath: string;
2327
keyStoreAliasPassword: string;
2428
keyStorePassword: string;
25-
androidBundle: boolean;
2629
}
2730

2831
interface IBuildController {

lib/helpers/android-bundle-validator-helper.ts

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import * as util from "util";
22
import { AndroidBundleValidatorMessages, TNS_ANDROID_RUNTIME_NAME } from "../constants";
33
import { VersionValidatorHelper } from "./version-validator-helper";
4+
import * as semver from "semver";
45

56
export class AndroidBundleValidatorHelper extends VersionValidatorHelper implements IAndroidBundleValidatorHelper {
67
public static MIN_RUNTIME_VERSION = "5.0.0";
8+
public static MIN_ANDROID_WITH_AAB_SUPPORT = "4.4.0";
79

810
constructor(protected $projectData: IProjectData,
911
protected $errors: IErrors,
1012
protected $options: IOptions,
11-
protected $projectDataService: IProjectDataService) {
13+
protected $projectDataService: IProjectDataService,
14+
private $mobileHelper: Mobile.IMobileHelper) {
1215
super();
1316
}
1417

@@ -31,6 +34,22 @@ export class AndroidBundleValidatorHelper extends VersionValidatorHelper impleme
3134
}
3235
}
3336
}
37+
38+
public validateDeviceApiLevel(device: Mobile.IDevice, buildData: IBuildData): void {
39+
if (this.$mobileHelper.isAndroidPlatform(device.deviceInfo.platform)) {
40+
const androidBuildData = <IAndroidBuildData>buildData;
41+
if (androidBuildData.androidBundle) {
42+
if (!!device.deviceInfo.version &&
43+
semver.lt(semver.coerce(device.deviceInfo.version), AndroidBundleValidatorHelper.MIN_ANDROID_WITH_AAB_SUPPORT)) {
44+
this.$errors.fail(util.format(
45+
AndroidBundleValidatorMessages.NOT_SUPPORTED_ANDROID_VERSION,
46+
device.deviceInfo.identifier,
47+
device.deviceInfo.version,
48+
AndroidBundleValidatorHelper.MIN_ANDROID_WITH_AAB_SUPPORT));
49+
}
50+
}
51+
}
52+
}
3453
}
3554

3655
$injector.register("androidBundleValidatorHelper", AndroidBundleValidatorHelper);

lib/helpers/deploy-command-helper.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { BuildController } from "../controllers/build-controller";
33

44
export class DeployCommandHelper {
55
constructor(
6+
private $androidBundleValidatorHelper: IAndroidBundleValidatorHelper,
67
private $buildDataService: IBuildDataService,
78
private $buildController: BuildController,
89
private $devicesService: Mobile.IDevicesService,
@@ -41,6 +42,7 @@ export class DeployCommandHelper {
4142
nativePrepare: { skipNativePrepare: additionalOptions && additionalOptions.skipNativePrepare }
4243
});
4344

45+
this.$androidBundleValidatorHelper.validateDeviceApiLevel(d, buildData);
4446
const buildAction = additionalOptions && additionalOptions.buildPlatform ?
4547
additionalOptions.buildPlatform.bind(additionalOptions.buildPlatform, d.deviceInfo.platform, buildData, this.$projectData) :
4648
this.$buildController.build.bind(this.$buildController, buildData);

lib/helpers/livesync-command-helper.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { DeployController } from "../controllers/deploy-controller";
33

44
export class LiveSyncCommandHelper implements ILiveSyncCommandHelper {
55
constructor(
6+
private $androidBundleValidatorHelper: IAndroidBundleValidatorHelper,
67
private $buildDataService: IBuildDataService,
78
private $projectData: IProjectData,
89
private $options: IOptions,
@@ -66,6 +67,7 @@ export class LiveSyncCommandHelper implements ILiveSyncCommandHelper {
6667
});
6768

6869
const buildData = this.$buildDataService.getBuildData(this.$projectData.projectDir, d.deviceInfo.platform, { ...this.$options.argv, outputPath, buildForDevice: !d.isEmulator, watch: !this.$options.release && this.$options.watch });
70+
this.$androidBundleValidatorHelper.validateDeviceApiLevel(d, buildData);
6971

7072
const buildAction = additionalOptions && additionalOptions.buildPlatform ?
7173
additionalOptions.buildPlatform.bind(additionalOptions.buildPlatform, d.deviceInfo.platform, buildData, this.$projectData) :
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { resolve, join } from "path";
2+
3+
export class AndroidBundleToolService implements IAndroidBundleToolService {
4+
private javaPath: string;
5+
private aabToolPath: string;
6+
constructor(
7+
private $childProcess: IChildProcess,
8+
private $sysInfo: ISysInfo,
9+
private $errors: IErrors
10+
) {
11+
this.aabToolPath = resolve(join(__dirname, "../../../vendor/aab-tool/bundletool.jar"));
12+
}
13+
14+
public async buildApks(options: IBuildApksOptions): Promise<void> {
15+
if (!options.signingData) {
16+
this.$errors.fail(`Unable to build "apks" without a signing information.`);
17+
}
18+
19+
const aabToolResult = await this.execBundleTool([
20+
"build-apks",
21+
"--bundle",
22+
options.aabFilePath,
23+
"--output", options.apksOutputPath,
24+
"--ks", options.signingData.keyStorePath,
25+
"--ks-pass", `pass:${options.signingData.keyStorePassword}`,
26+
"--ks-key-alias", options.signingData.keyStoreAlias,
27+
"--key-pass", `pass:${options.signingData.keyStoreAliasPassword}`
28+
]);
29+
if (aabToolResult.exitCode !== 0 && aabToolResult.stderr) {
30+
this.$errors.fail(`Unable to build "apks" from the provided "aab". Error: ${aabToolResult.stderr}`);
31+
}
32+
}
33+
34+
public async installApks(options: IInstallApksOptions): Promise<void> {
35+
const aabToolResult = await this.execBundleTool(["install-apks", "--apks", options.apksFilePath, "--device-id", options.deviceId]);
36+
if (aabToolResult.exitCode !== 0 && aabToolResult.stderr) {
37+
this.$errors.fail(`Unable to install "apks" on device "${options.deviceId}". Error: ${aabToolResult.stderr}`);
38+
}
39+
}
40+
41+
private async execBundleTool(args: string[]) {
42+
const javaPath = await this.getJavaPath();
43+
const defaultArgs = [
44+
"-jar",
45+
this.aabToolPath
46+
];
47+
48+
const result = await this.$childProcess.trySpawnFromCloseEvent(javaPath, _.concat(defaultArgs, args));
49+
50+
return result;
51+
}
52+
53+
private async getJavaPath(): Promise<string> {
54+
if (!this.javaPath) {
55+
this.javaPath = await this.$sysInfo.getJavaPath();
56+
}
57+
58+
return this.javaPath;
59+
}
60+
}
61+
62+
$injector.register("androidBundleToolService", AndroidBundleToolService);

0 commit comments

Comments
 (0)