「ファイルの中身に依存しつつそのファイル自体を更新する」コマンドラインツールはビルドシステムと相性が悪い

関数型プログラミングのメリットについて語られることはあっても,「ファイルの中身に依存しつつそのファイル自体を更新する」コマンドラインツールがビルドシステムと相性が悪い,みたいな話をする人はあんまりいないようなので書いてみた.

Windows には signtool.exe というデジタル署名ツールがある.実行ファイル myapp.exeに署名したい場合は以下のようにする.

signtool sign /a myapp.exe

十分にシンプルなように見える.が,典型的なビルドシステムはこの処理がお気に召さない.試しに Chromium や node.js で使われているメタビルドシステムである Gyp で書いてみる.

{
  'targets': [
    {
      'target_name': 'myapp',
      'type': 'executable',
      'sources': [
        'main.cc',
      ],
    },
    {
      'target_name': 'sign',
      'type': 'none',
      'dependencies': [
        'myapp',
      ],
      'actions': [
        {
          'action_name': 'run_signtool',
          'inputs': [
            '<(PRODUCT_DIR)/myapp.exe',
          ],
          'outputs': [
            '<(PRODUCT_DIR)/myapp.exe',
          ],
          'action': ['signtool', 'sign', '/a', '<(PRODUCT_DIR)/myapp.exe'],
          'msvs_cygwin_shell': 0,
        },
      ],
    },
  ],
}

この Gyp ファイルから生成された Ninja 用ビルドファイルは,以下のようにビルド時にエラーとなる.

C:\work\dag>ninja -C out/Default sign
ninja: warning: multiple rules generate myapp.exe. builds involving this target will not be correct; continuing anyway
ninja: Entering directory `out/Default'
ninja: error: dependency cycle: myapp.exe -> myapp.exe

myapp.exe をビルドするのに myapp.exe が必要なのはけしからん,とのこと.
私の場合,ラッパースクリプトを書いて署名前後でファイルパスの一部が変わるようにして回避することが多い.
例1.

mysigntool --input=myapp.exe --output=myapp.signed.exe

例2.

mysigntool --input=unsigned/myapp.exe --output=signed/myapp.exe

デジタル署名に限らず,mt.exe を利用したマニフェストファイルの埋め込み,rebase.exe を利用した事前バインド,Profile Guided Optimization (PGO) などで,うっかり入力ファイルと出力ファイルを同一パスにすると同様の問題に直面する.

なんでこんな話を書く気になったかというと,暇つぶしに Gyp にパッチを書いていて突然気付いたからである.
https://codereview.chromium.org/82703007#msg3
Directed Acyclic Graph (DAG; 無閉路有向グラフ) を事前に構築できることを前提としているシステムというのは,ビルドシステムにしろ MapReduce にしろ,案外日常的にお世話になっている.そういうシステムで「ちょっとした処理」を行うとして,なんだかうまく書けないなぁという処理,実はそれ入力と出力に循環があったりしませんか,というまあただそれだけの話.
こうやって考えてみると,世間のビルドシステムというのはファイルシステムと割とべったりくっついているということに気付かされる.

まとめ

入力パス名と出力パス名が同じになる操作は,一般的にビルドシステムと相性が悪い.典型的なビルドシステムでは,ファイルの内容ではなくパス名だけを用いて依存関係を記述する必要があることに注意.