WIP rework how we run processes in KGP
^KT-66517
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/AbstractPodInstallTask.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/AbstractPodInstallTask.kt
index 03803d2..78c150f 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/AbstractPodInstallTask.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/AbstractPodInstallTask.kt
@@ -17,6 +17,7 @@
import org.jetbrains.kotlin.gradle.utils.runCommand
import org.jetbrains.kotlin.gradle.utils.runCommandWithFallback
import java.io.File
+import java.nio.file.Files
/**
* The task takes the path to the Podfile and calls `pod install`
@@ -52,7 +53,7 @@
@TaskAction
open fun doPodInstall() {
- runPodInstall(false)
+ runPodInstall()
with(podsXcodeProjDirProvider.get()) {
check(exists() && isDirectory) {
@@ -63,28 +64,66 @@
private fun podExecutable(): String {
return when (val podPath = podExecutablePath.orNull?.asFile) {
- is File -> podPath.absolutePath.ifBlank { runWhichPod() }
- else -> runWhichPod()
+ is File -> podPath.absolutePath.ifBlank { getPodExecutablePath() }
+ else -> getPodExecutablePath()
}
}
- private fun runWhichPod(): String {
- val checkPodCommand = listOf("which", "pod")
- val output = runCommand(checkPodCommand, logger, { result ->
- if (result.retCode == 1) {
- missingPodsError()
- } else {
- sharedHandleError(checkPodCommand, result)
+ private fun getPodExecutablePath(): String {
+ val whichPodCommand = listOf("/usr/bin/which", "pod")
+ val output = mutableListOf<String>()
+ runCommand(
+ whichPodCommand,
+ logger,
+ onStdOutLine = { output.add(it) },
+ captureResult = CaptureCommandResult(
+ temporaryDirectory = Files.createTempDirectory("AbstractPodInstallTask").toFile().apply { deleteOnExit() },
+ onResult = { result ->
+ if (result.retCode != 0) {
+
+ }
+ }
+ ) { result ->
+// if (result.retCode == 1) {
+// missingPodsError()
+// } else {
+// sharedHandleError(checkPodCommand, result)
+// }
}
- })
-
- return output.removingTrailingNewline().ifBlank {
- throw IllegalStateException(missingPodsError())
- }
+ )
+ if (output.isEmpty()) throw IllegalStateException(missingPodsError())
+ return output[0]
}
- private fun runPodInstall(updateRepo: Boolean): String {
- val podInstallCommand = listOfNotNull(podExecutable(), "install", if (updateRepo) "--repo-update" else null)
+ private fun runPodInstall() {
+ val podInstallCommand = listOf(podExecutable(), "install")
+
+ var isRepoOutOfDate = false
+ val result = runPodInstallCommand(
+ podInstallCommand,
+ onStdOutLine = {
+ if (it.contains("out-of-date source repos which you can update with `pod repo update` or with `pod install --repo-update`")) {
+ isRepoOutOfDate = true
+ }
+ },
+ errorOnNonZeroExitCode = false
+ )
+
+ if (result.returnCode != 0) {
+ if (isRepoOutOfDate) {
+ logger.info("Retrying \"pod install\" with --repo-update")
+ var errorMessagePrefix =
+ runPodInstallCommand(
+ podInstallCommand + listOf("--repo-update"),
+ logger,
+ onStdOutLine = { line ->
+
+ }
+ )
+ } else {
+
+ }
+ }
return runCommandWithFallback(podInstallCommand,
logger,
@@ -96,13 +135,25 @@
CommandFallback.Error(sharedHandleError(podInstallCommand, result))
}
},
- processConfiguration = {
- directory(workingDir.get())
- // CocoaPods requires to be run with Unicode external encoding
- environment().putIfAbsent("LC_ALL", "en_US.UTF-8")
- })
+ )
}
+ private fun runPodInstallCommand(
+ command: List<String>,
+ onStdOutLine: (String) -> Unit,
+ errorOnNonZeroExitCode: Boolean,
+ ): RunProcessResult = runCommand(
+ command,
+ logger,
+ onStdOutLine = onStdOutLine,
+ errorOnNonZeroExitCode = errorOnNonZeroExitCode,
+ processConfiguration = {
+ directory(workingDir.get())
+ // CocoaPods requires to be run with Unicode external encoding
+ environment().putIfAbsent("LC_ALL", "en_US.UTF-8")
+ }
+ )
+
private fun sharedHandleError(podInstallCommand: List<String>, result: RunProcessResult): String? {
val command = podInstallCommand.joinToString(" ")
val output = result.stdErr.ifBlank { result.stdOut }
@@ -111,7 +162,7 @@
|'$command' command failed with an exception:
| stdErr: ${result.stdErr}
| stdOut: ${result.stdOut}
- | exitCode: ${result.retCode}
+ | exitCode: ${result.returnCode}
|
""".trimMargin()
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodBuildTask.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodBuildTask.kt
index a5f9835..d25ce13 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodBuildTask.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodBuildTask.kt
@@ -87,9 +87,13 @@
"-configuration", podBuildSettings.configuration,
)
- runCommand(podXcodeBuildCommand, logger) {
- directory(podsXcodeProjDir.asFile.parentFile)
- environment() // workaround for https://github.com/gradle/gradle/issues/27346
- }
+ runCommand(
+ podXcodeBuildCommand,
+ logger,
+ processConfiguration = {
+ directory(podsXcodeProjDir.asFile.parentFile)
+ environment() // workaround for https://github.com/gradle/gradle/issues/27346
+ }
+ )
}
}
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodInstallSyntheticTask.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodInstallSyntheticTask.kt
index 61a07b2..675e8a3 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodInstallSyntheticTask.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodInstallSyntheticTask.kt
@@ -45,7 +45,7 @@
override fun handleError(result: RunProcessResult): String? {
var message = """
- |'pod install' command on the synthetic project failed with return code: ${result.retCode}
+ |'pod install' command on the synthetic project failed with return code: ${result.returnCode}
|
| Error: ${result.stdErr.lines().filter { it.contains("[!]") }.joinToString("\n")}
|
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodInstallTask.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodInstallTask.kt
index 12eba64..ae19c9c 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodInstallTask.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodInstallTask.kt
@@ -42,7 +42,7 @@
val cocoapodsMessages = pods.get().map { MissingCocoapodsMessage(it).missingMessage }
return listOfNotNull(
- "'pod install' command failed with code ${result.retCode}.",
+ "'pod install' command failed with code ${result.returnCode}.",
"Error message:",
result.stdErr.lines().filter { it.isNotBlank() }.joinToString("\n"),
"""
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodSetupBuildTask.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodSetupBuildTask.kt
index d7c0870..bf2789d 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodSetupBuildTask.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/native/cocoapods/tasks/PodSetupBuildTask.kt
@@ -51,7 +51,13 @@
"-sdk", appleTarget.get().sdk,
)
- val outputText = runCommand(buildSettingsReceivingCommand, logger) { directory(podsXcodeProjDir.parentFile) }
+ val outputText = runCommand(
+ buildSettingsReceivingCommand,
+ logger,
+ processConfiguration = {
+ directory(podsXcodeProjDir.parentFile)
+ }
+ )
val buildSettingsProperties = PodBuildSettingsProperties.readSettingsFromReader(outputText.reader())
buildSettingsFile.getFile().let { bsf ->
diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/ProcessUtils.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/ProcessUtils.kt
index 5bdf379..195d1c8 100644
--- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/ProcessUtils.kt
+++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/utils/ProcessUtils.kt
@@ -6,102 +6,158 @@
package org.jetbrains.kotlin.gradle.utils
import org.gradle.api.logging.Logger
+import java.io.File
+import java.nio.file.Files
+import java.util.LinkedList
+import java.util.concurrent.LinkedBlockingQueue
import kotlin.concurrent.thread
-/**
- * Represents the result of running a process.
- *
- * @property stdOut The standard output of the process.
- * @property stdErr The standard error of the process.
- * @property retCode The return code of the process.
- * @property process The underlying `Process` object.
- */
-data class RunProcessResult(
- val stdOut: String,
- val stdErr: String,
- val retCode: Int,
+
+class RunProcessResult(
+ var output: File,
+ val returnCode: Int,
val process: Process,
-)
+ private val command: List<String>,
+) {
+ /**
+ * Write the full message to the logger, but trim the exception to prevent KT-66517
+ */
+ fun errorOnNonZeroExitCode(
+ headerMessage: String?,
+ logger: Logger,
+ ) {
+ if (returnCode == 0) return
+ val errorMessage = buildString {
+ headerMessage?.let {
+ logger.error(it)
+ appendLine(it)
+ }
-/**
- * Executes a command and returns the input text.
- *
- * @param command the command and its arguments to be executed as a list of strings.
- * @param logger an optional logger to log information about the command execution.
- * @param errorHandler (Optional) A function that handles any errors that occur during the command execution.
- * @param processConfiguration a function to configure the process before execution.
- * @return The input text of the executed command.
- */
-internal fun runCommand(
- command: List<String>,
- logger: Logger? = null,
- errorHandler: ((result: RunProcessResult) -> String?)? = null,
- processConfiguration: ProcessBuilder.() -> Unit = { },
-): String {
- val runResult = assembleAndRunProcess(command, logger, processConfiguration)
- check(runResult.retCode == 0) {
- errorHandler?.invoke(runResult) ?: createErrorMessage(command, runResult)
+ val commandFailedMessage = "Executing of '${command.joinToString(" ")}' failed with code ${returnCode} and message:\n"
+ logger.error(commandFailedMessage)
+ appendLine(commandFailedMessage)
+
+ val outputLimit = 100
+ var hasOverflown = false
+ val buffer = LinkedList<String>()
+ output.reader().forEachLine { line ->
+ logger.error(line)
+ buffer.addLast(line)
+ if (buffer.size > outputLimit) {
+ hasOverflown = true
+ buffer.removeFirst()
+ }
+ }
+ if (hasOverflown) {
+ appendLine("... last ${outputLimit} lines shown, see error log for the full output ...")
+ }
+ buffer.forEach { appendLine(it) }
+ }
+ error(errorMessage)
}
-
- return runResult.stdOut
}
-/**
- * Sealed class representing the fallback behavior for a command.
- */
+internal fun runCommand(
+ command: List<String>,
+ logger: Logger,
+ processConfiguration: ProcessBuilder.() -> Unit = { },
+ onStdOutLine: ((line: String) -> Unit)? = null,
+ onStdErrLine: ((line: String) -> Unit)? = null,
+ errorOnNonZeroExitCode: Boolean = true,
+): RunProcessResult {
+ val result = assembleAndRunProcess(command, logger, processConfiguration)
+ if (errorOnNonZeroExitCode && result.returnCode != 0) {
+ result.errorOnNonZeroExitCode(
+ headerMessage = null,
+ logger = logger,
+ )
+ }
+ return result
+}
+
sealed class CommandFallback {
data class Action(val fallback: String) : CommandFallback()
data class Error(val error: String?) : CommandFallback()
}
-/**
- * Executes the specified command with fallback behavior in case of non-zero return code.
- *
- * @param command the command and its arguments to be executed as a list of strings.
- * @param logger an optional logger to log information about the command execution.
- * @param fallback a function that provides the fallback behavior. It takes the return code, output, and process as parameters and returns a [CommandFallback] object.
- * @param processConfiguration a function to configure the process before execution.
- * @return the output of the command if the return code is 0, otherwise the fallback action or error.
- */
internal fun runCommandWithFallback(
command: List<String>,
logger: Logger? = null,
fallback: (result: RunProcessResult) -> CommandFallback,
processConfiguration: ProcessBuilder.() -> Unit = { },
-): String {
+) {
val runResult = assembleAndRunProcess(command, logger, processConfiguration)
- return if (runResult.retCode != 0) {
+ if (runResult.returnCode != 0) {
when (val fallbackOption = fallback(runResult)) {
is CommandFallback.Action -> fallbackOption.fallback
is CommandFallback.Error -> error(fallbackOption.error ?: createErrorMessage(command, runResult))
}
- } else {
- runResult.stdOut
}
}
+private enum class StreamMessage {
+ STDOUT,
+ STDERR,
+ STDOUT_EOF,
+ STDERR_EOF
+}
+
private fun assembleAndRunProcess(
command: List<String>,
logger: Logger? = null,
processConfiguration: ProcessBuilder.() -> Unit = { },
+ onStdOutLine: (line: String) -> Unit = {},
+ onStdErrLine: (line: String) -> Unit = {},
): RunProcessResult {
-
val process = ProcessBuilder(command).apply {
this.processConfiguration()
}.start()
+ val temporaryDirectory = Files.createTempDirectory("${process.pid()}").toFile().apply { deleteOnExit() }
- var inputText = ""
- var errorText = ""
+ logger?.info("Information about \"${command.joinToString(" ")}\" call:\n")
+ val outputFile = temporaryDirectory.resolve("output")
+ val queue = LinkedBlockingQueue<Pair<String, StreamMessage>>()
val inputThread = thread {
- inputText = process.inputStream.use {
- it.reader().readText()
+ process.inputStream.use {
+ it.reader().forEachLine { line ->
+ queue.put(Pair(line, StreamMessage.STDOUT))
+ }
}
+ queue.put(Pair("", StreamMessage.STDOUT_EOF))
+ }
+ val errorThread = thread {
+ process.errorStream.use {
+ it.reader().forEachLine { line ->
+ queue.put(Pair(line, StreamMessage.STDERR))
+ }
+ }
+ queue.put(Pair("", StreamMessage.STDERR_EOF))
}
- val errorThread = thread {
- errorText = process.errorStream.use {
- it.reader().readText()
+ var continueReadingStdout = true
+ var continueReadingStderr = true
+ outputFile.writer().use { writer ->
+ while (continueReadingStdout || continueReadingStderr) {
+ val (line, state) = queue.take()
+ when (state) {
+ StreamMessage.STDOUT_EOF -> {
+ continueReadingStdout = false
+ }
+ StreamMessage.STDERR_EOF -> {
+ continueReadingStderr = false
+ }
+ StreamMessage.STDOUT -> {
+ writer.appendLine(line)
+ onStdOutLine(line)
+ logger?.info(line)
+ }
+ StreamMessage.STDERR -> {
+ writer.appendLine(line)
+ onStdErrLine(line)
+ logger?.error(line)
+ }
+ }
}
}
@@ -109,24 +165,11 @@
errorThread.join()
val retCode = process.waitFor()
- logger?.info(
- """
- |Information about "${command.joinToString(" ")}" call:
- |
- |${inputText}
- """.trimMargin()
+
+ return RunProcessResult(
+ outputFile,
+ retCode,
+ process,
+ command,
)
-
- return RunProcessResult(inputText, errorText, retCode, process)
-}
-
-private fun createErrorMessage(command: List<String>, runResult: RunProcessResult): String {
- return """
- |Executing of '${command.joinToString(" ")}' failed with code ${runResult.retCode} and message:
- |
- |${runResult.stdOut}
- |
- |${runResult.stdErr}
- |
- """.trimMargin()
}
\ No newline at end of file