From 38ea2a57a522860c19296531c5aa475236747d2d Mon Sep 17 00:00:00 2001 From: ioannis Date: Tue, 21 Apr 2026 07:49:15 +0100 Subject: [PATCH] fix(web): handle non-UTF8 Windows console encodings in _build_web_ui MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review pointed out that even with the sync-assets fix applied, _build_web_ui still crashes on a stock Windows console before reaching npm: Python stdout defaults to cp1252 (or similar) and raises UnicodeEncodeError when print() hits the arrow/check glyphs used for status messages (→, ✗, ⚠, ✓). Reproduced locally in PowerShell: $ PYTHONIOENCODING=cp1252 python -c "from hermes_cli.main import _build_web_ui; _build_web_ui(Path('web'), fatal=True)" UnicodeEncodeError: 'charmap' codec can't encode character '\u2192' ... The previous PR body claimed "end-to-end verified on Windows 11", but that was under the venv's default (utf-8) stdout. A plain `py` or PowerShell invocation would still fail before sync-assets ever ran. Fix: inner _say() helper that falls back to text.encode(sys.stdout.encoding, errors="replace") when print() raises UnicodeEncodeError. Glyphs degrade to '?' on ASCII / cp1252 consoles; utf-8 consoles are unaffected. Verified the full build pipeline runs to completion with PYTHONIOENCODING=cp1252. Scoped tightly to _build_web_ui (the function this PR already touches); other call sites in the codebase with the same risk are out of scope. --- hermes_cli/main.py | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 3c027e908c5..e448e2b18ee 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -5681,13 +5681,25 @@ def _build_web_ui(web_dir: Path, *, fatal: bool = False) -> bool: if not _web_ui_build_needed(web_dir): return True + # Console-encoding-safe print: Windows consoles default to cp1252 + # (or similar) and will raise UnicodeEncodeError on arrow / check + # glyphs unless PYTHONIOENCODING=utf-8 is set. Routing every print + # in this function through _say() with errors="replace" keeps the + # build path usable on a stock `py -m hermes_cli.main web` invocation. + def _say(text: str) -> None: + try: + print(text) + except UnicodeEncodeError: + encoding = getattr(sys.stdout, "encoding", None) or "ascii" + print(text.encode(encoding, errors="replace").decode(encoding, errors="replace")) + npm = shutil.which("npm") if not npm: if fatal: - print("Web UI frontend not built and npm is not available.") - print("Install Node.js, then run: cd web && npm install && npm run build") + _say("Web UI frontend not built and npm is not available.") + _say("Install Node.js, then run: cd web && npm install && npm run build") return not fatal - print("→ Building web UI...") + _say("→ Building web UI...") def _relay(result: "subprocess.CompletedProcess") -> None: """Print captured npm output so users can see *why* a step failed. @@ -5702,17 +5714,17 @@ def _build_web_ui(web_dir: Path, *, fatal: bool = False) -> bool: continue text = blob.decode("utf-8", errors="replace").rstrip() if isinstance(blob, bytes) else blob.rstrip() if text: - print(text) + _say(text) r1 = _run_npm_install_deterministic(npm, web_dir, extra_args=("--silent",)) if r1.returncode != 0: - print( + _say( f" {'✗' if fatal else '⚠'} Web UI npm install failed" + ("" if fatal else " (hermes web will not be available)") ) _relay(r1) if fatal: - print(" Run manually: cd web && npm install && npm run build") + _say(" Run manually: cd web && npm install && npm run build") return False # First attempt r2 = subprocess.run( @@ -5747,20 +5759,20 @@ def _build_web_ui(web_dir: Path, *, fatal: bool = False) -> bool: # A stale UI is far better than no UI for non-interactive callers # (Windows Scheduled Tasks, CI) — issue #23817. if dist_index.exists(): - print(" ⚠ Web UI build failed — serving stale dist as fallback") + _say(" ⚠ Web UI build failed — serving stale dist as fallback") if stderr_tail: - print(f" Build error:\n {stderr_tail}") + _say(f" Build error:\n {stderr_tail}") return True - print( + _say( f" {'✗' if fatal else '⚠'} Web UI build failed" + ("" if fatal else " (hermes web will not be available)") ) _relay(r2) if fatal: - print(" Run manually: cd web && npm install && npm run build") + _say(" Run manually: cd web && npm install && npm run build") return False - print(" ✓ Web UI built") + _say(" ✓ Web UI built") return True