単純なバグ修正が引き起こした悪夢

この問題は、実は単純なところから始まりました。Stim(量子回路シミュレーション用 Python ライブラリ)の開発チームが、インターンとともに新機能をテストしていたときのこと。

start="auto" というパラメータを持つフロー(処理フロー)を名前付きで追加すると失敗するというバグを発見したのです。(出典)

バグ修正の PR を出した途端、Windows ビルドだけでなく、すべてのプラットフォームのビルドが失敗し始めた。

プルリクエストを作成して修正を提案したのですが、その直後から事態は急変します。Windows ビルドが失敗しただけでなく、すべての PR でビルドが崩壊。

単体テストの途中で「access violation(アクセス違反)」という曖昧かつ不吉なエラーが発生するようになったのです。これはセキュリティ脆弱性にも繋がる可能性のある深刻な問題でした。

Python パッケージビルドの複雑さ

この問題を理解するには、まず現在の Python パッケージビルドプロセスの複雑さを知る必要があります。

Stim は依存関係を最小限に抑えるという原則で設計されていました。しかしビルドシステム自体には多くの依存関係が存在していたのです。

現在、Python パッケージをビルドする推奨手法は以下のようなレイヤー構造になっています:

  • Docker コンテナ化:システム詳細がパッケージに紛れ込むのを防ぐため、コンテナ内でビルドする
  • auditwheel の実行:パッケージの互換性問題を自動修正する追加ツール
  • cibuildwheel の活用:複数プラットフォーム対応を自動化(Stim が使用)

Windows 特有の問題も複雑です:

  • C++ コンパイラの位置を特定するのに vswhere.exe というツールが必要
  • さらに標準ヘッダー(iostream など)を見つけるために vcvarsall.bat を実行する必要がある
  • この設定を経てもコンパイラが標準ヘッダーを見つけられないことが多い
ビルド段階 ツール・手法 課題
基本ビルド cibuildwheel 複雑な処理(SIMD 検出など)に対応しにくい
Windows 対応 vswhere.exe + vcvarsall.bat 複数の手順が必要
互換性修正 auditwheel Docker 後の追加処理が必須
CI/CD GitHub Actions ツール間の相互作用が不透明

「10% の複雑さ」に陥る罠

こうしたビルドツール(cibuildwheel など)は、通常ケースでは見事に機能します。しかし問題は、非標準的な要件(SIMD サポートの実行時検出など)に直面した途端、抽象化が破綻するということです。

開発者は結局のところ、ツール A にツール B を経由してツール C に説明させ、さらにツール D に指示を伝えるという、複数層の説明を重ねることになります。シンプルな要求が、複数の層を通ると「得体のしれない魔術的な呪文」へと変貌してしまうわけです。

こうした複雑な問題を解決しても、得られるのは一時的な安心感だけで、再利用可能な知見にはならない。

実際のところ、Windows ビルドの失敗原因を特定するのはほぼ不可能に近い状況でした。原因の候補は多数:

  • 自分のコード内のバグ
  • cibuildwheel 内のバグ
  • GitHub Actions 内のバグ
  • Visual Studio 内のバグ
  • これらツール間の相互作用による問題

どれが真犯人かを特定する手がかりはほとんどなく、結果は必ず「不本意な形での解決」に至るか、問題が棚上げされることになります。

自分の場合も、複数のビルドツールを運用している立場として、この種の問題は他人事ではありません。GCP 上でアプリケーションをデプロイするときも、ローカルの自作 PC で Stable Diffusion をビルドするときも、「ツール間の隠れた相互作用」が思わぬトラブルを引き起こすことがあります。単なるコードの問題ではなく、ビルドパイプライン全体の複雑性が積み重なった結果として表面化することが多いのです。

今後のビルドシステム改善の方向性

参考になる取り組みもあります。Qt 6 などの大規模プロジェクトでは、ビルドシステムにライブラリ側で提供される便利関数を組み込み、開発者が「設定の詳細を意識せずに」クロスプラットフォーム対応できるような仕組みが進み始めています。(出典)

const configureQtExeRootModule = @import("libqt6").configureQtExeRootModule;
try configureQtExeRootModule(b, exe, .{});

このようなアプローチは、複数層のツール間での「説明の負担」を軽減し、ユーザーが本来の開発に集中できる環境を作ります。