ベンダリングのベストプラクティス


Goは大変便利なパッケージ参照やビルドツールチェインを持ってますが・・・。 プライベートリポジトリからのパッケージ参照やリビジョンロックができない。 そこでオレオレですが、ベンダリングテクニックを紹介。

パッケージ管理

Goは公開リポジトリからその時点での最新の版のパッケージを 簡単に自分のGo開発環境に取り込むことができます。

ですが、バイナリリリース型の開発をしていると 以下の4つの機能不足を感じます。

  • オリジナルリポジトリの消滅やエンバグの影響を受けたくない
  • プライベートリポジトリからのパッケージ参照
  • リビジョンの固定
  • 依存パッケージの追加や削除を簡単に行いたい

(消失対策は何年も維持メンテする上での保険のようなものです)

標準機能としてベンダリング機能が追加

Go1.5で試験導入されたベンダリング機能は 1.6以降では標準機能として使えるようになりました。

GOPATH配下の「vendor」フォルダを特別に扱って このフォルダ配下に置かれたパッケージが importの検索対象に含められるようになりました。

以下のようなフォルダ構成にて

  • $GOPATH/vendor/golang.org/x/text/
  • $GOPATH/app/main.go

main.goからtextパッケージを「import “golang.org/x/text”」で 参照できます。

この機能を使ってプロジェクト毎に別バージョンの パッケージを利用することが容易になりました。

vendor配下のコード管理方法

  1. vendor配下をプロジェクトリポジトリに丸ごと追加する
  2. git subtree
  3. git submodule
  4. 依存解決用のツールを使う
  • 1.は他人のコードを取り込むことでリビジョンロックが実現でき、オリジナルの消失にも影響を受けません。ただオリジナルの更新に追従するのは大変です。
  • 2.は1.に加えてその後のアップデートを取り込み可能とします。
  • 1.と2.はリポジトリが重たくなる傾向があります。
  • 3.はその欠点を回避しますが、追加削除が面倒でオリジナルの消失には対策が必要です。
  • 4.は3.の利点をもち、追加削除を簡単にし、git依存をなくした方法です。

4.で唯一残った課題は「オリジナルリポジトリの消滅やエンバグの影響を受けたくない」で、解決方法のひとつはフォークしたリポジトリを参照する方法です。

でも、リポジトリURLの違うものをimport可能なようにGOPATH配下に配置するのはほとんどのツールが対応していません。

4.ではビルドに必要な依存ツールがまたひとつ増えてしまいます。

他の解決方法の模索

gitに限定さえすれば、

  • git clone+checkoutでリビジョンロックが実現可能
  • リポジトリURLにSSH経由URLを指定できればプライベートリポジトリも利用可能

なので、

  • 依存するリポジトリをフォークしておいて
  • 元々想定されるimportパスで参照可能なように
  • vendorフォルダ以下にgit cloneして
  • 固定したリビジョンをcheckoutする

以上のようなことがビルド直前に実行できれば、 プロジェクトリポジトリを重くせずに 安心してバイナリリリース型アプリケーションの開発ができそうです。

フォルダ構成

プロジェクトルートをGOPATHの先頭に入れる想定です。

  • ROOT/
    • src/
      • vendor/
        • golang.org/
          • x/
            • text/
      • app/
        • main.go
    • .gitignore
    • Makefile

以上のフォルダ構成と以下の2つの定番ツールで いつも同一のコードを参照したバイナリビルドを行えるようにしてみました。

  • make
  • git

.gitignore

bin/
pkg/
src/vendor/

Makefile例

export GO15VENDOREXPERIMENT=1
export GOPATH:=$(PWD)
GO:=$(shell which go)

define co
	if [ ! -e $2 ]; \
	then \
		git clone $1 $2; \
	else \
		cd $2 && git fetch origin; \
	fi
	cd $2 && git stash -u && git stash drop || true
	cd $2 && git checkout -q $3
	cd $2 && git reset --hard
endef

.PHONY: all depends test build

all: depends test build

depends:
	@$(call co, https://github.com/golang/text.git, src/vendor/golang.org/x/text, origin/master)

test:
	$(GO) test $$($(GO) list ./... | grep -v "^vendor/")

build:
	$(GO) build -o App app

dependsのルール定義で行を追加したり削除したりすれば依存の追加や削除が容易?です。 (「go get -d -x -u パッケージ」とかすれば依存パッケージ名の一覧が見れるので参考に)

ここでの依存記述は以下の順に指定します。

$(call co, リポジトリURL, チェックアウトフォルダ, リビジョン)
  • リポジトリURLにはプライベートなSSH用のURLも指定できます。
  • チェックアウトフォルダには「src/vendor/インポートパス」を指定します。
  • リビジョンにはハッシュ、フルブランチ名、タグ名が使えます。

以上の記述で

make depends
make build

にてビルドすればいつも同じコードを参照したビルドが実現できます。

おまけ

docker動作環境にて以下のようにすれば任意のgoバージョンでビルドできます。 (dockerに詳しい人向け)

make GO="docker run -it -v $PWD:$PWD -w $PWD -e GOPATH=$PWD golang:1.7 go"

まとめ

  • ベンダリングツールは細かい挙動の差に戸惑いやすい。
  • 結局パッケージングなどでmakeを使うことが多い。
  • ツール依存ふやすよりはmakeで解決できるんならそれで。
  • プロジェクトリポジトリに管理対象ではないコードを含めなくて済む。
  • リビジョンロックも実現できる。
  • 事前にフォークしておけば消失対策ができる。
  • makeを叩くだけで成果物が同一のコードを参照して出力される。
  • 一応この手法はGo1.5から使える。
  • ただしこの方法のプロダクトはgo getでインストールできません。

これでGoのパッケージエコシステムで 面倒だった課題はおおむね解決できるんじゃないでしょうか。

追記

go get 可能なライブラリを公開する場合は

3. git submodule

の手法がそのまま使えます。 ただし依存してるsubmodule一式がパブリックリポジトリに在ることと、フォルダツリーは標準推奨と同じでGOPATH配下で構築する必要があります。

go get では標準でリポジトリを「git clone --recursive」でクローンしますので、そのままsubmoduleは展開されます。

フォルダツリー例

  • GOPATH/src/github.com/<user-id>/<lib-name>
    • vendor/
      • golang.org/
        • x/
          • text/ <- submodule
    • lib.go