diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/PlatformSettings.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/PlatformSettings.kt index 88117cb303..8732edffec 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/PlatformSettings.kt +++ b/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) { fn.execute(infoPlistSettings) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt index 5587849149..78015f52c5 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt +++ b/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))) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/validation/ValidatedMacOSSigningSettings.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/validation/ValidatedMacOSSigningSettings.kt index ea6ac52660..273baecc4a 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/validation/ValidatedMacOSSigningSettings.kt +++ b/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, - project: Project + project: Project, + appStoreProvider: Provider ): 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 ) } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt index 9a34ac6279..1a988cff82 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt +++ b/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 = objects.nullableProperty() + @get:Input + @get:Optional + val macAppStore: Property = objects.nullableProperty() + + @get:Input + @get:Optional + val macAppCategory: Property = objects.nullableProperty() + + @get:InputFile + @get:Optional + @get:PathSensitive(PathSensitivity.ABSOLUTE) + val macEntitlementsFile: RegularFileProperty = objects.fileProperty() + @get:Input @get:Optional val packageBuildVersion: Property = objects.nullableProperty() + @get:InputFile + @get:Optional + @get:PathSensitive(PathSensitivity.ABSOLUTE) + val macProvisioningProfile: RegularFileProperty = objects.fileProperty() + @get:Input @get:Optional val winConsole: Property = 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 = """ + os + + 10.13 + + """.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) diff --git a/tutorials/Native_distributions_and_local_execution/README.md b/tutorials/Native_distributions_and_local_execution/README.md index 79df138556..317de052ac 100755 --- a/tutorials/Native_distributions_and_local_execution/README.md +++ b/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 " 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 diff --git a/tutorials/Signing_and_notarization_on_macOS/README.md b/tutorials/Signing_and_notarization_on_macOS/README.md index 767dcbdf3a..830054e688 100644 --- a/tutorials/Signing_and_notarization_on_macOS/README.md +++ b/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 -p " ``` +### 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: