diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/MacOSNotarizationSettings.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/MacOSNotarizationSettings.kt index 8e98968e81..2d3929ae66 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/MacOSNotarizationSettings.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/MacOSNotarizationSettings.kt @@ -9,6 +9,7 @@ import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.Property import org.gradle.api.provider.ProviderFactory import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal import org.gradle.api.tasks.Optional import org.jetbrains.compose.desktop.application.internal.ComposeProperties import org.jetbrains.compose.internal.utils.nullableProperty @@ -35,7 +36,15 @@ abstract class MacOSNotarizationSettings { @get:Input @get:Optional + val teamID: Property = objects.nullableProperty().apply { + set(ComposeProperties.macNotarizationTeamID(providers)) + } + + @Deprecated("This option is no longer supported and got replaced by teamID", level = DeprecationLevel.ERROR) + @get:Internal val ascProvider: Property = objects.nullableProperty().apply { - set(ComposeProperties.macNotarizationAscProvider(providers)) + set(providers.provider { + throw UnsupportedOperationException("This option is not supported by notary tool and was replaced by teamID") + }) } -} \ No newline at end of file +} diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeProjectProperties.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeProjectProperties.kt index 08123a4125..3c3094495d 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeProjectProperties.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeProjectProperties.kt @@ -19,7 +19,7 @@ internal object ComposeProperties { internal const val MAC_SIGN_PREFIX = "compose.desktop.mac.signing.prefix" internal const val MAC_NOTARIZATION_APPLE_ID = "compose.desktop.mac.notarization.appleID" internal const val MAC_NOTARIZATION_PASSWORD = "compose.desktop.mac.notarization.password" - internal const val MAC_NOTARIZATION_ASC_PROVIDER = "compose.desktop.mac.notarization.ascProvider" + internal const val MAC_NOTARIZATION_TEAM_ID_PROVIDER = "compose.desktop.mac.notarization.teamID" internal const val CHECK_JDK_VENDOR = "compose.desktop.packaging.checkJdkVendor" fun isVerbose(providers: ProviderFactory): Provider = @@ -46,8 +46,8 @@ internal object ComposeProperties { fun macNotarizationPassword(providers: ProviderFactory): Provider = providers.valueOrNull(MAC_NOTARIZATION_PASSWORD) - fun macNotarizationAscProvider(providers: ProviderFactory): Provider = - providers.valueOrNull(MAC_NOTARIZATION_ASC_PROVIDER) + fun macNotarizationTeamID(providers: ProviderFactory): Provider = + providers.valueOrNull(MAC_NOTARIZATION_TEAM_ID_PROVIDER) fun checkJdkVendor(providers: ProviderFactory): Provider = providers.valueOrNull(CHECK_JDK_VENDOR).toBooleanProvider(true) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ExternalToolRunner.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ExternalToolRunner.kt index c8775f9b26..02b242f3a8 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ExternalToolRunner.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ExternalToolRunner.kt @@ -11,6 +11,7 @@ import org.gradle.api.provider.Provider import org.gradle.process.ExecOperations import org.gradle.process.ExecResult import org.jetbrains.compose.internal.utils.ioFile +import java.io.ByteArrayInputStream import java.io.File import java.time.LocalDateTime import java.time.format.DateTimeFormatter @@ -33,7 +34,8 @@ internal class ExternalToolRunner( workingDir: File? = null, checkExitCodeIsNormal: Boolean = true, processStdout: Function1? = null, - logToConsole: LogToConsole = LogToConsole.OnlyWhenVerbose + logToConsole: LogToConsole = LogToConsole.OnlyWhenVerbose, + stdinStr: String? = null ): ExecResult { val logsDir = logsDir.ioFile logsDir.mkdirs() @@ -52,6 +54,10 @@ internal class ExternalToolRunner( // check exit value later spec.isIgnoreExitValue = true + if (stdinStr != null) { + spec.standardInput = ByteArrayInputStream(stdinStr.toByteArray()) + } + @Suppress("NAME_SHADOWING") val logToConsole = when (logToConsole) { LogToConsole.Always -> true diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt index 83707cf8b5..5983ab90fe 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt @@ -186,23 +186,13 @@ private fun JvmApplicationContext.configurePackagingTasks( "Unexpected target format for MacOS: $targetFormat" } - val notarizationRequestsDir = project.layout.buildDirectory.dir("compose/notarization/$app") - tasks.register( + tasks.register( taskNameAction = "notarize", taskNameObject = targetFormat.name, args = listOf(targetFormat) ) { dependsOn(packageFormat) inputDir.set(packageFormat.flatMap { it.destinationDir }) - requestsDir.set(notarizationRequestsDir) - configureCommonNotarizationSettings(this) - } - - tasks.register( - taskNameAction = "check", - taskNameObject = "notarizationStatus" - ) { - requestDir.set(notarizationRequestsDir) configureCommonNotarizationSettings(this) } } @@ -351,7 +341,6 @@ private fun JvmApplicationContext.configurePackageTask( internal fun JvmApplicationContext.configureCommonNotarizationSettings( notarizationTask: AbstractNotarizationTask ) { - notarizationTask.nonValidatedBundleID.set(app.nativeDistributions.macOS.bundleID) notarizationTask.nonValidatedNotarizationSettings = app.nativeDistributions.macOS.notarization } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/validation/ValidatedMacOSNotarizationSettings.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/validation/ValidatedMacOSNotarizationSettings.kt index 501fb162f2..cc635b6b95 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/validation/ValidatedMacOSNotarizationSettings.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/validation/ValidatedMacOSNotarizationSettings.kt @@ -5,25 +5,20 @@ package org.jetbrains.compose.desktop.application.internal.validation -import org.gradle.api.provider.Provider import org.jetbrains.compose.desktop.application.dsl.MacOSNotarizationSettings import org.jetbrains.compose.desktop.application.internal.ComposeProperties internal data class ValidatedMacOSNotarizationSettings( - val bundleID: String, val appleID: String, val password: String, - val ascProvider: String? + val teamID: String? ) -internal fun MacOSNotarizationSettings?.validate( - bundleIDProvider: Provider -): ValidatedMacOSNotarizationSettings { +internal fun MacOSNotarizationSettings?.validate(): ValidatedMacOSNotarizationSettings { checkNotNull(this) { ERR_NOTARIZATION_SETTINGS_ARE_NOT_PROVIDED } - val bundleID = validateBundleID(bundleIDProvider) check(!appleID.orNull.isNullOrEmpty()) { ERR_APPLE_ID_IS_EMPTY } @@ -31,10 +26,9 @@ internal fun MacOSNotarizationSettings?.validate( ERR_PASSWORD_IS_EMPTY } return ValidatedMacOSNotarizationSettings( - bundleID = bundleID, appleID = appleID.orNull!!, password = password.orNull!!, - ascProvider = ascProvider.orNull + teamID = teamID.orNull ) } @@ -50,4 +44,4 @@ private val ERR_PASSWORD_IS_EMPTY = """|$ERR_PREFIX password is null or empty. To specify: | * Use '${ComposeProperties.MAC_NOTARIZATION_PASSWORD}' Gradle property; | * Or use 'nativeDistributions.macOS.notarization.password' DSL property; - """.trimMargin() \ No newline at end of file + """.trimMargin() diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNotarizationStatusTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNotarizationStatusTask.kt deleted file mode 100644 index 1e19b9a1be..0000000000 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNotarizationStatusTask.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -package org.jetbrains.compose.desktop.application.tasks - -import org.gradle.api.file.DirectoryProperty -import org.gradle.api.tasks.* -import org.jetbrains.compose.desktop.application.internal.ExternalToolRunner -import org.jetbrains.compose.internal.utils.MacUtils -import org.jetbrains.compose.desktop.application.internal.NOTARIZATION_REQUEST_INFO_FILE_NAME -import org.jetbrains.compose.desktop.application.internal.NotarizationRequestInfo -import org.jetbrains.compose.internal.utils.ioFile - -abstract class AbstractCheckNotarizationStatusTask : AbstractNotarizationTask() { - @get:Internal - val requestDir: DirectoryProperty = objects.directoryProperty() - - @TaskAction - fun run() { - val notarization = validateNotarization() - - val requests = HashSet() - for (file in requestDir.ioFile.walk()) { - if (file.isFile && file.name == NOTARIZATION_REQUEST_INFO_FILE_NAME) { - try { - val status = NotarizationRequestInfo() - status.loadFrom(file) - requests.add(status) - } catch (e: Exception) { - logger.error("Invalid notarization request status file: $file", e) - } - } - } - - if (requests.isEmpty()) { - logger.quiet("No existing notarization requests") - return - } - - for (request in requests.sortedBy { it.uploadTime }) { - try { - logger.quiet("Checking status of notarization request '${request.uuid}'") - runExternalTool( - tool = MacUtils.xcrun, - args = listOf( - "altool", - "--notarization-info", request.uuid, - "--username", notarization.appleID, - "--password", notarization.password - ), - logToConsole = ExternalToolRunner.LogToConsole.Always - ) - } catch (e: Exception) { - logger.error("Could not check notarization request '${request.uuid}'", e) - } - } - } -} \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractNotarizationTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractNotarizationTask.kt index 4efbadacdd..ae2a1b6653 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractNotarizationTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractNotarizationTask.kt @@ -5,24 +5,67 @@ package org.jetbrains.compose.desktop.application.tasks -import org.gradle.api.provider.Property -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.Nested -import org.gradle.api.tasks.Optional +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.tasks.* import org.jetbrains.compose.desktop.application.dsl.MacOSNotarizationSettings -import org.jetbrains.compose.internal.utils.nullableProperty +import org.jetbrains.compose.desktop.application.dsl.TargetFormat +import org.jetbrains.compose.desktop.application.internal.files.checkExistingFile +import org.jetbrains.compose.desktop.application.internal.files.findOutputFileOrDir +import org.jetbrains.compose.desktop.application.internal.validation.ValidatedMacOSNotarizationSettings import org.jetbrains.compose.desktop.application.internal.validation.validate import org.jetbrains.compose.desktop.tasks.AbstractComposeDesktopTask +import org.jetbrains.compose.internal.utils.MacUtils +import org.jetbrains.compose.internal.utils.ioFile +import java.io.File +import javax.inject.Inject -abstract class AbstractNotarizationTask : AbstractComposeDesktopTask() { +abstract class AbstractNotarizationTask @Inject constructor( @get:Input - @get:Optional - internal val nonValidatedBundleID: Property = objects.nullableProperty() + val targetFormat: TargetFormat +) : AbstractComposeDesktopTask() { @get:Nested @get:Optional internal var nonValidatedNotarizationSettings: MacOSNotarizationSettings? = null - internal fun validateNotarization() = - nonValidatedNotarizationSettings.validate(nonValidatedBundleID) -} \ No newline at end of file + @get:InputDirectory + val inputDir: DirectoryProperty = objects.directoryProperty() + + init { + check(targetFormat != TargetFormat.AppImage) { "${TargetFormat.AppImage} cannot be notarized!" } + } + + @TaskAction + fun run() { + val notarization = nonValidatedNotarizationSettings.validate() + val packageFile = findOutputFileOrDir(inputDir.ioFile, targetFormat).checkExistingFile() + + notarize(notarization, packageFile) + staple(packageFile) + } + + private fun notarize( + notarization: ValidatedMacOSNotarizationSettings, + packageFile: File + ) { + logger.info("Uploading '${packageFile.name}' for notarization") + val args = listOfNotNull( + "notarytool", + "submit", + "--wait", + "--apple-id", + notarization.appleID, + "--team-id".takeIf { notarization.teamID != null }, + notarization.teamID, + packageFile.absolutePath + ) + runExternalTool(tool = MacUtils.xcrun, args = args, stdinStr = notarization.password) + } + + private fun staple(packageFile: File) { + runExternalTool( + tool = MacUtils.xcrun, + args = listOf("stapler", "staple", packageFile.absolutePath) + ) + } +} diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractUploadAppForNotarizationTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractUploadAppForNotarizationTask.kt deleted file mode 100644 index ebc5e22a4f..0000000000 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractUploadAppForNotarizationTask.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. - * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. - */ - -package org.jetbrains.compose.desktop.application.tasks - -import org.gradle.api.file.DirectoryProperty -import org.gradle.api.tasks.* -import org.jetbrains.compose.desktop.application.dsl.TargetFormat -import org.jetbrains.compose.desktop.application.internal.* -import org.jetbrains.compose.desktop.application.internal.files.checkExistingFile -import org.jetbrains.compose.desktop.application.internal.files.findOutputFileOrDir -import org.jetbrains.compose.internal.utils.MacUtils -import org.jetbrains.compose.internal.utils.ioFile -import java.io.File -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter -import javax.inject.Inject - -abstract class AbstractUploadAppForNotarizationTask @Inject constructor( - @get:Input - val targetFormat: TargetFormat -) : AbstractNotarizationTask() { - @get:InputDirectory - val inputDir: DirectoryProperty = objects.directoryProperty() - - @get:Internal - val requestsDir: DirectoryProperty = objects.directoryProperty() - - init { - check(targetFormat != TargetFormat.AppImage) { "${TargetFormat.AppImage} cannot be notarized!" } - } - - @TaskAction - fun run() { - val notarization = validateNotarization() - val packageFile = findOutputFileOrDir(inputDir.ioFile, targetFormat).checkExistingFile() - - logger.quiet("Uploading '${packageFile.name}' for notarization (package id: '${notarization.bundleID}')") - val args = arrayListOf( - "altool", - "--notarize-app", - "--primary-bundle-id", notarization.bundleID, - "--username", notarization.appleID, - "--password", notarization.password, - "--file", packageFile.absolutePath - ) - if (notarization.ascProvider != null) { - args.add("--asc-provider") - args.add(notarization.ascProvider) - } - - runExternalTool( - tool = MacUtils.xcrun, - args = args, - processStdout = { output -> - processUploadToolOutput(packageFile, output) - } - ) - } - - private fun processUploadToolOutput(packageFile: File, output: String) { - val m = "RequestUUID = ([A-Za-z0-9\\-]+)".toRegex().find(output) - ?: error("Could not determine RequestUUID from output: $output") - - val requestId = m.groupValues[1] - - val uploadTime = LocalDateTime.now() - .format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss")) - val requestDir = requestsDir.ioFile.resolve("$uploadTime-${targetFormat.id}") - val packageCopy = requestDir.resolve(packageFile.name) - packageFile.copyTo(packageCopy) - val requestInfo = NotarizationRequestInfo(uuid = requestId, uploadTime = uploadTime) - val requestInfoFile = requestDir.resolve(NOTARIZATION_REQUEST_INFO_FILE_NAME) - requestInfo.saveTo(requestInfoFile) - - logger.quiet("Request UUID: $requestId") - logger.quiet("Request UUID is saved to ${requestInfoFile.absolutePath}") - logger.quiet("Uploaded file is saved to ${packageCopy.absolutePath}") - } -} diff --git a/tutorials/Signing_and_notarization_on_macOS/README.md b/tutorials/Signing_and_notarization_on_macOS/README.md index 1a34c473e0..d79be326e5 100644 --- a/tutorials/Signing_and_notarization_on_macOS/README.md +++ b/tutorials/Signing_and_notarization_on_macOS/README.md @@ -221,38 +221,58 @@ The following Gradle properties can be used instead of DSL properties: Those properties could be stored in `$HOME/.gradle/gradle.properties` to use across multiple applications. -### Configuring notarization settings +### Notarization -Notarization is only required for apps outside the App Store. +Distributing your macOS application outside the App Store +requires notarization. +Notarization involves submitting your application to Apple for verification. +If your software passes the verification, +it's signed by Apple, stating that it has been notarized. -``` kotlin -macOS { - notarization { - appleID.set("john.doe@example.com") - password.set("@keychain:NOTARIZATION_PASSWORD") - - // optional - ascProvider.set("") - } -} +To notarize your app, you can use `notarize` task: ``` - -* Set `appleID` to your Apple ID. - * Alternatively, the `compose.desktop.mac.notarization.appleID` Gradle property can be used. -* Set `password` to the app-specific password created previously. - * Alternatively, the `compose.desktop.mac.notarization.password` Gradle property can be used. - * Don't write raw password directly into a build script. - * If the password was added to the keychain, as described previously, it can be referenced as - ``` - @keychain:NOTARIZATION_PASSWORD - ``` -* Set `ascProvider` to your Team ID, if your account is associated with multiple teams. - * Alternatively, the `compose.desktop.mac.notarization.ascProvider` Gradle property can be used. - * To get a table of team IDs associated with a given username and password, run: +./gradlew notarizeDmg \ + -Pcompose.desktop.mac.notarization.appleID= \ + -Pcompose.desktop.mac.notarization.password= \ + -Pcompose.desktop.mac.notarization.teamID= +``` +where: +* `` — your Apple ID; +* `` — the app-specific password created previously; +* `` — your Team. To get a table of team IDs associated with a given username and password, run: ``` xcrun altool --list-providers -u -p " ``` +Team ID + + +The following tasks can be used for notarization: +* `notarizeDmg` — build, sign and notarize `.dmg` installer; +* `notarizeReleaseDmg` — same as `notarizeDmg`, but with [ProGuard](tutorials/Native_distributions_and_local_execution/README.md). +* `notarizePkg` — build, sign and notarize `.pkg` installer; +* `notarizeReleasePkg` — same as `notarizePkg`, but with [ProGuard](tutorials/Native_distributions_and_local_execution/README.md). + +The notarization settings can also be set using the DSL. +For example, it is possible to pass credentials using environment variables: +``` +compose.desktop.application { + nativeDistributions { + macOS { + notarization { + val providers = project.providers + appleID.set(providers.environmentVariable("NOTARIZATION_APPLE_ID")) + password.set(providers.environmentVariable("NOTARIZATION_PASSWORD")) + teamId.set(providers.environmentVariable("NOTARIZATION_TEAM_ID")) + } + } + } +} +``` + +According to Apple, for 98 percent of software notarization completes within 15 minutes. +To learn more on how to avoid long response times, check [the official documentation](https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution/customizing_the_notarization_workflow#3561440). + ### Configuring provisioning profile For testing on TestFlight (when publishing to the App Store), you need to add a provisioning @@ -375,14 +395,3 @@ The following tasks are available: (no separate step is required). * Use `notarize` (e.g. `notarizeDmg`) to upload an application for notarization. Notarization is only required for apps outside the App Store. - Once the upload finishes, a `RequestUUID` will be printed. - The notarization process takes some time. - Once the notarization process finishes, an email will be sent to you. - Uploaded file is saved to `/compose/notarization/main/-` -* Use `checkNotarizationStatus` to check a status of - last notarization requests. You can also use a command-line command to check any notarization request: -``` -xcrun altool --notarization-info - --username - --password "@keychain:NOTARIZATION_PASSWORD" -``` diff --git a/tutorials/Signing_and_notarization_on_macOS/notarization-team-id.png b/tutorials/Signing_and_notarization_on_macOS/notarization-team-id.png new file mode 100644 index 0000000000..98c81b2e24 Binary files /dev/null and b/tutorials/Signing_and_notarization_on_macOS/notarization-team-id.png differ