mirror of
https://github.com/jlengrand/compose-multiplatform.git
synced 2026-03-10 08:11:20 +00:00
committed by
Alexey Tsvetkov
parent
8dd40e60e3
commit
d1908bb41a
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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 org.gradle.api.file.Directory
|
||||
import org.gradle.api.provider.Property
|
||||
import org.gradle.api.provider.Provider
|
||||
import org.gradle.process.ExecOperations
|
||||
import org.gradle.process.ExecResult
|
||||
import java.io.File
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
internal class ExternalToolRunner(
|
||||
private val verbose: Property<Boolean>,
|
||||
private val logsDir: Provider<Directory>,
|
||||
private val execOperations: ExecOperations
|
||||
) {
|
||||
operator fun invoke(
|
||||
tool: File,
|
||||
args: Collection<String>,
|
||||
environment: Map<String, Any> = emptyMap(),
|
||||
workingDir: File? = null,
|
||||
checkExitCodeIsNormal: Boolean = true,
|
||||
processStdout: Function1<String, Unit>? = null,
|
||||
forceLogToFile: Boolean = false
|
||||
): ExecResult {
|
||||
val logsDir = logsDir.ioFile
|
||||
logsDir.mkdirs()
|
||||
|
||||
val toolName = tool.nameWithoutExtension
|
||||
val logToConsole = verbose.get() && !forceLogToFile
|
||||
val outFile = logsDir.resolve("${toolName}-${currentTimeStamp()}-out.txt")
|
||||
val errFile = logsDir.resolve("${toolName}-${currentTimeStamp()}-err.txt")
|
||||
|
||||
val result = outFile.outputStream().buffered().use { outFileStream ->
|
||||
errFile.outputStream().buffered().use { errFileStream ->
|
||||
execOperations.exec { spec ->
|
||||
spec.executable = tool.absolutePath
|
||||
spec.args(*args.toTypedArray())
|
||||
workingDir?.let { wd -> spec.workingDir(wd) }
|
||||
spec.environment(environment)
|
||||
// check exit value later
|
||||
spec.isIgnoreExitValue = true
|
||||
|
||||
if (logToConsole) {
|
||||
spec.standardOutput = spec.standardOutput.alsoOutputTo(outFileStream)
|
||||
spec.errorOutput = spec.errorOutput.alsoOutputTo(errFileStream)
|
||||
} else {
|
||||
spec.standardOutput = outFileStream
|
||||
spec.errorOutput = errFileStream
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (checkExitCodeIsNormal && result.exitValue != 0) {
|
||||
val errMsg = buildString {
|
||||
appendLine("External tool execution failed:")
|
||||
val cmd = (listOf(tool.absolutePath) + args).joinToString(", ")
|
||||
appendLine("* Command: [$cmd]")
|
||||
appendLine("* Working dir: [${workingDir?.absolutePath.orEmpty()}]")
|
||||
appendLine("* Exit code: ${result.exitValue}")
|
||||
appendLine("* Standard output log: ${outFile.absolutePath}")
|
||||
appendLine("* Error log: ${errFile.absolutePath}")
|
||||
}
|
||||
|
||||
error(errMsg)
|
||||
}
|
||||
|
||||
if (processStdout != null) {
|
||||
processStdout(outFile.readText())
|
||||
}
|
||||
|
||||
if (result.exitValue == 0) {
|
||||
outFile.delete()
|
||||
errFile.delete()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun currentTimeStamp() =
|
||||
LocalDateTime.now()
|
||||
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss"))
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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 org.jetbrains.compose.desktop.application.internal.validation.ValidatedMacOSSigningSettings
|
||||
import java.io.File
|
||||
import java.util.regex.Pattern
|
||||
|
||||
internal class MacSigner(
|
||||
val settings: ValidatedMacOSSigningSettings,
|
||||
private val runExternalTool: ExternalToolRunner
|
||||
) {
|
||||
private lateinit var signKey: String
|
||||
|
||||
init {
|
||||
runExternalTool(
|
||||
MacUtils.security,
|
||||
args = listOfNotNull(
|
||||
"find-certificate",
|
||||
"-a",
|
||||
"-c",
|
||||
settings.fullDeveloperID,
|
||||
settings.keychain?.absolutePath
|
||||
),
|
||||
processStdout = { stdout ->
|
||||
signKey = findCertificate(stdout)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun sign(file: File) {
|
||||
val args = arrayListOf(
|
||||
"-vvvv",
|
||||
"--timestamp",
|
||||
"--options", "runtime",
|
||||
"--force",
|
||||
"--prefix", settings.prefix,
|
||||
"--sign", signKey
|
||||
)
|
||||
|
||||
settings.keychain?.let {
|
||||
args.add("--keychain")
|
||||
args.add(it.absolutePath)
|
||||
}
|
||||
|
||||
args.add(file.absolutePath)
|
||||
|
||||
runExternalTool(MacUtils.codesign, args)
|
||||
}
|
||||
|
||||
private fun findCertificate(certificates: String): String {
|
||||
val regex = Pattern.compile("\"alis\"<blob>=\"([^\"]+)\"")
|
||||
val m = regex.matcher(certificates)
|
||||
if (!m.find()) {
|
||||
val keychainPath = settings.keychain?.absolutePath
|
||||
error(
|
||||
"Could not find certificate for '${settings.identity}'" +
|
||||
" in keychain [${keychainPath.orEmpty()}]"
|
||||
)
|
||||
}
|
||||
|
||||
val result = m.group(1)
|
||||
if (m.find())
|
||||
error(
|
||||
"Multiple matching certificates are found for '${settings.fullDeveloperID}'. " +
|
||||
"Please specify keychain containing unique matching certificate."
|
||||
)
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -5,60 +5,24 @@
|
||||
|
||||
package org.jetbrains.compose.desktop.application.internal.files
|
||||
|
||||
import org.gradle.process.ExecOperations
|
||||
import org.jetbrains.compose.desktop.application.internal.MacUtils
|
||||
import org.jetbrains.compose.desktop.application.internal.MacSigner
|
||||
import org.jetbrains.compose.desktop.application.internal.isJarFile
|
||||
import org.jetbrains.compose.desktop.application.internal.validation.ValidatedMacOSSigningSettings
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.PrintStream
|
||||
import java.util.regex.Pattern
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
internal class MacJarSignFileCopyingProcessor(
|
||||
private val signer: MacSigner,
|
||||
private val tempDir: File,
|
||||
private val execOperations: ExecOperations,
|
||||
private val signing: ValidatedMacOSSigningSettings
|
||||
) : FileCopyingProcessor {
|
||||
private val signKey: String
|
||||
|
||||
init {
|
||||
val certificates = ByteArrayOutputStream().use { baos ->
|
||||
PrintStream(baos).use { ps ->
|
||||
execOperations.exec { exec ->
|
||||
exec.executable = MacUtils.security.absolutePath
|
||||
val args = arrayListOf("find-certificate", "-a", "-c", signing.fullDeveloperID)
|
||||
signing.keychain?.let { args.add(it.absolutePath) }
|
||||
exec.args(*args.toTypedArray())
|
||||
exec.standardOutput = ps
|
||||
}
|
||||
}
|
||||
baos.toString()
|
||||
}
|
||||
val regex = Pattern.compile("\"alis\"<blob>=\"([^\"]+)\"")
|
||||
val m = regex.matcher(certificates)
|
||||
if (!m.find()) {
|
||||
val keychainPath = signing.keychain?.absolutePath
|
||||
error(
|
||||
"Could not find certificate for '${signing.identity}'" +
|
||||
" in keychain [${keychainPath.orEmpty()}]"
|
||||
)
|
||||
}
|
||||
|
||||
signKey = m.group(1)
|
||||
if (m.find()) error("Multiple matching certificates are found for '${signing.fullDeveloperID}'. " +
|
||||
"Please specify keychain containing unique matching certificate.")
|
||||
}
|
||||
|
||||
override fun copy(source: File, target: File) {
|
||||
if (source.isJarFile) {
|
||||
signNativeLibsInJar(source, target)
|
||||
} else {
|
||||
SimpleFileCopyingProcessor.copy(source, target)
|
||||
if (source.name.isDylibPath) {
|
||||
signDylib(target)
|
||||
signer.sign(target)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,7 +45,7 @@ internal class MacJarSignFileCopyingProcessor(
|
||||
val unpackedDylibFile = tempDir.resolve(sourceEntry.name.substringAfterLast("/"))
|
||||
try {
|
||||
zin.copyTo(unpackedDylibFile)
|
||||
signDylib(unpackedDylibFile)
|
||||
signer.sign(unpackedDylibFile)
|
||||
val targetEntry = ZipEntry(sourceEntry.name).apply {
|
||||
comment = sourceEntry.comment
|
||||
extra = sourceEntry.extra
|
||||
@@ -95,29 +59,6 @@ internal class MacJarSignFileCopyingProcessor(
|
||||
unpackedDylibFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
private fun signDylib(dylibFile: File) {
|
||||
val args = arrayListOf(
|
||||
"-vvvv",
|
||||
"--timestamp",
|
||||
"--options", "runtime",
|
||||
"--force",
|
||||
"--prefix", signing.prefix,
|
||||
"--sign", signKey
|
||||
)
|
||||
|
||||
signing.keychain?.let {
|
||||
args.add("--keychain")
|
||||
args.add(it.absolutePath)
|
||||
}
|
||||
|
||||
args.add(dylibFile.absolutePath)
|
||||
|
||||
execOperations.exec { exec ->
|
||||
exec.executable = MacUtils.codesign.absolutePath
|
||||
exec.args(*args.toTypedArray())
|
||||
}.assertNormalExitValue()
|
||||
}
|
||||
}
|
||||
|
||||
private val String.isDylibPath
|
||||
|
||||
@@ -58,11 +58,11 @@ private class ErrorsCollector {
|
||||
correctFormat: String? = null
|
||||
) {
|
||||
val msg = buildString {
|
||||
appendln("* Illegal version for '$targetFormat': $error.")
|
||||
appendLine("* Illegal version for '$targetFormat': $error.")
|
||||
if (correctFormat != null) {
|
||||
appendln(" * Correct format: $correctFormat")
|
||||
appendLine(" * Correct format: $correctFormat")
|
||||
}
|
||||
appendln(" * You can specify the correct version using DSL properties: " +
|
||||
appendLine(" * You can specify the correct version using DSL properties: " +
|
||||
dslPropertiesFor(targetFormat).joinToString(", ")
|
||||
)
|
||||
}
|
||||
|
||||
@@ -172,12 +172,13 @@ abstract class AbstractJPackageTask @Inject constructor(
|
||||
@get:Nested
|
||||
internal var nonValidatedMacSigningSettings: MacOSSigningSettings? = null
|
||||
|
||||
private inline fun <T> withValidatedMacOSSigning(fn: (ValidatedMacOSSigningSettings) -> T): T? =
|
||||
nonValidatedMacSigningSettings?.let { nonValidated ->
|
||||
if (currentOS == OS.MacOS && nonValidated.sign.get()) {
|
||||
fn(nonValidated.validate(nonValidatedMacBundleID))
|
||||
} else null
|
||||
}
|
||||
private val macSigner: MacSigner? by lazy {
|
||||
val nonValidatedSettings = nonValidatedMacSigningSettings
|
||||
if (currentOS == OS.MacOS && nonValidatedSettings?.sign?.get() == true) {
|
||||
val validatedSettings = nonValidatedSettings.validate(nonValidatedMacBundleID)
|
||||
MacSigner(validatedSettings, runExternalTool)
|
||||
} else null
|
||||
}
|
||||
|
||||
@get:LocalState
|
||||
protected val signDir: Provider<Directory> = project.layout.buildDirectory.dir("compose/tmp/sign")
|
||||
@@ -272,11 +273,11 @@ abstract class AbstractJPackageTask @Inject constructor(
|
||||
cliArg("--mac-package-name", macPackageName)
|
||||
cliArg("--mac-package-identifier", nonValidatedMacBundleID)
|
||||
|
||||
withValidatedMacOSSigning { signing ->
|
||||
macSigner?.let { signer ->
|
||||
cliArg("--mac-sign", true)
|
||||
cliArg("--mac-signing-key-user-name", signing.identity)
|
||||
cliArg("--mac-signing-keychain", signing.keychain)
|
||||
cliArg("--mac-package-signing-prefix", signing.prefix)
|
||||
cliArg("--mac-signing-key-user-name", signer.settings.identity)
|
||||
cliArg("--mac-signing-keychain", signer.settings.keychain)
|
||||
cliArg("--mac-package-signing-prefix", signer.settings.prefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -324,16 +325,12 @@ abstract class AbstractJPackageTask @Inject constructor(
|
||||
override fun prepareWorkingDir(inputChanges: InputChanges) {
|
||||
val libsDir = libsDir.ioFile
|
||||
val fileProcessor =
|
||||
withValidatedMacOSSigning { signing ->
|
||||
macSigner?.let { signer ->
|
||||
val tmpDirForSign = signDir.ioFile
|
||||
fileOperations.delete(tmpDirForSign)
|
||||
tmpDirForSign.mkdirs()
|
||||
|
||||
MacJarSignFileCopyingProcessor(
|
||||
tempDir = tmpDirForSign,
|
||||
execOperations = execOperations,
|
||||
signing = signing
|
||||
)
|
||||
MacJarSignFileCopyingProcessor(signer, tmpDirForSign)
|
||||
} ?: SimpleFileCopyingProcessor
|
||||
fun copyFileToLibsDir(sourceFile: File): File {
|
||||
val targetFileName =
|
||||
@@ -388,7 +385,8 @@ abstract class AbstractJPackageTask @Inject constructor(
|
||||
private fun patchInfoPlistIfNeeded() {
|
||||
if (currentOS != OS.MacOS || targetFormat != TargetFormat.AppImage) return
|
||||
|
||||
val infoPlist = destinationDir.ioFile.resolve("${packageName.get()}.app/Contents/Info.plist")
|
||||
val appDir = destinationDir.ioFile.resolve("${packageName.get()}.app/")
|
||||
val infoPlist = appDir.resolve("Contents/Info.plist")
|
||||
if (!infoPlist.exists()) return
|
||||
|
||||
val content = infoPlist.readText()
|
||||
@@ -405,12 +403,13 @@ abstract class AbstractJPackageTask @Inject constructor(
|
||||
if (i >= 0) {
|
||||
val newContent = buildString {
|
||||
append(content.substring(0, i))
|
||||
appendln(stringToAppend)
|
||||
appendLine(stringToAppend)
|
||||
append(" ")
|
||||
appendln(content.substring(i, content.length))
|
||||
appendLine(content.substring(i, content.length))
|
||||
}
|
||||
infoPlist.writeText(newContent)
|
||||
}
|
||||
macSigner?.sign(appDir)
|
||||
}
|
||||
|
||||
override fun initState() {
|
||||
|
||||
@@ -16,7 +16,9 @@ import org.gradle.api.tasks.Internal
|
||||
import org.gradle.api.tasks.LocalState
|
||||
import org.gradle.process.ExecOperations
|
||||
import org.gradle.process.ExecResult
|
||||
import org.jetbrains.compose.desktop.application.internal.*
|
||||
import org.jetbrains.compose.desktop.application.internal.ComposeProperties
|
||||
import org.jetbrains.compose.desktop.application.internal.ExternalToolRunner
|
||||
import org.jetbrains.compose.desktop.application.internal.alsoOutputTo
|
||||
import org.jetbrains.compose.desktop.application.internal.ioFile
|
||||
import org.jetbrains.compose.desktop.application.internal.notNullProperty
|
||||
@@ -48,71 +50,7 @@ abstract class AbstractComposeDesktopTask : DefaultTask() {
|
||||
})
|
||||
}
|
||||
|
||||
internal fun runExternalTool(
|
||||
tool: File,
|
||||
args: Collection<String>,
|
||||
environment: Map<String, Any> = emptyMap(),
|
||||
workingDir: File? = null,
|
||||
checkExitCodeIsNormal: Boolean = true,
|
||||
processStdout: Function1<String, Unit>? = null,
|
||||
forceLogToFile: Boolean = false
|
||||
): ExecResult {
|
||||
val logsDir = logsDir.ioFile
|
||||
logsDir.mkdirs()
|
||||
|
||||
val toolName = tool.nameWithoutExtension
|
||||
val logToConsole = verbose.get() && !forceLogToFile
|
||||
val outFile = logsDir.resolve("${toolName}-${currentTimeStamp()}-out.txt")
|
||||
val errFile = logsDir.resolve("${toolName}-${currentTimeStamp()}-err.txt")
|
||||
|
||||
val result = outFile.outputStream().buffered().use { outFileStream ->
|
||||
errFile.outputStream().buffered().use { errFileStream ->
|
||||
execOperations.exec { spec ->
|
||||
spec.executable = tool.absolutePath
|
||||
spec.args(*args.toTypedArray())
|
||||
workingDir?.let { wd -> spec.workingDir(wd) }
|
||||
spec.environment(environment)
|
||||
// check exit value later
|
||||
spec.isIgnoreExitValue = true
|
||||
|
||||
if (logToConsole) {
|
||||
spec.standardOutput = spec.standardOutput.alsoOutputTo(outFileStream)
|
||||
spec.errorOutput = spec.errorOutput.alsoOutputTo(errFileStream)
|
||||
} else {
|
||||
spec.standardOutput = outFileStream
|
||||
spec.errorOutput = errFileStream
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (checkExitCodeIsNormal && result.exitValue != 0) {
|
||||
val errMsg = buildString {
|
||||
appendln("External tool execution failed:")
|
||||
val cmd = (listOf(tool.absolutePath) + args).joinToString(", ")
|
||||
appendln("* Command: [$cmd]")
|
||||
appendln("* Working dir: [${workingDir?.absolutePath.orEmpty()}]")
|
||||
appendln("* Exit code: ${result.exitValue}")
|
||||
appendln("* Standard output log: ${outFile.absolutePath}")
|
||||
appendln("* Error log: ${errFile.absolutePath}")
|
||||
}
|
||||
|
||||
error(errMsg)
|
||||
}
|
||||
|
||||
if (processStdout != null) {
|
||||
processStdout(outFile.readText())
|
||||
}
|
||||
|
||||
if (result.exitValue == 0) {
|
||||
outFile.delete()
|
||||
errFile.delete()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun currentTimeStamp() =
|
||||
LocalDateTime.now()
|
||||
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss"))
|
||||
@get:Internal
|
||||
internal val runExternalTool: ExternalToolRunner
|
||||
get() = ExternalToolRunner(verbose, logsDir, execOperations)
|
||||
}
|
||||
@@ -7,10 +7,7 @@ package org.jetbrains.compose.gradle
|
||||
|
||||
import org.gradle.internal.impldep.org.testng.Assert
|
||||
import org.gradle.testkit.runner.TaskOutcome
|
||||
import org.jetbrains.compose.desktop.application.internal.OS
|
||||
import org.jetbrains.compose.desktop.application.internal.currentArch
|
||||
import org.jetbrains.compose.desktop.application.internal.currentOS
|
||||
import org.jetbrains.compose.desktop.application.internal.currentTarget
|
||||
import org.jetbrains.compose.desktop.application.internal.*
|
||||
import org.jetbrains.compose.test.*
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assumptions
|
||||
@@ -159,6 +156,55 @@ class DesktopApplicationTest : GradlePluginTestBase() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMacSign() {
|
||||
Assumptions.assumeTrue(currentOS == OS.MacOS)
|
||||
|
||||
fun security(vararg args: Any): ProcessRunResult {
|
||||
val args = args.map {
|
||||
if (it is File) it.absolutePath else it.toString()
|
||||
}
|
||||
return runProcess(MacUtils.security, args)
|
||||
}
|
||||
|
||||
fun withNewDefaultKeychain(newKeychain: File, fn: () -> Unit) {
|
||||
val originalKeychain =
|
||||
security("default-keychain")
|
||||
.out
|
||||
.trim()
|
||||
.trim('"')
|
||||
|
||||
try {
|
||||
security("default-keychain", "-s", newKeychain)
|
||||
fn()
|
||||
} finally {
|
||||
security("default-keychain", "-s", originalKeychain)
|
||||
}
|
||||
}
|
||||
|
||||
with(testProject(TestProjects.macSign)) {
|
||||
val keychain = file("compose.test.keychain")
|
||||
val password = "compose.test"
|
||||
|
||||
withNewDefaultKeychain(keychain) {
|
||||
security("default-keychain", "-s", keychain)
|
||||
security("unlock-keychain", "-p", password, keychain)
|
||||
|
||||
gradle(":createDistributable").build().checks { check ->
|
||||
check.taskOutcome(":createDistributable", TaskOutcome.SUCCESS)
|
||||
val appDir = testWorkDir.resolve("build/compose/binaries/main/app/TestPackage.app/")
|
||||
val result = runProcess(MacUtils.codesign, args = listOf("--verify", "--verbose", appDir.absolutePath))
|
||||
val actualOutput = result.err.trim()
|
||||
val expectedOutput = """
|
||||
|${appDir.absolutePath}: valid on disk
|
||||
|${appDir.absolutePath}: satisfies its Designated Requirement
|
||||
""".trimMargin().trim()
|
||||
Assert.assertEquals(expectedOutput, actualOutput)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOptionsWithSpaces() {
|
||||
with(testProject(TestProjects.optionsWithSpaces)) {
|
||||
|
||||
@@ -12,6 +12,7 @@ object TestProjects {
|
||||
const val moduleClashCli = "application/moduleClashCli"
|
||||
const val javaLogger = "application/javaLogger"
|
||||
const val macOptions = "application/macOptions"
|
||||
const val macSign = "application/macSign"
|
||||
const val optionsWithSpaces = "application/optionsWithSpaces"
|
||||
const val unpackSkiko = "application/unpackSkiko"
|
||||
const val jsMpp = "misc/jsMpp"
|
||||
|
||||
@@ -16,11 +16,11 @@ fun File.modify(fn: (String) -> String) {
|
||||
fun File.checkExists(): File = apply {
|
||||
check(exists()) {
|
||||
buildString {
|
||||
appendln("Requested file does not exist: $absolutePath")
|
||||
appendLine("Requested file does not exist: $absolutePath")
|
||||
parentFile?.listFiles()?.let { siblingFiles ->
|
||||
appendln("Other files in the same directory: ${parentFile.absolutePath}")
|
||||
appendLine("Other files in the same directory: ${parentFile.absolutePath}")
|
||||
siblingFiles.forEach {
|
||||
appendln(" * ${it.name}")
|
||||
appendLine(" * ${it.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.test
|
||||
|
||||
import java.io.File
|
||||
|
||||
internal data class ProcessRunResult(val exitCode: Int, val out: String, val err: String)
|
||||
|
||||
internal fun runProcess(
|
||||
tool: File,
|
||||
args: Collection<String>,
|
||||
checkExitCodeIsNormal: Boolean = true
|
||||
): ProcessRunResult {
|
||||
val outFile = File.createTempFile("run-process-compose-tests-out.txt", null).apply { deleteOnExit() }
|
||||
val errFile = File.createTempFile("run-process-compose-tests-err.txt", null).apply { deleteOnExit() }
|
||||
return try {
|
||||
val cmd = arrayOf(tool.absolutePath, *args.toTypedArray())
|
||||
val process = ProcessBuilder().run {
|
||||
redirectError(errFile)
|
||||
redirectOutput(outFile)
|
||||
command(*cmd)
|
||||
start()
|
||||
}
|
||||
val exitCode = process.waitFor()
|
||||
if (checkExitCodeIsNormal) {
|
||||
check(exitCode == 0) {
|
||||
buildString {
|
||||
appendLine("Non-zero exit code: $exitCode")
|
||||
appendLine("Command: ${cmd.joinToString(", ")}")
|
||||
appendLine("Out:")
|
||||
outFile.forEachLine { line ->
|
||||
appendLine(" >$line")
|
||||
}
|
||||
appendLine("Err:")
|
||||
errFile.forEachLine { line ->
|
||||
appendLine(" >$line")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ProcessRunResult(exitCode = exitCode, out = outFile.readText(), err = errFile.readText())
|
||||
} finally {
|
||||
outFile.delete()
|
||||
errFile.delete()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||
|
||||
plugins {
|
||||
id "org.jetbrains.kotlin.jvm"
|
||||
id "org.jetbrains.compose"
|
||||
}
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url "https://maven.pkg.jetbrains.space/public/p/compose/dev"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib"
|
||||
implementation compose.desktop.currentOs
|
||||
}
|
||||
|
||||
compose.desktop {
|
||||
application {
|
||||
mainClass = "MainKt"
|
||||
nativeDistributions {
|
||||
packageName = "TestPackage"
|
||||
macOS {
|
||||
bundleID = "signing.test.package"
|
||||
|
||||
signing {
|
||||
sign.set(true)
|
||||
identity.set("Compose Test")
|
||||
keychain.set(project.file("compose.test.keychain").absolutePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,10 @@
|
||||
pluginManagement {
|
||||
plugins {
|
||||
id 'org.jetbrains.kotlin.jvm' version 'KOTLIN_VERSION_PLACEHOLDER'
|
||||
id 'org.jetbrains.compose' version 'COMPOSE_VERSION_PLACEHOLDER'
|
||||
}
|
||||
repositories {
|
||||
mavenLocal()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fun main() {
|
||||
}
|
||||
Reference in New Issue
Block a user