diff --git a/docs/man_pages/project/testing/build-ios.md b/docs/man_pages/project/testing/build-ios.md index 9e6127ee8b..de0ee38850 100644 --- a/docs/man_pages/project/testing/build-ios.md +++ b/docs/man_pages/project/testing/build-ios.md @@ -15,6 +15,7 @@ Builds the project for iOS and produces an `APP` or `IPA` that you can manually * `--release` - If set, produces a release build. Otherwise, produces a debug build. * `--for-device` - If set, produces an application package that you can deploy on device. Otherwise, produces a build that you can run only in the native iOS Simulator. * `--copy-to` - Specifies the file path where the built `.ipa` will be copied. If it points to a non-existent directory, it will be created. If the specified value is directory, the original file name will be used. +* `--team-id` - If used without parameter, lists all team names and ids. If used with team name or id, it will switch to automatic signing mode and configure the .xcodeproj file of your app. In this case .xcconfig should not contain any provisioning/team id flags. This team id will be further used for codesigning the app. For Xcode 9.0+, xcodebuild will be allowed to update and modify automatically managed provisioning profiles. * `--provision` - If used without parameter, lists all eligible provisioning profiles. If used with UUID or name of your provisioning profile, it will switch to manual signing mode and configure the .xcodeproj file of your app. In this case xcconfig should not contain any provisioning/team id flags. This provisioning profile will be further used for codesigning the app. <% } %> <% if(isHtml) { %> diff --git a/lib/commands/appstore-upload.ts b/lib/commands/appstore-upload.ts index dfca00ff9c..48671fea98 100644 --- a/lib/commands/appstore-upload.ts +++ b/lib/commands/appstore-upload.ts @@ -83,7 +83,7 @@ export class PublishIOS implements ICommand { let archivePath = await iOSProjectService.archive(this.$projectData); this.$logger.info("Archive at: " + archivePath); - let exportPath = await iOSProjectService.exportArchive(this.$projectData, { archivePath, teamID }); + let exportPath = await iOSProjectService.exportArchive(this.$projectData, { archivePath, teamID, provision: mobileProvisionIdentifier || this.$options.provision }); this.$logger.info("Export at: " + exportPath); ipaFilePath = exportPath; diff --git a/lib/commands/build.ts b/lib/commands/build.ts index d0185286d2..04b86c12e4 100644 --- a/lib/commands/build.ts +++ b/lib/commands/build.ts @@ -57,7 +57,7 @@ export class BuildIosCommand extends BuildCommandBase implements ICommand { public canExecute(args: string[]): Promise { super.validatePlatform(this.$devicePlatformsConstants.iOS); - return args.length === 0 && this.$platformService.validateOptions(this.$options.provision, this.$projectData, this.$platformsData.availablePlatforms.iOS); + return args.length === 0 && this.$platformService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, this.$platformsData.availablePlatforms.iOS); } } @@ -89,7 +89,7 @@ export class BuildAndroidCommand extends BuildCommandBase implements ICommand { const platformProjectService = platformData.platformProjectService; await platformProjectService.validate(this.$projectData); - return args.length === 0 && await this.$platformService.validateOptions(this.$options.provision, this.$projectData, this.$platformsData.availablePlatforms.Android); + return args.length === 0 && await this.$platformService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, this.$platformsData.availablePlatforms.Android); } } diff --git a/lib/commands/debug.ts b/lib/commands/debug.ts index 144ae61d23..cc04b09411 100644 --- a/lib/commands/debug.ts +++ b/lib/commands/debug.ts @@ -154,7 +154,7 @@ export class DebugIOSCommand implements ICommand { this.$errors.fail(`Applications for platform ${this.$devicePlatformsConstants.iOS} can not be built on this OS`); } - return await this.debugPlatformCommand.canExecute(args) && await this.$platformService.validateOptions(this.$options.provision, this.$projectData, this.$platformsData.availablePlatforms.iOS); + return await this.debugPlatformCommand.canExecute(args) && await this.$platformService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, this.$platformsData.availablePlatforms.iOS); } public platform = this.$devicePlatformsConstants.iOS; @@ -186,7 +186,7 @@ export class DebugAndroidCommand implements ICommand { return this.debugPlatformCommand.execute(args); } public async canExecute(args: string[]): Promise { - return await this.debugPlatformCommand.canExecute(args) && await this.$platformService.validateOptions(this.$options.provision, this.$projectData, this.$platformsData.availablePlatforms.Android); + return await this.debugPlatformCommand.canExecute(args) && await this.$platformService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, this.$platformsData.availablePlatforms.Android); } public platform = this.$devicePlatformsConstants.Android; diff --git a/lib/commands/deploy.ts b/lib/commands/deploy.ts index 3625fb8ae2..3fc90bc5c7 100644 --- a/lib/commands/deploy.ts +++ b/lib/commands/deploy.ts @@ -48,7 +48,7 @@ export class DeployOnDeviceCommand implements ICommand { const platformProjectService = platformData.platformProjectService; await platformProjectService.validate(this.$projectData); - return this.$platformService.validateOptions(this.$options.provision, this.$projectData, args[0]); + return this.$platformService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, args[0]); } } diff --git a/lib/commands/prepare.ts b/lib/commands/prepare.ts index bf1d775c9a..489ebcd358 100644 --- a/lib/commands/prepare.ts +++ b/lib/commands/prepare.ts @@ -16,7 +16,7 @@ export class PrepareCommand implements ICommand { public async canExecute(args: string[]): Promise { const platform = args[0]; - const result = await this.$platformCommandParameter.validate(platform) && await this.$platformService.validateOptions(this.$options.provision, this.$projectData, platform); + const result = await this.$platformCommandParameter.validate(platform) && await this.$platformService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, platform); if (result) { const platformData = this.$platformsData.getPlatformData(platform, this.$projectData); const platformProjectService = platformData.platformProjectService; diff --git a/lib/commands/run.ts b/lib/commands/run.ts index 2f30032475..76a974a185 100644 --- a/lib/commands/run.ts +++ b/lib/commands/run.ts @@ -97,7 +97,7 @@ export class RunIosCommand implements ICommand { } public async canExecute(args: string[]): Promise { - return await this.runCommand.canExecute(args) && await this.$platformService.validateOptions(this.$options.provision, this.$projectData, this.$platformsData.availablePlatforms.iOS); + return await this.runCommand.canExecute(args) && await this.$platformService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, this.$platformsData.availablePlatforms.iOS); } } @@ -140,7 +140,7 @@ export class RunAndroidCommand 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 this.$platformService.validateOptions(this.$options.provision, this.$projectData, this.$platformsData.availablePlatforms.Android); + return this.$platformService.validateOptions(this.$options.provision, this.$options.teamId, this.$projectData, this.$platformsData.availablePlatforms.Android); } } diff --git a/lib/definitions/platform.d.ts b/lib/definitions/platform.d.ts index 058f33fffa..3c5e8cd5d1 100644 --- a/lib/definitions/platform.d.ts +++ b/lib/definitions/platform.d.ts @@ -101,7 +101,7 @@ interface IPlatformService extends NodeJS.EventEmitter { * If no platform is provided or a falsy (null, undefined, "", false...) platform is provided, * the options will be validated for all available platforms. */ - validateOptions(provision: any, projectData: IProjectData, platform?: string): Promise; + validateOptions(provision: true | string, teamId: true | string, projectData: IProjectData, platform?: string): Promise; /** * Executes prepare, build and installOnPlatform when necessary to ensure that the latest version of the app is installed on specified platform. @@ -218,7 +218,7 @@ interface IPlatformOptions extends IPlatformSpecificData, ICreateProjectOptions /** * Platform specific data required for project preparation. */ -interface IPlatformSpecificData extends IProvision { +interface IPlatformSpecificData extends IProvision, ITeamIdentifier { /** * Target SDK for Android. */ diff --git a/lib/definitions/project-changes.d.ts b/lib/definitions/project-changes.d.ts index 04ae5edab4..0f2143102a 100644 --- a/lib/definitions/project-changes.d.ts +++ b/lib/definitions/project-changes.d.ts @@ -22,12 +22,12 @@ interface IProjectChangesInfo extends IAddedNativePlatform { readonly changesRequirePrepare: boolean; } -interface IProjectChangesOptions extends IAppFilesUpdaterOptions, IProvision { +interface IProjectChangesOptions extends IAppFilesUpdaterOptions, IProvision, ITeamIdentifier { nativePlatformStatus?: "1" | "2" | "3"; } interface IProjectChangesService { - checkForChanges(platform: string, projectData: IProjectData, buildOptions: IProjectChangesOptions): IProjectChangesInfo; + checkForChanges(platform: string, projectData: IProjectData, buildOptions: IProjectChangesOptions): Promise; getPrepareInfo(platform: string, projectData: IProjectData): IPrepareInfo; savePrepareInfo(platform: string, projectData: IProjectData): void; getPrepareInfoFilePath(platform: string, projectData: IProjectData): string; diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index 9a328d9255..f8d4cbd076 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -178,7 +178,7 @@ interface IPlatformProjectService extends NodeJS.EventEmitter { * @param {any} provision UUID of the provisioning profile used in iOS option validation. * @returns {void} */ - validateOptions(projectId?: string, provision?: true | string): Promise; + validateOptions(projectId?: string, provision?: true | string, teamId?: true | string): Promise; validatePlugins(projectData: IProjectData): Promise; @@ -265,7 +265,7 @@ interface IPlatformProjectService extends NodeJS.EventEmitter { * Check the current state of the project, and validate against the options. * If there are parts in the project that are inconsistent with the desired options, marks them in the changeset flags. */ - checkForChanges(changeset: IProjectChangesInfo, options: IProjectChangesOptions, projectData: IProjectData): void; + checkForChanges(changeset: IProjectChangesInfo, options: IProjectChangesOptions, projectData: IProjectData): Promise; } interface IAndroidProjectPropertiesManager { diff --git a/lib/options.ts b/lib/options.ts index 7f71b3dcbb..a5c960007a 100644 --- a/lib/options.ts +++ b/lib/options.ts @@ -34,7 +34,7 @@ export class Options extends commonOptionsLibPath.OptionsBase { androidTypings: { type: OptionType.Boolean }, bundle: { type: OptionType.Boolean }, all: { type: OptionType.Boolean }, - teamId: { type: OptionType.String }, + teamId: { type: OptionType.Object }, syncAllFiles: { type: OptionType.Boolean, default: false }, liveEdit: { type: OptionType.Boolean }, chrome: { type: OptionType.Boolean }, diff --git a/lib/services/android-project-service.ts b/lib/services/android-project-service.ts index 4dabcefe6d..8b7c53afea 100644 --- a/lib/services/android-project-service.ts +++ b/lib/services/android-project-service.ts @@ -447,7 +447,7 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject await adb.executeShellCommand(["rm", "-rf", deviceRootPath]); } - public checkForChanges(changesInfo: IProjectChangesInfo, options: IProjectChangesOptions, projectData: IProjectData): void { + public async checkForChanges(changesInfo: IProjectChangesInfo, options: IProjectChangesOptions, projectData: IProjectData): Promise { // Nothing android specific to check yet. } @@ -517,12 +517,18 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject if (this.$androidToolsInfo.getToolsInfo().androidHomeEnvVar) { const gradlew = this.$hostInfo.isWindows ? "gradlew.bat" : "./gradlew"; + const localArgs = [...gradleArgs]; + if (this.$logger.getLevel() === "INFO") { + localArgs.push("--quiet"); + this.$logger.info("Gradle build..."); + } + childProcessOpts = childProcessOpts || {}; childProcessOpts.cwd = childProcessOpts.cwd || projectRoot; childProcessOpts.stdio = childProcessOpts.stdio || "inherit"; return await this.spawn(gradlew, - gradleArgs, + localArgs, childProcessOpts, spawnFromEventOptions); } diff --git a/lib/services/ios-project-service.ts b/lib/services/ios-project-service.ts index cd75de7dad..65c2fe47d3 100644 --- a/lib/services/ios-project-service.ts +++ b/lib/services/ios-project-service.ts @@ -61,7 +61,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ } if (projectData && projectData.platformsDir && this._platformsDirCache !== projectData.platformsDir) { - let projectRoot = path.join(projectData.platformsDir, "ios"); + let projectRoot = path.join(projectData.platformsDir, this.$devicePlatformsConstants.iOS.toLowerCase()); this._platformData = { frameworkPackageName: "tns-ios", @@ -93,13 +93,23 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ return this._platformData; } - public async validateOptions(projectId: string, provision: true | string): Promise { + public async validateOptions(projectId: string, provision: true | string, teamId: true | string): Promise { + if (provision && teamId) { + this.$errors.failWithoutHelp("The options --provision and --teamId are mutually exclusive."); + } + if (provision === true) { - await this.$iOSProvisionService.list(projectId); + await this.$iOSProvisionService.listProvisions(projectId); this.$errors.failWithoutHelp("Please provide provisioning profile uuid or name with the --provision option."); return false; } + if (teamId === true) { + await this.$iOSProvisionService.listTeams(); + this.$errors.failWithoutHelp("Please provide team id or team name with the --teamId options."); + return false; + } + return true; } @@ -194,19 +204,19 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ let args = ["archive", "-archivePath", archivePath, "-configuration", (!buildConfig || buildConfig.release) ? "Release" : "Debug"] .concat(this.xcbuildProjectArgs(projectRoot, projectData, "scheme")); - await this.$childProcess.spawnFromEvent("xcodebuild", args, "exit", { stdio: 'inherit' }); + 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 }): Promise { - let projectRoot = this.getPlatformData(projectData).projectRoot; - let archivePath = options.archivePath; + 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. - let exportPath = path.resolve(options.exportDir || path.join(projectRoot, "/build/archive")); - let exportFile = path.join(exportPath, projectData.projectName + ".ipa"); + 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 = ` @@ -218,6 +228,13 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ plistTemplate += ` teamID ${options.teamID} `; + } + if (options && options.provision) { + plistTemplate += ` provisioningProfiles + + ${projectData.projectId} + ${options.provision} + `; } plistTemplate += ` method app-store @@ -230,23 +247,24 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ // Save the options... temp.track(); - let exportOptionsPlist = temp.path({ prefix: "export-", suffix: ".plist" }); + const exportOptionsPlist = temp.path({ prefix: "export-", suffix: ".plist" }); this.$fs.writeFile(exportOptionsPlist, plistTemplate); - let args = ["-exportArchive", - "-archivePath", archivePath, - "-exportPath", exportPath, - "-exportOptionsPlist", exportOptionsPlist - ]; - await this.$childProcess.spawnFromEvent("xcodebuild", args, "exit", { stdio: 'inherit' }); - + await this.xcodebuild( + [ + "-exportArchive", + "-archivePath", archivePath, + "-exportPath", exportPath, + "-exportOptionsPlist", exportOptionsPlist + ], + projectRoot); return exportFile; } /** * Exports .xcarchive for a development device. */ - private async exportDevelopmentArchive(projectData: IProjectData, buildConfig: IBuildConfig, options: { archivePath: string, exportDir?: string, teamID?: string }): Promise { + private async exportDevelopmentArchive(projectData: IProjectData, buildConfig: IBuildConfig, options: { archivePath: string, exportDir?: string, teamID?: string, provision?: string }): Promise { let platformData = this.getPlatformData(projectData); let projectRoot = platformData.projectRoot; let archivePath = options.archivePath; @@ -257,7 +275,15 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ method - ${exportOptionsMethod} + ${exportOptionsMethod}`; + if (options && options.provision) { + plistTemplate += ` provisioningProfiles + + ${projectData.projectId} + ${options.provision} +`; + } + plistTemplate += ` uploadBitcode @@ -272,15 +298,14 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ let exportPath = path.resolve(options.exportDir || buildOutputPath); let exportFile = path.join(exportPath, projectData.projectName + ".ipa"); - let args = ["-exportArchive", - "-archivePath", archivePath, - "-exportPath", exportPath, - "-exportOptionsPlist", exportOptionsPlist - ]; - await this.$childProcess.spawnFromEvent("xcodebuild", args, "exit", - { stdio: buildConfig.buildOutputStdio || 'inherit', cwd: this.getPlatformData(projectData).projectRoot }, - { emitOptions: { eventName: constants.BUILD_OUTPUT_EVENT_NAME }, throwError: true }); - + await this.xcodebuild( + [ + "-exportArchive", + "-archivePath", archivePath, + "-exportPath", exportPath, + "-exportOptionsPlist", exportOptionsPlist + ], + projectRoot, buildConfig.buildOutputStdio); return exportFile; } @@ -388,27 +413,62 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ await this.setupSigningForDevice(projectRoot, buildConfig, projectData); } - if (buildConfig && buildConfig.codeSignIdentity) { - args.push(`CODE_SIGN_IDENTITY=${buildConfig.codeSignIdentity}`); - } + await this.xcodebuild(args, projectRoot, buildConfig.buildOutputStdio); + await this.createIpa(projectRoot, projectData, buildConfig); + } - if (buildConfig && buildConfig.mobileProvisionIdentifier) { - args.push(`PROVISIONING_PROFILE=${buildConfig.mobileProvisionIdentifier}`); + private async xcodebuild(args: string[], cwd: string, stdio: any = "inherit"): Promise { + const localArgs = [...args]; + const xcodeBuildVersion = await this.getXcodeVersion(); + try { + if (helpers.versionCompare(xcodeBuildVersion, "9.0") >= 0) { + localArgs.push("-allowProvisioningUpdates"); + } + } catch (e) { + this.$logger.warn("Failed to detect whether -allowProvisioningUpdates can be used with your xcodebuild version due to error: " + e); } - - if (buildConfig && buildConfig.teamId) { - args.push(`DEVELOPMENT_TEAM=${buildConfig.teamId}`); + if (this.$logger.getLevel() === "INFO") { + localArgs.push("-quiet"); + this.$logger.info("Xcode build..."); } - - // this.$logger.out("xcodebuild..."); - await this.$childProcess.spawnFromEvent("xcodebuild", - args, + return this.$childProcess.spawnFromEvent("xcodebuild", + localArgs, "exit", - { stdio: buildConfig.buildOutputStdio || "inherit", cwd: this.getPlatformData(projectData).projectRoot }, + { stdio: stdio || "inherit", cwd }, { emitOptions: { eventName: constants.BUILD_OUTPUT_EVENT_NAME }, throwError: true }); - // this.$logger.out("xcodebuild build succeded."); + } - await this.createIpa(projectRoot, projectData, buildConfig); + 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.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?: mobileprovision.provision.MobileProvision): Promise { @@ -447,9 +507,9 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ xcode.save(); // this.cache(uuid); - this.$logger.trace("Set Manual signing style and provisioning profile."); + this.$logger.trace(`Set Manual signing style and provisioning profile: ${mobileprovision.Name} (${mobileprovision.UUID})`); } else { - this.$logger.trace("The specified provisioning profile allready set in the Xcode."); + this.$logger.trace(`The specified provisioning profile is already set in the Xcode: ${provision}`); } } else { // read uuid from Xcode and cache... @@ -471,16 +531,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ xcode.save(); } else if (!buildConfig.provision && !(signing && signing.style === "Manual" && !buildConfig.teamId)) { const teamId = await this.getDevelopmentTeam(projectData, buildConfig.teamId); - - // Remove teamId from build config as we'll set the signing in Xcode project directly. - // In case we do not remove it, we'll pass DEVELOPMENT_TEAM= to xcodebuild, which is unnecessary. - if (buildConfig.teamId) { - delete buildConfig.teamId; - } - - xcode.setAutomaticSigningStyle(projectData.projectName, teamId); - xcode.save(); - this.$logger.trace("Set Automatic signing style and team."); + await this.setupSigningFromTeam(projectRoot, projectData, teamId); } } @@ -493,20 +544,12 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ "CONFIGURATION_BUILD_DIR=" + path.join(projectRoot, "build", "emulator"), "CODE_SIGN_IDENTITY=" ]); - - await this.$childProcess.spawnFromEvent("xcodebuild", args, "exit", - { stdio: buildOutputStdio || "inherit", cwd: this.getPlatformData(projectData).projectRoot }, - { emitOptions: { eventName: constants.BUILD_OUTPUT_EVENT_NAME }, throwError: true }); + await this.xcodebuild(args, projectRoot, buildOutputStdio); } private async createIpa(projectRoot: string, projectData: IProjectData, buildConfig: IBuildConfig): Promise { - let xarchivePath = await this.archive(projectData, buildConfig); - let exportFileIpa = await this.exportDevelopmentArchive(projectData, - buildConfig, - { - archivePath: xarchivePath, - }); - + const archivePath = await this.archive(projectData, buildConfig); + const exportFileIpa = await this.exportDevelopmentArchive(projectData, buildConfig, { archivePath, provision: buildConfig.provision || buildConfig.mobileProvisionIdentifier }); return exportFileIpa; } @@ -655,12 +698,16 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } public async prepareProject(projectData: IProjectData, platformSpecificData: IPlatformSpecificData): Promise { - let provision = platformSpecificData && platformSpecificData.provision; + const projectRoot = path.join(projectData.platformsDir, "ios"); + const provision = platformSpecificData && platformSpecificData.provision; + const teamId = platformSpecificData && platformSpecificData.teamId; if (provision) { - let projectRoot = path.join(projectData.platformsDir, "ios"); await this.setupSigningFromProvision(projectRoot, projectData, provision, platformSpecificData.mobileProvisionData); } + if (teamId) { + await this.setupSigningFromTeam(projectRoot, projectData, teamId); + } let project = this.createPbxProj(projectData); @@ -910,28 +957,46 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f return Promise.resolve(); } - public checkForChanges(changesInfo: IProjectChangesInfo, options: IProjectChangesOptions, projectData: IProjectData): void { - const provision = options.provision; - if (provision !== undefined) { + public async checkForChanges(changesInfo: IProjectChangesInfo, {provision, teamId}: IProjectChangesOptions, projectData: IProjectData): Promise { + const hasProvision = provision !== undefined; + const hasTeamId = teamId !== undefined; + if (hasProvision || hasTeamId) { // Check if the native project's signing is set to the provided provision... const pbxprojPath = this.getPbxProjPath(projectData); if (this.$fs.exists(pbxprojPath)) { const xcode = this.$pbxprojDomXcode.Xcode.open(pbxprojPath); const signing = xcode.getSigning(projectData.projectName); - if (signing && signing.style === "Manual") { - for (let name in signing.configurations) { - let config = signing.configurations[name]; - if (config.uuid !== provision && config.name !== provision) { - changesInfo.signingChanged = true; - break; + + if (hasProvision) { + if (signing && signing.style === "Manual") { + for (const name in signing.configurations) { + const config = signing.configurations[name]; + if (config.uuid !== provision && config.name !== provision) { + changesInfo.signingChanged = true; + break; + } } + } else { + // Specifying provisioning profile requires "Manual" signing style. + // If the current signing style was not "Manual" it was probably "Automatic" or, + // it was not uniform for the debug and release build configurations. + changesInfo.signingChanged = true; + } + } + if (hasTeamId) { + if (signing && signing.style === "Automatic") { + if (signing.team !== teamId) { + const teamIdsForName = await this.$iOSProvisionService.getTeamIdsWithName(teamId); + if (!teamIdsForName.some(id => id === signing.team)) { + changesInfo.signingChanged = true; + } + } + } else { + // Specifying team id or name requires "Automatic" signing style. + // If the current signing style was not "Automatic" it was probably "Manual". + changesInfo.signingChanged = true; } - } else { - // Specifying provisioning profile requires "Manual" signing style. - // If the current signing style was not "Manual" it was probably "Automatic" or, - // it was not uniform for the debug and release build configurations. - changesInfo.signingChanged = true; } } else { changesInfo.signingChanged = true; @@ -1211,51 +1276,11 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f } let splitedXcodeBuildVersion = xcodeBuildVersion.split("."); - if (splitedXcodeBuildVersion.length === 3) { - xcodeBuildVersion = `${splitedXcodeBuildVersion[0]}.${splitedXcodeBuildVersion[1]}`; - } + xcodeBuildVersion = `${splitedXcodeBuildVersion[0] || 0}.${splitedXcodeBuildVersion[1] || 0}`; return xcodeBuildVersion; } - 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 = {}; - for (let file of files) { - let filePath = path.join(dir, file); - let data = this.$fs.readText(filePath, "utf8"); - let teamId = this.getProvisioningProfileValue("TeamIdentifier", data); - let teamName = this.getProvisioningProfileValue("TeamName", data); - if (teamId) { - teamIds[teamId] = teamName; - } - } - - let teamIdsArray = new Array<{ id: string, name: string }>(); - for (let teamId in teamIds) { - teamIdsArray.push({ id: teamId, name: teamIds[teamId] }); - } - - return teamIdsArray; - } - - private getProvisioningProfileValue(name: string, text: string): string { - let findStr = "" + name + ""; - let index = text.indexOf(findStr); - if (index > 0) { - index = text.indexOf("", index + findStr.length); - if (index > 0) { - index += "".length; - let endIndex = text.indexOf("", index); - let result = text.substring(index, endIndex); - return result; - } - } - - return null; - } - private getBuildXCConfigFilePath(projectData: IProjectData): string { let buildXCConfig = path.join(projectData.appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName, "build.xcconfig"); @@ -1293,7 +1318,7 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f teamId = teamId || this.readTeamId(projectData); if (!teamId) { - let teams = this.getDevelopmentTeams(); + let teams = await this.$iOSProvisionService.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 commands."); if (teams.length === 1) { diff --git a/lib/services/ios-provision-service.ts b/lib/services/ios-provision-service.ts index 65622ac9e5..19c0ba380c 100644 --- a/lib/services/ios-provision-service.ts +++ b/lib/services/ios-provision-service.ts @@ -1,7 +1,7 @@ import * as mobileprovision from "ios-mobileprovision-finder"; -import { createTable } from "../common/helpers"; +import { createTable, quoteString } from "../common/helpers"; -const months = ["Jan", "Feb", "Marc", "Apr", "May", "June", "July", "Aug", "Sept", "Oct", "Nov", "Dec"]; +const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; function formatDate(date: Date): string { return `${date.getDay()} ${months[date.getMonth()]} ${date.getFullYear()}`; } @@ -22,7 +22,7 @@ export class IOSProvisionService { || match.nonEligable.find(prov => prov.Name === uuidOrName); } - public async list(projectId: string): Promise { + public async listProvisions(projectId: string): Promise { const data = await this.queryProvisioningProfilesAndDevices(projectId); const devices = data.devices; const match = data.match; @@ -49,7 +49,7 @@ export class IOSProvisionService { function pushProvision(prov: mobileprovision.provision.MobileProvision) { table.push(["", "", "", ""]); - table.push(["\"" + prov.Name + "\"", prov.TeamName, prov.Type, formatTotalDeviceCount(prov)]); + table.push([quoteString(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"], "", "", ""]); } @@ -59,7 +59,12 @@ export class IOSProvisionService { this.$logger.out(); this.$logger.out("There are also " + match.nonEligable.length + " non-eligable provisioning profiles."); this.$logger.out(); + } + public async listTeams(): Promise { + const teams = await this.getDevelopmentTeams(); + const table = createTable(["Team Name", "Team ID"], teams.map(team => [quoteString(team.name), team.id])); + this.$logger.out(table.toString()); } private async queryProvisioningProfilesAndDevices(projectId: string): Promise<{ devices: string[], match: mobileprovision.provision.Result }> { @@ -90,6 +95,31 @@ export class IOSProvisionService { return { devices, match }; } + + public async getDevelopmentTeams(): Promise<{ id: string, name: string }[]> { + const teams: { [teamName: string]: Set } = {}; + // NOTE: We are reading all provisioning profiles and collect team information from them. + // It would be better if we can check the Apple ID registered in Xcode and read the teams associated with it. + mobileprovision.provision.read().forEach(provision => + provision.TeamIdentifier && provision.TeamIdentifier.forEach(id => { + if (!teams[provision.TeamName]) { + teams[provision.TeamName] = new Set(); + } + teams[provision.TeamName].add(id); + }) + ); + const teamsArray = Object.keys(teams).reduce((arr, name) => { + teams[name].forEach(id => arr.push({ id, name })); + return arr; + }, []); + return teamsArray; + } + + public async getTeamIdsWithName(teamName: string): Promise { + const allTeams = await this.getDevelopmentTeams(); + const matchingTeamIds = allTeams.filter(team => team.name === teamName).map(team => team.id); + return matchingTeamIds; + } } $injector.register("iOSProvisionService", IOSProvisionService); diff --git a/lib/services/local-build-service.ts b/lib/services/local-build-service.ts index 8bf4cb0bb9..c705416d1f 100644 --- a/lib/services/local-build-service.ts +++ b/lib/services/local-build-service.ts @@ -12,6 +12,7 @@ export class LocalBuildService extends EventEmitter { this.$projectData.initializeProjectData(platformBuildOptions.projectDir); await this.$platformService.preparePlatform(platform, platformBuildOptions, platformTemplate, this.$projectData, { provision: platformBuildOptions.provision, + teamId: platformBuildOptions.teamId, sdk: null, frameworkPath: null, ignoreScripts: false diff --git a/lib/services/platform-service.ts b/lib/services/platform-service.ts index 1d072f6592..ba813d48ac 100644 --- a/lib/services/platform-service.ts +++ b/lib/services/platform-service.ts @@ -228,18 +228,18 @@ export class PlatformService extends EventEmitter implements IPlatformService { return true; } - public async validateOptions(provision: true | string, projectData: IProjectData, platform?: string): Promise { + public async validateOptions(provision: true | string, teamId: true | string, projectData: IProjectData, platform?: string): Promise { if (platform) { platform = this.$mobileHelper.normalizePlatformName(platform); this.$logger.trace("Validate options for platform: " + platform); let platformData = this.$platformsData.getPlatformData(platform, projectData); - return await platformData.platformProjectService.validateOptions(projectData.projectId, provision); + return await platformData.platformProjectService.validateOptions(projectData.projectId, provision, teamId); } else { let valid = true; for (let availablePlatform in this.$platformsData.availablePlatforms) { this.$logger.trace("Validate options for platform: " + availablePlatform); let platformData = this.$platformsData.getPlatformData(availablePlatform, projectData); - valid = valid && await platformData.platformProjectService.validateOptions(projectData.projectId, provision); + valid = valid && await platformData.platformProjectService.validateOptions(projectData.projectId, provision, teamId); } return valid; @@ -282,7 +282,7 @@ export class PlatformService extends EventEmitter implements IPlatformService { const bundle = appFilesUpdaterOptions.bundle; const nativePlatformStatus = (nativePrepare && nativePrepare.skipNativePrepare) ? constants.NativePlatformStatus.requiresPlatformAdd : constants.NativePlatformStatus.requiresPrepare; - const changesInfo = this.$projectChangesService.checkForChanges(platform, projectData, { bundle, release: appFilesUpdaterOptions.release, provision: config.provision, nativePlatformStatus }); + const changesInfo = await this.$projectChangesService.checkForChanges(platform, projectData, { bundle, release: appFilesUpdaterOptions.release, provision: config.provision, teamId: config.teamId, nativePlatformStatus }); this.$logger.trace("Changes info in prepare platform:", changesInfo); return changesInfo; diff --git a/lib/services/project-changes-service.ts b/lib/services/project-changes-service.ts index 08c87a3df5..e9982cae43 100644 --- a/lib/services/project-changes-service.ts +++ b/lib/services/project-changes-service.ts @@ -54,7 +54,7 @@ export class ProjectChangesService implements IProjectChangesService { return this._changesInfo; } - public checkForChanges(platform: string, projectData: IProjectData, projectChangesOptions: IProjectChangesOptions): IProjectChangesInfo { + public async checkForChanges(platform: string, projectData: IProjectData, projectChangesOptions: IProjectChangesOptions): Promise { let platformData = this.$platformsData.getPlatformData(platform, projectData); this._changesInfo = new ProjectChangesInfo(); if (!this.ensurePrepareInfo(platform, projectData, projectChangesOptions)) { @@ -87,7 +87,7 @@ export class ProjectChangesService implements IProjectChangesService { } let projectService = platformData.platformProjectService; - projectService.checkForChanges(this._changesInfo, projectChangesOptions, projectData); + await projectService.checkForChanges(this._changesInfo, projectChangesOptions, projectData); if (projectChangesOptions.bundle !== this._prepareInfo.bundle || projectChangesOptions.release !== this._prepareInfo.release) { this._changesInfo.appFilesChanged = true; diff --git a/test/ios-project-service.ts b/test/ios-project-service.ts index d0c97cde5f..956c4343a8 100644 --- a/test/ios-project-service.ts +++ b/test/ios-project-service.ts @@ -611,12 +611,12 @@ describe("iOS Project Service Signing", () => { }); describe("Check for Changes", () => { - it("sets signingChanged if no Xcode project exists", () => { + it("sets signingChanged if no Xcode project exists", async () => { let changes = {}; - iOSProjectService.checkForChanges(changes, { bundle: false, release: false, provision: "NativeScriptDev" }, projectData); + await iOSProjectService.checkForChanges(changes, { bundle: false, release: false, provision: "NativeScriptDev", teamId: undefined }, projectData); assert.isTrue(!!changes.signingChanged); }); - it("sets signingChanged if the Xcode projects is configured with Automatic signing, but proivsion is specified", () => { + it("sets signingChanged if the Xcode projects is configured with Automatic signing, but proivsion is specified", async () => { files[pbxproj] = ""; pbxprojDomXcode.Xcode.open = function (path: string) { assert.equal(path, pbxproj); @@ -627,10 +627,10 @@ describe("iOS Project Service Signing", () => { }; }; let changes = {}; - iOSProjectService.checkForChanges(changes, { bundle: false, release: false, provision: "NativeScriptDev" }, projectData); + await iOSProjectService.checkForChanges(changes, { bundle: false, release: false, provision: "NativeScriptDev", teamId: undefined }, projectData); assert.isTrue(!!changes.signingChanged); }); - it("sets signingChanged if the Xcode projects is configured with Manual signing, but the proivsion specified differs the selected in the pbxproj", () => { + it("sets signingChanged if the Xcode projects is configured with Manual signing, but the proivsion specified differs the selected in the pbxproj", async () => { files[pbxproj] = ""; pbxprojDomXcode.Xcode.open = function (path: string) { assert.equal(path, pbxproj); @@ -646,10 +646,10 @@ describe("iOS Project Service Signing", () => { }; }; let changes = {}; - iOSProjectService.checkForChanges(changes, { bundle: false, release: false, provision: "NativeScriptDev" }, projectData); + await iOSProjectService.checkForChanges(changes, { bundle: false, release: false, provision: "NativeScriptDev", teamId: undefined }, projectData); assert.isTrue(!!changes.signingChanged); }); - it("does not set signingChanged if the Xcode projects is configured with Manual signing and proivsion matches", () => { + it("does not set signingChanged if the Xcode projects is configured with Manual signing and proivsion matches", async () => { files[pbxproj] = ""; pbxprojDomXcode.Xcode.open = function (path: string) { assert.equal(path, pbxproj); @@ -665,7 +665,7 @@ describe("iOS Project Service Signing", () => { }; }; let changes = {}; - iOSProjectService.checkForChanges(changes, { bundle: false, release: false, provision: "NativeScriptDev" }, projectData); + await iOSProjectService.checkForChanges(changes, { bundle: false, release: false, provision: "NativeScriptDev", teamId: undefined }, projectData); assert.isFalse(!!changes.signingChanged); }); }); @@ -684,7 +684,7 @@ describe("iOS Project Service Signing", () => { }); it("fails with proper error if the provision can not be found", async () => { try { - await iOSProjectService.prepareProject(projectData, { sdk: undefined, provision: "NativeScriptDev2" }); + 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); } @@ -705,7 +705,7 @@ describe("iOS Project Service Signing", () => { } }; }; - await iOSProjectService.prepareProject(projectData, { sdk: undefined, provision: "NativeScriptDev" }); + 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 () => { @@ -724,7 +724,7 @@ describe("iOS Project Service Signing", () => { } }; }; - await iOSProjectService.prepareProject(projectData, { sdk: undefined, provision: "NativeScriptDist" }); + 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 () => { @@ -743,7 +743,7 @@ describe("iOS Project Service Signing", () => { } }; }; - await iOSProjectService.prepareProject(projectData, { sdk: undefined, provision: "NativeScriptAdHoc" }); + 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()"]); }); }); diff --git a/test/platform-commands.ts b/test/platform-commands.ts index cf7ff0e1e8..52a5695698 100644 --- a/test/platform-commands.ts +++ b/test/platform-commands.ts @@ -46,11 +46,11 @@ class PlatformData implements IPlatformData { class ErrorsNoFailStub implements IErrors { printCallStack: boolean = false; - fail(formatStr: string, ...args: any[]): void; - fail(opts: { formatStr?: string; errorCode?: number; suppressCommandHelp?: boolean }, ...args: any[]): void; + fail(formatStr: string, ...args: any[]): never; + fail(opts: { formatStr?: string; errorCode?: number; suppressCommandHelp?: boolean }, ...args: any[]): never; - fail(...args: any[]) { throw new Error(); } - failWithoutHelp(message: string, ...args: any[]): void { + fail(...args: any[]): never { throw new Error(); } + failWithoutHelp(message: string, ...args: any[]): never { throw new Error(); } diff --git a/test/platform-service.ts b/test/platform-service.ts index 22d5bf78d0..9333bb297e 100644 --- a/test/platform-service.ts +++ b/test/platform-service.ts @@ -150,6 +150,7 @@ describe('Platform Service Tests', () => { const config: IPlatformOptions = { ignoreScripts: false, provision: null, + teamId: null, sdk: null, frameworkPath: null }; @@ -441,7 +442,7 @@ describe('Platform Service Tests', () => { platformService = testInjector.resolve("platformService"); const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { bundle: false, release: release }; - await platformService.preparePlatform(platformToTest, appFilesUpdaterOptions, "", projectData, { provision: null, sdk: null, frameworkPath: null, ignoreScripts: false }); + await platformService.preparePlatform(platformToTest, appFilesUpdaterOptions, "", projectData, { provision: null, teamId: null, sdk: null, frameworkPath: null, ignoreScripts: false }); } async function testPreparePlatform(platformToTest: string, release?: boolean): Promise { @@ -868,7 +869,7 @@ describe('Platform Service Tests', () => { try { testInjector.resolve("$logger").warn = (text: string) => warnings += text; const appFilesUpdaterOptions: IAppFilesUpdaterOptions = { bundle: false, release: false }; - await platformService.preparePlatform("android", appFilesUpdaterOptions, "", projectData, { provision: null, sdk: null, frameworkPath: null, ignoreScripts: false }); + await platformService.preparePlatform("android", appFilesUpdaterOptions, "", projectData, { provision: null, teamId: null, sdk: null, frameworkPath: null, ignoreScripts: false }); } finally { testInjector.resolve("$logger").warn = oldLoggerWarner; } diff --git a/test/project-changes-service.ts b/test/project-changes-service.ts index cc29656ebd..f4b091bb73 100644 --- a/test/project-changes-service.ts +++ b/test/project-changes-service.ts @@ -141,10 +141,10 @@ describe("Project Changes Service Tests", () => { }); describe("Accumulates Changes From Project Services", () => { - it("accumulates changes from the project service", () => { - let iOSChanges = serviceTest.projectChangesService.checkForChanges("ios", serviceTest.projectData, { bundle: false, release: false, provision: undefined }); + it("accumulates changes from the project service", async () => { + let iOSChanges = await serviceTest.projectChangesService.checkForChanges("ios", serviceTest.projectData, { bundle: false, release: false, provision: undefined, teamId: undefined }); assert.isTrue(!!iOSChanges.signingChanged, "iOS signingChanged expected to be true"); - let androidChanges = serviceTest.projectChangesService.checkForChanges("android", serviceTest.projectData, { bundle: false, release: false, provision: undefined }); + let androidChanges = await serviceTest.projectChangesService.checkForChanges("android", serviceTest.projectData, { bundle: false, release: false, provision: undefined, teamId: undefined }); assert.isFalse(!!androidChanges.signingChanged, "Android signingChanged expected to be false"); }); }); diff --git a/test/stubs.ts b/test/stubs.ts index 1a4902a444..39c27f06b4 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -178,14 +178,14 @@ export class FileSystemStub implements IFileSystem { } export class ErrorsStub implements IErrors { - fail(formatStr: string, ...args: any[]): void; - fail(opts: { formatStr?: string; errorCode?: number; suppressCommandHelp?: boolean }, ...args: any[]): void; + fail(formatStr: string, ...args: any[]): never; + fail(opts: { formatStr?: string; errorCode?: number; suppressCommandHelp?: boolean }, ...args: any[]): never; - fail(...args: any[]) { + fail(...args: any[]): never { throw new Error(require("util").format.apply(null, args || [])); } - failWithoutHelp(message: string, ...args: any[]): void { + failWithoutHelp(message: string, ...args: any[]): never { throw new Error(message); } @@ -333,7 +333,7 @@ export class PlatformProjectServiceStub extends EventEmitter implements IPlatfor async cleanProject(projectRoot: string, projectData: IProjectData): Promise { return Promise.resolve(); } - checkForChanges(changesInfo: IProjectChangesInfo, options: IProjectChangesOptions, projectData: IProjectData): void { + async checkForChanges(changesInfo: IProjectChangesInfo, options: IProjectChangesOptions, projectData: IProjectData): Promise { // Nothing yet. } } @@ -556,7 +556,7 @@ export class ChildProcessStub { } export class ProjectChangesService implements IProjectChangesService { - public checkForChanges(platform: string): IProjectChangesInfo { + public async checkForChanges(platform: string): Promise { return {}; }