Repository: scikit-build/scikit-build-core
Pull request #844 (2024-08-02 17:56:58)

fix: expand some variables when constructing paths
Trying a potential fix for #843.


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/844?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) Report
Attention: Patch coverage is `75.00000%` with `1 line` in your changes missing coverage. Please review.
> Project coverage is 83.59%. Comparing base [(`74dd119`)](https://app.codecov.io/gh/scikit-build/scikit-build-core/commit/74dd1195e8c4b86e8f59378e8474cd30a4e11319?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) to head [(`b210c4e`)](https://app.codecov.io/gh/scikit-build/scikit-build-core/commit/b210c4eae87743cf4c47b472ca0d116275aa72a1?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build).

| [Files](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/844?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) | Patch % | Lines |
|---|---|---|
| [src/scikit\_build\_core/program\_search.py](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/844?src=pr&el=tree&filepath=src%2Fscikit_build_core%2Fprogram_search.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build#diff-c3JjL3NjaWtpdF9idWlsZF9jb3JlL3Byb2dyYW1fc2VhcmNoLnB5) | 75.00% | [1 Missing :warning: ](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/844?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) |

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main     #844   +/-   ##
=======================================
  Coverage   83.58%   83.59%           
=======================================
  Files          74       74           
  Lines        4259     4260    +1     
=======================================
+ Hits         3560     3561    +1     
  Misses        699      699           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/844?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build).


Patch:
diff --git a/docs/configuration.md b/docs/configuration.md
index 7f50d7f..ebb59b6 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -704,22 +704,25 @@ You can add a message to be printed after a successful or failed build. For exam
 
 ```toml
 [tool.scikit-build]
-messages.after-sucesss = "[green]Wheel successfully built"
+messages.after-sucesss = "{green}Wheel successfully built"
 messages.after-failure = """
-[red bold]Sorry[/bold], build failed.
+{bold.red}Sorry{normal}, build failed. Your platform is {platform.platform}.
 """
 ```
 
-This will be run through Python's formatter, so escape curly brackets. There
-currently are no items provided in the format call, but some may be added in the
-future if requested.
+This will be run through Python's formatter, so escape curly brackets if you
+need them.  Currently, there are several formatter-style keywords available:
+`sys`, `platform` (parenthesis will be added for items like `platform.platform`
+for you), `__version__` for scikit-build-core's version, and style keywords.
 
-A small mini-language for colorization using `[]` is also provided somewhat
-similar to [Rich](https://rich.readthedocs.io). Supported keywords inside the
-square brackets are `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, and
-`bold`.  These can be reset with a proceeding `/`. `reset` is also supported.
-Unsupported options will be ignored, but keep in mind other keywords from Rich
-might be added in the future.
+For styles, the colors are `default`, `red`, `green`, `yellow`, `blue`,
+`magenta`, `cyan`, and `white`. These can be accessed as `fg.*` or `bg.*`,
+without a qualifier the foreground is assumed. Styles like `normal`, `bold`,
+`italic`, `underline`, `reverse` are also provided. A full clearing of all
+styles is possible with `reset`. These all can be chained, as well, so
+`bold.red.bg.blue` is valid, and will produce an optimized escape code.
+Remember that you need to set the environment variable `FORCE_COLOR` to see
+colors with pip.
 
 ## Other options
 
diff --git a/pyproject.toml b/pyproject.toml
index 1b664a1..c52f396 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -252,6 +252,7 @@ extend-select = [
 ignore = [
     "PLE1205", # Format check doesn't work with our custom logger
     "PT004",   # Incorrect, just usefixtures instead.
+    "PT013",   # It's correct to import classes for typing!
     "RUF009",  # Too easy to get a false positive
     "PYI025",  # Wants Set to be renamed AbstractSet
     "ISC001",  # Conflicts with formatter
diff --git a/src/scikit_build_core/_logging.py b/src/scikit_build_core/_logging.py
index c60b420..68e6902 100644
--- a/src/scikit_build_core/_logging.py
+++ b/src/scikit_build_core/_logging.py
@@ -1,12 +1,26 @@
 from __future__ import annotations
 
 import contextlib
+import dataclasses
+import enum
 import functools
 import logging
 import os
-import re
+import platform
 import sys
-from typing import Any, NoReturn
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, NoReturn
+
+if TYPE_CHECKING:
+    from collections.abc import Iterator
+
+    from ._compat.typing import Literal, Self
+
+    StrMapping = Mapping[str, "Style"]
+else:
+    StrMapping = Mapping
+
+from . import __version__
 
 __all__ = [
     "ScikitBuildLogger",
@@ -16,6 +30,7 @@ __all__ = [
     "rich_warning",
     "rich_error",
     "LEVEL_VALUE",
+    "Style",
 ]
 
 
@@ -36,6 +51,15 @@ LEVEL_VALUE = {
 }
 
 
+class PlatformHelper:
+    def __getattr__(self, name: str) -> Any:
+        result = getattr(platform, name)
+        return result() if callable(result) else result
+
+    def __repr__(self) -> str:
+        return repr(platform)
+
+
 class FStringMessage:
     "This class captures a formatted string message and only produces it on demand."
 
@@ -95,74 +119,249 @@ class ScikitBuildLogger:
 logger = ScikitBuildLogger(raw_logger)
 
 
-ANY_ESCAPE = re.compile(r"\[([\w\s/]+)\]")
-
-
-_COLORS = {
-    "red": "\33[91m",
-    "green": "\33[92m",
-    "yellow": "\33[93m",
-    "blue": "\33[94m",
-    "magenta": "\33[95m",
-    "cyan": "\33[96m",
-    "bold": "\33[1m",
-    "/red": "\33[0m",
-    "/green": "\33[0m",
-    "/blue": "\33[0m",
-    "/yellow": "\33[0m",
-    "/magenta": "\33[0m",
-    "/cyan": "\33[0m",
-    "/bold": "\33[22m",
-    "reset": "\33[0m",
-}
-_NO_COLORS = {color: "" for color in _COLORS}
-
-
-def colors() -> dict[str, str]:
+def colors() -> bool:
     if "NO_COLOR" in os.environ:
-        return _NO_COLORS
+        return False
     # Pip reroutes sys.stdout, so FORCE_COLOR is required there
     if os.environ.get("FORCE_COLOR", ""):
-        return _COLORS
+        return True
     # Avoid ValueError: I/O operation on closed file
     with contextlib.suppress(ValueError):
         # Assume sys.stderr is similar to sys.stdout
         isatty = sys.stdout.isatty()
         if isatty and not sys.platform.startswith("win"):
-            return _COLORS
-    return _NO_COLORS
+            return True
+    return False
 
 
-def _sub_rich(m: re.Match[str]) -> str:
-    """
-    Replace rich-like tags, but only if they are defined in colors.
-    """
-    color_dict = colors()
-    try:
-        return "".join(color_dict[x] for x in m.group(1).split())
-    except KeyError:
-        return m.group(0)
+class Colors(enum.Enum):
+    black = 0
+    red = 1
+    green = 2
+    yellow = 3
+    blue = 4
+    magenta = 5
+    cyan = 6
+    white = 7
+    default = 9
+
+
+class Styles(enum.Enum):
+    bold = 1
+    italic = 3
+    underline = 4
+    reverse = 7
+    reset = 0
+    normal = 22
 
 
-def _process_rich(msg: object) -> str:
-    return ANY_ESCAPE.sub(
-        _sub_rich,
-        str(msg),
+@dataclasses.dataclass(frozen=True)
+class Style(StrMapping):
+    color: bool = dataclasses.field(default_factory=colors)
+    styles: tuple[int, ...] = dataclasses.field(default_factory=tuple)
+    current: int = 0
+
+    def __str__(self) -> str:
+        styles = ";".join(str(x) for x in self.styles)
+        return f"\33[{styles}m" if styles and self.color else ""
+
+    @property
+    def fg(self) -> Self:
+        return dataclasses.replace(self, current=30)
+
+    @property
+    def bg(self) -> Self:
+        return dataclasses.replace(self, current=40)
+
+    @property
+    def bold(self) -> Self:
+        return dataclasses.replace(self, styles=(*self.styles, Styles.bold.value))
+
+    @property
+    def italic(self) -> Self:
+        return dataclasses.replace(self, styles=(*self.styles, Styles.italic.value))
+
+    @property
+    def underline(self) -> Self:
+        return dataclasses.replace(self, styles=(*self.styles, Styles.underline.value))
+
+    @property
+    def reverse(self) -> Self:
+        return dataclasses.replace(self, styles=(*self.styles, Styles.reverse.value))
+
+    @property
+    def reset(self) -> Self:
+        return dataclasses.replace(self, styles=(Styles.reset.value,), current=0)
+
+    @property
+    def normal(self) -> Self:
+        return dataclasses.replace(self, styles=(*self.styles, Styles.normal.value))
+
+    @property
+    def black(self) -> Self:
+        return dataclasses.replace(
+            self, styles=(*self.styles, Colors.black.value + (self.current or 30))
+        )
+
+    @property
+    def red(self) -> Self:
+        return dataclasses.replace(
+            self, styles=(*self.styles, Colors.red.value + (self.current or 30))
+        )
+
+    @property
+    def green(self) -> Self:
+        return dataclasses.replace(
+            self, styles=(*self.styles, Colors.green.value + (self.current or 30))
+        )
+
+    @property
+    def yellow(self) -> Self:
+        return dataclasses.replace(
+            self, styles=(*self.styles, Colors.yellow.value + (self.current or 30))
+        )
+
+    @property
+    def blue(self) -> Self:
+        return dataclasses.replace(
+            self, styles=(*self.styles, Colors.blue.value + (self.current or 30))
+        )
+
+    @property
+    def magenta(self) -> Self:
+        return dataclasses.replace(
+            self, styles=(*self.styles, Colors.magenta.value + (self.current or 30))
+        )
+
+    @property
+    def cyan(self) -> Self:
+        return dataclasses.replace(
+            self, styles=(*self.styles, Colors.cyan.value + (self.current or 30))
+        )
+
+    @property
+    def white(self) -> Self:
+        return dataclasses.replace(
+            self, styles=(*self.styles, Colors.white.value + (self.current or 30))
+        )
+
+    @property
+    def default(self) -> Self:
+        return dataclasses.replace(
+            self, styles=(*self.styles, Colors.default.value + (self.current or 30))
+        )
+
+    _keys = (
+        "bold",
+        "italic",
+        "underline",
+        "reverse",
+        "reset",
+        "normal",
+        "black",
+        "red",
+        "green",
+        "yellow",
+        "blue",
+        "magenta",
+        "cyan",
+        "white",
+        "default",
     )
 
+    def __len__(self) -> int:
+        return len(self._keys)
 
-def rich_print(*args: object, **kwargs: object) -> None:
-    args_2 = tuple(_process_rich(arg) for arg in args)
-    if args != args_2:
-        args_2 = (*args_2[:-1], args_2[-1] + colors()["reset"])
-    print(*args_2, **kwargs, flush=True)  # type: ignore[call-overload] # noqa: T201
+    def __getitem__(self, name: str) -> Self:
+        return getattr(self, name)  # type: ignore[no-any-return]
 
+    def __iter__(self) -> Iterator[str]:
+        return iter(self._keys)
 
-@functools.lru_cache(maxsize=None)
-def rich_warning(*args: object, **kwargs: object) -> None:
-    rich_print("[red][yellow]WARNING:[/bold]", *args, **kwargs)
 
+_style = Style()
+_nostyle = Style(color=False)
 
-def rich_error(*args: object, **kwargs: object) -> NoReturn:
-    rich_print("[red][bold]ERROR:[/bold]", *args, **kwargs)
+
+def rich_print(
+    *args: object,
+    file: object = None,
+    sep: str = " ",
+    end: str = "\n",
+    color: Literal[
+        "", "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"
+    ] = "",
+    **kwargs: object,
+) -> None:
+    """
+    Print a message with style and useful common includes provided via formatting.
+
+    This function will process every argument with the following formatting:
+
+        - ``{__version__}``: The version of scikit-build-core.
+        - ``{platform}``: The platform module.
+        - ``{sys}``: The sys module.
+        - Colors and styles.
+
+    Any keyword arguments will be passed directly to the `str.format` method
+    unless they conflict with the above. ``print`` arguments work as normal, and
+    the output will be flushed.
+
+    Each argument will clear the style afterwards if a style is applied. The
+    ``color=`` argument will set a default color to apply to every argument, and
+    is available to arguments as ``{color}``.
+    """
+    if color:
+        kwargs["color"] = _style[color]
+
+    args_1 = tuple(str(arg) for arg in args)
+    args_1_gen = (
+        arg.format(
+            __version__=__version__,
+            platform=PlatformHelper(),
+            sys=sys,
+            **_nostyle,
+            **kwargs,
+        )
+        for arg in args_1
+    )
+    args_2_gen = (
+        arg.format(
+            __version__=__version__,
+            platform=PlatformHelper(),
+            sys=sys,
+            **_style,
+            **kwargs,
+        )
+        for arg in args_1
+    )
+    if color:
+        args_2 = (f"{_style[color]}{new}{_style.reset}" for new in args_2_gen)
+    else:
+        args_2 = (
+            new if new == orig else f"{new}{_style.reset}"
+            for new, orig in zip(args_2_gen, args_1_gen)
+        )
+    print(*args_2, flush=True, sep=sep, end=end, file=file)  # type: ignore[call-overload]
+
+
+@functools.lru_cache(maxsize=None)
+def rich_warning(
+    *args: str,
+    color: Literal[
+        "", "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"
+    ] = "yellow",
+    **kwargs: object,
+) -> None:
+    rich_print("{bold.yellow}WARNING:", *args, color=color, **kwargs)  # type: ignore[arg-type]
+
+
+def rich_error(
+    *args: str,
+    color: Literal[
+        "", "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"
+    ] = "red",
+    **kwargs: object,
+) -> NoReturn:
+    rich_print("{bold.red}ERROR:", *args, color=color, **kwargs)  # type: ignore[arg-type]
     raise SystemExit(7)
diff --git a/src/scikit_build_core/ast/ast.py b/src/scikit_build_core/ast/ast.py
index a83beef..0c736d8 100644
--- a/src/scikit_build_core/ast/ast.py
+++ b/src/scikit_build_core/ast/ast.py
@@ -87,9 +87,11 @@ def parse(
 if __name__ == "__main__":
     with Path(sys.argv[1]).open(encoding="utf-8-sig") as f:
         for node in parse(tokenize(f.read())):
+            node_name = node.name.replace("{", "{{").replace("}", "}}")
+            node_value = node.value.replace("{", "{{").replace("}", "}}")
             cnode = dataclasses.replace(
                 node,
-                name=f"[bold blue]{node.name}[/bold /blue]",
-                value=f"[green]{node.value}[/green]",
+                name=f"{{bold.blue}}{node_name}{{default}}",
+                value=f"{{green}}{node_value}{{default}}",
             )
             rich_print(cnode)
diff --git a/src/scikit_build_core/ast/tokenizer.py b/src/scikit_build_core/ast/tokenizer.py
index 556c1f7..961dcfa 100644
--- a/src/scikit_build_core/ast/tokenizer.py
+++ b/src/scikit_build_core/ast/tokenizer.py
@@ -77,5 +77,6 @@ if __name__ == "__main__":
     with Path(sys.argv[1]).open(encoding="utf-8-sig") as f:
         for token in tokenize(f.read()):
             rich_print(
-                f"[green]{token.type.name}[/green][red]([/red]{token.value}[red])[/red]"
+                "{green}{token.type.name}{red}({default}{token.value}{red})",
+                token=token,
             )
diff --git a/src/scikit_build_core/build/__init__.py b/src/scikit_build_core/build/__init__.py
index 7b75fa7..93cbc25 100644
--- a/src/scikit_build_core/build/__init__.py
+++ b/src/scikit_build_core/build/__init__.py
@@ -36,7 +36,7 @@ def build_wheel(
         ).wheel_filename
     except FailedLiveProcessError as err:
         sys.stdout.flush()
-        rich_print(f"\n[red bold]*** {' '.join(err.args)}", file=sys.stderr)
+        rich_print("\n{bold}***", *err.args, color="red", file=sys.stderr)
         if err.msg:
             rich_print(err.msg)
         raise SystemExit(1) from None
@@ -60,7 +60,7 @@ def build_editable(
         ).wheel_filename
     except FailedLiveProcessError as err:
         sys.stdout.flush()
-        rich_print(f"\n[red bold]*** {' '.join(err.args)}", file=sys.stderr)
+        rich_print("\n{bold}***", *err.args, color="red", file=sys.stderr)
         if err.msg:
             rich_print(err.msg)
         raise SystemExit(1) from None
@@ -103,7 +103,7 @@ def build_sdist(
         return skbuild_build_sdist(sdist_directory, config_settings)
     except FailedLiveProcessError as err:
         sys.stdout.flush()
-        rich_print(f"\n[red bold]*** {' '.join(err.args)}", file=sys.stderr)
+        rich_print("\n{bold}***", *err.args, color="red", file=sys.stderr)
         if err.msg:
             rich_print(err.msg)
         raise SystemExit(1) from None
diff --git a/src/scikit_build_core/build/sdist.py b/src/scikit_build_core/build/sdist.py
index 3c96748..554e807 100644
--- a/src/scikit_build_core/build/sdist.py
+++ b/src/scikit_build_core/build/sdist.py
@@ -93,8 +93,7 @@ def build_sdist(
     config_settings: dict[str, list[str] | str] | None = None,
 ) -> str:
     rich_print(
-        f"[green]***[/green] [bold][green]scikit-build-core {__version__}[/green]",
-        "[red](sdist)[/red]",
+        f"{{green}}***{{bold.green}} scikit-build-core {__version__}{{red}} (sdist)",
     )
 
     with Path("pyproject.toml").open("rb") as f:
diff --git a/src/scikit_build_core/build/wheel.py b/src/scikit_build_core/build/wheel.py
index ce23e96..86b513f 100644
--- a/src/scikit_build_core/build/wheel.py
+++ b/src/scikit_build_core/build/wheel.py
@@ -13,7 +13,6 @@ from typing import TYPE_CHECKING, Any
 from packaging.requirements import Requirement
 from packaging.utils import canonicalize_name
 
-from .. import __version__
 from .._compat import tomllib
 from .._compat.typing import Literal, assert_never
 from .._logging import LEVEL_VALUE, logger, rich_error, rich_print
@@ -151,7 +150,7 @@ def _build_wheel_impl(
 
     if settings_reader.settings.fail:
         if settings_reader.settings.messages.after_failure:
-            rich_print(settings_reader.settings.messages.after_failure.format())
+            rich_print(settings_reader.settings.messages.after_failure)
             raise SystemExit(7)
         rich_error("scikit-build-core's fail setting was enabled. Exiting immediately.")
 
@@ -184,11 +183,14 @@ def _build_wheel_impl(
             pyproject, config_settings or {}, state=state, retry=True
         )
         if "failed" not in settings_reader.overrides:
-            err.msg = settings_reader.settings.messages.after_failure.format()
+            err.msg = settings_reader.settings.messages.after_failure
             raise
 
         rich_print(
-            f"\n[yellow bold]*** {' '.join(err.args)} - retrying due to override..."
+            "\n***",
+            *err.args,
+            "- retrying due to override...",
+            color="yellow",
         )
 
         logger.setLevel(LEVEL_VALUE[settings_reader.settings.logging.level])
@@ -235,7 +237,7 @@ def _build_wheel_impl_impl(
 
     if settings.wheel.cmake:
         cmake = CMake.default_search(version=settings.cmake.version, env=os.environ)
-        cmake_msg = [f"using [blue]CMake {cmake.version}[/blue]"]
+        cmake_msg = [f"using {{blue}}CMake {cmake.version}{{default}}"]
     else:
         cmake = None
         cmake_msg = []
@@ -246,9 +248,9 @@ def _build_wheel_impl_impl(
         targetlib = "platlib" if settings.wheel.platlib else "purelib"
 
     rich_print(
-        f"[green]***[/green] [bold][green]scikit-build-core {__version__}[/green]",
+        "{green}*** {bold}scikit-build-core {__version__}",
         *cmake_msg,
-        f"[red]({state})[/red]",
+        f"{{red}}({state})",
     )
 
     with tempfile.TemporaryDirectory() as tmpdir, fix_win_37_all_permissions(tmpdir):
@@ -386,7 +388,7 @@ def _build_wheel_impl_impl(
                 config=config,
             )
 
-            rich_print("[green]***[/green] [bold]Configuring CMake...")
+            rich_print("{green}***", "{bold}Configuring CMake...")
             # Setting the install prefix because some libs hardcode CMAKE_INSTALL_PREFIX
             # Otherwise `cmake --install --prefix` would work by itself
             defines = {"CMAKE_INSTALL_PREFIX": install_dir}
@@ -411,13 +413,17 @@ def _build_wheel_impl_impl(
             )
             generator = builder.get_generator() or default_gen
             rich_print(
-                f"[green]***[/green] [bold]Building project with [blue]{generator}[/blue]..."
+                "{green}***",
+                f"{{bold}}Building project with {{blue}}{generator}{{default}}...",
             )
             build_args: list[str] = []
             builder.build(build_args=build_args)
 
             if not (editable and settings.editable.mode == "inplace"):
-                rich_print("[green]***[/green] [bold]Installing project into wheel...")
+                rich_print(
+                    "{green}***",
+                    "{bold}Installing project into wheel...",
+                )
                 builder.install(install_dir)
 
             if not builder.config.single_config and builder.config.build_type:
@@ -428,7 +434,7 @@ def _build_wheel_impl_impl(
 
         assert wheel_directory is not None
 
-        rich_print(f"[green]***[/green] [bold]Making {state}...")
+        rich_print("{green}***", f"{{bold}}Making {state}...")
         packages = _get_packages(
             packages=settings.wheel.packages,
             name=normalized_name,
@@ -499,9 +505,9 @@ def _build_wheel_impl_impl(
                 raise AssertionError(msg)
 
     wheel_filename: str = wheel.wheelpath.name
-    rich_print(f"[green]***[/green] [bold]Created[/bold] {wheel_filename}...")
+    rich_print("{green}***", f"{{bold}}Created{{normal}} {wheel_filename}...")
     if settings.messages.after_success:
-        rich_print(settings.messages.after_success.format())
+        rich_print(settings.messages.after_success)
     return WheelImplReturn(
         wheel_filename=wheel_filename, mapping=mapping, settings=settings
     )
diff --git a/src/scikit_build_core/builder/__main__.py b/src/scikit_build_core/builder/__main__.py
index d416b71..967d8d7 100644
--- a/src/scikit_build_core/builder/__main__.py
+++ b/src/scikit_build_core/builder/__main__.py
@@ -18,19 +18,21 @@ def __dir__() -> list[str]:
 
 
 def main() -> None:
-    rich_print(f"[bold]Scikit-build-core {__version__}[/bold] on Python {sys.version}")
+    rich_print(
+        f"{{bold}}Scikit-build-core {__version__}{{normal}} on Python {sys.version}"
+    )
 
     ip_sysconfig(color="green")
 
-    rich_print(f"[bold blue]Default Wheel Tag:[/bold] {WheelTag.compute_best([])}")
+    rich_print(f"{{bold.blue}}Default Wheel Tag:{{normal}} {WheelTag.compute_best([])}")
     rich_print(
-        "[blue] - Note: use [bold]python -m scikit_build_core.builder.wheel_tag -h[/bold] for further options[/blue]"
+        "{blue} - Note: use {bold}python -m scikit_build_core.builder.wheel_tag -h{normal} for further options"
     )
 
     if Path("pyproject.toml").is_file():
         req = GetRequires()
         all_req = [*req.cmake(), *req.ninja(), *req.dynamic_metadata()]
-        rich_print(f"[bold red]Get Requires:[/bold] {all_req!r}")
+        rich_print(f"{{bold.red}}Get Requires:{{normal}} {all_req!r}")
 
     ip_program_search(color="magenta")
 
diff --git a/src/scikit_build_core/builder/sysconfig.py b/src/scikit_build_core/builder/sysconfig.py
index ad6539c..6ac3b68 100644
--- a/src/scikit_build_core/builder/sysconfig.py
+++ b/src/scikit_build_core/builder/sysconfig.py
@@ -14,6 +14,8 @@ from .._logging import logger, rich_print
 if TYPE_CHECKING:
     from collections.abc import Mapping
 
+    from .._compat.typing import Literal
+
 __all__ = [
     "get_abi_flags",
     "get_cmake_platform",
@@ -200,31 +202,55 @@ def get_abi_flags() -> str:
     return "".join(sorted(abi_flags))
 
 
-def info_print(*, color: str = "") -> None:
+def info_print(
+    *,
+    color: Literal[
+        "", "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"
+    ] = "",
+) -> None:
     """
     Print information about the Python environment.
     """
-
     rich_print(
-        f"[bold {color}]Detected Python Library:[/bold] {get_python_library(os.environ, abi3=False)}"
+        "{bold}Detected Python Library:",
+        get_python_library(os.environ, abi3=False),
+        color=color,
+    )
+    rich_print(
+        "{bold}Detected ABI3 Python Library:",
+        get_python_library(os.environ, abi3=True),
+        color=color,
+    )
+    rich_print(
+        "{bold}Detected Python Include Directory:",
+        get_python_include_dir(),
+        color=color,
     )
     rich_print(
-        f"[bold {color}]Detected ABI3 Python Library:[/bold] {get_python_library(os.environ, abi3=True)}"
+        "{bold}Detected NumPy Include Directory:",
+        get_numpy_include_dir(),
+        color=color,
     )
     rich_print(
-        f"[bold {color}]Detected Python Include Directory:[/bold] {get_python_include_dir()}"
+        "{bold}Detected Platform:",
+        get_platform(),
+        color=color,
     )
     rich_print(
-        f"[bold {color}]Detected NumPy Include Directory:[/bold] {get_numpy_include_dir()}"
+        "{bold}Detected SOABI:",
+        get_soabi(os.environ, abi3=False),
+        color=color,
     )
-    rich_print(f"[bold {color}]Detected Platform:[/bold] {get_platform()}")
     rich_print(
-        f"[bold {color}]Detected SOABI:[/bold] {get_soabi(os.environ, abi3=False)}"
+        "{color}Detected ABI3 SOABI:",
+        get_soabi(os.environ, abi3=True),
+        color=color,
     )
     rich_print(
-        f"[bold {color}]Detected ABI3 SOABI:[/bold] {get_soabi(os.environ, abi3=True)}"
+        "{bold}Detected ABI flags:",
+        get_abi_flags(),
+        color=color,
     )
-    rich_print(f"[bold {color}Detected ABI flags:[/bold] {get_abi_flags()}")
 
 
 if __name__ == "__main__":
diff --git a/src/scikit_build_core/hatch/plugin.py b/src/scikit_build_core/hatch/plugin.py
index e6919da..f319928 100644
--- a/src/scikit_build_core/hatch/plugin.py
+++ b/src/scikit_build_core/hatch/plugin.py
@@ -17,7 +17,6 @@ from packaging.version import Version
 
 from scikit_build_core.settings.skbuild_model import ScikitBuildSettings
 
-from .. import __version__
 from .._compat.importlib.metadata import version as pkg_version
 from .._compat.typing import Literal
 from .._logging import logger, rich_print
@@ -143,9 +142,10 @@ class ScikitBuildHook(BuildHookInterface):  # type: ignore[type-arg]
         cmake = CMake.default_search(version=settings.cmake.version, env=os.environ)
 
         rich_print(
-            f"[green]***[/green] [bold][green]scikit-build-core {__version__}[/green]",
-            f"using [blue]CMake {cmake.version}[/blue]",
-            f"[red]({state})[/red]",
+            "{green}***",
+            "{bold.green}scikit-build-core {__version__}",
+            f"using {{blue}}CMake {cmake.version}",
+            f"{{red}}({state})",
         )
 
         self.__tmp_dir = Path(tempfile.mkdtemp()).resolve()
@@ -214,7 +214,7 @@ class ScikitBuildHook(BuildHookInterface):  # type: ignore[type-arg]
             config=config,
         )
 
-        rich_print("[green]***[/green] [bold]Configuring CMake...")
+        rich_print("{green}***", "{bold}Configuring CMake...")
         # Setting the install prefix because some libs hardcode CMAKE_INSTALL_PREFIX
         # Otherwise `cmake --install --prefix` would work by itself
         defines = {"CMAKE_INSTALL_PREFIX": install_dir}
@@ -237,12 +237,13 @@ class ScikitBuildHook(BuildHookInterface):  # type: ignore[type-arg]
         )
         generator = builder.get_generator() or default_gen
         rich_print(
-            f"[green]***[/green] [bold]Building project with [blue]{generator}[/blue]..."
+            "{green}***",
+            f"{{bold}}Building project with {{blue}}{generator}{{default}}...",
         )
         build_args: list[str] = []
         builder.build(build_args=build_args)
 
-        rich_print("[green]***[/green] [bold]Installing project into wheel...")
+        rich_print("{green}***", "{bold}Installing project into wheel...")
         builder.install(install_dir)
 
         files = list(wheel_dirs["headers"].iterdir())
diff --git a/src/scikit_build_core/program_search.py b/src/scikit_build_core/program_search.py
index 832c510..9b6cc75 100644
--- a/src/scikit_build_core/program_search.py
+++ b/src/scikit_build_core/program_search.py
@@ -2,6 +2,7 @@ from __future__ import annotations
 
 import contextlib
 import json
+import os.path
 import shutil
 import subprocess
 from pathlib import Path
@@ -17,6 +18,8 @@ if TYPE_CHECKING:
 
     from packaging.specifiers import SpecifierSet
 
+    from ._compat.typing import Literal
+
 __all__ = [
     "Program",
     "best_program",
@@ -50,7 +53,7 @@ def _get_cmake_path(*, module: bool = True) -> Generator[Path, None, None]:
     for candidate in candidates:
         cmake_path = shutil.which(candidate)
         if cmake_path is not None:
-            yield Path(cmake_path)
+            yield Path(os.path.expandvars(cmake_path))
 
 
 def _get_ninja_path(*, module: bool = True) -> Generator[Path, None, None]:
@@ -69,7 +72,7 @@ def _get_ninja_path(*, module: bool = True) -> Generator[Path, None, None]:
     for candidate in candidates:
         ninja_path = shutil.which(candidate)
         if ninja_path is not None:
-            yield Path(ninja_path)
+            yield Path(os.path.expandvars(ninja_path))
 
 
 def get_cmake_program(cmake_path: Path) -> Program:
@@ -154,7 +157,7 @@ def get_make_programs() -> Generator[Path, None, None]:
     for candidate in candidates:
         make_path = shutil.which(candidate)
         if make_path is not None:
-            yield Path(make_path)
+            yield Path(os.path.expandvars(make_path))
 
 
 def best_program(
@@ -173,17 +176,26 @@ def best_program(
     return None
 
 
-def info_print(*, color: str = "") -> None:
+def info_print(
+    *,
+    color: Literal[
+        "", "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"
+    ] = "",
+) -> None:
     """
     Print information about the program search.
     """
-    rich_print(f"[bold {color}]Detected CMake and Ninja[/bold] (all versions):")
+    rich_print("{bold}Detected CMake and Ninja{normal} (all versions):", color=color)
     for n, prog in enumerate(get_cmake_programs()):
-        s = " " if n else "*"
-        rich_print(f"{s} [bold {color}]CMake:[/bold] {prog.path} {prog.version!r}")
+        s = " " if n else "{default}*{color}"
+        rich_print(
+            f"{s} {{bold}}CMake:{{normal}} {prog.path} {prog.version!r}", color=color
+        )
     for n, prog in enumerate(get_ninja_programs()):
-        s = " " if n else "*"
-        rich_print(f"{s} [bold {color}]Ninja:[/bold] {prog.path} {prog.version!r}")
+        s = " " if n else "{default}*{color}"
+        rich_print(
+            f"{s} {{bold}}Ninja:{{normal}} {prog.path} {prog.version!r}", color=color
+        )
 
 
 if __name__ == "__main__":
diff --git a/src/scikit_build_core/settings/skbuild_read_settings.py b/src/scikit_build_core/settings/skbuild_read_settings.py
index caff05b..4f9880b 100644
--- a/src/scikit_build_core/settings/skbuild_read_settings.py
+++ b/src/scikit_build_core/settings/skbuild_read_settings.py
@@ -313,12 +313,14 @@ class SettingsReader:
             ]
             suggestions_dict = self.suggestions(index)
             if suggestions_dict:
-                rich_print(f"[red][bold]ERROR:[/bold] Unrecognized options in {name}:")
+                rich_print(
+                    f"{{bold.red}}ERROR:{{normal}} Unrecognized options in {name}:"
+                )
                 for option, suggestions in suggestions_dict.items():
-                    rich_print(f"  [red]{option}", end="")
+                    rich_print(f"  {{red}}{option}", end="")
                     if suggestions:
                         sugstr = ", ".join(suggestions)
-                        rich_print(f"[yellow] -> Did you mean: {sugstr}?", end="")
+                        rich_print(f"{{yellow}} -> Did you mean: {sugstr}?", end="")
                     rich_print()
 
     def validate_may_exit(self) -> None:


Test patch:
diff --git a/tests/test_cmake_config.py b/tests/test_cmake_config.py
index ddc170a..508db02 100644
--- a/tests/test_cmake_config.py
+++ b/tests/test_cmake_config.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import os
+import os.path
 import shutil
 from pathlib import Path
 from typing import TYPE_CHECKING
@@ -15,7 +16,7 @@ from scikit_build_core.errors import CMakeNotFoundError
 if TYPE_CHECKING:
     from collections.abc import Generator
 
-DIR = Path(__file__).parent.resolve()
+DIR = Path(os.path.expandvars(__file__)).parent.resolve()
 
 
 def configure_args(config: CMaker, *, init: bool = False) -> Generator[str, None, None]:
diff --git a/tests/test_logging.py b/tests/test_logging.py
index 6fd4828..1586271 100644
--- a/tests/test_logging.py
+++ b/tests/test_logging.py
@@ -1,32 +1,74 @@
-from scikit_build_core._logging import rich_print
+from __future__ import annotations
 
+import platform
+import sys
+from typing import TYPE_CHECKING
 
-def test_rich_print_nocolor(capsys, monkeypatch):
+if TYPE_CHECKING:
+    from pytest import CaptureFixture, MonkeyPatch
+
+import scikit_build_core._logging
+from scikit_build_core import __version__
+from scikit_build_core._logging import Style, rich_print
+
+
+def test_rich_print_nocolor(capsys: CaptureFixture[str], monkeypatch: MonkeyPatch):
     monkeypatch.setenv("NO_COLOR", "1")
     monkeypatch.delenv("FORCE_COLOR", raising=False)
-    rich_print("[red]hello[/red] world", end="")
+    monkeypatch.setattr(scikit_build_core._logging, "_style", Style())
+    rich_print("{red}hello{default} world", end="")
     assert capsys.readouterr().out == "hello world"
 
 
-def test_rich_print_nocolor_forcecolor(capsys, monkeypatch):
+def test_rich_print_nocolor_forcecolor(
+    capsys: CaptureFixture[str], monkeypatch: MonkeyPatch
+):
     monkeypatch.setenv("NO_COLOR", "1")
     monkeypatch.setenv("FORCE_COLOR", "1")
-    rich_print("[red]hello[/red] world", end="")
+    monkeypatch.setattr(scikit_build_core._logging, "_style", Style())
+    rich_print("{red}hello{default} world", end="")
     assert capsys.readouterr().out == "hello world"
 
 
-def test_rich_print_forcecolor(capsys, monkeypatch):
+def test_rich_print_forcecolor(capsys: CaptureFixture[str], monkeypatch: MonkeyPatch):
     monkeypatch.setenv("FORCE_COLOR", "1")
     monkeypatch.delenv("NO_COLOR", raising=False)
-    rich_print("[red bold]hello[/bold] world", end="")
-    assert capsys.readouterr().out == "\33[91m\33[1mhello\33[22m world\33[0m"
+    monkeypatch.setattr(scikit_build_core._logging, "_style", Style())
+    rich_print("{bold.red}hello{normal} world", end="")
+    assert capsys.readouterr().out == "\33[1;31mhello\33[22m world\33[0m"
 
 
-def test_rich_notrichbrackets(capsys, monkeypatch):
-    monkeypatch.setenv("FORCE_COLOR", "1")
-    monkeypatch.delenv("NO_COLOR", raising=False)
-    rich_print("[red bold]hello[/bold] world[/red] [notrich]", end="")
+def test_rich_print_fgbg(capsys: CaptureFixture[str], monkeypatch: MonkeyPatch):
+    monkeypatch.setattr(scikit_build_core._logging, "_style", Style(color=True))
+    rich_print("{bold.fg.red.bg.blue}hello world", end="")
+    assert capsys.readouterr().out == "\33[1;31;44mhello world\33[0m"
+
+
+def test_rich_notrichbrackets(capsys: CaptureFixture[str], monkeypatch: MonkeyPatch):
+    monkeypatch.setattr(scikit_build_core._logging, "_style", Style(color=True))
+    rich_print("{bold.red}hello{normal} world{default} [notrich]", end="")
     assert (
-        capsys.readouterr().out
-        == "\33[91m\33[1mhello\33[22m world\33[0m [notrich]\33[0m"
+        capsys.readouterr().out == "\33[1;31mhello\33[22m world\33[39m [notrich]\33[0m"
     )
+
+
+def test_rich_print_subs(capsys: CaptureFixture[str], monkeypatch: MonkeyPatch):
+    monkeypatch.setattr(scikit_build_core._logging, "_style", Style(color=True))
+    rich_print("{platform.platform} {__version__} {sys.version}", end="")
+    assert (
+        capsys.readouterr().out == f"{platform.platform()} {__version__} {sys.version}"
+    )
+
+
+def test_rich_print_each_color(capsys: CaptureFixture[str], monkeypatch: MonkeyPatch):
+    monkeypatch.setattr(scikit_build_core._logging, "_style", Style(color=True))
+    rich_print("hello", "world", color="green", end="")
+    assert capsys.readouterr().out == "\33[32mhello\33[0m \33[32mworld\33[0m"
+
+
+def test_rich_print_internal_color(
+    capsys: CaptureFixture[str], monkeypatch: MonkeyPatch
+):
+    monkeypatch.setattr(scikit_build_core._logging, "_style", Style(color=True))
+    rich_print("hello {color}world", color="green", end="")
+    assert capsys.readouterr().out == "\33[32mhello \33[32mworld\33[0m"