fix(windows): stop a PowerShell BOM breaking hook rebuilds (#3028)

Windows PowerShell 5.1 writes a UTF-8 BOM, so the .graphify_root marker it produced carried
a leading BOM that Python's utf-8 decode kept as U+FEFF; .strip() left it in, breaking the
post-commit rebuild (WinError 123) and silently mis-rooting _infer_merge_root. Write the
marker BOM-less from PowerShell (WriteAllText + UTF8Encoding $false) and read the marker
with utf-8-sig at the reader sites so already-broken installs recover too.
This commit is contained in:
rohit-jsfreaky
2026-08-25 18:06:46 +01:00
committed by safishamsi
parent 5687fe1bcc
commit 3c448660cd
10 changed files with 99 additions and 15 deletions
+1 -1
View File
@@ -439,7 +439,7 @@ def _infer_merge_root(graph_path: Path) -> str | None:
try:
marker = parent / ".graphify_root"
if marker.exists():
recorded = marker.read_text(encoding="utf-8").strip()
recorded = marker.read_text(encoding="utf-8-sig").strip()
if recorded:
return str(Path(recorded).resolve())
except OSError:
+2 -2
View File
@@ -165,7 +165,7 @@ try:
_out = os.environ.get('GRAPHIFY_OUT', 'graphify-out')
_saved = Path(_out) / '.graphify_root'
if _saved.exists():
_txt = _saved.read_text(encoding='utf-8').strip()
_txt = _saved.read_text(encoding='utf-8-sig').strip()
if _txt:
_root = Path(_txt)
_rebuild_code(_root, changed_paths=changed, force=_force)
@@ -214,7 +214,7 @@ try:
_out = os.environ.get('GRAPHIFY_OUT', 'graphify-out')
_saved = Path(_out) / '.graphify_root'
if _saved.exists():
_txt = _saved.read_text(encoding='utf-8').strip()
_txt = _saved.read_text(encoding='utf-8-sig').strip()
if _txt:
_root = Path(_txt)
_rebuild_code(_root, force=_force)
+1 -1
View File
@@ -691,7 +691,7 @@ def _resolve_source_path(src: str, graph_path: Path) -> Path | None:
out_dir = gp.parent
candidates: list[Path] = []
try:
recorded = (out_dir / ".graphify_root").read_text(encoding="utf-8").strip()
recorded = (out_dir / ".graphify_root").read_text(encoding="utf-8-sig").strip()
if recorded:
candidates.append(Path(recorded))
except (OSError, ValueError):
+8 -3
View File
@@ -116,10 +116,15 @@ if (-not $GRAPHIFY_PYTHON) {
$GRAPHIFY_PYTHON = Find-GraphifyPython
}
# Save interpreter path — all subsequent steps read this
$GRAPHIFY_PYTHON | Out-File -FilePath graphify-out\.graphify_python -Encoding utf8 -NoNewline
# Save interpreter path — all subsequent steps read this.
# `Out-File -Encoding utf8` always writes a BOM on Windows PowerShell 5.1 (utf8NoBOM
# only exists from PowerShell 6), and that BOM rides into the saved path, so the hook
# rebuild fails with WinError 123 (#3028). WriteAllText with an explicit BOM-less
# encoding writes the bytes POSIX writes, and adds no trailing newline.
$Utf8NoBom = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllText((Join-Path $PWD 'graphify-out\.graphify_python'), [string]$GRAPHIFY_PYTHON, $Utf8NoBom)
# Save scan root so `graphify update` (no args) knows where to look next time
(Resolve-Path INPUT_PATH).Path | Out-File -FilePath graphify-out\.graphify_root -Encoding utf8 -NoNewline
[System.IO.File]::WriteAllText((Join-Path $PWD 'graphify-out\.graphify_root'), (Resolve-Path INPUT_PATH).Path, $Utf8NoBom)
```
If the import succeeds, print nothing and move straight to Step 2.
+1 -1
View File
@@ -369,7 +369,7 @@ class _StoredSourcePaths:
root_marker = out / ".graphify_root"
if root_marker.exists():
try:
saved_root = Path(root_marker.read_text(encoding="utf-8").strip())
saved_root = Path(root_marker.read_text(encoding="utf-8-sig").strip())
if saved_root.is_absolute():
# #2603: the marker holds the SCAN root, but stored
# source_file values are relative to the BUILD's cwd
@@ -264,3 +264,27 @@ def test_prune_reextracted_absolute_node_not_deleted(tmp_path):
G = build_merge([new_chunk], graph_path, prune_sources=["mod.py"], dedup=False)
labels = {d["label"] for _, d in G.nodes(data=True)}
assert "gone" in labels, "re-extracted file wrongly pruned across mismatched forms (#2012/#1796)"
def test_graphify_root_marker_with_a_utf8_bom_still_resolves(tmp_path):
"""A marker written by Windows PowerShell 5.1 carries a UTF-8 BOM (#3028).
`Out-File -Encoding utf8` on 5.1 always prepends EF BB BF there is no
BOM-less utf8 there and `str.strip()` does not remove U+FEFF, so the BOM
survived into the recorded path. Worse than an error: `C:\...` is no
longer drive-qualified, so `Path.resolve()` treated it as relative and silently
joined it onto the cwd. Reading with `utf-8-sig` drops an optional BOM and
leaves a BOM-less file untouched, so existing broken checkouts heal in place.
"""
out = tmp_path / "out"
out.mkdir()
graph_path = out / "graph.json"
real_root = tmp_path / "elsewhere" / "repo"
real_root.mkdir(parents=True)
(out / ".graphify_root").write_bytes(
b"\xef\xbb\xbf" + str(real_root).encode("utf-8")
)
resolved = _infer_merge_root(graph_path)
assert resolved == str(real_root.resolve())
assert "" not in (resolved or "")
+24 -1
View File
@@ -320,7 +320,9 @@ def test_rebuild_bodies_read_graphify_root(name, body):
# The recovered root is what gets rebuilt, not a hardcoded cwd.
assert "_rebuild_code(_root" in body, f"{name} does not pass the recovered root"
# Quote-safe inside the shell-double-quoted launcher: single quotes only.
assert "read_text(encoding='utf-8')" in body, f"{name} root read is not single-quoted"
# `utf-8-sig` rather than `utf-8` so a Windows PowerShell 5.1 BOM cannot ride
# into the path (#3028); the codec is a no-op on a BOM-less marker.
assert "read_text(encoding='utf-8-sig')" in body, f"{name} root read is not single-quoted"
def test_rebuild_bodies_with_graphify_root_are_valid_python():
@@ -1144,3 +1146,24 @@ def test_both_hooks_configured(tmp_path):
for name in ("post-commit", "post-checkout"):
hook_text = (repo / ".git" / "hooks" / name).read_text()
assert 'export GRAPHIFY_VIZ_NODE_LIMIT="${GRAPHIFY_VIZ_NODE_LIMIT:-42}"' in hook_text
@pytest.mark.parametrize(
"name,body",
[("post-commit", _REBUILD_BODY_COMMIT), ("post-checkout", _REBUILD_BODY_CHECKOUT)],
)
def test_rebuild_bodies_tolerate_a_bom_in_graphify_root(name, body):
"""The rebuild must survive a marker written by Windows PowerShell 5.1 (#3028).
`Out-File -Encoding utf8` on 5.1 always writes a UTF-8 BOM, so the path the
hook reads back begins with U+FEFF. `strip()` does not remove it, and it rode
straight into a Windows path API: every post-commit rebuild died with
`WinError 123` while `hook install` / `hook status` still reported success.
`utf-8-sig` drops an optional BOM and is a no-op on a clean file.
"""
assert "encoding='utf-8-sig'" in body, (
f"{name} rebuild body must read .graphify_root BOM-tolerantly"
)
assert "encoding='utf-8')" not in body, (
f"{name} rebuild body still has a BOM-intolerant read"
)
+22
View File
@@ -1093,3 +1093,25 @@ def test_semantic_cache_calls_pass_prompt_file_for_every_split_host():
)
# The placeholder is inert unless the body tells the agent what to substitute.
assert "SPEC_PATH below is the **absolute** path" in a.content, a.path
def test_windows_skill_writes_marker_files_without_a_bom():
"""The Windows bootstrap must not write the sidecar markers with a BOM (#3028).
`Out-File -Encoding utf8` always emits EF BB BF on Windows PowerShell 5.1 --
`utf8NoBOM` only exists from PowerShell 6 -- and `-NoNewline` does nothing about
it. The BOM then rode into `.graphify_python` / `.graphify_root`, so every
post-commit rebuild died with WinError 123 while `hook install` and `hook status`
both still reported success. The readers now decode with `utf-8-sig`; this keeps
the writer from producing the BOM in the first place.
"""
core, _ = _platform_artifacts("windows")
for marker in (".graphify_python", ".graphify_root"):
assert f"Out-File -FilePath graphify-out\{marker} -Encoding utf8" not in core, (
f"the windows render still writes {marker} with a BOM-emitting Out-File"
)
assert f"WriteAllText((Join-Path $PWD 'graphify-out\{marker}')" in core, (
f"{marker} must be written through WriteAllText with a BOM-less encoding"
)
assert "New-Object System.Text.UTF8Encoding $false" in core, \
"the BOM-less encoding object must be constructed in the windows render"
@@ -116,10 +116,15 @@ if (-not $GRAPHIFY_PYTHON) {
$GRAPHIFY_PYTHON = Find-GraphifyPython
}
# Save interpreter path — all subsequent steps read this
$GRAPHIFY_PYTHON | Out-File -FilePath graphify-out\.graphify_python -Encoding utf8 -NoNewline
# Save interpreter path — all subsequent steps read this.
# `Out-File -Encoding utf8` always writes a BOM on Windows PowerShell 5.1 (utf8NoBOM
# only exists from PowerShell 6), and that BOM rides into the saved path, so the hook
# rebuild fails with WinError 123 (#3028). WriteAllText with an explicit BOM-less
# encoding writes the bytes POSIX writes, and adds no trailing newline.
$Utf8NoBom = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllText((Join-Path $PWD 'graphify-out\.graphify_python'), [string]$GRAPHIFY_PYTHON, $Utf8NoBom)
# Save scan root so `graphify update` (no args) knows where to look next time
(Resolve-Path INPUT_PATH).Path | Out-File -FilePath graphify-out\.graphify_root -Encoding utf8 -NoNewline
[System.IO.File]::WriteAllText((Join-Path $PWD 'graphify-out\.graphify_root'), (Resolve-Path INPUT_PATH).Path, $Utf8NoBom)
```
If the import succeeds, print nothing and move straight to Step 2.
+8 -3
View File
@@ -50,10 +50,15 @@ if (-not $GRAPHIFY_PYTHON) {
$GRAPHIFY_PYTHON = Find-GraphifyPython
}
# Save interpreter path — all subsequent steps read this
$GRAPHIFY_PYTHON | Out-File -FilePath graphify-out\.graphify_python -Encoding utf8 -NoNewline
# Save interpreter path — all subsequent steps read this.
# `Out-File -Encoding utf8` always writes a BOM on Windows PowerShell 5.1 (utf8NoBOM
# only exists from PowerShell 6), and that BOM rides into the saved path, so the hook
# rebuild fails with WinError 123 (#3028). WriteAllText with an explicit BOM-less
# encoding writes the bytes POSIX writes, and adds no trailing newline.
$Utf8NoBom = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllText((Join-Path $PWD 'graphify-out\.graphify_python'), [string]$GRAPHIFY_PYTHON, $Utf8NoBom)
# Save scan root so `graphify update` (no args) knows where to look next time
(Resolve-Path INPUT_PATH).Path | Out-File -FilePath graphify-out\.graphify_root -Encoding utf8 -NoNewline
[System.IO.File]::WriteAllText((Join-Path $PWD 'graphify-out\.graphify_root'), (Resolve-Path INPUT_PATH).Path, $Utf8NoBom)
```
If the import succeeds, print nothing and move straight to Step 2.