Skip to content

Commit a29294f

Browse files
committed
SCons: Refactor color output implementation
1 parent d2ada64 commit a29294f

File tree

11 files changed

+211
-304
lines changed

11 files changed

+211
-304
lines changed

SConstruct

Lines changed: 12 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@ EnsureSConsVersion(4, 0)
55
EnsurePythonVersion(3, 8)
66

77
# System
8-
import atexit
98
import glob
109
import os
1110
import pickle
1211
import sys
13-
import time
1412
from collections import OrderedDict
1513
from importlib.util import module_from_spec, spec_from_file_location
1614
from types import ModuleType
@@ -52,13 +50,14 @@ _helper_module("platform_methods", "platform_methods.py")
5250
_helper_module("version", "version.py")
5351
_helper_module("core.core_builders", "core/core_builders.py")
5452
_helper_module("main.main_builders", "main/main_builders.py")
53+
_helper_module("misc.utility.color", "misc/utility/color.py")
5554

5655
# Local
5756
import gles3_builders
5857
import glsl_builders
5958
import methods
6059
import scu_builders
61-
from methods import Ansi, print_error, print_info, print_warning
60+
from misc.utility.color import STDERR_COLOR, print_error, print_info, print_warning
6261
from platform_methods import architecture_aliases, architectures, compatibility_platform_aliases
6362

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

77-
time_at_start = time.time()
78-
7976
for x in sorted(glob.glob("platform/*")):
8077
if not os.path.isdir(x) or not os.path.exists(x + "/detect.py"):
8178
continue
@@ -702,6 +699,14 @@ if env["arch"] == "x86_32":
702699
else:
703700
env.Append(CCFLAGS=["-msse2"])
704701

702+
# Explicitly specify colored output.
703+
if methods.using_gcc(env):
704+
env.AppendUnique(CCFLAGS=["-fdiagnostics-color" if STDERR_COLOR else "-fno-diagnostics-color"])
705+
elif methods.using_clang(env) or methods.using_emcc(env):
706+
env.AppendUnique(CCFLAGS=["-fcolor-diagnostics" if STDERR_COLOR else "-fno-color-diagnostics"])
707+
if sys.platform == "win32":
708+
env.AppendUnique(CCFLAGS=["-fansi-escape-codes"])
709+
705710
# Set optimize and debug_symbols flags.
706711
# "custom" means do nothing and let users set their own optimization flags.
707712
# Needs to happen after configure to have `env.msvc` defined.
@@ -1086,30 +1091,5 @@ methods.show_progress(env)
10861091
# TODO: replace this with `env.Dump(format="json")`
10871092
# once we start requiring SCons 4.0 as min version.
10881093
methods.dump(env)
1089-
1090-
1091-
def print_elapsed_time():
1092-
elapsed_time_sec = round(time.time() - time_at_start, 2)
1093-
time_centiseconds = round((elapsed_time_sec % 1) * 100)
1094-
print(
1095-
"{}[Time elapsed: {}.{:02}]{}".format(
1096-
Ansi.GRAY,
1097-
time.strftime("%H:%M:%S", time.gmtime(elapsed_time_sec)),
1098-
time_centiseconds,
1099-
Ansi.RESET,
1100-
)
1101-
)
1102-
1103-
1104-
atexit.register(print_elapsed_time)
1105-
1106-
1107-
def purge_flaky_files():
1108-
paths_to_keep = [env["ninja_file"]]
1109-
for build_failure in GetBuildFailures():
1110-
path = build_failure.node.path
1111-
if os.path.isfile(path) and path not in paths_to_keep:
1112-
os.remove(path)
1113-
1114-
1115-
atexit.register(purge_flaky_files)
1094+
methods.prepare_purge(env)
1095+
methods.prepare_timer()

doc/tools/doc_status.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@
1010

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

13-
from methods import COLOR_SUPPORTED, Ansi, toggle_color
13+
from misc.utility.color import STDOUT_COLOR, Ansi, toggle_color
1414

1515
################################################################################
1616
# Config #
1717
################################################################################
1818

1919
flags = {
20-
"c": COLOR_SUPPORTED,
20+
"c": STDOUT_COLOR,
2121
"b": False,
2222
"g": False,
2323
"s": False,
@@ -330,7 +330,8 @@ def generate_for_class(c: ET.Element):
330330
table_column_names.append("Docs URL")
331331
table_columns.append("url")
332332

333-
toggle_color(flags["c"])
333+
if flags["c"]:
334+
toggle_color(True)
334335

335336
################################################################################
336337
# Help #

doc/tools/make_rst.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
sys.path.insert(0, root_directory := os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../"))
1414

1515
import version
16-
from methods import Ansi, toggle_color
16+
from misc.utility.color import Ansi, toggle_color
1717

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

700-
toggle_color(args.color)
700+
if args.color:
701+
toggle_color(True)
701702

702703
# Retrieve heading translations for the given language.
703704
if not args.dry_run and args.lang != "en":

methods.py

Lines changed: 31 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -7,125 +7,16 @@
77
import subprocess
88
import sys
99
from collections import OrderedDict
10-
from enum import Enum
1110
from io import StringIO, TextIOWrapper
1211
from pathlib import Path
13-
from typing import Final, Generator, List, Optional, Union, cast
12+
from typing import Generator, List, Optional, Union, cast
13+
14+
from misc.utility.color import print_error, print_info, print_warning
1415

1516
# Get the "Godot" folder name ahead of time
1617
base_folder_path = str(os.path.abspath(Path(__file__).parent)) + "/"
1718
base_folder_only = os.path.basename(os.path.normpath(base_folder_path))
1819

19-
################################################################################
20-
# COLORIZE
21-
################################################################################
22-
23-
IS_CI: Final[bool] = bool(os.environ.get("CI"))
24-
IS_TTY: Final[bool] = bool(sys.stdout.isatty())
25-
26-
27-
def _color_supported() -> bool:
28-
"""
29-
Enables ANSI escape code support on Windows 10 and later (for colored console output).
30-
See here: https://github.com/python/cpython/issues/73245
31-
"""
32-
if sys.platform == "win32" and IS_TTY:
33-
try:
34-
from ctypes import WinError, byref, windll # type: ignore
35-
from ctypes.wintypes import DWORD # type: ignore
36-
37-
stdout_handle = windll.kernel32.GetStdHandle(DWORD(-11))
38-
mode = DWORD(0)
39-
if not windll.kernel32.GetConsoleMode(stdout_handle, byref(mode)):
40-
raise WinError()
41-
mode = DWORD(mode.value | 4)
42-
if not windll.kernel32.SetConsoleMode(stdout_handle, mode):
43-
raise WinError()
44-
except (TypeError, OSError) as e:
45-
print(f"Failed to enable ANSI escape code support, disabling color output.\n{e}", file=sys.stderr)
46-
return False
47-
48-
return IS_TTY or IS_CI
49-
50-
51-
# Colors are disabled in non-TTY environments such as pipes. This means
52-
# that if output is redirected to a file, it won't contain color codes.
53-
# Colors are always enabled on continuous integration.
54-
COLOR_SUPPORTED: Final[bool] = _color_supported()
55-
_can_color: bool = COLOR_SUPPORTED
56-
57-
58-
def toggle_color(value: Optional[bool] = None) -> None:
59-
"""
60-
Explicitly toggle color codes, regardless of support.
61-
62-
- `value`: An optional boolean to explicitly set the color
63-
state instead of toggling.
64-
"""
65-
global _can_color
66-
_can_color = value if value is not None else not _can_color
67-
68-
69-
class Ansi(Enum):
70-
"""
71-
Enum class for adding ansi colorcodes directly into strings.
72-
Automatically converts values to strings representing their
73-
internal value, or an empty string in a non-colorized scope.
74-
"""
75-
76-
RESET = "\x1b[0m"
77-
78-
BOLD = "\x1b[1m"
79-
DIM = "\x1b[2m"
80-
ITALIC = "\x1b[3m"
81-
UNDERLINE = "\x1b[4m"
82-
STRIKETHROUGH = "\x1b[9m"
83-
REGULAR = "\x1b[22;23;24;29m"
84-
85-
BLACK = "\x1b[30m"
86-
RED = "\x1b[31m"
87-
GREEN = "\x1b[32m"
88-
YELLOW = "\x1b[33m"
89-
BLUE = "\x1b[34m"
90-
MAGENTA = "\x1b[35m"
91-
CYAN = "\x1b[36m"
92-
WHITE = "\x1b[37m"
93-
94-
LIGHT_BLACK = "\x1b[90m"
95-
LIGHT_RED = "\x1b[91m"
96-
LIGHT_GREEN = "\x1b[92m"
97-
LIGHT_YELLOW = "\x1b[93m"
98-
LIGHT_BLUE = "\x1b[94m"
99-
LIGHT_MAGENTA = "\x1b[95m"
100-
LIGHT_CYAN = "\x1b[96m"
101-
LIGHT_WHITE = "\x1b[97m"
102-
103-
GRAY = LIGHT_BLACK if IS_CI else BLACK
104-
"""
105-
Special case. GitHub Actions doesn't convert `BLACK` to gray as expected, but does convert `LIGHT_BLACK`.
106-
By implementing `GRAY`, we handle both cases dynamically, while still allowing for explicit values if desired.
107-
"""
108-
109-
def __str__(self) -> str:
110-
global _can_color
111-
return str(self.value) if _can_color else ""
112-
113-
114-
def print_info(*values: object) -> None:
115-
"""Prints a informational message with formatting."""
116-
print(f"{Ansi.GRAY}{Ansi.BOLD}INFO:{Ansi.REGULAR}", *values, Ansi.RESET)
117-
118-
119-
def print_warning(*values: object) -> None:
120-
"""Prints a warning message with formatting."""
121-
print(f"{Ansi.YELLOW}{Ansi.BOLD}WARNING:{Ansi.REGULAR}", *values, Ansi.RESET, file=sys.stderr)
122-
123-
124-
def print_error(*values: object) -> None:
125-
"""Prints an error message with formatting."""
126-
print(f"{Ansi.RED}{Ansi.BOLD}ERROR:{Ansi.REGULAR}", *values, Ansi.RESET, file=sys.stderr)
127-
128-
12920
# Listing all the folders we have converted
13021
# for SCU in scu_builders.py
13122
_scu_folders = set()
@@ -505,6 +396,8 @@ def mySpawn(sh, escape, cmd, args, env):
505396

506397

507398
def no_verbose(env):
399+
from misc.utility.color import Ansi
400+
508401
colors = [Ansi.BLUE, Ansi.BOLD, Ansi.REGULAR, Ansi.RESET]
509402

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

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

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

1021914

915+
def prepare_purge(env):
916+
from SCons.Script.Main import GetBuildFailures
917+
918+
def purge_flaky_files():
919+
paths_to_keep = [env["ninja_file"]]
920+
for build_failure in GetBuildFailures():
921+
path = build_failure.node.path
922+
if os.path.isfile(path) and path not in paths_to_keep:
923+
os.remove(path)
924+
925+
atexit.register(purge_flaky_files)
926+
927+
928+
def prepare_timer():
929+
import time
930+
931+
def print_elapsed_time(time_at_start: float):
932+
time_elapsed = time.time() - time_at_start
933+
time_formatted = time.strftime("%H:%M:%S", time.gmtime(time_elapsed))
934+
time_centiseconds = round((time_elapsed % 1) * 100)
935+
print_info(f"Time elapsed: {time_formatted}.{time_centiseconds}")
936+
937+
atexit.register(print_elapsed_time, time.time())
938+
939+
1022940
def dump(env):
1023941
# Dumps latest build information for debugging purposes and external tools.
1024942
from json import dump

misc/scripts/install_d3d12_sdk_windows.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

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

11-
from methods import Ansi
11+
from misc.utility.color import Ansi
1212

1313
# Base Godot dependencies path
1414
# If cross-compiling (no LOCALAPPDATA), we install in `bin`

0 commit comments

Comments
 (0)