Thaddeus Crews 4 месяцев назад
Родитель
Сommit
2b1f463de5
6 измененных файлов с 104 добавлено и 83 удалено
  1. 3 3
      SConstruct
  2. 4 5
      doc/tools/doc_status.py
  3. 3 2
      doc/tools/make_rst.py
  4. 2 2
      methods.py
  5. 6 6
      misc/scripts/install_d3d12_sdk_windows.py
  6. 86 65
      misc/utility/color.py

+ 3 - 3
SConstruct

@@ -58,7 +58,7 @@ import gles3_builders
 import glsl_builders
 import methods
 import scu_builders
-from misc.utility.color import STDERR_COLOR, print_error, print_info, print_warning
+from misc.utility.color import is_stderr_color, print_error, print_info, print_warning
 from platform_methods import architecture_aliases, architectures, compatibility_platform_aliases
 
 if ARGUMENTS.get("target", "editor") == "editor":
@@ -704,9 +704,9 @@ if env["arch"] == "x86_32":
 
 # Explicitly specify colored output.
 if methods.using_gcc(env):
-    env.AppendUnique(CCFLAGS=["-fdiagnostics-color" if STDERR_COLOR else "-fno-diagnostics-color"])
+    env.AppendUnique(CCFLAGS=["-fdiagnostics-color" if is_stderr_color() else "-fno-diagnostics-color"])
 elif methods.using_clang(env) or methods.using_emcc(env):
-    env.AppendUnique(CCFLAGS=["-fcolor-diagnostics" if STDERR_COLOR else "-fno-color-diagnostics"])
+    env.AppendUnique(CCFLAGS=["-fcolor-diagnostics" if is_stderr_color() else "-fno-color-diagnostics"])
     if sys.platform == "win32":
         env.AppendUnique(CCFLAGS=["-fansi-escape-codes"])
 

+ 4 - 5
doc/tools/doc_status.py

@@ -10,14 +10,14 @@ from typing import Dict, List, Set
 
 sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../"))
 
-from misc.utility.color import NO_COLOR, STDOUT_COLOR, Ansi, toggle_color
+from misc.utility.color import Ansi, force_stdout_color, is_stdout_color
 
 ################################################################################
 #                                    Config                                    #
 ################################################################################
 
 flags = {
-    "c": STDOUT_COLOR,
+    "c": is_stdout_color(),
     "b": False,
     "g": False,
     "s": False,
@@ -114,7 +114,7 @@ def validate_tag(elem: ET.Element, tag: str) -> None:
 
 
 def color(color: str, string: str) -> str:
-    if NO_COLOR:
+    if not is_stdout_color():
         return string
     color_format = "".join([str(x) for x in colors[color]])
     return f"{color_format}{string}{Ansi.RESET}"
@@ -332,8 +332,7 @@ if flags["u"]:
     table_column_names.append("Docs URL")
     table_columns.append("url")
 
-if flags["c"]:
-    toggle_color(True)
+force_stdout_color(flags["c"])
 
 ################################################################################
 #                                     Help                                     #

+ 3 - 2
doc/tools/make_rst.py

@@ -13,7 +13,7 @@ from typing import Any, Dict, List, Optional, TextIO, Tuple, Union
 sys.path.insert(0, root_directory := os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../"))
 
 import version
-from misc.utility.color import Ansi, toggle_color
+from misc.utility.color import Ansi, force_stderr_color, force_stdout_color
 
 # $DOCS_URL/path/to/page.html(#fragment-tag)
 GODOT_DOCS_PATTERN = re.compile(r"^\$DOCS_URL/(.*)\.html(#.*)?$")
@@ -698,7 +698,8 @@ def main() -> None:
     args = parser.parse_args()
 
     if args.color:
-        toggle_color(True)
+        force_stdout_color(True)
+        force_stderr_color(True)
 
     # Retrieve heading translations for the given language.
     if not args.dry_run and args.lang != "en":

+ 2 - 2
methods.py

@@ -428,9 +428,9 @@ def use_windows_spawn_fix(self, platform=None):
 
 
 def no_verbose(env):
-    from misc.utility.color import Ansi
+    from misc.utility.color import Ansi, is_stdout_color
 
-    colors = [Ansi.BLUE, Ansi.BOLD, Ansi.REGULAR, Ansi.RESET]
+    colors = [Ansi.BLUE, Ansi.BOLD, Ansi.REGULAR, Ansi.RESET] if is_stdout_color() else ["", "", "", ""]
 
     # There is a space before "..." to ensure that source file names can be
     # Ctrl + clicked in the VS Code terminal.

+ 6 - 6
misc/scripts/install_d3d12_sdk_windows.py

@@ -8,7 +8,7 @@ import urllib.request
 
 sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../"))
 
-from misc.utility.color import Ansi
+from misc.utility.color import Ansi, color_print
 
 # Base Godot dependencies path
 # If cross-compiling (no LOCALAPPDATA), we install in `bin`
@@ -42,7 +42,7 @@ if not os.path.exists(deps_folder):
     os.makedirs(deps_folder)
 
 # Mesa NIR
-print(f"{Ansi.BOLD}[1/3] Mesa NIR{Ansi.RESET}")
+color_print(f"{Ansi.BOLD}[1/3] Mesa NIR")
 if os.path.isfile(mesa_archive):
     os.remove(mesa_archive)
 print(f"Downloading Mesa NIR {mesa_filename} ...")
@@ -69,7 +69,7 @@ if dlltool == "":
     dlltool = shutil.which("x86_64-w64-mingw32-dlltool") or ""
 has_mingw = gendef != "" and dlltool != ""
 
-print(f"{Ansi.BOLD}[2/3] WinPixEventRuntime{Ansi.RESET}")
+color_print(f"{Ansi.BOLD}[2/3] WinPixEventRuntime")
 if os.path.isfile(pix_archive):
     os.remove(pix_archive)
 print(f"Downloading WinPixEventRuntime {pix_version} ...")
@@ -100,7 +100,7 @@ else:
 print(f"WinPixEventRuntime {pix_version} installed successfully.\n")
 
 # DirectX 12 Agility SDK
-print(f"{Ansi.BOLD}[3/3] DirectX 12 Agility SDK{Ansi.RESET}")
+color_print(f"{Ansi.BOLD}[3/3] DirectX 12 Agility SDK")
 if os.path.isfile(agility_sdk_archive):
     os.remove(agility_sdk_archive)
 print(f"Downloading DirectX 12 Agility SDK {agility_sdk_version} ...")
@@ -116,5 +116,5 @@ os.remove(agility_sdk_archive)
 print(f"DirectX 12 Agility SDK {agility_sdk_version} installed successfully.\n")
 
 # Complete message
-print(f'{Ansi.GREEN}All Direct3D 12 SDK components were installed to "{deps_folder}" successfully!{Ansi.RESET}')
-print(f'{Ansi.GREEN}You can now build Godot with Direct3D 12 support enabled by running "scons d3d12=yes".{Ansi.RESET}')
+color_print(f'{Ansi.GREEN}All Direct3D 12 SDK components were installed to "{deps_folder}" successfully!')
+color_print(f'{Ansi.GREEN}You can now build Godot with Direct3D 12 support enabled by running "scons d3d12=yes".')

+ 86 - 65
misc/utility/color.py

@@ -1,86 +1,58 @@
 from __future__ import annotations
 
 import os
+import re
 import sys
 from enum import Enum
 from typing import Final
 
 # Colors are disabled in non-TTY environments such as pipes. This means if output is redirected
-# to a file, it won't contain color codes. Colors are always enabled on continuous integration.
+# to a file, it won't contain color codes. Colors are enabled by default on continuous integration.
 
 IS_CI: Final[bool] = bool(os.environ.get("CI"))
 NO_COLOR: Final[bool] = bool(os.environ.get("NO_COLOR"))
+CLICOLOR_FORCE: Final[bool] = bool(os.environ.get("CLICOLOR_FORCE"))
 STDOUT_TTY: Final[bool] = bool(sys.stdout.isatty())
 STDERR_TTY: Final[bool] = bool(sys.stderr.isatty())
 
 
-def _color_supported(stdout: bool) -> bool:
-    """
-    Validates if the current environment supports colored output. Attempts to enable ANSI escape
-    code support on Windows 10 and later.
-    """
-    if IS_CI:
-        return True
+_STDOUT_ORIGINAL: Final[bool] = False if NO_COLOR else CLICOLOR_FORCE or IS_CI or STDOUT_TTY
+_STDERR_ORIGINAL: Final[bool] = False if NO_COLOR else CLICOLOR_FORCE or IS_CI or STDERR_TTY
+_stdout_override: bool = _STDOUT_ORIGINAL
+_stderr_override: bool = _STDERR_ORIGINAL
 
-    if sys.platform != "win32":
-        return STDOUT_TTY if stdout else STDERR_TTY
-    else:
-        from ctypes import POINTER, WINFUNCTYPE, WinError, windll
-        from ctypes.wintypes import BOOL, DWORD, HANDLE
 
-        STD_HANDLE = -11 if stdout else -12
-        ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
+def is_stdout_color() -> bool:
+    return _stdout_override
 
-        def err_handler(result, func, args):
-            if not result:
-                raise WinError()
-            return args
 
-        GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)(("GetStdHandle", windll.kernel32), ((1, "nStdHandle"),))
-        GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))(
-            ("GetConsoleMode", windll.kernel32),
-            ((1, "hConsoleHandle"), (2, "lpMode")),
-        )
-        GetConsoleMode.errcheck = err_handler
-        SetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, DWORD)(
-            ("SetConsoleMode", windll.kernel32),
-            ((1, "hConsoleHandle"), (1, "dwMode")),
-        )
-        SetConsoleMode.errcheck = err_handler
+def is_stderr_color() -> bool:
+    return _stderr_override
 
-        try:
-            handle = GetStdHandle(STD_HANDLE)
-            flags = GetConsoleMode(handle)
-            SetConsoleMode(handle, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
-            return True
-        except OSError:
-            return False
 
-
-STDOUT_COLOR: Final[bool] = _color_supported(True)
-STDERR_COLOR: Final[bool] = _color_supported(False)
-_stdout_override: bool = STDOUT_COLOR
-_stderr_override: bool = STDERR_COLOR
+def force_stdout_color(value: bool) -> None:
+    """
+    Explicitly set `stdout` support for ANSI escape codes.
+    If environment overrides exist, does nothing.
+    """
+    if not NO_COLOR or not CLICOLOR_FORCE:
+        global _stdout_override
+        _stdout_override = value
 
 
-def toggle_color(stdout: bool, value: bool | None = None) -> None:
+def force_stderr_color(value: bool) -> None:
     """
-    Explicitly toggle color codes, regardless of support.
-
-    - `stdout`: A boolean to choose the output stream. `True` for stdout, `False` for stderr.
-    - `value`: An optional boolean to explicitly set the color state instead of toggling.
+    Explicitly set `stderr` support for ANSI escape codes.
+    If environment overrides exist, does nothing.
     """
-    if stdout:
-        global _stdout_override
-        _stdout_override = value if value is not None else not _stdout_override
-    else:
+    if not NO_COLOR or not CLICOLOR_FORCE:
         global _stderr_override
-        _stderr_override = value if value is not None else not _stderr_override
+        _stderr_override = value
 
 
 class Ansi(Enum):
     """
-    Enum class for adding ansi codepoints directly into strings. Automatically converts values to
+    Enum class for adding ANSI codepoints directly into strings. Automatically converts values to
     strings representing their internal value.
     """
 
@@ -107,25 +79,74 @@ class Ansi(Enum):
         return self.value
 
 
+RE_ANSI = re.compile(r"\x1b\[[=\?]?[;\d]+[a-zA-Z]")
+
+
+def color_print(*values: object, sep: str | None = " ", end: str | None = "\n", flush: bool = False) -> None:
+    """Prints a colored message to `stdout`. If disabled, ANSI codes are automatically stripped."""
+    if is_stdout_color():
+        print(*values, sep=sep, end=f"{Ansi.RESET}{end}", flush=flush)
+    else:
+        print(RE_ANSI.sub("", (sep or " ").join(map(str, values))), sep="", end=end, flush=flush)
+
+
+def color_printerr(*values: object, sep: str | None = " ", end: str | None = "\n", flush: bool = False) -> None:
+    """Prints a colored message to `stderr`. If disabled, ANSI codes are automatically stripped."""
+    if is_stderr_color():
+        print(*values, sep=sep, end=f"{Ansi.RESET}{end}", flush=flush, file=sys.stderr)
+    else:
+        print(RE_ANSI.sub("", (sep or " ").join(map(str, values))), sep="", end=end, flush=flush, file=sys.stderr)
+
+
 def print_info(*values: object) -> None:
     """Prints a informational message with formatting."""
-    if _stdout_override:
-        print(f"{Ansi.GRAY}{Ansi.BOLD}INFO:{Ansi.REGULAR}", *values, Ansi.RESET)
-    else:
-        print("INFO:", *values)
+    color_print(f"{Ansi.GRAY}{Ansi.BOLD}INFO:{Ansi.REGULAR}", *values)
 
 
 def print_warning(*values: object) -> None:
     """Prints a warning message with formatting."""
-    if _stderr_override:
-        print(f"{Ansi.YELLOW}{Ansi.BOLD}WARNING:{Ansi.REGULAR}", *values, Ansi.RESET, file=sys.stderr)
-    else:
-        print("WARNING:", *values, file=sys.stderr)
+    color_printerr(f"{Ansi.YELLOW}{Ansi.BOLD}WARNING:{Ansi.REGULAR}", *values)
 
 
 def print_error(*values: object) -> None:
     """Prints an error message with formatting."""
-    if _stderr_override:
-        print(f"{Ansi.RED}{Ansi.BOLD}ERROR:{Ansi.REGULAR}", *values, Ansi.RESET, file=sys.stderr)
-    else:
-        print("ERROR:", *values, file=sys.stderr)
+    color_printerr(f"{Ansi.RED}{Ansi.BOLD}ERROR:{Ansi.REGULAR}", *values)
+
+
+if sys.platform == "win32":
+
+    def _win_color_fix():
+        """Attempts to enable ANSI escape code support on Windows 10 and later."""
+        from ctypes import POINTER, WINFUNCTYPE, WinError, windll
+        from ctypes.wintypes import BOOL, DWORD, HANDLE
+
+        STDOUT_HANDLE = -11
+        STDERR_HANDLE = -12
+        ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
+
+        def err_handler(result, func, args):
+            if not result:
+                raise WinError()
+            return args
+
+        GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)(("GetStdHandle", windll.kernel32), ((1, "nStdHandle"),))
+        GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))(
+            ("GetConsoleMode", windll.kernel32),
+            ((1, "hConsoleHandle"), (2, "lpMode")),
+        )
+        GetConsoleMode.errcheck = err_handler
+        SetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, DWORD)(
+            ("SetConsoleMode", windll.kernel32),
+            ((1, "hConsoleHandle"), (1, "dwMode")),
+        )
+        SetConsoleMode.errcheck = err_handler
+
+        for handle_id in [STDOUT_HANDLE, STDERR_HANDLE]:
+            try:
+                handle = GetStdHandle(handle_id)
+                flags = GetConsoleMode(handle)
+                SetConsoleMode(handle, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
+            except OSError:
+                pass
+
+    _win_color_fix()