Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package org.jetbrains.compose.desktop.application.dsl

import org.gradle.api.Action
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.model.ObjectFactory
import java.io.File
Expand Down Expand Up @@ -35,6 +36,7 @@ abstract class AbstractMacOSPlatformSettings : AbstractPlatformSettings() {
var dmgPackageBuildVersion: String? = null
var appCategory: String? = null
var minimumSystemVersion: String? = null
var layeredIconDir: DirectoryProperty = objects.directoryProperty()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we override the icon defined in iconFile by defining this property and packaging it on the new macOs/Xcode? Is this icon supported on the oldest OS, because it is compiled into backward-compatible assets?

If so, it is better to reuse the existing iconFile instead of adding a new property and check that if it is a directory ending with ".icon" then we should use the layeredIcon logic.

Reasoning:

  • iconFile, layeredIconDir change one thing (the application icon) and differ only in implementations
  • it is similar to using different extensions, just more unusual
  • we might need to support it in fileAssociation

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we support it for fileAssociation?:

compose.desktop {
    application {
        nativeDistributions {
            macOS {
                fileAssociation(
                    "text/kotlin",
                    "kott",
                    "Kotlin Source File2",
                    project.file("subdir/kotlin_icon_big.icon"),
                )

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a link to the example (gradle-plugins/compose/src/test/test-projects/application/macLayeredIcon/subdir/kotlin_icon_big.icon) in the PR description, near the snippet and change the path in the snippet to subdir/kotlin_icon_big.icon

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to describe necessary changes required in the documentation.

I suggest to just do it in the PR:

Documentation changes needed

Documentation section

.icns files or .icon directory ([example](gradle-plugins/compose/src/test/test-projects/application/macLayeredIcon/subdir/kotlin_icon_big.icon)) for macOS



/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ internal object PlistKeys {
val CFBundleTypeOSTypes by this
val CFBundleExecutable by this
val CFBundleIconFile by this
val CFBundleIconName by this
val CFBundleIdentifier by this
val CFBundleInfoDictionaryVersion by this
val CFBundleName by this
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package org.jetbrains.compose.desktop.application.internal

import org.gradle.api.logging.Logger
import org.jetbrains.compose.internal.utils.MacUtils
import java.io.File

internal class MacAssetsTool(private val runTool: ExternalToolRunner, private val logger: Logger) {

fun compileAssets(iconDir: File, workingDir: File, minimumSystemVersion: String?): File {
val toolVersion = checkAssetsToolVersion()
logger.info("compile mac assets is starting, supported actool version:$toolVersion")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
logger.info("compile mac assets is starting, supported actool version:$toolVersion")
logger.info("Compilation of mac assets is starting, actool version: $toolVersion")


val result = runTool(
tool = MacUtils.xcrun,
args = listOf(
"actool",
iconDir.absolutePath, // Input asset catalog
"--compile", workingDir.absolutePath,
"--app-icon", iconDir.name.removeSuffix(".icon"),
"--enable-on-demand-resources", "NO",
"--development-region", "en",
"--target-device", "mac",
"--platform", "macosx",
"--enable-icon-stack-fallback-generation=disabled",
"--include-all-app-icons",
"--minimum-deployment-target", minimumSystemVersion ?: "10.13",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"--minimum-deployment-target", minimumSystemVersion ?: "10.13",
"--minimum-deployment-target", minimumSystemVersion,

Better to pass a non-null parameter to not have "10.13" in multiple places. We seems can do that, by changing one of the calling code to minimumSystemVersion.getOrElse(KOTLIN_NATIVE_MIN_SUPPORTED_MAC_OS)!!

"--output-partial-info-plist", "/dev/null"
),
)

if (result.exitValue != 0) {
error("Could not compile the layered icons directory into Assets.car.")
}
if (!assetsFile(workingDir).exists()) {
error("Could not find Assets.car in the working directory.")
}
return workingDir.resolve("Assets.car")
}

fun assetsFile(workingDir: File): File = workingDir.resolve("Assets.car")

private fun checkAssetsToolVersion(): String {
val requiredVersion = 26.0
var outputContent = ""
val result = runTool(
tool = MacUtils.xcrun,
args = listOf("actool", "--version"),
processStdout = { outputContent = it },
)

if (result.exitValue != 0) {
error("Could not get actool version: Command `xcrun actool -version` exited with code ${result.exitValue}\nStdOut: $outputContent\n")
}

val versionString: String? = try {
var versionContent = ""
runTool(
tool = MacUtils.plutil,
args = listOf(
"-extract",
"com\\.apple\\.actool\\.version.short-bundle-version",
"raw",
"-expect",
"string",
"-o",
"-",
"-"
),
stdinStr = outputContent,
processStdout = {
versionContent = it
}
)
versionContent
} catch (e: Exception) {
error("Could not check actool version. Error: ${e.message}")
}

if (versionString.isNullOrBlank()) {
error("Could not extract short-bundle-version from actool output: '$outputContent'. Assuming it meets requirements.")
}

val majorVersion = versionString
.split(".")
.firstOrNull()
?.toIntOrNull()
?: error("Could not get actool major version from version string '$versionString' . Output was: '$outputContent'. Assuming it meets requirements.")

if (majorVersion < requiredVersion) {
error(
"Unsupported actool version: $versionString. " +
"Version $requiredVersion or higher is required. " +
"Please update your Xcode Command Line Tools."
)
} else {
return versionString
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ internal fun JvmApplicationContext.configurePlatformSettings(
packageTask.iconFile.set(mac.iconFile.orElse(defaultResources.get { macIcon }))
packageTask.installationPath.set(mac.installationPath)
packageTask.fileAssociations.set(provider { mac.fileAssociations })
packageTask.macLayeredIcons.set(mac.layeredIconDir)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ private fun configureNativeApplication(
}
composeResourcesDirs.setFrom(binaryResources)
}
macLayeredIcons.set(app.distributions.macOS.layeredIconDir)
}

if (TargetFormat.Dmg in app.distributions.targetFormats) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import org.jetbrains.compose.desktop.application.internal.InfoPlistBuilder.InfoP
import org.jetbrains.compose.desktop.application.internal.InfoPlistBuilder.InfoPlistValue.InfoPlistMapValue
import org.jetbrains.compose.desktop.application.internal.InfoPlistBuilder.InfoPlistValue.InfoPlistStringValue
import org.jetbrains.compose.desktop.application.internal.JvmRuntimeProperties
import org.jetbrains.compose.desktop.application.internal.MacAssetsTool
import org.jetbrains.compose.desktop.application.internal.MacSigner
import org.jetbrains.compose.desktop.application.internal.MacSignerImpl
import org.jetbrains.compose.desktop.application.internal.NoCertificateSigner
Expand Down Expand Up @@ -293,7 +294,11 @@ abstract class AbstractJPackageTask @Inject constructor(

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


@get:InputDirectory
@get:Optional
internal val macLayeredIcons: DirectoryProperty = objects.directoryProperty()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
internal val macLayeredIcons: DirectoryProperty = objects.directoryProperty()
internal val layeredIconDir: DirectoryProperty = objects.directoryProperty()

to be consistent with the public property (if we still decide to keep it after this)


private val iconMapping by lazy {
val icons = fileAssociations.get().mapNotNull { it.iconFile }.distinct()
if (icons.isEmpty()) return@lazy emptyMap()
Expand Down Expand Up @@ -344,6 +349,8 @@ abstract class AbstractJPackageTask @Inject constructor(
} else null
}

private val macAssetsTool by lazy { MacAssetsTool(runExternalTool, logger) }

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

Expand Down Expand Up @@ -600,12 +607,28 @@ abstract class AbstractJPackageTask @Inject constructor(

fileOperations.clearDirs(jpackageResources)
if (currentOS == OS.MacOS) {
val systemVersion = macMinimumSystemVersion.orNull ?: "10.13"

macLayeredIcons.ioFileOrNull?.let { layeredIcon ->
if (layeredIcon.exists()) {
try {
macAssetsTool.compileAssets(
layeredIcon,
workingDir.ioFile,
systemVersion
)
} catch (e: Exception) {
logger.warn("Can not compile layered icon: ${e.message}")
}
}
}

InfoPlistBuilder(macExtraPlistKeysRawXml.orNull)
.also { setInfoPlistValues(it) }
.writeToFile(jpackageResources.ioFile.resolve("Info.plist"))

if (macAppStore.orNull == true) {
val systemVersion = macMinimumSystemVersion.orNull ?: "10.13"

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

val productDefPlistXml = """
<key>os</key>
<array>
Expand Down Expand Up @@ -642,6 +665,12 @@ abstract class AbstractJPackageTask @Inject constructor(
val appDir = destinationDir.ioFile.resolve("${packageName.get()}.app")
val runtimeDir = appDir.resolve("Contents/runtime")

macAssetsTool.assetsFile(workingDir.ioFile).apply {
if (exists()) {
copyTo(appDir.resolve("Contents/Resources/Assets.car"))
}
}

// Add the provisioning profile
macRuntimeProvisioningProfile.ioFileOrNull?.copyTo(
target = runtimeDir.resolve("Contents/embedded.provisionprofile"),
Expand Down Expand Up @@ -739,6 +768,10 @@ abstract class AbstractJPackageTask @Inject constructor(
)
}
}

if (macAssetsTool.assetsFile(workingDir.ioFile).exists()) {
macLayeredIcons.orNull?.let { plist[PlistKeys.CFBundleIconName] = it.asFile.name.removeSuffix(".icon") }
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@
package org.jetbrains.compose.desktop.application.tasks

import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
import org.gradle.api.tasks.Optional
import org.jetbrains.compose.desktop.application.internal.InfoPlistBuilder
import org.jetbrains.compose.desktop.application.internal.MacAssetsTool
import org.jetbrains.compose.desktop.application.internal.PlistKeys
import org.jetbrains.compose.internal.utils.ioFile
import org.jetbrains.compose.internal.utils.notNullProperty
import org.jetbrains.compose.internal.utils.nullableProperty
import java.io.File
import kotlin.getValue

private const val KOTLIN_NATIVE_MIN_SUPPORTED_MAC_OS = "10.13"

Expand Down Expand Up @@ -49,6 +52,12 @@ abstract class AbstractNativeMacApplicationPackageAppDirTask : AbstractNativeMac
@get:PathSensitive(PathSensitivity.ABSOLUTE)
val composeResourcesDirs: ConfigurableFileCollection = objects.fileCollection()

@get:InputDirectory
@get:Optional
internal val macLayeredIcons: DirectoryProperty = objects.directoryProperty()

private val macAssetsTool by lazy { MacAssetsTool(runExternalTool, logger) }

override fun createPackage(destinationDir: File, workingDir: File) {
val packageName = packageName.get()
val appDir = destinationDir.resolve("$packageName.app").apply { mkdirs() }
Expand All @@ -60,6 +69,18 @@ abstract class AbstractNativeMacApplicationPackageAppDirTask : AbstractNativeMac
executable.ioFile.copyTo(appExecutableFile)
appExecutableFile.setExecutable(true)

macLayeredIcons.orNull?.let {
try {
macAssetsTool.compileAssets(
iconDir = it.asFile,
workingDir = workingDir,
minimumSystemVersion = minimumSystemVersion.getOrElse(KOTLIN_NATIVE_MIN_SUPPORTED_MAC_OS)
)
} catch (e: Exception) {
logger.warn("Can not compile layered icon: ${e.message}")
}
}

val appIconFile = appResourcesDir.resolve("$packageName.icns")
iconFile.ioFile.copyTo(appIconFile)

Expand All @@ -74,6 +95,15 @@ abstract class AbstractNativeMacApplicationPackageAppDirTask : AbstractNativeMac
copySpec.into(appResourcesDir.resolve("compose-resources").apply { mkdirs() })
}
}

macAssetsTool.assetsFile(workingDir).let {
if (it.exists()) {
fileOperations.copy { copySpec ->
copySpec.from(it)
copySpec.into(appResourcesDir)
}
}
}
}

private fun InfoPlistBuilder.setupInfoPlist(executableName: String) {
Expand All @@ -90,5 +120,9 @@ abstract class AbstractNativeMacApplicationPackageAppDirTask : AbstractNativeMac
this[PlistKeys.NSHumanReadableCopyright] = copyright.orNull
this[PlistKeys.NSSupportsAutomaticGraphicsSwitching] = "true"
this[PlistKeys.NSHighResolutionCapable] = "true"

if (macAssetsTool.assetsFile(workingDir.ioFile).exists()) {
macLayeredIcons.orNull?.let { this[PlistKeys.CFBundleIconName] = it.asFile.name.removeSuffix(".icon") }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ internal object MacUtils {
File("/usr/bin/open").checkExistingFile()
}

val plutil: File by lazy {
File("/usr/bin/plutil").checkExistingFile()
}

}

internal object UnixUtils {
Expand Down
Loading