Browse Source

Switch to notarytool for notarization (#3642)

See https://github.com/JetBrains/compose-multiplatform/pull/3642 for details

Resolves #3208
Resolves #2253

---------

Co-authored-by: Michael Rittmeister <michael@rittmeister.in>
pull/3708/head
Alexey Tsvetkov 8 months ago committed by GitHub
parent
commit
50d45f3326
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 13
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/MacOSNotarizationSettings.kt
  2. 6
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ComposeProjectProperties.kt
  3. 8
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/ExternalToolRunner.kt
  4. 13
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt
  5. 14
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/validation/ValidatedMacOSNotarizationSettings.kt
  6. 60
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNotarizationStatusTask.kt
  7. 65
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractNotarizationTask.kt
  8. 82
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractUploadAppForNotarizationTask.kt
  9. 81
      tutorials/Signing_and_notarization_on_macOS/README.md
  10. BIN
      tutorials/Signing_and_notarization_on_macOS/notarization-team-id.png

13
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.Property
import org.gradle.api.provider.ProviderFactory import org.gradle.api.provider.ProviderFactory
import org.gradle.api.tasks.Input import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.Optional import org.gradle.api.tasks.Optional
import org.jetbrains.compose.desktop.application.internal.ComposeProperties import org.jetbrains.compose.desktop.application.internal.ComposeProperties
import org.jetbrains.compose.internal.utils.nullableProperty import org.jetbrains.compose.internal.utils.nullableProperty
@ -35,7 +36,15 @@ abstract class MacOSNotarizationSettings {
@get:Input @get:Input
@get:Optional @get:Optional
val teamID: Property<String?> = objects.nullableProperty<String>().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<String?> = objects.nullableProperty<String>().apply { val ascProvider: Property<String?> = objects.nullableProperty<String>().apply {
set(ComposeProperties.macNotarizationAscProvider(providers)) set(providers.provider {
throw UnsupportedOperationException("This option is not supported by notary tool and was replaced by teamID")
})
} }
} }

6
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_SIGN_PREFIX = "compose.desktop.mac.signing.prefix"
internal const val MAC_NOTARIZATION_APPLE_ID = "compose.desktop.mac.notarization.appleID" 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_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" internal const val CHECK_JDK_VENDOR = "compose.desktop.packaging.checkJdkVendor"
fun isVerbose(providers: ProviderFactory): Provider<Boolean> = fun isVerbose(providers: ProviderFactory): Provider<Boolean> =
@ -46,8 +46,8 @@ internal object ComposeProperties {
fun macNotarizationPassword(providers: ProviderFactory): Provider<String?> = fun macNotarizationPassword(providers: ProviderFactory): Provider<String?> =
providers.valueOrNull(MAC_NOTARIZATION_PASSWORD) providers.valueOrNull(MAC_NOTARIZATION_PASSWORD)
fun macNotarizationAscProvider(providers: ProviderFactory): Provider<String?> = fun macNotarizationTeamID(providers: ProviderFactory): Provider<String?> =
providers.valueOrNull(MAC_NOTARIZATION_ASC_PROVIDER) providers.valueOrNull(MAC_NOTARIZATION_TEAM_ID_PROVIDER)
fun checkJdkVendor(providers: ProviderFactory): Provider<Boolean> = fun checkJdkVendor(providers: ProviderFactory): Provider<Boolean> =
providers.valueOrNull(CHECK_JDK_VENDOR).toBooleanProvider(true) providers.valueOrNull(CHECK_JDK_VENDOR).toBooleanProvider(true)

8
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.ExecOperations
import org.gradle.process.ExecResult import org.gradle.process.ExecResult
import org.jetbrains.compose.internal.utils.ioFile import org.jetbrains.compose.internal.utils.ioFile
import java.io.ByteArrayInputStream
import java.io.File import java.io.File
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@ -33,7 +34,8 @@ internal class ExternalToolRunner(
workingDir: File? = null, workingDir: File? = null,
checkExitCodeIsNormal: Boolean = true, checkExitCodeIsNormal: Boolean = true,
processStdout: Function1<String, Unit>? = null, processStdout: Function1<String, Unit>? = null,
logToConsole: LogToConsole = LogToConsole.OnlyWhenVerbose logToConsole: LogToConsole = LogToConsole.OnlyWhenVerbose,
stdinStr: String? = null
): ExecResult { ): ExecResult {
val logsDir = logsDir.ioFile val logsDir = logsDir.ioFile
logsDir.mkdirs() logsDir.mkdirs()
@ -52,6 +54,10 @@ internal class ExternalToolRunner(
// check exit value later // check exit value later
spec.isIgnoreExitValue = true spec.isIgnoreExitValue = true
if (stdinStr != null) {
spec.standardInput = ByteArrayInputStream(stdinStr.toByteArray())
}
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING")
val logToConsole = when (logToConsole) { val logToConsole = when (logToConsole) {
LogToConsole.Always -> true LogToConsole.Always -> true

13
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" "Unexpected target format for MacOS: $targetFormat"
} }
val notarizationRequestsDir = project.layout.buildDirectory.dir("compose/notarization/$app") tasks.register<AbstractNotarizationTask>(
tasks.register<AbstractUploadAppForNotarizationTask>(
taskNameAction = "notarize", taskNameAction = "notarize",
taskNameObject = targetFormat.name, taskNameObject = targetFormat.name,
args = listOf(targetFormat) args = listOf(targetFormat)
) { ) {
dependsOn(packageFormat) dependsOn(packageFormat)
inputDir.set(packageFormat.flatMap { it.destinationDir }) inputDir.set(packageFormat.flatMap { it.destinationDir })
requestsDir.set(notarizationRequestsDir)
configureCommonNotarizationSettings(this)
}
tasks.register<AbstractCheckNotarizationStatusTask>(
taskNameAction = "check",
taskNameObject = "notarizationStatus"
) {
requestDir.set(notarizationRequestsDir)
configureCommonNotarizationSettings(this) configureCommonNotarizationSettings(this)
} }
} }
@ -351,7 +341,6 @@ private fun JvmApplicationContext.configurePackageTask(
internal fun JvmApplicationContext.configureCommonNotarizationSettings( internal fun JvmApplicationContext.configureCommonNotarizationSettings(
notarizationTask: AbstractNotarizationTask notarizationTask: AbstractNotarizationTask
) { ) {
notarizationTask.nonValidatedBundleID.set(app.nativeDistributions.macOS.bundleID)
notarizationTask.nonValidatedNotarizationSettings = app.nativeDistributions.macOS.notarization notarizationTask.nonValidatedNotarizationSettings = app.nativeDistributions.macOS.notarization
} }

14
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 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.dsl.MacOSNotarizationSettings
import org.jetbrains.compose.desktop.application.internal.ComposeProperties import org.jetbrains.compose.desktop.application.internal.ComposeProperties
internal data class ValidatedMacOSNotarizationSettings( internal data class ValidatedMacOSNotarizationSettings(
val bundleID: String,
val appleID: String, val appleID: String,
val password: String, val password: String,
val ascProvider: String? val teamID: String?
) )
internal fun MacOSNotarizationSettings?.validate( internal fun MacOSNotarizationSettings?.validate(): ValidatedMacOSNotarizationSettings {
bundleIDProvider: Provider<String?>
): ValidatedMacOSNotarizationSettings {
checkNotNull(this) { checkNotNull(this) {
ERR_NOTARIZATION_SETTINGS_ARE_NOT_PROVIDED ERR_NOTARIZATION_SETTINGS_ARE_NOT_PROVIDED
} }
val bundleID = validateBundleID(bundleIDProvider)
check(!appleID.orNull.isNullOrEmpty()) { check(!appleID.orNull.isNullOrEmpty()) {
ERR_APPLE_ID_IS_EMPTY ERR_APPLE_ID_IS_EMPTY
} }
@ -31,10 +26,9 @@ internal fun MacOSNotarizationSettings?.validate(
ERR_PASSWORD_IS_EMPTY ERR_PASSWORD_IS_EMPTY
} }
return ValidatedMacOSNotarizationSettings( return ValidatedMacOSNotarizationSettings(
bundleID = bundleID,
appleID = appleID.orNull!!, appleID = appleID.orNull!!,
password = password.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: """|$ERR_PREFIX password is null or empty. To specify:
| * Use '${ComposeProperties.MAC_NOTARIZATION_PASSWORD}' Gradle property; | * Use '${ComposeProperties.MAC_NOTARIZATION_PASSWORD}' Gradle property;
| * Or use 'nativeDistributions.macOS.notarization.password' DSL property; | * Or use 'nativeDistributions.macOS.notarization.password' DSL property;
""".trimMargin() """.trimMargin()

60
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractCheckNotarizationStatusTask.kt

@ -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<NotarizationRequestInfo>()
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)
}
}
}
}

65
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 package org.jetbrains.compose.desktop.application.tasks
import org.gradle.api.provider.Property import org.gradle.api.file.DirectoryProperty
import org.gradle.api.tasks.Input import org.gradle.api.tasks.*
import org.gradle.api.tasks.Nested
import org.gradle.api.tasks.Optional
import org.jetbrains.compose.desktop.application.dsl.MacOSNotarizationSettings 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.application.internal.validation.validate
import org.jetbrains.compose.desktop.tasks.AbstractComposeDesktopTask 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:Input
@get:Optional val targetFormat: TargetFormat
internal val nonValidatedBundleID: Property<String?> = objects.nullableProperty() ) : AbstractComposeDesktopTask() {
@get:Nested @get:Nested
@get:Optional @get:Optional
internal var nonValidatedNotarizationSettings: MacOSNotarizationSettings? = null internal var nonValidatedNotarizationSettings: MacOSNotarizationSettings? = null
internal fun validateNotarization() = @get:InputDirectory
nonValidatedNotarizationSettings.validate(nonValidatedBundleID) 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)
)
}
}

82
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractUploadAppForNotarizationTask.kt

@ -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}")
}
}

81
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. 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 To notarize your app, you can use `notarize<PACKAGING_FORMAT>` task:
macOS {
notarization {
appleID.set("john.doe@example.com")
password.set("@keychain:NOTARIZATION_PASSWORD")
// optional
ascProvider.set("<TEAM_ID>")
}
}
``` ```
./gradlew notarizeDmg \
* Set `appleID` to your Apple ID. -Pcompose.desktop.mac.notarization.appleID=<APPLE_ID> \
* Alternatively, the `compose.desktop.mac.notarization.appleID` Gradle property can be used. -Pcompose.desktop.mac.notarization.password=<PASSWORD> \
* Set `password` to the app-specific password created previously. -Pcompose.desktop.mac.notarization.teamID=<TEAM_ID>
* Alternatively, the `compose.desktop.mac.notarization.password` Gradle property can be used. ```
* Don't write raw password directly into a build script. where:
* If the password was added to the keychain, as described previously, it can be referenced as * `<APPLE_ID>` — your Apple ID;
``` * `<PASSWORD>` — the app-specific password created previously;
@keychain:NOTARIZATION_PASSWORD * `<TEAM_ID>` — your Team. To get a table of team IDs associated with a given username and password, run:
```
* 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:
``` ```
xcrun altool --list-providers -u <Apple ID> -p <Notarization password>" xcrun altool --list-providers -u <Apple ID> -p <Notarization password>"
``` ```
<img alt="Team ID" src="notarization-team-id.png" />
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 ### Configuring provisioning profile
For testing on TestFlight (when publishing to the App Store), you need to add a provisioning 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). (no separate step is required).
* Use `notarize<PACKAGING_FORMAT>` (e.g. `notarizeDmg`) to upload an application for notarization. * Use `notarize<PACKAGING_FORMAT>` (e.g. `notarizeDmg`) to upload an application for notarization.
Notarization is only required for apps outside the App Store. 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 `<BUILD_DIR>/compose/notarization/main/<UPLOAD_DATE>-<PACKAGING_FORMAT>`
* 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 <RequestUUID>
--username <Apple_ID>
--password "@keychain:NOTARIZATION_PASSWORD"
```

BIN
tutorials/Signing_and_notarization_on_macOS/notarization-team-id.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Loading…
Cancel
Save