diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/FileAssociation.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/FileAssociation.kt index fbddaea3bac..925b8988617 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/FileAssociation.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/FileAssociation.kt @@ -1,9 +1,11 @@ package org.jetbrains.compose.desktop.application.dsl +import java.io.File import java.io.Serializable data class FileAssociation( val mimeType: String, val extension: String, val description: String, + val iconFile: File?, ) : Serializable diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationDistributions.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationDistributions.kt index e3c75bc840f..2a51e0ab1b0 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationDistributions.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationDistributions.kt @@ -6,6 +6,7 @@ package org.jetbrains.compose.desktop.application.dsl import org.gradle.api.Action +import java.io.File internal val DEFAULT_RUNTIME_MODULES = arrayOf( "java.base", "java.desktop", "java.logging", "jdk.crypto.ec" @@ -33,8 +34,12 @@ abstract class JvmApplicationDistributions : AbstractDistributions() { fn.execute(windows) } - internal val fileAssociations: MutableSet = mutableSetOf() - fun fileAssociation(mimeType: String, extension: String, description: String) { - fileAssociations.add(FileAssociation(mimeType, extension, description)) + fun fileAssociation( + mimeType: String, extension: String, description: String, + linuxIcon: File? = null, windowsIcon: File? = null, macOSIcon: File? = null, + ) { + linux.fileAssociation(mimeType, extension, description, linuxIcon) + windows.fileAssociation(mimeType, extension, description, windowsIcon) + macOS.fileAssociation(mimeType, extension, description, macOSIcon) } } \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/PlatformSettings.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/PlatformSettings.kt index f8b3e3450b1..0d864043544 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/PlatformSettings.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/PlatformSettings.kt @@ -8,6 +8,7 @@ package org.jetbrains.compose.desktop.application.dsl import org.gradle.api.Action import org.gradle.api.file.RegularFileProperty import org.gradle.api.model.ObjectFactory +import java.io.File import javax.inject.Inject abstract class AbstractPlatformSettings { @@ -17,6 +18,11 @@ abstract class AbstractPlatformSettings { val iconFile: RegularFileProperty = objects.fileProperty() var packageVersion: String? = null var installationPath: String? = null + + internal val fileAssociations: MutableSet = mutableSetOf() + fun fileAssociation(mimeType: String, extension: String, description: String, iconFile: File? = null) { + fileAssociations.add(FileAssociation(mimeType, extension, description, iconFile)) + } } abstract class AbstractMacOSPlatformSettings : AbstractPlatformSettings() { diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt index fcea25ddf10..b4af732fc75 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureJvmApplication.kt @@ -322,7 +322,6 @@ 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) } packageTask.destinationDir.set(app.nativeDistributions.outputBaseDir.map { @@ -376,6 +375,7 @@ internal fun JvmApplicationContext.configurePlatformSettings( packageTask.linuxRpmLicenseType.set(provider { linux.rpmLicenseType }) packageTask.iconFile.set(linux.iconFile.orElse(defaultResources.get { linuxIcon })) packageTask.installationPath.set(linux.installationPath) + packageTask.fileAssociations.set(linux.fileAssociations) } } OS.Windows -> { @@ -389,6 +389,7 @@ internal fun JvmApplicationContext.configurePlatformSettings( packageTask.winUpgradeUuid.set(provider { win.upgradeUuid }) packageTask.iconFile.set(win.iconFile.orElse(defaultResources.get { windowsIcon })) packageTask.installationPath.set(win.installationPath) + packageTask.fileAssociations.set(win.fileAssociations) } } OS.MacOS -> { @@ -415,6 +416,7 @@ internal fun JvmApplicationContext.configurePlatformSettings( packageTask.nonValidatedMacSigningSettings = app.nativeDistributions.macOS.signing packageTask.iconFile.set(mac.iconFile.orElse(defaultResources.get { macIcon })) packageTask.installationPath.set(mac.installationPath) + packageTask.fileAssociations.set(mac.fileAssociations) } } } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt index a1b99cd917d..3a2e338453c 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt +++ b/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.MacJarSignFileCo import org.jetbrains.compose.desktop.application.internal.JvmRuntimeProperties import org.jetbrains.compose.desktop.application.internal.validation.validate import org.jetbrains.compose.internal.utils.* +import org.jetbrains.kotlin.gradle.internal.ensureParentDirsCreated import java.io.* import java.nio.file.LinkOption import java.util.* @@ -250,6 +251,22 @@ abstract class AbstractJPackageTask @Inject constructor( @get:Input @get:Optional val fileAssociations: SetProperty = objects.setProperty(FileAssociation::class.java) + + private val iconMapping by lazy { + val icons = fileAssociations.orNull.orEmpty().mapNotNull { it.iconFile } + if (icons.isEmpty()) return@lazy emptyMap() + val iconTempNames = generateSequence { + icons.mapTo(mutableSetOf()) { String(CharArray(10) { ('a'..'z').random() }) } + }.first { it.size == icons.size } + val appDir = destinationDir.ioFile.resolve("${packageName.get()}.app") + val iconsDir = appDir.resolve("Contents").resolve("Resources") + if (iconsDir.exists()) { + iconsDir.deleteRecursively() + } + icons.zip(iconTempNames) { icon, newName -> + icon to iconsDir.resolve(newName + icon.name.drop(icon.nameWithoutExtension.length)) + }.toMap() + } private lateinit var jvmRuntimeInfo: JvmRuntimeProperties @@ -387,12 +404,14 @@ abstract class AbstractJPackageTask @Inject constructor( associations.mapIndexed { index, association -> propertyFilesDirJava.resolve("FA${extension}${if (index > 0) index.toString() else ""}.properties") .apply { + val withoutIcon = """ + mime-type=${association.mimeType} + extension=${association.extension} + description=${association.description} + """.trimIndent() writeText( - """ - mime-type=${association.mimeType} - extension=${association.extension} - description=${association.description} - """.trimIndent() + if (association.iconFile == null) withoutIcon + else "${withoutIcon}\nicon=${association.iconFile.normalizedPath()}" ) } } @@ -604,6 +623,15 @@ abstract class AbstractJPackageTask @Inject constructor( macSigner.sign(runtimeDir, runtimeEntitlementsFile, forceEntitlements = true) macSigner.sign(appDir, appEntitlementsFile, forceEntitlements = true) + + if (iconMapping.isNotEmpty()) { + for ((originalIcon, newIcon) in iconMapping) { + if (originalIcon.exists()) { + newIcon.ensureParentDirsCreated() + originalIcon.copyTo(newIcon) + } + } + } } override fun initState() { @@ -661,10 +689,11 @@ abstract class AbstractJPackageTask @Inject constructor( .groupBy { it.mimeType to it.description } .map { (key, extensions) -> val (mimeType, description) = key + val iconPath = extensions.firstNotNullOfOrNull { it.iconFile }?.let { iconMapping[it]?.name } InfoPlistMapValue( PlistKeys.CFBundleTypeRole to InfoPlistStringValue("Editor"), PlistKeys.CFBundleTypeExtensions to InfoPlistListValue(extensions.map { InfoPlistStringValue(it.extension) }), - PlistKeys.CFBundleTypeIconFile to InfoPlistStringValue("$packageName.icns"), + PlistKeys.CFBundleTypeIconFile to InfoPlistStringValue(iconPath ?: "$packageName.icns"), PlistKeys.CFBundleTypeMIMETypes to InfoPlistStringValue(mimeType), PlistKeys.CFBundleTypeName to InfoPlistStringValue(description), PlistKeys.CFBundleTypeOSTypes to InfoPlistListValue(InfoPlistStringValue("****")),