Skip to content

Commit f6d1291

Browse files
committed
Refactor: Use plutil to parse actool version
This commit refactors the `actool` version parsing logic to use the `plutil` command-line tool instead of a manual XML parser. This change simplifies the code and improves robustness. - Replaced the XML parsing implementation in `MacAssetsTool` with a call to `plutil -extract`. - Updated `try`/`catch` blocks to handle exceptions from the tool execution. - Added a `plutil` file definition to `MacUtils`. - Integration tests for macOS layered icons (`testMacLayeredIcon`, `testMacLayeredIconRemove`) are now skipped if the `actool` version is less than 26, ensuring they run only on supported environments.
1 parent 3cce19c commit f6d1291

File tree

5 files changed

+51
-39
lines changed

5 files changed

+51
-39
lines changed

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

Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ package org.jetbrains.compose.desktop.application.internal
33
import org.gradle.api.logging.Logger
44
import org.jetbrains.compose.internal.utils.MacUtils
55
import java.io.File
6-
import java.io.StringReader
7-
import javax.xml.parsers.DocumentBuilderFactory
86

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

@@ -55,42 +53,38 @@ internal class MacAssetsTool(private val runTool: ExternalToolRunner, private va
5553
}
5654

5755
val versionString: String? = try {
58-
val dbFactory = DocumentBuilderFactory.newInstance()
59-
// Disable DTD loading to prevent XXE vulnerabilities and issues with network access or missing DTDs
60-
dbFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false)
61-
dbFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false)
62-
val dBuilder = dbFactory.newDocumentBuilder()
63-
val xmlInput = org.xml.sax.InputSource(StringReader(outputContent))
64-
val doc = dBuilder.parse(xmlInput)
65-
doc.documentElement.normalize() // Recommended practice
66-
val nodeList = doc.getElementsByTagName("key")
67-
var version: String? = null
68-
for (i in 0 until nodeList.length) {
69-
if ("short-bundle-version" == nodeList.item(i).textContent) {
70-
// Find the next sibling element which should be <string>
71-
var nextSibling = nodeList.item(i).nextSibling
72-
while (nextSibling != null && nextSibling.nodeType != org.w3c.dom.Node.ELEMENT_NODE) {
73-
nextSibling = nextSibling.nextSibling
74-
}
75-
if (nextSibling != null && nextSibling.nodeName == "string") {
76-
version = nextSibling.textContent
77-
break
78-
}
56+
var versionContent = ""
57+
runTool(
58+
tool = MacUtils.plutil,
59+
args = listOf(
60+
"-extract",
61+
"com\\.apple\\.actool\\.version.short-bundle-version",
62+
"raw",
63+
"-expect",
64+
"string",
65+
"-o",
66+
"-",
67+
"-"
68+
),
69+
stdinStr = outputContent,
70+
processStdout = {
71+
versionContent = it
7972
}
80-
}
81-
version
73+
)
74+
versionContent
8275
} catch (e: Exception) {
83-
error("Could not parse actool version XML from output: '$outputContent'. Error: ${e.message}")
76+
error("Could not check actool version. Error: ${e.message}")
8477
}
8578

86-
if (versionString == null) {
79+
if (versionString.isNullOrBlank()) {
8780
error("Could not extract short-bundle-version from actool output: '$outputContent'. Assuming it meets requirements.")
8881
}
8982

90-
val majorVersion = versionString.split(".").firstOrNull()?.toIntOrNull()
91-
if (majorVersion == null) {
92-
error("Could not get actool major version from version string '$versionString' . Output was: '$outputContent'. Assuming it meets requirements.")
93-
}
83+
val majorVersion = versionString
84+
.split(".")
85+
.firstOrNull()
86+
?.toIntOrNull()
87+
?: error("Could not get actool major version from version string '$versionString' . Output was: '$outputContent'. Assuming it meets requirements.")
9488

9589
if (majorVersion < requiredVersion) {
9690
error(

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -611,14 +611,14 @@ abstract class AbstractJPackageTask @Inject constructor(
611611

612612
macLayeredIcons.ioFileOrNull?.let { layeredIcon ->
613613
if (layeredIcon.exists()) {
614-
runCatching {
614+
try {
615615
macAssetsTool.compileAssets(
616616
layeredIcon,
617617
workingDir.ioFile,
618618
systemVersion
619619
)
620-
}.onFailure {
621-
logger.warn("Can not compile layered icon: ${it.message}")
620+
} catch (e: Exception) {
621+
logger.warn("Can not compile layered icon: ${e.message}")
622622
}
623623
}
624624
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,14 @@ abstract class AbstractNativeMacApplicationPackageAppDirTask : AbstractNativeMac
7070
appExecutableFile.setExecutable(true)
7171

7272
macLayeredIcons.orNull?.let {
73-
runCatching {
73+
try {
7474
macAssetsTool.compileAssets(
7575
iconDir = it.asFile,
7676
workingDir = workingDir,
7777
minimumSystemVersion = minimumSystemVersion.getOrElse(KOTLIN_NATIVE_MIN_SUPPORTED_MAC_OS)
7878
)
79-
}.onFailure { error ->
80-
logger.warn("Can not compile layered icon: ${error.message}")
79+
} catch (e: Exception) {
80+
logger.warn("Can not compile layered icon: ${e.message}")
8181
}
8282
}
8383

gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/utils/osUtils.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ internal object MacUtils {
8080
File("/usr/bin/open").checkExistingFile()
8181
}
8282

83+
val plutil: File by lazy {
84+
File("/usr/bin/plutil").checkExistingFile()
85+
}
86+
8387
}
8488

8589
internal object UnixUtils {

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ class DesktopApplicationTest : GradlePluginTestBase() {
369369

370370
@Test
371371
fun testMacLayeredIcon() {
372-
Assumptions.assumeTrue(currentOS == OS.MacOS)
372+
Assumptions.assumeTrue(currentOS == OS.MacOS && (getActoolMajorVersion() ?: 0) >= 26)
373373

374374
with(testProject("application/macLayeredIcon")) {
375375
val supportedString = "compile mac assets is starting, supported actool version:"
@@ -394,7 +394,7 @@ class DesktopApplicationTest : GradlePluginTestBase() {
394394

395395
@Test
396396
fun testMacLayeredIconRemove() {
397-
Assumptions.assumeTrue(currentOS == OS.MacOS)
397+
Assumptions.assumeTrue(currentOS == OS.MacOS && (getActoolMajorVersion() ?: 0) >= 26)
398398

399399
with(testProject("application/macLayeredIcon")) {
400400
val supportedString = "compile mac assets is starting, supported actool version:"
@@ -438,6 +438,20 @@ class DesktopApplicationTest : GradlePluginTestBase() {
438438
}
439439
}
440440

441+
private fun getActoolMajorVersion(): Int? {
442+
val command = """xcrun actool --version | plutil -extract "com\.apple\.actool\.version.short-bundle-version" raw -expect string -o - -"""
443+
444+
val process = ProcessBuilder("/bin/bash", "-c", command)
445+
.redirectErrorStream(true)
446+
.start()
447+
448+
val output = process.inputStream.bufferedReader().readText().trim()
449+
process.waitFor()
450+
return output.split(".")
451+
.firstOrNull()
452+
?.toIntOrNull()
453+
}
454+
441455
private fun macSignProject(
442456
identity: String,
443457
keychainFilename: String,

0 commit comments

Comments
 (0)