From 4cdb9301ed7e4bb866318cb0038b7432e67ec46f Mon Sep 17 00:00:00 2001 From: Andrey Yastrebov Date: Mon, 27 Nov 2023 14:23:30 +0100 Subject: [PATCH] KT-57650 run pod install --repo-update if repo is out of date --- .../cocoapods/tasks/AbstractPodInstallTask.kt | 40 +++++--- .../kotlin/gradle/utils/ProcessUtils.kt | 94 ++++++++++++++++--- 2 files changed, 104 insertions(+), 30 deletions(-) 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 a18631da6cc..c6161daa78c 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 @@ -11,10 +11,10 @@ import org.gradle.api.provider.Property import org.gradle.api.provider.Provider import org.gradle.api.tasks.* import org.gradle.work.DisableCachingByDefault +import org.jetbrains.kotlin.gradle.utils.CommandFallback import org.jetbrains.kotlin.gradle.utils.onlyIfCompat -import org.jetbrains.kotlin.gradle.utils.runCommand +import org.jetbrains.kotlin.gradle.utils.runCommandWithFallback import java.io.File -import java.io.IOException /** * The task takes the path to the Podfile and calls `pod install` @@ -45,19 +45,7 @@ abstract class AbstractPodInstallTask : CocoapodsTask() { @TaskAction open fun doPodInstall() { - // env is used here to work around the JVM PATH caching when spawning a child process with custom environment, i.e. LC_ALL - // The caching causes the ProcessBuilder to ignore changes in the PATH that may occur on incremental runs of the Gradle daemon - // KT-60394 - val podInstallCommand = listOf("env", "pod", "install") - - runCommand(podInstallCommand, - logger, - errorHandler = { retCode, output, process -> sharedHandleError(podInstallCommand, retCode, output, process) }, - processConfiguration = { - directory(workingDir.get()) - // CocoaPods requires to be run with Unicode external encoding - environment().putIfAbsent("LC_ALL", "en_US.UTF-8") - }) + runPodInstall(false) with(podsXcodeProjDirProvider.get()) { check(exists() && isDirectory) { @@ -66,6 +54,28 @@ abstract class AbstractPodInstallTask : CocoapodsTask() { } } + private fun runPodInstall(updateRepo: Boolean): String { + // env is used here to work around the JVM PATH caching when spawning a child process with custom environment, i.e. LC_ALL + // The caching causes the ProcessBuilder to ignore changes in the PATH that may occur on incremental runs of the Gradle daemon + // KT-60394 + val podInstallCommand = listOfNotNull("env", "pod", "install", if (updateRepo) "--repo-update" else null) + + return runCommandWithFallback(podInstallCommand, + logger, + fallback = { retCode, output, process -> + if (output.contains("out-of-date source repos which you can update with `pod repo update` or with `pod install --repo-update`") && updateRepo.not()) { + CommandFallback.Action(runPodInstall(true)) + } else { + CommandFallback.Error(sharedHandleError(podInstallCommand, retCode, output, process)) + } + }, + 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, retCode: Int, error: String, process: Process): String? { return if (error.contains("No such file or directory")) { val command = podInstallCommand.joinToString(" ") 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 03cef4be40d..f85dd3925b4 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,15 +6,80 @@ package org.jetbrains.kotlin.gradle.utils import org.gradle.api.logging.Logger -import java.io.IOException import kotlin.concurrent.thread +/** + * 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, logger: Logger? = null, errorHandler: ((retCode: Int, output: String, process: Process) -> String?)? = null, - processConfiguration: ProcessBuilder.() -> Unit = { } + processConfiguration: ProcessBuilder.() -> Unit = { }, ): String { + val runResult = assembleAndRunProcess(command, logger, processConfiguration) + check(runResult.retCode == 0) { + errorHandler?.invoke(runResult.retCode, runResult.output, runResult.process) ?: createErrorMessage(command, runResult) + } + + return runResult.inputText +} + +/** + * Sealed class representing the fallback behavior for a command. + */ +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, + logger: Logger? = null, + fallback: (retCode: Int, output: String, process: Process) -> CommandFallback, + processConfiguration: ProcessBuilder.() -> Unit = { }, +): String { + val runResult = assembleAndRunProcess(command, logger, processConfiguration) + return if (runResult.retCode != 0) { + when (val fallbackOption = fallback(runResult.retCode, runResult.output, runResult.process)) { + is CommandFallback.Action -> fallbackOption.fallback + is CommandFallback.Error -> error(fallbackOption.error ?: createErrorMessage(command, runResult)) + } + } else { + runResult.inputText + } +} + +private data class RunProcessResult( + val inputText: String, + val errorText: String, + val retCode: Int, + val process: Process, +) { + val output: String get() = inputText.ifBlank { errorText } +} + +private fun assembleAndRunProcess( + command: List, + logger: Logger? = null, + processConfiguration: ProcessBuilder.() -> Unit = { }, +): RunProcessResult { + val process = ProcessBuilder(command).apply { this.processConfiguration() }.start() @@ -46,17 +111,16 @@ internal fun runCommand( """.trimMargin() ) - check(retCode == 0) { - errorHandler?.invoke(retCode, inputText.ifBlank { errorText }, process) - ?: """ - |Executing of '${command.joinToString(" ")}' failed with code $retCode and message: - | - |$inputText - | - |$errorText - | - """.trimMargin() - } - - return inputText + return RunProcessResult(inputText, errorText, retCode, process) } + +private fun createErrorMessage(command: List, runResult: RunProcessResult): String { + return """ + |Executing of '${command.joinToString(" ")}' failed with code ${runResult.retCode} and message: + | + |${runResult.inputText} + | + |${runResult.errorText} + | + """.trimMargin() +} \ No newline at end of file