Skip to content

Support multiple .apk files produced from gradle build #3467

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const CONFIG_NS_FILE_NAME = "nsconfig.json";
export const CONFIG_NS_APP_RESOURCES_ENTRY = "appResourcesPath";
export const CONFIG_NS_APP_ENTRY = "appPath";
export const DEPENDENCIES_JSON_NAME = "dependencies.json";
export const APK_EXTENSION_NAME = ".apk";

export class PackageVersion {
static NEXT = "next";
Expand Down
14 changes: 12 additions & 2 deletions lib/definitions/platform.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,9 +260,9 @@ interface IPlatformData {
projectRoot: string;
normalizedPlatformName: string;
appDestinationDirectoryPath: string;
getDeviceBuildOutputPath(options: IRelease): string;
deviceBuildOutputPath: string;
emulatorBuildOutputPath?: string;
getValidPackageNames(buildOptions: { isReleaseBuild?: boolean, isForDevice?: boolean }): string[];
getValidBuildOutputData(buildOptions: IBuildOutputOptions): IValidBuildOutputData;
frameworkFilesExtensions: string[];
frameworkDirectoriesExtensions?: string[];
frameworkDirectoriesNames?: string[];
Expand All @@ -273,6 +273,16 @@ interface IPlatformData {
fastLivesyncFileExtensions: string[];
}

interface IValidBuildOutputData {
packageNames: string[];
regexes?: RegExp[];
}

interface IBuildOutputOptions {
isReleaseBuild?: boolean;
isForDevice?: boolean;
}

interface IPlatformsData {
availablePlatforms: any;
platformsNames: string[];
Expand Down
4 changes: 0 additions & 4 deletions lib/definitions/project.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,6 @@ interface IBuildForDevice {
buildForDevice: boolean;
}

interface IShouldInstall extends IBuildForDevice, IRelease {

}

interface INativePrepare {
skipNativePrepare: boolean;
}
Expand Down
38 changes: 11 additions & 27 deletions lib/services/android-project-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,19 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
platformProjectService: this,
emulatorServices: this.$androidEmulatorServices,
projectRoot: projectRoot,
getDeviceBuildOutputPath: (options: IRelease): string => {
return this.getDeviceBuildOutputPath(path.join(...deviceBuildOutputArr), projectData, options);
},
getValidPackageNames: (buildOptions: { isReleaseBuild?: boolean, isForDevice?: boolean }): string[] => {
deviceBuildOutputPath: path.join(...deviceBuildOutputArr),
getValidBuildOutputData: (buildOptions: IBuildOutputOptions): IValidBuildOutputData => {
const buildMode = buildOptions.isReleaseBuild ? Configurations.Release.toLowerCase() : Configurations.Debug.toLowerCase();

return [
`${packageName}-${buildMode}.apk`,
`${projectData.projectName}-${buildMode}.apk`,
`${projectData.projectName}.apk`,
`app-${buildMode}.apk`
];
return {
packageNames: [
`${packageName}-${buildMode}${constants.APK_EXTENSION_NAME}`,
`${projectData.projectName}-${buildMode}${constants.APK_EXTENSION_NAME}`,
`${projectData.projectName}${constants.APK_EXTENSION_NAME}`,
`${constants.APP_FOLDER_NAME}-${buildMode}${constants.APK_EXTENSION_NAME}`
],
regexes: [new RegExp(`${constants.APP_FOLDER_NAME}-.*-(${Configurations.Debug}|${Configurations.Release})${constants.APK_EXTENSION_NAME}`, "i"), new RegExp(`${packageName}-.*-(${Configurations.Debug}|${Configurations.Release})${constants.APK_EXTENSION_NAME}`, "i")]
};
},
frameworkFilesExtensions: [".jar", ".dat", ".so"],
configurationFileName: constants.MANIFEST_FILE_NAME,
Expand All @@ -108,23 +109,6 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
return this._platformData;
}

private getDeviceBuildOutputPath(currentPath: string, projectData: IProjectData, options: IRelease): string {
const currentPlatformData: IDictionary<any> = this.$projectDataService.getNSValue(projectData.projectDir, constants.TNS_ANDROID_RUNTIME_NAME);
const platformVersion = currentPlatformData && currentPlatformData[constants.VERSION_STRING];
const buildType = options.release === true ? "release" : "debug";
const normalizedPath = path.join(currentPath, buildType);

if (semver.valid(platformVersion)) {
const gradleAndroidPluginVersion3xx = "4.0.0";
const normalizedPlatformVersion = `${semver.major(platformVersion)}.${semver.minor(platformVersion)}.0`;
if (semver.lt(normalizedPlatformVersion, gradleAndroidPluginVersion3xx)) {
return currentPath;
}
}

return normalizedPath;
}

// TODO: Remove prior to the 4.0 CLI release @Pip3r4o @PanayotCankov
// Similar to the private method of the same name in platform-service.
private getCurrentPlatformVersion(platformData: IPlatformData, projectData: IProjectData): string {
Expand Down
16 changes: 9 additions & 7 deletions lib/services/ios-project-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,18 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
platformProjectService: this,
emulatorServices: this.$iOSEmulatorServices,
projectRoot: projectRoot,
getDeviceBuildOutputPath: (options: IRelease): string => {
return path.join(projectRoot, "build", "device");
},
emulatorBuildOutputPath: path.join(projectRoot, "build", "emulator"),
getValidPackageNames: (buildOptions: { isReleaseBuild?: boolean, isForDevice?: boolean }): string[] => {
deviceBuildOutputPath: path.join(projectRoot, constants.BUILD_DIR, "device"),
emulatorBuildOutputPath: path.join(projectRoot, constants.BUILD_DIR, "emulator"),
getValidBuildOutputData: (buildOptions: IBuildOutputOptions): IValidBuildOutputData => {
if (buildOptions.isForDevice) {
return [`${projectData.projectName}.ipa`];
return {
packageNames: [`${projectData.projectName}.ipa`]
};
}

return [`${projectData.projectName}.app`, `${projectData.projectName}.zip`];
return {
packageNames: [`${projectData.projectName}.app`, `${projectData.projectName}.zip`]
};
},
frameworkFilesExtensions: [".a", ".framework", ".bin"],
frameworkDirectoriesExtensions: [".framework"],
Expand Down
76 changes: 49 additions & 27 deletions lib/services/platform-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,13 +342,13 @@ export class PlatformService extends EventEmitter implements IPlatformService {

const platformData = this.$platformsData.getPlatformData(platform, projectData);
const forDevice = !buildConfig || buildConfig.buildForDevice;
outputPath = outputPath || (forDevice ? platformData.getDeviceBuildOutputPath(buildConfig) : platformData.emulatorBuildOutputPath || platformData.getDeviceBuildOutputPath(buildConfig));
outputPath = outputPath || (forDevice ? platformData.deviceBuildOutputPath : platformData.emulatorBuildOutputPath || platformData.deviceBuildOutputPath);
if (!this.$fs.exists(outputPath)) {
return true;
}

const packageNames = platformData.getValidPackageNames({ isForDevice: forDevice });
const packages = this.getApplicationPackages(outputPath, packageNames);
const validBuildOutputData = platformData.getValidBuildOutputData({ isForDevice: forDevice });
const packages = this.getApplicationPackages(outputPath, validBuildOutputData);
if (packages.length === 0) {
return true;
}
Expand Down Expand Up @@ -450,7 +450,7 @@ export class PlatformService extends EventEmitter implements IPlatformService {

const platformData = this.$platformsData.getPlatformData(platform, projectData);
const deviceBuildInfo: IBuildInfo = await this.getDeviceBuildInfo(device, projectData);
const localBuildInfo = this.getBuildInfo(platform, platformData, { buildForDevice: !device.isEmulator, release: release.release }, outputPath);
const localBuildInfo = this.getBuildInfo(platform, platformData, { buildForDevice: !device.isEmulator }, outputPath);
return !localBuildInfo || !deviceBuildInfo || deviceBuildInfo.buildTime !== localBuildInfo.buildTime;
}

Expand Down Expand Up @@ -561,12 +561,12 @@ export class PlatformService extends EventEmitter implements IPlatformService {
await this.$devicesService.execute(action, this.getCanExecuteAction(platform, runOptions));
}

private getBuildOutputPath(platform: string, platformData: IPlatformData, options: IShouldInstall): string {
private getBuildOutputPath(platform: string, platformData: IPlatformData, options: IBuildForDevice): string {
if (platform.toLowerCase() === this.$devicePlatformsConstants.iOS.toLowerCase()) {
return options.buildForDevice ? platformData.getDeviceBuildOutputPath(options) : platformData.emulatorBuildOutputPath;
return options.buildForDevice ? platformData.deviceBuildOutputPath : platformData.emulatorBuildOutputPath;
}

return platformData.getDeviceBuildOutputPath(options);
return platformData.deviceBuildOutputPath;
}

private async getDeviceBuildInfoFilePath(device: Mobile.IDevice, projectData: IProjectData): Promise<string> {
Expand All @@ -586,7 +586,7 @@ export class PlatformService extends EventEmitter implements IPlatformService {
}
}

private getBuildInfo(platform: string, platformData: IPlatformData, options: IShouldInstall, buildOutputPath?: string): IBuildInfo {
private getBuildInfo(platform: string, platformData: IPlatformData, options: IBuildForDevice, buildOutputPath?: string): IBuildInfo {
buildOutputPath = buildOutputPath || this.getBuildOutputPath(platform, platformData, options);
const buildInfoFile = path.join(buildOutputPath, buildInfoFileName);
if (this.$fs.exists(buildInfoFile)) {
Expand Down Expand Up @@ -746,27 +746,50 @@ export class PlatformService extends EventEmitter implements IPlatformService {
return platformData.platformProjectService.isPlatformPrepared(platformData.projectRoot, projectData);
}

private getApplicationPackages(buildOutputPath: string, validPackageNames: string[]): IApplicationPackage[] {
private getApplicationPackages(buildOutputPath: string, validBuildOutputData: IValidBuildOutputData): IApplicationPackage[] {
// Get latest package` that is produced from build
const candidates = this.$fs.readDirectory(buildOutputPath);
const packages = _.filter(candidates, candidate => {
return _.includes(validPackageNames, candidate);
}).map(currentPackage => {
currentPackage = path.join(buildOutputPath, currentPackage);

return {
packageName: currentPackage,
time: this.$fs.getFsStats(currentPackage).mtime
};
});
let result = this.getApplicationPackagesCore(this.$fs.readDirectory(buildOutputPath).map(filename => path.join(buildOutputPath, filename)), validBuildOutputData.packageNames);
if (result) {
return result;
}

const candidates = this.$fs.enumerateFilesInDirectorySync(buildOutputPath);
result = this.getApplicationPackagesCore(candidates, validBuildOutputData.packageNames);
if (result) {
return result;
}

if (validBuildOutputData.regexes && validBuildOutputData.regexes.length) {
return this.createApplicationPackages(candidates.filter(filepath => _.some(validBuildOutputData.regexes, regex => regex.test(path.basename(filepath)))));
}

return [];
}

return packages;
private getApplicationPackagesCore(candidates: string[], validPackageNames: string[]): IApplicationPackage[] {
const packages = candidates.filter(filePath => _.includes(validPackageNames, path.basename(filePath)));
if (packages.length > 0) {
return this.createApplicationPackages(packages);
}

return null;
}

private createApplicationPackages(packages: string[]): IApplicationPackage[] {
return packages.map(filepath => this.createApplicationPackage(filepath));
}

private getLatestApplicationPackage(buildOutputPath: string, validPackageNames: string[]): IApplicationPackage {
let packages = this.getApplicationPackages(buildOutputPath, validPackageNames);
private createApplicationPackage(packageName: string): IApplicationPackage {
return {
packageName,
time: this.$fs.getFsStats(packageName).mtime
};
}

private getLatestApplicationPackage(buildOutputPath: string, validBuildOutputData: IValidBuildOutputData): IApplicationPackage {
let packages = this.getApplicationPackages(buildOutputPath, validBuildOutputData);
if (packages.length === 0) {
const packageExtName = path.extname(validPackageNames[0]);
const packageExtName = path.extname(validBuildOutputData.packageNames[0]);
this.$errors.fail("No %s found in %s directory", packageExtName, buildOutputPath);
}

Expand All @@ -776,11 +799,11 @@ export class PlatformService extends EventEmitter implements IPlatformService {
}

public getLatestApplicationPackageForDevice(platformData: IPlatformData, buildConfig: IBuildConfig, outputPath?: string): IApplicationPackage {
return this.getLatestApplicationPackage(outputPath || platformData.getDeviceBuildOutputPath(buildConfig), platformData.getValidPackageNames({ isForDevice: true, isReleaseBuild: buildConfig.release }));
return this.getLatestApplicationPackage(outputPath || platformData.deviceBuildOutputPath, platformData.getValidBuildOutputData({ isForDevice: true, isReleaseBuild: buildConfig.release }));
}

public getLatestApplicationPackageForEmulator(platformData: IPlatformData, buildConfig: IBuildConfig, outputPath?: string): IApplicationPackage {
return this.getLatestApplicationPackage(outputPath || platformData.emulatorBuildOutputPath || platformData.getDeviceBuildOutputPath(buildConfig), platformData.getValidPackageNames({ isForDevice: false, isReleaseBuild: buildConfig.release }));
return this.getLatestApplicationPackage(outputPath || platformData.emulatorBuildOutputPath || platformData.deviceBuildOutputPath, platformData.getValidBuildOutputData({ isForDevice: false, isReleaseBuild: buildConfig.release }));
}

private async updatePlatform(platform: string, version: string, platformTemplate: string, projectData: IProjectData, config: IPlatformOptions): Promise<void> {
Expand Down Expand Up @@ -813,7 +836,6 @@ export class PlatformService extends EventEmitter implements IPlatformService {
} else {
this.$errors.failWithoutHelp("Native Platform cannot be updated.");
}

}

private async updatePlatformCore(platformData: IPlatformData, updateOptions: IUpdatePlatformOptions, projectData: IProjectData, config: IPlatformOptions): Promise<void> {
Expand Down
6 changes: 2 additions & 4 deletions test/platform-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,8 @@ class PlatformData implements IPlatformData {
};
emulatorServices: Mobile.IEmulatorPlatformServices = null;
projectRoot = "";
getDeviceBuildOutputPath = (buildTypeOption: IRelease) => {
return "";
}
getValidPackageNames = (buildOptions: { isForDevice?: boolean, isReleaseBuild?: boolean }) => [""];
deviceBuildOutputPath = "";
getValidBuildOutputData = (buildOptions: IBuildOutputOptions) => ({ packageNames: [""] });
validPackageNamesForDevice: string[] = [];
frameworkFilesExtensions = [".jar", ".dat"];
appDestinationDirectoryPath = "";
Expand Down
85 changes: 84 additions & 1 deletion test/platform-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ function createTestInjector() {
testInjector.register("projectChangesService", ProjectChangesLib.ProjectChangesService);
testInjector.register("emulatorPlatformService", stubs.EmulatorPlatformService);
testInjector.register("analyticsService", {
track: async (): Promise<any[]> => undefined
track: async (): Promise<any[]> => undefined,
trackEventActionInGoogleAnalytics: () => Promise.resolve()
});
testInjector.register("messages", Messages);
testInjector.register("devicePathProvider", {});
Expand Down Expand Up @@ -914,4 +915,86 @@ describe('Platform Service Tests', () => {
assert.isFalse(warnings.indexOf("has errors") !== -1);
});
});

describe("build", () => {
function mockData(buildOutput: string[], projectName: string): void {
mockPlatformsData(projectName);
mockFileSystem(buildOutput);
platformService.saveBuildInfoFile = () => undefined;
}

function mockPlatformsData(projectName: string): void {
const platformsData = testInjector.resolve("platformsData");
platformsData.getPlatformData = (platform: string) => {
return {
deviceBuildOutputPath: "",
platformProjectService: {
buildProject: () => Promise.resolve(),
on: () => ({}),
removeListener: () => ({})
},
getValidBuildOutputData: () => ({
packageNames: ["app-debug.apk", "app-release.apk", `${projectName}-debug.apk`, `${projectName}-release.apk`],
regexes: [/app-.*-(debug|release).apk/, new RegExp(`${projectName}-.*-(debug|release).apk`)]
})
};
};
}

function mockFileSystem(enumeratedFiles: string[]): void {
const fs = testInjector.resolve<IFileSystem>("fs");
fs.enumerateFilesInDirectorySync = () => enumeratedFiles;
fs.readDirectory = () => [];
fs.getFsStats = () => (<any>({ mtime: new Date() }));
}

describe("android platform", () => {
function getTestCases(configuration: string, apkName: string) {
return [{
name: "no additional options are specified in .gradle file",
buildOutput: [`/my/path/${configuration}/${apkName}-${configuration}.apk`],
expectedResult: `/my/path/${configuration}/${apkName}-${configuration}.apk`
}, {
name: "productFlavors are specified in .gradle file",
buildOutput: [`/my/path/arm64Demo/${configuration}/${apkName}-arm64-demo-${configuration}.apk`,
`/my/path/arm64Full/${configuration}/${apkName}-arm64-full-${configuration}.apk`,
`/my/path/armDemo/${configuration}/${apkName}-arm-demo-${configuration}.apk`,
`/my/path/armFull/${configuration}/${apkName}-arm-full-${configuration}.apk`,
`/my/path/x86Demo/${configuration}/${apkName}-x86-demo-${configuration}.apk`,
`/my/path/x86Full/${configuration}/${apkName}-x86-full-${configuration}.apk`],
expectedResult: `/my/path/x86Full/${configuration}/${apkName}-x86-full-${configuration}.apk`
}, {
name: "split options are specified in .gradle file",
buildOutput: [`/my/path/${configuration}/${apkName}-arm64-v8a-${configuration}.apk`,
`/my/path/${configuration}/${apkName}-armeabi-v7a-${configuration}.apk`,
`/my/path/${configuration}/${apkName}-universal-${configuration}.apk`,
`/my/path/${configuration}/${apkName}-x86-${configuration}.apk`],
expectedResult: `/my/path/${configuration}/${apkName}-x86-${configuration}.apk`
}, {
name: "android-runtime has version < 4.0.0",
buildOutput: [`/my/path/apk/${apkName}-${configuration}.apk`],
expectedResult: `/my/path/apk/${apkName}-${configuration}.apk`
}];
}

const platform = "Android";
const buildConfigs = [{buildForDevice: false}, {buildForDevice: true}];
const apkNames = ["app", "testProj"];
const configurations = ["debug", "release"];

_.each(apkNames, apkName => {
_.each(buildConfigs, buildConfig => {
_.each(configurations, configuration => {
_.each(getTestCases(configuration, apkName), testCase => {
it(`should find correct ${configuration} ${apkName}.apk when ${testCase.name} and buildConfig is ${JSON.stringify(buildConfig)}`, async () => {
mockData(testCase.buildOutput, apkName);
const actualResult = await platformService.buildPlatform(platform, <IBuildConfig>buildConfig, <IProjectData>{projectName: ""});
assert.deepEqual(actualResult, testCase.expectedResult);
});
});
});
});
});
});
});
});
Loading