Browse Source

Apple App Store (#1613)

* Add appStore option

* Fix "--app-image" option

* Add appCategory option

* Document signing for the Apple App Store

* Fix system version in the Product Definition Property List

* fixup! Fix "--app-image" option

* fixup! Document signing for the Apple App Store

* fixup! Add appStore option

* fixup! Add appCategory option

* Add provisioningProfile option

* Add entitlementsFile option (needs to be customised if your Mac app uses entitlements not in the default file)
pull/1707/head
Thomas Vos 3 years ago committed by GitHub
parent
commit
793b377147
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/PlatformSettings.kt
  2. 4
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt
  3. 10
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/validation/ValidatedMacOSSigningSettings.kt
  4. 47
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt
  5. 11
      tutorials/Native_distributions_and_local_execution/README.md
  6. 49
      tutorials/Signing_and_notarization_on_macOS/README.md

5
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/PlatformSettings.kt

@ -20,6 +20,9 @@ open class MacOSPlatformSettings @Inject constructor(objects: ObjectFactory): Pl
var packageName: String? = null var packageName: String? = null
var dockName: String? = null var dockName: String? = null
var setDockNameSameAsPackageName: Boolean = true var setDockNameSameAsPackageName: Boolean = true
var appStore: Boolean = false
var appCategory: String? = null
var entitlementsFile: RegularFileProperty = objects.fileProperty()
var packageBuildVersion: String? = null var packageBuildVersion: String? = null
var dmgPackageVersion: String? = null var dmgPackageVersion: String? = null
var dmgPackageBuildVersion: String? = null var dmgPackageBuildVersion: String? = null
@ -45,6 +48,8 @@ open class MacOSPlatformSettings @Inject constructor(objects: ObjectFactory): Pl
fn.execute(notarization) fn.execute(notarization)
} }
val provisioningProfile: RegularFileProperty = objects.fileProperty()
internal val infoPlistSettings = InfoPlistSettings() internal val infoPlistSettings = InfoPlistSettings()
fun infoPlist(fn: Action<InfoPlistSettings>) { fun infoPlist(fn: Action<InfoPlistSettings>) {
fn.execute(infoPlistSettings) fn.execute(infoPlistSettings)

4
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt

@ -296,8 +296,12 @@ internal fun AbstractJPackageTask.configurePlatformSettings(app: Application) {
else else
provider { mac.dockName } provider { mac.dockName }
) )
macAppStore.set(mac.appStore)
macAppCategory.set(mac.appCategory)
macEntitlementsFile.set(mac.entitlementsFile)
packageBuildVersion.set(packageBuildVersionFor(project, app, targetFormat)) packageBuildVersion.set(packageBuildVersionFor(project, app, targetFormat))
nonValidatedMacBundleID.set(provider { mac.bundleID }) nonValidatedMacBundleID.set(provider { mac.bundleID })
macProvisioningProfile.set(mac.provisioningProfile)
macExtraPlistKeysRawXml.set(provider { mac.infoPlistSettings.extraKeysRawXml }) macExtraPlistKeysRawXml.set(provider { mac.infoPlistSettings.extraKeysRawXml })
nonValidatedMacSigningSettings = app.nativeDistributions.macOS.signing nonValidatedMacSigningSettings = app.nativeDistributions.macOS.signing
iconFile.set(mac.iconFile.orElse(DefaultIcons.forMac(project))) iconFile.set(mac.iconFile.orElse(DefaultIcons.forMac(project)))

10
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/validation/ValidatedMacOSSigningSettings.kt

@ -18,6 +18,7 @@ internal data class ValidatedMacOSSigningSettings(
val identity: String, val identity: String,
val keychain: File?, val keychain: File?,
val prefix: String, val prefix: String,
private val appStore: Boolean
) { ) {
val fullDeveloperID: String val fullDeveloperID: String
get() { get() {
@ -26,14 +27,15 @@ internal data class ValidatedMacOSSigningSettings(
return when { return when {
identity.startsWith(developerIdPrefix) -> identity identity.startsWith(developerIdPrefix) -> identity
identity.startsWith(thirdPartyMacDeveloperPrefix) -> identity identity.startsWith(thirdPartyMacDeveloperPrefix) -> identity
else -> developerIdPrefix + identity else -> (if (!appStore) developerIdPrefix else thirdPartyMacDeveloperPrefix) + identity
} }
} }
} }
internal fun MacOSSigningSettings.validate( internal fun MacOSSigningSettings.validate(
bundleIDProvider: Provider<String?>, bundleIDProvider: Provider<String?>,
project: Project project: Project,
appStoreProvider: Provider<Boolean?>
): ValidatedMacOSSigningSettings { ): ValidatedMacOSSigningSettings {
check(currentOS == OS.MacOS) { ERR_WRONG_OS } check(currentOS == OS.MacOS) { ERR_WRONG_OS }
@ -52,12 +54,14 @@ internal fun MacOSSigningSettings.validate(
} }
keychainFile keychainFile
} else null } else null
val appStore = appStoreProvider.orNull == true
return ValidatedMacOSSigningSettings( return ValidatedMacOSSigningSettings(
bundleID = bundleID, bundleID = bundleID,
identity = signIdentity, identity = signIdentity,
keychain = keychainFile, keychain = keychainFile,
prefix = signPrefix prefix = signPrefix,
appStore = appStore
) )
} }

47
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt

@ -127,10 +127,28 @@ abstract class AbstractJPackageTask @Inject constructor(
@get:Optional @get:Optional
val macDockName: Property<String?> = objects.nullableProperty() val macDockName: Property<String?> = objects.nullableProperty()
@get:Input
@get:Optional
val macAppStore: Property<Boolean?> = objects.nullableProperty()
@get:Input
@get:Optional
val macAppCategory: Property<String?> = objects.nullableProperty()
@get:InputFile
@get:Optional
@get:PathSensitive(PathSensitivity.ABSOLUTE)
val macEntitlementsFile: RegularFileProperty = objects.fileProperty()
@get:Input @get:Input
@get:Optional @get:Optional
val packageBuildVersion: Property<String?> = objects.nullableProperty() val packageBuildVersion: Property<String?> = objects.nullableProperty()
@get:InputFile
@get:Optional
@get:PathSensitive(PathSensitivity.ABSOLUTE)
val macProvisioningProfile: RegularFileProperty = objects.fileProperty()
@get:Input @get:Input
@get:Optional @get:Optional
val winConsole: Property<Boolean?> = objects.nullableProperty() val winConsole: Property<Boolean?> = objects.nullableProperty()
@ -183,7 +201,7 @@ abstract class AbstractJPackageTask @Inject constructor(
val nonValidatedSettings = nonValidatedMacSigningSettings val nonValidatedSettings = nonValidatedMacSigningSettings
if (currentOS == OS.MacOS && nonValidatedSettings?.sign?.get() == true) { if (currentOS == OS.MacOS && nonValidatedSettings?.sign?.get() == true) {
val validatedSettings = val validatedSettings =
nonValidatedSettings.validate(nonValidatedMacBundleID, project) nonValidatedSettings.validate(nonValidatedMacBundleID, project, macAppStore)
MacSigner(validatedSettings, runExternalTool) MacSigner(validatedSettings, runExternalTool)
} else null } else null
} }
@ -275,14 +293,23 @@ abstract class AbstractJPackageTask @Inject constructor(
macDockName.orNull?.let { dockName -> macDockName.orNull?.let { dockName ->
javaOption("-Xdock:name=$dockName") javaOption("-Xdock:name=$dockName")
} }
macProvisioningProfile.orNull?.let { provisioningProfile ->
cliArg("--app-content", provisioningProfile)
}
} }
} }
if (targetFormat != TargetFormat.AppImage) { if (targetFormat != TargetFormat.AppImage) {
// Args, that can only be used, when creating an installer // Args, that can only be used, when creating an installer
if (currentOS == OS.MacOS && macAppStore.orNull == true) {
// This is needed to prevent a directory does not exist error.
cliArg("--app-image", appImage.dir("${packageName.get()}.app"))
} else {
cliArg("--app-image", appImage) cliArg("--app-image", appImage)
}
cliArg("--install-dir", installationPath) cliArg("--install-dir", installationPath)
cliArg("--license-file", licenseFile) cliArg("--license-file", licenseFile)
cliArg("--resource-dir", jpackageResources)
when (currentOS) { when (currentOS) {
OS.Linux -> { OS.Linux -> {
@ -320,6 +347,9 @@ abstract class AbstractJPackageTask @Inject constructor(
OS.MacOS -> { OS.MacOS -> {
cliArg("--mac-package-name", macPackageName) cliArg("--mac-package-name", macPackageName)
cliArg("--mac-package-identifier", nonValidatedMacBundleID) cliArg("--mac-package-identifier", nonValidatedMacBundleID)
cliArg("--mac-app-store", macAppStore)
cliArg("--mac-app-category", macAppCategory)
cliArg("--mac-entitlements", macEntitlementsFile)
macSigner?.let { signer -> macSigner?.let { signer ->
cliArg("--mac-sign", true) cliArg("--mac-sign", true)
@ -424,6 +454,17 @@ abstract class AbstractJPackageTask @Inject constructor(
InfoPlistBuilder(macExtraPlistKeysRawXml.orNull) InfoPlistBuilder(macExtraPlistKeysRawXml.orNull)
.also { setInfoPlistValues(it) } .also { setInfoPlistValues(it) }
.writeToFile(jpackageResources.ioFile.resolve("Info.plist")) .writeToFile(jpackageResources.ioFile.resolve("Info.plist"))
if (macAppStore.orNull == true) {
val productDefPlistXml = """
<key>os</key>
<array>
<string>10.13</string>
</array>
""".trimIndent()
InfoPlistBuilder(productDefPlistXml)
.writeToFile(jpackageResources.ioFile.resolve("product-def.plist"))
}
} }
} }
@ -480,7 +521,9 @@ abstract class AbstractJPackageTask @Inject constructor(
plist[PlistKeys.CFBundlePackageType] = "APPL" plist[PlistKeys.CFBundlePackageType] = "APPL"
val packageVersion = packageVersion.get()!! val packageVersion = packageVersion.get()!!
plist[PlistKeys.CFBundleShortVersionString] = packageVersion plist[PlistKeys.CFBundleShortVersionString] = packageVersion
plist[PlistKeys.LSApplicationCategoryType] = "Unknown" // If building for the App Store, use "utilities" as default just like jpackage.
val category = macAppCategory.orNull ?: (if (macAppStore.orNull == true) "utilities" else null)
plist[PlistKeys.LSApplicationCategoryType] = category?.let { "public.app-category.$it" } ?: "Unknown"
val packageBuildVersion = packageBuildVersion.orNull ?: packageVersion val packageBuildVersion = packageBuildVersion.orNull ?: packageVersion
plist[PlistKeys.CFBundleVersion] = packageBuildVersion plist[PlistKeys.CFBundleVersion] = packageBuildVersion
val year = Calendar.getInstance().get(Calendar.YEAR) val year = Calendar.getInstance().get(Calendar.YEAR)

11
tutorials/Native_distributions_and_local_execution/README.md

@ -256,7 +256,7 @@ The following properties are available in the `nativeDistributions` DSL block:
* `version` — application's version (default value: Gradle project's [version](https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html#getVersion--)); * `version` — application's version (default value: Gradle project's [version](https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html#getVersion--));
* `description` — application's description (default value: none); * `description` — application's description (default value: none);
* `copyright` — application's copyright (default value: none); * `copyright` — application's copyright (default value: none);
* `vendor` — application's vendor (default value: none). * `vendor` — application's vendor (default value: none);
* `licenseFile` — application's license (default value: none). * `licenseFile` — application's license (default value: none).
``` kotlin ``` kotlin
@ -436,9 +436,16 @@ The following platform-specific options are available
* `packageName` — a name of the application; * `packageName` — a name of the application;
* `dockName` — a name of the application displayed in the menu bar, the "About <App>" menu item, in the dock, etc. * `dockName` — a name of the application displayed in the menu bar, the "About <App>" menu item, in the dock, etc.
Equals to `packageName` by default. Equals to `packageName` by default.
* `signing` and `notarization` — see * `signing`, `notarization`, and `provisioningProfile` — see
[the corresponding tutorial](/tutorials/Signing_and_notarization_on_macOS/README.md) [the corresponding tutorial](/tutorials/Signing_and_notarization_on_macOS/README.md)
for details; for details;
* `appStore = true` — build and sign for the Apple App Store. Requires at least JDK 17;
* `appCategory` — category of the app for the Apple App Store.
Default value is `utilities` when building for the App Store, `Unknown` otherwise.
See [LSApplicationCategoryType](https://developer.apple.com/documentation/bundleresources/information_property_list/lsapplicationcategorytype) for a list of valid categories;
* `entitlementsFile.set(File("PATH_TO_ENTITLEMENTS"))` — a path to file containing entitlements to use when signing.
When a custom file is provided, make sure to add the entitlements that are required for Java apps.
See [sandbox.plist](https://github.com/openjdk/jdk/blob/master/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/sandbox.plist) for the default file that is used when building for the App Store. It can be different depending on your JDK version.
* `dmgPackageVersion = "DMG_VERSION"` — a dmg-specific package version * `dmgPackageVersion = "DMG_VERSION"` — a dmg-specific package version
(see the section `Specifying package version` for details); (see the section `Specifying package version` for details);
* `pkgPackageVersion = "PKG_VERSION"` — a pkg-specific package version * `pkgPackageVersion = "PKG_VERSION"` — a pkg-specific package version

49
tutorials/Signing_and_notarization_on_macOS/README.md

@ -1,7 +1,7 @@
# Signing and notarizing distributions for macOS # Signing and notarizing distributions for macOS
Apple [requires](https://developer.apple.com/documentation/xcode/notarizing_macos_software_before_distribution) Apple [requires](https://developer.apple.com/documentation/xcode/notarizing_macos_software_before_distribution)
all 3rd apps to be signed and notarized (checked by Apple) all 3rd party apps to be signed and notarized (checked by Apple)
for running on recent versions of macOS. for running on recent versions of macOS.
## What is covered ## What is covered
@ -34,7 +34,10 @@ Open https://developer.apple.com/account/resources/certificates
* Check `Save to disk` option. * Check `Save to disk` option.
2. Create and install a new certificate using your [Apple Developer account](https://developer.apple.com/account/): 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 * Open https://developer.apple.com/account/resources/certificates/add
* Choose the `Developer ID Application` certificate type. * For publishing outside the App Store, choose the `Developer ID Application` certificate type.
For publishing on the App Store, you need two certificates. First select the `Mac App Distribution`
certificate type, and once you have completed the steps in this section, repeat them again for
the `Mac Installer Distribution` certificate type.
* Upload your Certificate Signing Request from the previous step. * Upload your Certificate Signing Request from the previous step.
* Download and install the certificate (drag & drop the certificate into the `Keychain Access` application). * Download and install the certificate (drag & drop the certificate into the `Keychain Access` application).
@ -44,8 +47,13 @@ You can find all installed certificates and their keychains by running the follo
``` ```
/usr/bin/security find-certificate -c "Developer ID Application" /usr/bin/security find-certificate -c "Developer ID Application"
``` ```
or the following commands when publishing on the App Store:
```
/usr/bin/security find-certificate -c "3rd Party Mac Developer Application"
/usr/bin/security find-certificate -c "3rd Party Mac Developer Installer"
```
If you have multiple `Developer ID Application` certificates installed, If you have multiple developer certificates of the same type installed,
you will need to specify the path to the keychain, containing you will need to specify the path to the keychain, containing
the certificate intended for signing. the certificate intended for signing.
@ -69,6 +77,24 @@ Open [the page](https://developer.apple.com/account/resources/identifiers/list)
* You can use an explicit bundle ID a wildcard, matching multiple bundle IDs. * 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`). * It is recommended to use the reverse DNS notation (e.g.`com.yoursitename.yourappname`).
## Preparing a Provisioning Profile
For testing on TestFlight (when publishing to the App Store), you need to add a provisioning
profile. You can skip this step otherwise.
#### Checking existing provisioning profiles
Open https://developer.apple.com/account/resources/profiles/list
#### Creating a new provisioning profile
1. Open [the page](https://developer.apple.com/account/resources/profiles/add) on Apple's developer portal.
2. Choose `Mac App Store` option under `Distribution`.
3. Select Profile Type `Mac`.
4. Select the App ID which you created earlier.
5. Select the Mac App Distribution certificate you created earlier.
6. Enter a name.
7. Click generate and download the provisioning profile.
## Creating an app-specific password ## Creating an app-specific password
To be able to upload an app for notarization, To be able to upload an app for notarization,
@ -179,7 +205,7 @@ macOS {
* Alternatively, the `compose.desktop.mac.signing.identity` Gradle property can be used. * 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. * 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. * 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. * This step is only necessary, if multiple developer certificates of the same type are installed.
The following Gradle properties can be used instead of DSL properties: The following Gradle properties can be used instead of DSL properties:
* `compose.desktop.mac.sign` enables or disables signing. * `compose.desktop.mac.sign` enables or disables signing.
@ -219,6 +245,21 @@ macOS {
xcrun altool --list-providers -u <Apple ID> -p <Notarization password>" xcrun altool --list-providers -u <Apple ID> -p <Notarization password>"
``` ```
### Configuring provisioning profile
For testing on TestFlight (when publishing to the App Store), you need to add a provisioning
profile. You can skip this step otherwise.
Note that this option requires JDK 18 due to [this issue](https://bugs.openjdk.java.net/browse/JDK-8274346).
``` kotlin
macOS {
provisioningProfile.set(project.file("embedded.provisionprofile"))
}
```
Make sure to rename your provisioning profile you created earlier to `embedded.provisionprofile`.
## Using Gradle ## Using Gradle
The following tasks are available: The following tasks are available:

Loading…
Cancel
Save