Browse Source

Customize Info.plist before jpackage

Resolves #679
pull/703/head 0.4.0-build212
Alexey Tsvetkov 4 years ago committed by Alexey Tsvetkov
parent
commit
1040af337b
  1. 59
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/InfoPlistBuilder.kt
  2. 1
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/packageVersions.kt
  3. 80
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt
  4. 20
      gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/gradle/DesktopApplicationTest.kt
  5. 36
      gradle-plugins/compose/src/test/test-projects/application/macOptions/Expected-Info.plist

59
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/InfoPlistBuilder.kt

@ -0,0 +1,59 @@
/*
* Copyright 2020-2021 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 java.io.File
import kotlin.reflect.KProperty
internal class InfoPlistBuilder {
private val values = LinkedHashMap<InfoPlistKey, String>()
operator fun get(key: InfoPlistKey): String? = values[key]
operator fun set(key: InfoPlistKey, value: String) {
values[key] = value
}
fun writeToFile(file: File) {
file.writer().buffered().use { writer ->
writer.run {
appendLine("<?xml version=\"1.0\" ?>")
appendLine("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"https://www.apple.com/DTDs/PropertyList-1.0.dtd\">")
appendLine("<plist version=\"1.0\">")
appendLine(" <dict>")
for ((k, v) in values) {
appendLine(" <key>${k.name}</key>")
appendLine(" <string>$v</string>")
}
appendLine(" </dict>")
appendLine("</plist>")
}
}
}
}
internal data class InfoPlistKey(val name: String)
internal object PlistKeys {
private operator fun getValue(thisRef: PlistKeys, property: KProperty<*>): InfoPlistKey =
InfoPlistKey(property.name)
val LSMinimumSystemVersion by this
val CFBundleDevelopmentRegion by this
val CFBundleAllowMixedLocalizations by this
val CFBundleExecutable by this
val CFBundleIconFile by this
val CFBundleIdentifier by this
val CFBundleInfoDictionaryVersion by this
val CFBundleName by this
val CFBundlePackageType by this
val CFBundleShortVersionString by this
val CFBundleSignature by this
val LSApplicationCategoryType by this
val CFBundleVersion by this
val NSHumanReadableCopyright by this
val NSSupportsAutomaticGraphicsSwitching by this
val NSHighResolutionCapable by this
}

1
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/packageVersions.kt

@ -19,6 +19,7 @@ internal fun packageVersionFor(
project.provider {
app.nativeDistributions.packageVersionFor(targetFormat)
?: project.version.toString().takeIf { it != "unspecified" }
?: "1.0.0"
}
private fun NativeDistributions.packageVersionFor(

80
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt

@ -25,6 +25,7 @@ import org.jetbrains.compose.desktop.application.internal.files.transformJar
import org.jetbrains.compose.desktop.application.internal.validation.ValidatedMacOSSigningSettings
import org.jetbrains.compose.desktop.application.internal.validation.validate
import java.io.*
import java.nio.file.Files
import java.util.*
import java.util.zip.ZipEntry
import javax.inject.Inject
@ -183,6 +184,9 @@ abstract class AbstractJPackageTask @Inject constructor(
@get:LocalState
protected val signDir: Provider<Directory> = project.layout.buildDirectory.dir("compose/tmp/sign")
@get:LocalState
protected val resourcesDir: Provider<Directory> = project.layout.buildDirectory.dir("compose/tmp/resources")
@get:LocalState
protected val skikoDir: Provider<Directory> = project.layout.buildDirectory.dir("compose/tmp/skiko")
@ -204,6 +208,7 @@ abstract class AbstractJPackageTask @Inject constructor(
// Args, that can only be used, when creating an app image or an installer w/o --app-image parameter
cliArg("--input", libsDir)
cliArg("--runtime-image", runtimeImage)
cliArg("--resource-dir", resourcesDir)
val mappedJar = libsMapping[launcherMainJar.ioFile]?.singleOrNull()
?: error("Main jar was not processed correctly: ${launcherMainJar.ioFile}")
@ -352,6 +357,14 @@ abstract class AbstractJPackageTask @Inject constructor(
listOf(copyFileToLibsDir(sourceFile))
}
}
fileOperations.delete(resourcesDir)
fileOperations.mkdir(resourcesDir)
if (currentOS == OS.MacOS) {
InfoPlistBuilder()
.also { setInfoPlistValues(it) }
.writeToFile(resourcesDir.ioFile.resolve("Info.plist"))
}
}
override fun jvmToolEnvironment(): MutableMap<String, String> =
@ -367,51 +380,10 @@ abstract class AbstractJPackageTask @Inject constructor(
override fun checkResult(result: ExecResult) {
super.checkResult(result)
patchInfoPlistIfNeeded()
val outputFile = findOutputFileOrDir(destinationDir.ioFile, targetFormat)
logger.lifecycle("The distribution is written to ${outputFile.canonicalPath}")
}
/**
* https://github.com/JetBrains/compose-jb/issues/545
*
* Patching Info.plist is necessary to avoid duplicating and supporting
* properties set by jpackage.
*
* Info.plist is patched only on macOS for app image.
* Packaged installers receive patched Info.plist through
* prebuilt [appImage].
*/
private fun patchInfoPlistIfNeeded() {
if (currentOS != OS.MacOS || targetFormat != TargetFormat.AppImage) return
val appDir = destinationDir.ioFile.resolve("${packageName.get()}.app/")
val infoPlist = appDir.resolve("Contents/Info.plist")
if (!infoPlist.exists()) return
val content = infoPlist.readText()
val nsSupportsAutomaticGraphicsSwitching = "<key>NSSupportsAutomaticGraphicsSwitching</key>"
val stringToAppend = "$nsSupportsAutomaticGraphicsSwitching<true/>"
if (content.indexOf(nsSupportsAutomaticGraphicsSwitching) >= 0) return
/**
* Dirty hack: to avoid parsing plist file, let's find known expected key substring,
* and insert the necessary keys before it.
*/
val knownExpectedKey = "<key>NSHighResolutionCapable</key>"
val i = content.indexOf(knownExpectedKey)
if (i >= 0) {
val newContent = buildString {
append(content.substring(0, i))
appendLine(stringToAppend)
append(" ")
appendLine(content.substring(i, content.length))
}
infoPlist.writeText(newContent)
}
macSigner?.sign(appDir)
}
override fun initState() {
val mappingFile = libsMappingFile.ioFile
if (mappingFile.exists()) {
@ -430,6 +402,32 @@ abstract class AbstractJPackageTask @Inject constructor(
libsMapping.saveTo(mappingFile)
logger.debug("Saved libs mapping to $mappingFile")
}
private fun setInfoPlistValues(plist: InfoPlistBuilder) {
check(currentOS == OS.MacOS) { "Current OS is not macOS: $currentOS" }
plist[PlistKeys.LSMinimumSystemVersion] = "10.13"
plist[PlistKeys.CFBundleDevelopmentRegion] = "English"
plist[PlistKeys.CFBundleAllowMixedLocalizations] = "true"
val packageName = packageName.get()
plist[PlistKeys.CFBundleExecutable] = packageName
plist[PlistKeys.CFBundleIconFile] = "$packageName.icns"
val bundleId = nonValidatedMacBundleID.orNull
?: launcherMainClass.get().substringBeforeLast(".")
plist[PlistKeys.CFBundleIdentifier] = bundleId
plist[PlistKeys.CFBundleInfoDictionaryVersion] = "6.0"
plist[PlistKeys.CFBundleName] = packageName
plist[PlistKeys.CFBundlePackageType] = "APPL"
val packageVersion = packageVersion.get()!!
plist[PlistKeys.CFBundleShortVersionString] = packageVersion
plist[PlistKeys.LSApplicationCategoryType] = "Unknown"
plist[PlistKeys.CFBundleVersion] = packageVersion
val year = Calendar.getInstance().get(Calendar.YEAR)
plist[PlistKeys.NSHumanReadableCopyright] = packageCopyright.orNull
?: "Copyright (C) $year"
plist[PlistKeys.NSSupportsAutomaticGraphicsSwitching] = "true"
plist[PlistKeys.NSHighResolutionCapable] = "true"
}
}
// Serializable is only needed to avoid breaking configuration cache:

20
gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/gradle/DesktopApplicationTest.kt

@ -13,7 +13,9 @@ import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assumptions
import org.junit.jupiter.api.Test
import java.io.File
import java.util.*
import java.util.jar.JarFile
import kotlin.collections.HashSet
class DesktopApplicationTest : GradlePluginTestBase() {
@Test
@ -143,6 +145,12 @@ class DesktopApplicationTest : GradlePluginTestBase() {
@Test
fun testMacOptions() {
fun String.normalized(): String =
trim().replace(
"Copyright (C) ${Calendar.getInstance().get(Calendar.YEAR)}",
"Copyright (C) CURRENT_YEAR"
)
Assumptions.assumeTrue(currentOS == OS.MacOS)
with(testProject(TestProjects.macOptions)) {
@ -150,8 +158,11 @@ class DesktopApplicationTest : GradlePluginTestBase() {
check.taskOutcome(":runDistributable", TaskOutcome.SUCCESS)
check.logContains("Hello, from Mac OS!")
val appDir = testWorkDir.resolve("build/compose/binaries/main/app/TestPackage.app/Contents/")
val infoPlist = appDir.resolve("Info.plist").checkExists().checkExists()
infoPlist.readText().checkContains("<key>NSSupportsAutomaticGraphicsSwitching</key><true/>")
val actualInfoPlist = appDir.resolve("Info.plist").checkExists()
val expectedInfoPlist = testWorkDir.resolve("Expected-Info.Plist")
val actualInfoPlistNormalized = actualInfoPlist.readText().normalized()
val expectedInfoPlistNormalized = expectedInfoPlist.readText().normalized()
Assert.assertEquals(actualInfoPlistNormalized, expectedInfoPlistNormalized)
}
}
}
@ -201,6 +212,11 @@ class DesktopApplicationTest : GradlePluginTestBase() {
""".trimMargin().trim()
Assert.assertEquals(expectedOutput, actualOutput)
}
gradle(":runDistributable").build().checks { check ->
check.taskOutcome(":runDistributable", TaskOutcome.SUCCESS)
check.logContains("Signed app successfully started!")
}
}
}
}

36
gradle-plugins/compose/src/test/test-projects/application/macOptions/Expected-Info.plist

@ -0,0 +1,36 @@
<?xml version="1.0" ?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSMinimumSystemVersion</key>
<string>10.13</string>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleAllowMixedLocalizations</key>
<string>true</string>
<key>CFBundleExecutable</key>
<string>TestPackage</string>
<key>CFBundleIconFile</key>
<string>TestPackage.icns</string>
<key>CFBundleIdentifier</key>
<string>MainKt</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>TestPackage</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>LSApplicationCategoryType</key>
<string>Unknown</string>
<key>CFBundleVersion</key>
<string>1.0.0</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright (C) CURRENT_YEAR</string>
<key>NSSupportsAutomaticGraphicsSwitching</key>
<string>true</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
</dict>
</plist>
Loading…
Cancel
Save