Go言語のDependency/Vendoringの問題と今後.gbあるいはGo1.5
Go言語のDependency/Vendoringは長く批判の的になってきた(cf. “0x74696d | go get considered harmful”, HN).Go1.5からは実験的にVendoringの機能が入り,サードパーティからはDave Chaney氏を中心としてgbというプロジェクベースのビルドツールが登場している.なぜこれらのリリースやツールが登場したのか?それらはどのように問題を解決しようとしているのか?をつらつらと書いてみる.
Dependencyの問題
最初にGo言語におけるDependecy(依存解決)の問題についてまとめる.Go言語のDependencyで問題なのはビルドの再現性が保証できないこと.この原因はimport
文にある.
Go言語で外部パッケージを利用したいときはimport
文を使ってソースコード内にそれを記述する.このimport
文は2通りの解釈のされ方をする.go get
はリモートレポジトリのfetch URLとして解釈し,コンパイラはローカルディスク上のソースのPathとして解釈する.例えばコマンドラインツールを作るときに外部パッケージとしてmitchellh/cliを使いたい場合は以下のように記述する.
import "github.com/mitchellh/cli"
これが書かれたコードをgo get
すると,ローカルディスクにmitchellh/cliがなければ$GOPATH/src
以下にそれがfetchされる.ビルド時はそのPathに存在するコードが利用される.
import
で問題になるのは,そこにバージョン(もしくはタグ,Revision)を指定できないこと.そのため独立した2つのgo get
が異なるコードをfetchしてしまう可能性がある.そのコードが互換をぶっ壊していたらビルドは失敗するかもしれない.つまり現状何もしないとビルドの再現性は保証できない.
では以下のようにタグやバージョンを書けるようにすれば?となる.が,これは言語の互換を壊すことになる.
import "github.com/pkg/term" "{hash,tag,version}"
以下のようにディレクトリ名にバージョン番号を埋め込むという方法もよく見る.が,これも結局異なるRevisionのコードをFetchしてまうことに変わりはなくビルドに再現性があるとは言えない.
import "github.com/project/v7/library"
Vendoring
再現性の問題を解決する方法として,依存するレポジトリを自分のレポジトリにそのまま含めてしまう(vendoringと呼ばれる)方法がある.こうしておくと依存レポジトリのupstreamの変更に影響を受けず,いつでもどのマシンでもビルドを再現できる.
しかし何もしないとコンパイラがそのレポジトリのPathを探せなくなりビルドができなくなる.ビルドするには以下のどちらかを行う必要がある.
$GOPATH
の書き換えimport
の書き換え
$GOPATH
の書き換え
まずは$GOPATH
を書き換える方法.この場合はそもそもコードをvendoringするときに$GOPATH/src/github.com...
と同じディレクトリ構成を作らなければならない.その上でそのディレクトリを$GOPATH
に追加してビルドを実行する.
例えば外部パッケージmitchellh/cliをレポジトリ内のext
ディレクトリにvendoringしたい場合は,まず以下のようなディレクトリ構成でそれをvendoringをする.
$ tree ext
ext
└── src
└── github.com
└── mitchellh
└── cli
そしてビルド時は以下のように$GOPATH
にext
ディレクトリを含めるようにする.
$ GOPATH=$(pwd)/ext:$GOPATH go build
このやり方が微妙なのは毎回自分で$GOPATH
の変更を意識しないといけないこと(Fork先でも意識してもらわないといけない).
importの書き換え
次にimport
を書き換える方法.レポジトリ内のvendoringしたディレクトリへと書き換えてしまう.例えばgithub.com/tcnksm/r
というレポジトリのext
ディレクトリに外部パッケージmitchellh/cliをvendoringしたとする.この場合は以下のようにimport
を書き換える.
import "github.com/mitchellh/cli" // Before
import "github.com/tcnksm/r/ext/cli" // After
これはあまり見ない.そもそもソースを書き換えるのが好まれないし,upstreamを見失うかもしれない.また多くの場合import
文が異常に長く複雑になる.
Godep
$GOPATH
の書き換えやそれに合ったディレクトリの作成,import
の書き換えを自分で管理するのは煩雑なのでこれらを簡単にするツールは多く登場している.その中で多く使われているのがgodepというツール.
godep
は使い始めるのも簡単で,例えば現在のレポジトリの依存をすべてvendoringするには以下を実行するだけで良い.
$ godep save
godep
はレポジトリ内にGodep/_workspace
を作成しその中に$GOPATH
の流儀に従い依存をvendoringする.そして同時にGodep.json
ファイルを作成し依存のバージョンも管理してくれる.
go
コマンドを実行する場合は,以下のようにそれをgodep
でラップすれば$GOAPTH
の書き換えもしてくれる.
$ godep go build
godep save -r
とするとimport
を書き換えになる.そうすると以後$GOPATH
の書き換えは不要になりgodep
コマンドは不要になる.
Godep?
Godep
は多く使われているが以下のような問題がある
Godep.json
は決定版の依存管理ファイルではない- そもそも依存管理ファイル(
Godep.json
)を持つのが鬱陶しい $GOAPATH
書き換え方式だとgo get
が使えない
import
の書き換え(godep save -r
)をするとgo get
は使えるが問題が起こる.例えばr
というレポジトリがあり,mainパッケージr/c
とmainパッケージではないr/p
があるとする.r/c
はr/p
をimportしており,r/p
は外部パッケージのd
に依存しているとする.このときd
をr/Godeps/_workspace
にvendoringしてimport
をd
からr/Godeps/_workspace/.../d
に書き換えるとする.
これだけなら問題ないが,別のパッケージu
が登場してr/p
とd
に依存していると問題が起こる.r/p
はもはやd
に依存しておらずr/Godeps/_workspace/.../d
をimportする.d
とr/Godeps/_workspace/.../d
は異なるパッケージなのでtype assertionなどで死ぬ
cannot use d (type "github.com/tcnksm/r/Godeps/_workspace/src/github.com/dep/d".D) as type "github.com/tcnksm/d".D in argument to ...
こういうわけでvendoringはまだ最高の解が存在するとは言えない.
gb
そんな中gb
というツールも登場している.gb
はプロジェクトベースのビルドツール.プロジェクトごとに必要なコード/その依存をすべてvendoringし同じビルドを再現する.なぜプロジェクトベースが良いのか.
- 自分で書いたコードとそれが依存する他人が書いたコードを明確に分けて(ディレクトリを分けて)管理することができる
- ビルドのたびに外部にfetchする必要がなくいつでもビルドができる.外的要因(e.g., GitHubがダウンしている)の影響を受けない
- 逆に依存はすべてプロジェクトに含まれているので依存ライブラリのアップデートはatomicになり,チームメンバーすべてに影響する(
go get -u
は不要になる)
そしてgb
には以下の特徴がある.
go
toolのWrapperではない(すべて書き直されている)$GOPATH
の書き換えをしないimport
の書き換えをしない- 依存管理ファイルが必要ない(
gb-vendor
プラグインを使うと必要)
使い心地は本人による記事を見るのが良い.プロジェクトとしてすべてのコードを管理することになるのでシンプルかつ明確になるなと思う.vendoringと自分のソースの境界も明確なので,vendoringしたパッケージのアップデートもそこまで苦ではない(gb-vendor
を使う,もしくはgit
やhg
を使う).
Godep
のようなハックではないのも良い.特に複数人のチームで大規模な開発をしていくときに便利なのではと思う(そういう場合がちゃんと考えられてる).「go
toolの書き直し」て!と思うかもしれないがDave Chaney氏なので信頼できる.
Go1.5
Go本家もちゃんとこの問題には取り組んでいて2015年8月にリリースされる予定になっているGo1.5にvendoringの機能が実験的に入りそう.まだちゃんとしたProposalではなさそうだが以下のようになりそう.
r
というレポジトリのパッケージr/p
がパッケージd
に依存しているとする- パッケージ
d
をr/vendor/d
にvendoringしr
内でビルドするとd
はr/vendor/d
と解釈される r/vendor/p
が存在しない(vendoringしていない)場合はp
と解釈する- 複数の解釈がありえる場合はもっとも長いspecificなものを選択する
例えばgithub.com/tcnksm/r
というレポジトリにmitchellh/cliをvendoringしたい場合は,vendor
ディレクトリに以下のようにvendoringする.
$ tree vendor
vendor
└── github.com
└── mitchellh
└── cli
この時vendoring機能を有効にしてビルドするとvendor
が存在するので
import "github.com/mitchellh/cli"
は,以下のように解釈される.
import "github.com/tcnksm/r/vendor/github.com/mitchellh/cli"
この機能はすでに試すことができる.Go1.5をビルドして以下の環境変数を有効にすればよい.
$ export GO15VENDOREXPERIMENT=1
これはimport
文のResolution(解像度)を考慮しないといけなくなるが,普通に良さそう.import
を書き換える必要もないしvendor
を使うか使わないかをプロジェクトごとにわけることができる.まだ確定ではないので静観するがgo
toolだけでなんとかできるならそれに越したことはない.
まとめ
とりあえず社内でチーム開発するもの,かつ大規模になりそう(外部の依存が多そう)なものにはgb
を考えてみても良いのではと思っている.個人的に開発してるものはそこまで外部パッケージに依存してないので困ってない.もし必要になればGo本家が確定するまではMakefile
とGodep
で頑張る.