TL;DR
- モジュール有効下の
go get
で取得できるバージョンは、さまざまな条件によって異なる場合がある- 特にproxyを利用したときと、利用しない時で取得できるバージョンが異なる場合がある
- それらを詳しく調べた
- proxy の
/@v/list
のバージョン選定アルゴリズムを推測した
随記
本調査の問題意識は「モジュール有効下で単にgo getした時に出てくるバージョンはどう決まっているのか」である。
発端は mattn/jvgrep (以下jvgrep) で普通に go get github.com/mattn/jvgrep
したときに出てくるバージョンが v5.8.1+incompatible だったことから。
なおこの時点で v5.8.5 がリリースされていた。
go 自体のバージョンは調査時点は 1.14.4 であった。
モジュール有効下時の go get
(以下単に go get
とする) は、実行時の環境の差異によってその振る舞いを微妙に変える。
例えば go.mod が特定できないディレクトリ、すなわちルートに至るまでいかなる親にも go.mod が存在しない場合、 @upgrade
, @latest
, @patch
の仮想バージョン指定は意味をなさず、すべて同じ動作になる。これらの動作の詳細は説明しない。
これらの動作は現在インストール済みのモジュールのバージョンに依存するのだが go get
はそのソースを go.mod に依存しているため、なければ全部同じで「最新のモジュールを取り出す」にならざるをえない。
以下では go.mod が存在しない場合を想定している。
今回フォーカスしているのは go get
が「最新のモジュール」のバージョンをどのように決定しているかである。
知っての通り go get
は通常ならば proxy.golang.org を介してモジュールを取得する。
proxy の説明は go help goproxy
で参照できるが、
いま知っておくべきなのはバージョン一覧を取得する
https://proxy.golang.org/{モジュール名}/@v/list
(以下 /@v/list
) と最新バージョンを取得する
https://proxy.golang.org/{モジュール名}/@latest
(以下 /@latest
) だけである。
なおモジュール名には具体的には github.com/mattn/jvgrep
みたいな文字列が入ることになる。
ここで1つ疑問が増えるわけだが jvgrep の /@latest
は v4.9.0+incompatible になっている。 go get
で取れるのは v5.8.1+incompatible なのでこの差異がどこから来るのか。
go のソースコードを読んで、時に書き換えて調べてみた。
まず v5.8.1+incompatible のほうだが、これは /@v/list
から semver で最新版を決定していた。
/@latest
は一切かかわっていない。
次に proxy を使わずに直接 go get
した場合は v4.9.0+incompatible が出てきた。
なお proxy を使わない場合の取得コマンドは次の通り。
$ GOPROXY=direct GO111MODULE=on go get github.com/mattn/jvgrep
以降 go がバージョンを取り扱うとき3種類のバージョンが存在することを意識する必要がある。
1つは VCS に付けられるタグで v1.2.3 のように semver に従ってるやつのこと。 go はコレを他のバージョンのソースにしているが、そのまま使わないことも多い。 むしろこのことが混乱を招いていると思える。 これをA系といおう。
2つめはメジャーバージョンで v0, v1, v2, … といった感じ。 これはタグによるバージョンから決まる。 また v0 と v1 は同じモジュール名として認識されるよう特別扱いされている。 これをB系としよう。
なお v2 以降のメジャーバージョンは v2 and Beyond に示されるように、
モジュール名を /v2
付きに変える必要がある。
なおこのあたりを調べているときに気が付いたのが v8
(有名なJavaScriptエンジン)のようなレポジトリ名は go では正しく扱えないことが判明した。
参考: https://github.com/golang/go/issues/28435#issuecomment-440324231
3つめは v5.8.1+incompatible や v0.0.0-{commitid}-{datetime} のような go が管理目的で便宜的に生成したバージョン名。 go.mod などの中でよく目にする形式である。 これはC系とする。
最新バージョンの決定の話に戻ろう。
最新バージョンの決定は VCS 毎にアルゴリズムが異なってしまう可能性があるのだが、 今回は github なので git に限定する。
この場合は複雑なのだが、基本的にはタグの一覧 (refs/tags/*
) に依存する。
このタグは当然A系。
A系の内、B系に変換して v0 および v1 に属する最新バージョン、そこに go.mod が存在した場合は最新バージョンの選定に使われるタグは v0 および v1 のものに限定される。 (codrepo.go#L208-L226)
C系バージョンにおいて +incompatible
が付く条件は、A系のうち v0 および v1 に属さず go.mod がないやつ。
つまり go.mod がないならば v2.0.0 (A) 以降は常に +incompatible だ。
v0 および v1 に go.mod が追加されたら全ての +incompatible (C)よりも v1 の compatible (無印) が優先される。
具体的にいうと、仮に jvgrep に v1.10.0 として正しい go.mod を置いたならば GOPROXY=direct GO111MODULE=on go get github.com/mattn/jvgrep
で出てくるのは v4.9.0+incompatible ではなく v1.10.0 になるだろう。
このあたりはモジュールとしての利用において不用意なメジャーアップグレードを避ける目的で納得度が高い。 しかしツールとしての利用においてはやや不満がある。
ここまでで最新バージョンを決定できない場合 +incompatible の中から選定される。 この 選定アルゴリズム は一見ややこしい。 だが言葉にすると意外と簡単、「go.mod が存在するメジャーバージョンより前のバージョンの最新」だ。
jvgrep で具体例をみてみよう。 jvgrep は v5.8.2 (A)から go.mod を含むようになった。 そのため v5 (B) よりも古い中で一番新しいものが最新バージョンとなる。 一覧をみると それが v4.9.0+incompatible (C) だということがわかる。
おっと忘れていたがソースコードで示すとココで昇順にソートされたバージョン一覧を取得して、 コッチでその末尾を取るため最新のバージョンになる。 この周辺ではバージョン選定における束縛を実装しているので、興味がある人は読んでみると良い。
長くなったが GOPROXY=direct GO111MODULE=on go get github.com/mattn/jvgrep
で出てくるのは v4.9.0+incompatible ということだ。
推測ではあるが先にあげたこの疑問の答えはコレだろう。proxy の /@latest
はこのアルゴリズムによって決定されているのだろう。
ここで1つ疑問が増えるわけだが jvgrep の
/@latest
は v4.9.0+incompatible になっている。go get
で取れるのは v5.8.1+incompatible なのでこの差異がどこから来るのか。
一方で /@v/list
のアルゴリズムはわからない。
残念ながら proxy.golang.org のソースコードは非公開であるため推測する以外の方法がない。
該当部分だけパッケージとして切り出されている可能性はある。
知っていたら教えて欲しい。
ただおそらく v0 および v1 に属するやつ全部、それ以外でも go.mod のないやつは全部、ということは推測に難くない。
つまり GOPROXY=direct GO111MODULE=on go get
のバージョン候補の選定と、proxy の /@v/list
の選定アルゴリズムは異なってる。
これも混乱の一要因であるといえよう。
なお go.mod ありの v2 (A) 以降は、モジュール名を変えるしかないのでこれは諦めてほしい。
その際にレポジトリ内にフォルダを掘るか go.mod の module
を修正するにとどめるかは各自の選択となっている。
ややこしく長い話になってしまいましたが、ここまで真面目に読んでいただいた方、ご苦労さま&ありがとうございます。