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 8732edffec..4b9476d1fa 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 @@ -23,6 +23,7 @@ open class MacOSPlatformSettings @Inject constructor(objects: ObjectFactory): Pl var appStore: Boolean = false var appCategory: String? = null var entitlementsFile: RegularFileProperty = objects.fileProperty() + var runtimeEntitlementsFile: RegularFileProperty = objects.fileProperty() var packageBuildVersion: String? = null var dmgPackageVersion: String? = null var dmgPackageBuildVersion: String? = null @@ -49,6 +50,7 @@ open class MacOSPlatformSettings @Inject constructor(objects: ObjectFactory): Pl } val provisioningProfile: RegularFileProperty = objects.fileProperty() + val runtimeProvisioningProfile: RegularFileProperty = objects.fileProperty() internal val infoPlistSettings = InfoPlistSettings() fun infoPlist(fn: Action) { diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigner.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigner.kt index 5fd61949a1..61112ec94b 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigner.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigner.kt @@ -7,6 +7,7 @@ package org.jetbrains.compose.desktop.application.internal import org.jetbrains.compose.desktop.application.internal.validation.ValidatedMacOSSigningSettings import java.io.File +import java.nio.file.Files import java.util.regex.Pattern internal class MacSigner( @@ -31,7 +32,15 @@ internal class MacSigner( ) } - fun sign(file: File) { + /** + * If [entitlements] file is provided, executables are signed with entitlements. + * Set [forceEntitlements] to `true` to sign all types of files with the provided [entitlements]. + */ + fun sign( + file: File, + entitlements: File? = null, + forceEntitlements: Boolean = false + ) { val args = arrayListOf( "-vvvv", "--timestamp", @@ -46,6 +55,13 @@ internal class MacSigner( args.add(it.absolutePath) } + if (forceEntitlements || Files.isExecutable(file.toPath())) { + entitlements?.let { + args.add("--entitlements") + args.add(it.absolutePath) + } + } + args.add(file.absolutePath) runExternalTool(MacUtils.codesign, args) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigningHelper.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigningHelper.kt new file mode 100644 index 0000000000..0ece83a9a7 --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigningHelper.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2020-2022 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.internal + +import org.jetbrains.compose.desktop.application.internal.files.isDylibPath +import java.io.File +import kotlin.io.path.isExecutable +import kotlin.io.path.isRegularFile +import kotlin.io.path.isSymbolicLink + +internal class MacSigningHelper( + private val macSigner: MacSigner, + private val runtimeProvisioningProfile: File?, + private val entitlementsFile: File?, + private val runtimeEntitlementsFile: File?, + destinationDir: File, + packageName: String +) { + private val appDir = destinationDir.resolve("$packageName.app") + private val runtimeDir = appDir.resolve("Contents/runtime") + + fun modifyRuntimeIfNeeded() { + // Only resign modify the runtime if a provisioning profile or alternative entitlements file is provided. + // If no entitlements file is provided, the runtime cannot be resigned. + if (runtimeProvisioningProfile == null && + // When resigning the runtime, an app entitlements file is also needed. + (runtimeEntitlementsFile == null || entitlementsFile == null) + ) { + return + } + + // Add the provisioning profile + runtimeProvisioningProfile?.let { + addRuntimeProvisioningProfile(runtimeDir, it) + } + + // Resign the runtime completely (and also the app dir only) + resignRuntimeAndAppDir(appDir, runtimeDir) + } + + private fun addRuntimeProvisioningProfile( + runtimeDir: File, + runtimeProvisioningProfile: File + ) { + runtimeProvisioningProfile.copyTo( + target = runtimeDir.resolve("Contents/embedded.provisionprofile"), + overwrite = true + ) + } + + private fun resignRuntimeAndAppDir( + appDir: File, + runtimeDir: File + ) { + // Sign all libs and executables in runtime + runtimeDir.walk().forEach { file -> + val path = file.toPath() + if (path.isRegularFile() && (path.isExecutable() || path.toString().isDylibPath)) { + if (path.isSymbolicLink()) { + // Ignore symbolic links + } else { + // Resign file + macSigner.unsign(file) + macSigner.sign(file, runtimeEntitlementsFile) + } + } + } + + // Resign runtime directory + macSigner.unsign(runtimeDir) + macSigner.sign(runtimeDir, runtimeEntitlementsFile, forceEntitlements = true) + + // Resign app directory (contents other than runtime were already signed by jpackage) + macSigner.unsign(appDir) + macSigner.sign(appDir, entitlementsFile, forceEntitlements = true) + } +} 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 8300e7334e..1b30ec9fd1 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 @@ -311,9 +311,11 @@ internal fun AbstractJPackageTask.configurePlatformSettings(app: Application) { macAppStore.set(mac.appStore) macAppCategory.set(mac.appCategory) macEntitlementsFile.set(mac.entitlementsFile) + macRuntimeEntitlementsFile.set(mac.runtimeEntitlementsFile) packageBuildVersion.set(packageBuildVersionFor(project, app, targetFormat)) nonValidatedMacBundleID.set(provider { mac.bundleID }) macProvisioningProfile.set(mac.provisioningProfile) + macRuntimeProvisioningProfile.set(mac.runtimeProvisioningProfile) 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/files/MacJarSignFileCopyingProcessor.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/files/MacJarSignFileCopyingProcessor.kt index 58389a560c..3804feff20 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/files/MacJarSignFileCopyingProcessor.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/files/MacJarSignFileCopyingProcessor.kt @@ -86,5 +86,5 @@ internal class MacJarSignFileCopyingProcessor( } } -private val String.isDylibPath +internal val String.isDylibPath get() = endsWith(".dylib") || endsWith(".jnilib") 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 6f30ee2927..d4a71f29c2 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 @@ -140,6 +140,11 @@ abstract class AbstractJPackageTask @Inject constructor( @get:PathSensitive(PathSensitivity.ABSOLUTE) val macEntitlementsFile: RegularFileProperty = objects.fileProperty() + @get:InputFile + @get:Optional + @get:PathSensitive(PathSensitivity.ABSOLUTE) + val macRuntimeEntitlementsFile: RegularFileProperty = objects.fileProperty() + @get:Input @get:Optional val packageBuildVersion: Property = objects.nullableProperty() @@ -149,6 +154,11 @@ abstract class AbstractJPackageTask @Inject constructor( @get:PathSensitive(PathSensitivity.ABSOLUTE) val macProvisioningProfile: RegularFileProperty = objects.fileProperty() + @get:InputFile + @get:Optional + @get:PathSensitive(PathSensitivity.ABSOLUTE) + val macRuntimeProvisioningProfile: RegularFileProperty = objects.fileProperty() + @get:Input @get:Optional val winConsole: Property = objects.nullableProperty() @@ -490,10 +500,26 @@ abstract class AbstractJPackageTask @Inject constructor( override fun checkResult(result: ExecResult) { super.checkResult(result) + modifyRuntimeOnMacOsIfNeeded() val outputFile = findOutputFileOrDir(destinationDir.ioFile, targetFormat) logger.lifecycle("The distribution is written to ${outputFile.canonicalPath}") } + private fun modifyRuntimeOnMacOsIfNeeded() { + if (currentOS != OS.MacOS || targetFormat != TargetFormat.AppImage) return + macSigner?.let { macSigner -> + val macSigningHelper = MacSigningHelper( + macSigner = macSigner, + runtimeProvisioningProfile = macRuntimeProvisioningProfile.ioFileOrNull, + entitlementsFile = macEntitlementsFile.ioFileOrNull, + runtimeEntitlementsFile = macRuntimeEntitlementsFile.ioFileOrNull, + destinationDir = destinationDir.ioFile, + packageName = packageName.get() + ) + macSigningHelper.modifyRuntimeIfNeeded() + } + } + override fun initState() { val mappingFile = libsMappingFile.ioFile if (mappingFile.exists()) { diff --git a/tutorials/Native_distributions_and_local_execution/README.md b/tutorials/Native_distributions_and_local_execution/README.md index 317de052ac..1a810e2eda 100755 --- a/tutorials/Native_distributions_and_local_execution/README.md +++ b/tutorials/Native_distributions_and_local_execution/README.md @@ -436,7 +436,7 @@ 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`, `notarization`, and `provisioningProfile` — see + * `signing`, `notarization`, `provisioningProfile`, and `runtimeProvisioningProfile` — 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; @@ -446,6 +446,13 @@ The following platform-specific options are available * `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. + If no file is provided the default entitlements provided by jpackage are used. + See [the corresponding tutorial](/tutorials/Signing_and_notarization_on_macOS/README.md#configuring-entitlements) + * `runtimeEntitlementsFile.set(File("PATH_TO_RUNTIME_ENTITLEMENTS"))` — a path to file containing entitlements to use when signing the JVM runtime. + 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. + If no file is provided then `entitlementsFile` is used. If that was also not provided, the default entitlements provided by jpackage are used. + See [the corresponding tutorial](/tutorials/Signing_and_notarization_on_macOS/README.md#configuring-entitlements) * `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 830054e688..56cae86f11 100644 --- a/tutorials/Signing_and_notarization_on_macOS/README.md +++ b/tutorials/Signing_and_notarization_on_macOS/README.md @@ -67,7 +67,6 @@ Open [the page](https://developer.apple.com/account/resources/identifiers/list) #### 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. @@ -81,6 +80,11 @@ Open [the page](https://developer.apple.com/account/resources/identifiers/list) For testing on TestFlight (when publishing to the App Store), you need to add a provisioning profile. You can skip this step otherwise. +First make sure you have created two app IDs, one for your app, and another one for the JVM runtime. +They should look like this: +App ID for app: `com.yoursitename.yourappname` (format: `YOURBUNDLEID`) +App ID for runtime: `com.oracle.java.com.yoursitename.yourappname` (format: `com.oracle.java.YOURBUNDLEID`) + #### Checking existing provisioning profiles Open https://developer.apple.com/account/resources/profiles/list @@ -94,6 +98,8 @@ Open https://developer.apple.com/account/resources/profiles/list 5. Select the Mac App Distribution certificate you created earlier. 6. Enter a name. 7. Click generate and download the provisioning profile. + +Note that you need to create two of these profiles, one for your app and another one for the JVM runtime. ## Creating an app-specific password @@ -255,10 +261,110 @@ Note that this option requires JDK 18 due to [this issue](https://bugs.openjdk.j ``` kotlin macOS { provisioningProfile.set(project.file("embedded.provisionprofile")) + runtimeProvisioningProfile.set(project.file("runtime.provisionprofile")) +} +``` + +Make sure to rename your provisioning profile you created earlier to `embedded.provisionprofile` +and the provisioning profile for the JVM runtime to `runtime.provisionprofile`. + +### Configuring entitlements + +For TestFlight you need to set some special entitlements. + +Create a file `entitlements.plist` with the following content: + +```xml + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.cs.debugger + + com.apple.security.device.audio-input + + com.apple.application-identifier + TEAMID.APPID + com.apple.developer.team-identifier + TEAMID + + + +``` +These are the entitlements for your application. Set `TEAMID` to your team ID and `APPID` to your app bundle ID. + +Then create another file called `runtime-entitlements.plist` with the following content: +```xml + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.cs.debugger + + com.apple.security.device.audio-input + + + +``` + +These are the entitlements for the JVM runtime. + +Now configure the entitlements in Gradle like this: + +``` kotlin +macOS { + entitlementsFile.set(project.file("entitlements.plist")) + runtimeEntitlementsFile.set(project.file("runtime-entitlements.plist")) } ``` -Make sure to rename your provisioning profile you created earlier to `embedded.provisionprofile`. +### TestFlight + +Some special configuration is needed to get the app working in TestFlight. If something is +incorrect, the App Store will either send an email or show that your build is "Not Available for Testing". +The build could still work for the App Store but won't work in TestFlight. + +If that is the case, make sure the following is configured correctly: + +1. Provisioning profiles for both app and JVM runtime are provided. +2. Entitlement files for both app and JVM runtime are provided. +3. Both entitlement files contain at least the values provided [here](#configuring-entitlements). +4. Team ID and App ID are the same in the app entitlements file and the app provisioning profile. + +Furthermore, make sure you follow the steps to get the app working on the App Store. +That means signing with the correct certificates, setting `appStore` to `true` in Gradle, etc. + +Note that apps for both the App Store and TestFlight are sandboxed. + +If you are loading native libraries from JVM code, they must be loaded directly from the app bundle (because of sandbox and signing). +That means they cannot first be extracted from a JAR and then loaded (what some libraries do). +You can include native libraries in the bundle using `fromFiles` (see [here](/tutorials/Native_distributions_and_local_execution#customizing-content)) +and then you can load them in JVM code using `System.loadLibrary("LIBRARYNAME")`. +Note that the Skiko native library used by Compose is already loaded correctly if you are using the +default application configuration. + +In case you are still experiencing issues with TestFlight, you could consider opening a TSI with +Apple, and they may be able to give you a more detailed error message. ## Using Gradle