diff --git a/cli/src/config.ts b/cli/src/config.ts index e504f2539..dc303df6b 100644 --- a/cli/src/config.ts +++ b/cli/src/config.ts @@ -267,7 +267,12 @@ async function loadIOSConfig(rootDir: string, extConfig: ExternalConfig): Promis const podPath = lazy(() => determineGemfileOrCocoapodPath(rootDir, platformDirAbs, nativeProjectDirAbs)); const webDirAbs = lazy(() => determineIOSWebDirAbs(nativeProjectDirAbs, nativeTargetDirAbs, nativeXcodeProjDirAbs)); const cordovaPluginsDir = 'capacitor-cordova-ios-plugins'; - + const buildOptions = { + xcodeExportMethod: extConfig.ios?.buildOptions?.exportMethod, + xcodeSigningStyle: extConfig.ios?.buildOptions?.signingStyle, + signingCertificate: extConfig.ios?.buildOptions?.signingCertificate, + provisioningProfile: extConfig.ios?.buildOptions?.provisioningProfile, + }; return { name, minVersion: '14.0', @@ -287,6 +292,7 @@ async function loadIOSConfig(rootDir: string, extConfig: ExternalConfig): Promis webDir: lazy(async () => relative(platformDirAbs, await webDirAbs)), webDirAbs, podPath, + buildOptions, }; } diff --git a/cli/src/declarations.ts b/cli/src/declarations.ts index d12b832bf..1556f9af4 100644 --- a/cli/src/declarations.ts +++ b/cli/src/declarations.ts @@ -478,6 +478,35 @@ export interface CapacitorConfig { * @default true */ initialFocus?: boolean; + + buildOptions?: { + /** + * The signing style to use when building the app for distribution. + * + * @since 7.0.0 + * @default 'automatic' + */ + signingStyle?: 'automatic' | 'manual'; + /** + * The method used by xcodebuild to export the archive + * + * @since 7.0.0 + * @default 'app-store-connect' + */ + exportMethod?: string; + /** + * A certificate name, SHA-1 hash, or automatic selector to use for signing for iOS builds. + * + * @since 7.0.0 + */ + signingCertificate?: string; + /** + * A provisioning profile name or UUID for iOS builds. + * + * @since 7.0.0 + */ + provisioningProfile?: string; + }; }; server?: { diff --git a/cli/src/definitions.ts b/cli/src/definitions.ts index a81278120..893a9d030 100644 --- a/cli/src/definitions.ts +++ b/cli/src/definitions.ts @@ -101,6 +101,16 @@ export interface AndroidConfig extends PlatformConfig { }; } +export enum XcodeExportMethod { + AppStoreConnect = 'app-store-connect', + ReleaseTesting = 'release-testing', + Enterprise = 'enterprise', + Debugging = 'debugging', + DeveloperID = 'developer-id', + MacApplication = 'mac-application', + Validation = 'validation', +} + export interface IOSConfig extends PlatformConfig { readonly cordovaPluginsDir: string; readonly cordovaPluginsDirAbs: string; @@ -117,6 +127,13 @@ export interface IOSConfig extends PlatformConfig { readonly nativeXcodeProjDirAbs: string; readonly nativeXcodeWorkspaceDir: Promise; readonly nativeXcodeWorkspaceDirAbs: Promise; + readonly buildOptions: { + teamId?: string; + exportMethod?: XcodeExportMethod; + xcodeSigningStyle?: 'automatic' | 'manual'; + signingCertificate?: string; + provisioningProfile?: string; + }; } export type WebConfig = PlatformConfig; diff --git a/cli/src/index.ts b/cli/src/index.ts index 7afede477..abcc1590b 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -147,6 +147,41 @@ export function runProgram(config: Config): void { 'jarsigner', ]), ) + .addOption( + new Option('--xcode-team-id ', 'The Developer team to use for building and exporting the archive'), + ) + .addOption( + new Option( + '--xcode-export-method ', + 'Describes how xcodebuild should export the archive (default: app-store-connect)', + ).choices([ + 'app-store-connect', + 'release-testing', + 'enterprise', + 'debugging', + 'developer-id', + 'mac-application', + 'validation', + ]), + ) + .addOption( + new Option( + '--xcode-signing-style ', + 'The iOS signing style to use when building the app for distribution (default: automatic)', + ).choices(['automatic', 'manual']), + ) + .addOption( + new Option( + '--xcode-signing-certificate ', + 'A certificate name, SHA-1 hash, or automatic selector to use for signing for iOS builds', + ), + ) + .addOption( + new Option( + '--xcode-provisioning-profile ', + 'A provisioning profile name or UUID for iOS builds', + ), + ) .action( wrapAction( telemetryAction( @@ -163,6 +198,11 @@ export function runProgram(config: Config): void { androidreleasetype, signingType, configuration, + xcodeTeamId, + xcodeExportMethod, + xcodeSigningStyle, + xcodeSigningCertificate, + xcodeProvisioningProfile, }, ) => { const { buildCommand } = await import('./tasks/build'); @@ -176,6 +216,11 @@ export function runProgram(config: Config): void { androidreleasetype, signingtype: signingType, configuration, + xcodeTeamId, + xcodeExportMethod, + xcodeSigningType: xcodeSigningStyle, + xcodeSigningCertificate, + xcodeProvisioningProfile, }); }, ), diff --git a/cli/src/ios/build.ts b/cli/src/ios/build.ts index 9f8958c40..8a8e11bcb 100644 --- a/cli/src/ios/build.ts +++ b/cli/src/ios/build.ts @@ -3,9 +3,9 @@ import { basename, join } from 'path'; import { rimraf } from 'rimraf'; import { runTask } from '../common'; -import type { Config } from '../definitions'; +import { XcodeExportMethod, type Config } from '../definitions'; import { logSuccess } from '../log'; -import type { BuildCommandOptions } from '../tasks/build'; +import { type BuildCommandOptions } from '../tasks/build'; import { checkPackageManager } from '../util/spm'; import { runCommand } from '../util/subprocess'; @@ -25,32 +25,56 @@ export async function buildiOS(config: Config, buildOptions: BuildCommandOptions projectName = basename(await config.ios.nativeXcodeProjDirAbs); } + if ( + buildOptions.xcodeSigningType == 'manual' && + (!buildOptions.xcodeSigningCertificate || !buildOptions.xcodeProvisioningProfile) + ) { + throw 'Manually signed Xcode builds require a signing certificate and provisioning profile.'; + } + + const buildArgs = [ + typeOfBuild, + projectName, + '-scheme', + `${theScheme}`, + '-destination', + `generic/platform=iOS`, + '-archivePath', + `${theScheme}.xcarchive`, + 'archive', + ]; + + if (buildOptions.xcodeTeamId) { + buildArgs.push(`DEVELOPMENT_TEAM=${buildOptions.xcodeTeamId}`); + } + + if (buildOptions.xcodeSigningType == 'manual') { + buildArgs.push(`PROVISIONING_PROFILE_SPECIFIER=${buildOptions.xcodeProvisioningProfile}`); + } + await runTask('Building xArchive', async () => - runCommand( - 'xcodebuild', - [ - typeOfBuild, - projectName, - '-scheme', - `${theScheme}`, - '-destination', - `generic/platform=iOS`, - '-archivePath', - `${theScheme}.xcarchive`, - 'archive', - ], - { - cwd: config.ios.nativeProjectDirAbs, - }, - ), + runCommand('xcodebuild', buildArgs, { + cwd: config.ios.nativeProjectDirAbs, + }), ); + const manualSigningContents = `provisioningProfiles + +${config.app.appId} +${buildOptions.xcodeProvisioningProfile ?? ''} + +signingCertificate +${buildOptions.xcodeSigningCertificate ?? ''}`; + const archivePlistContents = ` method -app-store-connect +${buildOptions.xcodeExportMethod ?? XcodeExportMethod.AppStoreConnect} +signingStyle +${buildOptions.xcodeSigningType} +${buildOptions.xcodeSigningType == 'manual' ? manualSigningContents : ''} `; @@ -58,26 +82,27 @@ export async function buildiOS(config: Config, buildOptions: BuildCommandOptions writeFileSync(archivePlistPath, archivePlistContents); + const archiveArgs = [ + 'archive', + '-archivePath', + `${theScheme}.xcarchive`, + '-exportArchive', + '-exportOptionsPlist', + 'archive.plist', + '-exportPath', + 'output', + '-configuration', + buildOptions.configuration, + ]; + + if (buildOptions.xcodeSigningType == 'automatic') { + archiveArgs.push('-allowProvisioningUpdates'); + } + await runTask('Building IPA', async () => - runCommand( - 'xcodebuild', - [ - 'archive', - '-archivePath', - `${theScheme}.xcarchive`, - '-exportArchive', - '-exportOptionsPlist', - 'archive.plist', - '-exportPath', - 'output', - '-allowProvisioningUpdates', - '-configuration', - buildOptions.configuration, - ], - { - cwd: config.ios.nativeProjectDirAbs, - }, - ), + runCommand('xcodebuild', archiveArgs, { + cwd: config.ios.nativeProjectDirAbs, + }), ); await runTask('Cleaning up', async () => { diff --git a/cli/src/tasks/build.ts b/cli/src/tasks/build.ts index 783600624..c7e6b2415 100644 --- a/cli/src/tasks/build.ts +++ b/cli/src/tasks/build.ts @@ -1,6 +1,7 @@ import { buildAndroid } from '../android/build'; import { selectPlatforms, promptForPlatform } from '../common'; import type { Config } from '../definitions'; +import { XcodeExportMethod } from '../definitions'; import { fatal, isFatal } from '../errors'; import { buildiOS } from '../ios/build'; @@ -14,6 +15,11 @@ export interface BuildCommandOptions { androidreleasetype?: 'AAB' | 'APK'; signingtype?: 'apksigner' | 'jarsigner'; configuration: string; + xcodeTeamId?: string; + xcodeExportMethod?: XcodeExportMethod; + xcodeSigningType?: 'automatic' | 'manual'; + xcodeSigningCertificate?: string; + xcodeProvisioningProfile?: string; } export async function buildCommand( @@ -42,6 +48,12 @@ export async function buildCommand( androidreleasetype: buildOptions.androidreleasetype || config.android.buildOptions.releaseType || 'AAB', signingtype: buildOptions.signingtype || config.android.buildOptions.signingType || 'jarsigner', configuration: buildOptions.configuration || 'Release', + xcodeTeamId: buildOptions.xcodeTeamId || config.ios.buildOptions.teamId, + xcodeExportMethod: + buildOptions.xcodeExportMethod || config.ios.buildOptions.exportMethod || XcodeExportMethod.AppStoreConnect, + xcodeSigningType: buildOptions.xcodeSigningType || config.ios.buildOptions.xcodeSigningStyle || 'automatic', + xcodeSigningCertificate: buildOptions.xcodeSigningCertificate || config.ios.buildOptions.signingCertificate, + xcodeProvisioningProfile: buildOptions.xcodeProvisioningProfile || config.ios.buildOptions.provisioningProfile, }; try { diff --git a/cli/src/tasks/copy.ts b/cli/src/tasks/copy.ts index c59028279..6943677ab 100644 --- a/cli/src/tasks/copy.ts +++ b/cli/src/tasks/copy.ts @@ -154,6 +154,7 @@ async function copyCapacitorConfig(config: Config, nativeAbsDir: string) { await runTask(`Creating ${c.strong(nativeConfigFile)} in ${nativeRelDir}`, async () => { delete (config.app.extConfig.android as any)?.buildOptions; + delete (config.app.extConfig.ios as any)?.buildOptions; await writeJSON(nativeConfigFilePath, config.app.extConfig, { spaces: '\t', });