diff --git a/lib/commands/appstore-upload.ts b/lib/commands/appstore-upload.ts index 77bfd8bf8d..ca142c8e64 100644 --- a/lib/commands/appstore-upload.ts +++ b/lib/commands/appstore-upload.ts @@ -60,7 +60,6 @@ export class PublishIOS implements ICommand { const platformInfo: IPreparePlatformInfo = { platform, appFilesUpdaterOptions, - skipModulesNativeCheck: !this.$options.syncAllFiles, platformTemplate: this.$options.platformTemplate, projectData: this.$projectData, config: this.$options, diff --git a/lib/commands/build.ts b/lib/commands/build.ts index fbee66cf3b..71ea5f109e 100644 --- a/lib/commands/build.ts +++ b/lib/commands/build.ts @@ -17,7 +17,6 @@ export class BuildCommandBase { const platformInfo: IPreparePlatformInfo = { platform, appFilesUpdaterOptions, - skipModulesNativeCheck: !this.$options.syncAllFiles, platformTemplate: this.$options.platformTemplate, projectData: this.$projectData, config: this.$options, diff --git a/lib/commands/prepare.ts b/lib/commands/prepare.ts index 22966fa1a1..a9ffd70f51 100644 --- a/lib/commands/prepare.ts +++ b/lib/commands/prepare.ts @@ -14,7 +14,6 @@ export class PrepareCommand implements ICommand { const platformInfo: IPreparePlatformInfo = { platform: args[0], appFilesUpdaterOptions, - skipModulesNativeCheck: !this.$options.syncAllFiles, platformTemplate: this.$options.platformTemplate, projectData: this.$projectData, config: this.$options, diff --git a/lib/constants.ts b/lib/constants.ts index d0d9b8a0de..42e724c28c 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -217,3 +217,5 @@ export const PACKAGE_PLACEHOLDER_NAME = "__PACKAGE__"; export class AddPlaformErrors { public static InvalidFrameworkPathStringFormat = "Invalid frameworkPath: %s. Please ensure the specified frameworkPath exists."; } + +export const PLUGIN_BUILD_DATA_FILENAME = "plugin-data.json"; diff --git a/lib/definitions/files-hash-service.d.ts b/lib/definitions/files-hash-service.d.ts index cd6b3f2b5f..753d63d65a 100644 --- a/lib/definitions/files-hash-service.d.ts +++ b/lib/definitions/files-hash-service.d.ts @@ -1,4 +1,5 @@ interface IFilesHashService { generateHashes(files: string[]): Promise; getChanges(files: string[], oldHashes: IStringDictionary): Promise; -} \ No newline at end of file + hasChangesInShasums(oldHashes: IStringDictionary, newHashes: IStringDictionary): boolean; +} diff --git a/lib/definitions/project-changes.d.ts b/lib/definitions/project-changes.d.ts index 1704a44ed9..9096f1cee9 100644 --- a/lib/definitions/project-changes.d.ts +++ b/lib/definitions/project-changes.d.ts @@ -41,8 +41,12 @@ interface IProjectChangesOptions extends IAppFilesUpdaterOptions, IProvision, IT nativePlatformStatus?: "1" | "2" | "3"; } +interface ICheckForChangesOptions extends IPlatform, IProjectDataComposition { + projectChangesOptions: IProjectChangesOptions; +} + interface IProjectChangesService { - checkForChanges(platform: string, projectData: IProjectData, buildOptions: IProjectChangesOptions): Promise; + checkForChanges(checkForChangesOpts: ICheckForChangesOptions): Promise; getPrepareInfo(platform: string, projectData: IProjectData): IPrepareInfo; savePrepareInfo(platform: string, projectData: IProjectData): void; getPrepareInfoFilePath(platform: string, projectData: IProjectData): string; diff --git a/lib/services/android-plugin-build-service.ts b/lib/services/android-plugin-build-service.ts index 6c03ecaa4e..66c7144f78 100644 --- a/lib/services/android-plugin-build-service.ts +++ b/lib/services/android-plugin-build-service.ts @@ -1,5 +1,5 @@ import * as path from "path"; -import { MANIFEST_FILE_NAME, INCLUDE_GRADLE_NAME, ASSETS_DIR, RESOURCES_DIR, TNS_ANDROID_RUNTIME_NAME, AndroidBuildDefaults } from "../constants"; +import { MANIFEST_FILE_NAME, INCLUDE_GRADLE_NAME, ASSETS_DIR, RESOURCES_DIR, TNS_ANDROID_RUNTIME_NAME, AndroidBuildDefaults, PLUGIN_BUILD_DATA_FILENAME } from "../constants"; import { getShortPluginName, hook } from "../common/helpers"; import { Builder, parseString } from "xml2js"; import { ILogger } from "log4js"; @@ -25,7 +25,8 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { private $npm: INodePackageManager, private $projectDataService: IProjectDataService, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, - private $errors: IErrors) { } + private $errors: IErrors, + private $filesHashService: IFilesHashService) { } private static MANIFEST_ROOT = { $: { @@ -172,23 +173,80 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { this.validateOptions(options); const manifestFilePath = this.getManifest(options.platformsAndroidDirPath); const androidSourceDirectories = this.getAndroidSourceDirectories(options.platformsAndroidDirPath); - const shouldBuildAar = !!manifestFilePath || androidSourceDirectories.length > 0; + const shortPluginName = getShortPluginName(options.pluginName); + const pluginTempDir = path.join(options.tempPluginDirPath, shortPluginName); + const pluginSourceFileHashesInfo = await this.getSourceFilesHashes(options.platformsAndroidDirPath, shortPluginName); + + const shouldBuildAar = await this.shouldBuildAar({ + manifestFilePath, + androidSourceDirectories, + pluginTempDir, + pluginSourceDir: options.platformsAndroidDirPath, + shortPluginName, + fileHashesInfo: pluginSourceFileHashesInfo + }); if (shouldBuildAar) { - const shortPluginName = getShortPluginName(options.pluginName); - const pluginTempDir = path.join(options.tempPluginDirPath, shortPluginName); - const pluginTempMainSrcDir = path.join(pluginTempDir, "src", "main"); + this.cleanPluginDir(pluginTempDir); + const pluginTempMainSrcDir = path.join(pluginTempDir, "src", "main"); await this.updateManifest(manifestFilePath, pluginTempMainSrcDir, shortPluginName); this.copySourceSetDirectories(androidSourceDirectories, pluginTempMainSrcDir); await this.setupGradle(pluginTempDir, options.platformsAndroidDirPath, options.projectDir); await this.buildPlugin({ pluginDir: pluginTempDir, pluginName: options.pluginName }); this.copyAar(shortPluginName, pluginTempDir, options.aarOutputDir); + this.writePluginHashInfo(pluginSourceFileHashesInfo, pluginTempDir); } return shouldBuildAar; } + private cleanPluginDir(pluginTempDir: string): void { + // In case plugin was already built in the current process, we need to clean the old sources as they may break the new build. + this.$fs.deleteDirectory(pluginTempDir); + this.$fs.ensureDirectoryExists(pluginTempDir); + } + + private getSourceFilesHashes(pluginTempPlatformsAndroidDir: string, shortPluginName: string): Promise { + const pathToAar = path.join(pluginTempPlatformsAndroidDir, `${shortPluginName}.aar`); + const pluginNativeDataFiles = this.$fs.enumerateFilesInDirectorySync(pluginTempPlatformsAndroidDir, (file: string, stat: IFsStats) => file !== pathToAar); + return this.$filesHashService.generateHashes(pluginNativeDataFiles); + } + + private writePluginHashInfo(fileHashesInfo: IStringDictionary, pluginTempDir: string): void { + const buildDataFile = this.getPathToPluginBuildDataFile(pluginTempDir); + this.$fs.writeJson(buildDataFile, fileHashesInfo); + } + + private async shouldBuildAar(opts: { + manifestFilePath: string, + androidSourceDirectories: string[], + pluginTempDir: string, + pluginSourceDir: string, + shortPluginName: string, + fileHashesInfo: IStringDictionary + }): Promise { + + let shouldBuildAar = !!opts.manifestFilePath || !!opts.androidSourceDirectories.length; + + if (shouldBuildAar && + this.$fs.exists(opts.pluginTempDir) && + this.$fs.exists(path.join(opts.pluginSourceDir, `${opts.shortPluginName}.aar`))) { + + const buildDataFile = this.getPathToPluginBuildDataFile(opts.pluginTempDir); + if (this.$fs.exists(buildDataFile)) { + const oldHashes = this.$fs.readJson(buildDataFile); + shouldBuildAar = this.$filesHashService.hasChangesInShasums(oldHashes, opts.fileHashesInfo); + } + } + + return shouldBuildAar; + } + + private getPathToPluginBuildDataFile(pluginDir: string): string { + return path.join(pluginDir, PLUGIN_BUILD_DATA_FILENAME); + } + private async updateManifest(manifestFilePath: string, pluginTempMainSrcDir: string, shortPluginName: string): Promise { let updatedManifestContent; this.$fs.ensureDirectoryExists(pluginTempMainSrcDir); @@ -256,7 +314,7 @@ export class AndroidPluginBuildService implements IAndroidPluginBuildService { return runtimeGradleVersions || {}; } - private getGradleVersions(packageData: { gradle: { version: string, android: string }}): IRuntimeGradleVersions { + private getGradleVersions(packageData: { gradle: { version: string, android: string } }): IRuntimeGradleVersions { const packageJsonGradle = packageData && packageData.gradle; let runtimeVersions: IRuntimeGradleVersions = null; if (packageJsonGradle && (packageJsonGradle.version || packageJsonGradle.android)) { diff --git a/lib/services/files-hash-service.ts b/lib/services/files-hash-service.ts index 4f3e9d81fb..45f1603843 100644 --- a/lib/services/files-hash-service.ts +++ b/lib/services/files-hash-service.ts @@ -26,6 +26,14 @@ export class FilesHashService implements IFilesHashService { public async getChanges(files: string[], oldHashes: IStringDictionary): Promise { const newHashes = await this.generateHashes(files); + return this.getChangesInShasums(oldHashes, newHashes); + } + + public hasChangesInShasums(oldHashes: IStringDictionary, newHashes: IStringDictionary): boolean { + return !!_.keys(this.getChangesInShasums(oldHashes, newHashes)).length; + } + + private getChangesInShasums(oldHashes: IStringDictionary, newHashes: IStringDictionary): IStringDictionary { return _.omitBy(newHashes, (hash: string, pathToFile: string) => !!_.find(oldHashes, (oldHash: string, oldPath: string) => pathToFile === oldPath && hash === oldHash)); } } diff --git a/lib/services/livesync/livesync-service.ts b/lib/services/livesync/livesync-service.ts index 5b1589380d..888e31dfb1 100644 --- a/lib/services/livesync/livesync-service.ts +++ b/lib/services/livesync/livesync-service.ts @@ -469,7 +469,6 @@ export class LiveSyncService extends EventEmitter implements IDebugLiveSyncServi deviceBuildInfoDescriptor, liveSyncData, settings, - skipModulesNativeCheck: !liveSyncData.watchAllFiles, bundle: liveSyncData.bundle, release: liveSyncData.release, env: liveSyncData.env diff --git a/lib/services/platform-service.ts b/lib/services/platform-service.ts index b9628ba979..6b73941bd5 100644 --- a/lib/services/platform-service.ts +++ b/lib/services/platform-service.ts @@ -269,13 +269,17 @@ export class PlatformService extends EventEmitter implements IPlatformService { const bundle = appFilesUpdaterOptions.bundle; const nativePlatformStatus = (nativePrepare && nativePrepare.skipNativePrepare) ? constants.NativePlatformStatus.requiresPlatformAdd : constants.NativePlatformStatus.requiresPrepare; - const changesInfo = await this.$projectChangesService.checkForChanges(platform, projectData, { - bundle, - release: appFilesUpdaterOptions.release, - provision: config.provision, - teamId: config.teamId, - nativePlatformStatus, - skipModulesNativeCheck: skipNativeCheckOptions.skipModulesNativeCheck + const changesInfo = await this.$projectChangesService.checkForChanges({ + platform, + projectData, + projectChangesOptions: { + bundle, + release: appFilesUpdaterOptions.release, + provision: config.provision, + teamId: config.teamId, + nativePlatformStatus, + skipModulesNativeCheck: skipNativeCheckOptions.skipModulesNativeCheck + } }); this.$logger.trace("Changes info in prepare platform:", changesInfo); diff --git a/lib/services/project-changes-service.ts b/lib/services/project-changes-service.ts index 1f6c54fbd0..7c92087431 100644 --- a/lib/services/project-changes-service.ts +++ b/lib/services/project-changes-service.ts @@ -1,6 +1,6 @@ import * as path from "path"; import { NODE_MODULES_FOLDER_NAME, NativePlatformStatus, PACKAGE_JSON_FILE_NAME, APP_GRADLE_FILE_NAME, BUILD_XCCONFIG_FILE_NAME } from "../constants"; -import { getHash } from "../common/helpers"; +import { getHash, hook } from "../common/helpers"; const prepareInfoFileName = ".nsprepareinfo"; @@ -45,18 +45,25 @@ export class ProjectChangesService implements IProjectChangesService { private _outputProjectMtime: number; private _outputProjectCTime: number; + private get $hooksService(): IHooksService { + return this.$injector.resolve("hooksService"); + } + constructor( private $platformsData: IPlatformsData, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, private $fs: IFileSystem, - private $filesHashService: IFilesHashService) { + private $filesHashService: IFilesHashService, + private $injector: IInjector) { } public get currentChanges(): IProjectChangesInfo { return this._changesInfo; } - public async checkForChanges(platform: string, projectData: IProjectData, projectChangesOptions: IProjectChangesOptions): Promise { + @hook("checkForChanges") + public async checkForChanges(checkForChangesOpts: ICheckForChangesOptions): Promise { + const { platform, projectData, projectChangesOptions } = checkForChangesOpts; const platformData = this.$platformsData.getPlatformData(platform, projectData); this._changesInfo = new ProjectChangesInfo(); const isNewPrepareInfo = await this.ensurePrepareInfo(platform, projectData, projectChangesOptions); diff --git a/lib/services/test-execution-service.ts b/lib/services/test-execution-service.ts index 8105310c29..ceef5e28e4 100644 --- a/lib/services/test-execution-service.ts +++ b/lib/services/test-execution-service.ts @@ -59,7 +59,6 @@ class TestExecutionService implements ITestExecutionService { const preparePlatformInfo: IPreparePlatformInfo = { platform, appFilesUpdaterOptions, - skipModulesNativeCheck: !this.$options.syncAllFiles, platformTemplate: this.$options.platformTemplate, projectData, config: this.$options, @@ -187,7 +186,6 @@ class TestExecutionService implements ITestExecutionService { const preparePlatformInfo: IPreparePlatformInfo = { platform, appFilesUpdaterOptions, - skipModulesNativeCheck: !this.$options.syncAllFiles, platformTemplate: this.$options.platformTemplate, projectData, config: this.$options, diff --git a/test/project-changes-service.ts b/test/project-changes-service.ts index 60b36ffa69..9876fac6bf 100644 --- a/test/project-changes-service.ts +++ b/test/project-changes-service.ts @@ -6,6 +6,7 @@ import { PlatformsData } from "../lib/platforms-data"; import { ProjectChangesService } from "../lib/services/project-changes-service"; import * as Constants from "../lib/constants"; import { FileSystem } from "../lib/common/file-system"; +import { HooksServiceStub } from "./stubs"; // start tracking temporary folders/files temp.track(); @@ -36,6 +37,7 @@ class ProjectChangesServiceTest extends BaseServiceTest { this.injector.register("logger", { warn: () => ({}) }); + this.injector.register("hooksService", HooksServiceStub); const fs = this.injector.resolve("fs"); fs.writeJson(path.join(this.projectDir, Constants.PACKAGE_JSON_FILE_NAME), { @@ -149,9 +151,28 @@ describe("Project Changes Service Tests", () => { describe("Accumulates Changes From Project Services", () => { it("accumulates changes from the project service", async () => { - const iOSChanges = await serviceTest.projectChangesService.checkForChanges("ios", serviceTest.projectData, { bundle: false, release: false, provision: undefined, teamId: undefined }); + const iOSChanges = await serviceTest.projectChangesService.checkForChanges({ + platform: "ios", + projectData: serviceTest.projectData, + projectChangesOptions: { + bundle: false, + release: false, + provision: undefined, + teamId: undefined + } + }); assert.isTrue(!!iOSChanges.signingChanged, "iOS signingChanged expected to be true"); - const androidChanges = await serviceTest.projectChangesService.checkForChanges("android", serviceTest.projectData, { bundle: false, release: false, provision: undefined, teamId: undefined }); + + const androidChanges = await serviceTest.projectChangesService.checkForChanges({ + platform: "android", + projectData: serviceTest.projectData, + projectChangesOptions: { + bundle: false, + release: false, + provision: undefined, + teamId: undefined + } + }); assert.isFalse(!!androidChanges.signingChanged, "Android signingChanged expected to be false"); }); }); diff --git a/test/services/android-plugin-build-service.ts b/test/services/android-plugin-build-service.ts index bcb4ccb458..62950ef546 100644 --- a/test/services/android-plugin-build-service.ts +++ b/test/services/android-plugin-build-service.ts @@ -1,6 +1,6 @@ import { AndroidPluginBuildService } from "../../lib/services/android-plugin-build-service"; import { assert } from "chai"; -import { INCLUDE_GRADLE_NAME, AndroidBuildDefaults } from "../../lib/constants"; +import { INCLUDE_GRADLE_NAME, AndroidBuildDefaults, PLUGIN_BUILD_DATA_FILENAME } from "../../lib/constants"; import { getShortPluginName } from "../../lib/common/helpers"; import * as FsLib from "../../lib/common/file-system"; import * as path from "path"; @@ -30,6 +30,8 @@ describe('androidPluginBuildService', () => { latestRuntimeGradleAndroidVersion?: string, projectRuntimeGradleVersion?: string, projectRuntimeGradleAndroidVersion?: string, + addPreviousBuildInfo?: boolean, + hasChangesInShasums?: boolean, }): IBuildOptions { options = options || {}; spawnFromEventCalled = false; @@ -53,6 +55,7 @@ describe('androidPluginBuildService', () => { latestRuntimeGradleAndroidVersion?: string, projectRuntimeGradleVersion?: string, projectRuntimeGradleAndroidVersion?: string, + hasChangesInShasums?: boolean }): void { const testInjector: IInjector = new stubs.InjectorStub(); testInjector.register("fs", FsLib.FileSystem); @@ -71,6 +74,11 @@ describe('androidPluginBuildService', () => { } }); testInjector.register('npm', setupNpm(options)); + testInjector.register('filesHashService', { + generateHashes: async (files: string[]): Promise => ({}), + getChanges: async (files: string[], oldHashes: IStringDictionary): Promise => ({}), + hasChangesInShasums: (oldHashes: IStringDictionary, newHashes: IStringDictionary): boolean => !!options.hasChangesInShasums + }); fs = testInjector.resolve("fs"); androidBuildPluginService = testInjector.resolve(AndroidPluginBuildService); @@ -113,7 +121,8 @@ describe('androidPluginBuildService', () => { addResFolder?: boolean, addAssetsFolder?: boolean, addIncludeGradle?: boolean, - addLegacyIncludeGradle?: boolean + addLegacyIncludeGradle?: boolean, + addPreviousBuildInfo?: boolean, }) { const validAndroidManifestContent = ` @@ -125,8 +134,8 @@ describe('androidPluginBuildService', () => { >text_string `; const validIncludeGradleContent = -`android {` + - (options.addLegacyIncludeGradle ? ` + `android {` + + (options.addLegacyIncludeGradle ? ` productFlavors { "nativescript-pro-ui" { dimension "nativescript-pro-ui" @@ -161,6 +170,13 @@ dependencies { if (options.addLegacyIncludeGradle || options.addIncludeGradle) { fs.writeFile(path.join(pluginFolder, INCLUDE_GRADLE_NAME), validIncludeGradleContent); } + + if (options.addPreviousBuildInfo) { + const pluginBuildDir = path.join(tempFolder, "my_plugin"); + fs.ensureDirectoryExists(pluginBuildDir); + fs.writeFile(path.join(pluginBuildDir, PLUGIN_BUILD_DATA_FILENAME), "{}"); + fs.writeFile(path.join(pluginFolder, "my_plugin.aar"), "{}"); + } } describe('buildAar', () => { @@ -206,6 +222,29 @@ dependencies { assert.isTrue(spawnFromEventCalled); }); + it('builds aar when plugin is already build and source files have changed since last buid', async () => { + const config: IBuildOptions = setup({ + addManifest: true, + addPreviousBuildInfo: true, + hasChangesInShasums: true + }); + + await androidBuildPluginService.buildAar(config); + + assert.isTrue(spawnFromEventCalled); + }); + + it('does not build aar when plugin is already build and source files have not changed', async () => { + const config: IBuildOptions = setup({ + addManifest: true, + addPreviousBuildInfo: true + }); + + await androidBuildPluginService.buildAar(config); + + assert.isFalse(spawnFromEventCalled); + }); + it('builds aar with the latest runtime gradle versions when no project dir is specified', async () => { const expectedGradleVersion = "1.2.3"; const expectedAndroidVersion = "4.5.6"; @@ -316,7 +355,7 @@ dependencies { function getGradleAndroidPluginVersion() { const gradleWrappersContent = fs.readText(path.join(tempFolder, shortPluginName, "build.gradle")); - const androidVersionRegex = /com\.android\.tools\.build\:gradle\:(.*)\'\n/g; + const androidVersionRegex = /com\.android\.tools\.build\:gradle\:(.*)\'\r?\n/g; const androidVersion = androidVersionRegex.exec(gradleWrappersContent)[1]; return androidVersion; @@ -324,7 +363,7 @@ dependencies { function getGradleVersion() { const buildGradleContent = fs.readText(path.join(tempFolder, shortPluginName, "gradle", "wrapper", "gradle-wrapper.properties")); - const gradleVersionRegex = /gradle\-(.*)\-bin\.zip\n/g; + const gradleVersionRegex = /gradle\-(.*)\-bin\.zip\r?\n/g; const gradleVersion = gradleVersionRegex.exec(buildGradleContent)[1]; return gradleVersion; diff --git a/test/stubs.ts b/test/stubs.ts index 6448255adc..72575b03ab 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -651,7 +651,7 @@ export class ChildProcessStub { } export class ProjectChangesService implements IProjectChangesService { - public async checkForChanges(platform: string): Promise { + public async checkForChanges(checkForChangesOpts: ICheckForChangesOptions): Promise { return {}; }