Repository: pantsbuild/pants

Issue:
Log color is broken sometimes
The decision of whether to use color is made very early in the startup process, while the default value for the `--[no-]colors` flag is being computed:
https://github.com/pantsbuild/pants/blob/af5fad7466465b71db1f9447f19918270b86e35c/src/python/pants/option/global_options.py#L533-L541

Instead, the `--[no-]colors` flag should become a ternary (either explicitly e.g. "yes, no, auto" or implicitly e.g. "true, false, None"), and in case of "auto", we should lazily detect the TTY per destination:
https://github.com/pantsbuild/pants/blob/af5fad7466465b71db1f9447f19918270b86e35c/src/rust/engine/logging/src/logger.rs#L198-L199 


Discussion:
I've noticed this. I think it's when pantsd is used. In fact, I don't think colors for the WARN part ever work for me with pantsd.
The way that we should likely solve this will be shaken up quite a bit by #11536, but it is still an issue.

Post #11536, we should likely convert the `--colors` flag into a ternary of `None/True/False`, and move the defaulting of the `None` case deeper into 1) the `Console` object at creation time based on `stdio` (which will have been replaced), and 2) the `logging` module, which should inspect whether the destination accepts color.
> we should likely convert the --colors flag into a ternary of None/True/False

What is `None` here? Auto-detect what to do?
> > we should likely convert the --colors flag into a ternary of None/True/False
> 
> What is `None` here? Auto-detect what to do?

Yea. So could convert into an actual ternary... would probably want to do `--dynamic-ui` at the same time. I didn't see a reason for it before, but the ternary allow us to delay deciding what we're going to do until post-options parsing as we launch the UI, or create a `Console`.

EDIT: Updated the description with this info.

Resolving patch:
diff --git a/src/python/pants/bin/local_pants_runner.py b/src/python/pants/bin/local_pants_runner.py
--- a/src/python/pants/bin/local_pants_runner.py
+++ b/src/python/pants/bin/local_pants_runner.py
@@ -30,6 +30,7 @@
 from pants.help.help_info_extracter import HelpInfoExtracter
 from pants.help.help_printer import HelpPrinter
 from pants.init.engine_initializer import EngineInitializer, GraphScheduler, GraphSession
+from pants.init.logging import stdio_destination_use_color
 from pants.init.options_initializer import OptionsInitializer
 from pants.init.specs_calculator import calculate_specs
 from pants.option.arg_splitter import HelpRequest
@@ -119,10 +120,22 @@ def create(
         build_config, options = options_initializer.build_config_and_options(
             options_bootstrapper, env, raise_=True
         )
+        stdio_destination_use_color(options.for_global_scope().colors)
 
         run_tracker = RunTracker(options_bootstrapper.args, options)
         union_membership = UnionMembership.from_rules(build_config.union_rules)
 
+        # Option values are usually computed lazily on demand, but command line options are
+        # eagerly computed for validation.
+        with options_initializer.handle_unknown_flags(options_bootstrapper, env, raise_=True):
+            for scope in options.scope_to_flags.keys():
+                options.for_scope(scope)
+
+        # Verify configs.
+        global_bootstrap_options = options_bootstrapper.bootstrap_options.for_global_scope()
+        if global_bootstrap_options.verify_config:
+            options.verify_configs(options_bootstrapper.config)
+
         # If we're running with the daemon, we'll be handed a warmed Scheduler, which we use
         # to initialize a session here.
         graph_session = cls._init_graph_session(
@@ -136,17 +149,6 @@ def create(
             cancellation_latch,
         )
 
-        # Option values are usually computed lazily on demand, but command line options are
-        # eagerly computed for validation.
-        with options_initializer.handle_unknown_flags(options_bootstrapper, env, raise_=True):
-            for scope in options.scope_to_flags.keys():
-                options.for_scope(scope)
-
-        # Verify configs.
-        global_bootstrap_options = options_bootstrapper.bootstrap_options.for_global_scope()
-        if global_bootstrap_options.verify_config:
-            options.verify_configs(options_bootstrapper.config)
-
         specs = calculate_specs(
             options_bootstrapper=options_bootstrapper,
             options=options,
diff --git a/src/python/pants/init/logging.py b/src/python/pants/init/logging.py
--- a/src/python/pants/init/logging.py
+++ b/src/python/pants/init/logging.py
@@ -1,6 +1,8 @@
 # Copyright 2018 Pants project contributors (see CONTRIBUTORS.md).
 # Licensed under the Apache License, Version 2.0 (see LICENSE).
 
+from __future__ import annotations
+
 import http.client
 import locale
 import logging
@@ -86,6 +88,19 @@ def stdio_destination(stdin_fileno: int, stdout_fileno: int, stderr_fileno: int)
         native_engine.stdio_thread_console_clear()
 
 
+def stdio_destination_use_color(use_color: bool) -> None:
+    """Sets a color mode for the current thread's destination.
+
+    True or false force color to be used or not used: None causes TTY detection to decide whether
+    color will be used.
+
+    NB: This method is independent from either `stdio_destination` or `initialize_stdio` because
+    we cannot decide whether to use color for a particular destination until it is open AND we have
+    parsed options for the relevant connection.
+    """
+    native_engine.stdio_thread_console_color_mode_set(use_color)
+
+
 @contextmanager
 def _python_logging_setup(
     level: LogLevel, *, print_stacktrace: bool, local_cleanup: bool
@@ -151,7 +166,6 @@ def initialize_stdio(global_bootstrap_options: OptionValueContainer) -> Iterator
     """
     global_level = global_bootstrap_options.level
     log_show_rust_3rdparty = global_bootstrap_options.log_show_rust_3rdparty
-    use_color = global_bootstrap_options.colors
     show_target = global_bootstrap_options.show_log_target
     log_levels_by_target = _get_log_levels_by_target(global_bootstrap_options)
     print_stacktrace = global_bootstrap_options.print_stacktrace
@@ -175,7 +189,6 @@ def initialize_stdio(global_bootstrap_options: OptionValueContainer) -> Iterator
         raw_stdin, sys.stdout, sys.stderr = native_engine.stdio_initialize(
             global_level.level,
             log_show_rust_3rdparty,
-            use_color,
             show_target,
             {k: v.level for k, v in log_levels_by_target.items()},
             tuple(literal_filters),
diff --git a/src/python/pants/option/global_options.py b/src/python/pants/option/global_options.py
--- a/src/python/pants/option/global_options.py
+++ b/src/python/pants/option/global_options.py
@@ -575,7 +575,8 @@ def register_bootstrap_options(cls, register):
             default=sys.stdout.isatty(),
             help=(
                 "Whether Pants should use colors in output or not. This may also impact whether "
-                "some tools Pants run use color."
+                "some tools Pants runs use color.\n\nWhen unset, this value defaults based on "
+                "whether the output destination supports color."
             ),
         )
 


Test patch: