diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/common/CLITool.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/common/CLITool.kt
index 42382acbcca..574b362db5e 100644
--- a/compiler/cli/src/org/jetbrains/kotlin/cli/common/CLITool.kt
+++ b/compiler/cli/src/org/jetbrains/kotlin/cli/common/CLITool.kt
@@ -58,8 +58,8 @@ abstract class CLITool {
val collector = PrintingMessageCollector(errStream, messageRenderer, arguments.verbose)
try {
- if (PlainTextMessageRenderer.COLOR_ENABLED) {
- AnsiConsole.systemInstall()
+ if (messageRenderer is PlainTextMessageRenderer) {
+ messageRenderer.enableColorsIfNeeded()
}
errStream.print(messageRenderer.renderPreamble())
@@ -80,8 +80,8 @@ abstract class CLITool {
} finally {
errStream.print(messageRenderer.renderConclusion())
- if (PlainTextMessageRenderer.COLOR_ENABLED) {
- AnsiConsole.systemUninstall()
+ if (messageRenderer is PlainTextMessageRenderer) {
+ messageRenderer.disableColorsIfNeeded()
}
}
}
diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/common/messages/PlainTextMessageRenderer.java b/compiler/cli/src/org/jetbrains/kotlin/cli/common/messages/PlainTextMessageRenderer.java
index bac5670ae91..250f57a2202 100644
--- a/compiler/cli/src/org/jetbrains/kotlin/cli/common/messages/PlainTextMessageRenderer.java
+++ b/compiler/cli/src/org/jetbrains/kotlin/cli/common/messages/PlainTextMessageRenderer.java
@@ -18,6 +18,7 @@ package org.jetbrains.kotlin.cli.common.messages;
import kotlin.text.StringsKt;
import org.fusesource.jansi.Ansi;
+import org.fusesource.jansi.AnsiConsole;
import org.fusesource.jansi.internal.CLibrary;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -31,21 +32,19 @@ import java.util.Set;
import static org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.*;
public abstract class PlainTextMessageRenderer implements MessageRenderer {
- public static final boolean COLOR_ENABLED;
+ private static final boolean IS_STDERR_A_TTY;
static {
- boolean colorEnabled = false;
+ boolean isStderrATty = false;
// TODO: investigate why ANSI escape codes on Windows only work in REPL for some reason
if (!PropertiesKt.isWindows() && "true".equals(CompilerSystemProperties.KOTLIN_COLORS_ENABLED_PROPERTY.getValue())) {
try {
- // AnsiConsole doesn't check isatty() for stderr (see https://github.com/fusesource/jansi/pull/35).
- colorEnabled = CLibrary.isatty(CLibrary.STDERR_FILENO) != 0;
+ isStderrATty = CLibrary.isatty(CLibrary.STDERR_FILENO) != 0;
}
- catch (UnsatisfiedLinkError e) {
- colorEnabled = false;
+ catch (UnsatisfiedLinkError ignored) {
}
}
- COLOR_ENABLED = colorEnabled;
+ IS_STDERR_A_TTY = isStderrATty;
}
private static final String LINE_SEPARATOR = System.lineSeparator();
@@ -55,11 +54,10 @@ public abstract class PlainTextMessageRenderer implements MessageRenderer {
private final boolean colorEnabled;
public PlainTextMessageRenderer() {
- this(COLOR_ENABLED);
+ this(IS_STDERR_A_TTY);
}
- // This constructor is not used in this project
- // but it can be useful in a compilation server to still be able to generate colored output
+ // This constructor can be used in a compilation server to still be able to generate colored output, even if stderr is not a TTY.
@SuppressWarnings("WeakerAccess")
public PlainTextMessageRenderer(boolean colorEnabled) {
this.colorEnabled = colorEnabled;
@@ -180,4 +178,16 @@ public abstract class PlainTextMessageRenderer implements MessageRenderer {
public String renderConclusion() {
return "";
}
+
+ public void enableColorsIfNeeded() {
+ if (colorEnabled) {
+ AnsiConsole.systemInstall();
+ }
+ }
+
+ public void disableColorsIfNeeded() {
+ if (colorEnabled) {
+ AnsiConsole.systemUninstall();
+ }
+ }
}
diff --git a/compiler/tests/org/jetbrains/kotlin/integration/ColorsTest.kt b/compiler/tests/org/jetbrains/kotlin/integration/ColorsTest.kt
new file mode 100644
index 00000000000..4c6b062c0b7
--- /dev/null
+++ b/compiler/tests/org/jetbrains/kotlin/integration/ColorsTest.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+package org.jetbrains.kotlin.integration
+
+import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
+import org.jetbrains.kotlin.cli.common.config.addKotlinSourceRoot
+import org.jetbrains.kotlin.cli.common.isWindows
+import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation
+import org.jetbrains.kotlin.cli.common.messages.MessageRenderer
+import org.jetbrains.kotlin.cli.common.messages.PlainTextMessageRenderer
+import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector
+import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
+import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
+import org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler
+import org.jetbrains.kotlin.config.JVMConfigurationKeys
+import org.jetbrains.kotlin.test.ConfigurationKind
+import org.jetbrains.kotlin.test.KotlinTestUtils
+import org.jetbrains.kotlin.test.TestCaseWithTmpdir
+import org.jetbrains.kotlin.test.TestJdkKind
+import java.io.ByteArrayOutputStream
+import java.io.File
+import java.io.PrintStream
+
+// This test checks that the compiler outputs ANSI escape codes enabling colors, bold text, etc when outputting warnings/errors.
+// By default, the compiler does so on non-Windows platforms only if the output is a terminal (isatty returns 1 for stderr),
+// but you can also pass a custom instance of PlainTextMessageRenderer and override that parameter.
+class ColorsTest : TestCaseWithTmpdir() {
+ fun testColorsDisabledByDefault() {
+ doTest(MessageRenderer.WITHOUT_PATHS, false)
+ }
+
+ fun testColorsDisabledWithDefaultConstructor() {
+ doTest(CustomRenderer(), false)
+ }
+
+ fun testColorsEnabledCustom() {
+ doTest(CustomRenderer(true), true)
+ }
+
+ fun testColorsDisabledCustom() {
+ doTest(CustomRenderer(false), false)
+ }
+
+ private fun doTest(renderer: MessageRenderer, colorsShouldBeEnabled: Boolean) {
+ // Colors are currently disabled on Windows.
+ if (isWindows) return
+
+ // Create a source file which yields exactly one error when being compiled.
+ File(tmpdir, "source.kt").writeText("val result: String = 42")
+
+ val log = ByteArrayOutputStream()
+
+ val configuration = KotlinTestUtils.newConfiguration(ConfigurationKind.ALL, TestJdkKind.FULL_JDK).apply {
+ put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, PrintingMessageCollector(PrintStream(log), renderer, false))
+ addKotlinSourceRoot(tmpdir.absolutePath)
+ put(JVMConfigurationKeys.OUTPUT_DIRECTORY, tmpdir)
+ }
+
+ val environment = KotlinCoreEnvironment.createForTests(testRootDisposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES)
+
+ // Compilation should return false, because there's one error.
+ assertFalse(KotlinToJVMBytecodeCompiler.compileBunchOfSources(environment))
+
+ val firstBytes = log.toByteArray().take(7)
+ val logStartsWithColors = firstBytes.joinToString(" ") { it.toString(16) } == "1b 5b 31 3b 33 31 6d"
+ val logStartsWithWordError = firstBytes == "error: ".map { it.code.toByte() }
+
+ when {
+ logStartsWithColors -> if (!colorsShouldBeEnabled) {
+ fail("There should be no colors in the compiler log, but it seems that there are.")
+ }
+ logStartsWithWordError -> if (colorsShouldBeEnabled) {
+ fail("There should be colors in the compiler log, but there aren't any.")
+ }
+ else -> {
+ fail("The compiler log starts with something unexpected. Possibly the test needs to be updated.")
+ }
+ }
+ }
+
+ private class CustomRenderer : PlainTextMessageRenderer {
+ constructor() : super()
+ constructor(colorsShouldBeEnabled: Boolean) : super(colorsShouldBeEnabled)
+
+ override fun getName(): String = "Test"
+
+ // Do not output paths, so that the log will start with the word "error", so that we can investigate just the first few bytes
+ // of the log and see if it's the color enabling ANSI codes, or the word "error".
+ override fun getPath(location: CompilerMessageSourceLocation): String? = null
+ }
+}