gomとgoのvendoring

先日,ちょっと勘違いをして,gomに変なぷるりを送ってしまったので,反省を込めて.

github.com

こういうissueが立っていてなんか変だなーと思ったんだけど,やっぱり俺のミスだった.

Goのvendroing

まずはじめに,goにはvendoring機能というのがある.

Go言語のDependency/Vendoringの問題と今後.gbあるいはGo1.5 | SOTA

相変わらず詳しく書いてくれているいい記事もあるので,そちらを参考にしてもらったほうがわかりやすいかもしれない.

依存パッケージを同じ所に入れてしまう問題

goで外部パッケージを利用する場合,import文を使うのが普通だと思う. このimport文,例えば自分でpackageを作って公開したような場合には,その利用者が go get した時に,依存として一緒にダウンロードしてくれる. そのため, go get するときには,importで突っ込んでいる外部パッケージは,特に気を配ることなくインストールされビルドできる.

では,自分の手元で開発したものをビルドする場合はどうなるだろうか?

この場合,import文にかかれている外部パッケージは,予め自分で go get しておくなりして,GOPATH配下に存在し,参照できる状態でなければならない. しかし,ここには開発者にとっては割と大きな問題が発生する.

例えば,hogeというパッケージを開発するために,aというようなパッケージを依存で入れるとする. go get github.com/h3poteto/a というようなコマンドにより,パッケージaをローカルにインストールする.

次に,fugaというパッケージを同じくローカル開発する際に,パッケージaへの依存が再び発生したとしよう. go get -u github.com/h3poteto/a というようなコマンドを打つ.このとき, -u はパッケージのアップデートまでやってくれる.

そうすると,実はhogeとfugaで要求しているaのバージョンが違うという場合が発生する.

これは,ローカルで複数のパッケージを開発していれば常に付きまとってくる問題だ.

なんかrubyのgemで, bundle install の際に --path を指定せずにインストールしたら,グローバルにgemがいっぱいインストールされてるみたいな.

一応,goのパッケージの思想としては,常に後方互換性を維持しながらアップデートするという意識はされているが,それは個々の開発者に依ってしまう.

vendoring

そこで,go1.6から,vendoringという機能が正式にサポートされた.

プロジェクトの直下に vendor というディレクトリがあった場合には,import文の依存解決順序が, vendor -> $GOROOT -> $GOPATHという順序でパッケージを探してくれる.

これにより,開発時の依存パッケージはプロジェクト直下のvendor に突っ込んでおけば,$GOPATH配下のパッケージ群を汚染すると事無く依存解決することができる.

ただし,このvendoring機能は$GOPATH配下でのみ機能する という重要な制約がある.

Gomが提供してくれる機能

話は変わってgomというパッケージがある.

github.com

gomは,importで入るパッケージのバージョン管理をしつつ,go1.5以前でもvendoring的な機能を提供していた. なお,今回はvendoring機能にのみ注目したいので,バージョン管理の部分はあまり触れない.

が,本来gomを使いたくなる欲求はこのバージョン管理がメインだと思うので,そのへんはREADMEを読んでいただけると良いかと.

Go1.5以前

Go1.5以前,gomは gom install すると _vendor というディレクトリに依存パッケージ群を入れてくれていた.

そして,gom コマンドを使うことで,_vendor 配下のパッケージも参照してくれるようになり,普通にimport文を使うだけで依存解決することができた.

なお,Go1.5やGo1.6では GO15VENDOREXPERIMENT=0 という環境変数をセットすることにより,goが提供するvendoring機能を無効化することができたため,特にvendor ディレクトリは作らず _vendorディレクトリだけで依存解決をしていた.

そのため,もちろんこの状況下で通常のgoコマンドは動かず,ラップされたgomコマンドを使わなければならなかった.

Go1.6以降

Go1.6以降ではvendoring機能が使える.

そのため,gomは vendor ディレクトリに依存パッケージをインストールし,goのvendoring機能により依存解決をする. そのため gom installvendor ディレクトへのインストールさえ完了してしまえば,あとは通常のgoコマンドでも依存解決することができるようになる.

大事なのはGOPATH

このvendoring機能,とても便利に見えるがgomを使っていて一つ罠にハマった.

Go1.5以前では,gomは _vendor にパッケージをインストールし,ラップされたgomコマンドを通して依存解決していた.

そのため,プロジェクト自体はどこのディレクトリに置いても問題なくgomが依存解決してくれていた.

しかし,go1.6以降,gomはパッケージのインストールのみを担当し,依存解決自体はgoのvendoring機能を使うようになった.

そのため,プロジェクトの配置は 必ず$GOPATH配下なければならない

俺はうっかり,Go1.5時代のままgoのバージョンをあげていたので,最近のgoになって突然gomだけでは依存解決できなくなっていた.

検証する

ちょっと検証してみよう.

まず,testというpackageを作ってみる. 中身は空っぽだけど,import文だけ書いて,こんなディレクトリ構成にしてみる.

test
.
├── Gomfile
└── main.go

ちなみにこのとき,まだvendorは作成していない.

たとえば$GOPATHを~/goにした上で,~/testにプロジェクトを置いた場合.

[akira]:~/test$ echo $GOPATH
/home/akira/go
[akira]:~/test$ gom build
main.go:4:2: cannot find package "github.com/spf13/pflag" in any of:
    /usr/local/go/src/github.com/spf13/pflag (from $GOROOT)
    /home/akira/test/vendor/src/github.com/spf13/pflag (from $GOPATH)
    /home/akira/test/src/github.com/spf13/pflag
    /home/akira/go/src/github.com/spf13/pflag
gom:  exit status 1

ここで~/testを$GOPATH/src/testに移動してみた場合.

[akira]:~/go/src/test$ gom build
main.go:4:2: cannot find package "github.com/spf13/pflag" in any of:
    /usr/local/go/src/github.com/spf13/pflag (from $GOROOT)
    /home/akira/go/src/test/vendor/src/github.com/spf13/pflag (from $GOPATH)
    /home/akira/go/src/test/src/github.com/spf13/pflag
    /home/akira/go/src/github.com/spf13/pflag
gom:  exit status 1

ここまではvendorディレクトリの参照は行われていない.

ここでvendorディレクトリを作ってみる.

[akira]:~/go/src/test$ mkdir vendor
[akira]:~/go/src/test$ gom build
main.go:4:2: cannot find package "github.com/spf13/pflag" in any of:
    /home/akira/go/src/test/vendor/github.com/spf13/pflag (vendor tree)
    /usr/local/go/src/github.com/spf13/pflag (from $GOROOT)
    /home/akira/go/src/test/vendor/src/github.com/spf13/pflag (from $GOPATH)
    /home/akira/go/src/test/src/github.com/spf13/pflag
    /home/akira/go/src/github.com/spf13/pflag
gom:  exit status 1

すると見事にvendor treeという文字が出てくる.

~/testの方にもvendorディレクトリを作ってみよう.

[akira]:~/test$ ls
Gomfile  main.go  vendor
[akira]:~/test$ gom build
main.go:4:2: cannot find package "github.com/spf13/pflag" in any of:
    /usr/local/go/src/github.com/spf13/pflag (from $GOROOT)
    /home/akira/test/vendor/src/github.com/spf13/pflag (from $GOPATH)
    /home/akira/test/src/github.com/spf13/pflag
    /home/akira/go/src/github.com/spf13/pflag
gom:  exit status 1

ここではvendorディレクトリを作ってもvendorの参照は行われない.

というような感じ.

$GOPATH配下で,かつvendorディレクトリが存在する場合のみ,vendor treeという参照が発生している. この状態でvendor以下に依存パッケージがあれば,解決してくれるというわけだ.

結論

goのソースファイルは全部GOPATH配下に置いて開発しよう

CircleCI(番外編)

circleciの自動ビルドを組んでいると,git clone等の処理はcircleciがやってくれる. そして,goの環境設定も自動でやってくれる. ただし,困ったことに,goだからといって,GOPATH配下にgit cloneはしてくれない.

そのため,vendoring機能をアテにして,Gomfileとかを書いて,gom install したとしても,circleciでは,GOPATHとは関係ないディレクトリに配置されてしまう.

これはなにもGomだけの問題ではなく,vendoring機能をcircleciにまで持ち込もうと思ったら,必ず発生してくる問題だ.

一応戦おうとしている人はいる.

https://robots.thoughtbot.com/configure-circleci-for-go

が,なかなかクリティカルな回答がなかったため,ここに載せておく.

rsyncを使うというアイディアは同じだ. これがsymlinkでいけるのかどうかまでは,検証していない.

もしかしたら行けるかもしれないけど,さすがにあんまりsymlinkの解決まではしてくれなそうな気がして…….

GOPATHの書き換え

まず,circleciのGOPATHは~/.go_workspaceあたりになっている.これは不便なので勝手に書き換えよう.

machine:
  environment:
    GOPATH: "$HOME/go"
    REPO: "$GOPATH/src/github.com/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME"
    PATH: "$PATH:/usr/local/go/bin:$GOPATH/bin"

このようにして,PATHを通しておくと, go getしたプロブラムも使える.

circleciが用意してくれている環境変数はこちらを参考にした.

circleci.com

ソースの移動とビルド

dependencies:
  cache_directories:
    - "vendor"  # このへんは好み
    - "~/go"
  pre:
    - mkdir -p "$REPO"
    - go get github.com/mattn/gom
  override:
    - gom install
    - rsync -azC --delete ./ "$REPO"
    - cd "$REPO" && gom build

gom install ではvendorディレクトリにパッケージを入れるだけなので,先にやっておく. パッケージが入ってからrsyncして,以降は,REPOディレクトリ配下(GOPATH以下に配置されているためvendoring機能が使える)でビルドを行う.

ビルドがvendoring機能に依存するため,以降の操作ではREPOディレクトリに移動することが多くなる.

テスト

テストもREPOディレクトリで行う.

test:
  override:
    - cd "$REPO" && go test...

いちいちcdするのがめんどくさいんだけど,コマンド実行ディレクトリを指定する方法がわからなかった. そのうえ,rsyncするまではホームディレクトリでの実行で問題ないわけで,途中からディレクトリを切り替えるなんて,結構むずいな……と思ったので諦めてcdした.

というわけでcircleciでのビルドもできたぞー.