Browse Source

SCons: Improve colored output

Thaddeus Crews 1 year ago
parent
commit
d8761f2c79

+ 1 - 1
.pre-commit-config.yaml

@@ -98,7 +98,7 @@ repos:
         name: doc-status
         name: doc-status
         language: python
         language: python
         entry: python doc/tools/doc_status.py
         entry: python doc/tools/doc_status.py
-        args: [doc/classes, modules/*/doc_classes, platform/*/doc_classes]
+        args: [doc/classes, modules/*/doc_classes, platform/*/doc_classes, -c]
         pass_filenames: false
         pass_filenames: false
         files: ^(doc/classes|.*/doc_classes)/.*\.xml$
         files: ^(doc/classes|.*/doc_classes)/.*\.xml$
 
 

+ 4 - 22
SConstruct

@@ -58,31 +58,13 @@ import gles3_builders
 import glsl_builders
 import glsl_builders
 import methods
 import methods
 import scu_builders
 import scu_builders
-from methods import print_error, print_warning
+from methods import Ansi, print_error, print_info, print_warning
 from platform_methods import architecture_aliases, architectures, compatibility_platform_aliases
 from platform_methods import architecture_aliases, architectures, compatibility_platform_aliases
 
 
 if ARGUMENTS.get("target", "editor") == "editor":
 if ARGUMENTS.get("target", "editor") == "editor":
     _helper_module("editor.editor_builders", "editor/editor_builders.py")
     _helper_module("editor.editor_builders", "editor/editor_builders.py")
     _helper_module("editor.template_builders", "editor/template_builders.py")
     _helper_module("editor.template_builders", "editor/template_builders.py")
 
 
-# Enable ANSI escape code support on Windows 10 and later (for colored console output).
-# <https://github.com/python/cpython/issues/73245>
-if sys.stdout.isatty() and sys.platform == "win32":
-    try:
-        from ctypes import WinError, byref, windll  # type: ignore
-        from ctypes.wintypes import DWORD  # type: ignore
-
-        stdout_handle = windll.kernel32.GetStdHandle(DWORD(-11))
-        mode = DWORD(0)
-        if not windll.kernel32.GetConsoleMode(stdout_handle, byref(mode)):
-            raise WinError()
-        mode = DWORD(mode.value | 4)
-        if not windll.kernel32.SetConsoleMode(stdout_handle, mode):
-            raise WinError()
-    except Exception as e:
-        methods._colorize = False
-        print_error(f"Failed to enable ANSI escape code support, disabling color output.\n{e}")
-
 # Scan possible build platforms
 # Scan possible build platforms
 
 
 platform_list = []  # list of platforms
 platform_list = []  # list of platforms
@@ -630,7 +612,7 @@ detect.configure(env)
 
 
 print(f'Building for platform "{env["platform"]}", architecture "{env["arch"]}", target "{env["target"]}".')
 print(f'Building for platform "{env["platform"]}", architecture "{env["arch"]}", target "{env["target"]}".')
 if env.dev_build:
 if env.dev_build:
-    print("NOTE: Developer build, with debug optimization level and debug symbols (unless overridden).")
+    print_info("Developer build, with debug optimization level and debug symbols (unless overridden).")
 
 
 # Enforce our minimal compiler version requirements
 # Enforce our minimal compiler version requirements
 cc_version = methods.get_compiler_version(env)
 cc_version = methods.get_compiler_version(env)
@@ -1095,10 +1077,10 @@ def print_elapsed_time():
     time_centiseconds = round((elapsed_time_sec % 1) * 100)
     time_centiseconds = round((elapsed_time_sec % 1) * 100)
     print(
     print(
         "{}[Time elapsed: {}.{:02}]{}".format(
         "{}[Time elapsed: {}.{:02}]{}".format(
-            methods.ANSI.GRAY,
+            Ansi.GRAY,
             time.strftime("%H:%M:%S", time.gmtime(elapsed_time_sec)),
             time.strftime("%H:%M:%S", time.gmtime(elapsed_time_sec)),
             time_centiseconds,
             time_centiseconds,
-            methods.ANSI.RESET,
+            Ansi.RESET,
         )
         )
     )
     )
 
 

+ 18 - 29
doc/tools/doc_status.py

@@ -3,18 +3,21 @@
 import fnmatch
 import fnmatch
 import math
 import math
 import os
 import os
-import platform
 import re
 import re
 import sys
 import sys
 import xml.etree.ElementTree as ET
 import xml.etree.ElementTree as ET
 from typing import Dict, List, Set
 from typing import Dict, List, Set
 
 
+sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../"))
+
+from methods import COLOR_SUPPORTED, Ansi, toggle_color
+
 ################################################################################
 ################################################################################
 #                                    Config                                    #
 #                                    Config                                    #
 ################################################################################
 ################################################################################
 
 
 flags = {
 flags = {
-    "c": platform.platform() != "Windows",  # Disable by default on windows, since we use ANSI escape codes
+    "c": COLOR_SUPPORTED,
     "b": False,
     "b": False,
     "g": False,
     "g": False,
     "s": False,
     "s": False,
@@ -85,16 +88,16 @@ table_column_names = [
     "Constructors",
     "Constructors",
 ]
 ]
 colors = {
 colors = {
-    "name": [36],  # cyan
-    "part_big_problem": [4, 31],  # underline, red
-    "part_problem": [31],  # red
-    "part_mostly_good": [33],  # yellow
-    "part_good": [32],  # green
-    "url": [4, 34],  # underline, blue
-    "section": [1, 4],  # bold, underline
-    "state_off": [36],  # cyan
-    "state_on": [1, 35],  # bold, magenta/plum
-    "bold": [1],  # bold
+    "name": [Ansi.CYAN],  # cyan
+    "part_big_problem": [Ansi.RED, Ansi.UNDERLINE],  # underline, red
+    "part_problem": [Ansi.RED],  # red
+    "part_mostly_good": [Ansi.YELLOW],  # yellow
+    "part_good": [Ansi.GREEN],  # green
+    "url": [Ansi.BLUE, Ansi.UNDERLINE],  # underline, blue
+    "section": [Ansi.BOLD, Ansi.UNDERLINE],  # bold, underline
+    "state_off": [Ansi.CYAN],  # cyan
+    "state_on": [Ansi.BOLD, Ansi.MAGENTA],  # bold, magenta/plum
+    "bold": [Ansi.BOLD],  # bold
 }
 }
 overall_progress_description_weight = 10
 overall_progress_description_weight = 10
 
 
@@ -111,13 +114,8 @@ def validate_tag(elem: ET.Element, tag: str) -> None:
 
 
 
 
 def color(color: str, string: str) -> str:
 def color(color: str, string: str) -> str:
-    if flags["c"] and terminal_supports_color():
-        color_format = ""
-        for code in colors[color]:
-            color_format += "\033[" + str(code) + "m"
-        return color_format + string + "\033[0m"
-    else:
-        return string
+    color_format = "".join([str(x) for x in colors[color]])
+    return f"{color_format}{string}{Ansi.RESET}"
 
 
 
 
 ansi_escape = re.compile(r"\x1b[^m]*m")
 ansi_escape = re.compile(r"\x1b[^m]*m")
@@ -127,16 +125,6 @@ def nonescape_len(s: str) -> int:
     return len(ansi_escape.sub("", s))
     return len(ansi_escape.sub("", s))
 
 
 
 
-def terminal_supports_color():
-    p = sys.platform
-    supported_platform = p != "Pocket PC" and (p != "win32" or "ANSICON" in os.environ)
-
-    is_a_tty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
-    if not supported_platform or not is_a_tty:
-        return False
-    return True
-
-
 ################################################################################
 ################################################################################
 #                                   Classes                                    #
 #                                   Classes                                    #
 ################################################################################
 ################################################################################
@@ -342,6 +330,7 @@ if flags["u"]:
     table_column_names.append("Docs URL")
     table_column_names.append("Docs URL")
     table_columns.append("url")
     table_columns.append("url")
 
 
+toggle_color(flags["c"])
 
 
 ################################################################################
 ################################################################################
 #                                     Help                                     #
 #                                     Help                                     #

+ 15 - 43
doc/tools/make_rst.py

@@ -10,10 +10,10 @@ import xml.etree.ElementTree as ET
 from collections import OrderedDict
 from collections import OrderedDict
 from typing import Any, Dict, List, Optional, TextIO, Tuple, Union
 from typing import Any, Dict, List, Optional, TextIO, Tuple, Union
 
 
-# Import hardcoded version information from version.py
-root_directory = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../")
-sys.path.append(root_directory)  # Include the root directory
-import version  # noqa: E402
+sys.path.insert(0, root_directory := os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../"))
+
+import version
+from methods import Ansi, toggle_color
 
 
 # $DOCS_URL/path/to/page.html(#fragment-tag)
 # $DOCS_URL/path/to/page.html(#fragment-tag)
 GODOT_DOCS_PATTERN = re.compile(r"^\$DOCS_URL/(.*)\.html(#.*)?$")
 GODOT_DOCS_PATTERN = re.compile(r"^\$DOCS_URL/(.*)\.html(#.*)?$")
@@ -90,8 +90,6 @@ BASE_STRINGS = [
 ]
 ]
 strings_l10n: Dict[str, str] = {}
 strings_l10n: Dict[str, str] = {}
 
 
-STYLES: Dict[str, str] = {}
-
 CLASS_GROUPS: Dict[str, str] = {
 CLASS_GROUPS: Dict[str, str] = {
     "global": "Globals",
     "global": "Globals",
     "node": "Nodes",
     "node": "Nodes",
@@ -699,31 +697,7 @@ def main() -> None:
     )
     )
     args = parser.parse_args()
     args = parser.parse_args()
 
 
-    should_color = bool(args.color or sys.stdout.isatty() or os.environ.get("CI"))
-
-    # Enable ANSI escape code support on Windows 10 and later (for colored console output).
-    # <https://github.com/python/cpython/issues/73245>
-    if should_color and sys.stdout.isatty() and sys.platform == "win32":
-        try:
-            from ctypes import WinError, byref, windll  # type: ignore
-            from ctypes.wintypes import DWORD  # type: ignore
-
-            stdout_handle = windll.kernel32.GetStdHandle(DWORD(-11))
-            mode = DWORD(0)
-            if not windll.kernel32.GetConsoleMode(stdout_handle, byref(mode)):
-                raise WinError()
-            mode = DWORD(mode.value | 4)
-            if not windll.kernel32.SetConsoleMode(stdout_handle, mode):
-                raise WinError()
-        except Exception:
-            should_color = False
-
-    STYLES["red"] = "\x1b[91m" if should_color else ""
-    STYLES["green"] = "\x1b[92m" if should_color else ""
-    STYLES["yellow"] = "\x1b[93m" if should_color else ""
-    STYLES["bold"] = "\x1b[1m" if should_color else ""
-    STYLES["regular"] = "\x1b[22m" if should_color else ""
-    STYLES["reset"] = "\x1b[0m" if should_color else ""
+    toggle_color(args.color)
 
 
     # Retrieve heading translations for the given language.
     # Retrieve heading translations for the given language.
     if not args.dry_run and args.lang != "en":
     if not args.dry_run and args.lang != "en":
@@ -834,16 +808,16 @@ def main() -> None:
     if state.script_language_parity_check.hit_count > 0:
     if state.script_language_parity_check.hit_count > 0:
         if not args.verbose:
         if not args.verbose:
             print(
             print(
-                f'{STYLES["yellow"]}{state.script_language_parity_check.hit_count} code samples failed parity check. Use --verbose to get more information.{STYLES["reset"]}'
+                f"{Ansi.YELLOW}{state.script_language_parity_check.hit_count} code samples failed parity check. Use --verbose to get more information.{Ansi.RESET}"
             )
             )
         else:
         else:
             print(
             print(
-                f'{STYLES["yellow"]}{state.script_language_parity_check.hit_count} code samples failed parity check:{STYLES["reset"]}'
+                f"{Ansi.YELLOW}{state.script_language_parity_check.hit_count} code samples failed parity check:{Ansi.RESET}"
             )
             )
 
 
             for class_name in state.script_language_parity_check.hit_map.keys():
             for class_name in state.script_language_parity_check.hit_map.keys():
                 class_hits = state.script_language_parity_check.hit_map[class_name]
                 class_hits = state.script_language_parity_check.hit_map[class_name]
-                print(f'{STYLES["yellow"]}- {len(class_hits)} hits in class "{class_name}"{STYLES["reset"]}')
+                print(f'{Ansi.YELLOW}- {len(class_hits)} hits in class "{class_name}"{Ansi.RESET}')
 
 
                 for context, error in class_hits:
                 for context, error in class_hits:
                     print(f"  - {error} in {format_context_name(context)}")
                     print(f"  - {error} in {format_context_name(context)}")
@@ -853,24 +827,22 @@ def main() -> None:
 
 
     if state.num_warnings >= 2:
     if state.num_warnings >= 2:
         print(
         print(
-            f'{STYLES["yellow"]}{state.num_warnings} warnings were found in the class reference XML. Please check the messages above.{STYLES["reset"]}'
+            f"{Ansi.YELLOW}{state.num_warnings} warnings were found in the class reference XML. Please check the messages above.{Ansi.RESET}"
         )
         )
     elif state.num_warnings == 1:
     elif state.num_warnings == 1:
         print(
         print(
-            f'{STYLES["yellow"]}1 warning was found in the class reference XML. Please check the messages above.{STYLES["reset"]}'
+            f"{Ansi.YELLOW}1 warning was found in the class reference XML. Please check the messages above.{Ansi.RESET}"
         )
         )
 
 
     if state.num_errors >= 2:
     if state.num_errors >= 2:
         print(
         print(
-            f'{STYLES["red"]}{state.num_errors} errors were found in the class reference XML. Please check the messages above.{STYLES["reset"]}'
+            f"{Ansi.RED}{state.num_errors} errors were found in the class reference XML. Please check the messages above.{Ansi.RESET}"
         )
         )
     elif state.num_errors == 1:
     elif state.num_errors == 1:
-        print(
-            f'{STYLES["red"]}1 error was found in the class reference XML. Please check the messages above.{STYLES["reset"]}'
-        )
+        print(f"{Ansi.RED}1 error was found in the class reference XML. Please check the messages above.{Ansi.RESET}")
 
 
     if state.num_warnings == 0 and state.num_errors == 0:
     if state.num_warnings == 0 and state.num_errors == 0:
-        print(f'{STYLES["green"]}No warnings or errors found in the class reference XML.{STYLES["reset"]}')
+        print(f"{Ansi.GREEN}No warnings or errors found in the class reference XML.{Ansi.RESET}")
         if not args.dry_run:
         if not args.dry_run:
             print(f"Wrote reStructuredText files for each class to: {args.output}")
             print(f"Wrote reStructuredText files for each class to: {args.output}")
     else:
     else:
@@ -881,12 +853,12 @@ def main() -> None:
 
 
 
 
 def print_error(error: str, state: State) -> None:
 def print_error(error: str, state: State) -> None:
-    print(f'{STYLES["red"]}{STYLES["bold"]}ERROR:{STYLES["regular"]} {error}{STYLES["reset"]}')
+    print(f"{Ansi.RED}{Ansi.BOLD}ERROR:{Ansi.REGULAR} {error}{Ansi.RESET}")
     state.num_errors += 1
     state.num_errors += 1
 
 
 
 
 def print_warning(warning: str, state: State) -> None:
 def print_warning(warning: str, state: State) -> None:
-    print(f'{STYLES["yellow"]}{STYLES["bold"]}WARNING:{STYLES["regular"]} {warning}{STYLES["reset"]}')
+    print(f"{Ansi.YELLOW}{Ansi.BOLD}WARNING:{Ansi.REGULAR} {warning}{Ansi.RESET}")
     state.num_warnings += 1
     state.num_warnings += 1
 
 
 
 

+ 85 - 22
methods.py

@@ -10,26 +10,63 @@ from collections import OrderedDict
 from enum import Enum
 from enum import Enum
 from io import StringIO, TextIOWrapper
 from io import StringIO, TextIOWrapper
 from pathlib import Path
 from pathlib import Path
-from typing import Generator, List, Optional, Union, cast
+from typing import Final, Generator, List, Optional, Union, cast
 
 
 # Get the "Godot" folder name ahead of time
 # Get the "Godot" folder name ahead of time
 base_folder_path = str(os.path.abspath(Path(__file__).parent)) + "/"
 base_folder_path = str(os.path.abspath(Path(__file__).parent)) + "/"
 base_folder_only = os.path.basename(os.path.normpath(base_folder_path))
 base_folder_only = os.path.basename(os.path.normpath(base_folder_path))
-# Listing all the folders we have converted
-# for SCU in scu_builders.py
-_scu_folders = set()
+
+################################################################################
+# COLORIZE
+################################################################################
+
+IS_CI: Final[bool] = bool(os.environ.get("CI"))
+IS_TTY: Final[bool] = bool(sys.stdout.isatty())
+
+
+def _color_supported() -> bool:
+    """
+    Enables ANSI escape code support on Windows 10 and later (for colored console output).
+    See here: https://github.com/python/cpython/issues/73245
+    """
+    if sys.platform == "win32" and IS_TTY:
+        try:
+            from ctypes import WinError, byref, windll  # type: ignore
+            from ctypes.wintypes import DWORD  # type: ignore
+
+            stdout_handle = windll.kernel32.GetStdHandle(DWORD(-11))
+            mode = DWORD(0)
+            if not windll.kernel32.GetConsoleMode(stdout_handle, byref(mode)):
+                raise WinError()
+            mode = DWORD(mode.value | 4)
+            if not windll.kernel32.SetConsoleMode(stdout_handle, mode):
+                raise WinError()
+        except (TypeError, OSError) as e:
+            print(f"Failed to enable ANSI escape code support, disabling color output.\n{e}", file=sys.stderr)
+            return False
+
+    return IS_TTY or IS_CI
+
+
 # Colors are disabled in non-TTY environments such as pipes. This means
 # Colors are disabled in non-TTY environments such as pipes. This means
 # that if output is redirected to a file, it won't contain color codes.
 # that if output is redirected to a file, it won't contain color codes.
 # Colors are always enabled on continuous integration.
 # Colors are always enabled on continuous integration.
-_colorize = bool(sys.stdout.isatty() or os.environ.get("CI"))
+COLOR_SUPPORTED: Final[bool] = _color_supported()
+_can_color: bool = COLOR_SUPPORTED
 
 
 
 
-def set_scu_folders(scu_folders):
-    global _scu_folders
-    _scu_folders = scu_folders
+def toggle_color(value: Optional[bool] = None) -> None:
+    """
+    Explicitly toggle color codes, regardless of support.
+
+    - `value`: An optional boolean to explicitly set the color
+    state instead of toggling.
+    """
+    global _can_color
+    _can_color = value if value is not None else not _can_color
 
 
 
 
-class ANSI(Enum):
+class Ansi(Enum):
     """
     """
     Enum class for adding ansi colorcodes directly into strings.
     Enum class for adding ansi colorcodes directly into strings.
     Automatically converts values to strings representing their
     Automatically converts values to strings representing their
@@ -39,6 +76,7 @@ class ANSI(Enum):
     RESET = "\x1b[0m"
     RESET = "\x1b[0m"
 
 
     BOLD = "\x1b[1m"
     BOLD = "\x1b[1m"
+    DIM = "\x1b[2m"
     ITALIC = "\x1b[3m"
     ITALIC = "\x1b[3m"
     UNDERLINE = "\x1b[4m"
     UNDERLINE = "\x1b[4m"
     STRIKETHROUGH = "\x1b[9m"
     STRIKETHROUGH = "\x1b[9m"
@@ -53,24 +91,49 @@ class ANSI(Enum):
     CYAN = "\x1b[36m"
     CYAN = "\x1b[36m"
     WHITE = "\x1b[37m"
     WHITE = "\x1b[37m"
 
 
-    PURPLE = "\x1b[38;5;93m"
-    PINK = "\x1b[38;5;206m"
-    ORANGE = "\x1b[38;5;214m"
-    GRAY = "\x1b[38;5;244m"
+    LIGHT_BLACK = "\x1b[90m"
+    LIGHT_RED = "\x1b[91m"
+    LIGHT_GREEN = "\x1b[92m"
+    LIGHT_YELLOW = "\x1b[93m"
+    LIGHT_BLUE = "\x1b[94m"
+    LIGHT_MAGENTA = "\x1b[95m"
+    LIGHT_CYAN = "\x1b[96m"
+    LIGHT_WHITE = "\x1b[97m"
+
+    GRAY = LIGHT_BLACK if IS_CI else BLACK
+    """
+    Special case. GitHub Actions doesn't convert `BLACK` to gray as expected, but does convert `LIGHT_BLACK`.
+    By implementing `GRAY`, we handle both cases dynamically, while still allowing for explicit values if desired.
+    """
 
 
     def __str__(self) -> str:
     def __str__(self) -> str:
-        global _colorize
-        return str(self.value) if _colorize else ""
+        global _can_color
+        return str(self.value) if _can_color else ""
+
+
+def print_info(*values: object) -> None:
+    """Prints a informational message with formatting."""
+    print(f"{Ansi.GRAY}{Ansi.BOLD}INFO:{Ansi.REGULAR}", *values, Ansi.RESET)
 
 
 
 
 def print_warning(*values: object) -> None:
 def print_warning(*values: object) -> None:
     """Prints a warning message with formatting."""
     """Prints a warning message with formatting."""
-    print(f"{ANSI.YELLOW}{ANSI.BOLD}WARNING:{ANSI.REGULAR}", *values, ANSI.RESET, file=sys.stderr)
+    print(f"{Ansi.YELLOW}{Ansi.BOLD}WARNING:{Ansi.REGULAR}", *values, Ansi.RESET, file=sys.stderr)
 
 
 
 
 def print_error(*values: object) -> None:
 def print_error(*values: object) -> None:
     """Prints an error message with formatting."""
     """Prints an error message with formatting."""
-    print(f"{ANSI.RED}{ANSI.BOLD}ERROR:{ANSI.REGULAR}", *values, ANSI.RESET, file=sys.stderr)
+    print(f"{Ansi.RED}{Ansi.BOLD}ERROR:{Ansi.REGULAR}", *values, Ansi.RESET, file=sys.stderr)
+
+
+# Listing all the folders we have converted
+# for SCU in scu_builders.py
+_scu_folders = set()
+
+
+def set_scu_folders(scu_folders):
+    global _scu_folders
+    _scu_folders = scu_folders
 
 
 
 
 def add_source_files_orig(self, sources, files, allow_gen=False):
 def add_source_files_orig(self, sources, files, allow_gen=False):
@@ -163,7 +226,7 @@ def get_version_info(module_version_string="", silent=False):
     if os.getenv("BUILD_NAME") is not None:
     if os.getenv("BUILD_NAME") is not None:
         build_name = str(os.getenv("BUILD_NAME"))
         build_name = str(os.getenv("BUILD_NAME"))
         if not silent:
         if not silent:
-            print(f"Using custom build name: '{build_name}'.")
+            print_info(f"Using custom build name: '{build_name}'.")
 
 
     import version
     import version
 
 
@@ -185,7 +248,7 @@ def get_version_info(module_version_string="", silent=False):
     if os.getenv("GODOT_VERSION_STATUS") is not None:
     if os.getenv("GODOT_VERSION_STATUS") is not None:
         version_info["status"] = str(os.getenv("GODOT_VERSION_STATUS"))
         version_info["status"] = str(os.getenv("GODOT_VERSION_STATUS"))
         if not silent:
         if not silent:
-            print(f"Using version status '{version_info['status']}', overriding the original '{version.status}'.")
+            print_info(f"Using version status '{version_info['status']}', overriding the original '{version.status}'.")
 
 
     # Parse Git hash if we're in a Git repo.
     # Parse Git hash if we're in a Git repo.
     githash = ""
     githash = ""
@@ -441,7 +504,7 @@ def use_windows_spawn_fix(self, platform=None):
 
 
 
 
 def no_verbose(env):
 def no_verbose(env):
-    colors = [ANSI.BLUE, ANSI.BOLD, ANSI.REGULAR, ANSI.RESET]
+    colors = [Ansi.BLUE, Ansi.BOLD, Ansi.REGULAR, Ansi.RESET]
 
 
     # There is a space before "..." to ensure that source file names can be
     # There is a space before "..." to ensure that source file names can be
     # Ctrl + clicked in the VS Code terminal.
     # Ctrl + clicked in the VS Code terminal.
@@ -792,7 +855,7 @@ def show_progress(env):
     # Progress reporting is not available in non-TTY environments since it messes with the output
     # Progress reporting is not available in non-TTY environments since it messes with the output
     # (for example, when writing to a file). Ninja has its own progress/tracking tool that clashes
     # (for example, when writing to a file). Ninja has its own progress/tracking tool that clashes
     # with ours.
     # with ours.
-    if not env["progress"] or not sys.stdout.isatty() or env["ninja"]:
+    if not env["progress"] or not IS_TTY or env["ninja"]:
         return
         return
 
 
     NODE_COUNT_FILENAME = f"{base_folder_path}.scons_node_count"
     NODE_COUNT_FILENAME = f"{base_folder_path}.scons_node_count"
@@ -1480,7 +1543,7 @@ def generate_copyright_header(filename: str) -> str:
 """
 """
     filename = filename.split("/")[-1].ljust(MARGIN)
     filename = filename.split("/")[-1].ljust(MARGIN)
     if len(filename) > MARGIN:
     if len(filename) > MARGIN:
-        print(f'WARNING: Filename "{filename}" too large for copyright header.')
+        print_warning(f'Filename "{filename}" too large for copyright header.')
     return TEMPLATE % filename
     return TEMPLATE % filename
 
 
 
 

+ 7 - 14
misc/scripts/install_d3d12_sdk_windows.py

@@ -6,16 +6,9 @@ import subprocess
 import sys
 import sys
 import urllib.request
 import urllib.request
 
 
-# Enable ANSI escape code support on Windows 10 and later (for colored console output).
-# <https://github.com/python/cpython/issues/73245>
-if sys.platform == "win32":
-    from ctypes import byref, c_int, windll
+sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../"))
 
 
-    stdout_handle = windll.kernel32.GetStdHandle(c_int(-11))
-    mode = c_int(0)
-    windll.kernel32.GetConsoleMode(c_int(stdout_handle), byref(mode))
-    mode = c_int(mode.value | 4)
-    windll.kernel32.SetConsoleMode(c_int(stdout_handle), mode)
+from methods import Ansi
 
 
 # Base Godot dependencies path
 # Base Godot dependencies path
 # If cross-compiling (no LOCALAPPDATA), we install in `bin`
 # If cross-compiling (no LOCALAPPDATA), we install in `bin`
@@ -49,7 +42,7 @@ if not os.path.exists(deps_folder):
     os.makedirs(deps_folder)
     os.makedirs(deps_folder)
 
 
 # Mesa NIR
 # Mesa NIR
-print("\x1b[1m[1/3] Mesa NIR\x1b[0m")
+print(f"{Ansi.BOLD}[1/3] Mesa NIR{Ansi.RESET}")
 if os.path.isfile(mesa_archive):
 if os.path.isfile(mesa_archive):
     os.remove(mesa_archive)
     os.remove(mesa_archive)
 print(f"Downloading Mesa NIR {mesa_filename} ...")
 print(f"Downloading Mesa NIR {mesa_filename} ...")
@@ -76,7 +69,7 @@ if dlltool == "":
     dlltool = shutil.which("x86_64-w64-mingw32-dlltool") or ""
     dlltool = shutil.which("x86_64-w64-mingw32-dlltool") or ""
 has_mingw = gendef != "" and dlltool != ""
 has_mingw = gendef != "" and dlltool != ""
 
 
-print("\x1b[1m[2/3] WinPixEventRuntime\x1b[0m")
+print(f"{Ansi.BOLD}[2/3] WinPixEventRuntime{Ansi.RESET}")
 if os.path.isfile(pix_archive):
 if os.path.isfile(pix_archive):
     os.remove(pix_archive)
     os.remove(pix_archive)
 print(f"Downloading WinPixEventRuntime {pix_version} ...")
 print(f"Downloading WinPixEventRuntime {pix_version} ...")
@@ -107,7 +100,7 @@ else:
 print(f"WinPixEventRuntime {pix_version} installed successfully.\n")
 print(f"WinPixEventRuntime {pix_version} installed successfully.\n")
 
 
 # DirectX 12 Agility SDK
 # DirectX 12 Agility SDK
-print("\x1b[1m[3/3] DirectX 12 Agility SDK\x1b[0m")
+print(f"{Ansi.BOLD}[3/3] DirectX 12 Agility SDK{Ansi.RESET}")
 if os.path.isfile(agility_sdk_archive):
 if os.path.isfile(agility_sdk_archive):
     os.remove(agility_sdk_archive)
     os.remove(agility_sdk_archive)
 print(f"Downloading DirectX 12 Agility SDK {agility_sdk_version} ...")
 print(f"Downloading DirectX 12 Agility SDK {agility_sdk_version} ...")
@@ -123,5 +116,5 @@ os.remove(agility_sdk_archive)
 print(f"DirectX 12 Agility SDK {agility_sdk_version} installed successfully.\n")
 print(f"DirectX 12 Agility SDK {agility_sdk_version} installed successfully.\n")
 
 
 # Complete message
 # Complete message
-print(f'\x1b[92mAll Direct3D 12 SDK components were installed to "{deps_folder}" successfully!\x1b[0m')
-print('\x1b[92mYou can now build Godot with Direct3D 12 support enabled by running "scons d3d12=yes".\x1b[0m')
+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}')

+ 5 - 21
modules/text_server_adv/gdextension_build/SConstruct

@@ -1,29 +1,13 @@
 #!/usr/bin/env python
 #!/usr/bin/env python
-from misc.utility.scons_hints import *
 
 
 import atexit
 import atexit
-import sys
 import time
 import time
+from typing import TYPE_CHECKING
 
 
 import methods
 import methods
 
 
-# Enable ANSI escape code support on Windows 10 and later (for colored console output).
-# <https://github.com/python/cpython/issues/73245>
-if sys.stdout.isatty() and sys.platform == "win32":
-    try:
-        from ctypes import WinError, byref, windll  # type: ignore
-        from ctypes.wintypes import DWORD  # type: ignore
-
-        stdout_handle = windll.kernel32.GetStdHandle(DWORD(-11))
-        mode = DWORD(0)
-        if not windll.kernel32.GetConsoleMode(stdout_handle, byref(mode)):
-            raise WinError()
-        mode = DWORD(mode.value | 4)
-        if not windll.kernel32.SetConsoleMode(stdout_handle, mode):
-            raise WinError()
-    except Exception as e:
-        methods._colorize = False
-        methods.print_error(f"Failed to enable ANSI escape code support, disabling color output.\n{e}")
+if TYPE_CHECKING:
+    from misc.utility.scons_hints import *
 
 
 # For the reference:
 # For the reference:
 # - CCFLAGS are compilation flags shared between C and C++
 # - CCFLAGS are compilation flags shared between C and C++
@@ -793,10 +777,10 @@ def print_elapsed_time():
     time_centiseconds = round((elapsed_time_sec % 1) * 100)
     time_centiseconds = round((elapsed_time_sec % 1) * 100)
     print(
     print(
         "{}[Time elapsed: {}.{:02}]{}".format(
         "{}[Time elapsed: {}.{:02}]{}".format(
-            methods.ANSI.GRAY,
+            methods.Ansi.GRAY,
             time.strftime("%H:%M:%S", time.gmtime(elapsed_time_sec)),
             time.strftime("%H:%M:%S", time.gmtime(elapsed_time_sec)),
             time_centiseconds,
             time_centiseconds,
-            methods.ANSI.RESET,
+            methods.Ansi.RESET,
         )
         )
     )
     )
 
 

+ 3 - 39
modules/text_server_adv/gdextension_build/methods.py

@@ -1,49 +1,13 @@
 import os
 import os
 import sys
 import sys
-from enum import Enum
 
 
-# Colors are disabled in non-TTY environments such as pipes. This means
-# that if output is redirected to a file, it won't contain color codes.
-# Colors are always enabled on continuous integration.
-_colorize = bool(sys.stdout.isatty() or os.environ.get("CI"))
+sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../../"))
 
 
-
-class ANSI(Enum):
-    """
-    Enum class for adding ansi colorcodes directly into strings.
-    Automatically converts values to strings representing their
-    internal value, or an empty string in a non-colorized scope.
-    """
-
-    RESET = "\x1b[0m"
-
-    BOLD = "\x1b[1m"
-    ITALIC = "\x1b[3m"
-    UNDERLINE = "\x1b[4m"
-    STRIKETHROUGH = "\x1b[9m"
-    REGULAR = "\x1b[22;23;24;29m"
-
-    BLACK = "\x1b[30m"
-    RED = "\x1b[31m"
-    GREEN = "\x1b[32m"
-    YELLOW = "\x1b[33m"
-    BLUE = "\x1b[34m"
-    MAGENTA = "\x1b[35m"
-    CYAN = "\x1b[36m"
-    WHITE = "\x1b[37m"
-
-    PURPLE = "\x1b[38;5;93m"
-    PINK = "\x1b[38;5;206m"
-    ORANGE = "\x1b[38;5;214m"
-    GRAY = "\x1b[38;5;244m"
-
-    def __str__(self) -> str:
-        global _colorize
-        return str(self.value) if _colorize else ""
+from methods import Ansi
 
 
 
 
 def no_verbose(env):
 def no_verbose(env):
-    colors = [ANSI.BLUE, ANSI.BOLD, ANSI.REGULAR, ANSI.RESET]
+    colors = [Ansi.BLUE, Ansi.BOLD, Ansi.REGULAR, Ansi.RESET]
 
 
     # There is a space before "..." to ensure that source file names can be
     # There is a space before "..." to ensure that source file names can be
     # Ctrl + clicked in the VS Code terminal.
     # Ctrl + clicked in the VS Code terminal.

+ 5 - 21
modules/text_server_fb/gdextension_build/SConstruct

@@ -1,29 +1,13 @@
 #!/usr/bin/env python
 #!/usr/bin/env python
-from misc.utility.scons_hints import *
 
 
 import atexit
 import atexit
-import sys
 import time
 import time
+from typing import TYPE_CHECKING
 
 
 import methods
 import methods
 
 
-# Enable ANSI escape code support on Windows 10 and later (for colored console output).
-# <https://github.com/python/cpython/issues/73245>
-if sys.stdout.isatty() and sys.platform == "win32":
-    try:
-        from ctypes import WinError, byref, windll  # type: ignore
-        from ctypes.wintypes import DWORD  # type: ignore
-
-        stdout_handle = windll.kernel32.GetStdHandle(DWORD(-11))
-        mode = DWORD(0)
-        if not windll.kernel32.GetConsoleMode(stdout_handle, byref(mode)):
-            raise WinError()
-        mode = DWORD(mode.value | 4)
-        if not windll.kernel32.SetConsoleMode(stdout_handle, mode):
-            raise WinError()
-    except Exception as e:
-        methods._colorize = False
-        methods.print_error(f"Failed to enable ANSI escape code support, disabling color output.\n{e}")
+if TYPE_CHECKING:
+    from misc.utility.scons_hints import *
 
 
 # For the reference:
 # For the reference:
 # - CCFLAGS are compilation flags shared between C and C++
 # - CCFLAGS are compilation flags shared between C and C++
@@ -335,10 +319,10 @@ def print_elapsed_time():
     time_centiseconds = round((elapsed_time_sec % 1) * 100)
     time_centiseconds = round((elapsed_time_sec % 1) * 100)
     print(
     print(
         "{}[Time elapsed: {}.{:02}]{}".format(
         "{}[Time elapsed: {}.{:02}]{}".format(
-            methods.ANSI.GRAY,
+            methods.Ansi.GRAY,
             time.strftime("%H:%M:%S", time.gmtime(elapsed_time_sec)),
             time.strftime("%H:%M:%S", time.gmtime(elapsed_time_sec)),
             time_centiseconds,
             time_centiseconds,
-            methods.ANSI.RESET,
+            methods.Ansi.RESET,
         )
         )
     )
     )
 
 

+ 3 - 39
modules/text_server_fb/gdextension_build/methods.py

@@ -1,49 +1,13 @@
 import os
 import os
 import sys
 import sys
-from enum import Enum
 
 
-# Colors are disabled in non-TTY environments such as pipes. This means
-# that if output is redirected to a file, it won't contain color codes.
-# Colors are always enabled on continuous integration.
-_colorize = bool(sys.stdout.isatty() or os.environ.get("CI"))
+sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../../"))
 
 
-
-class ANSI(Enum):
-    """
-    Enum class for adding ansi colorcodes directly into strings.
-    Automatically converts values to strings representing their
-    internal value, or an empty string in a non-colorized scope.
-    """
-
-    RESET = "\x1b[0m"
-
-    BOLD = "\x1b[1m"
-    ITALIC = "\x1b[3m"
-    UNDERLINE = "\x1b[4m"
-    STRIKETHROUGH = "\x1b[9m"
-    REGULAR = "\x1b[22;23;24;29m"
-
-    BLACK = "\x1b[30m"
-    RED = "\x1b[31m"
-    GREEN = "\x1b[32m"
-    YELLOW = "\x1b[33m"
-    BLUE = "\x1b[34m"
-    MAGENTA = "\x1b[35m"
-    CYAN = "\x1b[36m"
-    WHITE = "\x1b[37m"
-
-    PURPLE = "\x1b[38;5;93m"
-    PINK = "\x1b[38;5;206m"
-    ORANGE = "\x1b[38;5;214m"
-    GRAY = "\x1b[38;5;244m"
-
-    def __str__(self) -> str:
-        global _colorize
-        return str(self.value) if _colorize else ""
+from methods import Ansi
 
 
 
 
 def no_verbose(env):
 def no_verbose(env):
-    colors = [ANSI.BLUE, ANSI.BOLD, ANSI.REGULAR, ANSI.RESET]
+    colors = [Ansi.BLUE, Ansi.BOLD, Ansi.REGULAR, Ansi.RESET]
 
 
     # There is a space before "..." to ensure that source file names can be
     # There is a space before "..." to ensure that source file names can be
     # Ctrl + clicked in the VS Code terminal.
     # Ctrl + clicked in the VS Code terminal.

+ 2 - 2
platform/linuxbsd/detect.py

@@ -3,7 +3,7 @@ import platform
 import sys
 import sys
 from typing import TYPE_CHECKING
 from typing import TYPE_CHECKING
 
 
-from methods import get_compiler_version, print_error, print_warning, using_gcc
+from methods import get_compiler_version, print_error, print_info, print_warning, using_gcc
 from platform_methods import detect_arch, validate_arch
 from platform_methods import detect_arch, validate_arch
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
@@ -492,7 +492,7 @@ def configure(env: "SConsEnvironment"):
         else:
         else:
             # The default crash handler depends on glibc, so if the host uses
             # The default crash handler depends on glibc, so if the host uses
             # a different libc (BSD libc, musl), libexecinfo is required.
             # a different libc (BSD libc, musl), libexecinfo is required.
-            print("Note: Using `execinfo=no` disables the crash handler on platforms where glibc is missing.")
+            print_info("Using `execinfo=no` disables the crash handler on platforms where glibc is missing.")
     else:
     else:
         env.Append(CPPDEFINES=["CRASH_HANDLER_ENABLED"])
         env.Append(CPPDEFINES=["CRASH_HANDLER_ENABLED"])
 
 

+ 2 - 2
platform/web/detect.py

@@ -13,7 +13,7 @@ from emscripten_helpers import (
 )
 )
 from SCons.Util import WhereIs
 from SCons.Util import WhereIs
 
 
-from methods import get_compiler_version, print_error, print_warning
+from methods import get_compiler_version, print_error, print_info, print_warning
 from platform_methods import validate_arch
 from platform_methods import validate_arch
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
@@ -107,7 +107,7 @@ def configure(env: "SConsEnvironment"):
         env.Append(LINKFLAGS=["-sASSERTIONS=1"])
         env.Append(LINKFLAGS=["-sASSERTIONS=1"])
 
 
     if env.editor_build and env["initial_memory"] < 64:
     if env.editor_build and env["initial_memory"] < 64:
-        print("Note: Forcing `initial_memory=64` as it is required for the web editor.")
+        print_info("Forcing `initial_memory=64` as it is required for the web editor.")
         env["initial_memory"] = 64
         env["initial_memory"] = 64
 
 
     env.Append(LINKFLAGS=["-sINITIAL_MEMORY=%sMB" % env["initial_memory"]])
     env.Append(LINKFLAGS=["-sINITIAL_MEMORY=%sMB" % env["initial_memory"]])