ビルド前ゲートの cp932 クラッシュ — Mac で露見しない Windows 文字コード地雷と 3 層の回帰ガード
症状
v1.6.11 の Windows ビルド中に「リリースファイルの版番号整合性チェックに失敗」と表示された。ところがコードを確認しても版番号は正しく揃っている。
実体は版番号のズレではなかった。build_app.py のビルド前ゲートが subprocess で呼ぶ tools/bump_version.py の出力に含まれる絵文字(✅ 等)が、日本語 Windows の cp932 コードページでエンコードできず UnicodeEncodeError でクラッシュし、非 0 終了を「版番号不一致」と誤表示していた。
UnicodeEncodeError: 'cp932' codec can't encode character '✅' in position 0
Mac で露見しない理由
macOS のターミナルは標準で UTF-8 を使う。bump_version.py をそのまま動かしても絵文字は問題なく出力され、subprocess を介した呼び出しでも stdout は UTF-8 のまま流れる。
Windows の日本語環境ではコードページが cp932(Shift-JIS の拡張)になっており、Python プロセスのデフォルトエンコーディングも cp932 に引きずられる。✅(U+2705)は cp932 に存在しない文字なので、標準出力への書き込み時点で例外が発生する。Mac では一切再現しないため、ビルドを Mac で行っている開発フローでは長期間潜伏できる地雷だった。
修正 ① — 絵文字を ASCII に置換
tools/bump_version.py の出力に含まれていたすべての絵文字を ASCII に置き換えた。
# 修正前
print(f"✅ {file}: {old} → {new}")
print(f"❌ NG: {file}")
# 修正後
print(f"[OK] {file}: {old} → {new}")
print(f"[NG] {file}")
ツール出力に装飾的な絵文字を使うのは読みやすさに貢献するが、パイプを介して別プロセスが受け取る出力では文字コードの互換性が保証できない。特にビルドスクリプトから呼ばれる用途では ASCII に限定する方が安全。
修正 ② — subprocess を UTF-8 強制
build_app.py のゲート側でも subprocess 呼び出しを UTF-8 で固定した。
result = subprocess.run(
[sys.executable, "tools/bump_version.py", "--check"],
capture_output=True,
encoding="utf-8",
errors="replace",
env={**os.environ, "PYTHONUTF8": "1", "PYTHONIOENCODING": "utf-8"},
)
PYTHONUTF8=1 は Python 3.7+ で標準入出力を UTF-8 に強制する環境変数。PYTHONIOENCODING=utf-8 は旧来の方式で、どちらかが効けばよい二重保険。errors="replace" は万一エンコード不能文字が残っていてもクラッシュしないフォールバック。
修正 ③ — 回帰ガード test_windows_cp932_safety.py の 3 層設計
「修正したから OK」ではなく、同じクラッシュが再燃しないように 3 つの観点でテストを追加した。
第 1 層 — 静的検出: ソースファイルを読み込んで cp932 に存在しない文字が含まれていないかを機械的に確認する。
def test_bump_version_no_cp932_unsafe_chars():
src = pathlib.Path("tools/bump_version.py").read_text(encoding="utf-8")
for i, ch in enumerate(src):
try:
ch.encode("cp932")
except (UnicodeEncodeError, UnicodeDecodeError):
raise AssertionError(f"cp932 非対応文字 U+{ord(ch):04X} at pos {i}: {ch!r}")
第 2 層 — behavioral 検証: PYTHONIOENCODING=cp932 を設定した subprocess として実際に bump_version.py --check を動かし、終了コードが 0 であることを確認する。
def test_bump_version_check_runs_under_cp932():
result = subprocess.run(
[sys.executable, "tools/bump_version.py", "--check"],
capture_output=True,
env={**os.environ, "PYTHONIOENCODING": "cp932"},
)
assert result.returncode == 0, result.stderr
第 3 層 — negative check: 絵文字を意図的に注入したコードで第 1 層が確実に FAIL することを確認する。ガードが「常に通過する」テストになっていないことを保証する。
def test_cp932_detector_catches_emoji():
with pytest.raises(AssertionError):
src = "print('✅ ok')"
for ch in src:
ch.encode("cp932")
学び:Windows 専用エラーは渡す前に Mac で潰す
Mac のみで開発・ビルドを行う環境では、Windows 固有の文字コード問題は実機テストまで発覚しない。今回のように「Mac で正常動作する → Windows で初めてクラッシュ → ビルド失敗を別の問題として誤解する」という流れは起きやすい。
「Windows でしか出ないエラーがあるなら、Windows に渡す前に Mac 側で機械的に検知する」という発想でテストを設計すれば、実機ビルドの失敗ではなく CI レベルで事前に潰せる。