"go build"した時に何が起きているのか？

この記事は [How “go build” Works](https://maori.geek.nz/how-go-build-works-750bb2ba6d8e) の翻訳記事です。

`go build`はどのようにして最も単純なGolangプログラムをコンパイルしているのでしょうか？

この記事ではその疑問を解消することを目標にしています。

最も単純な以下のプログラムについて考えてみましょう。

```go
// main.go
package main

func main() {}
```

`go build main.go`を実行すると、1.1Mbの実行可能ファイル`main`が出力され、何も実行されません。 この何もしないバイナリを作成するために、`go build`は何をしたのでしょうか？

`go build`コマンドはいくつかの便利なオプションを提供しています。

1. `-work`: `go build`は、作業ファイル用の一時フォルダーを作成します。 この引数は、そのフォルダーの場所を出力し、ビルド後に削除しません
2. `-a`: Golangは、以前にビルドされたパッケージをキャッシュします。 `-a`は`go build`にキャッシュを無視させるので、ビルドはすべてのステップを出力します
3. `-p 1`: これにより、処理が単一スレッドで行われるように設定され、出力が線形にログに記録されます
4. `-x`: `go build`は、`compile`などの他のGolangツールのラッパーです。 `-x`は、これらのツールに送信されるコマンドと引数を出力します

`go build -work -a -p 1 -x main.go`を実行すると`main`だけでなく大量のログが出て、`build`で`main`を作成する際に行われていることを我々に教えてくれます。

ログはまず以下の内容を出力します。

```
WORK=/var/folders/rw/gtb29xf92fv23f0zqsg42s840000gn/T/go-build940616988
```

これは、構造が次のような作業ディレクトリです。

```
├── b001
│   ├── _pkg_.a
│   ├── exe
│   ├── importcfg
│   └── importcfg.link
├── b002
│   └── ...
├── b003
│   └── ...
├── b004
│   └── ...
├── b006
│   └── ...
├── b007
│   └── ...
└── b008
    └── ...
```

`go build`は、完了する必要のあるタスクのアクショングラフを定義します。 

このグラフの各アクションは、独自のサブディレクトリ（`NewObjdir`で定義）を取得します。 

グラフの最初のノード`b001`は、メインバイナリをコンパイルするためのルートタスクです。 

依存する各アクションの数は大きく、最後は`b008`です。 （`b005`がどこに行ったのかわかりませんが問題ないと思われるので割愛します）

## b008

実行される最初のアクションは、グラフの末端の`b008`です。

```sh
mkdir -p $WORK/b008/

cat >$WORK/b008/importcfg << 'EOF'
# import config
EOF

cd /<..>/src/runtime/internal/sys

/<..>/compile 
  -o $WORK/b008/_pkg_.a 
  -trimpath "$WORK/b008=>" 
  -p runtime/internal/sys 
  -std 
  -+ 
  -complete 
  -buildid gEtYPexVP43wWYWCxFKi/gEtYPexVP43wWYWCxFKi 
  -goversion go1.14.7 
  -D "" 
  -importcfg $WORK/b008/importcfg 
  -pack 
  -c=16 
  ./arch.go ./arch_amd64.go ./intrinsics.go ./intrinsics_common.go ./stubs.go ./sys.go ./zgoarch_amd64.go ./zgoos_darwin.go ./zversion.go

/<..>/buildid -w $WORK/b008/_pkg_.a

cp $WORK/b008/_pkg_.a /<..>/Caches/go-build/01/01b...60a-d
```

`b008`では

1. アクションディレクトリを作成します（すべてのアクションがこれを行うため、以降ではこの記述を省略します）
2. ツール`compile`で使用する`importcfg`ファイルを作成します（空です）
3. ディレクトリを`runtime/internal/sys`パッケージのソースフォルダに変更します。 このパッケージには、ランタイムで使用される定数が含まれています
4. パッケージをコンパイルします
5. `buildid`を使用してメタデータをパッケージに書き込み（`-w`）、パッケージを`go-build`キャッシュにコピーします（すべてのパッケージがキャッシュされるため、以降ではこの記述を省略します）

これをツール`compile`に送信された引数に分解してみましょう（`go tool compile --help`でも説明されています）。

1. `-o` 出力先のファイル
2. `-trimpath` ソースファイルのパスからprefix`$WORK/b008=>`を取り除く
3. `-p` `import`で使われるパッケージ名をセット
4. `-std` `compiling standard library`(現時点ではよくわかりませんでした)
5. `-+` `compiling runtime`(これもわかりませんでした)
6. `-complete` コンパイラはCまたはアセンブリではなく完全なパッケージを出力
7. `-buildid` メタデータにbuild idを付与する
8. `-goversion` コンパイルされたパッケージに必要なバージョン
9. `-D` ローカルなインポートで使う相対パスは""
10. `-importcfg` インポート設定ファイルは他のパッケージを参照
11. `-pack` パッケージをオブジェクトファイル`.o`ではなくアーカイブ`.a`として作る
12. `-c` ビルド時にどれだけ並列で処理するか
13. パッケージ内のファイルのリスト

これらの引数のほとんどはすべての`compile`コマンドで同じなので、以降ではこの記述を省略します。

`b008`の出力は`runtime/internal/sys`に対応している`$WORK/b008/_pkg_.a`というアーカイブファイルです。

### buildid

`buildid`とは何かを説明しましょう。

`buildid`の形式は`<actionid>/<contentid>`です。 

これは、パッケージをキャッシュして `go build`のパフォーマンスを向上させるためのインデックスとして使用されます。 

`<actionid>`は、アクション（すべての呼び出し、引数、および入力ファイル）のハッシュです。 `<contentid>`は、出力 `.a`ファイルのハッシュです。 

`go build`アクションごとに、同じ`<actionid>`を持つ別のアクションによって作成されたコンテンツをキャッシュで検索できます。 

これは `buildid.go`に実装されています。

`buildid`はメタデータとしてファイルに保存されるため、`<contentid>`を取得するために毎回ハッシュする必要はありません。 このIDは、 `go tool buildid <file>`で確認できます（バイナリでも機能します）。

上記の `b008`のログでは、`buildID`は `compile`ツールによって`gEtYPexVP43wWYWCxFKi/gEtYPexVP43wWYWCxFKi`として設定されています。 

これは単なるプレースホルダーであり、後でキャッシュされる前に、 `go tool buildid -w`で正しい`gEtYPexVP43wWYWCxFKi/b-rPboOuD0POrlJWPTEi`に上書きされます。

## b007

次は`b007`です

```sh
cat >$WORK/b007/importcfg << 'EOF'
# import config
packagefile runtime/internal/sys=$WORK/b008/_pkg_.a
EOF

cd /<..>/src/runtime/internal/math

/<..>/compile 
  -o $WORK/b007/_pkg_.a 
  -p runtime/internal/math 
  -importcfg $WORK/b007/importcfg 
  ...
  ./math.go
```

1. `packagefile runtime/internal/sys=$WORK/b008/_pkg_.a`と書かれた`importcfg`を作成しています これは`b007`が`b008`に依存していることを示します
2. `runtime/internal/math`をコンパイル `math.go`の中身を覗いてみると確かに`b008`で作られた`runtime/internal/sys`をインポートしています

`b007`の出力は`runtime/internal/math`に対応している`$WORK/b007/_pkg_.a`というアーカイブファイルです。

## b006

```sh
cat >$WORK/b006/go_asm.h << 'EOF'
EOF

cd /<..>/src/runtime/internal/atomic

/<..>/asm 
  -I $WORK/b006/ 
  -I /<..>/go/1.14.7/libexec/pkg/include 
  -D GOOS_darwin 
  -D GOARCH_amd64 
  -gensymabis 
  -o $WORK/b006/symabis 
  ./asm_amd64.s

/<..>/asm 
  -I $WORK/b006/ 
  -I /<..>/go/1.14.7/libexec/pkg/include 
  -D GOOS_darwin 
  -D GOARCH_amd64 
  -o $WORK/b006/asm_amd64.o 
  ./asm_amd64.s

cat >$WORK/b006/importcfg << 'EOF'
# import config
EOF

/<..>/compile 
  -o $WORK/b006/_pkg_.a 
  -p runtime/internal/atomic 
  -symabis $WORK/b006/symabis 
  -asmhdr $WORK/b006/go_asm.h 
  -importcfg $WORK/b006/importcfg
  ...
  ./atomic_amd64.go ./stubs.go

/<..>/pack r $WORK/b006/_pkg_.a $WORK/b006/asm_amd64.o
```

ここで、通常の`.go`ファイルから抜け出し、低レベルのGoアセンブリ`.s`ファイルの処理を開始します。

1. ヘッダファイル`go_asm.h`を作成
2. 低レベルな関数が集まった`runtime/internal/atomic`パッケージに移動
3. ツール`go tool asm`（`go tool asm --help`で説明）を実行して、`symabis` "Symbol Application Binary Interfaces（ABI）ファイル" を作成し、次にオブジェクトファイル`asm_amd64.o`を作成
4. `compile`を使用して、`symabis`ファイルと `-asmhdr`を含むヘッダーを含む`_pkg_.a`ファイルを作成
5. `pack`コマンドで`asm_amd64.o`を`_pkg_.a`の中に加える

`asm`ツールはここでは以下の引数を伴って呼び出されています。

1. `-I`: アクション `b007`および`libexec/pkg/includes`フォルダーを含めます。 `includes`には3つのファイル`asm_ppc64x.h`、`funcdata.h`と`textflag.h`があり、すべて低レベルの関数定義があります。 例えば、FIXED_FRAMEは、スタックフレームの固定部分のサイズを定義します
2. `-D`: 事前定義されたシンボルを含める
3. `-gensymabis`: `symabis`ファイルを作成する
4. `-o`: 出力先のファイル

`b006`の出力は`runtime/internal/atomic`に対応している`$WORK/b006/_pkg_.a`というアーカイブファイルです。

## b004

```sh
cd /<..>/src/internal/cpu

/<..>/asm ... -o $WORK/b004/symabis ./cpu_x86.s
/<..>/asm ... -o $WORK/b004/cpu_x86.o ./cpu_x86.s

/<..>/compile ... -o $WORK/b004/_pkg_.a ./cpu.go ./cpu_amd64.go ./cpu_x86.go

/<..>/pack r $WORK/b004/_pkg_.a $WORK/b004/cpu_x86.o
```

`b004`は対象が`internal/cpu`に変わった以外は`b006`と同様です。

最初に`symabis`とオブジェクトファイルを`cpu_x86.s`をアセンブルして作成して、goファイルをコンパイルした後、それらを合わせて、アーカイブ`_pkg_.a`を作成します。

`b004`の出力は`internal/cpu`に対応している`$WORK/b004/_pkg_.a`というアーカイブファイルです。

## b003

```sh
cat >$WORK/b003/go_asm.h << 'EOF'
EOF

cd /<..>/src/internal/bytealg

/<..>/asm ... -o $WORK/b003/symabis ./compare_amd64.s ./count_amd64.s ./equal_amd64.s ./index_amd64.s ./indexbyte_amd64.s

cat >$WORK/b003/importcfg << 'EOF'
# import config
packagefile internal/cpu=$WORK/b004/_pkg_.a
EOF

/<..>/compile ... -o $WORK/b003/_pkg_.a -p internal/bytealg ./bytealg.go ./compare_native.go ./count_native.go ./equal_generic.go ./equal_native.go ./index_amd64.go ./index_native.go ./indexbyte_native.go

/<..>/asm ... -o $WORK/b003/compare_amd64.o ./compare_amd64.s
/<..>/asm ... -o $WORK/b003/count_amd64.o ./count_amd64.s
/<..>/asm ... -o $WORK/b003/equal_amd64.o ./equal_amd64.s
/<..>/asm ... -o $WORK/b003/index_amd64.o ./index_amd64.s
/<..>/asm ... -o $WORK/b003/indexbyte_amd64.o ./indexbyte_amd64.s

/<..>/pack r $WORK/b003/_pkg_.a $WORK/b003/compare_amd64.o $WORK/b003/count_amd64.o $WORK/b003/equal_amd64.o $WORK/b003/index_amd64.o $WORK/b003/indexbyte_amd64.o
```

`b003`もやることは`b004`や`b006`と同じです。

このパッケージの主な問題は、多くのオブジェクトファイル`.o`を作成するために複数の`.s`ファイルがあり、それぞれを`_pkg_.a`ファイルに追加する必要があることです。

`b003`の出力は`internal/bytealg`に対応している`$WORK/b003/_pkg_.a`というアーカイブファイルです。

## b002

```sh
cat >$WORK/b002/go_asm.h << 'EOF'
EOF

cd /<..>/src/runtime

/<..>/asm 
  ... 
  -o $WORK/b002/symabis 
  ./asm.s ./asm_amd64.s ./duff_amd64.s ./memclr_amd64.s ./memmove_amd64.s ./preempt_amd64.s ./rt0_darwin_amd64.s ./sys_darwin_amd64.s
  
cat >$WORK/b002/importcfg << 'EOF'
# import config
packagefile internal/bytealg=$WORK/b003/_pkg_.a
packagefile internal/cpu=$WORK/b004/_pkg_.a
packagefile runtime/internal/atomic=$WORK/b006/_pkg_.a
packagefile runtime/internal/math=$WORK/b007/_pkg_.a
packagefile runtime/internal/sys=$WORK/b008/_pkg_.a
EOF

/<..>/compile 
  -o $WORK/b002/_pkg_.a 
  ...
  -p runtime 
  ./alg.go ./atomic_pointer.go ./cgo.go ./cgocall.go ./cgocallback.go ./cgocheck.go ./chan.go ./checkptr.go ./compiler.go ./complex.go ./cpuflags.go ./cpuflags_amd64.go ./cpuprof.go ./cputicks.go ./debug.go ./debugcall.go ./debuglog.go ./debuglog_off.go ./defs_darwin_amd64.go ./env_posix.go ./error.go ./extern.go ./fastlog2.go ./fastlog2table.go ./float.go ./hash64.go ./heapdump.go ./iface.go ./lfstack.go ./lfstack_64bit.go ./lock_sema.go ./malloc.go ./map.go ./map_fast32.go ./map_fast64.go ./map_faststr.go ./mbarrier.go ./mbitmap.go ./mcache.go ./mcentral.go ./mem_darwin.go ./mfinal.go ./mfixalloc.go ./mgc.go ./mgcmark.go ./mgcscavenge.go ./mgcstack.go ./mgcsweep.go ./mgcsweepbuf.go ./mgcwork.go ./mheap.go ./mpagealloc.go ./mpagealloc_64bit.go ./mpagecache.go ./mpallocbits.go ./mprof.go ./mranges.go ./msan0.go ./msize.go ./mstats.go ./mwbbuf.go ./nbpipe_pipe.go ./netpoll.go ./netpoll_kqueue.go ./os_darwin.go ./os_nonopenbsd.go ./panic.go ./plugin.go ./preempt.go ./preempt_nonwindows.go ./print.go ./proc.go ./profbuf.go ./proflabel.go ./race0.go ./rdebug.go ./relax_stub.go ./runtime.go ./runtime1.go ./runtime2.go ./rwmutex.go ./select.go ./sema.go ./signal_amd64.go ./signal_darwin.go ./signal_darwin_amd64.go ./signal_unix.go ./sigqueue.go ./sizeclasses.go ./slice.go ./softfloat64.go ./stack.go ./string.go ./stubs.go ./stubs_amd64.go ./stubs_nonlinux.go ./symtab.go ./sys_darwin.go ./sys_darwin_64.go ./sys_nonppc64x.go ./sys_x86.go ./time.go ./time_nofake.go ./timestub.go ./trace.go ./traceback.go ./type.go ./typekind.go ./utf8.go ./vdso_in_none.go ./write_err.go
  
/<..>/asm ... -o $WORK/b002/asm.o ./asm.s
/<..>/asm ... -o $WORK/b002/asm_amd64.o ./asm_amd64.s
/<..>/asm ... -o $WORK/b002/duff_amd64.o ./duff_amd64.s
/<..>/asm ... -o $WORK/b002/memclr_amd64.o ./memclr_amd64.s
/<..>/asm ... -o $WORK/b002/memmove_amd64.o ./memmove_amd64.s
/<..>/asm ... -o $WORK/b002/preempt_amd64.o ./preempt_amd64.s
/<..>/asm ... -o $WORK/b002/rt0_darwin_amd64.o ./rt0_darwin_amd64.s
/<..>/asm ... -o $WORK/b002/sys_darwin_amd64.o ./sys_darwin_amd64.s
  
/<..>/pack r $WORK/b002/_pkg_.a $WORK/b002/asm.o $WORK/b002/asm_amd64.o $WORK/b002/duff_amd64.o $WORK/b002/memclr_amd64.o $WORK/b002/memmove_amd64.o $WORK/b002/preempt_amd64.o $WORK/b002/rt0_darwin_amd64.o $WORK/b002/sys_darwin_amd64.o
```

`b002`を見ればこれまでのアクションがなぜ必要だったのかわかります。

`b002`はGoのバイナリの実行に必要な`runtime`パッケージ全てを含んでいます。例えば、`b002`には `mgc.go`というGoのGCの実装も含まれています。これは`b004`(`internal/cpu`)と`b006`(`runtime/internal/atomic`)をインポートしています。

`b002`はコアライブラリの中では最も複雑なパッケージかもしれませんが、ビルド自体は今までと同じ工程で行われます。つまり`asm`、`compile`して出力されたファイルを`pack`して`_pkg_.a`にしています。

`b002`の出力は`runtime`に対応している`$WORK/b002/_pkg_.a`というアーカイブファイルです。

## b001

```sh
cat >$WORK/b001/importcfg << 'EOF'
# import config
packagefile runtime=$WORK/b002/_pkg_.a
EOF

cd /<..>/main

/<..>/compile ... -o $WORK/b001/_pkg_.a -p main ./main.go

cat >$WORK/b001/importcfg.link << 'EOF'
packagefile command-line-arguments=$WORK/b001/_pkg_.a
packagefile runtime=$WORK/b002/_pkg_.a
packagefile internal/bytealg=$WORK/b003/_pkg_.a
packagefile internal/cpu=$WORK/b004/_pkg_.a
packagefile runtime/internal/atomic=$WORK/b006/_pkg_.a
packagefile runtime/internal/math=$WORK/b007/_pkg_.a
packagefile runtime/internal/sys=$WORK/b008/_pkg_.a
EOF

/<..>/link 
  -o $WORK/b001/exe/a.out 
  -importcfg $WORK/b001/importcfg.link 
  -buildmode=exe 
  -buildid=yC-qrh2sY_qI0zh2-NE7/owNzOBTqPO00FkqK0_lF/HPXqvMz_4PvKsQzqGWgD/yC-qrh2sY_qI0zh2-NE7 
  -extld=clang 
  $WORK/b001/_pkg_.a

mv $WORK/b001/exe/a.out main
```

First it builds an importcfg that includes runtime built in b002 to then compile main.go to _pkg_.a

1. 最初に、`b002`の`runtime`を含むようにして`importcfg`を作成した後、`main.go`をコンパイルして`_pkg_.a`を作成します。
2. 以前に登場した全パッケージに加えて`command-line-arguments=$WORK/b001/_pkg_.a`を含む`importcfg.link`を作成したら、`link`コマンドでそれらをリンクして実行ファイルを作成します。
3. 最後に`main`にリネームして出力先に移動します。

`link`の引数の補足をしておきましょう。

1. `-buildmode`: 実行ファイルをビルドする
2. `-extld`: 外部のリンカを参照する

ようやくお目当てのものが手に入りました。

`b001`から生まれるのが`main`バイナリです。

## Bazelとの類似点

効率的なキャッシュを実現するためのアクショングラフの作成は、Bazelが高速ビルドに使用するビルドツールと同じアイデアです。

Golangの `actionid`と`contentid`は、Bazelがキャッシュで使用する`actioncache`と `content-addressable store（CAS)`に対応しています。

BazelはGoogleの製品であり、Golangも同様です。 彼らがソフトウェアを迅速そして正確にビルドする方法について同様の哲学をもったことは非常に合理的と言えるでしょう。

Bazelの `rules_go`パッケージでは、`builder`コードで `go build`を再実装する方法を確認できます。 

アクショングラフ、フォルダ管理、およびキャッシュはBazelによって外部で処理されるため、これは非常にクリーンな実装です。

## 次のステップへ

`go build`は、今回のような何もしないプログラムでも、それをコンパイルするために多くのことを行っていました。 

ツール（ `compile` `asm`）やその入力ファイルと出力ファイル（`.a` `.o` `.s`）については今回はあまり詳しく説明しませんでした。 

また、今回は最も基本的なプログラムをコンパイルしているだけです。 

次のようにしてコンパイルをもっと複雑なものにできます。

1. 他のパッケージをインポートする 例えば`Hello world`を出力するため`fmt`をインポートするとさらに23個のアクションがアクショングラフに追加される
2. 外部パッケージを参照するために`go.mod`を使用する
3. `GOOS`と`GOARCH`の値を変えて他のアーキテクチャ向けにビルドする 例えば、`wasm`向けのコンパイルではアクションや引数の内容が全く異なってくる

`go build`を実行してログを検査することは、Goコンパイラがどのように機能するかを学ぶためのアプローチとしてはトップダウンなアプローチです。 基礎から学びたい場合は次のようなリソースに飛び込むのに最適な出発点です。

1. [Introduction to the Go compiler](https://github.com/golang/go/blob/release-branch.go1.13/src/cmd/compile/README.md)
2. [Go: Overview of the Compiler](https://medium.com/a-journey-with-go/go-overview-of-the-compiler-4e5a153ca889)
3. [Go at Google: Language Design in the Service of Software Engineering](https://talks.golang.org/2012/splash.article)
4. [build.go](https://github.com/golang/go/blob/master/src/cmd/go/internal/work/build.go)
5. [compile/main.go](https://github.com/golang/go/blob/master/src/cmd/compile/main.go)

## References

- [How “go build” Works](https://maori.geek.nz/how-go-build-works-750bb2ba6d8e)