Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File associations #4957

Merged
merged 10 commits into from
Jul 2, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.jetbrains.compose.desktop.application.dsl

import java.io.Serializable

data class FileAssociation(
zhelenskiy marked this conversation as resolved.
Show resolved Hide resolved
val mimeType: String,
val extension: String,
val description: String,
) : Serializable
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,9 @@ abstract class JvmApplicationDistributions : AbstractDistributions() {
fun windows(fn: Action<WindowsPlatformSettings>) {
fn.execute(windows)
}

internal val fileAssociations: MutableSet<FileAssociation> = mutableSetOf()
fun fileAssociation(mimeType: String, extension: String, description: String) {
zhelenskiy marked this conversation as resolved.
Show resolved Hide resolved
zhelenskiy marked this conversation as resolved.
Show resolved Hide resolved
fileAssociations.add(FileAssociation(mimeType, extension, description))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,63 @@

package org.jetbrains.compose.desktop.application.internal

import org.jetbrains.compose.desktop.application.internal.InfoPlistBuilder.InfoPlistValue.*
import java.io.File
import kotlin.reflect.KProperty

private const val indent = " "
private fun indentForLevel(level: Int) = indent.repeat(level)

internal class InfoPlistBuilder(private val extraPlistKeysRawXml: String? = null) {
private val values = LinkedHashMap<InfoPlistKey, String>()
internal sealed class InfoPlistValue {
abstract fun toString(nestingLevel: Int): String
override fun toString(): String = toString(0)
zhelenskiy marked this conversation as resolved.
Show resolved Hide resolved
data class InfoPlistListValue(val elements: List<InfoPlistValue>) : InfoPlistValue() {
override fun toString(nestingLevel: Int): String =
if (elements.isEmpty()) "${indentForLevel(nestingLevel)}<array/>"
else elements.joinToString(
separator = "\n",
prefix = "${indentForLevel(nestingLevel)}<array>\n",
postfix = "\n${indentForLevel(nestingLevel)}</array>"
) {
it.toString(nestingLevel + 1)
}

override fun toString(): String = super.toString()
zhelenskiy marked this conversation as resolved.
Show resolved Hide resolved

constructor(vararg elements: InfoPlistValue) : this(elements.asList())
}

data class InfoPlistMapValue(val elements: Map<InfoPlistKey, InfoPlistValue>) : InfoPlistValue() {
override fun toString(nestingLevel: Int): String =
if (elements.isEmpty()) "${indentForLevel(nestingLevel)}<dict/>"
else elements.entries.joinToString(
separator = "\n",
prefix = "${indentForLevel(nestingLevel)}<dict>\n",
postfix = "\n${indentForLevel(nestingLevel)}</dict>",
) { (key, value) ->
"${indentForLevel(nestingLevel + 1)}<key>${key.name}</key>\n${value.toString(nestingLevel + 1)}"
}
override fun toString() = super.toString()
zhelenskiy marked this conversation as resolved.
Show resolved Hide resolved

constructor(vararg elements: Pair<InfoPlistKey, InfoPlistValue>) : this(elements.toMap())
}

data class InfoPlistStringValue(val value: String) : InfoPlistValue() {
override fun toString(nestingLevel: Int): String = if (value.isEmpty()) "${indentForLevel(nestingLevel)}<string/>" else "${indentForLevel(nestingLevel)}<string>$value</string>"
zhelenskiy marked this conversation as resolved.
Show resolved Hide resolved
override fun toString() = super.toString()
zhelenskiy marked this conversation as resolved.
Show resolved Hide resolved
}
}

private val values = LinkedHashMap<InfoPlistKey, InfoPlistValue>()

operator fun get(key: InfoPlistKey): InfoPlistValue? = values[key]
operator fun set(key: InfoPlistKey, value: String?) = set(key, value?.let(::InfoPlistStringValue))
operator fun set(key: InfoPlistKey, value: List<InfoPlistValue>?) = set(key, value?.let(::InfoPlistListValue))
operator fun set(key: InfoPlistKey, value: Map<InfoPlistKey, InfoPlistValue>?) =
set(key, value?.let(::InfoPlistMapValue))

operator fun get(key: InfoPlistKey): String? = values[key]
operator fun set(key: InfoPlistKey, value: String?) {
operator fun set(key: InfoPlistKey, value: InfoPlistValue?) {
if (value != null) {
values[key] = value
} else {
Expand All @@ -26,13 +75,13 @@ internal class InfoPlistBuilder(private val extraPlistKeysRawXml: String? = null
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>")
appendLine("${indentForLevel(1)}<dict>")
for ((k, v) in values) {
appendLine(" <key>${k.name}</key>")
appendLine(" <string>$v</string>")
appendLine("${indentForLevel(2)}<key>${k.name}</key>")
appendLine(v.toString(2))
}
extraPlistKeysRawXml?.let { appendLine(it) }
appendLine(" </dict>")
appendLine("${indentForLevel(1)}</dict>")
appendLine("</plist>")
}
}
Expand All @@ -48,6 +97,13 @@ internal object PlistKeys {
val LSMinimumSystemVersion by this
val CFBundleDevelopmentRegion by this
val CFBundleAllowMixedLocalizations by this
val CFBundleDocumentTypes by this
val CFBundleTypeRole by this
val CFBundleTypeExtensions by this
val CFBundleTypeIconFile by this
val CFBundleTypeMIMETypes by this
val CFBundleTypeName by this
val CFBundleTypeOSTypes by this
val CFBundleExecutable by this
val CFBundleIconFile by this
val CFBundleIdentifier by this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ private fun JvmApplicationContext.configurePackageTask(
packageTask.packageVendor.set(packageTask.provider { executables.vendor })
packageTask.packageVersion.set(packageVersionFor(packageTask.targetFormat))
packageTask.licenseFile.set(executables.licenseFile)
packageTask.fileAssociations.set(executables.fileAssociations)
zhelenskiy marked this conversation as resolved.
Show resolved Hide resolved
}

packageTask.destinationDir.set(app.nativeDistributions.outputBaseDir.map {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ import org.gradle.api.file.*
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.provider.SetProperty
import org.gradle.api.tasks.*
import org.gradle.api.tasks.Optional
import org.gradle.process.ExecResult
import org.gradle.work.ChangeType
import org.gradle.work.InputChanges
import org.jetbrains.compose.desktop.application.dsl.FileAssociation
import org.jetbrains.compose.desktop.application.dsl.MacOSSigningSettings
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.compose.desktop.application.internal.*
import org.jetbrains.compose.desktop.application.internal.InfoPlistBuilder.InfoPlistValue.*
import org.jetbrains.compose.desktop.application.internal.files.*
import org.jetbrains.compose.desktop.application.internal.files.MacJarSignFileCopyingProcessor
import org.jetbrains.compose.desktop.application.internal.JvmRuntimeProperties
Expand Down Expand Up @@ -244,6 +247,10 @@ abstract class AbstractJPackageTask @Inject constructor(
@get:Optional
val javaRuntimePropertiesFile: RegularFileProperty = objects.fileProperty()

@get:Input
@get:Optional
val fileAssociations: SetProperty<FileAssociation> = objects.setProperty(FileAssociation::class.java)

private lateinit var jvmRuntimeInfo: JvmRuntimeProperties

@get:Optional
Expand Down Expand Up @@ -273,6 +280,9 @@ abstract class AbstractJPackageTask @Inject constructor(
@get:LocalState
protected val skikoDir: Provider<Directory> = project.layout.buildDirectory.dir("compose/tmp/skiko")

@get:LocalState
protected val propertyFilesDir: Provider<Directory> = project.layout.buildDirectory.dir("compose/tmp/propertyFiles")

@get:Internal
private val libsDir: Provider<Directory> = workingDir.map {
it.dir("libs")
Expand Down Expand Up @@ -368,6 +378,31 @@ abstract class AbstractJPackageTask @Inject constructor(
cliArg("--license-file", licenseFile)
cliArg("--resource-dir", jpackageResources)

val propertyFilesDirJava = propertyFilesDir.ioFile
fileOperations.clearDirs(propertyFilesDir)

val fileAssociationFiles = fileAssociations.orNull.orEmpty()
.groupBy { it.extension }
.mapValues { (extension, associations) ->
associations.mapIndexed { index, association ->
propertyFilesDirJava.resolve("FA${extension}${if (index > 0) index.toString() else ""}.properties")
.apply {
writeText(
"""
mime-type=${association.mimeType}
extension=${association.extension}
description=${association.description}
zhelenskiy marked this conversation as resolved.
Show resolved Hide resolved
""".trimIndent()
)
}
}
}.values.flatten()

for (fileAssociationFile in fileAssociationFiles) {
cliArg("--file-associations", fileAssociationFile)
}


when (currentOS) {
OS.Linux -> {
cliArg("--linux-shortcut", linuxShortcut)
Expand Down Expand Up @@ -620,6 +655,22 @@ abstract class AbstractJPackageTask @Inject constructor(
?: "Copyright (C) $year"
plist[PlistKeys.NSSupportsAutomaticGraphicsSwitching] = "true"
plist[PlistKeys.NSHighResolutionCapable] = "true"
val fileAssociationMutableSet = fileAssociations.orNull
if (!fileAssociationMutableSet.isNullOrEmpty()) {
plist[PlistKeys.CFBundleDocumentTypes] = fileAssociationMutableSet
.groupBy { it.mimeType to it.description }
.map { (key, extensions) ->
val (mimeType, description) = key
InfoPlistMapValue(
PlistKeys.CFBundleTypeRole to InfoPlistStringValue("Editor"),
PlistKeys.CFBundleTypeExtensions to InfoPlistListValue(extensions.map { InfoPlistStringValue(it.extension) }),
PlistKeys.CFBundleTypeIconFile to InfoPlistStringValue("$packageName.icns"),
PlistKeys.CFBundleTypeMIMETypes to InfoPlistStringValue(mimeType),
PlistKeys.CFBundleTypeName to InfoPlistStringValue(description),
PlistKeys.CFBundleTypeOSTypes to InfoPlistListValue(InfoPlistStringValue("****")),
)
}
}
}
}

Expand Down
zhelenskiy marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,37 +1,37 @@
<?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>12.0</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>
zhelenskiy marked this conversation as resolved.
Show resolved Hide resolved
<key>LSMinimumSystemVersion</key>
<string>12.0</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>

<key>CFBundleURLTypes</key>
<array>
Expand All @@ -44,5 +44,5 @@
</array>
</dict>
</array>
</dict>
</dict>
</plist>
Loading