Skip to content

Commit

Permalink
Customize Info.plist before jpackage
Browse files Browse the repository at this point in the history
Resolves #679
  • Loading branch information
AlexeyTsvetkov committed May 27, 2021
1 parent 63beaea commit 1040af3
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 43 deletions.
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")

Expand All @@ -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}")
Expand Down Expand Up @@ -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> =
Expand All @@ -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()) {
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -143,15 +145,24 @@ 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)) {
gradle(":runDistributable").build().checks { check ->
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)
}
}
}
Expand Down Expand Up @@ -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!")
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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>

0 comments on commit 1040af3

Please sign in to comment.