[MPP] Run pod install through env to prevent ProcessBuilder from missing
PATH modifications

^KT-60394
diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/CocoaPodsIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/CocoaPodsIT.kt
index a61c143..9fc29bb 100644
--- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/CocoaPodsIT.kt
+++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/CocoaPodsIT.kt
@@ -554,18 +554,18 @@
     @DisplayName("Cinterop commonization on")
     @GradleTest
     fun testCinteropCommonizationOn(gradleVersion: GradleVersion) {
-        testCinteropCommonizationExecutes(gradleVersion, buildArguments=arrayOf("-Pkotlin.mpp.enableCInteropCommonization=true"))
+        testCinteropCommonizationExecutes(gradleVersion, buildArguments = arrayOf("-Pkotlin.mpp.enableCInteropCommonization=true"))
     }
 
     @DisplayName("Cinterop commonization unspecified")
     @GradleTest
     fun testCinteropCommonizationUnspecified(gradleVersion: GradleVersion) {
-        testCinteropCommonizationExecutes(gradleVersion, buildArguments=emptyArray())
+        testCinteropCommonizationExecutes(gradleVersion, buildArguments = emptyArray())
     }
 
     private fun testCinteropCommonizationExecutes(
         gradleVersion: GradleVersion,
-        buildArguments: Array<String>
+        buildArguments: Array<String>,
     ) {
         nativeProjectWithCocoapodsAndIosAppPodFile(cocoapodsCommonizationProjectName, gradleVersion) {
             buildWithCocoapodsWrapper(":commonize", *buildArguments) {
@@ -875,18 +875,92 @@
         }
     }
 
+    private val maybeCocoaPodsIsNotInstalledError = "Possible reason: CocoaPods is not installed"
+    private val maybePodfileIsIncorrectError = "Please, check that podfile contains following lines in header"
+
+    @DisplayName("Pod install emits correct error when pod binary is not present in PATH")
+    @GradleTest
+    fun testPodInstallErrorWithoutCocoaPodsInPATH(gradleVersion: GradleVersion) {
+        val pathWithoutCocoapods = "/bin:/usr/bin"
+        nativeProjectWithCocoapodsAndIosAppPodFile(
+            gradleVersion = gradleVersion,
+            environmentVariables = EnvironmentalVariables(
+                mapOf("PATH" to pathWithoutCocoapods)
+            )
+        ) {
+            buildGradleKts.addCocoapodsBlock(
+                """
+                    podfile = project.file("ios-app/Podfile")
+                """.trimIndent()
+            )
+
+            buildAndFailWithCocoapodsWrapper(
+                podInstallTaskName,
+            ) {
+                assertOutputDoesNotContain(maybePodfileIsIncorrectError)
+                assertOutputContains(maybeCocoaPodsIsNotInstalledError)
+            }
+        }
+    }
+
+    @DisplayName("Pod install emits other errors when pod install runs, but fails later")
+    @GradleTest
+    fun testOtherPodInstallErrors(gradleVersion: GradleVersion) {
+        nativeProjectWithCocoapodsAndIosAppPodFile(
+            gradleVersion = gradleVersion
+        ) {
+            buildGradleKts.addCocoapodsBlock(
+                """
+                    podfile = project.file("ios-app/Podfile")
+                """.trimIndent()
+            )
+
+            projectPath.resolve("ios-app/Podfile").append(
+                """
+                    raise "Dead"
+                """.trimIndent()
+            )
+
+            buildAndFailWithCocoapodsWrapper(
+                podInstallTaskName,
+            ) {
+                assertOutputContains(maybePodfileIsIncorrectError)
+                assertOutputDoesNotContain(maybeCocoaPodsIsNotInstalledError)
+            }
+        }
+    }
+
+    private fun TestProject.buildAndFailWithCocoapodsWrapper(
+        vararg buildArguments: String,
+        assertions: BuildResult.() -> Unit = {},
+    ) = buildWithCocoapodsWrapperUsing { buildOptions ->
+        buildAndFail(
+            *buildArguments,
+            buildOptions = buildOptions,
+            assertions = assertions,
+        )
+    }
+
     private fun TestProject.buildWithCocoapodsWrapper(
         vararg buildArguments: String,
         assertions: BuildResult.() -> Unit = {},
+    ) = buildWithCocoapodsWrapperUsing { buildOptions ->
+        build(
+            *buildArguments,
+            buildOptions = buildOptions,
+            assertions = assertions,
+        )
+    }
+
+    private fun TestProject.buildWithCocoapodsWrapperUsing(
+        builder: TestProject.(BuildOptions) -> Unit,
     ) {
         val buildOptions = this.buildOptions.copy(
             nativeOptions = this.buildOptions.nativeOptions.copy(
                 cocoapodsGenerateWrapper = true
             )
         )
-        build(*buildArguments, buildOptions = buildOptions) {
-            assertions()
-        }
+        builder(buildOptions)
     }
 
     private fun TestProject.addPodToPodfile(iosAppLocation: String, pod: String) {
diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/CocoaPodsXcodeIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/CocoaPodsXcodeIT.kt
index 97e2e39..b5f41b7 100644
--- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/CocoaPodsXcodeIT.kt
+++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/native/CocoaPodsXcodeIT.kt
@@ -256,7 +256,7 @@
     build("$taskPrefix:$DUMMY_FRAMEWORK_TASK_NAME", buildOptions = buildOptions)
 
     runProcess(
-        cmd = listOf("pod", "install"),
+        cmd = listOf("env", "pod", "install"),
         environmentVariables = environmentVariables.environmentalVariables,
         workingDir = iosAppPath.toFile(),
     )
diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/testbase/cocoapodsTestHelpers.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/testbase/cocoapodsTestHelpers.kt
index ddd5356..adeb1e4 100644
--- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/testbase/cocoapodsTestHelpers.kt
+++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/testbase/cocoapodsTestHelpers.kt
@@ -196,13 +196,14 @@
     projectName: String = templateProjectName,
     gradleVersion: GradleVersion,
     buildOptions: BuildOptions = this.defaultBuildOptions,
+    environmentVariables: EnvironmentalVariables = EnvironmentalVariables(cocoaPodsEnvironmentVariables()),
     projectBlock: TestProject.() -> Unit = {},
 ) {
     nativeProject(
         projectName,
         gradleVersion,
         buildOptions = buildOptions,
-        environmentVariables = EnvironmentalVariables(cocoaPodsEnvironmentVariables())
+        environmentVariables = environmentVariables,
     ) {
         preparePodfile("ios-app", ImportMode.FRAMEWORKS)
         projectBlock()
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 d661038..78301f7 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
@@ -42,14 +42,14 @@
 
     @TaskAction
     open fun doPodInstall() {
-        val podInstallCommand = listOf("pod", "install")
+        // 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 = ::handleError,
-                   exceptionHandler = { e: IOException ->
-                       CocoapodsErrorHandlingUtil.handle(e, podInstallCommand)
-                   },
+                   errorHandler = { retCode, output, process -> sharedHandleError(podInstallCommand, retCode, output, process) },
                    processConfiguration = {
                        directory(workingDir.get())
                        // CocoaPods requires to be run with Unicode external encoding
@@ -63,17 +63,14 @@
         }
     }
 
-    abstract fun handleError(retCode: Int, error: String, process: Process): String?
-}
-
-private object CocoapodsErrorHandlingUtil {
-    fun handle(e: IOException, command: List<String>) {
-        if (e.message?.contains("No such file or directory") == true) {
-            val message = """ 
-               |'${command.take(2).joinToString(" ")}' command failed with an exception:
-               | ${e.message}
+    private fun sharedHandleError(podInstallCommand: List<String>, retCode: Int, error: String, process: Process): String? {
+        return if (error.contains("No such file or directory")) {
+            val command = podInstallCommand.joinToString(" ")
+            """ 
+               |'$command' command failed with an exception:
+               | $error
                |        
-               |        Full command: ${command.joinToString(" ")}
+               |        Full command: $command
                |        
                |        Possible reason: CocoaPods is not installed
                |        Please check that CocoaPods v1.10 or above is installed.
@@ -83,10 +80,10 @@
                |        To install CocoaPods execute 'sudo gem install cocoapods'
                |
             """.trimMargin()
-            throw IllegalStateException(message)
         } else {
-            throw e
+            handleError(retCode, error, process)
         }
     }
 
-}
+    abstract fun handleError(retCode: Int, error: String, process: Process): String?
+}
\ No newline at end of file
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 478a9e7..03cef4b 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
@@ -13,22 +13,11 @@
     command: List<String>,
     logger: Logger? = null,
     errorHandler: ((retCode: Int, output: String, process: Process) -> String?)? = null,
-    exceptionHandler: ((ex: IOException) -> Unit)? = null,
     processConfiguration: ProcessBuilder.() -> Unit = { }
 ): String {
-    var process: Process? = null
-    try {
-        process = ProcessBuilder(command)
-            .apply {
-                this.processConfiguration()
-            }.start()
-    } catch (e: IOException) {
-        if (exceptionHandler != null) exceptionHandler(e) else throw e
-    }
-
-    if (process == null) {
-        throw IllegalStateException("Failed to run command ${command.joinToString(" ")}")
-    }
+    val process = ProcessBuilder(command).apply {
+        this.processConfiguration()
+    }.start()
 
     var inputText = ""
     var errorText = ""