diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 80ab81e867..d01c52eb73 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -15,7 +15,7 @@ $injector.require("androidPluginBuildService", "./services/android-plugin-build- $injector.require("iOSEntitlementsService", "./services/ios-entitlements-service"); $injector.require("iOSProjectService", "./services/ios-project-service"); $injector.require("iOSProvisionService", "./services/ios-provision-service"); -$injector.require("xCConfigService", "./services/xcconfig-service"); +$injector.require("xcconfigService", "./services/xcconfig-service"); $injector.require("cocoapodsService", "./services/cocoapods-service"); diff --git a/lib/declarations.d.ts b/lib/declarations.d.ts index 631462421e..e7933a5152 100644 --- a/lib/declarations.d.ts +++ b/lib/declarations.d.ts @@ -844,6 +844,13 @@ interface IVerifyXcprojOptions { * Designed for getting information about xcproj. */ interface IXcprojService { + /** + * Returns the path to the xcodeproj file + * @param projectData Information about the project. + * @param platformData Information about the platform. + * @return {string} The full path to the xcodeproj + */ + getXcodeprojPath(projectData: IProjectData, platformData: IPlatformData): string; /** * Checks whether the system needs xcproj to execute ios builds successfully. * In case the system does need xcproj but does not have it, prints an error message. @@ -856,6 +863,11 @@ interface IXcprojService { * @return {Promise} collected info about xcproj. */ getXcprojInfo(): Promise; + /** + * Checks if xcproj is available and throws an error in case when it is not available. + * @return {Promise} + */ + checkIfXcodeprojIsRequired(): Promise; } /** @@ -880,6 +892,32 @@ interface IXcprojInfo { xcprojAvailable: boolean; } +interface IXcconfigService { + /** + * Returns the path to the xcconfig file + * @param projectRoot The path to root folder of native project (platforms/ios) + * @param opts + * @returns {string} + */ + getPluginsXcconfigFilePath(projectRoot: string, opts: IRelease): string; + + /** + * Returns the value of a property from a xcconfig file. + * @param xcconfigFilePath The path to the xcconfig file + * @param propertyName The name of the property which value will be returned + * @returns {string} + */ + readPropertyValue(xcconfigFilePath: string, propertyName: string): string; + + /** + * Merges the content of source file into destination file + * @param sourceFile The content of thes source file + * @param destinationFile The content of the destination file + * @returns {Promise} + */ + mergeFiles(sourceFile: string, destinationFile: string): Promise; +} + /** * Describes helper used during execution of deploy commands. */ diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index b9f7aa950f..ac8b8bd1c2 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -400,9 +400,10 @@ interface IPlatformProjectService extends NodeJS.EventEmitter, IPlatformProjectS */ removePluginNativeCode(pluginData: IPluginData, projectData: IProjectData): Promise; - afterPrepareAllPlugins(projectData: IProjectData): Promise; beforePrepareAllPlugins(projectData: IProjectData, dependencies?: IDependencyData[]): Promise; + handleNativeDependenciesChange(projectData: IProjectData, opts: IRelease): Promise; + /** * Gets the path wheren App_Resources should be copied. * @returns {string} Path to native project, where App_Resources should be copied. @@ -410,7 +411,7 @@ interface IPlatformProjectService extends NodeJS.EventEmitter, IPlatformProjectS getAppResourcesDestinationDirectoryPath(projectData: IProjectData): string; cleanDeviceTempFolder(deviceIdentifier: string, projectData: IProjectData): Promise; - processConfigurationFilesFromAppResources(projectData: IProjectData, opts: { release: boolean, installPods: boolean }): Promise; + processConfigurationFilesFromAppResources(projectData: IProjectData, opts: { release: boolean }): Promise; /** * Ensures there is configuration file (AndroidManifest.xml, Info.plist) in app/App_Resources. @@ -483,6 +484,14 @@ interface ICocoaPodsService { */ getPodfileFooter(): string; + /** + * Merges the Podfile's content from App_Resources in the project's Podfile. + * @param projectData Information about the project. + * @param platformData Information about the platform. + * @returns {Promise} + */ + applyPodfileFromAppResources(projectData: IProjectData, platformData: IPlatformData): Promise; + /** * Prepares the Podfile content of a plugin and merges it in the project's Podfile. * @param {string} moduleName The module which the Podfile is from. @@ -524,6 +533,14 @@ interface ICocoaPodsService { * @returns {Promise} Information about the spawned process. */ executePodInstall(projectRoot: string, xcodeProjPath: string): Promise; + + /** + * Merges pod's xcconfig file into project's xcconfig file + * @param projectData + * @param platformData + * @param opts + */ + mergePodXcconfigFile(projectData: IProjectData, platformData: IPlatformData, opts: IRelease): Promise; } interface IRubyFunction { diff --git a/lib/services/android-project-service.ts b/lib/services/android-project-service.ts index f53f16765e..396cc69c45 100644 --- a/lib/services/android-project-service.ts +++ b/lib/services/android-project-service.ts @@ -577,10 +577,6 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject } } - public async afterPrepareAllPlugins(projectData: IProjectData): Promise { - return; - } - public async beforePrepareAllPlugins(projectData: IProjectData, dependencies?: IDependencyData[]): Promise { const shouldUseNewRoutine = this.runtimeVersionIsGreaterThanOrEquals(projectData, "3.3.0"); @@ -609,6 +605,10 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject } } + public async handleNativeDependenciesChange(projectData: IProjectData, opts: IRelease): Promise { + return; + } + private filterUniqueDependencies(dependencies: IDependencyData[]): IDependencyData[] { const depsDictionary = dependencies.reduce((dict, dep) => { const collision = dict[dep.name]; diff --git a/lib/services/cocoapods-service.ts b/lib/services/cocoapods-service.ts index 5f793a9be8..042970e5c4 100644 --- a/lib/services/cocoapods-service.ts +++ b/lib/services/cocoapods-service.ts @@ -1,6 +1,6 @@ import { EOL } from "os"; import * as path from "path"; -import { PluginNativeDirNames, PODFILE_NAME } from "../constants"; +import { PluginNativeDirNames, PODFILE_NAME, NS_BASE_PODFILE } from "../constants"; export class CocoaPodsService implements ICocoaPodsService { private static PODFILE_POST_INSTALL_SECTION_NAME = "post_install"; @@ -11,7 +11,8 @@ export class CocoaPodsService implements ICocoaPodsService { private $errors: IErrors, private $xcprojService: IXcprojService, private $logger: ILogger, - private $config: IConfiguration) { } + private $config: IConfiguration, + private $xcconfigService: IXcconfigService) { } public getPodfileHeader(targetName: string): string { return `use_frameworks!${EOL}${EOL}target "${targetName}" do${EOL}`; @@ -52,6 +53,26 @@ export class CocoaPodsService implements ICocoaPodsService { return podInstallResult; } + public async mergePodXcconfigFile(projectData: IProjectData, platformData: IPlatformData, opts: IRelease) { + const podFilesRootDirName = path.join("Pods", "Target Support Files", `Pods-${projectData.projectName}`); + const podFolder = path.join(platformData.projectRoot, podFilesRootDirName); + if (this.$fs.exists(podFolder)) { + const podXcconfigFilePath = opts && opts.release ? path.join(podFolder, `Pods-${projectData.projectName}.release.xcconfig`) + : path.join(podFolder, `Pods-${projectData.projectName}.debug.xcconfig`); + const pluginsXcconfigFilePath = this.$xcconfigService.getPluginsXcconfigFilePath(platformData.projectRoot, opts); + await this.$xcconfigService.mergeFiles(podXcconfigFilePath, pluginsXcconfigFilePath); + } + } + + public async applyPodfileFromAppResources(projectData: IProjectData, platformData: IPlatformData): Promise { + const { projectRoot, normalizedPlatformName } = platformData; + const mainPodfilePath = path.join(projectData.appResourcesDirectoryPath, normalizedPlatformName, PODFILE_NAME); + const projectPodfilePath = this.getProjectPodfilePath(projectRoot); + if (this.$fs.exists(projectPodfilePath) || this.$fs.exists(mainPodfilePath)) { + await this.applyPodfileToProject(NS_BASE_PODFILE, mainPodfilePath, projectData, projectRoot); + } + } + public async applyPodfileToProject(moduleName: string, podfilePath: string, projectData: IProjectData, nativeProjectPath: string): Promise { if (!this.$fs.exists(podfilePath)) { this.removePodfileFromProject(moduleName, podfilePath, projectData, nativeProjectPath); diff --git a/lib/services/ios-project-service.ts b/lib/services/ios-project-service.ts index 6845651be2..a5bfa13aaf 100644 --- a/lib/services/ios-project-service.ts +++ b/lib/services/ios-project-service.ts @@ -11,7 +11,6 @@ import * as temp from "temp"; import * as plist from "plist"; import { IOSProvisionService } from "./ios-provision-service"; import { IOSEntitlementsService } from "./ios-entitlements-service"; -import { XCConfigService } from "./xcconfig-service"; import * as mobileprovision from "ios-mobileprovision-finder"; import { BUILD_XCCONFIG_FILE_NAME, IosProjectConstants } from "../constants"; @@ -47,7 +46,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ private $platformEnvironmentRequirements: IPlatformEnvironmentRequirements, private $plistParser: IPlistParser, private $sysInfo: ISysInfo, - private $xCConfigService: XCConfigService) { + private $xcconfigService: IXcconfigService) { super($fs, $projectDataService); } @@ -791,19 +790,15 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f this.$fs.deleteDirectory(this.getAppResourcesDestinationDirectoryPath(projectData)); } - public async processConfigurationFilesFromAppResources(projectData: IProjectData, opts: { release: boolean, installPods: boolean }): Promise { - await this.mergeInfoPlists({ release: opts.release }, projectData); + public async processConfigurationFilesFromAppResources(projectData: IProjectData, opts: IRelease): Promise { + await this.mergeInfoPlists(projectData, opts); await this.$iOSEntitlementsService.merge(projectData); - await this.mergeProjectXcconfigFiles(opts.release, projectData); + await this.mergeProjectXcconfigFiles(projectData, opts); for (const pluginData of await this.getAllInstalledPlugins(projectData)) { await this.$pluginVariablesService.interpolatePluginVariables(pluginData, this.getPlatformData(projectData).configurationFilePath, projectData.projectDir); } this.$pluginVariablesService.interpolateAppIdentifier(this.getPlatformData(projectData).configurationFilePath, projectData.projectIdentifiers.ios); - - if (opts.installPods) { - await this.installPodsIfAny(projectData); - } } private getInfoPlistPath(projectData: IProjectData): string { @@ -826,7 +821,7 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f return Promise.resolve(); } - private async mergeInfoPlists(buildOptions: IRelease, projectData: IProjectData): Promise { + private async mergeInfoPlists(projectData: IProjectData, buildOptions: IRelease): Promise { const projectDir = projectData.projectDir; const infoPlistPath = path.join(projectData.appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName, this.getPlatformData(projectData).configurationFileName); this.ensureConfigurationFileInAppResources(); @@ -913,18 +908,6 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f return (this.$injector.resolve("pluginsService")).getAllInstalledPlugins(projectData); } - private getXcodeprojPath(projectData: IProjectData): string { - return path.join(this.getPlatformData(projectData).projectRoot, projectData.projectName + IosProjectConstants.XcodeProjExtName); - } - - private getPluginsDebugXcconfigFilePath(projectData: IProjectData): string { - return path.join(this.getPlatformData(projectData).projectRoot, "plugins-debug.xcconfig"); - } - - private getPluginsReleaseXcconfigFilePath(projectData: IProjectData): string { - return path.join(this.getPlatformData(projectData).projectRoot, "plugins-release.xcconfig"); - } - private replace(name: string): string { if (_.startsWith(name, '"')) { name = name.substr(1, name.length - 2); @@ -939,7 +922,7 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } private getPbxProjPath(projectData: IProjectData): string { - return path.join(this.getXcodeprojPath(projectData), "project.pbxproj"); + return path.join(this.$xcprojService.getXcodeprojPath(projectData, this.getPlatformData(projectData)), "project.pbxproj"); } private createPbxProj(projectData: IProjectData): any { @@ -980,32 +963,20 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f this.$cocoapodsService.removePodfileFromProject(pluginData.name, this.$cocoapodsService.getPluginPodfilePath(pluginData), projectData, projectRoot); } - public async afterPrepareAllPlugins(projectData: IProjectData): Promise { - await this.installPodsIfAny(projectData); - } - - public async installPodsIfAny(projectData: IProjectData): Promise { - const projectRoot = this.getPlatformData(projectData).projectRoot; - const mainPodfilePath = path.join(projectData.appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName, constants.PODFILE_NAME); - if (this.$fs.exists(this.$cocoapodsService.getProjectPodfilePath(projectRoot)) || this.$fs.exists(mainPodfilePath)) { - const xcodeProjPath = this.getXcodeprojPath(projectData); - const xcuserDataPath = path.join(xcodeProjPath, "xcuserdata"); - const sharedDataPath = path.join(xcodeProjPath, "xcshareddata"); - - if (!this.$fs.exists(xcuserDataPath) && !this.$fs.exists(sharedDataPath)) { - this.$logger.info("Creating project scheme..."); - await this.checkIfXcodeprojIsRequired(); - - const createSchemeRubyScript = `ruby -e "require 'xcodeproj'; xcproj = Xcodeproj::Project.open('${projectData.projectName}.xcodeproj'); xcproj.recreate_user_schemes; xcproj.save"`; - await this.$childProcess.exec(createSchemeRubyScript, { cwd: this.getPlatformData(projectData).projectRoot }); - } - - await this.$cocoapodsService.applyPodfileToProject(constants.NS_BASE_PODFILE, mainPodfilePath, projectData, this.getPlatformData(projectData).projectRoot); + public async handleNativeDependenciesChange(projectData: IProjectData, opts: IRelease): Promise { + const platformData = this.getPlatformData(projectData); + await this.$cocoapodsService.applyPodfileFromAppResources(projectData, platformData); - await this.$cocoapodsService.executePodInstall(projectRoot, xcodeProjPath); + const projectPodfilePath = this.$cocoapodsService.getProjectPodfilePath(platformData.projectRoot); + if (this.$fs.exists(projectPodfilePath)) { + await this.$cocoapodsService.executePodInstall(platformData.projectRoot, this.$xcprojService.getXcodeprojPath(projectData, platformData)); + // The `pod install` command adds a new target to the .pbxproject. This target adds additional build phases to xcodebuild. + // Some of these phases relies on env variables (like PODS_PODFILE_DIR_PATH or PODS_ROOT). + // These variables are produced from merge of pod's xcconfig file and project's xcconfig file. + // So the correct order is `pod install` to be executed before merging pod's xcconfig file. + await this.$cocoapodsService.mergePodXcconfigFile(projectData, platformData, opts); } } - public beforePrepareAllPlugins(): Promise { return Promise.resolve(); } @@ -1064,7 +1035,7 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } public getDeploymentTarget(projectData: IProjectData): semver.SemVer { - const target = this.$xCConfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "IPHONEOS_DEPLOYMENT_TARGET"); + const target = this.$xcconfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "IPHONEOS_DEPLOYMENT_TARGET"); if (!target) { return null; } @@ -1208,34 +1179,24 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f this.$fs.writeFile(path.join(headersFolderPath, "module.modulemap"), modulemap); } - private async mergeXcconfigFiles(pluginFile: string, projectFile: string): Promise { - if (!this.$fs.exists(projectFile)) { - this.$fs.writeFile(projectFile, ""); - } - - await this.checkIfXcodeprojIsRequired(); - const escapedProjectFile = projectFile.replace(/'/g, "\\'"), - escapedPluginFile = pluginFile.replace(/'/g, "\\'"), - mergeScript = `require 'xcodeproj'; Xcodeproj::Config.new('${escapedProjectFile}').merge(Xcodeproj::Config.new('${escapedPluginFile}')).save_as(Pathname.new('${escapedProjectFile}'))`; - await this.$childProcess.exec(`ruby -e "${mergeScript}"`); - } - - private async mergeProjectXcconfigFiles(release: boolean, projectData: IProjectData): Promise { - const pluginsXcconfigFilePath = release ? this.getPluginsReleaseXcconfigFilePath(projectData) : this.getPluginsDebugXcconfigFilePath(projectData); + private async mergeProjectXcconfigFiles(projectData: IProjectData, opts: IRelease): Promise { + const platformData = this.getPlatformData(projectData); + const pluginsXcconfigFilePath = this.$xcconfigService.getPluginsXcconfigFilePath(platformData.projectRoot, opts); this.$fs.deleteFile(pluginsXcconfigFilePath); - const allPlugins: IPluginData[] = await (this.$injector.resolve("pluginsService")).getAllInstalledPlugins(projectData); + const pluginsService = this.$injector.resolve("pluginsService"); + const allPlugins: IPluginData[] = await pluginsService.getAllInstalledPlugins(projectData); for (const plugin of allPlugins) { const pluginPlatformsFolderPath = plugin.pluginPlatformsFolderPath(IOSProjectService.IOS_PLATFORM_NAME); const pluginXcconfigFilePath = path.join(pluginPlatformsFolderPath, BUILD_XCCONFIG_FILE_NAME); if (this.$fs.exists(pluginXcconfigFilePath)) { - await this.mergeXcconfigFiles(pluginXcconfigFilePath, pluginsXcconfigFilePath); + await this.$xcconfigService.mergeFiles(pluginXcconfigFilePath, pluginsXcconfigFilePath); } } const appResourcesXcconfigPath = path.join(projectData.appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName, BUILD_XCCONFIG_FILE_NAME); if (this.$fs.exists(appResourcesXcconfigPath)) { - await this.mergeXcconfigFiles(appResourcesXcconfigPath, pluginsXcconfigFilePath); + await this.$xcconfigService.mergeFiles(appResourcesXcconfigPath, pluginsXcconfigFilePath); } if (!this.$fs.exists(pluginsXcconfigFilePath)) { @@ -1246,7 +1207,7 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } // Set Entitlements Property to point to default file if not set explicitly by the user. - const entitlementsPropertyValue = this.$xCConfigService.readPropertyValue(pluginsXcconfigFilePath, constants.CODE_SIGN_ENTITLEMENTS); + const entitlementsPropertyValue = this.$xcconfigService.readPropertyValue(pluginsXcconfigFilePath, constants.CODE_SIGN_ENTITLEMENTS); if (entitlementsPropertyValue === null && this.$fs.exists(this.$iOSEntitlementsService.getPlatformsEntitlementsPath(projectData))) { temp.track(); const tempEntitlementsDir = temp.mkdirSync("entitlements"); @@ -1254,28 +1215,7 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f const entitlementsRelativePath = this.$iOSEntitlementsService.getPlatformsEntitlementsRelativePath(projectData); this.$fs.writeFile(tempEntitlementsFilePath, `CODE_SIGN_ENTITLEMENTS = ${entitlementsRelativePath}${EOL}`); - await this.mergeXcconfigFiles(tempEntitlementsFilePath, pluginsXcconfigFilePath); - } - - const podFilesRootDirName = path.join("Pods", "Target Support Files", `Pods-${projectData.projectName}`); - const podFolder = path.join(this.getPlatformData(projectData).projectRoot, podFilesRootDirName); - if (this.$fs.exists(podFolder)) { - if (release) { - await this.mergeXcconfigFiles(path.join(this.getPlatformData(projectData).projectRoot, podFilesRootDirName, `Pods-${projectData.projectName}.release.xcconfig`), this.getPluginsReleaseXcconfigFilePath(projectData)); - } else { - await this.mergeXcconfigFiles(path.join(this.getPlatformData(projectData).projectRoot, podFilesRootDirName, `Pods-${projectData.projectName}.debug.xcconfig`), this.getPluginsDebugXcconfigFilePath(projectData)); - } - } - } - - private async checkIfXcodeprojIsRequired(): Promise { - const xcprojInfo = await this.$xcprojService.getXcprojInfo(); - if (xcprojInfo.shouldUseXcproj && !xcprojInfo.xcprojAvailable) { - const errorMessage = `You are using CocoaPods version ${xcprojInfo.cocoapodVer} which does not support Xcode ${xcprojInfo.xcodeVersion.major}.${xcprojInfo.xcodeVersion.minor} yet.${EOL}${EOL}You can update your cocoapods by running $sudo gem install cocoapods from a terminal.${EOL}${EOL}In order for the NativeScript CLI to be able to work correctly with this setup you need to install xcproj command line tool and add it to your PATH. Xcproj can be installed with homebrew by running $ brew install xcproj from the terminal`; - - this.$errors.failWithoutHelp(errorMessage); - - return true; + await this.$xcconfigService.mergeFiles(tempEntitlementsFilePath, pluginsXcconfigFilePath); } } @@ -1301,7 +1241,7 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } private readTeamId(projectData: IProjectData): string { - let teamId = this.$xCConfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "DEVELOPMENT_TEAM"); + let teamId = this.$xcconfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "DEVELOPMENT_TEAM"); const fileName = path.join(this.getPlatformData(projectData).projectRoot, "teamid"); if (this.$fs.exists(fileName)) { @@ -1312,19 +1252,19 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } private readXCConfigProvisioningProfile(projectData: IProjectData): string { - return this.$xCConfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "PROVISIONING_PROFILE"); + return this.$xcconfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "PROVISIONING_PROFILE"); } private readXCConfigProvisioningProfileForIPhoneOs(projectData: IProjectData): string { - return this.$xCConfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "PROVISIONING_PROFILE[sdk=iphoneos*]"); + return this.$xcconfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "PROVISIONING_PROFILE[sdk=iphoneos*]"); } private readXCConfigProvisioningProfileSpecifier(projectData: IProjectData): string { - return this.$xCConfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "PROVISIONING_PROFILE_SPECIFIER"); + return this.$xcconfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "PROVISIONING_PROFILE_SPECIFIER"); } private readXCConfigProvisioningProfileSpecifierForIPhoneOs(projectData: IProjectData): string { - return this.$xCConfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]"); + return this.$xcconfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]"); } private async getDevelopmentTeam(projectData: IProjectData, teamId?: string): Promise { diff --git a/lib/services/prepare-platform-native-service.ts b/lib/services/prepare-platform-native-service.ts index d78f3cd709..4de09e99a7 100644 --- a/lib/services/prepare-platform-native-service.ts +++ b/lib/services/prepare-platform-native-service.ts @@ -42,9 +42,10 @@ export class PreparePlatformNativeService extends PreparePlatformService impleme await config.platformData.platformProjectService.prepareProject(config.projectData, config.platformSpecificData); } - const shouldPrepareModules = !config.changesInfo || config.changesInfo.modulesChanged; + const hasModulesChange = !config.changesInfo || config.changesInfo.modulesChanged; + const hasConfigChange = !config.changesInfo || config.changesInfo.configChanged; - if (shouldPrepareModules) { + if (hasModulesChange) { await this.$pluginsService.validate(config.platformData, config.projectData); const appDestinationDirectoryPath = path.join(config.platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME); @@ -64,9 +65,9 @@ export class PreparePlatformNativeService extends PreparePlatformService impleme await this.$nodeModulesBuilder.prepareNodeModules({ nodeModulesData, release: config.appFilesUpdaterOptions.release }); } - if (!config.changesInfo || config.changesInfo.configChanged || config.changesInfo.modulesChanged) { - // Passing !shouldPrepareModules` we assume that if the node modules are prepared base Podfile content is added and `pod install` is executed. - await config.platformData.platformProjectService.processConfigurationFilesFromAppResources(config.projectData, {release:config.appFilesUpdaterOptions.release, installPods: !shouldPrepareModules}); + if (hasModulesChange || hasConfigChange) { + await config.platformData.platformProjectService.processConfigurationFilesFromAppResources(config.projectData, { release: config.appFilesUpdaterOptions.release }); + await config.platformData.platformProjectService.handleNativeDependenciesChange(config.projectData, { release: config.appFilesUpdaterOptions.release }); } config.platformData.platformProjectService.interpolateConfigurationFile(config.projectData, config.platformSpecificData); diff --git a/lib/services/xcconfig-service.ts b/lib/services/xcconfig-service.ts index f595d5332b..59a33353ec 100644 --- a/lib/services/xcconfig-service.ts +++ b/lib/services/xcconfig-service.ts @@ -1,12 +1,42 @@ -export class XCConfigService { - constructor(private $fs: IFileSystem) { +import * as path from "path"; + +export class XcconfigService implements IXcconfigService { + constructor( + private $childProcess: IChildProcess, + private $fs: IFileSystem, + private $xcprojService: IXcprojService) { } + + public getPluginsXcconfigFilePath(projectRoot: string, opts: IRelease): string { + if (opts && opts.release) { + return this.getPluginsReleaseXcconfigFilePath(projectRoot); + } + + return this.getPluginsDebugXcconfigFilePath(projectRoot); + } + + private getPluginsDebugXcconfigFilePath(projectRoot: string): string { + return path.join(projectRoot, "plugins-debug.xcconfig"); + } + + private getPluginsReleaseXcconfigFilePath(projectRoot: string): string { + return path.join(projectRoot, "plugins-release.xcconfig"); + } + + public async mergeFiles(sourceFile: string, destinationFile: string): Promise { + if (!this.$fs.exists(destinationFile)) { + this.$fs.writeFile(destinationFile, ""); + } + + // TODO: Consider to remove this method + await this.$xcprojService.checkIfXcodeprojIsRequired(); + + const escapedDestinationFile = destinationFile.replace(/'/g, "\\'"); + const escapedSourceFile = sourceFile.replace(/'/g, "\\'"); + + const mergeScript = `require 'xcodeproj'; Xcodeproj::Config.new('${escapedDestinationFile}').merge(Xcodeproj::Config.new('${escapedSourceFile}')).save_as(Pathname.new('${escapedDestinationFile}'))`; + await this.$childProcess.exec(`ruby -e "${mergeScript}"`); } - /** - * Returns the Value of a Property from a XC Config file. - * @param xcconfigFilePath - * @param propertyName - */ public readPropertyValue(xcconfigFilePath: string, propertyName: string): string { if (this.$fs.exists(xcconfigFilePath)) { const text = this.$fs.readText(xcconfigFilePath); @@ -37,4 +67,4 @@ export class XCConfigService { } } -$injector.register("xCConfigService", XCConfigService); +$injector.register("xcconfigService", XcconfigService); diff --git a/lib/services/xcproj-service.ts b/lib/services/xcproj-service.ts index c4ca1f4c8f..2344a89065 100644 --- a/lib/services/xcproj-service.ts +++ b/lib/services/xcproj-service.ts @@ -1,6 +1,8 @@ import * as semver from "semver"; import * as helpers from "../common/helpers"; import { EOL } from "os"; +import * as path from "path"; +import { IosProjectConstants } from "../constants"; class XcprojService implements IXcprojService { private xcprojInfoCache: IXcprojInfo; @@ -12,6 +14,10 @@ class XcprojService implements IXcprojService { private $xcodeSelectService: IXcodeSelectService) { } + public getXcodeprojPath(projectData: IProjectData, platformData: IPlatformData): string { + return path.join(platformData.projectRoot, projectData.projectName + IosProjectConstants.XcodeProjExtName); + } + public async verifyXcproj(opts: IVerifyXcprojOptions): Promise { const xcprojInfo = await this.getXcprojInfo(); if (xcprojInfo.shouldUseXcproj && !xcprojInfo.xcprojAvailable) { @@ -62,6 +68,17 @@ class XcprojService implements IXcprojService { return this.xcprojInfoCache; } + + public async checkIfXcodeprojIsRequired(): Promise { + const xcprojInfo = await this.getXcprojInfo(); + if (xcprojInfo.shouldUseXcproj && !xcprojInfo.xcprojAvailable) { + const errorMessage = `You are using CocoaPods version ${xcprojInfo.cocoapodVer} which does not support Xcode ${xcprojInfo.xcodeVersion.major}.${xcprojInfo.xcodeVersion.minor} yet.${EOL}${EOL}You can update your cocoapods by running $sudo gem install cocoapods from a terminal.${EOL}${EOL}In order for the NativeScript CLI to be able to work correctly with this setup you need to install xcproj command line tool and add it to your PATH. Xcproj can be installed with homebrew by running $ brew install xcproj from the terminal`; + + this.$errors.failWithoutHelp(errorMessage); + + return true; + } + } } $injector.register("xcprojService", XcprojService); diff --git a/lib/tools/node-modules/node-modules-dest-copy.ts b/lib/tools/node-modules/node-modules-dest-copy.ts index cbdfd51ae0..55bcaaedc4 100644 --- a/lib/tools/node-modules/node-modules-dest-copy.ts +++ b/lib/tools/node-modules/node-modules-dest-copy.ts @@ -107,16 +107,7 @@ export class NpmPluginPrepare { ) { } - protected async beforePrepare(dependencies: IDependencyData[], platform: string, projectData: IProjectData): Promise { - await this.$platformsData.getPlatformData(platform, projectData).platformProjectService.beforePrepareAllPlugins(projectData, dependencies); - } - protected async afterPrepare(dependencies: IDependencyData[], platform: string, projectData: IProjectData): Promise { - await this.$platformsData.getPlatformData(platform, projectData).platformProjectService.afterPrepareAllPlugins(projectData); - this.writePreparedDependencyInfo(dependencies, platform, projectData); - } - - private writePreparedDependencyInfo(dependencies: IDependencyData[], platform: string, projectData: IProjectData): void { const prepareData: IDictionary = {}; _.each(dependencies, d => { prepareData[d.name] = true; @@ -163,7 +154,8 @@ export class NpmPluginPrepare { return; } - await this.beforePrepare(dependencies, platform, projectData); + await this.$platformsData.getPlatformData(platform, projectData).platformProjectService.beforePrepareAllPlugins(projectData, dependencies); + for (const dependencyKey in dependencies) { const dependency = dependencies[dependencyKey]; const isPlugin = !!dependency.nativescript; diff --git a/test/cocoapods-service.ts b/test/cocoapods-service.ts index 92fe7f314a..df8a0d64c5 100644 --- a/test/cocoapods-service.ts +++ b/test/cocoapods-service.ts @@ -3,6 +3,7 @@ import { assert } from "chai"; import { CocoaPodsService } from "../lib/services/cocoapods-service"; import { EOL } from "os"; import { LoggerStub, ErrorsStub } from "./stubs"; +import { XcconfigService } from "../lib/services/xcconfig-service"; interface IMergePodfileHooksTestCase { input: string; @@ -22,6 +23,7 @@ function createTestInjector(): IInjector { testInjector.register("xcprojService", {}); testInjector.register("logger", LoggerStub); testInjector.register("config", {}); + testInjector.register("xcconfigService", XcconfigService); return testInjector; } diff --git a/test/ios-project-service.ts b/test/ios-project-service.ts index 61bfcad100..4978372bb5 100644 --- a/test/ios-project-service.ts +++ b/test/ios-project-service.ts @@ -8,7 +8,7 @@ import * as HostInfoLib from "../lib/common/host-info"; import * as iOSProjectServiceLib from "../lib/services/ios-project-service"; import { IOSProjectService } from "../lib/services/ios-project-service"; import { IOSEntitlementsService } from "../lib/services/ios-entitlements-service"; -import { XCConfigService } from "../lib/services/xcconfig-service"; +import { XcconfigService } from "../lib/services/xcconfig-service"; import * as LoggerLib from "../lib/common/logger"; import * as OptionsLib from "../lib/options"; import * as yok from "../lib/common/yok"; @@ -63,7 +63,7 @@ function createTestInjector(projectPath: string, projectName: string, xcode?: IX testInjector.register("cocoapodsService", CocoaPodsService); testInjector.register("iOSProjectService", iOSProjectServiceLib.IOSProjectService); testInjector.register("iOSProvisionService", {}); - testInjector.register("xCConfigService", XCConfigService); + testInjector.register("xcconfigService", XcconfigService); testInjector.register("iOSEntitlementsService", IOSEntitlementsService); testInjector.register("logger", LoggerLib.Logger); testInjector.register("options", OptionsLib.Options); @@ -106,7 +106,11 @@ function createTestInjector(projectPath: string, projectName: string, xcode?: IX return { shouldUseXcproj: false }; - } + }, + getXcodeprojPath: (projData: IProjectData, platformData: IPlatformData) => { + return path.join(platformData.projectRoot, projData.projectName + ".xcodeproj"); + }, + checkIfXcodeprojIsRequired: () => ({}) }); testInjector.register("iosDeviceOperations", {}); testInjector.register("pluginVariablesService", PluginVariablesService); @@ -129,7 +133,7 @@ function createTestInjector(projectPath: string, projectName: string, xcode?: IX testInjector.register("packageManager", PackageManager); testInjector.register("npm", NodePackageManager); testInjector.register("yarn", YarnPackageManager); - testInjector.register("xCConfigService", XCConfigService); + testInjector.register("xcconfigService", XcconfigService); testInjector.register("settingsService", SettingsService); testInjector.register("httpClient", {}); testInjector.register("platformEnvironmentRequirements", {}); @@ -578,11 +582,12 @@ describe("Source code support", () => { const platformsFolderPath = path.join(projectPath, "platforms", "ios"); fs.createDirectory(platformsFolderPath); - const iOSProjectService = testInjector.resolve("iOSProjectService"); - - iOSProjectService.getXcodeprojPath = () => { + const xcprojService = testInjector.resolve("xcprojService"); + xcprojService.getXcodeprojPath = () => { return path.join(__dirname, "files"); }; + + const iOSProjectService = testInjector.resolve("iOSProjectService"); let pbxProj: any; iOSProjectService.savePbxProj = (project: any): Promise => { pbxProj = project; @@ -636,9 +641,11 @@ describe("Source code support", () => { }; }); - iOSProjectService.getXcodeprojPath = () => { + const xcprojService = testInjector.resolve("xcprojService"); + xcprojService.getXcodeprojPath = () => { return path.join(__dirname, "files"); }; + let pbxProj: any; iOSProjectService.savePbxProj = (project: any): Promise => { pbxProj = project; @@ -993,6 +1000,7 @@ describe("iOS Project Service Signing", () => { }; const changes = {}; await iOSProjectService.checkForChanges(changes, { bundle: false, release: false, provision: "NativeScriptDev", teamId: undefined, useHotModuleReload: false }, projectData); + console.log("CHANGES !!!! ", changes); assert.isFalse(!!changes.signingChanged); }); }); @@ -1083,7 +1091,7 @@ describe("Merge Project XCConfig files", () => { return; } const assertPropertyValues = (expected: any, xcconfigPath: string, injector: IInjector) => { - const service = injector.resolve('xCConfigService'); + const service = injector.resolve('xcconfigService'); _.forOwn(expected, (value, key) => { const actual = service.readPropertyValue(xcconfigPath, key); assert.equal(actual, value); @@ -1092,6 +1100,7 @@ describe("Merge Project XCConfig files", () => { let projectName: string; let projectPath: string; + let projectRoot: string; let testInjector: IInjector; let iOSProjectService: IOSProjectService; let projectData: IProjectData; @@ -1099,6 +1108,7 @@ describe("Merge Project XCConfig files", () => { let appResourcesXcconfigPath: string; let appResourceXCConfigContent: string; let iOSEntitlementsService: IOSEntitlementsService; + let xcconfigService: IXcconfigService; beforeEach(() => { projectName = "projectDirectory"; @@ -1125,6 +1135,8 @@ describe("Merge Project XCConfig files", () => { }; fs = testInjector.resolve("fs"); fs.writeJson(path.join(projectPath, "package.json"), testPackageJson); + xcconfigService = testInjector.resolve("xcconfigService"); + projectRoot = iOSProjectService.getPlatformData(projectData).projectRoot; }); it("Uses the build.xcconfig file content from App_Resources", async () => { @@ -1133,10 +1145,9 @@ describe("Merge Project XCConfig files", () => { // run merge for all release: debug|release for (const release in [true, false]) { - await (iOSProjectService).mergeProjectXcconfigFiles(release, projectData); + await (iOSProjectService).mergeProjectXcconfigFiles(projectData, { release }); - const destinationFilePath = release ? (iOSProjectService).getPluginsReleaseXcconfigFilePath(projectData) - : (iOSProjectService).getPluginsDebugXcconfigFilePath(projectData); + const destinationFilePath = xcconfigService.getPluginsXcconfigFilePath(projectRoot, { release: !!release }); assert.isTrue(fs.exists(destinationFilePath), 'Target build xcconfig is missing for release: ' + release); const expected = { @@ -1160,10 +1171,9 @@ describe("Merge Project XCConfig files", () => { return realExistsFunction(filePath); }; - await (iOSProjectService).mergeProjectXcconfigFiles(release, projectData); + await (iOSProjectService).mergeProjectXcconfigFiles(projectData, { release }); - const destinationFilePath = release ? (iOSProjectService).getPluginsReleaseXcconfigFilePath(projectData) - : (iOSProjectService).getPluginsDebugXcconfigFilePath(projectData); + const destinationFilePath = xcconfigService.getPluginsXcconfigFilePath(projectRoot, { release: !!release }); assert.isTrue(fs.exists(destinationFilePath), 'Target build xcconfig is missing for release: ' + release); const expected = { @@ -1181,10 +1191,9 @@ describe("Merge Project XCConfig files", () => { // run merge for all release: debug|release for (const release in [true, false]) { - await (iOSProjectService).mergeProjectXcconfigFiles(release, projectData); + await (iOSProjectService).mergeProjectXcconfigFiles(projectData, { release }); - const destinationFilePath = release ? (iOSProjectService).getPluginsReleaseXcconfigFilePath(projectData) - : (iOSProjectService).getPluginsDebugXcconfigFilePath(projectData); + const destinationFilePath = xcconfigService.getPluginsXcconfigFilePath(projectRoot, { release: !!release }); assert.isTrue(fs.exists(destinationFilePath), 'Target build xcconfig is missing for release: ' + release); const expected = { @@ -1200,10 +1209,9 @@ describe("Merge Project XCConfig files", () => { it("creates empty plugins-.xcconfig in case there are no build.xcconfig in App_Resources and in plugins", async () => { // run merge for all release: debug|release for (const release in [true, false]) { - await (iOSProjectService).mergeProjectXcconfigFiles(release, projectData); + await (iOSProjectService).mergeProjectXcconfigFiles(projectData, { release }); - const destinationFilePath = release ? (iOSProjectService).getPluginsReleaseXcconfigFilePath(projectData) - : (iOSProjectService).getPluginsDebugXcconfigFilePath(projectData); + const destinationFilePath = xcconfigService.getPluginsXcconfigFilePath(projectRoot, { release: !!release }); assert.isTrue(fs.exists(destinationFilePath), 'Target build xcconfig is missing for release: ' + release); const content = fs.readFile(destinationFilePath).toString(); @@ -1244,8 +1252,8 @@ describe("buildProject", () => { devicesService.initialize = () => ({}); devicesService.getDeviceInstances = () => data.devices || []; - const xCConfigService = testInjector.resolve("xCConfigService"); - xCConfigService.readPropertyValue = (projectDir: string, propertyName: string) => { + const xcconfigService = testInjector.resolve("xcconfigService"); + xcconfigService.readPropertyValue = (projectDir: string, propertyName: string) => { if (propertyName === "IPHONEOS_DEPLOYMENT_TARGET") { return data.deploymentTarget; } @@ -1366,3 +1374,27 @@ describe("buildProject", () => { executeTests(testCases, { buildForDevice: false }); }); }); + +describe("handleNativeDependenciesChange", () => { + it("ensure the correct order of pod install and merging pod's xcconfig file", async () => { + const executedCocoapodsMethods: string[] = []; + const projectPodfilePath = "my/test/project/platforms/ios/Podfile"; + + const testInjector = createTestInjector("myTestProjectPath", "myTestProjectName"); + const iOSProjectService = testInjector.resolve("iOSProjectService"); + const projectData = testInjector.resolve("projectData"); + + const cocoapodsService = testInjector.resolve("cocoapodsService"); + cocoapodsService.executePodInstall = async () => executedCocoapodsMethods.push("podInstall"); + cocoapodsService.mergePodXcconfigFile = async () => executedCocoapodsMethods.push("podMerge"); + cocoapodsService.applyPodfileFromAppResources = async () => ({}); + cocoapodsService.getProjectPodfilePath = () => projectPodfilePath; + + const fs = testInjector.resolve("fs"); + fs.exists = (filePath: string) => filePath === projectPodfilePath; + + await iOSProjectService.handleNativeDependenciesChange(projectData); + + assert.deepEqual(executedCocoapodsMethods, ["podInstall", "podMerge"]); + }); +}); diff --git a/test/npm-support.ts b/test/npm-support.ts index 8039530151..bde20bc4f8 100644 --- a/test/npm-support.ts +++ b/test/npm-support.ts @@ -182,8 +182,8 @@ async function setupProject(dependencies?: any): Promise { platformProjectService: { prepareProject: (): any => null, prepareAppResources: (): any => null, - afterPrepareAllPlugins: () => Promise.resolve(), beforePrepareAllPlugins: () => Promise.resolve(), + handleNativeDependenciesChange: () => Promise.resolve(), getAppResourcesDestinationDirectoryPath: () => path.join(androidFolderPath, "src", "main", "res"), processConfigurationFilesFromAppResources: () => Promise.resolve(), ensureConfigurationFileInAppResources: (): any => null, diff --git a/test/platform-service.ts b/test/platform-service.ts index 16881a7d57..19c9fb8c53 100644 --- a/test/platform-service.ts +++ b/test/platform-service.ts @@ -496,6 +496,7 @@ describe('Platform Service Tests', () => { } }, processConfigurationFilesFromAppResources: () => Promise.resolve(), + handleNativeDependenciesChange: () => Promise.resolve(), ensureConfigurationFileInAppResources: (): any => null, interpolateConfigurationFile: (): void => undefined, isPlatformPrepared: (projectRoot: string) => false, @@ -931,6 +932,7 @@ describe('Platform Service Tests', () => { afterCreateProject: (projectRoot: string): any => null, getAppResourcesDestinationDirectoryPath: () => testDirData.appResourcesFolderPath, processConfigurationFilesFromAppResources: () => Promise.resolve(), + handleNativeDependenciesChange: () => Promise.resolve(), ensureConfigurationFileInAppResources: (): any => null, interpolateConfigurationFile: (): void => undefined, isPlatformPrepared: (projectRoot: string) => false, diff --git a/test/plugin-prepare.ts b/test/plugin-prepare.ts index 27a93cd10a..00dd7b04b1 100644 --- a/test/plugin-prepare.ts +++ b/test/plugin-prepare.ts @@ -7,22 +7,26 @@ class TestNpmPluginPrepare extends NpmPluginPrepare { public preparedDependencies: IDictionary = {}; constructor(private previouslyPrepared: IDictionary) { - super(null, null, null, null); + super(null, null, { + getPlatformData: () => { + return { + platformProjectService: { + beforePrepareAllPlugins: () => Promise.resolve() + } + }; + } + }, null); } protected getPreviouslyPreparedDependencies(platform: string): IDictionary { return this.previouslyPrepared; } - protected async beforePrepare(dependencies: IDependencyData[], platform: string): Promise { + protected async afterPrepare(dependencies: IDependencyData[], platform: string): Promise { _.each(dependencies, d => { this.preparedDependencies[d.name] = true; }); } - - protected async afterPrepare(dependencies: IDependencyData[], platform: string): Promise { - // DO NOTHING - } } describe("Plugin preparation", () => { diff --git a/test/stubs.ts b/test/stubs.ts index bbf28e6eeb..752e7125d4 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -445,7 +445,7 @@ export class PlatformProjectServiceStub extends EventEmitter implements IPlatfor async removePluginNativeCode(pluginData: IPluginData): Promise { } - async afterPrepareAllPlugins(): Promise { + async handleNativeDependenciesChange(): Promise { return Promise.resolve(); } async beforePrepareAllPlugins(): Promise { diff --git a/test/xcconfig-service.ts b/test/xcconfig-service.ts index c60c5998e4..9305f882ab 100644 --- a/test/xcconfig-service.ts +++ b/test/xcconfig-service.ts @@ -1,6 +1,6 @@ import temp = require("temp"); import { assert } from "chai"; -import { XCConfigService } from "../lib/services/xcconfig-service"; +import { XcconfigService } from "../lib/services/xcconfig-service"; import * as yok from "../lib/common/yok"; // start tracking temporary folders/files @@ -14,8 +14,10 @@ describe("XCConfig Service Tests", () => { return true; } }); + testInjector.register('childProcess', {}); + testInjector.register('xcprojService', {}); - testInjector.register('xCConfigService', XCConfigService); + testInjector.register('xcconfigService', XcconfigService); return testInjector; }; @@ -28,8 +30,8 @@ describe("XCConfig Service Tests", () => { }); }; - function getXCConfigService(injector: IInjector): XCConfigService { - return injector.resolve("xCConfigService"); + function getXCConfigService(injector: IInjector): IXcconfigService { + return injector.resolve("xcconfigService"); } function getFileSystemMock(injector: IInjector): any {