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

そしてビルド時は以下のように$GOPATHextディレクトリを含めるようにする.

$ 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/cr/pをimportしており,r/pは外部パッケージのdに依存しているとする.このときdr/Godeps/_workspaceにvendoringしてimportdからr/Godeps/_workspace/.../dに書き換えるとする.

これだけなら問題ないが,別のパッケージuが登場してr/pdに依存していると問題が起こる.r/pはもはやdに依存しておらずr/Godeps/_workspace/.../dをimportする.dr/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を使う,もしくはgithgを使う).

Godepのようなハックではないのも良い.特に複数人のチームで大規模な開発をしていくときに便利なのではと思う(そういう場合がちゃんと考えられてる).「gotoolの書き直し」て!と思うかもしれないがDave Chaney氏なので信頼できる.

Go1.5

Go本家もちゃんとこの問題には取り組んでいて2015年8月にリリースされる予定になっているGo1.5にvendoringの機能が実験的に入りそう.まだちゃんとしたProposalではなさそうだが以下のようになりそう.

  • rというレポジトリのパッケージr/pがパッケージdに依存しているとする
  • パッケージdr/vendor/dにvendoringしr内でビルドするとdr/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本家が確定するまではMakefileGodepで頑張る.

参考