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 a61c143a602..9fc29bbbe9f 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 @@ class CocoaPodsIT : KGPBaseTest() { @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 + buildArguments: Array, ) { nativeProjectWithCocoapodsAndIosAppPodFile(cocoapodsCommonizationProjectName, gradleVersion) { buildWithCocoapodsWrapper(":commonize", *buildArguments) { @@ -875,18 +875,92 @@ class CocoaPodsIT : KGPBaseTest() { } } + 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 97e2e398635..b5f41b7e961 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 @@ private fun TestProject.manualPodInstall(taskPrefix: String, iosAppPath: Path) { 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 ddd5356f5ff..adeb1e45d61 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 @@ fun KGPBaseTest.nativeProjectWithCocoapodsAndIosAppPodFile( 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 d6610384e54..78301f7fa0f 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 @@ abstract class AbstractPodInstallTask : CocoapodsTask() { @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 class AbstractPodInstallTask : CocoapodsTask() { } } - abstract fun handleError(retCode: Int, error: String, process: Process): String? -} - -private object CocoapodsErrorHandlingUtil { - fun handle(e: IOException, command: List) { - 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, 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 @@ private object CocoapodsErrorHandlingUtil { | 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 478a9e73da8..03cef4be40d 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 @@ internal fun runCommand( command: List, 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 = ""