タラバガニー設計局stalins.clubNOTE/notes/go-toolexec-build-hook

Goの-toolexecはビルドツール呼び出しを横取りする

go build -toolexec='cmd args' を指定すると、Go commandがcompilerやassemblerなどのtoolchain programを直接起動せず、指定したwrapperを経由して起動する。

Goのhelpに書かれているモデルは単純で、通常なら

/path/to/compile <arguments...>

と起動されるところが、

cmd args /path/to/compile <arguments...>

になる。

wrapperは最初の引数で「本来起動されるtool」を受け取り、残りの引数を調べたり書き換えたりした後で、本物のtoolをexecできる。

wrapperから見えるもの

Goはwrapperに TOOLEXEC_IMPORTPATH を渡す。これは現在buildしているpackageのimport pathで、Go 1.16から正式に設定されるようになった。

最小のwrapperは概ね次のような動作になる。

tool := os.Args[1]
args := os.Args[2:]

fmt.Println(filepath.Base(tool), os.Getenv("TOOLEXEC_IMPORTPATH"))

cmd := exec.Command(tool, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Run()

実際にGo自身のtestdataにもほぼ同じwrapperが含まれている。

compileだけを横取りするわけではない

-toolexec は名前からcompiler hookのように見えるが、実際にはcompile、asm、link、vetなど複数のtool invocationに介在する。

そのためsource rewritingをしたいwrapperでは、

  1. 起動対象toolのbasenameを見る
  2. compile のときだけGo source引数を処理する
  3. その他は原則そのままpass-throughする

という実装になることが多い。

この性質により、GoのOpenTelemetryコンパイル時計装は何をしているのか のようなinstrumentationだけでなく、tool profiler、compiler差し替え、coverage補助などにも使える。

source rewritingはどう成立するか

Go compilerはbuild対象の .go file pathをcommand line argumentとして受け取る。

したがってwrapperはcompile invocationを受けた時点で、

  1. 元sourceを読む
  2. AST等で変換する
  3. temporary fileへ書く
  4. compilerの引数に含まれる元file pathをtemporary fileへ差し替える
  5. 本物のcompilerを起動する

という処理ができる。

重要なのは、repository上のsourceを直接書き換える必要がない点である。これがcompile-time instrumentationを「既存コードへの変更なし」で成立させる基礎になっている。

-toolexec はGo commandより一段下にある

-toolexec が介入するのは、module resolutionやpackage graphの構築より後、Go commandが各toolchain programを実行する段階である。

そのため、

  • go list
  • module selection
  • build tagの大枠
  • dependency graph生成

そのものを完全に置き換える仕組みではない。

一方、各packageをcompileする直前の入力には触れられるので、依存packageを含めたsource-level transformationには非常に強い。

ビルドシステム上の意味

このflagの面白いところは、compiler plugin APIを増やすことなく、単なるprocess wrapperとしてtoolchainへ介入できることにある。

反面、wrapperはGoのinternal compiler invocationに近い場所を見るため、toolchain version差分やbuild cacheとの整合性を考えなければならない。特にcache identityについては -toolexecでソースを書き換えるならGoのbuild cache identityを考える が重要になる。

関連

出典

▸ ノート一覧に戻る