Alexey Tsvetkov
4 years ago
committed by
GitHub
18 changed files with 945 additions and 124 deletions
@ -0,0 +1,30 @@
|
||||
package org.jetbrains.compose.desktop.application.dsl |
||||
|
||||
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.Optional |
||||
import org.jetbrains.compose.desktop.application.internal.ComposeProperties |
||||
import org.jetbrains.compose.desktop.application.internal.nullableProperty |
||||
import javax.inject.Inject |
||||
|
||||
abstract class MacOSNotarizationSettings { |
||||
@get:Inject |
||||
protected abstract val objects: ObjectFactory |
||||
|
||||
@get:Inject |
||||
protected abstract val providers: ProviderFactory |
||||
|
||||
@get:Input |
||||
@get:Optional |
||||
val appleID: Property<String?> = objects.nullableProperty<String>().apply { |
||||
set(ComposeProperties.macNotarizationAppleID(providers)) |
||||
} |
||||
|
||||
@get:Input |
||||
@get:Optional |
||||
val password: Property<String?> = objects.nullableProperty<String>().apply { |
||||
set(ComposeProperties.macNotarizationPassword(providers)) |
||||
} |
||||
} |
@ -0,0 +1,41 @@
|
||||
package org.jetbrains.compose.desktop.application.dsl |
||||
|
||||
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.Optional |
||||
import org.jetbrains.compose.desktop.application.internal.ComposeProperties |
||||
import org.jetbrains.compose.desktop.application.internal.notNullProperty |
||||
import org.jetbrains.compose.desktop.application.internal.nullableProperty |
||||
import javax.inject.Inject |
||||
|
||||
abstract class MacOSSigningSettings { |
||||
@get:Inject |
||||
protected abstract val objects: ObjectFactory |
||||
@get:Inject |
||||
protected abstract val providers: ProviderFactory |
||||
|
||||
@get:Input |
||||
val sign: Property<Boolean> = objects.notNullProperty<Boolean>().apply { |
||||
set( |
||||
ComposeProperties.macSign(providers) |
||||
.orElse(false) |
||||
) |
||||
} |
||||
@get:Input |
||||
@get:Optional |
||||
val identity: Property<String?> = objects.nullableProperty<String>().apply { |
||||
set(ComposeProperties.macSignIdentity(providers)) |
||||
} |
||||
@get:Input |
||||
@get:Optional |
||||
val keychain: Property<String?> = objects.nullableProperty<String>().apply { |
||||
set(ComposeProperties.macSignKeychain(providers)) |
||||
} |
||||
@get:Input |
||||
@get:Optional |
||||
val prefix: Property<String?> = objects.nullableProperty<String>().apply { |
||||
set(ComposeProperties.macSignPrefix(providers)) |
||||
} |
||||
} |
@ -0,0 +1,7 @@
|
||||
package org.jetbrains.compose.desktop.application.internal |
||||
|
||||
import java.io.File |
||||
|
||||
internal interface FileCopyingProcessor { |
||||
fun copy(source: File, target: File) |
||||
} |
@ -0,0 +1,116 @@
|
||||
package org.jetbrains.compose.desktop.application.internal |
||||
|
||||
import org.gradle.process.ExecOperations |
||||
import org.jetbrains.compose.desktop.application.internal.validation.ValidatedMacOSSigningSettings |
||||
import java.io.* |
||||
import java.util.regex.Pattern |
||||
import java.util.zip.ZipEntry |
||||
import java.util.zip.ZipInputStream |
||||
import java.util.zip.ZipOutputStream |
||||
|
||||
internal class MacJarSignFileCopyingProcessor( |
||||
private val tempDir: File, |
||||
private val execOperations: ExecOperations, |
||||
private val signing: ValidatedMacOSSigningSettings |
||||
) : FileCopyingProcessor { |
||||
private val signKey: String |
||||
|
||||
init { |
||||
val certificates = ByteArrayOutputStream().use { baos -> |
||||
PrintStream(baos).use { ps -> |
||||
execOperations.exec { exec -> |
||||
exec.executable = MacUtils.security.absolutePath |
||||
val args = arrayListOf("find-certificate", "-a", "-c", signing.fullDeveloperID) |
||||
signing.keychain?.let { args.add(it.absolutePath) } |
||||
exec.args(*args.toTypedArray()) |
||||
exec.standardOutput = ps |
||||
} |
||||
} |
||||
baos.toString() |
||||
} |
||||
val regex = Pattern.compile("\"alis\"<blob>=\"([^\"]+)\"") |
||||
val m = regex.matcher(certificates) |
||||
if (!m.find()) { |
||||
val keychainPath = signing.keychain?.absolutePath |
||||
error( |
||||
"Could not find certificate for '${signing.identity}'" + |
||||
" in keychain [${keychainPath.orEmpty()}]" |
||||
) |
||||
} |
||||
|
||||
signKey = m.group(1) |
||||
if (m.find()) error("Multiple matching certificates are found for '${signing.fullDeveloperID}'. " + |
||||
"Please specify keychain containing unique matching certificate.") |
||||
} |
||||
|
||||
override fun copy(source: File, target: File) { |
||||
if (!source.isJarFile) { |
||||
SimpleFileCopyingProcessor.copy(source, target) |
||||
return |
||||
} |
||||
|
||||
if (target.exists()) target.delete() |
||||
|
||||
ZipInputStream(FileInputStream(source).buffered()).use { zin -> |
||||
ZipOutputStream(FileOutputStream(target).buffered()).use { zout -> |
||||
copyAndSignNativeLibs(zin, zout) |
||||
} |
||||
} |
||||
} |
||||
|
||||
private fun copyAndSignNativeLibs(zin: ZipInputStream, zout: ZipOutputStream) { |
||||
for (sourceEntry in generateSequence { zin.nextEntry }) { |
||||
if (!sourceEntry.name.endsWith(".dylib")) { |
||||
zout.putNextEntry(ZipEntry(sourceEntry)) |
||||
zin.copyTo(zout) |
||||
} else { |
||||
|
||||
val unpackedDylibFile = tempDir.resolve(sourceEntry.name.substringAfterLast("/")) |
||||
try { |
||||
unpackedDylibFile.outputStream().buffered().use { |
||||
zin.copyTo(it) |
||||
} |
||||
|
||||
signDylib(unpackedDylibFile) |
||||
val targetEntry = ZipEntry(sourceEntry.name).apply { |
||||
comment = sourceEntry.comment |
||||
extra = sourceEntry.extra |
||||
method = sourceEntry.method |
||||
size = unpackedDylibFile.length() |
||||
} |
||||
zout.putNextEntry(targetEntry) |
||||
|
||||
unpackedDylibFile.inputStream().buffered().use { |
||||
it.copyTo(zout) |
||||
} |
||||
} finally { |
||||
unpackedDylibFile.delete() |
||||
} |
||||
} |
||||
zout.closeEntry() |
||||
} |
||||
} |
||||
|
||||
private fun signDylib(dylibFile: File) { |
||||
val args = arrayListOf( |
||||
"-vvvv", |
||||
"--timestamp", |
||||
"--options", "runtime", |
||||
"--force", |
||||
"--prefix", signing.prefix, |
||||
"--sign", signKey |
||||
) |
||||
|
||||
signing.keychain?.let { |
||||
args.add("--keychain") |
||||
args.add(it.absolutePath) |
||||
} |
||||
|
||||
args.add(dylibFile.absolutePath) |
||||
|
||||
execOperations.exec { exec -> |
||||
exec.executable = MacUtils.codesign.absolutePath |
||||
exec.args(*args.toTypedArray()) |
||||
}.assertNormalExitValue() |
||||
} |
||||
} |
@ -0,0 +1,9 @@
|
||||
package org.jetbrains.compose.desktop.application.internal |
||||
|
||||
import java.io.File |
||||
|
||||
object SimpleFileCopyingProcessor : FileCopyingProcessor { |
||||
override fun copy(source: File, target: File) { |
||||
source.copyTo(target, overwrite = true) |
||||
} |
||||
} |
@ -0,0 +1,40 @@
|
||||
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 |
||||
) |
||||
|
||||
internal fun MacOSNotarizationSettings.validate( |
||||
bundleIDProvider: Provider<String?> |
||||
): ValidatedMacOSNotarizationSettings { |
||||
val bundleID = validateBundleID(bundleIDProvider) |
||||
check(!appleID.orNull.isNullOrEmpty()) { |
||||
ERR_APPLE_ID_IS_EMPTY |
||||
} |
||||
check(!password.orNull.isNullOrEmpty()) { |
||||
ERR_PASSWORD_IS_EMPTY |
||||
} |
||||
return ValidatedMacOSNotarizationSettings( |
||||
bundleID = bundleID, |
||||
appleID = appleID.orNull!!, |
||||
password = password.orNull!! |
||||
) |
||||
} |
||||
|
||||
private const val ERR_PREFIX = "Notarization settings error:" |
||||
private val ERR_APPLE_ID_IS_EMPTY = |
||||
"""|$ERR_PREFIX appleID is null or empty. To specify: |
||||
| * Use '${ComposeProperties.MAC_NOTARIZATION_APPLE_ID}' Gradle property; |
||||
| * Or use 'nativeDistributions.macOS.notarization.appleID' DSL property; |
||||
""".trimMargin() |
||||
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() |
@ -0,0 +1,67 @@
|
||||
package org.jetbrains.compose.desktop.application.internal.validation |
||||
|
||||
import org.gradle.api.provider.Provider |
||||
import org.jetbrains.compose.desktop.application.dsl.MacOSSigningSettings |
||||
import org.jetbrains.compose.desktop.application.internal.ComposeProperties |
||||
import org.jetbrains.compose.desktop.application.internal.OS |
||||
import org.jetbrains.compose.desktop.application.internal.currentOS |
||||
import java.io.File |
||||
|
||||
internal data class ValidatedMacOSSigningSettings( |
||||
val bundleID: String, |
||||
val identity: String, |
||||
val keychain: File?, |
||||
val prefix: String, |
||||
) { |
||||
val fullDeveloperID: String |
||||
get() { |
||||
val developerIdPrefix = "Developer ID Application: " |
||||
val thirdPartyMacDeveloperPrefix = "3rd Party Mac Developer Application: " |
||||
return when { |
||||
identity.startsWith(developerIdPrefix) -> identity |
||||
identity.startsWith(thirdPartyMacDeveloperPrefix) -> identity |
||||
else -> developerIdPrefix + identity |
||||
} |
||||
} |
||||
} |
||||
|
||||
internal fun MacOSSigningSettings.validate( |
||||
bundleIDProvider: Provider<String?> |
||||
): ValidatedMacOSSigningSettings { |
||||
check(currentOS == OS.MacOS) { ERR_WRONG_OS } |
||||
|
||||
val bundleID = validateBundleID(bundleIDProvider) |
||||
val signPrefix = this.prefix.orNull |
||||
?: (bundleID.substringBeforeLast(".") + ".").takeIf { bundleID.contains('.') } |
||||
?: error(ERR_UNKNOWN_PREFIX) |
||||
val signIdentity = this.identity.orNull |
||||
?: error(ERR_UNKNOWN_SIGN_ID) |
||||
val keychainFile = this.keychain.orNull?.let { File(it) } |
||||
if (keychainFile != null) { |
||||
check(keychainFile.exists()) { |
||||
"$ERR_PREFIX keychain is not an existing file: ${keychainFile.absolutePath}" |
||||
} |
||||
} |
||||
|
||||
return ValidatedMacOSSigningSettings( |
||||
bundleID = bundleID, |
||||
identity = signIdentity, |
||||
keychain = keychainFile, |
||||
prefix = signPrefix |
||||
) |
||||
} |
||||
|
||||
private const val ERR_PREFIX = "Signing settings error:" |
||||
private val ERR_WRONG_OS = |
||||
"$ERR_PREFIX macOS was expected, actual OS is $currentOS" |
||||
private val ERR_UNKNOWN_PREFIX = |
||||
"""|$ERR_PREFIX Could not infer signing prefix. To specify: |
||||
| * Set bundleID to reverse DNS notation (e.g. "com.mycompany.myapp"); |
||||
| * Use '${ComposeProperties.MAC_SIGN_PREFIX}' Gradle property; |
||||
| * Use 'nativeExecutables.macOS.signing.prefix' DSL property; |
||||
""".trimMargin() |
||||
private val ERR_UNKNOWN_SIGN_ID = |
||||
"""|$ERR_PREFIX signing identity is null or empty. To specify: |
||||
| * Use '${ComposeProperties.MAC_SIGN_ID}' Gradle property; |
||||
| * Use 'nativeExecutables.macOS.signing.identity' DSL property; |
||||
""".trimMargin() |
@ -0,0 +1,22 @@
|
||||
package org.jetbrains.compose.desktop.application.internal.validation |
||||
|
||||
import org.gradle.api.provider.Provider |
||||
|
||||
internal fun validateBundleID(bundleIDProvider: Provider<String?>): String { |
||||
val bundleID = bundleIDProvider.orNull |
||||
check(!bundleID.isNullOrEmpty()) { ERR_BUNDLE_ID_IS_EMPTY } |
||||
check(bundleID.matches("[A-Za-z0-9\\-\\.]+".toRegex())) { ERR_BUNDLE_ID_WRONG_FORMAT } |
||||
return bundleID |
||||
} |
||||
|
||||
private const val ERR_PREFIX = "macOS settings error:" |
||||
private const val BUNDLE_ID_FORMAT = |
||||
"bundleID may only contain alphanumeric characters (A-Z, a-z, 0-9), hyphen (-) and period (.) characters" |
||||
private val ERR_BUNDLE_ID_IS_EMPTY = |
||||
"""|$ERR_PREFIX bundleID is empty or null. To specify: |
||||
| * Use 'nativeExecutables.macOS.bundleID' DSL property; |
||||
| * $BUNDLE_ID_FORMAT; |
||||
| * Use reverse DNS notation (e.g. "com.mycompany.myapp"); |
||||
|""".trimMargin() |
||||
private val ERR_BUNDLE_ID_WRONG_FORMAT = |
||||
"$ERR_PREFIX $BUNDLE_ID_FORMAT" |
@ -0,0 +1,27 @@
|
||||
package org.jetbrains.compose.desktop.application.tasks |
||||
|
||||
import org.gradle.api.file.RegularFileProperty |
||||
import org.gradle.api.tasks.* |
||||
import org.jetbrains.compose.desktop.application.internal.MacUtils |
||||
import org.jetbrains.compose.desktop.application.internal.ioFile |
||||
|
||||
abstract class AbstractCheckNotarizationStatusTask : AbstractNotarizationTask() { |
||||
@get:InputFile |
||||
val requestIDFile: RegularFileProperty = objects.fileProperty() |
||||
|
||||
@TaskAction |
||||
fun run() { |
||||
val notarization = validateNotarization() |
||||
|
||||
val requestId = requestIDFile.ioFile.readText() |
||||
execOperations.exec { exec -> |
||||
exec.executable = MacUtils.xcrun.absolutePath |
||||
exec.args( |
||||
"altool", |
||||
"--notarization-info", requestId, |
||||
"--username", notarization.appleID, |
||||
"--password", notarization.password |
||||
) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,31 @@
|
||||
package org.jetbrains.compose.desktop.application.tasks |
||||
|
||||
import org.gradle.api.DefaultTask |
||||
import org.gradle.api.model.ObjectFactory |
||||
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.process.ExecOperations |
||||
import org.jetbrains.compose.desktop.application.dsl.MacOSNotarizationSettings |
||||
import org.jetbrains.compose.desktop.application.internal.nullableProperty |
||||
import org.jetbrains.compose.desktop.application.internal.validation.validate |
||||
import javax.inject.Inject |
||||
|
||||
abstract class AbstractNotarizationTask( |
||||
) : DefaultTask() { |
||||
@get:Inject |
||||
protected abstract val objects: ObjectFactory |
||||
@get:Inject |
||||
protected abstract val execOperations: ExecOperations |
||||
|
||||
@get:Input |
||||
@get:Optional |
||||
internal val nonValidatedBundleID: Property<String?> = objects.nullableProperty() |
||||
|
||||
@get:Nested |
||||
internal lateinit var nonValidatedNotarizationSettings: MacOSNotarizationSettings |
||||
|
||||
internal fun validateNotarization() = |
||||
nonValidatedNotarizationSettings.validate(nonValidatedBundleID) |
||||
} |
@ -0,0 +1,68 @@
|
||||
package org.jetbrains.compose.desktop.application.tasks |
||||
|
||||
import org.gradle.api.file.DirectoryProperty |
||||
import org.gradle.api.file.RegularFileProperty |
||||
import org.gradle.api.tasks.* |
||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat |
||||
import org.jetbrains.compose.desktop.application.internal.* |
||||
import java.io.ByteArrayOutputStream |
||||
import java.io.PrintStream |
||||
import javax.inject.Inject |
||||
|
||||
abstract class AbstractUploadAppForNotarizationTask @Inject constructor( |
||||
@get:Input |
||||
val targetFormat: TargetFormat, |
||||
) : AbstractNotarizationTask() { |
||||
@get:InputDirectory |
||||
val inputDir: DirectoryProperty = objects.directoryProperty() |
||||
|
||||
@get:OutputFile |
||||
val requestIDFile: RegularFileProperty = objects.fileProperty() |
||||
|
||||
init { |
||||
check(targetFormat != TargetFormat.AppImage) { "${TargetFormat.AppImage} cannot be notarized!" } |
||||
} |
||||
|
||||
@TaskAction |
||||
fun run() { |
||||
val notarization = validateNotarization() |
||||
|
||||
val inputFile = findOutputFileOrDir(inputDir.ioFile, targetFormat) |
||||
val file = inputFile.checkExistingFile() |
||||
|
||||
logger.quiet("Uploading '${file.name}' for notarization (package id: '${notarization.bundleID}')") |
||||
val (res, output) = ByteArrayOutputStream().use { baos -> |
||||
PrintStream(baos).use { ps -> |
||||
val res = execOperations.exec { exec -> |
||||
exec.executable = MacUtils.xcrun.absolutePath |
||||
exec.args( |
||||
"altool", |
||||
"--notarize-app", |
||||
"--primary-bundle-id", notarization.bundleID, |
||||
"--username", notarization.appleID, |
||||
"--password", notarization.password, |
||||
"--file", file |
||||
) |
||||
exec.standardOutput = ps |
||||
} |
||||
|
||||
res to baos.toString() |
||||
} |
||||
} |
||||
if (res.exitValue != 0) { |
||||
logger.error("Uploading failed. Stdout: $output") |
||||
res.assertNormalExitValue() |
||||
} |
||||
val m = "RequestUUID = ([A-Za-z0-9\\-]+)".toRegex().find(output) |
||||
?: error("Could not determine RequestUUID from output: $output") |
||||
|
||||
val requestId = m.groupValues[1] |
||||
requestIDFile.ioFile.apply { |
||||
parentFile.mkdirs() |
||||
writeText(requestId) |
||||
} |
||||
|
||||
logger.quiet("Request UUID: $requestId") |
||||
logger.quiet("Request UUID is saved to ${requestIDFile.ioFile.absolutePath}") |
||||
} |
||||
} |
@ -0,0 +1,226 @@
|
||||
# Signing and notarizing distributions for macOS |
||||
|
||||
Apple [requires](https://developer.apple.com/documentation/xcode/notarizing_macos_software_before_distribution) |
||||
all 3rd apps to be signed and notarized (checked by Apple) |
||||
for running on recent versions of macOS. |
||||
|
||||
## What is covered |
||||
|
||||
In this tutorial, we'll show you how to sign and notarize |
||||
native distributions of Compose apps (in `dmg` or `pkg` formats) |
||||
for distribution on macOS. |
||||
|
||||
## Prerequisites |
||||
|
||||
* [Xcode](https://developer.apple.com/xcode/). The tutorial was checked with Xcode 12.3. |
||||
* JDK 15+ (JDK 14 is not guaranteed to work). The tutorial was checked with OpenJDK 15.0.1. |
||||
|
||||
## Preparing a Developer ID certificate |
||||
|
||||
You will need a Developer ID certificate for signing your app. |
||||
|
||||
#### Checking existing Developer ID certificates |
||||
|
||||
Open https://developer.apple.com/account/resources/certificates |
||||
|
||||
#### Creating a new Developer ID certificate |
||||
1. [Create a certificate signing request](https://help.apple.com/developer-account/#/devbfa00fef7): |
||||
* Open `Keychain Access`. |
||||
* Open the menu dialog |
||||
``` |
||||
Keychain Access > Certificate Assistant > Request a Certificate from a Certificate Authority |
||||
``` |
||||
* Enter your Developer ID email and common name. |
||||
* Check `Save to disk` option. |
||||
2. Create and install a new certificate using your [Apple Developer account](https://developer.apple.com/account/): |
||||
* Open https://developer.apple.com/account/resources/certificates/add |
||||
* Choose the `Developer ID Application` certificate type. |
||||
* Upload your Certificate Signing Request from the previous step. |
||||
* Download and install the certificate (drag & drop the certificate into the `Keychain Access` application). |
||||
|
||||
#### Viewing installed certificates |
||||
|
||||
You can find all installed certificates and their keychains by running the following command: |
||||
``` |
||||
/usr/bin/security find-certificate -c "Developer ID Application" |
||||
``` |
||||
|
||||
If you have multiple `Developer ID Application` certificates installed, |
||||
you will need to specify the path to the keychain, containing |
||||
the certificate intended for signing. |
||||
|
||||
## Preparing an App ID |
||||
|
||||
An App ID represents one or more applications in Apple's ecosystem. |
||||
|
||||
#### Viewing existing App IDs |
||||
|
||||
Open [the page](https://developer.apple.com/account/resources/identifiers/list) on Apple's developer portal. |
||||
|
||||
#### Creating a new App ID |
||||
|
||||
|
||||
1. Open [the page](https://developer.apple.com/account/resources/identifiers/add/bundleId) on Apple's developer portal. |
||||
2. Choose `App ID` option. |
||||
3. Choose `App` type. |
||||
4. Fill the `Bundle ID` field. |
||||
* A [bundle ID](https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleidentifier) |
||||
uniquely identifies an application in Apple's ecosystem. |
||||
* You can use an explicit bundle ID a wildcard, matching multiple bundle IDs. |
||||
* It is recommended to use the reverse DNS notation (e.g.`com.yoursitename.yourappname`). |
||||
|
||||
## Creating an app-specific password |
||||
|
||||
To be able to upload an app for notarization, |
||||
you will need an app-specific password associated with your Apple ID. |
||||
|
||||
Follow these steps to generate a new password: |
||||
1. Sign in to your [Apple ID](https://appleid.apple.com/account/home) account page. |
||||
2. In the Security section, click Generate Password below App-Specific Passwords. |
||||
|
||||
See [this Apple support page](https://support.apple.com/en-us/HT204397) for more information |
||||
on the app-specific passwords. |
||||
|
||||
## Adding an app-specific password to a keychain |
||||
|
||||
To avoid remembering your one-time password, or writing it in scripts, |
||||
you can add it to the keychain by running: |
||||
``` |
||||
# Any name can be used instead of NOTARIZATION_PASSWORD |
||||
|
||||
xcrun altool --store-password-in-keychain-item "NOTARIZATION_PASSWORD" |
||||
--username <apple_id> |
||||
--password <password> |
||||
``` |
||||
|
||||
Then you'll be able to refer to the password like `@keychain:NOTARIZATION_PASSWORD` |
||||
without the need to write the password itself. |
||||
|
||||
## Configuring Gradle |
||||
|
||||
### Gradle DSL |
||||
|
||||
DSL properties should be specified in `macOS` DSL block of Compose Desktop DSL: |
||||
``` kotlin |
||||
import org.jetbrains.compose.compose |
||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat |
||||
|
||||
plugins { |
||||
kotlin("jvm") |
||||
id("org.jetbrains.compose") |
||||
} |
||||
|
||||
dependencies { |
||||
implementation(compose.desktop.currentOS) |
||||
} |
||||
|
||||
compose.desktop { |
||||
application { |
||||
mainClass = "example.MainKt" |
||||
|
||||
nativeDistributions { |
||||
targetFormats(TargetFormat.Dmg) |
||||
|
||||
macOS { |
||||
// macOS DSL settings |
||||
} |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### Gradle properties |
||||
|
||||
Some properties can also be specified using |
||||
[Gradle properties](https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties). |
||||
|
||||
* Default Gradle properties (`compose.desktop.mac.*`) have lower priority, than DSL properties. |
||||
* Gradle properties can be specified (the items are listed in order of ascending priority): |
||||
* In `gradle.properties` file in Gradle home; |
||||
* In `gradle.properties` file in project's root; |
||||
* In command-line |
||||
``` |
||||
./gradlew packageDmg -Pcompose.desktop.mac.sign=true |
||||
``` |
||||
* Note, that `local.properties` is not a standard Gradle file, so it is not supported by default. |
||||
You can load custom properties from it manually in a script, if you want. |
||||
|
||||
### Configuring bundle ID |
||||
|
||||
``` kotlin |
||||
macOS { |
||||
bundleID = "com.example-company.example-app" |
||||
} |
||||
``` |
||||
|
||||
A [bundle ID](https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleidentifier) |
||||
uniquely identifies an application in Apple's ecosystem. |
||||
* A bundle ID must be specified using the `bundleID` DSL property. |
||||
* Use only alphanumeric characters (`A-Z`, `a-z`, and `0-9`), hyphen (`-`) and period (`.`) characters. |
||||
* Use the reverse DNS notation of your domain (e.g. |
||||
`com.yoursitename.yourappname`). |
||||
* The specified bundle ID must match one of your App IDs. |
||||
|
||||
### Configuring signing settings |
||||
|
||||
``` kotlin |
||||
macOS { |
||||
signing { |
||||
sign.set(true) |
||||
identity.set("John Doe") |
||||
// keychain.set("/path/to/keychain") |
||||
} |
||||
} |
||||
``` |
||||
|
||||
* Set the `sign` DSL property or to `true`. |
||||
* Alternatively, the `compose.desktop.mac.sign` Gradle property can be used. |
||||
* Set the `identity` DSL property to the certificate's name, e.g. `"John Doe"`. |
||||
* Alternatively, the `compose.desktop.mac.signing.identity` Gradle property can be used. |
||||
* Optionally, set the `keychain` DSL property to the path to the specific keychain, containing your certificate. |
||||
* Alternatively, the `compose.desktop.mac.signing.keychain` Gradle property can be used. |
||||
* This step is only necessary, if multiple `Developer ID Application` certificates are installed. |
||||
|
||||
The following Gradle properties can be used instead of DSL properties: |
||||
* `compose.desktop.mac.sign` enables or disables signing. |
||||
Possible values: `true` or `false`. |
||||
* `compose.desktop.mac.signing.identity` overrides the `identity` DSL property. |
||||
* `compose.desktop.mac.signing.keychain` overrides the `keychain` DSL property. |
||||
|
||||
### Configuring notarization settings |
||||
|
||||
``` kotlin |
||||
macOS { |
||||
notarization { |
||||
appleID.set("john.doe@example.com") |
||||
password.set("@keychain:NOTARIZATION_PASSWORD") |
||||
} |
||||
} |
||||
``` |
||||
|
||||
* Set `appleID` to your Apple ID. |
||||
* Alternatively, the `compose.desktop.mac.notarization.appleID` can be used. |
||||
* Set `password` to the app-specific password created previously. |
||||
* Alternatively, the `compose.desktop.mac.notarization.password` 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 |
||||
``` |
||||
|
||||
## Using Gradle |
||||
|
||||
The following tasks are available: |
||||
* Use `createDistributable` or `package<PACKAGING_FORMAT>` to get a signed application |
||||
(no separate step is required). |
||||
* Use `notarize<PACKAGING_FORMAT>` to upload an application for notarization. |
||||
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. |
||||
* Use `checkNotarizationStatus<PACKAGING_FORMAT>` to check a status of |
||||
the last notarization request. You can also use a command-line command directly: |
||||
``` |
||||
xcrun altool --notarization-info <RequestUUID> |
||||
--username <Apple_ID> |
||||
--password "@keychain:NOTARIZATION_PASSWORD" |
||||
``` |
Loading…
Reference in new issue