diff --git a/lib/definitions/plugins.d.ts b/lib/definitions/plugins.d.ts index 76376de67e..876e5a0d07 100644 --- a/lib/definitions/plugins.d.ts +++ b/lib/definitions/plugins.d.ts @@ -4,6 +4,7 @@ interface IPluginsService { prepare(pluginData: IDependencyData): IFuture; getAllInstalledPlugins(): IFuture; ensureAllDependenciesAreInstalled(): IFuture; + afterPrepareAllPlugins(): IFuture; } interface IPluginData extends INodeModuleData { diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index 3f4aad3a53..8c03c7ed9e 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -40,6 +40,7 @@ interface IPlatformProjectService { updatePlatform(currentVersion: string, newVersion: string): IFuture; preparePluginNativeCode(pluginData: IPluginData): IFuture; removePluginNativeCode(pluginData: IPluginData): IFuture; + afterPrepareAllPlugins(): IFuture; } interface IAndroidProjectPropertiesManager { diff --git a/lib/services/android-project-service.ts b/lib/services/android-project-service.ts index 441d3e5681..825938f56e 100644 --- a/lib/services/android-project-service.ts +++ b/lib/services/android-project-service.ts @@ -296,6 +296,10 @@ class AndroidProjectService extends projectServiceBaseLib.PlatformProjectService }).future()(); } + public afterPrepareAllPlugins(): IFuture { + return Future.fromResult(); + } + private getLibraryRelativePath(basePath: string, libraryPath: string): string { return path.relative(basePath, libraryPath).split("\\").join("/"); } diff --git a/lib/services/ios-project-service.ts b/lib/services/ios-project-service.ts index 9653a98db5..f696743122 100644 --- a/lib/services/ios-project-service.ts +++ b/lib/services/ios-project-service.ts @@ -2,19 +2,21 @@ "use strict"; import Future = require("fibers/future"); -import path = require("path"); -import shell = require("shelljs"); -import util = require("util"); -import xcode = require("xcode"); +import * as path from "path"; +import * as shell from "shelljs"; +import * as util from "util"; +import * as os from "os"; +import * as xcode from "xcode"; import constants = require("./../constants"); import helpers = require("./../common/helpers"); import projectServiceBaseLib = require("./platform-project-service-base"); -class IOSProjectService extends projectServiceBaseLib.PlatformProjectServiceBase implements IPlatformProjectService { +export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServiceBase implements IPlatformProjectService { private static XCODE_PROJECT_EXT_NAME = ".xcodeproj"; private static XCODEBUILD_MIN_VERSION = "6.0"; private static IOS_PROJECT_NAME_PLACEHOLDER = "__PROJECT_NAME__"; private static IOS_PLATFORM_NAME = "ios"; + private static PODFILE_POST_INSTALL_SECTION_NAME = "post_install"; private get $npmInstallationManager(): INpmInstallationManager { return this.$injector.resolve("npmInstallationManager"); @@ -262,7 +264,6 @@ class IOSProjectService extends projectServiceBaseLib.PlatformProjectServiceBase this.savePbxProj(project).wait(); } - }).future()(); } @@ -270,6 +271,10 @@ class IOSProjectService extends projectServiceBaseLib.PlatformProjectServiceBase return this.$fs.deleteDirectory(this.platformData.appResourcesDestinationDirectoryPath); } + private get projectPodFilePath(): string { + return path.join(this.platformData.projectRoot, "Podfile"); + } + private replace(name: string): string { if(_.startsWith(name, '"')) { name = name.substr(1, name.length-2); @@ -303,22 +308,41 @@ class IOSProjectService extends projectServiceBaseLib.PlatformProjectServiceBase public preparePluginNativeCode(pluginData: IPluginData): IFuture { return (() => { let pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath(IOSProjectService.IOS_PLATFORM_NAME); - _.each(this.getAllDynamicFrameworksForPlugin(pluginData).wait(), fileName => this.addLibrary(path.join(pluginPlatformsFolderPath, fileName)).wait()); + this.prepareDynamicFrameworks(pluginPlatformsFolderPath, pluginData).wait(); + this.prepareCocoapods(pluginPlatformsFolderPath).wait(); }).future()(); } public removePluginNativeCode(pluginData: IPluginData): IFuture { return (() => { let pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath(IOSProjectService.IOS_PLATFORM_NAME); - let project = this.createPbxProj(); - - _.each(this.getAllDynamicFrameworksForPlugin(pluginData).wait(), fileName => { - let fullFrameworkPath = path.join(pluginPlatformsFolderPath, fileName); - let relativeFrameworkPath = this.getFrameworkRelativePath(fullFrameworkPath); - project.removeFramework(relativeFrameworkPath, { customFramework: true, embed: true }) - }); - - this.savePbxProj(project).wait(); + this.removeDynamicFrameworks(pluginPlatformsFolderPath, pluginData).wait(); + this.removeCocoapods(pluginPlatformsFolderPath).wait(); + }).future()(); + } + + public afterPrepareAllPlugins(): IFuture { + return (() => { + if(this.$fs.exists(this.projectPodFilePath).wait()) { + // Check availability + try { + this.$childProcess.exec("gem which cocoapods").wait(); + } catch(e) { + this.$errors.failWithoutHelp("CocoaPods are not installed. Run `sudo gem install cocoapods` and try again."); + } + + let projectPodfileContent = this.$fs.readText(this.projectPodFilePath).wait(); + this.$logger.trace("Project Podfile content"); + this.$logger.trace(projectPodfileContent); + + let firstPostInstallIndex = projectPodfileContent.indexOf(IOSProjectService.PODFILE_POST_INSTALL_SECTION_NAME); + if(firstPostInstallIndex !== -1 && firstPostInstallIndex !== projectPodfileContent.lastIndexOf(IOSProjectService.PODFILE_POST_INSTALL_SECTION_NAME)) { + this.$logger.warn(`Podfile contains more than one post_install sections. You need to open ${this.projectPodFilePath} file and manually resolve this issue.`); + } + + this.$logger.info("Installing pods..."); + this.$childProcess.exec("pod install", { cwd: this.platformData.projectRoot }).wait(); + } }).future()(); } @@ -379,5 +403,57 @@ class IOSProjectService extends projectServiceBaseLib.PlatformProjectServiceBase this.$fs.rename(path.join(fileRootLocation, oldFileName), path.join(fileRootLocation, newFileName)).wait(); }).future()(); } + + private prepareDynamicFrameworks(pluginPlatformsFolderPath: string, pluginData: IPluginData): IFuture { + return (() => { + _.each(this.getAllDynamicFrameworksForPlugin(pluginData).wait(), fileName => this.addLibrary(path.join(pluginPlatformsFolderPath, fileName)).wait()); + }).future()(); + } + + private prepareCocoapods(pluginPlatformsFolderPath: string): IFuture { + return (() => { + let pluginPodFilePath = path.join(pluginPlatformsFolderPath, "Podfile"); + if(this.$fs.exists(pluginPodFilePath).wait()) { + let pluginPodFileContent = this.$fs.readText(pluginPodFilePath).wait(); + let contentToWrite = this.buildPodfileContent(pluginPodFilePath, pluginPodFileContent); + this.$fs.appendFile(this.projectPodFilePath, contentToWrite).wait(); + } + }).future()(); + } + + private removeDynamicFrameworks(pluginPlatformsFolderPath: string, pluginData: IPluginData): IFuture { + return (() => { + let project = this.createPbxProj(); + + _.each(this.getAllDynamicFrameworksForPlugin(pluginData).wait(), fileName => { + let fullFrameworkPath = path.join(pluginPlatformsFolderPath, fileName); + let relativeFrameworkPath = this.getFrameworkRelativePath(fullFrameworkPath); + project.removeFramework(relativeFrameworkPath, { customFramework: true, embed: true }) + }); + + this.savePbxProj(project).wait(); + }).future()(); + } + + private removeCocoapods(pluginPlatformsFolderPath: string): IFuture { + return (() => { + let pluginPodFilePath = path.join(pluginPlatformsFolderPath, "Podfile"); + if(this.$fs.exists(pluginPodFilePath).wait()) { + let pluginPodFileContent = this.$fs.readText(pluginPodFilePath).wait(); + let projectPodFileContent = this.$fs.readText(this.projectPodFilePath).wait(); + let contentToRemove= this.buildPodfileContent(pluginPodFilePath, pluginPodFileContent); + projectPodFileContent = helpers.stringReplaceAll(projectPodFileContent, contentToRemove, ""); + if(_.isEmpty(projectPodFileContent)) { + this.$fs.deleteFile(this.projectPodFilePath).wait(); + } else { + this.$fs.writeFile(this.projectPodFilePath, projectPodFileContent).wait(); + } + } + }).future()(); + } + + private buildPodfileContent(pluginPodFilePath: string, pluginPodFileContent: string): string { + return `# Begin Podfile - ${pluginPodFilePath} ${os.EOL} ${pluginPodFileContent} ${os.EOL} # End Podfile ${os.EOL}`; + } } $injector.register("iOSProjectService", IOSProjectService); \ No newline at end of file diff --git a/lib/services/plugins-service.ts b/lib/services/plugins-service.ts index 6377e5c00f..52a2af48c8 100644 --- a/lib/services/plugins-service.ts +++ b/lib/services/plugins-service.ts @@ -111,7 +111,6 @@ export class PluginsService implements IPluginsService { this.validateXml(resultXml); this.$fs.writeFile(configurationFilePath, resultXml).wait(); } - this.$projectFilesManager.processPlatformSpecificFiles(pluginDestinationPath, platform).wait(); @@ -145,6 +144,14 @@ export class PluginsService implements IPluginsService { }).future()(); } + public afterPrepareAllPlugins(): IFuture { + let action = (pluginDestinationPath: string, platform: string, platformData: IPlatformData) => { + return platformData.platformProjectService.afterPrepareAllPlugins(); + }; + + return this.executeForAllInstalledPlatforms(action); + } + private get nodeModulesPath(): string { return path.join(this.$projectData.projectDir, "node_modules"); } diff --git a/lib/tools/broccoli/node-modules-dest-copy.ts b/lib/tools/broccoli/node-modules-dest-copy.ts index 7fd2ea8634..eea64ee722 100644 --- a/lib/tools/broccoli/node-modules-dest-copy.ts +++ b/lib/tools/broccoli/node-modules-dest-copy.ts @@ -76,6 +76,10 @@ export class DestCopy implements IBroccoliPlugin { this.$pluginsService.prepare(dependency).wait(); } }); + + if(!_.isEmpty(this.dependencies)) { + this.$pluginsService.afterPrepareAllPlugins().wait(); + } } public rebuild(treeDiff: IDiffResult): void { diff --git a/test/ios-project-service.ts b/test/ios-project-service.ts new file mode 100644 index 0000000000..e5f6b8090e --- /dev/null +++ b/test/ios-project-service.ts @@ -0,0 +1,149 @@ +/// +"use strict"; + +import Future = require("fibers/future"); +import * as path from "path"; +import temp = require("temp"); +temp.track(); + +import ChildProcessLib = require("../lib/common/child-process"); +import ConfigLib = require("../lib/config"); +import ErrorsLib = require("../lib/common/errors"); +import FileSystemLib = require("../lib/common/file-system"); +import HostInfoLib = require("../lib/common/host-info"); +import iOSProjectServiceLib = require("../lib/services/ios-project-service"); +import LoggerLib = require("../lib/common/logger"); +import OptionsLib = require("../lib/options"); +import ProjectDataLib = require("../lib/project-data"); + +import yok = require("../lib/common/yok"); + +import { assert } from "chai"; + +function createTestInjector(projectPath: string, projectName: string): IInjector { + let testInjector = new yok.Yok(); + testInjector.register("childProcess", ChildProcessLib.ChildProcess); + testInjector.register("config", ConfigLib.Configuration); + testInjector.register("errors", ErrorsLib.Errors); + testInjector.register("fs", FileSystemLib.FileSystem); + testInjector.register("hostInfo", HostInfoLib.HostInfo); + testInjector.register("injector", testInjector); + testInjector.register("iOSEmulatorServices", {}); + testInjector.register("iOSProjectService", iOSProjectServiceLib.IOSProjectService); + testInjector.register("logger", LoggerLib.Logger); + testInjector.register("options", OptionsLib.Options); + testInjector.register("projectData", { + platformsDir: projectPath, + projectName: projectName + }); + testInjector.register("projectHelper", {}); + testInjector.register("staticConfig", ConfigLib.StaticConfig); + + return testInjector; +} + +describe("Cocoapods support", () => { + it("adds plugin with Podfile", () => { + let projectName = "projectDirectory"; + let projectPath = temp.mkdirSync(projectName); + + let testInjector = createTestInjector(projectPath, projectName); + let fs: IFileSystem = testInjector.resolve("fs"); + + let packageJsonData = { + "name": "myProject", + "version": "0.1.0", + "nativescript": { + "id": "org.nativescript.myProject", + "tns-android": { + "version": "1.0.0" + } + } + }; + fs.writeJson(path.join(projectPath, "package.json"), packageJsonData).wait(); + + let platformsFolderPath = path.join(projectPath, "ios"); + fs.createDirectory(platformsFolderPath).wait(); + + let iOSProjectService = testInjector.resolve("iOSProjectService"); + iOSProjectService.prepareDynamicFrameworks = (pluginPlatformsFolderPath: string, pluginData: IPluginData): IFuture => { + return Future.fromResult(); + }; + + let pluginPath = temp.mkdirSync("pluginDirectory"); + let pluginPlatformsFolderPath = path.join(pluginPath, "platforms", "ios"); + let pluginPodfilePath = path.join(pluginPlatformsFolderPath, "Podfile"); + let pluginPodfileContent = ["source 'https://github.com/CocoaPods/Specs.git'", "platform :ios, '8.1'", "pod 'GoogleMaps'"].join("\n"); + fs.writeFile(pluginPodfilePath, pluginPodfileContent).wait(); + + let pluginData = { + pluginPlatformsFolderPath(platform: string): string { + return pluginPlatformsFolderPath; + } + }; + + iOSProjectService.preparePluginNativeCode(pluginData).wait(); + + let projectPodfilePath = path.join(platformsFolderPath, "Podfile"); + assert.isTrue(fs.exists(projectPodfilePath).wait()); + + let actualProjectPodfileContent = fs.readText(projectPodfilePath).wait(); + let expectedProjectPodfileContent = [`# Begin Podfile - ${pluginPodfilePath} `, ` ${pluginPodfileContent} `, " # End Podfile \n"].join("\n"); + assert.equal(actualProjectPodfileContent, expectedProjectPodfileContent); + }); + it("adds and removes plugin with Podfile", () => { + let projectName = "projectDirectory2"; + let projectPath = temp.mkdirSync(projectName); + + let testInjector = createTestInjector(projectPath, projectName); + let fs: IFileSystem = testInjector.resolve("fs"); + + let packageJsonData = { + "name": "myProject2", + "version": "0.1.0", + "nativescript": { + "id": "org.nativescript.myProject2", + "tns-android": { + "version": "1.0.0" + } + } + }; + fs.writeJson(path.join(projectPath, "package.json"), packageJsonData).wait(); + + let platformsFolderPath = path.join(projectPath, "ios"); + fs.createDirectory(platformsFolderPath).wait(); + + let iOSProjectService = testInjector.resolve("iOSProjectService"); + iOSProjectService.prepareDynamicFrameworks = (pluginPlatformsFolderPath: string, pluginData: IPluginData): IFuture => { + return Future.fromResult(); + }; + iOSProjectService.removeDynamicFrameworks = (pluginPlatformsFolderPath: string, pluginData: IPluginData): IFuture => { + return Future.fromResult(); + } + + let pluginPath = temp.mkdirSync("pluginDirectory"); + let pluginPlatformsFolderPath = path.join(pluginPath, "platforms", "ios"); + let pluginPodfilePath = path.join(pluginPlatformsFolderPath, "Podfile"); + let pluginPodfileContent = ["source 'https://github.com/CocoaPods/Specs.git'", "platform :ios, '8.1'", "pod 'GoogleMaps'"].join("\n"); + fs.writeFile(pluginPodfilePath, pluginPodfileContent).wait(); + + let pluginData = { + pluginPlatformsFolderPath(platform: string): string { + return pluginPlatformsFolderPath; + } + }; + + iOSProjectService.preparePluginNativeCode(pluginData).wait(); + + let projectPodfilePath = path.join(platformsFolderPath, "Podfile"); + assert.isTrue(fs.exists(projectPodfilePath).wait()); + + let actualProjectPodfileContent = fs.readText(projectPodfilePath).wait(); + let expectedProjectPodfileContent = [`# Begin Podfile - ${pluginPodfilePath} `, ` ${pluginPodfileContent} `, " # End Podfile \n"].join("\n"); + assert.equal(actualProjectPodfileContent, expectedProjectPodfileContent); + + iOSProjectService.removePluginNativeCode(pluginData).wait(); + + assert.isFalse(fs.exists(projectPodfilePath).wait()); + }); +}); \ No newline at end of file diff --git a/test/npm-support.ts b/test/npm-support.ts index e16d8e2f8c..76d369f348 100644 --- a/test/npm-support.ts +++ b/test/npm-support.ts @@ -120,7 +120,8 @@ function setupProject(): IFuture { normalizedPlatformName: "Android", platformProjectService: { prepareProject: () => Future.fromResult(), - prepareAppResources: () => Future.fromResult() + prepareAppResources: () => Future.fromResult(), + afterPrepareAllPlugins: () => Future.fromResult() } } }; diff --git a/test/stubs.ts b/test/stubs.ts index 8ddca77715..b0fb426a30 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -250,7 +250,8 @@ export class PlatformsDataStub implements IPlatformsData { appDestinationDirectoryPath: "", appResourcesDestinationDirectoryPath: "", preparePluginNativeCode: () => Future.fromResult(), - removePluginNativeCode: () => Future.fromResult() + removePluginNativeCode: () => Future.fromResult(), + afterPrepareAllPlugins: () => Future.fromResult() }; } @@ -271,7 +272,7 @@ export class PlatformProjectServiceStub implements IPlatformProjectService { validPackageNamesForDevice: [], frameworkFilesExtensions: [], appDestinationDirectoryPath: "", - appResourcesDestinationDirectoryPath: "" + appResourcesDestinationDirectoryPath: "", }; } validate(): IFuture { @@ -316,6 +317,9 @@ export class PlatformProjectServiceStub implements IPlatformProjectService { removePluginNativeCode(pluginData: IPluginData): IFuture { return Future.fromResult(); } + afterPrepareAllPlugins(): IFuture { + return Future.fromResult(); + } } export class ProjectDataService implements IProjectDataService {