From 8d173a031b6b7181f5a77165f2471a06a05cde96 Mon Sep 17 00:00:00 2001 From: Sam Clegg Date: Mon, 6 Oct 2025 12:02:02 -0700 Subject: [PATCH] Remove windows-specific console coloring Windows 10 and above support ANSI coloring and there should be very few emscripten users on older version of windows at this point (if any?). The downside is that windows 7 will no longer have nice colored error messages, but the upside is that we can cleanup and refactor this code now (see #25495). --- ChangeLog.md | 2 + test/test_other.py | 15 ++----- tools/cmdline.py | 2 - tools/colored_logger.py | 98 +++++++---------------------------------- tools/diagnostics.py | 92 +++----------------------------------- 5 files changed, 27 insertions(+), 182 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index d22df7fda65b3..de4c9bf79556f 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -20,6 +20,8 @@ See docs/process.md for more on how version tagging works. 4.0.16 (in development) ----------------------- +- For windows users, colored console output for error messages and logging now + requires Windows 10 or above. (#25502) - A warning was added about usage of embind without C++17 or above. (#25424) - The minimum supported versions of Node, Chrome and Firefox were bumped enabling the removal of the `globalThis` polyfill and universally enabling diff --git a/test/test_other.py b/test/test_other.py index 8e01ea79763ce..8caf035a2f2b6 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -11607,21 +11607,12 @@ def test_color_diagnostics_disable(self, flag): }) def test_color_diagnostics_force(self, flag): create_file('src.c', 'int main() {') - # -fansi-escape-codes is needed here to make this test work on windows, which doesn't - # use ansi codes by default - output = self.expect_fail([EMCC, '-fansi-escape-codes', flag, 'src.c']) + # -fansi-escape-codes is needed on windows in order to get clang to emit ANSI colors + output = self.expect_fail([EMCC, flag, '-fansi-escape-codes', 'src.c']) self.assertIn("\x1b[1msrc.c:1:13: \x1b[0m\x1b[0;1;31merror: \x1b[0m\x1b[1mexpected '}'\x1b[0m", output) - # Verify that emcc errors show up as red and bold + # Verify that emcc errors show up as red and bold from emcc self.assertIn('emcc: \x1b[31m\x1b[1m', output) - if WINDOWS: - # Also test without -fansi-escape-codes on windows. - # In those mode the code will use kernel calls such as SetConsoleTextAttribute to - # change the output color. We cannot detect this in the output, but we can at least - # get coverage of the code path in the diagnositics.py. - output = self.expect_fail([EMCC, flag, 'src.c']) - self.assertNotIn('\x1b', output) - def test_sanitizer_color(self): create_file('src.c', ''' #include diff --git a/tools/cmdline.py b/tools/cmdline.py index c5d7021d78ce7..0f27b3c4bae8c 100644 --- a/tools/cmdline.py +++ b/tools/cmdline.py @@ -513,8 +513,6 @@ def consume_arg_file(): diagnostics.color_enabled = True elif arg in ('-fno-color-diagnostics', '-fdiagnostics-color=never'): diagnostics.color_enabled = False - elif arg == '-fansi-escape-codes': - diagnostics.force_ansi = True elif arg == '-fno-exceptions': settings.DISABLE_EXCEPTION_CATCHING = 1 settings.DISABLE_EXCEPTION_THROWING = 1 diff --git a/tools/colored_logger.py b/tools/colored_logger.py index 2802415a1ae55..b5e90ca2206e5 100644 --- a/tools/colored_logger.py +++ b/tools/colored_logger.py @@ -11,88 +11,25 @@ import logging -def add_coloring_to_emit_windows(fn): +def ansi_color_available(): + if not sys.platform.startswith('win'): + return sys.stderr.isatty() + # Constants from the Windows API STD_OUTPUT_HANDLE = -11 + ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 - def _get_color(): - SHORT = ctypes.c_short - WORD = ctypes.c_ushort - - class COORD(ctypes.Structure): - _fields_ = [ - ("X", SHORT), - ("Y", SHORT)] - - class SMALL_RECT(ctypes.Structure): - _fields_ = [ - ("Left", SHORT), - ("Top", SHORT), - ("Right", SHORT), - ("Bottom", SHORT)] - - class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure): - _fields_ = [ - ("dwSize", COORD), - ("dwCursorPosition", COORD), - ("wAttributes", WORD), - ("srWindow", SMALL_RECT), - ("dwMaximumWindowSize", COORD)] + kernel32 = ctypes.windll.kernel32 + stdout_handle = kernel32.GetStdHandle(STD_OUTPUT_HANDLE) - hdl = ctypes.windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE) - csbi = CONSOLE_SCREEN_BUFFER_INFO() - ctypes.windll.kernel32.GetConsoleScreenBufferInfo(hdl, ctypes.byref(csbi)) - return csbi.wAttributes + # Get the current console mode + console_mode = ctypes.c_uint() + if not kernel32.GetConsoleMode(stdout_handle, ctypes.byref(console_mode)): + # Handle error if GetConsoleMode fails + return False - def _set_color(code): - hdl = ctypes.windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE) - ctypes.windll.kernel32.SetConsoleTextAttribute(hdl, code) - - def new(*args): - # wincon.h - FOREGROUND_BLACK = 0x0000 # noqa - FOREGROUND_BLUE = 0x0001 # noqa - FOREGROUND_GREEN = 0x0002 # noqa - FOREGROUND_CYAN = 0x0003 # noqa - FOREGROUND_RED = 0x0004 # noqa - FOREGROUND_MAGENTA = 0x0005 # noqa - FOREGROUND_YELLOW = 0x0006 # noqa - FOREGROUND_GREY = 0x0007 # noqa - FOREGROUND_INTENSITY = 0x0008 # foreground color is intensified. - - FOREGROUND_WHITE = FOREGROUND_BLUE|FOREGROUND_GREEN |FOREGROUND_RED # noqa - - BACKGROUND_BLACK = 0x0000 # noqa - BACKGROUND_BLUE = 0x0010 # noqa - BACKGROUND_GREEN = 0x0020 # noqa - BACKGROUND_CYAN = 0x0030 # noqa - BACKGROUND_RED = 0x0040 # noqa - BACKGROUND_MAGENTA = 0x0050 # noqa - BACKGROUND_YELLOW = 0x0060 # noqa - BACKGROUND_GREY = 0x0070 # noqa - BACKGROUND_INTENSITY = 0x0080 # background color is intensified. - levelno = args[1].levelno - if (levelno >= 50): - color = BACKGROUND_YELLOW | FOREGROUND_RED | FOREGROUND_INTENSITY | BACKGROUND_INTENSITY - elif (levelno >= 40): - color = FOREGROUND_RED | FOREGROUND_INTENSITY - elif (levelno >= 30): - color = FOREGROUND_YELLOW | FOREGROUND_INTENSITY - elif (levelno >= 20): - color = FOREGROUND_GREEN - elif (levelno >= 10): - color = FOREGROUND_MAGENTA - else: - color = FOREGROUND_WHITE - - old_color = _get_color() - _set_color(color) - ret = fn(*args) - _set_color(old_color) - return ret - - new.orig_func = fn - return new + # Check if the flag is set in the current console mode + return (console_mode.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0 def add_coloring_to_emit_ansi(fn): @@ -117,11 +54,8 @@ def new(*args): def enable(): - if sys.stderr.isatty(): - if sys.platform.startswith('win'): - logging.StreamHandler.emit = add_coloring_to_emit_windows(logging.StreamHandler.emit) - else: - logging.StreamHandler.emit = add_coloring_to_emit_ansi(logging.StreamHandler.emit) + if ansi_color_available(): + logging.StreamHandler.emit = add_coloring_to_emit_ansi(logging.StreamHandler.emit) def disable(): diff --git a/tools/diagnostics.py b/tools/diagnostics.py index b75f206bc6349..3bf3a00357745 100644 --- a/tools/diagnostics.py +++ b/tools/diagnostics.py @@ -6,24 +6,22 @@ """Simple color-enabled diagnositics reporting functions. """ -import ctypes import logging import os import sys from typing import Dict -WINDOWS = sys.platform.startswith('win') +from . import colored_logger +color_enabled = colored_logger.ansi_color_available() logger = logging.getLogger('diagnostics') -color_enabled = sys.stderr.isatty() tool_name = os.path.splitext(os.path.basename(sys.argv[0]))[0] -force_ansi = False # diagnostic levels WARN = 1 ERROR = 2 -# available colors +# available (ANSI) colors RED = 1 GREEN = 2 YELLOW = 3 @@ -43,94 +41,19 @@ ERROR: 'error: ', } -# Constants from the Windows API -STD_OUTPUT_HANDLE = -11 - - -def output_color_windows(color): - assert not force_ansi - # TODO(sbc): This code is duplicated in colored_logger.py. Refactor. - # wincon.h - FOREGROUND_BLACK = 0x0000 # noqa - FOREGROUND_BLUE = 0x0001 # noqa - FOREGROUND_GREEN = 0x0002 # noqa - FOREGROUND_CYAN = 0x0003 # noqa - FOREGROUND_RED = 0x0004 # noqa - FOREGROUND_MAGENTA = 0x0005 # noqa - FOREGROUND_YELLOW = 0x0006 # noqa - FOREGROUND_GREY = 0x0007 # noqa - - color_map = { - RED: FOREGROUND_RED, - GREEN: FOREGROUND_GREEN, - YELLOW: FOREGROUND_YELLOW, - BLUE: FOREGROUND_BLUE, - MAGENTA: FOREGROUND_MAGENTA, - CYAN: FOREGROUND_CYAN, - WHITE: FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED, - } - - sys.stderr.flush() - hdl = ctypes.windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE) - ctypes.windll.kernel32.SetConsoleTextAttribute(hdl, color_map[color]) - - -def get_color_windows(): - assert not force_ansi - SHORT = ctypes.c_short - WORD = ctypes.c_ushort - - class COORD(ctypes.Structure): - _fields_ = [ - ("X", SHORT), - ("Y", SHORT)] - - class SMALL_RECT(ctypes.Structure): - _fields_ = [ - ("Left", SHORT), - ("Top", SHORT), - ("Right", SHORT), - ("Bottom", SHORT)] - - class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure): - _fields_ = [ - ("dwSize", COORD), - ("dwCursorPosition", COORD), - ("wAttributes", WORD), - ("srWindow", SMALL_RECT), - ("dwMaximumWindowSize", COORD)] - - hdl = ctypes.windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE) - csbi = CONSOLE_SCREEN_BUFFER_INFO() - ctypes.windll.kernel32.GetConsoleScreenBufferInfo(hdl, ctypes.byref(csbi)) - return csbi.wAttributes - - -def reset_color_windows(): - assert not force_ansi - sys.stderr.flush() - hdl = ctypes.windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE) - ctypes.windll.kernel32.SetConsoleTextAttribute(hdl, default_color) - def output_color(color): - if WINDOWS and not force_ansi: - output_color_windows(color) - return '' + assert color_enabled return '\033[3%sm' % color def bold(): - if WINDOWS and not force_ansi: - # AFAICT there is no way to enable bold output on windows - return '' + assert color_enabled return '\033[1m' def reset_color(): - if WINDOWS and not force_ansi: - reset_color_windows() - return '' + assert color_enabled return '\033[0m' @@ -260,7 +183,4 @@ def capture_warnings(argv): return manager.capture_warnings(argv) -if WINDOWS: - default_color = get_color_windows() - manager = WarningManager()