安全なビルドタスクとクリーンタスクを実装するために気をつける点

ビルドシステムを使いこなすのは難しい。この場合「使いこなす」とは、Visual StudioでF5を押してビルドしたり、メニューから「クリーン」を選んで実行する、程度の内容ではなく、ビルドタスクを記述して他のユーザーに自分以外の開発環境でビルドを期待通りに動かす程度には複雑な操作を行うことである。

あるビルドが期待通りに動かない、という問題は、もう少し具体的に挙げるなら、たとえば次のような状態だ:

  1. ビルドが成功していると言われているのに、期待しているアウトプットが無い
  2. ビルドが途中でエラーを起こして終了する
  3. ビルドが完了しない
  4. ビルドが想定外のソースを元に行われて完了する
  5. ビルドが期待通りのアウトプットを生成しているにもかかわらず、失敗と表示される
  6. ビルドは一応期待通りに完了するが、時間がかかり過ぎる
  7. ビルドが期待通りにエラーを出すのだが、そのメッセージが間違っているか、意味が伝わらない

同様に、クリーンアップが期待通りに動かない問題にも、いくつかパターンがある:

  1. クリーンアップで、ビルド出力以外のファイルが削除される
  2. クリーンアップが期待通りにファイルを削除しない
  3. クリーンアップで、期待されている以上にファイルが削除され、次のビルドに時間がかかりすぎる

「ビルドが成功していると言われているのに、期待しているアウトプットが無い」典型例は、ビルド中のあるステップで失敗しているのに、それに気付かずにビルドが続行してしまう場合だ。子プロセスを実行しているなら、その結果を終了コードなどで確認する必要がある。あるいは、コマンドは実行継続中なのに、呼び出し元には処理が戻っていて、その後の処理に依存しているのに、それに気付かずに続行しているのかもしれない。

「ビルドが途中でエラーを起こして終了する」場合や「ビルドが完了しない」場合というのは、ユーザーのカスタムビルドタスクがおかしい場合もあれば、そもそもビルドエンジン(ビルドツール)がおかしい場合もある。ビルドツールもプログラムに過ぎないのだから、そのような問題は当然発生しうるわけで、これらは理解しやすい問題と言える(デバッグの難易度は別論として)。

「ビルドが想定外のソースを元に行われる」というのは、たとえば次のような場合だ:

  • 一旦ソースコードに何かしらの加工を加えた上でコンパイラに解析させるビルドの場合(AOPなどを想定すると良い)、きちんと最新のユーザーソースをもとにAOP処理が行わなれないと、古いソースがコンパイラに渡されることになりかねない、といった例がある。TypeScriptがJavaScriptに正常にコンパイルされていないのに、最後にコンパイルされた時に生成されたJavaScriptファイルをそれ以降のビルドに使って、実行時エラーを起こす、なんてこともあり得る。
  • あるいは、「クリーンアップが期待通りにファイルを削除しない」問題と組み合わさることで、不正に残されたビルドキャッシュから「クリーンなはずの」ソースをビルドしようとして、一貫性のないアウトプットが生成されることがあり得る。

「ビルドが期待通りのアウトプットを生成しているにもかかわらず、失敗と表示される」という状態は、一番分かりやすい例では子プロセスの終了コードのチェックが反転している(0をエラー扱いする)などの場合で、ビルド出力がある以上実質的な問題はないという場合もあるが、続く処理があってもそこで終了してしまうかもしれないという問題がある。

「ビルドは一応期待通りに完了するが、時間がかかり過ぎる」というのは、ビルドそのものが妨げられているとは言えないが、例えば次のような場合に問題になる:

  • ビルドキャッシュをうまく構築できていない。(キャッシュの生成に失敗している、認識できていない、普通にビルドするよりキャッシュの構築のほうが時間がかかる、など)
  • 一回構築するのに長大な時間を要するビルドタスクなのに、クリーンするたびに再実行しなければならない。100MBのダウンロードが、毎回クリーンのたびに実行されるのはまずい。

「ビルドが期待通りにエラーを出すのだが、そのメッセージが間違っているか、意味が伝わらない」というのも、ビルドそのものが妨げられているわけではないが、問題が発生した時に原因の追及が困難になる。

クリーンアップに話を切り替えよう。

そもそも、クリーンアップタスクが難しくなる要因のひとつは、「雑にビルド出力ファイルや中間出力ファイルを消せない」という点にある。ユーザーがデバッグに必要なファイルをとりあえずbinディレクトリにコピーして置いておくのだけど、安易に消してほしくない、ということがあるかもしれない。まともなMSBuildカスタムビルドタスクがbinディレクトリをまるっと削除することが無いのは、そういう理由による。ここをガン無視すれば「クリーンアップで、ビルド出力以外のファイルが削除される」という問題は生じるものの、想定されていない再ビルド結果には、ならなくなる。

「クリーンアップが期待通りにファイルを削除しない」というのは、ビルドタスクの作業を追加して 中間ファイルが新しく増えたことを忘れている、というのがよくある要因だろう。また、そもそもビルドタスクの中で外部ツールを呼んでファイルを生成していたりすると、呼び出すタスクを実装している側も、その外部ツールの実行結果なんて把握していられないこともあり、そうなると一貫しない結果が生じる余地が大きくなる。

「クリーンアップで、期待されている以上にファイルが削除され、次のビルドに時間がかかりすぎる」というのは、たとえばクリーンビルド時にMavenやNuGetの依存ライブラリを毎回ダウンロードしてローカルにキャッシュする作業が毎回発生してはならない、ということである。完全に「クリーン」なビルドがきちんと実現できるのは一番安全ではあるが、毎回ダウンロードまで完了しなければクリーンビルドが出来ないというのは、やりすぎである。

この問題の亜種で、「クリーンアップで消してはならないファイルを消してしまって、それなのに間違ったビルド初期化キャッシュ情報が残っている結果、初回ビルドの時に行ったダウンロードと初期化の処理は行われず、結果的に以降のビルドが失敗する」という例もあると思われる。

…とまあ、いろいろな事情が考えられる。いずれにしろ、ビルドタスクが想定通りに動かない問題に遭遇したら、こんな感じで列挙した問題のいずれかにひっかかっていることもあると思うので、参考になることがあれば参考にされたい。