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 2 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. 49
      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 dockName: String? = null
var setDockNameSameAsPackageName: Boolean = true
var appStore: Boolean = false
var appCategory: String? = null
var entitlementsFile: RegularFileProperty = objects.fileProperty()
var packageBuildVersion: String? = null
var dmgPackageVersion: String? = null
var dmgPackageBuildVersion: String? = null
@ -45,6 +48,8 @@ open class MacOSPlatformSettings @Inject constructor(objects: ObjectFactory): Pl
fn.execute(notarization)
}
val provisioningProfile: RegularFileProperty = objects.fileProperty()
internal val infoPlistSettings = InfoPlistSettings()
fun infoPlist(fn: Action<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
provider { mac.dockName }
)
macAppStore.set(mac.appStore)
macAppCategory.set(mac.appCategory)
macEntitlementsFile.set(mac.entitlementsFile)
packageBuildVersion.set(packageBuildVersionFor(project, app, targetFormat))
nonValidatedMacBundleID.set(provider { mac.bundleID })
macProvisioningProfile.set(mac.provisioningProfile)
macExtraPlistKeysRawXml.set(provider { mac.infoPlistSettings.extraKeysRawXml })
nonValidatedMacSigningSettings = app.nativeDistributions.macOS.signing
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 keychain: File?,
val prefix: String,
private val appStore: Boolean
) {
val fullDeveloperID: String
get() {
@ -26,14 +27,15 @@ internal data class ValidatedMacOSSigningSettings(
return when {
identity.startsWith(developerIdPrefix) -> identity
identity.startsWith(thirdPartyMacDeveloperPrefix) -> identity
else -> developerIdPrefix + identity
else -> (if (!appStore) developerIdPrefix else thirdPartyMacDeveloperPrefix) + identity
}
}
}
internal fun MacOSSigningSettings.validate(
bundleIDProvider: Provider<String?>,
project: Project
project: Project,
appStoreProvider: Provider<Boolean?>
): ValidatedMacOSSigningSettings {
check(currentOS == OS.MacOS) { ERR_WRONG_OS }
@ -52,12 +54,14 @@ internal fun MacOSSigningSettings.validate(
}
keychainFile
} else null
val appStore = appStoreProvider.orNull == true
return ValidatedMacOSSigningSettings(
bundleID = bundleID,
identity = signIdentity,
keychain = keychainFile,
prefix = signPrefix
prefix = signPrefix,
appStore = appStore
)
}

49
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
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:Optional
val packageBuildVersion: Property<String?> = objects.nullableProperty()
@get:InputFile
@get:Optional
@get:PathSensitive(PathSensitivity.ABSOLUTE)
val macProvisioningProfile: RegularFileProperty = objects.fileProperty()
@get:Input
@get:Optional
val winConsole: Property<Boolean?> = objects.nullableProperty()
@ -183,7 +201,7 @@ abstract class AbstractJPackageTask @Inject constructor(
val nonValidatedSettings = nonValidatedMacSigningSettings
if (currentOS == OS.MacOS && nonValidatedSettings?.sign?.get() == true) {
val validatedSettings =
nonValidatedSettings.validate(nonValidatedMacBundleID, project)
nonValidatedSettings.validate(nonValidatedMacBundleID, project, macAppStore)
MacSigner(validatedSettings, runExternalTool)
} else null
}
@ -275,14 +293,23 @@ abstract class AbstractJPackageTask @Inject constructor(
macDockName.orNull?.let { dockName ->
javaOption("-Xdock:name=$dockName")
}
macProvisioningProfile.orNull?.let { provisioningProfile ->
cliArg("--app-content", provisioningProfile)
}
}
}
if (targetFormat != TargetFormat.AppImage) {
// Args, that can only be used, when creating an installer
cliArg("--app-image", appImage)
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("--install-dir", installationPath)
cliArg("--license-file", licenseFile)
cliArg("--resource-dir", jpackageResources)
when (currentOS) {
OS.Linux -> {
@ -320,6 +347,9 @@ abstract class AbstractJPackageTask @Inject constructor(
OS.MacOS -> {
cliArg("--mac-package-name", macPackageName)
cliArg("--mac-package-identifier", nonValidatedMacBundleID)
cliArg("--mac-app-store", macAppStore)
cliArg("--mac-app-category", macAppCategory)
cliArg("--mac-entitlements", macEntitlementsFile)
macSigner?.let { signer ->
cliArg("--mac-sign", true)
@ -424,6 +454,17 @@ abstract class AbstractJPackageTask @Inject constructor(
InfoPlistBuilder(macExtraPlistKeysRawXml.orNull)
.also { setInfoPlistValues(it) }
.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"
val packageVersion = packageVersion.get()!!
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
plist[PlistKeys.CFBundleVersion] = packageBuildVersion
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--));
* `description` — application's description (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).
``` kotlin
@ -436,9 +436,16 @@ The following platform-specific options are available
* `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.
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)
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
(see the section `Specifying package version` for details);
* `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
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.
## What is covered
@ -34,7 +34,10 @@ Open https://developer.apple.com/account/resources/certificates
* 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.
* 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.
* 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"
```
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
the certificate intended for signing.
@ -68,6 +76,24 @@ Open [the page](https://developer.apple.com/account/resources/identifiers/list)
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`).
## 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
@ -179,7 +205,7 @@ macOS {
* 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.
* 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:
* `compose.desktop.mac.sign` enables or disables signing.
@ -219,6 +245,21 @@ macOS {
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
The following tasks are available:

Loading…
Cancel
Save