diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/common/output/outputUtils.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/common/output/outputUtils.kt index f3218d55753..495d156d9a7 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/common/output/outputUtils.kt +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/common/output/outputUtils.kt @@ -23,13 +23,24 @@ import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.cli.common.messages.OutputMessageUtil import java.io.File +import java.io.FileNotFoundException fun OutputFileCollection.writeAll(outputDir: File, report: ((file: OutputFile, sources: List, output: File) -> Unit)?) { for (file in asList()) { val sources = file.sourceFiles val output = File(outputDir, file.relativePath) report?.invoke(file, sources, output) - FileUtil.writeToFile(output, file.asByteArray()) + try { + FileUtil.writeToFile(output, file.asByteArray()) + } catch (e: FileNotFoundException) { + if (outputDir.isDirectory) { + // output directory exists + throw NoPermissionException("error while writing $output (Permission denied)", e) + } + // Failed to create directory, possibly due to lack of write permission or clash with existing file. + // In both cases, the directory will not exist, so for the sake of simplicity, we treat them same way. + throw e + } } } @@ -38,8 +49,16 @@ fun OutputFileCollection.writeAllTo(outputDir: File) { } fun OutputFileCollection.writeAll(outputDir: File, messageCollector: MessageCollector, reportOutputFiles: Boolean) { - if (!reportOutputFiles) writeAllTo(outputDir) - else writeAll(outputDir) { _, sources, output -> - messageCollector.report(CompilerMessageSeverity.OUTPUT, OutputMessageUtil.formatOutputMessage(sources, output)) + try { + if (!reportOutputFiles) writeAllTo(outputDir) + else writeAll(outputDir) { _, sources, output -> + messageCollector.report(CompilerMessageSeverity.OUTPUT, OutputMessageUtil.formatOutputMessage(sources, output)) + } + } catch (e: NoPermissionException) { + messageCollector.report(CompilerMessageSeverity.ERROR, e.message!!) + } catch (e: FileNotFoundException) { + messageCollector.report(CompilerMessageSeverity.ERROR, "directory not found: $outputDir") } } + +private class NoPermissionException(message: String, cause: Throwable?) : IllegalStateException(message, cause) diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/CompileEnvironmentUtil.java b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/CompileEnvironmentUtil.java index cce1bd287c7..00d5b021c4f 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/CompileEnvironmentUtil.java +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/CompileEnvironmentUtil.java @@ -24,6 +24,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.kotlin.backend.common.output.OutputFile; import org.jetbrains.kotlin.backend.common.output.OutputFileCollection; +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity; import org.jetbrains.kotlin.cli.common.messages.MessageCollector; import org.jetbrains.kotlin.cli.common.modules.ModuleChunk; import org.jetbrains.kotlin.cli.common.modules.ModuleXmlParser; @@ -104,16 +105,24 @@ public class CompileEnvironmentUtil { } public static void writeToJar( - File jarPath, boolean jarRuntime, boolean noReflect, boolean resetJarTimestamps, FqName mainClass, OutputFileCollection outputFiles + File jarPath, + boolean jarRuntime, + boolean noReflect, + boolean resetJarTimestamps, + FqName mainClass, + OutputFileCollection outputFiles, + MessageCollector messageCollector ) { FileOutputStream outputStream = null; try { + // we should try to create the output dir first + jarPath.getParentFile().mkdirs(); outputStream = new FileOutputStream(jarPath); doWriteToJar(outputFiles, outputStream, mainClass, jarRuntime, noReflect, resetJarTimestamps); outputStream.close(); } catch (FileNotFoundException e) { - throw new CompileEnvironmentException("Invalid jar path " + jarPath, e); + messageCollector.report(CompilerMessageSeverity.ERROR, "Invalid jar path: " + jarPath, null); } catch (IOException e) { throw ExceptionUtilsKt.rethrow(e); diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinToJVMBytecodeCompiler.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinToJVMBytecodeCompiler.kt index 1812715ec94..456077883ca 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinToJVMBytecodeCompiler.kt +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinToJVMBytecodeCompiler.kt @@ -88,7 +88,7 @@ object KotlinToJVMBytecodeCompiler { val includeRuntime = configuration.get(JVMConfigurationKeys.INCLUDE_RUNTIME, false) val noReflect = configuration.get(JVMConfigurationKeys.NO_REFLECT, false) val resetJarTimestamps = !configuration.get(JVMConfigurationKeys.NO_RESET_JAR_TIMESTAMPS, false) - CompileEnvironmentUtil.writeToJar(jarPath, includeRuntime, noReflect, resetJarTimestamps, mainClassFqName, outputFiles) + CompileEnvironmentUtil.writeToJar(jarPath, includeRuntime, noReflect, resetJarTimestamps, mainClassFqName, outputFiles, messageCollector) if (reportOutputFiles) { val message = OutputMessageUtil.formatOutputMessage(outputFiles.asList().flatMap { it.sourceFiles }.distinct(), jarPath) messageCollector.report(OUTPUT, message) diff --git a/compiler/testData/integration/smoke/destinationDirClashingWithExistingFile/test.compile.expected b/compiler/testData/integration/smoke/destinationDirClashingWithExistingFile/test.compile.expected new file mode 100644 index 00000000000..83965866efb --- /dev/null +++ b/compiler/testData/integration/smoke/destinationDirClashingWithExistingFile/test.compile.expected @@ -0,0 +1,4 @@ +ERR: +error: directory not found: [Temp]/clashingFile + +Return code: 1 diff --git a/compiler/testData/integration/smoke/destinationDirClashingWithExistingFile/test.kt b/compiler/testData/integration/smoke/destinationDirClashingWithExistingFile/test.kt new file mode 100644 index 00000000000..f7e70c7b871 --- /dev/null +++ b/compiler/testData/integration/smoke/destinationDirClashingWithExistingFile/test.kt @@ -0,0 +1 @@ +class Test \ No newline at end of file diff --git a/compiler/testData/integration/smoke/destinationDirDoesNotExist/test.compile.expected b/compiler/testData/integration/smoke/destinationDirDoesNotExist/test.compile.expected new file mode 100644 index 00000000000..a14ac74940f --- /dev/null +++ b/compiler/testData/integration/smoke/destinationDirDoesNotExist/test.compile.expected @@ -0,0 +1 @@ +Return code: 0 diff --git a/compiler/testData/integration/smoke/destinationDirDoesNotExist/test.kt b/compiler/testData/integration/smoke/destinationDirDoesNotExist/test.kt new file mode 100644 index 00000000000..f7e70c7b871 --- /dev/null +++ b/compiler/testData/integration/smoke/destinationDirDoesNotExist/test.kt @@ -0,0 +1 @@ +class Test \ No newline at end of file diff --git a/compiler/testData/integration/smoke/destinationDirNoPermission/test.compile.expected b/compiler/testData/integration/smoke/destinationDirNoPermission/test.compile.expected new file mode 100644 index 00000000000..8f834e437f3 --- /dev/null +++ b/compiler/testData/integration/smoke/destinationDirNoPermission/test.compile.expected @@ -0,0 +1,4 @@ +ERR: +error: error while writing [Temp]/noPermissionDir/Test.class (Permission denied) + +Return code: 1 diff --git a/compiler/testData/integration/smoke/destinationDirNoPermission/test.kt b/compiler/testData/integration/smoke/destinationDirNoPermission/test.kt new file mode 100644 index 00000000000..f7e70c7b871 --- /dev/null +++ b/compiler/testData/integration/smoke/destinationDirNoPermission/test.kt @@ -0,0 +1 @@ +class Test \ No newline at end of file diff --git a/compiler/testData/integration/smoke/destinationJarClashingWithExistingFile/test.compile.expected b/compiler/testData/integration/smoke/destinationJarClashingWithExistingFile/test.compile.expected new file mode 100644 index 00000000000..4ec6a8a2d49 --- /dev/null +++ b/compiler/testData/integration/smoke/destinationJarClashingWithExistingFile/test.compile.expected @@ -0,0 +1,4 @@ +ERR: +error: invalid jar path: [Temp]/clashingFile/test.jar + +Return code: 1 diff --git a/compiler/testData/integration/smoke/destinationJarClashingWithExistingFile/test.kt b/compiler/testData/integration/smoke/destinationJarClashingWithExistingFile/test.kt new file mode 100644 index 00000000000..f7e70c7b871 --- /dev/null +++ b/compiler/testData/integration/smoke/destinationJarClashingWithExistingFile/test.kt @@ -0,0 +1 @@ +class Test \ No newline at end of file diff --git a/compiler/testData/integration/smoke/destinationJarNoPermission/test.compile.expected b/compiler/testData/integration/smoke/destinationJarNoPermission/test.compile.expected new file mode 100644 index 00000000000..9d8db77f244 --- /dev/null +++ b/compiler/testData/integration/smoke/destinationJarNoPermission/test.compile.expected @@ -0,0 +1,4 @@ +ERR: +error: invalid jar path: [Temp]/noPermissionDir/test.jar + +Return code: 1 diff --git a/compiler/testData/integration/smoke/destinationJarNoPermission/test.kt b/compiler/testData/integration/smoke/destinationJarNoPermission/test.kt new file mode 100644 index 00000000000..fb7337ede5b --- /dev/null +++ b/compiler/testData/integration/smoke/destinationJarNoPermission/test.kt @@ -0,0 +1 @@ +class A \ No newline at end of file diff --git a/compiler/tests/org/jetbrains/kotlin/integration/CompilerSmokeTest.java b/compiler/tests/org/jetbrains/kotlin/integration/CompilerSmokeTest.java index 1fb05ee2b88..c15ceccc56d 100644 --- a/compiler/tests/org/jetbrains/kotlin/integration/CompilerSmokeTest.java +++ b/compiler/tests/org/jetbrains/kotlin/integration/CompilerSmokeTest.java @@ -201,4 +201,50 @@ public class CompilerSmokeTest extends CompilerSmokeTestBase { runCompiler("noReflect.compile", "-include-runtime", "-no-reflect", "noReflect.kt", "-d", jar)); run("noReflect.run", "-cp", jar, "noReflect.NoReflectKt"); } + + // related to KT-14772, destination is not a jar, and clashing with an existing file + public void testDestinationDirClashingWithExistingFile() throws Exception { + String outputDir = tmpdir.getAbsolutePath() + File.separator + "clashingFile"; + File file = new File(outputDir); + file.createNewFile(); + runCompiler("test.compile", "test.kt", "-d", outputDir); + } + + // related to KT-18184, destination is not a jar, and permission denied + public void testDestinationDirNoPermission() throws Exception { + String outputDir = tmpdir.getAbsolutePath() + File.separator + "noPermissionDir"; + File file = new File(outputDir, "Test.class"); + file.getParentFile().mkdirs(); + file.createNewFile(); + // file.getParentFile().setReadOnly(); // won't work on Windows + file.setReadOnly(); // So we use this one as an alternative + runCompiler("test.compile", "test.kt", "-d", outputDir); + } + + // related to KT-18184, destination is a jar, and output directory does not exist, we should try to create + // output directory for it. + public void testDestinationDirDoesNotExist() throws Exception { + String jar = tmpdir.getAbsolutePath() + File.separator + "nonExistingDir" + File.separator + "test.jar"; + runCompiler("test.compile", "test.kt", "-d", jar); + } + + // related to KT-18184, destination is a jar, and output directory does not exist, and failed to created + // output directory due to clash with existing file. + public void testDestinationJarClashingWithExistingFile() throws Exception { + String jar = tmpdir.getAbsolutePath() + File.separator + "clashingFile" + File.separator + "test.jar"; + File file = new File(jar); + file.getParentFile().createNewFile(); + runCompiler("test.compile", "test.kt", "-d", jar); + } + + // related to KT-18184, Destination is a jar, and output directory exist, and permission denied to write jar. + public void testDestinationJarNoPermission() throws Exception { + String outputDir = tmpdir.getAbsolutePath() + File.separator + "noPermissionDir"; + File jar = new File(outputDir, "test.jar"); + jar.getParentFile().mkdirs(); + jar.createNewFile(); + // jar.getParentFile().setReadOnly(); // won't work on Windows + jar.setReadOnly(); // So we use this one as an alternative + runCompiler("test.compile", "test.kt", "-d", jar.getCanonicalPath()); + } }