先日,ちょっと勘違いをして,gomに変なぷるりを送ってしまったので,反省を込めて.
こういう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というパッケージがある.
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 install
で vendor
ディレクトへのインストールさえ完了してしまえば,あとは通常の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が用意してくれている環境変数はこちらを参考にした.
ソースの移動とビルド
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でのビルドもできたぞー.