Browse Source

Fix TestFlight builds (#1812)

pull/1844/head
Thomas Vos 3 years ago committed by Alexey Tsvetkov
parent
commit
7b281b82f5
  1. 2
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/PlatformSettings.kt
  2. 18
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigner.kt
  3. 80
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/MacSigningHelper.kt
  4. 2
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt
  5. 2
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/files/MacJarSignFileCopyingProcessor.kt
  6. 26
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt
  7. 9
      tutorials/Native_distributions_and_local_execution/README.md
  8. 110
      tutorials/Signing_and_notarization_on_macOS/README.md

2
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<InfoPlistSettings>) {

18
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)

80
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)
}
}

2
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)))

2
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")

26
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<String?> = 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<Boolean?> = 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()) {

9
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 <App>" 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

110
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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.cs.debugger</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.application-identifier</key>
<string>TEAMID.APPID</string>
<key>com.apple.developer.team-identifier</key>
<string>TEAMID</string>
<!-- Add additional entitlements here, for example for network or hardware access. -->
</dict>
</plist>
```
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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.cs.debugger</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
</dict>
</plist>
```
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

Loading…
Cancel
Save