Skip to content

Commit c6c374a

Browse files
feat: Rebuild plugins for Android only when necessary
In case CLI builds `.aar` file for the Android part of a plugin during project preparation, consecutive prepares should not build the `.aar` file. Add logic to keep the hashes of the files that have been used for building the plugin and persist them in the `<project dir>/platforms/tempPlugin/<plugin-name>/plugin-data.json`. Before building a new `.aar` file, CLI will check if the mentioned file exists and compare its content with the shasums of the current source files of the plugin. This way we ensure the `.aar` will be built only when the sources are changed.
1 parent a4c11e5 commit c6c374a

File tree

6 files changed

+122
-15
lines changed

6 files changed

+122
-15
lines changed

lib/constants.ts

+2
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,5 @@ export const PACKAGE_PLACEHOLDER_NAME = "__PACKAGE__";
217217
export class AddPlaformErrors {
218218
public static InvalidFrameworkPathStringFormat = "Invalid frameworkPath: %s. Please ensure the specified frameworkPath exists.";
219219
}
220+
221+
export const PLUGIN_BUILD_DATA_FILENAME = "plugin-data.json";
+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
interface IFilesHashService {
22
generateHashes(files: string[]): Promise<IStringDictionary>;
33
getChanges(files: string[], oldHashes: IStringDictionary): Promise<IStringDictionary>;
4-
}
4+
hasChangesInShasums(oldHashes: IStringDictionary, newHashes: IStringDictionary): boolean;
5+
}

lib/services/android-plugin-build-service.ts

+64-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as path from "path";
2-
import { MANIFEST_FILE_NAME, INCLUDE_GRADLE_NAME, ASSETS_DIR, RESOURCES_DIR, TNS_ANDROID_RUNTIME_NAME, AndroidBuildDefaults } from "../constants";
2+
import { MANIFEST_FILE_NAME, INCLUDE_GRADLE_NAME, ASSETS_DIR, RESOURCES_DIR, TNS_ANDROID_RUNTIME_NAME, AndroidBuildDefaults, PLUGIN_BUILD_DATA_FILENAME } from "../constants";
33
import { getShortPluginName, hook } from "../common/helpers";
44
import { Builder, parseString } from "xml2js";
55
import { ILogger } from "log4js";
@@ -25,7 +25,8 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService {
2525
private $npm: INodePackageManager,
2626
private $projectDataService: IProjectDataService,
2727
private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants,
28-
private $errors: IErrors) { }
28+
private $errors: IErrors,
29+
private $filesHashService: IFilesHashService) { }
2930

3031
private static MANIFEST_ROOT = {
3132
$: {
@@ -172,23 +173,79 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService {
172173
this.validateOptions(options);
173174
const manifestFilePath = this.getManifest(options.platformsAndroidDirPath);
174175
const androidSourceDirectories = this.getAndroidSourceDirectories(options.platformsAndroidDirPath);
175-
const shouldBuildAar = !!manifestFilePath || androidSourceDirectories.length > 0;
176+
const shortPluginName = getShortPluginName(options.pluginName);
177+
const pluginTempDir = path.join(options.tempPluginDirPath, shortPluginName);
178+
const pluginSourceFileHashesInfo = await this.getSourceFilesHashes(options.platformsAndroidDirPath, shortPluginName);
179+
180+
const shouldBuildAar = await this.shouldBuildAar({
181+
manifestFilePath,
182+
androidSourceDirectories,
183+
pluginTempDir,
184+
pluginSourceDir: options.platformsAndroidDirPath,
185+
shortPluginName,
186+
fileHashesInfo: pluginSourceFileHashesInfo
187+
});
176188

177189
if (shouldBuildAar) {
178-
const shortPluginName = getShortPluginName(options.pluginName);
179-
const pluginTempDir = path.join(options.tempPluginDirPath, shortPluginName);
180-
const pluginTempMainSrcDir = path.join(pluginTempDir, "src", "main");
190+
// In case plugin was already built in the current process, we need to clean the old sources as they may break the new build.
191+
this.$fs.deleteDirectory(pluginTempDir);
192+
this.$fs.ensureDirectoryExists(pluginTempDir);
181193

194+
const pluginTempMainSrcDir = path.join(pluginTempDir, "src", "main");
182195
await this.updateManifest(manifestFilePath, pluginTempMainSrcDir, shortPluginName);
183196
this.copySourceSetDirectories(androidSourceDirectories, pluginTempMainSrcDir);
184197
await this.setupGradle(pluginTempDir, options.platformsAndroidDirPath, options.projectDir);
185198
await this.buildPlugin({ pluginDir: pluginTempDir, pluginName: options.pluginName });
186199
this.copyAar(shortPluginName, pluginTempDir, options.aarOutputDir);
200+
this.writePluginHashInfo(pluginSourceFileHashesInfo, pluginTempDir);
187201
}
188202

189203
return shouldBuildAar;
190204
}
191205

206+
private getSourceFilesHashes(pluginTempPlatformsAndroidDir: string, shortPluginName: string): Promise<IStringDictionary> {
207+
const pathToAar = path.join(pluginTempPlatformsAndroidDir, `${shortPluginName}.aar`);
208+
const pluginNativeDataFiles = this.$fs.enumerateFilesInDirectorySync(pluginTempPlatformsAndroidDir, (file: string, stat: IFsStats) => {
209+
return file !== pathToAar;
210+
});
211+
212+
return this.$filesHashService.generateHashes(pluginNativeDataFiles);
213+
}
214+
215+
private async writePluginHashInfo(fileHashesInfo: IStringDictionary, pluginTempDir: string): Promise<void> {
216+
const buildDataFile = this.getPathToPluginBuildDataFile(pluginTempDir);
217+
this.$fs.writeJson(buildDataFile, fileHashesInfo);
218+
}
219+
220+
private async shouldBuildAar(opts: {
221+
manifestFilePath: string,
222+
androidSourceDirectories: string[],
223+
pluginTempDir: string,
224+
pluginSourceDir: string,
225+
shortPluginName: string,
226+
fileHashesInfo: IStringDictionary
227+
}): Promise<boolean> {
228+
229+
let shouldBuildAar = !!opts.manifestFilePath || opts.androidSourceDirectories.length > 0;
230+
231+
if (shouldBuildAar &&
232+
this.$fs.exists(opts.pluginTempDir) &&
233+
this.$fs.exists(path.join(opts.pluginSourceDir, `${opts.shortPluginName}.aar`))) {
234+
235+
const buildDataFile = this.getPathToPluginBuildDataFile(opts.pluginTempDir);
236+
if (this.$fs.exists(buildDataFile)) {
237+
const oldHashes = this.$fs.readJson(buildDataFile);
238+
shouldBuildAar = this.$filesHashService.hasChangesInShasums(oldHashes, opts.fileHashesInfo);
239+
}
240+
}
241+
242+
return shouldBuildAar;
243+
}
244+
245+
private getPathToPluginBuildDataFile(pluginDir: string): string {
246+
return path.join(pluginDir, PLUGIN_BUILD_DATA_FILENAME);
247+
}
248+
192249
private async updateManifest(manifestFilePath: string, pluginTempMainSrcDir: string, shortPluginName: string): Promise<void> {
193250
let updatedManifestContent;
194251
this.$fs.ensureDirectoryExists(pluginTempMainSrcDir);
@@ -256,7 +313,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService {
256313
return runtimeGradleVersions || {};
257314
}
258315

259-
private getGradleVersions(packageData: { gradle: { version: string, android: string }}): IRuntimeGradleVersions {
316+
private getGradleVersions(packageData: { gradle: { version: string, android: string } }): IRuntimeGradleVersions {
260317
const packageJsonGradle = packageData && packageData.gradle;
261318
let runtimeVersions: IRuntimeGradleVersions = null;
262319
if (packageJsonGradle && (packageJsonGradle.version || packageJsonGradle.android)) {

lib/services/files-hash-service.ts

+8
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ export class FilesHashService implements IFilesHashService {
2626

2727
public async getChanges(files: string[], oldHashes: IStringDictionary): Promise<IStringDictionary> {
2828
const newHashes = await this.generateHashes(files);
29+
return this.getChangesInShasums(oldHashes, newHashes);
30+
}
31+
32+
public hasChangesInShasums(oldHashes: IStringDictionary, newHashes: IStringDictionary): boolean {
33+
return !!_.keys(this.getChangesInShasums(oldHashes, newHashes)).length;
34+
}
35+
36+
private getChangesInShasums(oldHashes: IStringDictionary, newHashes: IStringDictionary): IStringDictionary {
2937
return _.omitBy(newHashes, (hash: string, pathToFile: string) => !!_.find(oldHashes, (oldHash: string, oldPath: string) => pathToFile === oldPath && hash === oldHash));
3038
}
3139
}

test/services/android-plugin-build-service.ts

+45-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { AndroidPluginBuildService } from "../../lib/services/android-plugin-build-service";
22
import { assert } from "chai";
3-
import { INCLUDE_GRADLE_NAME, AndroidBuildDefaults } from "../../lib/constants";
3+
import { INCLUDE_GRADLE_NAME, AndroidBuildDefaults, PLUGIN_BUILD_DATA_FILENAME } from "../../lib/constants";
44
import { getShortPluginName } from "../../lib/common/helpers";
55
import * as FsLib from "../../lib/common/file-system";
66
import * as path from "path";
@@ -30,6 +30,8 @@ describe('androidPluginBuildService', () => {
3030
latestRuntimeGradleAndroidVersion?: string,
3131
projectRuntimeGradleVersion?: string,
3232
projectRuntimeGradleAndroidVersion?: string,
33+
addPreviousBuildInfo?: boolean,
34+
hasChangesInShasums?: boolean,
3335
}): IBuildOptions {
3436
options = options || {};
3537
spawnFromEventCalled = false;
@@ -53,6 +55,7 @@ describe('androidPluginBuildService', () => {
5355
latestRuntimeGradleAndroidVersion?: string,
5456
projectRuntimeGradleVersion?: string,
5557
projectRuntimeGradleAndroidVersion?: string,
58+
hasChangesInShasums?: boolean
5659
}): void {
5760
const testInjector: IInjector = new stubs.InjectorStub();
5861
testInjector.register("fs", FsLib.FileSystem);
@@ -71,6 +74,11 @@ describe('androidPluginBuildService', () => {
7174
}
7275
});
7376
testInjector.register('npm', setupNpm(options));
77+
testInjector.register('filesHashService', <IFilesHashService>{
78+
generateHashes: async (files: string[]): Promise<IStringDictionary> => ({}),
79+
getChanges: async (files: string[], oldHashes: IStringDictionary): Promise<IStringDictionary> => ({}),
80+
hasChangesInShasums: (oldHashes: IStringDictionary, newHashes: IStringDictionary): boolean => !!options.hasChangesInShasums
81+
});
7482

7583
fs = testInjector.resolve("fs");
7684
androidBuildPluginService = testInjector.resolve<AndroidPluginBuildService>(AndroidPluginBuildService);
@@ -113,7 +121,8 @@ describe('androidPluginBuildService', () => {
113121
addResFolder?: boolean,
114122
addAssetsFolder?: boolean,
115123
addIncludeGradle?: boolean,
116-
addLegacyIncludeGradle?: boolean
124+
addLegacyIncludeGradle?: boolean,
125+
addPreviousBuildInfo?: boolean,
117126
}) {
118127
const validAndroidManifestContent = `<?xml version="1.0" encoding="UTF-8"?>
119128
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
@@ -125,8 +134,8 @@ describe('androidPluginBuildService', () => {
125134
>text_string</string>
126135
</resources>`;
127136
const validIncludeGradleContent =
128-
`android {` +
129-
(options.addLegacyIncludeGradle ? `
137+
`android {` +
138+
(options.addLegacyIncludeGradle ? `
130139
productFlavors {
131140
"nativescript-pro-ui" {
132141
dimension "nativescript-pro-ui"
@@ -161,6 +170,13 @@ dependencies {
161170
if (options.addLegacyIncludeGradle || options.addIncludeGradle) {
162171
fs.writeFile(path.join(pluginFolder, INCLUDE_GRADLE_NAME), validIncludeGradleContent);
163172
}
173+
174+
if (options.addPreviousBuildInfo) {
175+
const pluginBuildDir = path.join(tempFolder, "my_plugin");
176+
fs.ensureDirectoryExists(pluginBuildDir);
177+
fs.writeFile(path.join(pluginBuildDir, PLUGIN_BUILD_DATA_FILENAME), "{}");
178+
fs.writeFile(path.join(pluginFolder, "my_plugin.aar"), "{}");
179+
}
164180
}
165181

166182
describe('buildAar', () => {
@@ -206,6 +222,29 @@ dependencies {
206222
assert.isTrue(spawnFromEventCalled);
207223
});
208224

225+
it('builds aar when plugin is already build and source files have changed since last buid', async () => {
226+
const config: IBuildOptions = setup({
227+
addManifest: true,
228+
addPreviousBuildInfo: true,
229+
hasChangesInShasums: true
230+
});
231+
232+
await androidBuildPluginService.buildAar(config);
233+
234+
assert.isTrue(spawnFromEventCalled);
235+
});
236+
237+
it('does not build aar when plugin is already build and source files have not changed', async () => {
238+
const config: IBuildOptions = setup({
239+
addManifest: true,
240+
addPreviousBuildInfo: true
241+
});
242+
243+
await androidBuildPluginService.buildAar(config);
244+
245+
assert.isFalse(spawnFromEventCalled);
246+
});
247+
209248
it('builds aar with the latest runtime gradle versions when no project dir is specified', async () => {
210249
const expectedGradleVersion = "1.2.3";
211250
const expectedAndroidVersion = "4.5.6";
@@ -316,15 +355,15 @@ dependencies {
316355

317356
function getGradleAndroidPluginVersion() {
318357
const gradleWrappersContent = fs.readText(path.join(tempFolder, shortPluginName, "build.gradle"));
319-
const androidVersionRegex = /com\.android\.tools\.build\:gradle\:(.*)\'\n/g;
358+
const androidVersionRegex = /com\.android\.tools\.build\:gradle\:(.*)\'\r?\n/g;
320359
const androidVersion = androidVersionRegex.exec(gradleWrappersContent)[1];
321360

322361
return androidVersion;
323362
}
324363

325364
function getGradleVersion() {
326365
const buildGradleContent = fs.readText(path.join(tempFolder, shortPluginName, "gradle", "wrapper", "gradle-wrapper.properties"));
327-
const gradleVersionRegex = /gradle\-(.*)\-bin\.zip\n/g;
366+
const gradleVersionRegex = /gradle\-(.*)\-bin\.zip\r?\n/g;
328367
const gradleVersion = gradleVersionRegex.exec(buildGradleContent)[1];
329368

330369
return gradleVersion;

0 commit comments

Comments
 (0)