bun のコードページ問題を解決したい

bun#43660 は氷山の一角。根本の問題は bun がコンソールのコードページを変えることにある。その害を実測で示し、変えない実装を求める Issue を出す

📅 作成: 2026-10-03 / 更新: 2026-10-17

目的

  1. 以前に出した oven-sh/bun#43660 は氷山の一角。「bun 同士のパイプで化ける」は、表に出た症状の 1 つにすぎない
  2. 根本の問題は、bun がコンソールのコードページを変えること。コードページは窓ごとの設定で、同じ窓の全プロセスが共有している
  3. 変えることで、多くの害が出る。害を 1 つずつ実測で示す(「Issue の骨組み」の害の一覧)
  4. だから、コードページを変えないでほしい。コードページを変えた件そのものを問題として、新しい Issue を出す
  5. そうすれば bun#43660 も同時に直る。新しい Issue と bun#43660 の双方に、相互の関連を書き足す

Issue の骨組み

1. 背景

bun#43660 で、cp932 の窓では bun | bun の出力が化け、終了後に 65001 が残ると報告した。これは症状の 1 つだった。

2. 根本の問題

bun は実行中にコンソールの入力・出力コードページを書き換える。コードページは同じ窓の全プロセスで共有されるため、bun の外にまで影響が及ぶ。

3. 害の一覧と証拠

✅ 観測済み / ⬜ 未測定。

害証拠状態
H1パイプでつないだ相手の表示が化ける(bun#43660 の症状)。調査の 1.4.2・canary 版、T2・T5✅ 観測済み
H2結果が実行のたびに変わる。起動・終了の順で決まる。調査の 3 回の差、T5✅ 観測済み
H3終了後に 65001 が窓に残る。調査、T1・T5✅ 観測済み
H4同じ窓の別のプロセスが巻き込まれる。cmd が SJIS のスクリプトを読み違え、pause などのメッセージが英語になる。調査、T4✅ 観測済み
H5ほかの bun が窓を 932 に戻した後に、bun が UTF-8 のバイト列をそのまま書いて化ける(WriteConsoleW を使っていない)。調査の 1.4.2 の 3 回目、T5✅ 観測済み
H6強制終了すると元に戻らない。T3✅ 観測済み
H7bun 同士が直接つながっていなくても起きる(bun | node | bun)。T5✅ 観測済み

観測した内容は コードページ試験の結果 と bun#43660 の再現 にある。H2 の原因(起動・終了の順で決まる)は T5 で確かめた。

4. 依頼

コンソールのコードページを変えないでほしい。画面への出力は WriteConsoleW で UTF-16 のまま渡し、パイプ・ファイルにはバイト列をそのまま書けば、コードページに依存しない(「あるべき姿」)。node と deno が変えずに正しく動くことを、実装できる裏付けにする(T1・T2)。

5. bun#43660 も直る

コードページを変えなければ、H1〜H7 はどれも起きない。bun#43660 の症状(H1)もここに含まれる。

前提とあるべき姿

基準の窓

日本語版 Windows で、開いた直後の cmd の窓(コードページ 932)を基準にする。テストはコードページに一切触らない。

IMPORTANT: AIエージェントのセッションの窓は ConPTY 配下で、開始時点で 65001 になっている。そこで直接流すと基準の窓にならない。テストは conhost.exe で新しいコンソールを起こし、その中で走らせる。結果はファイルに書かせて読む。conhost.exe を名指しするのは、既定の端末が Windows Terminal だと新しい窓も ConPTY 配下になりうるため(未検証)。bun#43660 も Windows Terminal ではなく cmd.exe を直接使った条件で報告されている。

あるべき姿

相手書き方・読み方
コンソールへの出力WriteConsoleW で UTF-16 のまま渡す
パイプ・ファイルへの出力UTF-8 のバイト列をそのまま書く
コンソールからの入力ReadConsoleW で UTF-16 として読む
パイプからの入力届いたバイト列をそのまま受け取る

どれもコードページに依存しないため、書き換える理由がない。T1 で、node と deno がコードページを一度も変えずに 932 の窓へ日本語を正しく出すことを確かめた(内部で WriteConsoleW を使っているかまでは見ていない)。

bun#43660 で報告されていることと、この環境での再現

報告されていることこの環境
開始時のコードページが 65001 でないと、bun | bun で文字化けする再現した(安定版・canary 版とも)
片方が node なら起きない確かめた(T2: 片方が node か deno なら 65001 は残らない)
終了後に窓が 65001 のまま残る再現した。ただし残らない回もあった

使うランタイム

ランタイム版役割
bun1.4.3+c6da4a4d3調べる対象(安定版)
bun canary準備の時点の最新(全件の試験では 1.4.3-canary.1+bbdc5a519)調べる対象。_bin/bun-canary/ に置き、PATH を入れ替えて呼ぶ
bun PR #436621.4.3-canary.1+6b5271cb1bun#43660 への修正の PR の版。何が直り、何が残るかを見る。_bin/bun-pr-43662/ に置き、PATH を入れ替えて呼ぶ
nodev26.10.0比べる相手
deno2.9.7比べる相手

テスト項目

期待値

「あるべき姿」の実装なら、全項目で次のとおりになる。

項目示す害期待値
T1H3実行前・実行中・終了後のすべてで、入力・出力とも 932 のまま
T2H127 通りすべてで、受け手に UTF-8 のバイト列がそのまま届く(Windows PowerShell 5.1 経由の分は node | node と同じ結果になる)
T3H6強制終了の後も 932 のまま
T4H4SJIS の cmd が日本語を正しく出す
T5H1・H2・H3・H7起動・終了の順に関わらず、画面は abc 東京大阪 xyz と正しく出て、終了後も 932 のまま
T6全部安定版・canary 版とも、T1〜T5 の期待値どおり

T1. コードページの推移

基準の窓で、実行前・実行中・終了後の GetConsoleCP / GetConsoleOutputCP を記録する。実行中の値は各ランタイム自身が FFI で読む(読むだけ)。bun は bun:ffi、node は koffi、deno は Deno.dlopen。期待値は全時点で 932。

T2. パイプの組み合わせ

軸値
送り手bun / node / deno
受け手bun / node / deno
起動元cmd / pwsh 7 / Windows PowerShell 5.1(いずれも基準の窓の中から起動)

3 × 3 × 3 = 27 通り。受け手は受け取ったバイト列をそのままファイルへ書き、期待する UTF-8 のバイト列と突き合わせる。画面の見た目では判定しない。

NOTE: Windows PowerShell 5.1 は、bun を含まないネイティブコマンド同士のパイプでも非 ASCII が ? になる既知の事象がある。5.1 の行は node | node を基準にし、bun 固有の差だけを見る。

T3. 強制終了後の残留

待機中の bun を taskkill /F で止め、窓が 932 に戻っているかを確かめる。

T4. 同じ窓の隣のプロセスへの影響

bun | bun の直後(通常終了・強制終了)に、SJIS で書いた cmd が日本語を正しく出すかを確かめる。

T5. 順番を固定した再現

2 本以上のプロセスが起動・終了する順番を待ち時間で決め、偶然に頼らずに結果を出す。各プロセスは起動直後・書く直前・終了直前に GetConsoleCP / GetConsoleOutputCP を時刻つきでログへ書く(読むだけ)。送る文字列は abc 東京大阪 xyz にする。偶数の文字数(4 文字・12 バイト)で、CP932 として読まれても 2 バイトずつ別の文字に読み替わり、壊れたバイトが残らない。化けると abc 譚ア莠ャ螟ァ髦ェ xyz になるため、前後の ASCII と改行は残り、正しい形と化けた形を機械的に見分けられる(node の TextDecoder('shift_jis') による計算。コンソールの表示とは未照合)。

形順番の決め方確かめること
bun | bun起動の順は片側の起動前に ping -n 2 127.0.0.1 >nul、終了の順はスクリプト内の setTimeout起動の順 × 終了の順 × 画面に書く/書かない
bun | node wait | bun間の node が受け取ったデータを決めた時間だけ持ってから流すbun 同士が直接つながっていなくても起きるか(H7)
node | node wait | node同上比べる相手
deno | node wait | deno同上比べる相手
bun#43660 と同じ形順番を決めない(-e で 2 本を同時に起動)。10 回流す結果が回ごとに変わるか(H2)
ww を別の書き方でww の順で、受け手が fs.writeSync(1) ・ fs.write(1) ・ Bun.write(Bun.stdout) で画面に書く。bun の版だけPR #43662 が手当てしていない書き方で化けるか

仮説(各 bun が起動時に今の値を控えて 65001 にし、終了時に控えた値へ戻す)から予想する bun | bun の結果:

起動の順終了の順画面終了後
送り手が先送り手が先化ける65001 が残る
送り手が先受け手が先正しい932 に戻る
受け手が先送り手が先正しい932 に戻る
受け手が先受け手が先正しい65001 が残る

各組を 3 回ずつ流し、回の順に結果を並べる。経緯は bun#43660 の再現 を参照。

T6. 版の比較

T1〜T5 を bun の安定版と canary 版の両方で流す。node ・ deno は基準として版を 1 つに固定し、記録する。

テストの作り

tests/
  codepage.test.ts      T1〜T5 本体(node:test、bun test でも流す)。bun と canary を同じ表の列に並べて T6 にする
  fixtures/
    console.ts          コードページを読むだけの共通部品(bun:ffi / koffi / Deno.dlopen)
    emit.ts             abc 東京大阪 xyz を console.log で出す送り手
    sink.ts             受け取ったバイト列を画面かファイルへそのまま書く受け手
    hold.ts             受け取ったデータを決めた時間だけ持ってから流す中継(T5 の node wait)
    probe.ts            観測役(node)。コードページを記録し、窓の画面の文字を読み取る
    neighbor-932.cmd    932 用の隣のプロセス役(SJIS + CRLF)
    neighbor-437.cmd    437 用の隣のプロセス役(CP437 + CRLF)
  harness/
    new-console.ts      conhost.exe で新しいコンソールを起こし、その中で実行して終わるまで待つ
    judge.ts            画面・バイト列・コードページの判定と、証拠の置き場の results.jsonl への記録
  manual/
    issue-43660-repro.cmd   bun#43660 と同じ形のコマンドを手で流す(画面を目で見る)
tools/10_setup/
  setup-runtimes.ps1 / .cmd   準備フェーズ。ランタイムを _bin/ に取り、npm ci を流す
  fetch-bun-pr.ts       bun の PR の Windows x64 のビルドを、bun の CI(Buildkite)から取る
tools/40_test/
  run-tests.ps1 / run-tests.cmd   試験実施フェーズ。932 と 437 で、node --test と bun test を流す
tools/50_run/
  summarize-results.ts  証拠の置き場の記録から、結果表の材料を出す
  compare-evidence.ts   research/evidence_last/ と research/evidence/ の判定を比べる
  build-result-tables.ts research/evidence/ から結果資料の表を作り、証拠ファイルと試験コードへリンクする
  verify-issue-repro.ts Issue の本文に載せる再現コマンドを新しい窓で流し、画面を出す

どの PC でも同じ試験を流す

リポジトリを clone した人が、Windows なら誰でも同じ試験を流せるようにする。その PC に入っている bun ・ node ・ deno には頼らない。

準備フェーズ

tools/10_setup/setup-runtimes.cmd(中身は同名の ps1)が、版を決めた実行ファイルを公式の配布元から取り、_bin/ に置く。_bin/ は Git の管理外。すでに置いてあれば取り直さない。

ランタイム版置き場
bun1.4.3_bin/bun/
bun canary取った時点の最新(版を固定できないので、取った版を記録する)_bin/bun-canary/
nodev26.10.0_bin/node/
deno2.9.7_bin/deno/
pwsh7.6.6_bin/pwsh/
bun PR #43662PR の先頭コミットのビルド_bin/bun-pr-43662/。bun の CI(Buildkite)の成果物を tools/10_setup/fetch-bun-pr.ts で取り、SHA-1 を照らし合わせる

取ったあと、_bin/node/ の npm で npm ci を流す(koffi と型チェック用)。

試験実施フェーズ

  1. tools/40_test/run-tests.cmd が set PATH=…\_bin\…;%PATH% で、取った実行ファイルを PATH に置く
  2. where で、bun ・ node ・ deno ・ pwsh が _bin/ の下のものに解決されることを確かめる。違えば止める
  3. 各ランタイムの版(bun --revision ・ node -v ・ deno --version ・ pwsh の版)を versions.txt に書く
  4. 全件を流す。ケースごとのバッチ(run.cmd)は、実行ファイルを名前だけで呼び、パスは %~dp0 からの相対にする。ユーザ名を含むパスはどこにも入らない

決めたこと

証拠の置き場

置き場扱い
research/evidence_last/流すたびに上書きする。Git で除外しないが、commit しない
research/evidence/基準として commit する。evidence_last を確かめてから写す。再試験のあとは、指示を待たずに写す(ローカルルール)

あわせて行うこと

進め方

  1. deno を導入する(済: 2.9.7)
  2. fixtures とハーネスを作り、T1 で新しいコンソールの仕組みが動くことを確かめる(済)
  3. T1 → T2 → T3 → T4 → T5 の順に 1 項目ずつ足し、そのつど全件を流す(済)
  4. 結果の表を英語版・日本語版の資料にまとめ、害の一覧(H1〜H7)の状態を更新する(済: コードページ試験の結果)
  5. T6 で版を比べる(済: 安定版と canary 版を同じ表に並べた)
  6. 「Issue の骨組み」に沿って本文案を作り、ローカルルール「Issue と公開」の条件を満たしてから出す(済: bun#44693 として投稿した。本文は Issue bun#44693)
  7. bun#43660 に、新しい Issue への関連を書き足す(済: コメント)
  8. bun#43660 への修正の PR #43662 の版を試験一式で測り、直る害と残る害を #44693 にコメントする(済: 結果資料の結論に PR の列を足した。#44693 へのコメントと PR #43662 へのコメントとして投稿した。本文は Issue bun#44693。PR を見落としたまま #44693 を出したため)
  9. PR #43662 での robobun の問い(65001 が残った回の、全プロセスの起動と終了の順)に、時系列を測り直して答える(済: 60 回流して 結果資料 に足し、PR #43662 への返信として投稿した。本文は Issue bun#44693)
  10. 65001 が残る順番を PR #43662 のソースから導き、PR に伝える(済: 結果資料に足し、PR #43662 への続報として投稿した。本文は Issue bun#44693)
  11. どの PC でも同じ試験を流せる仕組みにし、証拠を research/evidence/ に残す(済。932 と 437 で全件を流し、research/evidence/ に写した。結果資料の表の ✅ / ❌ から証拠ファイルへリンクした)
  12. PR #43662 で 65001 が残る順番を、呼び出しを 1 つずつ記録して確かめる。あわせて 437 での時系列と、5.1 が間に入ると残らない理由を測る(済: 結果資料の呼び出しを 1 つずつ記録した結果と 時系列の表、T2 の 5.1 の項に足した。PR への続報は送信済みで、robobun から 10/17 に返信があり、それに 6b5271c の結果を返した)

返事を見張る