blog.syfm

徒然なるままに考えていることなどを書いていくブログ

dept を使った Go ツールの依存管理

はじめに

Go プロジェクトでは、しばしば静的解析ツールが使用されます。例えば、自分のあるプロジェクトで考えてみると、ソースコードの整形には gofmt 、lint には golangci-lint/golangci-lint 、モック用コードの生成には matryer/moq 、CI でのリリース作業では mitchellh/goxtcnksm/ghr が使われています。Go は静的解析を行いやすいプログラミング言語であるため、こうした静的解析ツールは日常的に使用されています。

しかし、Go プロジェクトにおいて、そういったツールの管理に関する問題がいくつか挙げられます。

  1. ツールのバージョン管理が煩雑
  2. ツールの統一的なインストール方法がない

ツールのバージョン管理が煩雑

Go 1.11 以前のバージョンでは、go getgo1 タグ or ブランチがない限り最新のソースコードを取得します。(go1 については budougumi0617 さんの記事 が詳しいです。)
ただ、ほとんどのリポジトリでは上記のようなタグ or ブランチが用意されていないため、あまり有用ではないと感じています。

同様の理由で go get の対象パッケージの依存パッケージ群のバージョンも管理できません。(ただし、リモートリポジトリに vendor ディレクトリがある場合はそちらに配置されているパッケージが優先的に使われます。)

ツールの統一的なインストール方法がない

Go ツールのインストールは主に go get によって行われます。ツールセットをインストールしようとする場合、例えばあるプロジェクトでは README.md に記述された go get スニペットをコピペして実行したり、またあるプロジェクトでは Makefile に記述したりと様々な方法があり、ツールセットをインストールするための方法にばらつきがあります。

ktr0731/dept

幸いなことに、1 つ目の問題は Go Modules の登場によって解決されましたが、2 つ目の問題は依然として残っていました。
この問題を解決するために、dept というツールをつくりました。このツールは、プロジェクトで使用するツールセットの依存管理を行い、Go Modules を利用して依存解決を行います。 以降ではその詳細について紹介します。

github.com

Go Modules におけるツールの依存管理

Go 1.11 では Russ Cox により提案された Go Modules が実験的に導入されました。 Go Modules では、関連のある複数のパッケージをまとめたものをモジュールとして定義し、それらをセマンティックバージョニングで管理し、セマンティックインポートバージョニングによって依存解決を行います。 ライブラリだけでなくツールもモジュール単位になりうるため、セマンティックバージョニングでモジュールを管理するだけでその恩恵を受けることができます。

詳細を解説すると、このポストの本題から外れてしまうので省略します。Go Modules に関する情報はすべて以下の Wiki にまとめられています。

github.com

ツールの依存管理については Wiki に一例が紹介されています。
この方法では、tools ディレクトリに tools.go という、使用したいツールのパッケージをブランクインポートした Go ファイルを作成し、go install <package> を行うことでそれらの依存を go.mod に記録しつつ、バイナリを生成します。

この方法は、新しいメカニズムを入れずに実現できますが、ワークフローが少々煩雑です。
また、依存管理されるツールのモジュールは go.mod に記述されますが、npm の devDependencies などとは異なり、開発時のみに使用するモジュールを分けて管理することができません。

dept は、Go Modules の持つ強みを活かしつつ、上記の様な問題を解決した、より簡単に扱えるラッパーツールです。

dept を使った依存管理

deptgo コマンドと似た非常にシンプルなインターフェースを提供しています。プロジェクトに新しくツールを追加するには dept get を使います。modules-aware mode の go get と同様にバージョンを指定してインストールすることもできます。

$ dept init
$ dept get github.com/mitchellh/gox github.com/tcnksm/ghr@v0.12.0

dept get を行うと、プロジェクトルートに gotool.mod が生成されます。以降はこのファイルを元にツールセットの決定論的なビルドを行うことができます。 透過的にツールを使用したい場合、dept exec が、その他の理由で生成されたバイナリを直接扱いたい場合は dept build が使えます。

$ dept exec ghr -v
ghr version v0.12.0

$ dept build
$ ls _tools
ghr             gox

この他にもいくつか便利なサブコマンドがあります。気になった方はぜひ dept -h を叩いてみてください。

dept の実装

これ以降は dept がどうやって上記のような機能を実装しているのかや、それに伴うつらみを紹介します。

ラッパーツールと銘打っている通り、dept 内部では go コマンドのサブコマンドである go get や、go buildgo mod などのコマンドを実行しています。
例えば、新しいツールを依存に追加する dept get であれば、go getgo build などを叩いたりします。また、これらのコマンドは Go Modules を使うために常に modules-aware mode で実行されます。
このあたりの操作は多少の違いはあれど、上記で紹介した tools.go を用意する方法とほとんど同様です。

これらのコマンドから使われる Go Modules のマニフェストファイル、go.mod は、以下のような理由からプロジェクトで使用される go.mod とは別に管理しています。

  • プロジェクトで使用するモジュールと、ツールが必要とするモジュールを別々に管理したいため
  • dept のリネーム機能や、一つのモジュール内に複数のツールがある場合に go.modシンタックスではそれらを表現できないため

特に二番目の理由はなかなか厄介な問題です。go.modJSON や TOML 等のデータ記述言語ではなく、独自のシンタックスを持った DSL であるため、拡張が非常に困難です。 以前、以下の issue にて議論が行われていましたが、

  1. go.mod を読み書きできる、十分なドキュメントがある API を提供するのであれば言語がなんであれ問題ない
  2. 既存のシンタックスは必要最小限のものであるため、現段階では DSL をそのまま使うべき

といった理由で現在のシンタックスが継続して使われるようになりました。

github.com

特に read-only な操作であれば、go mod edit -json を使うことで JSON として取得できるため、現状大きな不満はありません。

ただ、今回のような go.mod の表現を拡張するといった特異なケースだとやっぱり API が欲しくなります。
今の go.mod の読み書きを扱うパッケージは以下の様に internal 以下に存在し、dept でもこのパッケージを引っ張ってきて使用しています。

modfile - GoDoc

今後このパッケージは public にすることが予定されているようなので、これを心待ちにしています…。

まとめ

  • dept を使うと、ツールの管理が楽になるよ
  • go.mod DSL は拡張が難しくてしんどい時がある