Browse Source

Fix TestFlight builds (#1812)

pull/1820/head
Thomas Vos 3 years ago committed by GitHub
parent
commit
aa08279104
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  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 appStore: Boolean = false
var appCategory: String? = null var appCategory: String? = null
var entitlementsFile: RegularFileProperty = objects.fileProperty() var entitlementsFile: RegularFileProperty = objects.fileProperty()
var runtimeEntitlementsFile: 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
@ -49,6 +50,7 @@ open class MacOSPlatformSettings @Inject constructor(objects: ObjectFactory): Pl
} }
val provisioningProfile: RegularFileProperty = objects.fileProperty() val provisioningProfile: RegularFileProperty = objects.fileProperty()
val runtimeProvisioningProfile: RegularFileProperty = objects.fileProperty()
internal val infoPlistSettings = InfoPlistSettings() internal val infoPlistSettings = InfoPlistSettings()
fun infoPlist(fn: Action<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 org.jetbrains.compose.desktop.application.internal.validation.ValidatedMacOSSigningSettings
import java.io.File import java.io.File
import java.nio.file.Files
import java.util.regex.Pattern import java.util.regex.Pattern
internal class MacSigner( 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( val args = arrayListOf(
"-vvvv", "-vvvv",
"--timestamp", "--timestamp",
@ -46,6 +55,13 @@ internal class MacSigner(
args.add(it.absolutePath) args.add(it.absolutePath)
} }
if (forceEntitlements || Files.isExecutable(file.toPath())) {
entitlements?.let {
args.add("--entitlements")
args.add(it.absolutePath)
}
}
args.add(file.absolutePath) args.add(file.absolutePath)
runExternalTool(MacUtils.codesign, args) 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) macAppStore.set(mac.appStore)
macAppCategory.set(mac.appCategory) macAppCategory.set(mac.appCategory)
macEntitlementsFile.set(mac.entitlementsFile) macEntitlementsFile.set(mac.entitlementsFile)
macRuntimeEntitlementsFile.set(mac.runtimeEntitlementsFile)
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) macProvisioningProfile.set(mac.provisioningProfile)
macRuntimeProvisioningProfile.set(mac.runtimeProvisioningProfile)
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)))

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") 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) @get:PathSensitive(PathSensitivity.ABSOLUTE)
val macEntitlementsFile: RegularFileProperty = objects.fileProperty() val macEntitlementsFile: RegularFileProperty = objects.fileProperty()
@get:InputFile
@get:Optional
@get:PathSensitive(PathSensitivity.ABSOLUTE)
val macRuntimeEntitlementsFile: RegularFileProperty = objects.fileProperty()
@get:Input @get:Input
@get:Optional @get:Optional
val packageBuildVersion: Property<String?> = objects.nullableProperty() val packageBuildVersion: Property<String?> = objects.nullableProperty()
@ -149,6 +154,11 @@ abstract class AbstractJPackageTask @Inject constructor(
@get:PathSensitive(PathSensitivity.ABSOLUTE) @get:PathSensitive(PathSensitivity.ABSOLUTE)
val macProvisioningProfile: RegularFileProperty = objects.fileProperty() val macProvisioningProfile: RegularFileProperty = objects.fileProperty()
@get:InputFile
@get:Optional
@get:PathSensitive(PathSensitivity.ABSOLUTE)
val macRuntimeProvisioningProfile: RegularFileProperty = objects.fileProperty()
@get:Input @get:Input
@get:Optional @get:Optional
val winConsole: Property<Boolean?> = objects.nullableProperty() val winConsole: Property<Boolean?> = objects.nullableProperty()
@ -490,10 +500,26 @@ abstract class AbstractJPackageTask @Inject constructor(
override fun checkResult(result: ExecResult) { override fun checkResult(result: ExecResult) {
super.checkResult(result) super.checkResult(result)
modifyRuntimeOnMacOsIfNeeded()
val outputFile = findOutputFileOrDir(destinationDir.ioFile, targetFormat) val outputFile = findOutputFileOrDir(destinationDir.ioFile, targetFormat)
logger.lifecycle("The distribution is written to ${outputFile.canonicalPath}") 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() { override fun initState() {
val mappingFile = libsMappingFile.ioFile val mappingFile = libsMappingFile.ioFile
if (mappingFile.exists()) { 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; * `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`, `notarization`, and `provisioningProfile` — see * `signing`, `notarization`, `provisioningProfile`, and `runtimeProvisioningProfile` — 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; * `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. * `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. 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. 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 * `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

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 #### Creating a new App ID
1. Open [the page](https://developer.apple.com/account/resources/identifiers/add/bundleId) on Apple's developer portal. 1. Open [the page](https://developer.apple.com/account/resources/identifiers/add/bundleId) on Apple's developer portal.
2. Choose `App ID` option. 2. Choose `App ID` option.
3. Choose `App` type. 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 For testing on TestFlight (when publishing to the App Store), you need to add a provisioning
profile. You can skip this step otherwise. 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 #### Checking existing provisioning profiles
Open https://developer.apple.com/account/resources/profiles/list 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. 5. Select the Mac App Distribution certificate you created earlier.
6. Enter a name. 6. Enter a name.
7. Click generate and download the provisioning profile. 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 ## 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 ``` kotlin
macOS { macOS {
provisioningProfile.set(project.file("embedded.provisionprofile")) 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 ## Using Gradle

Loading…
Cancel
Save