diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 9c2dc19f67..a1ca02d469 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -10,6 +10,7 @@ $injector.require("projectDataService", "./services/project-data-service"); $injector.require("projectService", "./services/project-service"); $injector.require("androidProjectService", "./services/android-project-service"); $injector.require("iOSProjectService", "./services/ios-project-service"); +$injector.require("iOSProvisionService", "./services/ios-provision-service"); $injector.require("cocoapodsService", "./services/cocoapods-service"); diff --git a/lib/commands/build.ts b/lib/commands/build.ts index def9affbca..ecafd19f2f 100644 --- a/lib/commands/build.ts +++ b/lib/commands/build.ts @@ -1,6 +1,7 @@ export class BuildCommandBase { constructor(protected $options: IOptions, - private $platformService: IPlatformService) { } + protected $platformsData: IPlatformsData, + protected $platformService: IPlatformService) { } executeCore(args: string[]): IFuture { return (() => { @@ -17,9 +18,9 @@ export class BuildCommandBase { export class BuildIosCommand extends BuildCommandBase implements ICommand { constructor(protected $options: IOptions, - private $platformsData: IPlatformsData, + $platformsData: IPlatformsData, $platformService: IPlatformService) { - super($options, $platformService); + super($options, $platformsData, $platformService); } public allowedParameters: ICommandParameter[] = []; @@ -27,15 +28,19 @@ export class BuildIosCommand extends BuildCommandBase implements ICommand { public execute(args: string[]): IFuture { return this.executeCore([this.$platformsData.availablePlatforms.iOS]); } + + public canExecute(args: string[]): IFuture { + return this.$platformService.validateOptions(this.$platformsData.availablePlatforms.iOS); + } } $injector.registerCommand("build|ios", BuildIosCommand); export class BuildAndroidCommand extends BuildCommandBase implements ICommand { constructor(protected $options: IOptions, - private $platformsData: IPlatformsData, + $platformsData: IPlatformsData, private $errors: IErrors, $platformService: IPlatformService) { - super($options, $platformService); + super($options, $platformsData, $platformService); } public execute(args: string[]): IFuture { @@ -49,7 +54,7 @@ export class BuildAndroidCommand extends BuildCommandBase implements ICommand { if (this.$options.release && (!this.$options.keyStorePath || !this.$options.keyStorePassword || !this.$options.keyStoreAlias || !this.$options.keyStoreAliasPassword)) { this.$errors.fail("When producing a release build, you need to specify all --key-store-* options."); } - return args.length === 0; + return args.length === 0 && this.$platformService.validateOptions(this.$platformsData.availablePlatforms.Android).wait(); }).future()(); } } diff --git a/lib/commands/deploy.ts b/lib/commands/deploy.ts index adf9aad7f0..3d3557644d 100644 --- a/lib/commands/deploy.ts +++ b/lib/commands/deploy.ts @@ -23,7 +23,7 @@ export class DeployOnDeviceCommand implements ICommand { this.$errors.fail("When producing a release build, you need to specify all --key-store-* options."); } - return true; + return this.$platformService.validateOptions(args[0]).wait(); }).future()(); } diff --git a/lib/commands/prepare.ts b/lib/commands/prepare.ts index 660803f796..fcac356ab1 100644 --- a/lib/commands/prepare.ts +++ b/lib/commands/prepare.ts @@ -9,6 +9,10 @@ export class PrepareCommand implements ICommand { }).future()(); } + public canExecute(args: string[]): IFuture { + return this.$platformService.validateOptions(args[0]); + } + allowedParameters = [this.$platformCommandParameter]; } $injector.registerCommand("prepare", PrepareCommand); diff --git a/lib/commands/run.ts b/lib/commands/run.ts index 5dd2f0b810..e4d336c0da 100644 --- a/lib/commands/run.ts +++ b/lib/commands/run.ts @@ -1,6 +1,6 @@ export class RunCommandBase { - constructor(private $platformService: IPlatformService, - private $usbLiveSyncService: ILiveSyncService, + constructor(protected $platformService: IPlatformService, + protected $usbLiveSyncService: ILiveSyncService, protected $options: IOptions) { } public executeCore(args: string[]): IFuture { @@ -16,7 +16,8 @@ export class RunIosCommand extends RunCommandBase implements ICommand { constructor($platformService: IPlatformService, private $platformsData: IPlatformsData, $usbLiveSyncService: ILiveSyncService, - $options: IOptions) { + $options: IOptions, + private $injector: IInjector) { super($platformService, $usbLiveSyncService, $options); } @@ -25,6 +26,10 @@ export class RunIosCommand extends RunCommandBase implements ICommand { public execute(args: string[]): IFuture { return this.executeCore([this.$platformsData.availablePlatforms.iOS]); } + + public canExecute(args: string[]): IFuture { + return this.$platformService.validateOptions(this.$platformsData.availablePlatforms.iOS); + } } $injector.registerCommand("run|ios", RunIosCommand); @@ -48,7 +53,7 @@ export class RunAndroidCommand extends RunCommandBase implements ICommand { if (this.$options.release && (!this.$options.keyStorePath || !this.$options.keyStorePassword || !this.$options.keyStoreAlias || !this.$options.keyStoreAliasPassword)) { this.$errors.fail("When producing a release build, you need to specify all --key-store-* options."); } - return args.length === 0; + return args.length === 0 && this.$platformService.validateOptions(this.$platformsData.availablePlatforms.Android).wait(); }).future()(); } } diff --git a/lib/declarations.ts b/lib/declarations.ts index 6ea862903b..8b87f1c5b2 100644 --- a/lib/declarations.ts +++ b/lib/declarations.ts @@ -96,6 +96,7 @@ interface IOptions extends ICommonOptions { liveEdit: boolean; chrome: boolean; clean: boolean; + provision: any; } interface IInitService { diff --git a/lib/definitions/platform.d.ts b/lib/definitions/platform.d.ts index 3db446dc6c..137c63a920 100644 --- a/lib/definitions/platform.d.ts +++ b/lib/definitions/platform.d.ts @@ -76,6 +76,11 @@ interface IPlatformService { */ installApplication(device: Mobile.IDevice): IFuture; + /** + * Gets first chance to validate the options provided as command line arguments. + */ + validateOptions(platform: string): IFuture; + /** * Executes prepare, build and installOnPlatform when necessary to ensure that the latest version of the app is installed on specified platform. * - When --clean option is specified it builds the app on every change. If not, build is executed only when there are native changes. diff --git a/lib/definitions/project-changes.d.ts b/lib/definitions/project-changes.d.ts index dca107afd0..a99b196a49 100644 --- a/lib/definitions/project-changes.d.ts +++ b/lib/definitions/project-changes.d.ts @@ -4,6 +4,8 @@ interface IPrepareInfo { release: boolean; changesRequireBuild: boolean; changesRequireBuildTime: string; + + iOSProvisioningProfileUUID?: string; } interface IProjectChangesInfo { diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index f0174bc2f9..c839ce8ea2 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -103,6 +103,11 @@ interface IPlatformProjectService { */ afterCreateProject(projectRoot: string): void; + /** + * Gets first chance to validate the options provided as command line arguments. + */ + validateOptions(): IFuture; + buildProject(projectRoot: string, buildConfig?: IBuildConfig): IFuture; /** diff --git a/lib/options.ts b/lib/options.ts index 8035c78414..d9f840f560 100644 --- a/lib/options.ts +++ b/lib/options.ts @@ -17,6 +17,7 @@ export class Options extends commonOptionsLibPath.OptionsBase { copyFrom: { type: OptionType.String }, linkTo: { type: OptionType.String }, forDevice: { type: OptionType.Boolean }, + provision: { type: OptionType.Object }, client: { type: OptionType.Boolean, default: true }, production: { type: OptionType.Boolean }, debugTransport: { type: OptionType.Boolean }, diff --git a/lib/services/android-project-service.ts b/lib/services/android-project-service.ts index ef188a30eb..9333e999e2 100644 --- a/lib/services/android-project-service.ts +++ b/lib/services/android-project-service.ts @@ -75,6 +75,10 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject return this._platformData; } + public validateOptions(): IFuture { + return Future.fromResult(true); + } + public getAppResourcesDestinationDirectoryPath(frameworkVersion?: string): string { if (this.canUseGradle(frameworkVersion)) { return path.join(this.platformData.projectRoot, "src", "main", "res"); diff --git a/lib/services/ios-project-service.ts b/lib/services/ios-project-service.ts index c4ec93b7a1..df32e3dbb9 100644 --- a/lib/services/ios-project-service.ts +++ b/lib/services/ios-project-service.ts @@ -11,10 +11,8 @@ import { PlistSession } from "plist-merge-patch"; import { EOL } from "os"; import * as temp from "temp"; import * as plist from "plist"; -import { cert, provision } from "ios-mobileprovision-finder"; import { Xcode } from "pbxproj-dom/xcode"; - -type XcodeSigningStyle = "Manual" | "Automatic"; +import { IOSProvisionService } from "./ios-provision-service"; export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServiceBase implements IPlatformProjectService { private static XCODE_PROJECT_EXT_NAME = ".xcodeproj"; @@ -45,7 +43,8 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ private $devicesService: Mobile.IDevicesService, private $mobileHelper: Mobile.IMobileHelper, private $pluginVariablesService: IPluginVariablesService, - private $xcprojService: IXcprojService) { + private $xcprojService: IXcprojService, + private $iOSProvisionService: IOSProvisionService) { super($fs, $projectData, $projectDataService); } @@ -78,6 +77,17 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ }; } + public validateOptions(): IFuture { + return (() => { + if (this.$options.provision === true) { + this.$iOSProvisionService.list().wait(); + this.$errors.failWithoutHelp("Please provide provisioning profile uuid or name with the --provision option."); + return false; + } + return true; + }).future()(); + } + public getAppResourcesDestinationDirectoryPath(): string { let frameworkVersion = this.getFrameworkVersion(this.platformData.frameworkPackageName); @@ -248,6 +258,13 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ basicArgs.push("-xcconfig", path.join(projectRoot, this.$projectData.projectName, "build.xcconfig")); } + // if (this.$logger.getLevel() === "INFO") { + // let xcodeBuildVersion = this.getXcodeVersion(); + // if (helpers.versionCompare(xcodeBuildVersion, "8.0") >= 0) { + // basicArgs.push("-quiet"); + // } + // } + let buildForDevice = this.$options.forDevice || (buildConfig && buildConfig.buildForDevice); if (buildForDevice) { this.buildForDevice(projectRoot, basicArgs, buildConfig).wait(); @@ -296,7 +313,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ let xcodeBuildVersion = this.getXcodeVersion(); if (helpers.versionCompare(xcodeBuildVersion, "8.0") >= 0) { - buildConfig = this.getBuildConfig(projectRoot, buildConfig).wait(); + this.setupAutomaticSigning(projectRoot, buildConfig).wait(); } if (buildConfig && buildConfig.codeSignIdentity) { @@ -311,12 +328,85 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ args.push(`DEVELOPMENT_TEAM=${buildConfig.teamIdentifier}`); } + // this.$logger.out("xcodebuild..."); this.$childProcess.spawnFromEvent("xcodebuild", args, "exit", { cwd: this.$options, stdio: 'inherit' }).wait(); + // this.$logger.out("xcodebuild build succeded."); + this.createIpa(projectRoot).wait(); }).future()(); } + private setupManualSigning(projectRoot: string, buildConfig?: IiOSBuildConfig): IFuture { + return (() => { + if (this.$options.provision) { + const pbxprojPath = path.join(projectRoot, this.$projectData.projectName + ".xcodeproj", "project.pbxproj"); + const xcode = Xcode.open(pbxprojPath); + const signing = xcode.getSigning(this.$projectData.projectName); + + let shouldUpdateXcode = false; + if (signing && signing.style === "Manual") { + for(let config in signing.configurations) { + let options = signing.configurations[config]; + if (options.name !== this.$options.provision && options.name !== this.$options.provision) { + shouldUpdateXcode = true; + break; + } + } + } else { + shouldUpdateXcode = true; + } + + if (shouldUpdateXcode) { + // This is slow, it read through 260 mobileprovision files on my machine and does quite some checking whether provisioning profiles and devices will match. + // That's why we try to avoid id by checking in the Xcode first. + const pickStart = Date.now(); + const mobileprovision = this.$iOSProvisionService.pick(this.$options.provision).wait(); + 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: " + this.$options.provision); + } + + xcode.setManualSigningStyle(this.$projectData.projectName, { + 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.save(); + + // this.cache(uuid); + this.$logger.trace("Set Manual signing style and provisioning profile."); + } else { + this.$logger.trace("The specified provisioning profile allready set in the Xcode."); + } + } else { + // read uuid from Xcode and cache... + } + }).future()(); + } + + private setupAutomaticSigning(projectRoot: string, buildConfig?: IiOSBuildConfig): IFuture { + return (() => { + const pbxprojPath = path.join(projectRoot, this.$projectData.projectName + ".xcodeproj", "project.pbxproj"); + const xcode = Xcode.open(pbxprojPath); + const signing = xcode.getSigning(this.$projectData.projectName); + + if (!this.$options.provision && !(signing && signing.style === "Manual" && !this.$options.teamId)) { + if (buildConfig) { + delete buildConfig.teamIdentifier; + } + + const teamId = this.getDevelopmentTeam(); + + xcode.setAutomaticSigningStyle(this.$projectData.projectName, teamId); + xcode.save(); + this.$logger.trace("Set Automatic signing style and team."); + } + }).future()(); + } + private buildForSimulator(projectRoot: string, args: string[], buildConfig?: IiOSBuildConfig): IFuture { return (() => { args = args.concat([ @@ -339,10 +429,15 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ let xcrunArgs = [ "-sdk", "iphoneos", "PackageApplication", - "-v", path.join(buildOutputPath, this.$projectData.projectName + ".app"), + path.join(buildOutputPath, this.$projectData.projectName + ".app"), "-o", path.join(buildOutputPath, this.$projectData.projectName + ".ipa") ]; + // if (this.$logger.getLevel() !== "INFO") { + xcrunArgs.push("-verbose"); + // } + // this.$logger.out("Packaging project..."); this.$childProcess.spawnFromEvent("xcrun", xcrunArgs, "exit", { cwd: this.$options, stdio: 'inherit' }).wait(); + // this.$logger.out("Project package succeeded."); }).future()(); } @@ -494,6 +589,11 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } public prepareProject(): void { + if (this.$options.provision) { + let projectRoot = path.join(this.$projectData.platformsDir, "ios"); + this.setupManualSigning(projectRoot).wait(); + } + let project = this.createPbxProj(); this.provideLaunchScreenIfMissing(); @@ -988,78 +1088,7 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f return xcodeBuildVersion; } - private getBuildConfig(projectRoot: string, buildConfig: IiOSBuildConfig): IFuture { - return (() => { - // TRICKY: I am not sure why we totally disregard the buildConfig parameter here. - buildConfig = buildConfig || {}; - let isXCConfigDefined = false; - - if (this.$options.teamId) { - buildConfig.teamIdentifier = this.$options.teamId; - } else { - buildConfig = this.readXCConfigSigning(); - isXCConfigDefined = true; - if (!buildConfig.codeSignIdentity && !buildConfig.mobileProvisionIdentifier && !buildConfig.teamIdentifier) { - buildConfig = this.readBuildConfigFromPlatforms(); - } - } - - let signingStyle: XcodeSigningStyle; - if (buildConfig.codeSignIdentity || buildConfig.mobileProvisionIdentifier) { - signingStyle = "Manual"; - } else if (buildConfig.teamIdentifier) { - signingStyle = "Automatic"; - } else if (helpers.isInteractive()) { - isXCConfigDefined = false; - let signingStyles = [ - "Manual - Select existing provisioning profile for use", - "Automatic - Select Team ID for signing and let Xcode select managed provisioning profile" - ]; - let signingStyleIndex = signingStyles.indexOf(this.$prompter.promptForChoice("Select codesiging style", signingStyles).wait()); - signingStyle = new Array("Manual", "Automatic")[signingStyleIndex]; - - switch(signingStyle) { - case "Manual": - let profile = this.getProvisioningProfile().wait(); - if (!profile) { - this.$errors.failWithoutHelp("No matching provisioning profile found."); - } - this.persistProvisioningProfiles(profile.UUID).wait(); - this.$logger.info("Apply provisioning profile: " + profile.Name + " (" + profile.TeamName + ") " + profile.Type + " UUID: " + profile.UUID); - buildConfig.mobileProvisionIdentifier = profile.UUID; - buildConfig.teamIdentifier = profile.TeamIdentifier[0]; - break; - case "Automatic": - buildConfig.teamIdentifier = this.getDevelopmentTeam().wait(); - this.persistDevelopmentTeam(buildConfig.teamIdentifier).wait(); - break; - } - } - - if (signingStyle && !isXCConfigDefined) { - const pbxprojPath = path.join(projectRoot, this.$projectData.projectName + ".xcodeproj", "project.pbxproj"); - const xcode = Xcode.open(pbxprojPath); - switch(signingStyle) { - case "Manual": - xcode.setManualSigningStyle(this.$projectData.projectName); - break; - case "Automatic": - xcode.setAutomaticSigningStyle(this.$projectData.projectName, buildConfig.teamIdentifier); - break; - } - xcode.save(); - } - - if (isXCConfigDefined) { - delete buildConfig.mobileProvisionIdentifier; - delete buildConfig.teamIdentifier; - } - - return buildConfig; - }).future()(); - } - - private getDevelopmentTeams(): Array<{ id: string, name: string }> { + private getDevelopmentTeams(): Array<{id:string, name: string}> { let dir = path.join(process.env.HOME, "Library/MobileDevice/Provisioning Profiles/"); let files = this.$fs.readDirectory(dir); let teamIds: any = {}; @@ -1072,9 +1101,9 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f teamIds[teamId] = teamName; } } - let teamIdsArray = new Array<{ id: string, name: string }>(); + let teamIdsArray = new Array<{id:string, name: string}>(); for (let teamId in teamIds) { - teamIdsArray.push({ id: teamId, name: teamIds[teamId] }); + teamIdsArray.push({ id:teamId, name:teamIds[teamId] }); } return teamIdsArray; } @@ -1083,7 +1112,7 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f let findStr = "" + name + ""; let index = text.indexOf(findStr); if (index > 0) { - index = text.indexOf("", index + findStr.length); + index = text.indexOf("", index+findStr.length); if (index > 0) { index += "".length; let endIndex = text.indexOf("", index); @@ -1094,76 +1123,39 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f return null; } - private readXCConfigSigning(): IiOSBuildConfig { - const result: IiOSBuildConfig = {}; + private readTeamId(): string { let xcconfigFile = path.join(this.$projectData.appResourcesDirectoryPath, this.platformData.normalizedPlatformName, "build.xcconfig"); if (this.$fs.exists(xcconfigFile)) { let text = this.$fs.readText(xcconfigFile); + let teamId: string; text.split(/\r?\n/).forEach((line) => { line = line.replace(/\/(\/)[^\n]*$/, ""); - const read = (name: string) => { - if (line.indexOf(name) >= 0) { - let value = line.substr(line.lastIndexOf("=") + 1).trim(); - if (value.charAt(value.length - 1) === ';') { - value = value.substr(0, value.length - 1).trim(); - } - return value; + if (line.indexOf("DEVELOPMENT_TEAM") >= 0) { + teamId = line.split("=")[1].trim(); + if (teamId[teamId.length-1] === ';') { + teamId = teamId.slice(0, -1); } - return undefined; - }; - result.teamIdentifier = read("DEVELOPMENT_TEAM") || result.teamIdentifier; - result.codeSignIdentity = read("CODE_SIGN_IDENTITY") || result.codeSignIdentity; - result.mobileProvisionIdentifier = read("PROVISIONING_PROFILE[sdk=iphoneos*]") || result.mobileProvisionIdentifier; + } }); - } - return result; - } - - private getProvisioningProfile(): IFuture { - return (() => { - let profile: provision.MobileProvision; - - const allCertificates = cert.read(); - const allProfiles = provision.read(); - const query: provision.Query = { - Certificates: allCertificates.valid, - AppId: this.$projectData.projectId, - Type: "Development" - }; - - if (this.$options.device) { - query.ProvisionedDevices = [this.$options.device]; - } else { - this.$devicesService.initialize().wait(); - let deviceUDIDs = _(this.$devicesService.getDeviceInstances()) - .filter(d => this.$mobileHelper.isiOSPlatform(d.deviceInfo.platform)) - .map(d => d.deviceInfo.identifier) - .toJSON(); - query.ProvisionedDevices = deviceUDIDs; - } - - const result = provision.select(allProfiles, query); - const choiceMap = result.eligable.reduce((acc, p) => { - acc[`'${p.Name}' (${p.TeamName}) ${p.Type}`] = p; - return acc; - }, <{ [display: string]: provision.MobileProvision }>{}); - - const choices = Object.keys(choiceMap); - if (choices.length > 0) { - const choice = this.$prompter.promptForChoice( - `Select provisioning profiles (found ${result.eligable.length} eligable, and ${result.nonEligable.length} non-eligable)`, - choices - ).wait(); - profile = choiceMap[choice]; + if (teamId) { + return teamId; } - - return profile; - }).future()(); + } + let fileName = path.join(this.platformData.projectRoot, "teamid"); + if (this.$fs.exists(fileName)) { + return this.$fs.readText(fileName); + } + return null; } - private getDevelopmentTeam(): IFuture { - return (() => { - let teamId: string; + private getDevelopmentTeam(): string { + let teamId: string; + if (this.$options.teamId) { + teamId = this.$options.teamId; + } else { + teamId = this.readTeamId(); + } + if (!teamId) { let teams = this.getDevelopmentTeams(); this.$logger.warn("Xcode 8 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 commnads."); @@ -1177,73 +1169,27 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } let choice = this.$prompter.promptForChoice('Found multiple development teams, select one:', choices).wait(); teamId = teams[choices.indexOf(choice)].id; - } - return teamId; - }).future()(); - } - - private persistProvisioningProfiles(uuid: string): IFuture { - return (() => { - let choicesPersist = [ - "Yes, set the PROVISIONING_PROFILE[sdk=iphoneos*] setting in build.xcconfig file.", - "Yes, persist the mobileprovision uuid in platforms folder.", - "No, don't persist this setting." - ]; - let choicePersist = this.$prompter.promptForChoice("Do you want to make mobileprovision: " + uuid + " a persistent choice for your app?", choicesPersist).wait(); - switch (choicesPersist.indexOf(choicePersist)) { - case 0: - let xcconfigFile = path.join(this.$projectData.appResourcesDirectoryPath, this.platformData.normalizedPlatformName, "build.xcconfig"); - this.$fs.appendFile(xcconfigFile, "\nPROVISIONING_PROFILE[sdk=iphoneos*] = " + uuid + "\n"); - break; - case 1: - this.$fs.writeFile(path.join(this.platformData.projectRoot, "mobileprovision"), uuid); - const teamidPath = path.join(this.platformData.projectRoot, "teamid"); - if (this.$fs.exists(teamidPath)) { - this.$fs.deleteFile(teamidPath); - } - break; - default: - break; - } - }).future()(); - } - private persistDevelopmentTeam(teamId: string): IFuture { - return (() => { - let 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." - ]; - let choicePersist = this.$prompter.promptForChoice("Do you want to make teamId: " + teamId + " a persistent choice for your app?", choicesPersist).wait(); - switch (choicesPersist.indexOf(choicePersist)) { - case 0: - let xcconfigFile = path.join(this.$projectData.appResourcesDirectoryPath, this.platformData.normalizedPlatformName, "build.xcconfig"); - this.$fs.appendFile(xcconfigFile, "\nDEVELOPMENT_TEAM = " + teamId + "\n"); - break; - case 1: - this.$fs.writeFile(path.join(this.platformData.projectRoot, "teamid"), teamId); - const mobileprovisionPath = path.join(this.platformData.projectRoot, "mobileprovision"); - if (this.$fs.exists(mobileprovisionPath)) { - this.$fs.deleteFile(mobileprovisionPath); - } - break; - default: - break; + let 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." + ]; + let choicePersist = this.$prompter.promptForChoice("Do you want to make teamId: "+teamId+" a persistent choice for your app?", choicesPersist).wait(); + switch (choicesPersist.indexOf(choicePersist)) { + case 0: + let xcconfigFile = path.join(this.$projectData.appResourcesDirectoryPath, this.platformData.normalizedPlatformName, "build.xcconfig"); + this.$fs.appendFile(xcconfigFile, "\nDEVELOPMENT_TEAM = " + teamId + "\n"); + break; + case 1: + this.$fs.writeFile(path.join(this.platformData.projectRoot, "teamid"), teamId); + break; + default: + break; + } } - }).future()(); - } - - private readBuildConfigFromPlatforms(): IiOSBuildConfig { - let mobileprovisionPath = path.join(this.platformData.projectRoot, "mobileprovision"); - if (this.$fs.exists(mobileprovisionPath)) { - return { mobileProvisionIdentifier: this.$fs.readText(mobileprovisionPath) }; - } - let teamidPath = path.join(this.platformData.projectRoot, "teamid"); - if (this.$fs.exists(teamidPath)) { - return { teamIdentifier: this.$fs.readText(teamidPath) }; } - return {}; + return teamId; } } diff --git a/lib/services/ios-provision-service.ts b/lib/services/ios-provision-service.ts new file mode 100644 index 0000000000..15f343c6a0 --- /dev/null +++ b/lib/services/ios-provision-service.ts @@ -0,0 +1,98 @@ +import * as mobileprovision from "ios-mobileprovision-finder"; +import { createTable } from "../common/helpers"; + +const months = ["Jan", "Feb", "Marc", "Apr", "May", "June", "July", "Aug", "Sept", "Oct", "Nov", "Dec"]; +function formatDate(date: Date): string { + return `${date.getDay()} ${months[date.getMonth()]} ${date.getFullYear()}`; +} + +export class IOSProvisionService { + constructor( + private $logger: ILogger, + private $options: IOptions, + private $devicesService: Mobile.IDevicesService, + private $projectData: IProjectData, + private $mobileHelper: Mobile.IMobileHelper) { + } + + public pick(uuidOrName: string): IFuture { + return (() => { + const { match } = this.queryProvisioningProfilesAndDevices().wait(); + return match.eligable.find(prov => prov.UUID === uuidOrName) + || match.eligable.find(prov => prov.Name === uuidOrName) + || match.nonEligable.find(prov => prov.UUID === uuidOrName) + || match.nonEligable.find(prov => prov.Name === uuidOrName); + }).future()(); + } + + public list(): IFuture { + return (() => { + const { devices, match } = this.queryProvisioningProfilesAndDevices().wait(); + + function formatSupportedDeviceCount(prov: mobileprovision.provision.MobileProvision) { + if (devices.length > 0 && prov.Type === "Development") { + return prov.ProvisionedDevices.reduce((count, device) => count + (devices.indexOf(device) >= 0 ? 1 : 0), 0) + "/" + devices.length + " targets"; + } else { + return ""; + } + } + + function formatTotalDeviceCount(prov: mobileprovision.provision.MobileProvision) { + if (prov.Type === "Development" && prov.ProvisionedDevices) { + return prov.ProvisionedDevices.length + " total"; + } else if (prov.Type === "AdHoc") { + return "all"; + } else { + return ""; + } + } + + const table = createTable(["Provision Name / Provision UUID / App Id", "Team", "Type / Due", "Devices"], []); + + function pushProvision(prov: mobileprovision.provision.MobileProvision) { + table.push(["", "", "", ""]); + table.push(["\"" + prov.Name + "\"", prov.TeamName, prov.Type, formatTotalDeviceCount(prov)]); + table.push([prov.UUID, prov.TeamIdentifier && prov.TeamIdentifier.length > 0 ? "(" + prov.TeamIdentifier[0] + ")" : "", formatDate(prov.ExpirationDate), formatSupportedDeviceCount(prov)]); + table.push([prov.Entitlements["application-identifier"], "", "", ""]); + } + match.eligable.forEach(prov => pushProvision(prov)); + + this.$logger.out(table.toString()); + this.$logger.out(); + this.$logger.out("There are also " + match.nonEligable.length + " non-eligable provisioning profiles."); + this.$logger.out(); + + }).future()(); + } + + private queryProvisioningProfilesAndDevices(): IFuture<{ devices: string[], match: mobileprovision.provision.Result }> { + return (() => { + const certificates = mobileprovision.cert.read(); + const provisions = mobileprovision.provision.read(); + + const query: mobileprovision.provision.Query = { + Certificates: certificates.valid, + Unique: true, + AppId: this.$projectData.projectId + }; + + let devices: string[] = []; + if (this.$options.device) { + devices = [this.$options.device]; + } else { + this.$devicesService.initialize({ + platform: "ios" + }).wait(); + devices = _(this.$devicesService.getDeviceInstances()) + .filter(d => this.$mobileHelper.isiOSPlatform(d.deviceInfo.platform)) + .map(d => d.deviceInfo.identifier) + .toJSON(); + } + + const match = mobileprovision.provision.select(provisions, query); + + return { devices, match }; + }).future<{ devices: string[], match: mobileprovision.provision.Result }>()(); + } +} +$injector.register("iOSProvisionService", IOSProvisionService); diff --git a/lib/services/platform-service.ts b/lib/services/platform-service.ts index adad0a5773..7776fbf022 100644 --- a/lib/services/platform-service.ts +++ b/lib/services/platform-service.ts @@ -226,6 +226,13 @@ export class PlatformService implements IPlatformService { }).future()(); } + public validateOptions(platform: string): IFuture { + return (() => { + let platformData = this.$platformsData.getPlatformData(platform); + return platformData.platformProjectService.validateOptions().wait(); + }).future()(); + } + @helpers.hook('prepare') private preparePlatformCore(platform: string, changesInfo?: IProjectChangesInfo): IFuture { return (() => { diff --git a/lib/services/project-changes-service.ts b/lib/services/project-changes-service.ts index 6a484a3ee2..fdc4120102 100644 --- a/lib/services/project-changes-service.ts +++ b/lib/services/project-changes-service.ts @@ -76,6 +76,16 @@ export class ProjectChangesService implements IProjectChangesService { ]); } } + if (platform.toLowerCase() === this.$devicePlatformsConstants.iOS.toLowerCase()) { + const nextCommandProvisionUUID = this.$options.provision; + // We should consider reading here the provisioning profile UUID from the xcodeproj and xcconfig. + const prevProvisionUUID = this._prepareInfo.iOSProvisioningProfileUUID; + if (nextCommandProvisionUUID !== prevProvisionUUID) { + this._changesInfo.nativeChanged = true; + this._changesInfo.configChanged = true; + this._prepareInfo.iOSProvisioningProfileUUID = nextCommandProvisionUUID; + } + } if (this.$options.bundle !== this._prepareInfo.bundle || this.$options.release !== this._prepareInfo.release) { this._changesInfo.appFilesChanged = true; this._changesInfo.appResourcesChanged = true; diff --git a/package.json b/package.json index 9139a2bf2b..b19a1ba9f9 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "mute-stream": "0.0.5", "open": "0.0.5", "osenv": "0.1.3", - "pbxproj-dom": "1.0.3", + "pbxproj-dom": "^1.0.7", "plist": "1.1.0", "plist-merge-patch": "0.0.9", "plistlib": "0.2.1", diff --git a/test/ios-project-service.ts b/test/ios-project-service.ts index fd54f31ae1..0cbb090c5e 100644 --- a/test/ios-project-service.ts +++ b/test/ios-project-service.ts @@ -51,6 +51,7 @@ function createTestInjector(projectPath: string, projectName: string): IInjector testInjector.register("iOSEmulatorServices", {}); testInjector.register("cocoapodsService", CocoaPodsService); testInjector.register("iOSProjectService", iOSProjectServiceLib.IOSProjectService); + testInjector.register("iOSProvisionService", {}); testInjector.register("logger", LoggerLib.Logger); testInjector.register("options", OptionsLib.Options); testInjector.register("projectData", { diff --git a/test/stubs.ts b/test/stubs.ts index 3bbdeece57..0f6bc04257 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -281,6 +281,9 @@ export class PlatformProjectServiceStub implements IPlatformProjectService { getAppResourcesDestinationDirectoryPath(): string { return ""; } + validateOptions(): IFuture { + return Future.fromResult(true); + } validate(): IFuture { return Future.fromResult(); } @@ -573,6 +576,10 @@ export class CommandsService implements ICommandsService { export class PlatformServiceStub implements IPlatformService { + public validateOptions(): IFuture { + return Future.fromResult(true); + } + public addPlatforms(platforms: string[]): IFuture { return Future.fromResult(); }