Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SCons: Refactor color output implementation #101249

Merged
merged 1 commit into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 12 additions & 32 deletions SConstruct
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@ EnsureSConsVersion(4, 0)
EnsurePythonVersion(3, 8)

# System
import atexit
import glob
import os
import pickle
import sys
import time
from collections import OrderedDict
from importlib.util import module_from_spec, spec_from_file_location
from types import ModuleType
Expand Down Expand Up @@ -52,13 +50,14 @@ _helper_module("platform_methods", "platform_methods.py")
_helper_module("version", "version.py")
_helper_module("core.core_builders", "core/core_builders.py")
_helper_module("main.main_builders", "main/main_builders.py")
_helper_module("misc.utility.color", "misc/utility/color.py")

# Local
import gles3_builders
import glsl_builders
import methods
import scu_builders
from methods import Ansi, print_error, print_info, print_warning
from misc.utility.color import STDERR_COLOR, print_error, print_info, print_warning
from platform_methods import architecture_aliases, architectures, compatibility_platform_aliases

if ARGUMENTS.get("target", "editor") == "editor":
Expand All @@ -74,8 +73,6 @@ platform_doc_class_path = {}
platform_exporters = []
platform_apis = []

time_at_start = time.time()

for x in sorted(glob.glob("platform/*")):
if not os.path.isdir(x) or not os.path.exists(x + "/detect.py"):
continue
Expand Down Expand Up @@ -702,6 +699,14 @@ if env["arch"] == "x86_32":
else:
env.Append(CCFLAGS=["-msse2"])

# Explicitly specify colored output.
if methods.using_gcc(env):
env.AppendUnique(CCFLAGS=["-fdiagnostics-color" if 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"])
if sys.platform == "win32":
env.AppendUnique(CCFLAGS=["-fansi-escape-codes"])

# Set optimize and debug_symbols flags.
# "custom" means do nothing and let users set their own optimization flags.
# Needs to happen after configure to have `env.msvc` defined.
Expand Down Expand Up @@ -1086,30 +1091,5 @@ methods.show_progress(env)
# TODO: replace this with `env.Dump(format="json")`
# once we start requiring SCons 4.0 as min version.
methods.dump(env)


def print_elapsed_time():
elapsed_time_sec = round(time.time() - time_at_start, 2)
time_centiseconds = round((elapsed_time_sec % 1) * 100)
print(
"{}[Time elapsed: {}.{:02}]{}".format(
Ansi.GRAY,
time.strftime("%H:%M:%S", time.gmtime(elapsed_time_sec)),
time_centiseconds,
Ansi.RESET,
)
)


atexit.register(print_elapsed_time)


def purge_flaky_files():
paths_to_keep = [env["ninja_file"]]
for build_failure in GetBuildFailures():
path = build_failure.node.path
if os.path.isfile(path) and path not in paths_to_keep:
os.remove(path)


atexit.register(purge_flaky_files)
methods.prepare_purge(env)
methods.prepare_timer()
7 changes: 4 additions & 3 deletions doc/tools/doc_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@

sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../"))

from methods import COLOR_SUPPORTED, Ansi, toggle_color
from misc.utility.color import STDOUT_COLOR, Ansi, toggle_color

################################################################################
# Config #
################################################################################

flags = {
"c": COLOR_SUPPORTED,
"c": STDOUT_COLOR,
"b": False,
"g": False,
"s": False,
Expand Down Expand Up @@ -330,7 +330,8 @@ def generate_for_class(c: ET.Element):
table_column_names.append("Docs URL")
table_columns.append("url")

toggle_color(flags["c"])
if flags["c"]:
toggle_color(True)

################################################################################
# Help #
Expand Down
5 changes: 3 additions & 2 deletions doc/tools/make_rst.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
sys.path.insert(0, root_directory := os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../"))

import version
from methods import Ansi, toggle_color
from misc.utility.color import Ansi, toggle_color

# $DOCS_URL/path/to/page.html(#fragment-tag)
GODOT_DOCS_PATTERN = re.compile(r"^\$DOCS_URL/(.*)\.html(#.*)?$")
Expand Down Expand Up @@ -697,7 +697,8 @@ def main() -> None:
)
args = parser.parse_args()

toggle_color(args.color)
if args.color:
toggle_color(True)

# Retrieve heading translations for the given language.
if not args.dry_run and args.lang != "en":
Expand Down
144 changes: 31 additions & 113 deletions methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,125 +7,16 @@
import subprocess
import sys
from collections import OrderedDict
from enum import Enum
from io import StringIO, TextIOWrapper
from pathlib import Path
from typing import Final, Generator, List, Optional, Union, cast
from typing import Generator, List, Optional, Union, cast

from misc.utility.color import print_error, print_info, print_warning

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

################################################################################
# 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.
COLOR_SUPPORTED: Final[bool] = _color_supported()
_can_color: bool = COLOR_SUPPORTED


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):
"""
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"
DIM = "\x1b[2m"
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"

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 _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)


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)


# Listing all the folders we have converted
# for SCU in scu_builders.py
_scu_folders = set()
Expand Down Expand Up @@ -505,6 +396,8 @@ def mySpawn(sh, escape, cmd, args, env):


def no_verbose(env):
from misc.utility.color import Ansi

colors = [Ansi.BLUE, Ansi.BOLD, Ansi.REGULAR, Ansi.RESET]

# There is a space before "..." to ensure that source file names can be
Expand Down Expand Up @@ -875,7 +768,7 @@ def __init__(self):

# Progress reporting is not available in non-TTY environments since it
# messes with the output (for example, when writing to a file).
self.display = cast(bool, self.max and env["progress"] and IS_TTY)
self.display = cast(bool, self.max and env["progress"] and sys.stdout.isatty())
if self.display and not self.max:
print_info("Performing initial build, progress percentage unavailable!")

Expand Down Expand Up @@ -1019,6 +912,31 @@ def prepare_cache(env) -> None:
atexit.register(clean_cache, cache_path, cache_limit, env["verbose"])


def prepare_purge(env):
from SCons.Script.Main import GetBuildFailures

def purge_flaky_files():
paths_to_keep = [env["ninja_file"]]
for build_failure in GetBuildFailures():
path = build_failure.node.path
if os.path.isfile(path) and path not in paths_to_keep:
os.remove(path)

atexit.register(purge_flaky_files)


def prepare_timer():
import time

def print_elapsed_time(time_at_start: float):
time_elapsed = time.time() - time_at_start
time_formatted = time.strftime("%H:%M:%S", time.gmtime(time_elapsed))
time_centiseconds = round((time_elapsed % 1) * 100)
print_info(f"Time elapsed: {time_formatted}.{time_centiseconds}")

atexit.register(print_elapsed_time, time.time())


def dump(env):
# Dumps latest build information for debugging purposes and external tools.
from json import dump
Expand Down
2 changes: 1 addition & 1 deletion misc/scripts/install_d3d12_sdk_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../"))

from methods import Ansi
from misc.utility.color import Ansi

# Base Godot dependencies path
# If cross-compiling (no LOCALAPPDATA), we install in `bin`
Expand Down
Loading