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では、
- 起動対象toolのbasenameを見る
compileのときだけGo source引数を処理する- その他は原則そのままpass-throughする
という実装になることが多い。
この性質により、GoのOpenTelemetryコンパイル時計装は何をしているのか のようなinstrumentationだけでなく、tool profiler、compiler差し替え、coverage補助などにも使える。
source rewritingはどう成立するか
Go compilerはbuild対象の .go file pathをcommand line argumentとして受け取る。
したがってwrapperはcompile invocationを受けた時点で、
- 元sourceを読む
- AST等で変換する
- temporary fileへ書く
- compilerの引数に含まれる元file pathをtemporary fileへ差し替える
- 本物の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を考える が重要になる。
関連
- GoのOpenTelemetryコンパイル時計装は何をしているのか
- Goの-toolexecを実際のビルドへ差し込む方法
- -toolexecでソースを書き換えるならGoのbuild cache identityを考える