From 3786e42e7fab8b0a6a4c807760785954d6c6a0c3 Mon Sep 17 00:00:00 2001 From: fatme Date: Mon, 22 Apr 2019 01:05:42 +0300 Subject: [PATCH 1/3] refactor: refactor xcodebuild related part of ios-project-service --- lib/bootstrap.ts | 5 + lib/commands/appstore-upload.ts | 39 +- lib/constants.ts | 3 + lib/declarations.d.ts | 4 +- lib/definitions/project.d.ts | 6 +- lib/definitions/xcode.d.ts | 157 +++--- lib/services/ios-project-service.ts | 476 +----------------- .../ios/export-options-plist-service.ts | 98 ++++ lib/services/ios/ios-signing-service.ts | 194 +++++++ lib/services/ios/xcodebuild-args-service.ts | 102 ++++ .../ios/xcodebuild-command-service.ts | 29 ++ lib/services/ios/xcodebuild-service.ts | 58 +++ lib/services/xcproj-service.ts | 4 +- test/ios-project-service.ts | 432 +--------------- 14 files changed, 640 insertions(+), 967 deletions(-) create mode 100644 lib/services/ios/export-options-plist-service.ts create mode 100644 lib/services/ios/ios-signing-service.ts create mode 100644 lib/services/ios/xcodebuild-args-service.ts create mode 100644 lib/services/ios/xcodebuild-command-service.ts create mode 100644 lib/services/ios/xcodebuild-service.ts diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 486751b637..a8eda19224 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -19,6 +19,11 @@ $injector.require("iOSExtensionsService", "./services/ios-extensions-service"); $injector.require("iOSProjectService", "./services/ios-project-service"); $injector.require("iOSProvisionService", "./services/ios-provision-service"); $injector.require("xcconfigService", "./services/xcconfig-service"); +$injector.require("iOSSigningService", "./services/ios/ios-signing-service"); +$injector.require("xcodebuildArgsService", "./services/ios/xcodebuild-args-service"); +$injector.require("xcodebuildCommandService", "./services/ios/xcodebuild-command-service"); +$injector.require("xcodebuildService", "./services/ios/xcodebuild-service"); +$injector.require("exportOptionsPlistService", "./services/ios/export-options-plist-service"); $injector.require("cocoapodsService", "./services/cocoapods-service"); $injector.require("cocoaPodsPlatformManager", "./services/cocoapods-platform-manager"); diff --git a/lib/commands/appstore-upload.ts b/lib/commands/appstore-upload.ts index 0ab77c5afc..c1055a2f92 100644 --- a/lib/commands/appstore-upload.ts +++ b/lib/commands/appstore-upload.ts @@ -1,6 +1,5 @@ -import { StringCommandParameter } from "../common/command-params"; import * as path from "path"; -import { IOSProjectService } from "../services/ios-project-service"; +import { StringCommandParameter } from "../common/command-params"; export class PublishIOS implements ICommand { public allowedParameters: ICommandParameter[] = [new StringCommandParameter(this.$injector), new StringCommandParameter(this.$injector), @@ -13,7 +12,8 @@ export class PublishIOS implements ICommand { private $projectData: IProjectData, private $options: IOptions, private $prompter: IPrompter, - private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants) { + private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + private $xcodebuildService: IXcodebuildService) { this.$projectData.initializeProjectData(); } @@ -32,7 +32,6 @@ export class PublishIOS implements ICommand { let password = args[1]; const mobileProvisionIdentifier = args[2]; const codeSignIdentity = args[3]; - const teamID = this.$options.teamId; let ipaFilePath = this.$options.ipa ? path.resolve(this.$options.ipa) : null; if (!username) { @@ -68,35 +67,31 @@ export class PublishIOS implements ICommand { config: this.$options, env: this.$options.env }; + const buildConfig: IBuildConfig = { + projectDir: this.$options.path, + release: this.$options.release, + device: this.$options.device, + provision: this.$options.provision, + teamId: this.$options.teamId, + buildForDevice: true, + iCloudContainerEnvironment: this.$options.iCloudContainerEnvironment, + mobileProvisionIdentifier, + codeSignIdentity + }; if (mobileProvisionIdentifier || codeSignIdentity) { - const iOSBuildConfig: IBuildConfig = { - projectDir: this.$options.path, - release: this.$options.release, - device: this.$options.device, - provision: this.$options.provision, - teamId: this.$options.teamId, - buildForDevice: true, - iCloudContainerEnvironment: this.$options.iCloudContainerEnvironment, - mobileProvisionIdentifier, - codeSignIdentity - }; this.$logger.info("Building .ipa with the selected mobile provision and/or certificate."); // This is not very correct as if we build multiple targets we will try to sign all of them using the signing identity here. await this.$platformService.preparePlatform(platformInfo); - await this.$platformService.buildPlatform(platform, iOSBuildConfig, this.$projectData); - ipaFilePath = this.$platformService.lastOutputPath(platform, iOSBuildConfig, this.$projectData); + await this.$platformService.buildPlatform(platform, buildConfig, this.$projectData); + ipaFilePath = this.$platformService.lastOutputPath(platform, buildConfig, this.$projectData); } else { this.$logger.info("No .ipa, mobile provision or certificate set. Perfect! Now we'll build .xcarchive and let Xcode pick the distribution certificate and provisioning profile for you when exporting .ipa for AppStore submission."); await this.$platformService.preparePlatform(platformInfo); const platformData = this.$platformsData.getPlatformData(platform, this.$projectData); - const iOSProjectService = platformData.platformProjectService; - - const archivePath = await iOSProjectService.archive(this.$projectData); - this.$logger.info("Archive at: " + archivePath); - const exportPath = await iOSProjectService.exportArchive(this.$projectData, { archivePath, teamID, provision: mobileProvisionIdentifier || this.$options.provision }); + const exportPath = await this.$xcodebuildService.buildForAppStore(platformData, this.$projectData, buildConfig); this.$logger.info("Export at: " + exportPath); ipaFilePath = exportPath; diff --git a/lib/constants.ts b/lib/constants.ts index a1a8961018..ccd1ae5053 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -110,6 +110,9 @@ class ItunesConnectApplicationTypesClass implements IiTunesConnectApplicationTyp public Mac = "Mac OS X App"; } +export const iOSAppResourcesFolderName = "iOS"; +export const androidAppResourcesFolderName = "Android"; + export const ItunesConnectApplicationTypes = new ItunesConnectApplicationTypesClass(); export const VUE_NAME = "vue"; export const ANGULAR_NAME = "angular"; diff --git a/lib/declarations.d.ts b/lib/declarations.d.ts index 0298f9ea02..1be467010f 100644 --- a/lib/declarations.d.ts +++ b/lib/declarations.d.ts @@ -841,10 +841,10 @@ interface IXcprojService { /** * Returns the path to the xcodeproj file * @param projectData Information about the project. - * @param platformData Information about the platform. + * @param projectRoot The root folder of native project. * @return {string} The full path to the xcodeproj */ - getXcodeprojPath(projectData: IProjectData, platformData: IPlatformData): string; + getXcodeprojPath(projectData: IProjectData, projectRoot: string): 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. diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index 44c3a9df40..f68e51b970 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -446,7 +446,7 @@ interface IPlatformProjectService extends NodeJS.EventEmitter, IPlatformProjectS * @param {IPlatformData} platformData The data for the specified platform. * @returns {void} */ - stopServices(platformData: IPlatformData): Promise; + stopServices?(projectRoot: string): Promise; /** * Removes build artifacts specific to the platform @@ -454,7 +454,7 @@ interface IPlatformProjectService extends NodeJS.EventEmitter, IPlatformProjectS * @param {IProjectData} projectData DTO with information about the project. * @returns {void} */ - cleanProject(projectRoot: string, projectData: IProjectData): Promise + cleanProject?(projectRoot: string, projectData: IProjectData): Promise /** * Check the current state of the project, and validate against the options. @@ -466,7 +466,7 @@ interface IPlatformProjectService extends NodeJS.EventEmitter, IPlatformProjectS * Get the deployment target's version * Currently implemented only for iOS -> returns the value of IPHONEOS_DEPLOYMENT_TARGET property from xcconfig file */ - getDeploymentTarget(projectData: IProjectData): any; + getDeploymentTarget?(projectData: IProjectData): any; } interface IValidatePlatformOutput { diff --git a/lib/definitions/xcode.d.ts b/lib/definitions/xcode.d.ts index 065fbe2067..d1d524d45e 100644 --- a/lib/definitions/xcode.d.ts +++ b/lib/definitions/xcode.d.ts @@ -1,61 +1,100 @@ declare module "nativescript-dev-xcode" { - interface Options { - [key: string]: any; - - customFramework?: boolean; - embed?: boolean; - relativePath?: string; - } - - class project { - constructor(filename: string); - - parse(callback: () => void): void; - parseSync(): void; - - writeSync(options: any): string; - - addFramework(filepath: string, options?: Options): void; - removeFramework(filePath: string, options?: Options): void; - - addPbxGroup(filePathsArray: any[], name: string, path: string, sourceTree: string): void; - - removePbxGroup(groupName: string, path: string): void; - - addToHeaderSearchPaths(options?: Options): void; - removeFromHeaderSearchPaths(options?: Options): void; - updateBuildProperty(key: string, value: any): void; - - pbxXCBuildConfigurationSection(): any; - - addTarget(targetName: string, targetType: string, targetPath?: string): target; - addBuildPhase(filePathsArray: string[], - buildPhaseType: string, - comment: string, - target?: string, - optionsOrFolderType?: Object|string, - subfolderPath?: string - ): any; - addToBuildSettings(buildSetting: string, value: any, targetUuid?: string): void; - addPbxGroup( - filePathsArray: string[], - name: string, - path: string, - sourceTree: string, - opt: {filesRelativeToProject?: boolean, target?: string, uuid?: string, isMain?: boolean } - ): group; - addBuildProperty(prop: string, value: any, build_name?: string, productName?: string): void; - addToHeaderSearchPaths(file: string|Object, productName?: string): void; - removeTargetsByProductType(targetType: string): void - } - - class target { - uuid: string; - pbxNativeTarget: {productName: string} - } - - class group { - uuid: string; - pbxGroup: Object; - } + interface Options { + [key: string]: any; + + customFramework?: boolean; + embed?: boolean; + relativePath?: string; + } + + class project { + constructor(filename: string); + + parse(callback: () => void): void; + parseSync(): void; + + writeSync(options: any): string; + + addFramework(filepath: string, options?: Options): void; + removeFramework(filePath: string, options?: Options): void; + + addPbxGroup(filePathsArray: any[], name: string, path: string, sourceTree: string): void; + + removePbxGroup(groupName: string, path: string): void; + + addToHeaderSearchPaths(options?: Options): void; + removeFromHeaderSearchPaths(options?: Options): void; + updateBuildProperty(key: string, value: any): void; + + pbxXCBuildConfigurationSection(): any; + + addTarget(targetName: string, targetType: string, targetPath?: string): target; + addBuildPhase(filePathsArray: string[], + buildPhaseType: string, + comment: string, + target?: string, + optionsOrFolderType?: Object|string, + subfolderPath?: string + ): any; + addToBuildSettings(buildSetting: string, value: any, targetUuid?: string): void; + addPbxGroup( + filePathsArray: string[], + name: string, + path: string, + sourceTree: string, + opt: {filesRelativeToProject?: boolean, target?: string, uuid?: string, isMain?: boolean } + ): group; + addBuildProperty(prop: string, value: any, build_name?: string, productName?: string): void; + addToHeaderSearchPaths(file: string|Object, productName?: string): void; + removeTargetsByProductType(targetType: string): void + } + + class target { + uuid: string; + pbxNativeTarget: {productName: string} + } + + class group { + uuid: string; + pbxGroup: Object; + } +} + +interface IiOSSigningService { + setupSigningForDevice(projectRoot: string, projectData: IProjectData, buildConfig: IiOSBuildConfig): Promise; + setupSigningFromTeam(projectRoot: string, projectData: IProjectData, teamId: string): Promise; + setupSigningFromProvision(projectRoot: string, projectData: IProjectData, provision?: string, mobileProvisionData?: any): Promise; +} + +interface IXcodebuildService { + buildForSimulator(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise; + buildForDevice(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise; + buildForAppStore(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise; +} + +interface IXcodebuildArgsService { + getBuildForSimulatorArgs(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise; + getBuildForDeviceArgs(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise; +} + +interface IXcodebuildCommandService { + executeCommand(args: string[], options: IXcodebuildCommandOptions): Promise; +} + +interface IXcodebuildCommandOptions { + message?: string; + cwd: string; + stdio?: string; + spawnOptions?: any; +} + +interface IExportOptionsPlistService { + createDevelopmentExportOptionsPlist(archivePath: string, projectData: IProjectData, buildConfig: IBuildConfig): IExportOptionsPlistOutput; + createDistributionExportOptionsPlist(projectRoot: string, projectData: IProjectData, buildConfig: IBuildConfig): IExportOptionsPlistOutput; +} + +interface IExportOptionsPlistOutput { + exportFileDir: string; + exportFilePath: string; + exportOptionsPlistFilePath: string; } \ No newline at end of file diff --git a/lib/services/ios-project-service.ts b/lib/services/ios-project-service.ts index 0dec1a1390..6ddf99e502 100644 --- a/lib/services/ios-project-service.ts +++ b/lib/services/ios-project-service.ts @@ -1,6 +1,5 @@ import * as path from "path"; import * as shell from "shelljs"; -import * as semver from "semver"; import * as constants from "../constants"; import { Configurations } from "../common/constants"; import * as helpers from "../common/helpers"; @@ -12,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 * as mobileProvisionFinder from "ios-mobileprovision-finder"; import { BUILD_XCCONFIG_FILE_NAME, IosProjectConstants } from "../constants"; interface INativeSourceCodeGroup { @@ -38,19 +36,18 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ private $logger: ILogger, private $injector: IInjector, $projectDataService: IProjectDataService, - private $prompter: IPrompter, private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, - private $devicesService: Mobile.IDevicesService, - private $mobileHelper: Mobile.IMobileHelper, private $hostInfo: IHostInfo, private $xcprojService: IXcprojService, private $iOSProvisionService: IOSProvisionService, + private $iOSSigningService: IiOSSigningService, private $pbxprojDomXcode: IPbxprojDomXcode, private $xcode: IXcode, private $iOSEntitlementsService: IOSEntitlementsService, private $platformEnvironmentRequirements: IPlatformEnvironmentRequirements, private $plistParser: IPlistParser, private $xcconfigService: IXcconfigService, + private $xcodebuildService: IXcodebuildService, private $iOSExtensionsService: IIOSExtensionsService) { super($fs, $projectDataService); } @@ -194,376 +191,29 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ path.join(projectRoot, projectData.projectName)); } - /** - * Archive the Xcode project to .xcarchive. - * Returns the path to the .xcarchive. - */ - public async archive(projectData: IProjectData, buildConfig?: IBuildConfig, options?: { archivePath?: string, additionalArgs?: string[] }): Promise { - const platformData = this.getPlatformData(projectData); - const projectRoot = this.getPlatformData(projectData).projectRoot; - const archivePath = options && options.archivePath ? path.resolve(options.archivePath) : path.join(platformData.getBuildOutputPath(buildConfig), projectData.projectName + ".xcarchive"); - let args = ["archive", "-archivePath", archivePath, "-configuration", - getConfigurationName(!buildConfig || buildConfig.release)] - .concat(this.xcbuildProjectArgs(projectRoot, projectData, "scheme")); - - if (options && options.additionalArgs) { - args = args.concat(options.additionalArgs); - } - - await this.xcodebuild(args, projectRoot, buildConfig && buildConfig.buildOutputStdio); - return archivePath; - } - - /** - * Exports .xcarchive for AppStore distribution. - */ - public async exportArchive(projectData: IProjectData, options: { archivePath: string, exportDir?: string, teamID?: string, provision?: string }): Promise { - const projectRoot = this.getPlatformData(projectData).projectRoot; - const archivePath = options.archivePath; - // The xcodebuild exportPath expects directory and writes the .ipa at that directory. - const exportPath = path.resolve(options.exportDir || path.join(projectRoot, "/build/archive")); - const exportFile = path.join(exportPath, projectData.projectName + ".ipa"); - - // These are the options that you can set in the Xcode UI when exporting for AppStore deployment. - let plistTemplate = ` - - - -`; - if (options && options.teamID) { - plistTemplate += ` teamID - ${options.teamID} -`; - } - if (options && options.provision) { - plistTemplate += ` provisioningProfiles - - ${projectData.projectIdentifiers.ios} - ${options.provision} - `; - } - plistTemplate += ` method - app-store - uploadBitcode - - compileBitcode - - uploadSymbols - - -`; - - // Save the options... - temp.track(); - const exportOptionsPlist = temp.path({ prefix: "export-", suffix: ".plist" }); - this.$fs.writeFile(exportOptionsPlist, plistTemplate); - - await this.xcodebuild( - [ - "-exportArchive", - "-archivePath", archivePath, - "-exportPath", exportPath, - "-exportOptionsPlist", exportOptionsPlist - ], - projectRoot); - return exportFile; - } - - private iCloudContainerEnvironment(buildConfig: IBuildConfig): string { - return buildConfig && buildConfig.iCloudContainerEnvironment ? buildConfig.iCloudContainerEnvironment : null; - } - - /** - * Exports .xcarchive for a development device. - */ - private async exportDevelopmentArchive(projectData: IProjectData, buildConfig: IBuildConfig, options: { archivePath: string, provision?: string }): Promise { - const platformData = this.getPlatformData(projectData); - const projectRoot = platformData.projectRoot; - const archivePath = options.archivePath; - const exportOptionsMethod = await this.getExportOptionsMethod(projectData, archivePath); - const iCloudContainerEnvironment = this.iCloudContainerEnvironment(buildConfig); - let plistTemplate = ` - - - - method - ${exportOptionsMethod}`; - if (options && options.provision) { - plistTemplate += ` provisioningProfiles - - ${projectData.projectIdentifiers.ios} - ${options.provision} -`; - } - plistTemplate += ` - uploadBitcode - - compileBitcode - `; - if (iCloudContainerEnvironment) { - plistTemplate += ` - iCloudContainerEnvironment - ${iCloudContainerEnvironment}`; - } - plistTemplate += ` - -`; - - // Save the options... - temp.track(); - const exportOptionsPlist = temp.path({ prefix: "export-", suffix: ".plist" }); - this.$fs.writeFile(exportOptionsPlist, plistTemplate); - - // The xcodebuild exportPath expects directory and writes the .ipa at that directory. - const exportPath = path.resolve(path.dirname(archivePath)); - const exportFile = path.join(exportPath, projectData.projectName + ".ipa"); - - await this.xcodebuild( - [ - "-exportArchive", - "-archivePath", archivePath, - "-exportPath", exportPath, - "-exportOptionsPlist", exportOptionsPlist - ], - projectRoot, buildConfig.buildOutputStdio); - return exportFile; - } - - private xcbuildProjectArgs(projectRoot: string, projectData: IProjectData, product?: "scheme" | "target"): string[] { - const xcworkspacePath = path.join(projectRoot, projectData.projectName + ".xcworkspace"); - if (this.$fs.exists(xcworkspacePath)) { - return ["-workspace", xcworkspacePath, product ? "-" + product : "-scheme", projectData.projectName]; - } else { - const xcodeprojPath = path.join(projectRoot, projectData.projectName + ".xcodeproj"); - return ["-project", xcodeprojPath, product ? "-" + product : "-target", projectData.projectName]; - } - } - public async buildProject(projectRoot: string, projectData: IProjectData, buildConfig: IBuildConfig): Promise { - const basicArgs = [ - 'SHARED_PRECOMPS_DIR=' + path.join(projectRoot, 'build', 'sharedpch') - ]; - + const platformData = this.getPlatformData(projectData); const handler = (data: any) => { this.emit(constants.BUILD_OUTPUT_EVENT_NAME, data); }; if (buildConfig.buildForDevice) { + await this.$iOSSigningService.setupSigningForDevice(projectRoot, projectData, buildConfig); await attachAwaitDetach(constants.BUILD_OUTPUT_EVENT_NAME, this.$childProcess, handler, - this.buildForDevice(projectRoot, basicArgs, buildConfig, projectData)); + this.$xcodebuildService.buildForDevice(platformData, projectData, buildConfig)); } else { await attachAwaitDetach(constants.BUILD_OUTPUT_EVENT_NAME, this.$childProcess, handler, - this.buildForSimulator(projectRoot, basicArgs, projectData, buildConfig)); + this.$xcodebuildService.buildForSimulator(platformData, projectData, buildConfig)); } + // TODO: Check if we need to validate the identifier here this.validateApplicationIdentifier(projectData); } - private async buildForDevice(projectRoot: string, args: string[], buildConfig: IBuildConfig, projectData: IProjectData): Promise { - if (!buildConfig.release && !buildConfig.architectures) { - await this.$devicesService.initialize({ - platform: this.$devicePlatformsConstants.iOS.toLowerCase(), deviceId: buildConfig.device, - skipEmulatorStart: true - }); - const instances = this.$devicesService.getDeviceInstances(); - const devicesArchitectures = _(instances) - .filter(d => this.$mobileHelper.isiOSPlatform(d.deviceInfo.platform) && !!d.deviceInfo.activeArchitecture) - .map(d => d.deviceInfo.activeArchitecture) - .uniq() - .value(); - if (devicesArchitectures.length > 0) { - const architectures = this.getBuildArchitectures(projectData, buildConfig, devicesArchitectures); - if (devicesArchitectures.length > 1) { - architectures.push('ONLY_ACTIVE_ARCH=NO'); - } - buildConfig.architectures = architectures; - } - } - - args = args.concat((buildConfig && buildConfig.architectures) || this.getBuildArchitectures(projectData, buildConfig, ["armv7", "arm64"])); - - args = args.concat([ - "-sdk", DevicePlatformSdkName, - "BUILD_DIR=" + path.join(projectRoot, constants.BUILD_DIR) - ]); - - await this.setupSigningForDevice(projectRoot, buildConfig, projectData); - - await this.createIpa(projectRoot, projectData, buildConfig, args); - } - - private async xcodebuild(args: string[], cwd: string, stdio: any = "inherit"): Promise { - const localArgs = [...args]; - localArgs.push("-allowProvisioningUpdates"); - - if (this.$logger.getLevel() === "INFO") { - localArgs.push("-quiet"); - this.$logger.info("Xcode build..."); - } - - let commandResult; - try { - commandResult = await this.$childProcess.spawnFromEvent("xcodebuild", - localArgs, - "exit", - { stdio: stdio || "inherit", cwd }, - { emitOptions: { eventName: constants.BUILD_OUTPUT_EVENT_NAME }, throwError: true }); - } catch (err) { - this.$errors.failWithoutHelp(err.message); - } - - return commandResult; - } - - private getBuildArchitectures(projectData: IProjectData, buildConfig: IBuildConfig, architectures: string[]): string[] { - let result: string[] = []; - - const frameworkVersion = this.getFrameworkVersion(projectData); - if (semver.valid(frameworkVersion) && semver.lt(semver.coerce(frameworkVersion), "5.1.0")) { - const target = this.getDeploymentTarget(projectData); - if (target && target.major >= 11) { - // We need to strip 32bit architectures as of deployment target >= 11 it is not allowed to have such - architectures = _.filter(architectures, arch => { - const is64BitArchitecture = arch === "x86_64" || arch === "arm64"; - if (!is64BitArchitecture) { - this.$logger.warn(`The architecture ${arch} will be stripped as it is not supported for deployment target ${target.version}.`); - } - return is64BitArchitecture; - }); - } - result = [`ARCHS=${architectures.join(" ")}`, `VALID_ARCHS=${architectures.join(" ")}`]; - } - - return result; - } - - private async setupSigningFromTeam(projectRoot: string, projectData: IProjectData, teamId: string) { - const xcode = this.$pbxprojDomXcode.Xcode.open(this.getPbxProjPath(projectData)); - const signing = xcode.getSigning(projectData.projectName); - - let shouldUpdateXcode = false; - if (signing && signing.style === "Automatic") { - if (signing.team !== teamId) { - // Maybe the provided team is name such as "Telerik AD" and we need to convert it to CH******37 - const teamIdsForName = await this.$iOSProvisionService.getTeamIdsWithName(teamId); - if (!teamIdsForName.some(id => id === signing.team)) { - shouldUpdateXcode = true; - } - } - } else { - shouldUpdateXcode = true; - } - - if (shouldUpdateXcode) { - const teamIdsForName = await this.$iOSProvisionService.getTeamIdsWithName(teamId); - if (teamIdsForName.length > 0) { - this.$logger.trace(`Team id ${teamIdsForName[0]} will be used for team name "${teamId}".`); - teamId = teamIdsForName[0]; - } - - xcode.setAutomaticSigningStyle(projectData.projectName, teamId); - xcode.setAutomaticSigningStyleByTargetProductType("com.apple.product-type.app-extension", teamId); - xcode.save(); - - this.$logger.trace(`Set Automatic signing style and team id ${teamId}.`); - } else { - this.$logger.trace(`The specified ${teamId} is already set in the Xcode.`); - } - } - - private async setupSigningFromProvision(projectRoot: string, projectData: IProjectData, provision?: string, mobileProvisionData?: mobileProvisionFinder.provision.MobileProvision): Promise { - if (provision) { - const xcode = this.$pbxprojDomXcode.Xcode.open(this.getPbxProjPath(projectData)); - const signing = xcode.getSigning(projectData.projectName); - - let shouldUpdateXcode = false; - if (signing && signing.style === "Manual") { - for (const config in signing.configurations) { - const options = signing.configurations[config]; - if (options.name !== provision && options.uuid !== provision) { - shouldUpdateXcode = true; - break; - } - } - } else { - shouldUpdateXcode = true; - } - - if (shouldUpdateXcode) { - const pickStart = Date.now(); - const mobileprovision = mobileProvisionData || await this.$iOSProvisionService.pick(provision, projectData.projectIdentifiers.ios); - const pickEnd = Date.now(); - this.$logger.trace("Searched and " + (mobileprovision ? "found" : "failed to find ") + " matching provisioning profile. (" + (pickEnd - pickStart) + "ms.)"); - if (!mobileprovision) { - this.$errors.failWithoutHelp("Failed to find mobile provision with UUID or Name: " + provision); - } - const configuration = { - team: mobileprovision.TeamIdentifier && mobileprovision.TeamIdentifier.length > 0 ? mobileprovision.TeamIdentifier[0] : undefined, - uuid: mobileprovision.UUID, - name: mobileprovision.Name, - identity: mobileprovision.Type === "Development" ? "iPhone Developer" : "iPhone Distribution" - }; - xcode.setManualSigningStyle(projectData.projectName, configuration); - xcode.setManualSigningStyleByTargetProductType("com.apple.product-type.app-extension", configuration); - xcode.save(); - - // this.cache(uuid); - this.$logger.trace(`Set Manual signing style and provisioning profile: ${mobileprovision.Name} (${mobileprovision.UUID})`); - } else { - this.$logger.trace(`The specified provisioning profile is already set in the Xcode: ${provision}`); - } - } else { - // read uuid from Xcode and cache... - } - } - - private async setupSigningForDevice(projectRoot: string, buildConfig: IiOSBuildConfig, projectData: IProjectData): Promise { - const xcode = this.$pbxprojDomXcode.Xcode.open(this.getPbxProjPath(projectData)); - const signing = xcode.getSigning(projectData.projectName); - - const hasProvisioningProfileInXCConfig = - this.readXCConfigProvisioningProfileSpecifierForIPhoneOs(projectData) || - this.readXCConfigProvisioningProfileSpecifier(projectData) || - this.readXCConfigProvisioningProfileForIPhoneOs(projectData) || - this.readXCConfigProvisioningProfile(projectData); - - if (hasProvisioningProfileInXCConfig && (!signing || signing.style !== "Manual")) { - xcode.setManualSigningStyle(projectData.projectName); - xcode.save(); - } else if (!buildConfig.provision && !(signing && signing.style === "Manual" && !buildConfig.teamId)) { - const teamId = await this.getDevelopmentTeam(projectData, buildConfig.teamId); - await this.setupSigningFromTeam(projectRoot, projectData, teamId); - } - } - - private async buildForSimulator(projectRoot: string, args: string[], projectData: IProjectData, buildConfig?: IBuildConfig): Promise { - const architectures = this.getBuildArchitectures(projectData, buildConfig, ["i386", "x86_64"]); - - args = args - .concat(architectures) - .concat([ - "build", - "-configuration", getConfigurationName(buildConfig.release), - "-sdk", SimulatorPlatformSdkName, - "ONLY_ACTIVE_ARCH=NO", - "BUILD_DIR=" + path.join(projectRoot, constants.BUILD_DIR), - "CODE_SIGN_IDENTITY=", - ]) - .concat(this.xcbuildProjectArgs(projectRoot, projectData)); - - await this.xcodebuild(args, projectRoot, buildConfig.buildOutputStdio); - } - - private async createIpa(projectRoot: string, projectData: IProjectData, buildConfig: IBuildConfig, args: string[]): Promise { - const archivePath = await this.archive(projectData, buildConfig, { additionalArgs: args }); - const exportFileIpa = await this.exportDevelopmentArchive(projectData, buildConfig, { archivePath, provision: buildConfig.provision || buildConfig.mobileProvisionIdentifier }); - return exportFileIpa; - } - public isPlatformPrepared(projectRoot: string, projectData: IProjectData): boolean { return this.$fs.exists(path.join(projectRoot, projectData.projectName, constants.APP_FOLDER_NAME)); } @@ -632,10 +282,10 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ const provision = platformSpecificData && platformSpecificData.provision; const teamId = platformSpecificData && platformSpecificData.teamId; if (provision) { - await this.setupSigningFromProvision(projectRoot, projectData, provision, platformSpecificData.mobileProvisionData); + await this.$iOSSigningService.setupSigningFromProvision(projectRoot, projectData, provision, platformSpecificData.mobileProvisionData); } if (teamId) { - await this.setupSigningFromTeam(projectRoot, projectData, teamId); + await this.$iOSSigningService.setupSigningFromTeam(projectRoot, projectData, teamId); } const project = this.createPbxProj(projectData); @@ -671,7 +321,6 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ await this.prepareNativeSourceCode(constants.TNS_NATIVE_SOURCE_GROUP_NAME, resourcesNativeCodePath, projectData); } - } public prepareAppResources(appResourcesDirectoryPath: string, projectData: IProjectData): void { @@ -698,14 +347,6 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ return null; } - public async stopServices(): Promise { - return { stderr: "", stdout: "", exitCode: 0 }; - } - - public async cleanProject(projectRoot: string): Promise { - return Promise.resolve(); - } - 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); @@ -802,7 +443,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ } private getPbxProjPath(projectData: IProjectData): string { - return path.join(this.$xcprojService.getXcodeprojPath(projectData, this.getPlatformData(projectData)), "project.pbxproj"); + return path.join(this.$xcprojService.getXcodeprojPath(projectData, this.getPlatformData(projectData).projectRoot), "project.pbxproj"); } private createPbxProj(projectData: IProjectData): any { @@ -849,7 +490,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ const projectPodfilePath = this.$cocoapodsService.getProjectPodfilePath(platformData.projectRoot); if (this.$fs.exists(projectPodfilePath)) { - await this.$cocoapodsService.executePodInstall(platformData.projectRoot, this.$xcprojService.getXcodeprojPath(projectData, platformData)); + await this.$cocoapodsService.executePodInstall(platformData.projectRoot, this.$xcprojService.getXcodeprojPath(projectData, platformData.projectRoot)); // The `pod install` command adds a new target to the .pbxproject. This target adds additional build phases to Xcode project. // 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. @@ -861,6 +502,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ this.$iOSExtensionsService.removeExtensions({ pbxProjPath }); await this.addExtensions(projectData); } + public beforePrepareAllPlugins(): Promise { return Promise.resolve(); } @@ -912,13 +554,9 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ } } - public getDeploymentTarget(projectData: IProjectData): semver.SemVer { + public getDeploymentTarget(projectData: IProjectData): string { const target = this.$xcconfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "IPHONEOS_DEPLOYMENT_TARGET"); - if (!target) { - return null; - } - - return semver.coerce(target); + return target; } private getAllLibsForPluginWithFileExtension(pluginData: IPluginData, fileExtension: string): string[] { @@ -1130,80 +768,6 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ return buildXCConfig; } - private readTeamId(projectData: IProjectData): string { - let teamId = this.$xcconfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "DEVELOPMENT_TEAM"); - - const fileName = path.join(this.getPlatformData(projectData).projectRoot, "teamid"); - if (this.$fs.exists(fileName)) { - teamId = this.$fs.readText(fileName); - } - - return teamId; - } - - private readXCConfigProvisioningProfile(projectData: IProjectData): string { - 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*]"); - } - - private readXCConfigProvisioningProfileSpecifier(projectData: IProjectData): string { - 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*]"); - } - - private async getDevelopmentTeam(projectData: IProjectData, teamId?: string): Promise { - teamId = teamId || this.readTeamId(projectData); - - if (!teamId) { - const teams = await this.$iOSProvisionService.getDevelopmentTeams(); - this.$logger.warn("Xcode requires a team id to be specified when building for device."); - this.$logger.warn("You can specify the team id by setting the DEVELOPMENT_TEAM setting in build.xcconfig file located in App_Resources folder of your app, or by using the --teamId option when calling run, debug or livesync commands."); - if (teams.length === 1) { - teamId = teams[0].id; - this.$logger.warn("Found and using the following development team installed on your system: " + teams[0].name + " (" + teams[0].id + ")"); - } else if (teams.length > 0) { - if (!helpers.isInteractive()) { - this.$errors.failWithoutHelp(`Unable to determine default development team. Available development teams are: ${_.map(teams, team => team.id)}. Specify team in app/App_Resources/iOS/build.xcconfig file in the following way: DEVELOPMENT_TEAM = `); - } - - const choices: string[] = []; - for (const team of teams) { - choices.push(team.name + " (" + team.id + ")"); - } - const choice = await this.$prompter.promptForChoice('Found multiple development teams, select one:', choices); - teamId = teams[choices.indexOf(choice)].id; - - const choicesPersist = [ - "Yes, set the DEVELOPMENT_TEAM setting in build.xcconfig file.", - "Yes, persist the team id in platforms folder.", - "No, don't persist this setting." - ]; - const choicePersist = await this.$prompter.promptForChoice("Do you want to make teamId: " + teamId + " a persistent choice for your app?", choicesPersist); - switch (choicesPersist.indexOf(choicePersist)) { - case 0: - const xcconfigFile = path.join(projectData.appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName, BUILD_XCCONFIG_FILE_NAME); - this.$fs.appendFile(xcconfigFile, "\nDEVELOPMENT_TEAM = " + teamId + "\n"); - break; - case 1: - this.$fs.writeFile(path.join(this.getPlatformData(projectData).projectRoot, "teamid"), teamId); - break; - default: - break; - } - } - } - - this.$logger.trace(`Selected teamId is '${teamId}'.`); - - return teamId; - } - private validateApplicationIdentifier(projectData: IProjectData): void { const infoPlistPath = path.join(projectData.appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName, this.getPlatformData(projectData).configurationFileName); const mergedPlistPath = this.getPlatformData(projectData).configurationFilePath; @@ -1219,18 +783,6 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ this.$logger.warnWithLabel("The CFBundleIdentifier key inside the 'Info.plist' will be overriden by the 'id' inside 'package.json'."); } } - - private getExportOptionsMethod(projectData: IProjectData, archivePath: string): string { - const embeddedMobileProvisionPath = path.join(archivePath, 'Products', 'Applications', `${projectData.projectName}.app`, "embedded.mobileprovision"); - const provision = mobileProvisionFinder.provision.readFromFile(embeddedMobileProvisionPath); - - return { - "Development": "development", - "AdHoc": "ad-hoc", - "Distribution": "app-store", - "Enterprise": "enterprise" - }[provision.Type]; - } } $injector.register("iOSProjectService", IOSProjectService); diff --git a/lib/services/ios/export-options-plist-service.ts b/lib/services/ios/export-options-plist-service.ts new file mode 100644 index 0000000000..b25507b011 --- /dev/null +++ b/lib/services/ios/export-options-plist-service.ts @@ -0,0 +1,98 @@ +import * as path from "path"; +import * as temp from "temp"; +import * as mobileProvisionFinder from "ios-mobileprovision-finder"; + +export class ExportOptionsPlistService implements IExportOptionsPlistService { + constructor(private $fs: IFileSystem) { } + + public createDevelopmentExportOptionsPlist(archivePath: string, projectData: IProjectData, buildConfig: IBuildConfig): IExportOptionsPlistOutput { + const exportOptionsMethod = this.getExportOptionsMethod(projectData, archivePath); + const provision = buildConfig.provision || buildConfig.mobileProvisionIdentifier; + let plistTemplate = ` + + + + method + ${exportOptionsMethod}`; + if (provision) { + plistTemplate += ` provisioningProfiles + + ${projectData.projectIdentifiers.ios} + ${provision} +`; + } + plistTemplate += ` + uploadBitcode + + compileBitcode + + +`; + + // Save the options... + temp.track(); + const exportOptionsPlistFilePath = temp.path({ prefix: "export-", suffix: ".plist" }); + this.$fs.writeFile(exportOptionsPlistFilePath, plistTemplate); + + // The xcodebuild exportPath expects directory and writes the .ipa at that directory. + const exportFileDir = path.resolve(path.dirname(archivePath)); + const exportFilePath = path.join(exportFileDir, projectData.projectName + ".ipa"); + + return { exportFileDir, exportFilePath, exportOptionsPlistFilePath }; + } + + public createDistributionExportOptionsPlist(projectRoot: string, projectData: IProjectData, buildConfig: IBuildConfig): IExportOptionsPlistOutput { + const provision = buildConfig.provision || buildConfig.mobileProvisionIdentifier; + const teamId = buildConfig.teamId; + let plistTemplate = ` + + + +`; + if (teamId) { + plistTemplate += ` teamID + ${teamId} +`; + } + if (provision) { + plistTemplate += ` provisioningProfiles + + ${projectData.projectIdentifiers.ios} + ${provision} + `; + } + plistTemplate += ` method + app-store + uploadBitcode + + compileBitcode + + uploadSymbols + + +`; + + // Save the options... + temp.track(); + const exportOptionsPlistFilePath = temp.path({ prefix: "export-", suffix: ".plist" }); + this.$fs.writeFile(exportOptionsPlistFilePath, plistTemplate); + + const exportFileDir = path.resolve(path.join(projectRoot, "/build/archive")); + const exportFilePath = path.join(exportFileDir, projectData.projectName + ".ipa"); + + return { exportFileDir, exportFilePath, exportOptionsPlistFilePath }; + } + + private getExportOptionsMethod(projectData: IProjectData, archivePath: string): string { + const embeddedMobileProvisionPath = path.join(archivePath, 'Products', 'Applications', `${projectData.projectName}.app`, "embedded.mobileprovision"); + const provision = mobileProvisionFinder.provision.readFromFile(embeddedMobileProvisionPath); + + return { + "Development": "development", + "AdHoc": "ad-hoc", + "Distribution": "app-store", + "Enterprise": "enterprise" + }[provision.Type]; + } +} +$injector.register("exportOptionsPlistService", ExportOptionsPlistService); diff --git a/lib/services/ios/ios-signing-service.ts b/lib/services/ios/ios-signing-service.ts new file mode 100644 index 0000000000..ae92d8a28d --- /dev/null +++ b/lib/services/ios/ios-signing-service.ts @@ -0,0 +1,194 @@ +import * as path from "path"; +import * as mobileProvisionFinder from "ios-mobileprovision-finder"; +import { BUILD_XCCONFIG_FILE_NAME, iOSAppResourcesFolderName } from "../../constants"; +import * as helpers from "../../common/helpers"; +import { IOSProvisionService } from "../ios-provision-service"; + +export class IOSSigningService implements IiOSSigningService { + constructor( + private $errors: IErrors, + private $fs: IFileSystem, + private $iOSProvisionService: IOSProvisionService, + private $logger: ILogger, + private $pbxprojDomXcode: IPbxprojDomXcode, + private $prompter: IPrompter, + private $xcconfigService: IXcconfigService, + private $xcprojService: IXcprojService + ) { } + + public async setupSigningForDevice(projectRoot: string, projectData: IProjectData, buildConfig: IiOSBuildConfig): Promise { + const xcode = this.$pbxprojDomXcode.Xcode.open(this.getPbxProjPath(projectData, projectRoot)); + const signing = xcode.getSigning(projectData.projectName); + + const hasProvisioningProfileInXCConfig = + this.readXCConfigProvisioningProfileSpecifierForIPhoneOs(projectData) || + this.readXCConfigProvisioningProfileSpecifier(projectData) || + this.readXCConfigProvisioningProfileForIPhoneOs(projectData) || + this.readXCConfigProvisioningProfile(projectData); + + if (hasProvisioningProfileInXCConfig && (!signing || signing.style !== "Manual")) { + xcode.setManualSigningStyle(projectData.projectName); + xcode.save(); + } else if (!buildConfig.provision && !(signing && signing.style === "Manual" && !buildConfig.teamId)) { + const teamId = await this.getDevelopmentTeam(projectData, projectRoot, buildConfig.teamId); + await this.setupSigningFromTeam(projectRoot, projectData, teamId); + } + } + + public async setupSigningFromTeam(projectRoot: string, projectData: IProjectData, teamId: string): Promise { + const xcode = this.$pbxprojDomXcode.Xcode.open(this.getPbxProjPath(projectData, projectRoot)); + const signing = xcode.getSigning(projectData.projectName); + + let shouldUpdateXcode = false; + if (signing && signing.style === "Automatic") { + if (signing.team !== teamId) { + // Maybe the provided team is name such as "Telerik AD" and we need to convert it to CH******37 + const teamIdsForName = await this.$iOSProvisionService.getTeamIdsWithName(teamId); + if (!teamIdsForName.some(id => id === signing.team)) { + shouldUpdateXcode = true; + } + } + } else { + shouldUpdateXcode = true; + } + + if (shouldUpdateXcode) { + const teamIdsForName = await this.$iOSProvisionService.getTeamIdsWithName(teamId); + if (teamIdsForName.length > 0) { + this.$logger.trace(`Team id ${teamIdsForName[0]} will be used for team name "${teamId}".`); + teamId = teamIdsForName[0]; + } + + xcode.setAutomaticSigningStyle(projectData.projectName, teamId); + xcode.setAutomaticSigningStyleByTargetProductType("com.apple.product-type.app-extension", teamId); + xcode.save(); + + this.$logger.trace(`Set Automatic signing style and team id ${teamId}.`); + } else { + this.$logger.trace(`The specified ${teamId} is already set in the Xcode.`); + } + } + + public async setupSigningFromProvision(projectRoot: string, projectData: IProjectData, provision?: string, mobileProvisionData?: mobileProvisionFinder.provision.MobileProvision): Promise { + if (!provision) { + // read uuid from Xcode an cache... + return; + } + + const xcode = this.$pbxprojDomXcode.Xcode.open(this.getPbxProjPath(projectData, projectRoot)); + const signing = xcode.getSigning(projectData.projectName); + + let shouldUpdateXcode = false; + if (signing && signing.style === "Manual") { + for (const config in signing.configurations) { + const options = signing.configurations[config]; + if (options.name !== provision && options.uuid !== provision) { + shouldUpdateXcode = true; + break; + } + } + } else { + shouldUpdateXcode = true; + } + + if (shouldUpdateXcode) { + const pickStart = Date.now(); + const mobileprovision = mobileProvisionData || await this.$iOSProvisionService.pick(provision, projectData.projectIdentifiers.ios); + const pickEnd = Date.now(); + this.$logger.trace("Searched and " + (mobileprovision ? "found" : "failed to find ") + " matching provisioning profile. (" + (pickEnd - pickStart) + "ms.)"); + if (!mobileprovision) { + this.$errors.failWithoutHelp("Failed to find mobile provision with UUID or Name: " + provision); + } + const configuration = { + team: mobileprovision.TeamIdentifier && mobileprovision.TeamIdentifier.length > 0 ? mobileprovision.TeamIdentifier[0] : undefined, + uuid: mobileprovision.UUID, + name: mobileprovision.Name, + identity: mobileprovision.Type === "Development" ? "iPhone Developer" : "iPhone Distribution" + }; + xcode.setManualSigningStyle(projectData.projectName, configuration); + xcode.setManualSigningStyleByTargetProductType("com.apple.product-type.app-extension", configuration); + xcode.save(); + + // this.cache(uuid); + this.$logger.trace(`Set Manual signing style and provisioning profile: ${mobileprovision.Name} (${mobileprovision.UUID})`); + } else { + this.$logger.trace(`The specified provisioning profile is already set in the Xcode: ${provision}`); + } + } + + private getBuildXCConfigFilePath(projectData: IProjectData): string { + return path.join(projectData.appResourcesDirectoryPath, iOSAppResourcesFolderName, BUILD_XCCONFIG_FILE_NAME); + } + + private getPbxProjPath(projectData: IProjectData, projectRoot: string): string { + return path.join(this.$xcprojService.getXcodeprojPath(projectData, projectRoot), "project.pbxproj"); + } + + private async getDevelopmentTeam(projectData: IProjectData, projectRoot: string, teamId?: string): Promise { + teamId = teamId || this.readXCConfigDevelopmentTeam(projectData); + + if (!teamId) { + const teams = await this.$iOSProvisionService.getDevelopmentTeams(); + this.$logger.warn("Xcode requires a team id to be specified when building for device."); + this.$logger.warn("You can specify the team id by setting the DEVELOPMENT_TEAM setting in build.xcconfig file located in App_Resources folder of your app, or by using the --teamId option when calling run, debug or livesync commands."); + if (teams.length === 1) { + teamId = teams[0].id; + this.$logger.warn("Found and using the following development team installed on your system: " + teams[0].name + " (" + teams[0].id + ")"); + } else if (teams.length > 0) { + if (!helpers.isInteractive()) { + this.$errors.failWithoutHelp(`Unable to determine default development team. Available development teams are: ${_.map(teams, team => team.id)}. Specify team in app/App_Resources/iOS/build.xcconfig file in the following way: DEVELOPMENT_TEAM = `); + } + + const choices: string[] = []; + for (const team of teams) { + choices.push(team.name + " (" + team.id + ")"); + } + const choice = await this.$prompter.promptForChoice('Found multiple development teams, select one:', choices); + teamId = teams[choices.indexOf(choice)].id; + + const choicesPersist = [ + "Yes, set the DEVELOPMENT_TEAM setting in build.xcconfig file.", + "Yes, persist the team id in platforms folder.", + "No, don't persist this setting." + ]; + const choicePersist = await this.$prompter.promptForChoice("Do you want to make teamId: " + teamId + " a persistent choice for your app?", choicesPersist); + switch (choicesPersist.indexOf(choicePersist)) { + case 0: + const xcconfigFile = path.join(projectData.appResourcesDirectoryPath, "iOS", BUILD_XCCONFIG_FILE_NAME); + this.$fs.appendFile(xcconfigFile, "\nDEVELOPMENT_TEAM = " + teamId + "\n"); + break; + case 1: + this.$fs.writeFile(path.join(projectRoot, "teamid"), teamId); + break; + default: + break; + } + } + } + + this.$logger.trace(`Selected teamId is '${teamId}'.`); + + return teamId; + } + + private readXCConfigDevelopmentTeam(projectData: IProjectData): string { + return this.$xcconfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "DEVELOPMENT_TEAM"); + } + + private readXCConfigProvisioningProfile(projectData: IProjectData): string { + 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*]"); + } + + private readXCConfigProvisioningProfileSpecifier(projectData: IProjectData): string { + 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*]"); + } +} +$injector.register("iOSSigningService", IOSSigningService); diff --git a/lib/services/ios/xcodebuild-args-service.ts b/lib/services/ios/xcodebuild-args-service.ts new file mode 100644 index 0000000000..5165534428 --- /dev/null +++ b/lib/services/ios/xcodebuild-args-service.ts @@ -0,0 +1,102 @@ +import * as path from "path"; +import * as constants from "../../constants"; +import { Configurations } from "../../common/constants"; + +const DevicePlatformSdkName = "iphoneos"; +const SimulatorPlatformSdkName = "iphonesimulator"; + +export class XcodebuildArgsService implements IXcodebuildArgsService { + + constructor( + private $devicePlatformsConstants: Mobile.IDevicePlatformsConstants, + private $devicesService: Mobile.IDevicesService, + private $fs: IFileSystem, + private $logger: ILogger + ) { } + + public async getBuildForSimulatorArgs(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise { + let args = await this.getArchitecturesArgs(buildConfig); + + args = args + .concat([ + "build", + "-configuration", buildConfig.release ? Configurations.Release : Configurations.Debug, + "CODE_SIGN_IDENTITY=" + ]) + .concat(this.getBuildCommonArgs(platformData.projectRoot, SimulatorPlatformSdkName)) + .concat(this.getBuildLoggingArgs()) + .concat(this.getXcodeProjectArgs(platformData.projectRoot, projectData)); + + return args; + } + + public async getBuildForDeviceArgs(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise { + const architectures = await this.getArchitecturesArgs(buildConfig); + const archivePath = path.join(platformData.getBuildOutputPath(buildConfig), projectData.projectName + ".xcarchive"); + const args = [ + "archive", + "-archivePath", archivePath, + "-configuration", buildConfig.release ? Configurations.Release : Configurations.Debug + ] + .concat(this.getXcodeProjectArgs(platformData.projectRoot, projectData, "scheme")) + .concat(architectures) + .concat(this.getBuildCommonArgs(platformData.projectRoot, DevicePlatformSdkName)) + .concat(this.getBuildLoggingArgs()); + + return args; + } + + private async getArchitecturesArgs(buildConfig: IBuildConfig): Promise { + const args = []; + + const devicesArchitectures = buildConfig.buildForDevice ? await this.getArchitecturesFromConnectedDevices(buildConfig) : []; + if (!buildConfig.buildForDevice || devicesArchitectures.length > 1) { + args.push("ONLY_ACTIVE_ARCH=NO"); + } + + return args; + } + + private getXcodeProjectArgs(projectRoot: string, projectData: IProjectData, product?: "scheme" | "target"): string[] { + const xcworkspacePath = path.join(projectRoot, `${projectData.projectName}.xcworkspace`); + if (this.$fs.exists(xcworkspacePath)) { + return [ "-workspace", xcworkspacePath, "-scheme", projectData.projectName ]; + } + + const xcodeprojPath = path.join(projectRoot, `${projectData.projectName}.xcodeproj`); + return [ "-project", xcodeprojPath, product ? "-" + product : "-target", projectData.projectName ]; + } + + private getBuildLoggingArgs(): string[] { + return this.$logger.getLevel() === "INFO" ? ["-quiet"] : []; + } + + private getBuildCommonArgs(projectRoot: string, platformSdkName: string): string[] { + const args = [ + "-sdk", platformSdkName, + "BUILD_DIR=" + path.join(projectRoot, constants.BUILD_DIR), + 'SHARED_PRECOMPS_DIR=' + path.join(projectRoot, 'build', 'sharedpch'), + '-allowProvisioningUpdates' + ]; + + return args; + } + + private async getArchitecturesFromConnectedDevices(buildConfig: IiOSBuildConfig): Promise { + const platform = this.$devicePlatformsConstants.iOS.toLowerCase(); + await this.$devicesService.initialize({ + platform, + deviceId: buildConfig.device, + skipEmulatorStart: true + }); + const instances = this.$devicesService.getDevicesForPlatform(platform); + const architectures = _(instances) + .map(d => d.deviceInfo.activeArchitecture) + .filter(d => !!d) + .uniq() + .value(); + + return architectures; + } +} +$injector.register("xcodebuildArgsService", XcodebuildArgsService); diff --git a/lib/services/ios/xcodebuild-command-service.ts b/lib/services/ios/xcodebuild-command-service.ts new file mode 100644 index 0000000000..eb81c3e2fa --- /dev/null +++ b/lib/services/ios/xcodebuild-command-service.ts @@ -0,0 +1,29 @@ +import * as constants from "../../constants"; + +export class XcodebuildCommandService implements IXcodebuildCommandService { + constructor( + private $childProcess: IChildProcess, + private $errors: IErrors, + private $logger: ILogger + ) { } + + public async executeCommand(args: string[], options: { cwd: string, stdio: string, message?: string, spawnOptions?: any }): Promise { + const { message, cwd, stdio = "inherit", spawnOptions } = options; + this.$logger.info(message || "Xcode build..."); + + const childProcessOptions = { cwd, stdio }; + + try { + const commandResult = await this.$childProcess.spawnFromEvent("xcodebuild", + args, + "exit", + childProcessOptions, + spawnOptions || { emitOptions: { eventName: constants.BUILD_OUTPUT_EVENT_NAME }, throwError: true }); + + return commandResult; + } catch (err) { + this.$errors.failWithoutHelp(err.message); + } + } +} +$injector.register("xcodebuildCommandService", XcodebuildCommandService); diff --git a/lib/services/ios/xcodebuild-service.ts b/lib/services/ios/xcodebuild-service.ts new file mode 100644 index 0000000000..d1a3affef9 --- /dev/null +++ b/lib/services/ios/xcodebuild-service.ts @@ -0,0 +1,58 @@ +import * as path from "path"; + +export class XcodebuildService implements IXcodebuildService { + constructor( + private $exportOptionsPlistService: IExportOptionsPlistService, + private $xcodebuildArgsService: IXcodebuildArgsService, + private $xcodebuildCommandService: IXcodebuildCommandService + ) { } + + public async buildForDevice(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise { + const args = await this.$xcodebuildArgsService.getBuildForDeviceArgs(platformData, projectData, buildConfig); + await this.$xcodebuildCommandService.executeCommand(args, { cwd: platformData.projectRoot, stdio: buildConfig && buildConfig.buildOutputStdio }); + const archivePath = await this.createDevelopmentArchive(platformData, projectData, buildConfig); + return archivePath; + } + + public async buildForSimulator(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise { + const args = await this.$xcodebuildArgsService.getBuildForSimulatorArgs(platformData, projectData, buildConfig); + await this.$xcodebuildCommandService.executeCommand(args, { cwd: platformData.projectRoot, stdio: buildConfig.buildOutputStdio }); + } + + public async buildForAppStore(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise { + const args = await this.$xcodebuildArgsService.getBuildForDeviceArgs(platformData, projectData, buildConfig); + await this.$xcodebuildCommandService.executeCommand(args, { cwd: platformData.projectRoot, stdio: buildConfig && buildConfig.buildOutputStdio }); + const archivePath = await this.createDistributionArchive(platformData, projectData, buildConfig); + return archivePath; + } + + private async createDevelopmentArchive(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise { + const archivePath = path.join(platformData.getBuildOutputPath(buildConfig), projectData.projectName + ".xcarchive"); + const output = this.$exportOptionsPlistService.createDevelopmentExportOptionsPlist(archivePath, projectData, buildConfig); + const args = [ + "-exportArchive", + "-archivePath", archivePath, + "-exportPath", output.exportFileDir, + "-exportOptionsPlist", output.exportOptionsPlistFilePath + ]; + await this.$xcodebuildCommandService.executeCommand(args, { cwd: platformData.projectRoot, stdio: buildConfig.buildOutputStdio }); + + return output.exportFilePath; + } + + private async createDistributionArchive(platformData: IPlatformData, projectData: IProjectData, buildConfig: IBuildConfig): Promise { + const archivePath = path.join(platformData.getBuildOutputPath(buildConfig), projectData.projectName + ".xcarchive"); + const output = this.$exportOptionsPlistService.createDistributionExportOptionsPlist(archivePath, projectData, buildConfig); + const args = [ + "-exportArchive", + "-archivePath", archivePath, + "-exportPath", output.exportFileDir, + "-exportOptionsPlist", output.exportOptionsPlistFilePath + ]; + + await this.$xcodebuildCommandService.executeCommand(args, { cwd: platformData.projectRoot }); + + return output.exportFilePath; + } +} +$injector.register("xcodebuildService", XcodebuildService); diff --git a/lib/services/xcproj-service.ts b/lib/services/xcproj-service.ts index 2344a89065..966f20880c 100644 --- a/lib/services/xcproj-service.ts +++ b/lib/services/xcproj-service.ts @@ -14,8 +14,8 @@ class XcprojService implements IXcprojService { private $xcodeSelectService: IXcodeSelectService) { } - public getXcodeprojPath(projectData: IProjectData, platformData: IPlatformData): string { - return path.join(platformData.projectRoot, projectData.projectName + IosProjectConstants.XcodeProjExtName); + public getXcodeprojPath(projectData: IProjectData, projectRoot: string): string { + return path.join(projectRoot, projectData.projectName + IosProjectConstants.XcodeProjExtName); } public async verifyXcproj(opts: IVerifyXcprojOptions): Promise { diff --git a/test/ios-project-service.ts b/test/ios-project-service.ts index dae29f0f63..be0b2b1fb7 100644 --- a/test/ios-project-service.ts +++ b/test/ios-project-service.ts @@ -1,4 +1,4 @@ -import { join, resolve, dirname, basename, extname } from "path"; +import { join, dirname, basename, extname } from "path"; import { EOL } from "os"; import * as ChildProcessLib from "../lib/common/child-process"; import * as ConfigLib from "../lib/config"; @@ -35,6 +35,11 @@ import { ProjectDataStub } from "./stubs"; import { xcode } from "../lib/node/xcode"; import temp = require("temp"); import { CocoaPodsPlatformManager } from "../lib/services/cocoapods-platform-manager"; +import { XcodebuildService } from "../lib/services/ios/xcodebuild-service"; +import { XcodebuildCommandService } from "../lib/services/ios/xcodebuild-command-service"; +import { XcodebuildArgsService } from "../lib/services/ios/xcodebuild-args-service"; +import { ExportOptionsPlistService } from "../lib/services/ios/export-options-plist-service"; +import { IOSSigningService } from "../lib/services/ios/ios-signing-service"; temp.track(); class IOSSimulatorDiscoveryMock extends DeviceDiscovery { @@ -105,8 +110,8 @@ function createTestInjector(projectPath: string, projectName: string, xCode?: IX shouldUseXcproj: false }; }, - getXcodeprojPath: (projData: IProjectData, platformData: IPlatformData) => { - return join(platformData.projectRoot, projData.projectName + ".xcodeproj"); + getXcodeprojPath: (projData: IProjectData, projectRoot: string) => { + return join(projectRoot, projData.projectName + ".xcodeproj"); }, checkIfXcodeprojIsRequired: () => ({}) }); @@ -155,6 +160,13 @@ function createTestInjector(projectPath: string, projectName: string, xCode?: IX removeExtensions: () => { /* */ }, addExtensionsFromPath: () => Promise.resolve() }); + testInjector.register("timers", {}); + testInjector.register("iOSSigningService", IOSSigningService); + testInjector.register("xcodebuildService", XcodebuildService); + testInjector.register("xcodebuildCommandService", XcodebuildCommandService); + testInjector.register("xcodebuildArgsService", XcodebuildArgsService); + testInjector.register("exportOptionsPlistService", ExportOptionsPlistService); + return testInjector; } @@ -174,178 +186,6 @@ function createPackageJson(testInjector: IInjector, projectPath: string, project testInjector.resolve("fs").writeJson(join(projectPath, "package.json"), packageJsonData); } -function expectOption(args: string[], option: string, value: string, message?: string): void { - const index = args.indexOf(option); - assert.ok(index >= 0, "Expected " + option + " to be set."); - assert.ok(args.length > index + 1, "Expected " + option + " to have value"); - assert.equal(args[index + 1], value, message); -} - -function readOption(args: string[], option: string): string { - const index = args.indexOf(option); - assert.ok(index >= 0, "Expected " + option + " to be set."); - assert.ok(args.length > index + 1, "Expected " + option + " to have value"); - return args[index + 1]; -} - -describe("iOSProjectService", () => { - describe("archive", () => { - async function setupArchive(options?: { archivePath?: string }): Promise<{ run: () => Promise, assert: () => void }> { - const hasCustomArchivePath = options && options.archivePath; - - const projectName = "projectDirectory"; - const projectPath = temp.mkdirSync(projectName); - - const testInjector = createTestInjector(projectPath, projectName); - const iOSProjectService = testInjector.resolve("iOSProjectService"); - const projectData: IProjectData = testInjector.resolve("projectData"); - - const childProcess = testInjector.resolve("childProcess"); - let xcodebuildExeced = false; - - let archivePath: string; - - childProcess.spawnFromEvent = (cmd: string, args: string[]) => { - assert.equal(cmd, "xcodebuild", "Expected iOSProjectService.archive to call xcodebuild.archive"); - xcodebuildExeced = true; - - if (hasCustomArchivePath) { - archivePath = resolve(options.archivePath); - } else { - archivePath = join(projectPath, "platforms", "ios", "build", "Release-iphoneos", projectName + ".xcarchive"); - } - - assert.ok(args.indexOf("archive") >= 0, "Expected xcodebuild to be executed with archive param."); - - expectOption(args, "-archivePath", archivePath, hasCustomArchivePath ? "Wrong path passed to xcarchive" : "exports xcodearchive to platforms/ios/build/archive."); - expectOption(args, "-project", join(projectPath, "platforms", "ios", projectName + ".xcodeproj"), "Path to Xcode project is wrong."); - expectOption(args, "-scheme", projectName, "The provided scheme is wrong."); - - return Promise.resolve(); - }; - - let resultArchivePath: string; - - return { - run: async (): Promise => { - if (hasCustomArchivePath) { - resultArchivePath = await iOSProjectService.archive(projectData, null, { archivePath: options.archivePath }); - } else { - resultArchivePath = await iOSProjectService.archive(projectData, null); - } - }, - assert: () => { - assert.ok(xcodebuildExeced, "Expected xcodebuild archive to be executed"); - assert.equal(resultArchivePath, archivePath, "iOSProjectService.archive expected to return the path to the archive"); - } - }; - } - - if (require("os").platform() !== "darwin") { - console.log("Skipping iOS archive tests. They can work only on macOS"); - } else { - it("by default exports xcodearchive to platforms/ios/build/archive/.xcarchive", async () => { - const setup = await setupArchive(); - await setup.run(); - setup.assert(); - }); - it("can pass archivePath to xcodebuild -archivePath", async () => { - const setup = await setupArchive({ archivePath: "myarchive.xcarchive" }); - await setup.run(); - setup.assert(); - }); - } - }); - - describe("exportArchive", () => { - const noTeamPlist = ` - - - - method - app-store - uploadBitcode - - compileBitcode - - uploadSymbols - - -`; - - const myTeamPlist = ` - - - - teamID - MyTeam - method - app-store - uploadBitcode - - compileBitcode - - uploadSymbols - - -`; - - async function testExportArchive(options: { teamID?: string }, expectedPlistContent: string): Promise { - const projectName = "projectDirectory"; - const projectPath = temp.mkdirSync(projectName); - - const testInjector = createTestInjector(projectPath, projectName); - const iOSProjectService = testInjector.resolve("iOSProjectService"); - const projectData: IProjectData = testInjector.resolve("projectData"); - - const archivePath = join(projectPath, "platforms", "ios", "build", "archive", projectName + ".xcarchive"); - - const childProcess = testInjector.resolve("childProcess"); - const fs = testInjector.resolve("fs"); - - let xcodebuildExeced = false; - - childProcess.spawnFromEvent = (cmd: string, args: string[]) => { - assert.equal(cmd, "xcodebuild", "Expected xcodebuild to be called"); - xcodebuildExeced = true; - - assert.ok(args.indexOf("-exportArchive") >= 0, "Expected -exportArchive to be set on xcodebuild."); - - expectOption(args, "-archivePath", archivePath, "Expected the -archivePath to be passed to xcodebuild."); - expectOption(args, "-exportPath", join(projectPath, "platforms", "ios", "build", "archive"), "Expected the -archivePath to be passed to xcodebuild."); - const plist = readOption(args, "-exportOptionsPlist"); - - assert.ok(plist); - - const plistContent = fs.readText(plist); - // There may be better way to equal property lists - assert.equal(plistContent, expectedPlistContent, "Mismatch in exportOptionsPlist content"); - - return Promise.resolve(); - }; - - const resultIpa = await iOSProjectService.exportArchive(projectData, { archivePath, teamID: options.teamID }); - const expectedIpa = join(projectPath, "platforms", "ios", "build", "archive", projectName + ".ipa"); - - assert.equal(resultIpa, expectedIpa, "Expected IPA at the specified location"); - - assert.ok(xcodebuildExeced, "Expected xcodebuild to be executed"); - } - - if (require("os").platform() !== "darwin") { - console.log("Skipping iOS export archive tests. They can work only on macOS"); - } else { - it("calls xcodebuild -exportArchive to produce .IPA", async () => { - await testExportArchive({}, noTeamPlist); - }); - - it("passes the --team-id option down the xcodebuild -exportArchive throug the -exportOptionsPlist", async () => { - await testExportArchive({ teamID: "MyTeam" }, myTeamPlist); - }); - } - }); -}); - describe("Cocoapods support", () => { if (require("os").platform() !== "darwin") { console.log("Skipping Cocoapods tests. They cannot work on windows"); @@ -1025,91 +865,6 @@ describe("iOS Project Service Signing", () => { assert.isFalse(!!changes.signingChanged); }); }); - - describe("specifying provision", () => { - describe("from Automatic to provision name", () => { - beforeEach(() => { - files[pbxproj] = ""; - pbxprojDomXcode.Xcode.open = function (path: string) { - return { - getSigning(x: string) { - return { style: "Automatic", teamID: "AutoTeam" }; - } - }; - }; - }); - it("fails with proper error if the provision can not be found", async () => { - try { - await iOSProjectService.prepareProject(projectData, { sdk: undefined, provision: "NativeScriptDev2", teamId: undefined }); - } catch (e) { - assert.isTrue(e.toString().indexOf("Failed to find mobile provision with UUID or Name: NativeScriptDev2") >= 0); - } - }); - it("succeeds if the provision name is provided for development cert", async () => { - const stack: any = []; - pbxprojDomXcode.Xcode.open = function (path: string) { - assert.equal(path, pbxproj); - return { - getSigning() { - return { style: "Automatic", teamID: "AutoTeam" }; - }, - save() { - stack.push("save()"); - }, - setManualSigningStyle(targetName: string, manualSigning: any) { - stack.push({ targetName, manualSigning }); - }, - setManualSigningStyleByTargetProductType: () => ({}), - setManualSigningStyleByTargetKey: () => ({}) - }; - }; - await iOSProjectService.prepareProject(projectData, { sdk: undefined, provision: "NativeScriptDev", teamId: undefined }); - assert.deepEqual(stack, [{ targetName: projectDirName, manualSigning: { team: "TKID101", uuid: "12345", name: "NativeScriptDev", identity: "iPhone Developer" } }, "save()"]); - }); - it("succeds if the provision name is provided for distribution cert", async () => { - const stack: any = []; - pbxprojDomXcode.Xcode.open = function (path: string) { - assert.equal(path, pbxproj); - return { - getSigning() { - return { style: "Automatic", teamID: "AutoTeam" }; - }, - save() { - stack.push("save()"); - }, - setManualSigningStyle(targetName: string, manualSigning: any) { - stack.push({ targetName, manualSigning }); - }, - setManualSigningStyleByTargetProductType: () => ({}), - setManualSigningStyleByTargetKey: () => ({}) - }; - }; - await iOSProjectService.prepareProject(projectData, { sdk: undefined, provision: "NativeScriptDist", teamId: undefined }); - assert.deepEqual(stack, [{ targetName: projectDirName, manualSigning: { team: "TKID202", uuid: "6789", name: "NativeScriptDist", identity: "iPhone Distribution" } }, "save()"]); - }); - it("succeds if the provision name is provided for adhoc cert", async () => { - const stack: any = []; - pbxprojDomXcode.Xcode.open = function (path: string) { - assert.equal(path, pbxproj); - return { - getSigning() { - return { style: "Automatic", teamID: "AutoTeam" }; - }, - save() { - stack.push("save()"); - }, - setManualSigningStyle(targetName: string, manualSigning: any) { - stack.push({ targetName, manualSigning }); - }, - setManualSigningStyleByTargetProductType: () => ({}), - setManualSigningStyleByTargetKey: () => ({}) - }; - }; - await iOSProjectService.prepareProject(projectData, { sdk: undefined, provision: "NativeScriptAdHoc", teamId: undefined }); - assert.deepEqual(stack, [{ targetName: projectDirName, manualSigning: { team: "TKID303", uuid: "1010", name: "NativeScriptAdHoc", identity: "iPhone Distribution" } }, "save()"]); - }); - }); - }); }); describe("Merge Project XCConfig files", () => { @@ -1247,163 +1002,6 @@ describe("Merge Project XCConfig files", () => { }); }); -describe("buildProject", () => { - let xcodeBuildCommandArgs: string[] = []; - - function setup(data: { frameworkVersion: string, deploymentTarget: string, devices?: Mobile.IDevice[] }): IInjector { - const projectPath = "myTestProjectPath"; - const projectName = "myTestProjectName"; - const testInjector = createTestInjector(projectPath, projectName); - - const childProcess = testInjector.resolve("childProcess"); - childProcess.spawnFromEvent = (command: string, args: string[]) => { - if (command === "xcodebuild" && args[0] !== "-exportArchive") { - xcodeBuildCommandArgs = args; - } - }; - - const projectDataService = testInjector.resolve("projectDataService"); - projectDataService.getNSValue = (projectDir: string, propertyName: string) => { - if (propertyName === "tns-ios") { - return { - name: "tns-ios", - version: data.frameworkVersion - }; - } - }; - - const projectData = testInjector.resolve("projectData"); - projectData.appResourcesDirectoryPath = join(projectPath, "app", "App_Resources"); - - const devicesService = testInjector.resolve("devicesService"); - devicesService.initialize = () => ({}); - devicesService.getDeviceInstances = () => data.devices || []; - - const xcconfigService = testInjector.resolve("xcconfigService"); - xcconfigService.readPropertyValue = (projectDir: string, propertyName: string) => { - if (propertyName === "IPHONEOS_DEPLOYMENT_TARGET") { - return data.deploymentTarget; - } - }; - - const pbxprojDomXcode = testInjector.resolve("pbxprojDomXcode"); - pbxprojDomXcode.Xcode = { - open: () => ({ - getSigning: () => ({}), - setAutomaticSigningStyle: () => ({}), - setAutomaticSigningStyleByTargetProductType: () => ({}), - setAutomaticSigningStyleByTargetKey: () => ({}), - save: () => ({}) - }) - }; - - const iOSProvisionService = testInjector.resolve("iOSProvisionService"); - iOSProvisionService.getDevelopmentTeams = () => ({}); - iOSProvisionService.getTeamIdsWithName = () => ({}); - - return testInjector; - } - - function executeTests(testCases: any[], data: { buildForDevice: boolean }) { - _.each(testCases, testCase => { - it(`${testCase.name}`, async () => { - const testInjector = setup({ frameworkVersion: testCase.frameworkVersion, deploymentTarget: testCase.deploymentTarget }); - const projectData: IProjectData = testInjector.resolve("projectData"); - - const iOSProjectService = testInjector.resolve("iOSProjectService"); - (iOSProjectService).getExportOptionsMethod = () => ({}); - await iOSProjectService.buildProject("myProjectRoot", projectData, { buildForDevice: data.buildForDevice }); - - const archsItem = xcodeBuildCommandArgs.find(item => item.startsWith("ARCHS=")); - if (testCase.expectedArchs) { - const archsValue = archsItem.split("=")[1]; - assert.deepEqual(archsValue, testCase.expectedArchs); - } else { - assert.deepEqual(undefined, archsItem); - } - }); - }); - } - - describe("for device", () => { - afterEach(() => { - xcodeBuildCommandArgs = []; - }); - - const testCases = [{ - name: "shouldn't exclude armv7 architecture when deployment target 10", - frameworkVersion: "5.0.0", - deploymentTarget: "10.0", - expectedArchs: "armv7 arm64" - }, { - name: "should exclude armv7 architecture when deployment target is 11", - frameworkVersion: "5.0.0", - deploymentTarget: "11.0", - expectedArchs: "arm64" - }, { - name: "shouldn't pass architecture to xcodebuild command when frameworkVersion is 5.1.0", - frameworkVersion: "5.1.0", - deploymentTarget: "11.0" - }, { - name: "should pass only 64bit architecture to xcodebuild command when frameworkVersion is 5.0.0 and deployment target is 11.0", - frameworkVersion: "5.0.0", - deploymentTarget: "11.0", - expectedArchs: "arm64" - }, { - name: "should pass both architectures to xcodebuild command when frameworkVersion is 5.0.0 and deployment target is 10.0", - frameworkVersion: "5.0.0", - deploymentTarget: "10.0", - expectedArchs: "armv7 arm64" - }, { - name: "should pass both architectures to xcodebuild command when frameworkVersion is 5.0.0 and no deployment target", - frameworkVersion: "5.0.0", - deploymentTarget: null, - expectedArchs: "armv7 arm64" - }]; - - executeTests(testCases, { buildForDevice: true }); - }); - - describe("for simulator", () => { - afterEach(() => { - xcodeBuildCommandArgs = []; - }); - - const testCases = [{ - name: "shouldn't exclude i386 architecture when deployment target is 10", - frameworkVersion: "5.0.0", - deploymentTarget: "10.0", - expectedArchs: "i386 x86_64" - }, { - name: "should exclude i386 architecture when deployment target is 11", - frameworkVersion: "5.0.0", - deploymentTarget: "11.0", - expectedArchs: "x86_64" - }, { - name: "shouldn't pass architecture to xcodebuild command when frameworkVersion is 5.1.0", - frameworkVersion: "5.1.0", - deploymentTarget: "11.0" - }, { - name: "should pass only 64bit architecture to xcodebuild command when frameworkVersion is 5.0.0 and deployment target is 11.0", - frameworkVersion: "5.0.0", - deploymentTarget: "11.0", - expectedArchs: "x86_64" - }, { - name: "should pass both architectures to xcodebuild command when frameworkVersion is 5.0.0 and deployment target is 10.0", - frameworkVersion: "5.0.0", - deploymentTarget: "10.0", - expectedArchs: "i386 x86_64" - }, { - name: "should pass both architectures to xcodebuild command when frameworkVersion is 5.0.0 and no deployment target", - frameworkVersion: "5.0.0", - deploymentTarget: null, - expectedArchs: "i386 x86_64" - }]; - - executeTests(testCases, { buildForDevice: false }); - }); -}); - describe("handleNativeDependenciesChange", () => { it("ensure the correct order of pod install and merging pod's xcconfig file", async () => { const executedCocoapodsMethods: string[] = []; From 1232e2bdc297f4ec8d2a2dff1938ae16a6e1b9a6 Mon Sep 17 00:00:00 2001 From: fatme Date: Mon, 22 Apr 2019 01:06:28 +0300 Subject: [PATCH 2/3] test: add unit tests --- .../ios/export-options-plist-service.ts | 106 ++++++ test/services/ios/ios-signing-service.ts | 330 ++++++++++++++++++ test/services/ios/xcodebuild-args-service.ts | 132 +++++++ test/services/ios/xcodebuild-service.ts | 93 +++++ test/tns-appstore-upload.ts | 37 +- 5 files changed, 674 insertions(+), 24 deletions(-) create mode 100644 test/services/ios/export-options-plist-service.ts create mode 100644 test/services/ios/ios-signing-service.ts create mode 100644 test/services/ios/xcodebuild-args-service.ts create mode 100644 test/services/ios/xcodebuild-service.ts diff --git a/test/services/ios/export-options-plist-service.ts b/test/services/ios/export-options-plist-service.ts new file mode 100644 index 0000000000..e92c6e5a41 --- /dev/null +++ b/test/services/ios/export-options-plist-service.ts @@ -0,0 +1,106 @@ +import { Yok } from "../../../lib/common/yok"; +import { ExportOptionsPlistService } from "../../../lib/services/ios/export-options-plist-service"; +import { assert } from "chai"; +import { EOL } from "os"; + +let actualPlistTemplate: string = null; +const projectName = "myProjectName"; +const projectRoot = "/my/test/project/platforms/ios"; +const archivePath = "/my/test/archive/path"; + +function createTestInjector() { + const injector = new Yok(); + injector.register("fs", { + writeFile: (exportPath: string, plistTemplate: string) => { + actualPlistTemplate = plistTemplate; + } + }); + injector.register("exportOptionsPlistService", ExportOptionsPlistService); + + return injector; +} + +describe("ExportOptionsPlistService", () => { + describe("createDevelopmentExportOptionsPlist", () => { + const testCases = [ + { + name: "should create default export options plist", + buildConfig: {} + }, + { + name: "should create export options plist with provision", + buildConfig: { provision: "myTestProvision" }, + expectedPlist: "provisioningProfiles org.nativescript.myTestApp myTestProvision " + }, + { + name: "should create export options plist with mobileProvisionIdentifier", + buildConfig: { mobileProvisionIdentifier: "myTestProvision" }, + expectedPlist: "provisioningProfiles org.nativescript.myTestApp myTestProvision " + } + ]; + + _.each(testCases, testCase => { + _.each(["Development", "AdHoc", "Distribution", "Enterprise"], provisionType => { + it(testCase.name, () => { + const injector = createTestInjector(); + const exportOptionsPlistService = injector.resolve("exportOptionsPlistService"); + exportOptionsPlistService.getExportOptionsMethod = () => provisionType; + + const projectData = { projectName, projectIdentifiers: { ios: "org.nativescript.myTestApp" }}; + exportOptionsPlistService.createDevelopmentExportOptionsPlist(archivePath, projectData, testCase.buildConfig); + + const template = actualPlistTemplate.split(EOL).join(" "); + assert.isTrue(template.indexOf(`method ${provisionType}`) > 0); + assert.isTrue(template.indexOf("uploadBitcode ") > 0); + assert.isTrue(template.indexOf("compileBitcode ") > 0); + if (testCase.expectedPlist) { + assert.isTrue(template.indexOf(testCase.expectedPlist) > 0); + } + }); + }); + }); + }); + describe("createDistributionExportOptionsPlist", () => { + const testCases = [ + { + name: "should create default export options plist", + buildConfig: {} + }, + { + name: "should create export options plist with provision", + buildConfig: { provision: "myTestProvision" }, + expectedPlist: "provisioningProfiles org.nativescript.myTestApp myTestProvision " + }, + { + name: "should create export options plist with mobileProvisionIdentifier", + buildConfig: { mobileProvisionIdentifier: "myTestProvision" }, + expectedPlist: "provisioningProfiles org.nativescript.myTestApp myTestProvision " + }, + { + name: "should create export options plist with teamID", + buildConfig: { teamId: "myTeamId" }, + expectedPlist: "teamID myTeamId" + } + ]; + + _.each(testCases, testCase => { + it(testCase.name, () => { + const injector = createTestInjector(); + const exportOptionsPlistService = injector.resolve("exportOptionsPlistService"); + exportOptionsPlistService.getExportOptionsMethod = () => "app-store"; + + const projectData = { projectName, projectIdentifiers: { ios: "org.nativescript.myTestApp" }}; + exportOptionsPlistService.createDistributionExportOptionsPlist(projectRoot, projectData, testCase.buildConfig); + + const template = actualPlistTemplate.split(EOL).join(" "); + assert.isTrue(template.indexOf("method app-store") > 0); + assert.isTrue(template.indexOf("uploadBitcode ") > 0); + assert.isTrue(template.indexOf("compileBitcode ") > 0); + assert.isTrue(template.indexOf("uploadSymbols ") > 0); + if (testCase.expectedPlist) { + assert.isTrue(template.indexOf(testCase.expectedPlist) > 0); + } + }); + }); + }); +}); diff --git a/test/services/ios/ios-signing-service.ts b/test/services/ios/ios-signing-service.ts new file mode 100644 index 0000000000..c38f518094 --- /dev/null +++ b/test/services/ios/ios-signing-service.ts @@ -0,0 +1,330 @@ +import { Yok } from "../../../lib/common/yok"; +import { IOSSigningService } from "../../../lib/services/ios/ios-signing-service"; +import { assert } from "chai"; +import { ManualSigning } from "pbxproj-dom/xcode"; +import { Errors } from "../../../lib/common/errors"; + +interface IXcodeMock { + isSetManualSigningStyleCalled: boolean; + isSetManualSigningStyleByTargetProductTypeCalled: boolean; + isSetAutomaticSigningStyleCalled: boolean; + isSetAutomaticSigningStyleByTargetProductTypeCalled: boolean; + isSaveCalled: boolean; +} + +const projectRoot = "myProjectRoot"; +const teamId = "myTeamId"; +const projectData: any = { + projectName: "myProjectName", + appResourcesDirectoryPath: "app-resources/path", + projectIdentifiers: { + ios: "org.nativescript.testApp" + } +}; +const NativeScriptDev = { + Name: "NativeScriptDev", + TeamName: "Telerik AD", + TeamIdentifier: ["TKID101"], + Entitlements: { + "application-identifier": "*", + "com.apple.developer.team-identifier": "ABC" + }, + UUID: "12345", + ProvisionsAllDevices: false, + Type: "Development" +}; +const NativeScriptDist = { + Name: "NativeScriptDist", + TeamName: "Telerik AD", + TeamIdentifier: ["TKID202"], + Entitlements: { + "application-identifier": "*", + "com.apple.developer.team-identifier": "ABC" + }, + UUID: "6789", + ProvisionsAllDevices: true, + Type: "Distribution" +}; +const NativeScriptAdHoc = { + Name: "NativeScriptAdHoc", + TeamName: "Telerik AD", + TeamIdentifier: ["TKID303"], + Entitlements: { + "application-identifier": "*", + "com.apple.developer.team-identifier": "ABC" + }, + UUID: "1010", + ProvisionsAllDevices: true, + Type: "Distribution" +}; + +class XcodeMock implements IXcodeMock { + public isSetManualSigningStyleCalled = false; + public isSetManualSigningStyleByTargetProductTypeCalled = false; + public isSetAutomaticSigningStyleCalled = false; + public isSetAutomaticSigningStyleByTargetProductTypeCalled = false; + public isSaveCalled = false; + + constructor(private data: { signing: { style: string, team?: string } }) { } + + public getSigning() { + return this.data.signing; + } + + public setManualSigningStyle(projectName: string, configuration: ManualSigning) { + this.isSetManualSigningStyleCalled = true; + } + + public setManualSigningStyleByTargetProductType() { + this.isSetManualSigningStyleByTargetProductTypeCalled = true; + } + + public setAutomaticSigningStyle() { + this.isSetAutomaticSigningStyleCalled = true; + } + + public setAutomaticSigningStyleByTargetProductType() { + this.isSetAutomaticSigningStyleByTargetProductTypeCalled = true; + } + + public save() { + this.isSaveCalled = true; + } +} + +function setup(data: { + hasXCConfigrovisioning?: boolean, + hasXCConfigDevelopmentTeam?: boolean, + signing?: { style: string }, + teamIdsForName?: string[], + provision?: string +}): { injector: IInjector, xcodeMock: any } { + const { hasXCConfigrovisioning, hasXCConfigDevelopmentTeam, signing, teamIdsForName, provision = "myProvision" } = data; + const xcodeMock = new XcodeMock({ signing }); + + const injector = new Yok(); + injector.register("errors", Errors); + injector.register("fs", {}); + injector.register("iOSProvisionService", { + getTeamIdsWithName: () => teamIdsForName || [], + pick: async (uuidOrName: string, projId: string) => { + return ({ + NativeScriptDev, + NativeScriptDist, + NativeScriptAdHoc + })[uuidOrName]; + } + }); + injector.register("logger", { + trace: () => ({}) + }); + injector.register("pbxprojDomXcode", { + Xcode: { + open: () => xcodeMock + } + }); + injector.register("prompter", {}); + injector.register("xcconfigService", { + readPropertyValue: (xcconfigFilePath: string, propertyName: string) => { + if (propertyName.startsWith("PROVISIONING_PROFILE")) { + return hasXCConfigrovisioning ? provision : null; + } + if (propertyName.startsWith("DEVELOPMENT_TEAM")) { + return hasXCConfigDevelopmentTeam ? teamId : null; + } + } + }); + injector.register("xcprojService", { + getXcodeprojPath: () => "some/path" + }); + injector.register("iOSSigningService", IOSSigningService); + + return { injector, xcodeMock }; +} + +describe("IOSSigningService", () => { + describe("setupSigningForDevice", () => { + const testCases = [ + { + name: "should sign the project manually when PROVISIONING_PROFILE is provided from xcconfig and the project is still not signed", + arrangeData: { hasXCConfigrovisioning: true, signing: null }, + assert: (xcodeMock: IXcodeMock) => { + assert.isTrue(xcodeMock.isSetManualSigningStyleCalled); + assert.isTrue(xcodeMock.isSaveCalled); + } + }, + { + name: "should sign the project manually when PROVISIONING_PROFILE is provided from xcconfig and the project is automatically signed", + arrangeData: { hasXCConfigrovisioning: true, signing: { style: "Automatic" } }, + assert: (xcodeMock: IXcodeMock) => { + assert.isTrue(xcodeMock.isSetManualSigningStyleCalled); + assert.isTrue(xcodeMock.isSaveCalled); + } + }, + { + name: "shouldn't sign the project manually when PROVISIONING_PROFILE is provided from xcconfig and the project is already manually signed", + arrangeData: { hasXCConfigrovisioning: true, signing: { style: "Manual" } }, + assert: (xcodeMock: IXcodeMock) => { + assert.isFalse(xcodeMock.isSetManualSigningStyleCalled); + assert.isFalse(xcodeMock.isSaveCalled); + } + }, + { + name: "should sign the project automatically when PROVISIONING_PROFILE is not provided from xcconfig, DEVELOPMENT_TEAM is provided from xcconfig and the project is still not signed", + arrangeData: { hasXCConfigrovisioning: false, hasXCConfigDevelopmentTeam: true, signing: null }, + assert: (xcodeMock: IXcodeMock) => { + assert.isTrue(xcodeMock.isSetAutomaticSigningStyleCalled); + assert.isTrue(xcodeMock.isSetAutomaticSigningStyleByTargetProductTypeCalled); + assert.isTrue(xcodeMock.isSaveCalled); + } + }, + { + name: "should sign the project automatically when PROVISIONING_PROFILE is not provided from xcconfig, DEVELOPMENT_TEAM is provided from xcconfig and the project is automatically signed", + arrangeData: { hasXCConfigrovisioning: false, hasXCConfigDevelopmentTeam: true, signing: { style: "Automatic" } }, + assert: (xcodeMock: IXcodeMock) => { + assert.isTrue(xcodeMock.isSetAutomaticSigningStyleCalled); + assert.isTrue(xcodeMock.isSetAutomaticSigningStyleByTargetProductTypeCalled); + assert.isTrue(xcodeMock.isSaveCalled); + } + }, + { + name: "shouldn't sign the project when PROVISIONING_PROFILE is not provided from xcconfig, DEVELOPMENT_TEAM is provided from xcconfig and the project is already manually signed", + arrangeData: { hasXCConfigrovisioning: false, hasXCConfigDevelopmentTeam: true, signing: { style: "Manual" } }, + assert: (xcodeMock: IXcodeMock) => { + assert.isFalse(xcodeMock.isSetAutomaticSigningStyleCalled); + assert.isFalse(xcodeMock.isSetAutomaticSigningStyleByTargetProductTypeCalled); + assert.isFalse(xcodeMock.isSetManualSigningStyleCalled); + assert.isFalse(xcodeMock.isSaveCalled); + } + } + ]; + + _.each(testCases, testCase => { + it(testCase.name, async () => { + const { injector, xcodeMock } = setup(testCase.arrangeData); + + const iOSSigningService = injector.resolve("iOSSigningService"); + await iOSSigningService.setupSigningForDevice(projectRoot, projectData, (testCase).buildConfig || {}); + + testCase.assert(xcodeMock); + }); + }); + }); + describe("setupSigningFromTeam", () => { + const testCases = [ + { + name: "should sign the project for given teamId when the project is still not signed", + arrangeData: { signing: null }, + assert: (xcodeMock: IXcodeMock) => { + assert.isTrue(xcodeMock.isSetAutomaticSigningStyleCalled); + assert.isTrue(xcodeMock.isSetAutomaticSigningStyleByTargetProductTypeCalled); + assert.isTrue(xcodeMock.isSaveCalled); + assert.isFalse(xcodeMock.isSetManualSigningStyleCalled); + } + }, + { + name: "should sign the project for given teamId when the project is already automatically signed for another team", + arrangeData: { signing: { style: "Automatic" } }, + assert: (xcodeMock: IXcodeMock) => { + assert.isTrue(xcodeMock.isSetAutomaticSigningStyleCalled); + assert.isTrue(xcodeMock.isSetAutomaticSigningStyleByTargetProductTypeCalled); + assert.isTrue(xcodeMock.isSaveCalled); + assert.isFalse(xcodeMock.isSetManualSigningStyleCalled); + } + }, + { + name: "shouldn't sign the project for given teamId when the project is already automatically signed for this team", + arrangeData: { signing: { style: "Automatic", team: teamId }}, + assert: (xcodeMock: IXcodeMock) => { + assert.isFalse(xcodeMock.isSetAutomaticSigningStyleCalled); + assert.isFalse(xcodeMock.isSetAutomaticSigningStyleByTargetProductTypeCalled); + assert.isFalse(xcodeMock.isSaveCalled); + assert.isFalse(xcodeMock.isSetManualSigningStyleCalled); + } + }, + { + name: "shouldn't sign the project for given teamName when the project is already automatically signed for this team", + arrangeData: { signing: { style: "Automatic", team: "anotherTeamId" }, teamIdsForName: [ "anotherTeamId" ] }, + assert: (xcodeMock: IXcodeMock) => { + assert.isFalse(xcodeMock.isSetAutomaticSigningStyleCalled); + assert.isFalse(xcodeMock.isSetAutomaticSigningStyleByTargetProductTypeCalled); + assert.isFalse(xcodeMock.isSaveCalled); + assert.isFalse(xcodeMock.isSetManualSigningStyleCalled); + } + }, + { + name: "should set automatic signing style when the project is already manually signed", + arrangeData: { signing: { style: "Manual" }}, + assert: (xcodeMock: IXcodeMock) => { + assert.isTrue(xcodeMock.isSetAutomaticSigningStyleCalled); + assert.isTrue(xcodeMock.isSetAutomaticSigningStyleByTargetProductTypeCalled); + assert.isTrue(xcodeMock.isSaveCalled); + assert.isFalse(xcodeMock.isSetManualSigningStyleCalled); + } + } + ]; + + _.each(testCases, testCase => { + it(testCase.name, async () => { + const { injector, xcodeMock } = setup(testCase.arrangeData); + + const iOSSigningService: IiOSSigningService = injector.resolve("iOSSigningService"); + await iOSSigningService.setupSigningFromTeam(projectRoot, projectData, teamId); + + testCase.assert(xcodeMock); + }); + }); + }); + describe("setupSigningFromProvision", () => { + const testCases = [ + { + name: "should sign the project manually when it is still not signed", + arrangeData: { signing: null }, + assert: (xcodeMock: IXcodeMock) => { + assert.isTrue(xcodeMock.isSetManualSigningStyleCalled); + assert.isTrue(xcodeMock.isSetManualSigningStyleByTargetProductTypeCalled); + assert.isTrue(xcodeMock.isSaveCalled); + } + }, + { + name: "should sign the project manually when it is automatically signed", + arrangeData: { signing: { style: "Automatic" } }, + assert: (xcodeMock: IXcodeMock) => { + assert.isTrue(xcodeMock.isSetManualSigningStyleCalled); + assert.isTrue(xcodeMock.isSetManualSigningStyleByTargetProductTypeCalled); + assert.isTrue(xcodeMock.isSaveCalled); + } + }, + { + name: "shouldn't sign the project when it is already manual signed", + arrangeData: { signing: { style: "Manual" } }, + assert: (xcodeMock: IXcodeMock) => { + assert.isFalse(xcodeMock.isSetManualSigningStyleCalled); + assert.isFalse(xcodeMock.isSetManualSigningStyleByTargetProductTypeCalled); + assert.isFalse(xcodeMock.isSaveCalled); + } + } + ]; + + _.each(testCases, testCase => { + _.each(["NativeScriptDev", "NativeScriptDist", "NativeScriptAdHoc"], provision => { + it(`${testCase.name} for ${provision} provision`, async () => { + const { injector, xcodeMock } = setup(testCase.arrangeData); + + const iOSSigningService: IiOSSigningService = injector.resolve("iOSSigningService"); + await iOSSigningService.setupSigningFromProvision(projectRoot, projectData, provision); + + testCase.assert(xcodeMock); + }); + }); + }); + + it("should throw an error when no mobileProvisionData", async () => { + const provision = "myTestProvision"; + const { injector } = setup({ signing: null }); + + const iOSSigningService: IiOSSigningService = injector.resolve("iOSSigningService"); + assert.isRejected(iOSSigningService.setupSigningFromProvision(projectRoot, projectData, provision), `Failed to find mobile provision with UUID or Name: ${provision}`); + }); + }); +}); diff --git a/test/services/ios/xcodebuild-args-service.ts b/test/services/ios/xcodebuild-args-service.ts new file mode 100644 index 0000000000..84f477c027 --- /dev/null +++ b/test/services/ios/xcodebuild-args-service.ts @@ -0,0 +1,132 @@ +import { Yok } from "../../../lib/common/yok"; +import { DevicePlatformsConstants } from "../../../lib/common/mobile/device-platforms-constants"; +import { XcodebuildArgsService } from "../../../lib/services/ios/xcodebuild-args-service"; +import * as path from "path"; +import { assert } from "chai"; + +function createTestInjector(data: { logLevel: string, hasProjectWorkspace: boolean, connectedDevices?: any[] }): IInjector { + const injector = new Yok(); + injector.register("devicePlatformsConstants", DevicePlatformsConstants); + injector.register("devicesService", { + initialize: async () => ({}), + getDevicesForPlatform: () => data.connectedDevices || [] + }); + injector.register("fs", { + exists: () => data.hasProjectWorkspace + }); + injector.register("logger", { + getLevel: () => data.logLevel + }); + injector.register("xcodebuildArgsService", XcodebuildArgsService); + + return injector; +} + +const projectRoot = "path/to/my/app/folder/platforms/ios"; +const projectName = "myApp"; +const buildOutputPath = path.join(projectRoot, projectName, "archive"); + +function getCommonArgs() { + return [ + "BUILD_DIR=" + path.join(projectRoot, "build"), + "SHARED_PRECOMPS_DIR=" + path.join(projectRoot, 'build', 'sharedpch'), + "-allowProvisioningUpdates" + ]; +} + +function getXcodeProjectArgs(data?: { hasProjectWorkspace: boolean, hasTarget?: boolean }) { + return data && data.hasProjectWorkspace ? [ + "-workspace", path.join(projectRoot, `${projectName}.xcworkspace`), + "-scheme", projectName + ] : [ + "-project", path.join(projectRoot, `${projectName}.xcodeproj`), + data && data.hasTarget ? "-target" : "-scheme", projectName + ]; +} + +function getBuildLoggingArgs(logLevel: string): string[] { + if (logLevel === "INFO") { + return ["-quiet"]; + } + + return []; +} + +describe("xcodebuildArgsService", () => { + describe("getBuildForSimulatorArgs", () => { + _.each([true, false], hasProjectWorkspace => { + _.each(["INFO", "TRACE"], logLevel => { + _.each(["Debug", "Release"], configuration => { + it(`should return correct args when workspace is ${hasProjectWorkspace} with ${logLevel} log level and ${configuration} configuration`, async () => { + const injector = createTestInjector({ logLevel, hasProjectWorkspace }); + + const buildConfig = { buildForDevice: false, release: configuration === "Release" }; + const xcodebuildArgsService = injector.resolve("xcodebuildArgsService"); + const actualArgs = await xcodebuildArgsService.getBuildForSimulatorArgs({ projectRoot }, { projectName }, buildConfig); + + const expectedArgs = [ + "ONLY_ACTIVE_ARCH=NO", + "build", + "-configuration", configuration, + "CODE_SIGN_IDENTITY=", + "-sdk", "iphonesimulator" + ] + .concat(getCommonArgs()) + .concat(getBuildLoggingArgs(logLevel)) + .concat(getXcodeProjectArgs({ hasProjectWorkspace, hasTarget: true })); + + assert.deepEqual(actualArgs, expectedArgs); + }); + }); + }); + }); + }); + describe("getBuildForDeviceArgs", () => { + const testCases = [ + { + name: "should return correct args when there are more than one connected device", + connectedDevices: [{deviceInfo: {activeArchitecture: "arm64"}}, {deviceInfo: {activeArchitecture: "armv7"}}], + expectedArgs: ["ONLY_ACTIVE_ARCH=NO", "-sdk", "iphoneos"].concat(getCommonArgs()) + }, + { + name: "should return correct args when there is only one connected device", + connectedDevices: [{deviceInfo: {activeArchitecture: "arm64"}}], + expectedArgs: ["-sdk", "iphoneos"].concat(getCommonArgs()) + }, + { + name: "should return correct args when no connected devices", + connectedDevices: [], + expectedArgs: ["-sdk", "iphoneos"].concat(getCommonArgs()) + } + ]; + + _.each(testCases, testCase => { + _.each([true, false], hasProjectWorkspace => { + _.each(["INFO", "TRACE"], logLevel => { + _.each(["Debug", "Release"], configuration => { + it(`${testCase.name} when hasProjectWorkspace is ${hasProjectWorkspace} with ${logLevel} log level and ${configuration} configuration`, async () => { + const injector = createTestInjector({ logLevel, hasProjectWorkspace, connectedDevices: testCase.connectedDevices }); + + const platformData = { projectRoot, getBuildOutputPath: () => buildOutputPath }; + const projectData = { projectName }; + const buildConfig = { buildForDevice: true, release: configuration === "Release" }; + const xcodebuildArgsService: IXcodebuildArgsService = injector.resolve("xcodebuildArgsService"); + const actualArgs = await xcodebuildArgsService.getBuildForDeviceArgs(platformData, projectData, buildConfig); + + const expectedArgs = [ + "archive", + "-archivePath", path.join(buildOutputPath, `${projectName}.xcarchive`), + "-configuration", configuration + ] + .concat(getXcodeProjectArgs({ hasProjectWorkspace })) + .concat(testCase.expectedArgs) + .concat(getBuildLoggingArgs(logLevel)); + + assert.deepEqual(actualArgs, expectedArgs); + }); + }); + }); + }); + }); + }); +}); diff --git a/test/services/ios/xcodebuild-service.ts b/test/services/ios/xcodebuild-service.ts new file mode 100644 index 0000000000..17f1dfebca --- /dev/null +++ b/test/services/ios/xcodebuild-service.ts @@ -0,0 +1,93 @@ +import { Yok } from "../../../lib/common/yok"; +import { XcodebuildService } from "../../../lib/services/ios/xcodebuild-service"; +import * as path from "path"; +import { assert } from "chai"; + +const projectRoot = "path/to/my/app/folder/platforms/ios"; +const projectName = "myApp"; +const buildOutputPath = path.join(projectRoot, projectName, "archive"); +const exportOptionsPlistOutput = { + exportFileDir: buildOutputPath, + exportFilePath: path.join(buildOutputPath, `${projectName}.ipa`), + exportOptionsPlistFilePath: "/my/temp/options/plist/file/path" +}; +let actualBuildArgs: string[] = []; +let actualBuildOptions:IXcodebuildCommandOptions = null; + +function createTestInjector(): IInjector { + const injector = new Yok(); + injector.register("exportOptionsPlistService", { + createDevelopmentExportOptionsPlist: () => exportOptionsPlistOutput, + createDistributionExportOptionsPlist: () => exportOptionsPlistOutput + }); + injector.register("xcodebuildArgsService", { + getBuildForDeviceArgs: async () => [], + getBuildForSimulatorArgs: async () => [] + }); + injector.register("xcodebuildCommandService", { + executeCommand: async (args: string[], options: IXcodebuildCommandOptions) => { + actualBuildArgs = args; + actualBuildOptions = options; + } + }); + + injector.register("xcodebuildService", XcodebuildService); + + return injector; +} + +describe("xcodebuildService", () => { + describe("buildForDevice", () => { + it("should build correctly for device", async () => { + const injector = createTestInjector(); + const xcodebuildService = injector.resolve("xcodebuildService"); + const platformData = { getBuildOutputPath: () => buildOutputPath, projectRoot }; + const projectData = { projectName }; + + const buildResult = await xcodebuildService.buildForDevice(platformData, projectData, { }); + + const expectedBuildArgs = [ + '-exportArchive', + '-archivePath', path.join(platformData.getBuildOutputPath(), `${projectName}.xcarchive`), + '-exportPath', exportOptionsPlistOutput.exportFileDir, + '-exportOptionsPlist', exportOptionsPlistOutput.exportOptionsPlistFilePath + ]; + assert.deepEqual(actualBuildArgs, expectedBuildArgs); + assert.deepEqual(actualBuildOptions, { cwd: projectRoot, stdio: undefined }); + assert.deepEqual(buildResult, exportOptionsPlistOutput.exportFilePath); + }); + }); + describe("buildForSimulator", () => { + it("should build correctly for simulator", async () => { + const injector = createTestInjector(); + const xcodebuildService = injector.resolve("xcodebuildService"); + const platformData = { getBuildOutputPath: () => buildOutputPath, projectRoot }; + const projectData = { projectName }; + + await xcodebuildService.buildForSimulator(platformData, projectData, {}); + + assert.deepEqual(actualBuildArgs, []); + assert.deepEqual(actualBuildOptions, { cwd: projectRoot, stdio: undefined }); + }); + }); + describe("buildForAppStore", () => { + it("should build correctly for Appstore", async () => { + const injector = createTestInjector(); + const xcodebuildService = injector.resolve("xcodebuildService"); + const platformData = { getBuildOutputPath: () => buildOutputPath, projectRoot }; + const projectData = { projectName }; + + const buildResult = await xcodebuildService.buildForAppStore(platformData, projectData, {}); + + const expectedBuildArgs = [ + '-exportArchive', + '-archivePath', path.join(platformData.getBuildOutputPath(), `${projectName}.xcarchive`), + '-exportPath', exportOptionsPlistOutput.exportFileDir, + '-exportOptionsPlist', exportOptionsPlistOutput.exportOptionsPlistFilePath + ]; + assert.deepEqual(actualBuildArgs, expectedBuildArgs); + assert.deepEqual(actualBuildOptions, { cwd: projectRoot }); + assert.deepEqual(buildResult, exportOptionsPlistOutput.exportFilePath); + }); + }); +}); diff --git a/test/tns-appstore-upload.ts b/test/tns-appstore-upload.ts index 970a4d7771..47499f0766 100644 --- a/test/tns-appstore-upload.ts +++ b/test/tns-appstore-upload.ts @@ -18,6 +18,7 @@ class AppStore { platformService: any; iOSPlatformData: any; iOSProjectService: any; + xcodebuildService: IXcodebuildService; loggerService: LoggerStub; itmsTransporterService: any; @@ -27,16 +28,11 @@ class AppStore { archiveCalls: number = 0; expectedArchiveCalls: number = 0; exportArchiveCalls: number = 0; - expectedExportArchiveCalls: number = 0; itmsTransporterServiceUploadCalls: number = 0; expectedItmsTransporterServiceUploadCalls: number = 0; before() { this.iOSPlatformData = { - "platformProjectService": this.iOSProjectService = { - archive() { console.log("Archive!"); }, - exportArchive() { console.log("Export Archive!"); } - }, "projectRoot": "/Users/person/git/MyProject" }; this.initInjector({ @@ -62,6 +58,17 @@ class AppStore { chai.assert.equal(platform, "iOS"); return this.iOSPlatformData; } + }, + "xcodebuildService": this.xcodebuildService = { + buildForDevice: async () => { + this.archiveCalls++; + return "/Users/person/git/MyProject/platforms/ios/archive/MyProject.ipa"; + }, + buildForSimulator: async () => ({}), + buildForAppStore: async () => { + this.archiveCalls++; + return "/Users/person/git/MyProject/platforms/ios/archive/MyProject.ipa"; + } } } }); @@ -85,7 +92,6 @@ class AppStore { this.prompter.assert(); chai.assert.equal(this.preparePlatformCalls, this.expectedPreparePlatformCalls, "Mismatched number of $platformService.preparePlatform calls."); chai.assert.equal(this.archiveCalls, this.expectedArchiveCalls, "Mismatched number of iOSProjectService.archive calls."); - chai.assert.equal(this.exportArchiveCalls, this.expectedExportArchiveCalls, "Mismatched number of iOSProjectService.exportArchive calls."); chai.assert.equal(this.itmsTransporterServiceUploadCalls, this.expectedItmsTransporterServiceUploadCalls, "Mismatched number of itmsTransporterService.upload calls."); } @@ -107,27 +113,13 @@ class AppStore { expectArchive() { this.expectedArchiveCalls = 1; - this.iOSProjectService.archive = (projectData: IProjectData) => { + this.xcodebuildService.buildForDevice = (platformData: any, projectData: IProjectData) => { this.archiveCalls++; chai.assert.equal(projectData.projectDir, "/Users/person/git/MyProject"); return Promise.resolve("/Users/person/git/MyProject/platforms/ios/archive/MyProject.xcarchive"); }; } - expectExportArchive(expectedOptions?: { teamID?: string }) { - this.expectedExportArchiveCalls = 1; - this.iOSProjectService.exportArchive = (projectData: IProjectData, options?: { teamID?: string, archivePath?: string }) => { - this.exportArchiveCalls++; - chai.assert.equal(options.archivePath, "/Users/person/git/MyProject/platforms/ios/archive/MyProject.xcarchive", "Expected xcarchive path to be the one that we just archived."); - if (expectedOptions && expectedOptions.teamID) { - chai.assert.equal(options.teamID, expectedOptions.teamID, "Expected --team-id to be passed as teamID to the exportArchive"); - } else { - chai.assert.isUndefined(options.teamID, "Expected teamID in exportArchive to be undefined"); - } - return Promise.resolve("/Users/person/git/MyProject/platforms/ios/archive/MyProject.ipa"); - }; - } - expectITMSTransporterUpload() { this.expectedItmsTransporterServiceUploadCalls = 1; this.itmsTransporterService.upload = (options: IITMSData) => { @@ -144,7 +136,6 @@ class AppStore { this.expectItunesPrompt(); this.expectPreparePlatform(); this.expectArchive(); - this.expectExportArchive(); this.expectITMSTransporterUpload(); await this.command.execute([]); @@ -155,7 +146,6 @@ class AppStore { async itunesconnectArgs() { this.expectPreparePlatform(); this.expectArchive(); - this.expectExportArchive(); this.expectITMSTransporterUpload(); await this.command.execute([AppStore.itunesconnect.user, AppStore.itunesconnect.pass]); @@ -167,7 +157,6 @@ class AppStore { this.expectItunesPrompt(); this.expectPreparePlatform(); this.expectArchive(); - this.expectExportArchive({ teamID: "MyTeamID" }); this.expectITMSTransporterUpload(); this.options.teamId = "MyTeamID"; From d5c9e096f056e219cd44d5c837e7a65f4cc8bf10 Mon Sep 17 00:00:00 2001 From: fatme Date: Mon, 22 Apr 2019 16:27:13 +0300 Subject: [PATCH 3/3] chore: fix PR comments --- lib/services/android-project-service.ts | 4 ++-- lib/services/ios-project-service.ts | 1 - lib/services/ios/export-options-plist-service.ts | 4 ++-- lib/services/ios/xcodebuild-command-service.ts | 4 ++-- lib/services/platform-service.ts | 2 +- test/tns-appstore-upload.ts | 2 +- 6 files changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/services/android-project-service.ts b/lib/services/android-project-service.ts index 97b3b9e58f..a73447b5a1 100644 --- a/lib/services/android-project-service.ts +++ b/lib/services/android-project-service.ts @@ -348,9 +348,9 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject return nativescript && (nativescript.android || (nativescript.platforms && nativescript.platforms.android)); } - public async stopServices(platformData: IPlatformData): Promise { + public async stopServices(projectRoot: string): Promise { const result = await this.$gradleCommandService.executeCommand(["--stop", "--quiet"], { - cwd: platformData.projectRoot, + cwd: projectRoot, message: "Gradle stop services...", stdio: "pipe" }); diff --git a/lib/services/ios-project-service.ts b/lib/services/ios-project-service.ts index 6ddf99e502..33fd672742 100644 --- a/lib/services/ios-project-service.ts +++ b/lib/services/ios-project-service.ts @@ -210,7 +210,6 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ this.$xcodebuildService.buildForSimulator(platformData, projectData, buildConfig)); } - // TODO: Check if we need to validate the identifier here this.validateApplicationIdentifier(projectData); } diff --git a/lib/services/ios/export-options-plist-service.ts b/lib/services/ios/export-options-plist-service.ts index b25507b011..d64a3dfe44 100644 --- a/lib/services/ios/export-options-plist-service.ts +++ b/lib/services/ios/export-options-plist-service.ts @@ -41,7 +41,7 @@ export class ExportOptionsPlistService implements IExportOptionsPlistService { return { exportFileDir, exportFilePath, exportOptionsPlistFilePath }; } - public createDistributionExportOptionsPlist(projectRoot: string, projectData: IProjectData, buildConfig: IBuildConfig): IExportOptionsPlistOutput { + public createDistributionExportOptionsPlist(archivePath: string, projectData: IProjectData, buildConfig: IBuildConfig): IExportOptionsPlistOutput { const provision = buildConfig.provision || buildConfig.mobileProvisionIdentifier; const teamId = buildConfig.teamId; let plistTemplate = ` @@ -77,7 +77,7 @@ export class ExportOptionsPlistService implements IExportOptionsPlistService { const exportOptionsPlistFilePath = temp.path({ prefix: "export-", suffix: ".plist" }); this.$fs.writeFile(exportOptionsPlistFilePath, plistTemplate); - const exportFileDir = path.resolve(path.join(projectRoot, "/build/archive")); + const exportFileDir = path.resolve(path.dirname(archivePath)); const exportFilePath = path.join(exportFileDir, projectData.projectName + ".ipa"); return { exportFileDir, exportFilePath, exportOptionsPlistFilePath }; diff --git a/lib/services/ios/xcodebuild-command-service.ts b/lib/services/ios/xcodebuild-command-service.ts index eb81c3e2fa..172a19b873 100644 --- a/lib/services/ios/xcodebuild-command-service.ts +++ b/lib/services/ios/xcodebuild-command-service.ts @@ -8,10 +8,10 @@ export class XcodebuildCommandService implements IXcodebuildCommandService { ) { } public async executeCommand(args: string[], options: { cwd: string, stdio: string, message?: string, spawnOptions?: any }): Promise { - const { message, cwd, stdio = "inherit", spawnOptions } = options; + const { message, cwd, stdio, spawnOptions } = options; this.$logger.info(message || "Xcode build..."); - const childProcessOptions = { cwd, stdio }; + const childProcessOptions = { cwd, stdio: stdio || "inherit" }; try { const commandResult = await this.$childProcess.spawnFromEvent("xcodebuild", diff --git a/lib/services/platform-service.ts b/lib/services/platform-service.ts index 615b5a7193..d6b935eca6 100644 --- a/lib/services/platform-service.ts +++ b/lib/services/platform-service.ts @@ -694,7 +694,7 @@ export class PlatformService extends EventEmitter implements IPlatformService { let errorMessage; try { - await platformData.platformProjectService.stopServices(platformData); + await platformData.platformProjectService.stopServices(platformData.projectRoot); } catch (err) { errorMessage = err.message; } diff --git a/test/tns-appstore-upload.ts b/test/tns-appstore-upload.ts index 47499f0766..f6225139f9 100644 --- a/test/tns-appstore-upload.ts +++ b/test/tns-appstore-upload.ts @@ -64,7 +64,7 @@ class AppStore { this.archiveCalls++; return "/Users/person/git/MyProject/platforms/ios/archive/MyProject.ipa"; }, - buildForSimulator: async () => ({}), + buildForSimulator: () => Promise.resolve(), buildForAppStore: async () => { this.archiveCalls++; return "/Users/person/git/MyProject/platforms/ios/archive/MyProject.ipa";