Merge pull request #104617 from Repiteo/scons/color-refactor

SCons: Refactor `color.py`
This commit is contained in:
Rémi Verschelde 2025-03-28 14:33:19 +01:00
commit 3886fd1422
No known key found for this signature in database
GPG Key ID: C3336907360768E1
6 changed files with 111 additions and 90 deletions

View File

@ -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"])

View File

@ -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 #

View File

@ -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":

View File

@ -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.

View File

@ -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".')

View File

@ -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:
_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
def is_stdout_color() -> bool:
return _stdout_override
def is_stderr_color() -> bool:
return _stderr_override
def force_stdout_color(value: bool) -> None:
"""
Validates if the current environment supports colored output. Attempts to enable ANSI escape
code support on Windows 10 and later.
Explicitly set `stdout` support for ANSI escape codes.
If environment overrides exist, does nothing.
"""
if IS_CI:
return True
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 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
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 toggle_color(stdout: bool, value: bool | None = None) -> 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.
"""
if stdout:
if not NO_COLOR or not CLICOLOR_FORCE:
global _stdout_override
_stdout_override = value if value is not None else not _stdout_override
else:
_stdout_override = value
def force_stderr_color(value: bool) -> None:
"""
Explicitly set `stderr` support for ANSI escape codes.
If environment overrides exist, does nothing.
"""
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()