SCons: Improve colored output

This commit is contained in:
Thaddeus Crews 2024-06-22 09:42:30 -05:00
parent a372214a4a
commit d8761f2c79
No known key found for this signature in database
GPG key ID: 62181B86FE9E5D84
12 changed files with 150 additions and 255 deletions

View file

@ -10,26 +10,63 @@ from collections import OrderedDict
from enum import Enum
from io import StringIO, TextIOWrapper
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
base_folder_path = str(os.path.abspath(Path(__file__).parent)) + "/"
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
# 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"))
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.
Automatically converts values to strings representing their
@ -39,6 +76,7 @@ class ANSI(Enum):
RESET = "\x1b[0m"
BOLD = "\x1b[1m"
DIM = "\x1b[2m"
ITALIC = "\x1b[3m"
UNDERLINE = "\x1b[4m"
STRIKETHROUGH = "\x1b[9m"
@ -53,24 +91,49 @@ class ANSI(Enum):
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"
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:
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:
"""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:
"""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):
@ -163,7 +226,7 @@ def get_version_info(module_version_string="", silent=False):
if os.getenv("BUILD_NAME") is not None:
build_name = str(os.getenv("BUILD_NAME"))
if not silent:
print(f"Using custom build name: '{build_name}'.")
print_info(f"Using custom build name: '{build_name}'.")
import version
@ -185,7 +248,7 @@ def get_version_info(module_version_string="", silent=False):
if os.getenv("GODOT_VERSION_STATUS") is not None:
version_info["status"] = str(os.getenv("GODOT_VERSION_STATUS"))
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.
githash = ""
@ -441,7 +504,7 @@ def use_windows_spawn_fix(self, platform=None):
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
# 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
# (for example, when writing to a file). Ninja has its own progress/tracking tool that clashes
# 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
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)
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