Open19

Dive into `go install`

go install の挙動をソースコードを読んで調べる。

  • @{version_spec} の有無
  • go.mod の有無

で動作が変わるので、それが具体的にどう変わっているかを知りたい。

対象は go 1.16.5 のソース

なおドキュメント上は @{version_suffix} がある場合は module aware で動いてカレントディレクトリの go.mod は無視する。つまり常にmodule awareモード

またversion suffixが無い場合は、module awareモードもしくはGOPATHモードで動く。実際にどちらで動くははGO111MODULEの設定とgo.modの有無で決まる。module awareモードになった場合はmainモジュールのコンテキストで実行される

この2つのmodule awareモードがまったく同じ振る舞いなのか何か差分があるのかは不明だが、おそらくgo.modを見るか見ないかの違いがあると推測される。

src/cmd/go/main.go からスタート
以下特に断りがない限りパスはsrc/cmd/goからの相対表記とする。

install用のコマンドは work.CmdInstall で定義されている
これはinternal/work/build.go で定義されている runInstall() が本体

https://github.com/golang/go/blob/7677616a263e8ded606cc8297cb67ddc667a876e/src/cmd/go/internal/work/build.go#L578

これはどちらかを引き起こす

  • installOutsideModule()
    • @が含まれ、ローカルインポート(start with ./ nor ../)ではなく、絶対パスではないとき
  • InstallPackages()
    • cfg.ModulesEnabled && !modload.HasModRoot() の時にextraなチェックがある

    • load.PackagesAndErrors() が挟まる

      この子もmodloadの面倒を見てるっぽい。

なお -i は deprecated

InstallPackages() はどうもGOPATHモードの実装くさい。
いわゆる Builder 作って AutoAction を突っ込んでいくスタイル。
(これは過去にみたことがあるので追わない)

このパスでは modload ではなくて load を使ってるし
モジュールは関係ないと仮定して、なにか新事実がみつかるまで本件では調査しない。

installOutsideModule()はいろいろ仕込んだうえでInstallPackages()を呼び出している。
このいろいろがキモになりそう。とはいえ引数の違いにくらいしか反映されなさそうではある。

// installOutsideModule implements 'go install pkg@version'. It builds and
// installs one or more main packages in module mode while ignoring any go.mod
// in the current directory or parent directories.

いろいろの内容。

  1. パス&バージョンのエラーチェック 該当コード
  2. 1st引数のモジュールのgo.modを取得してチェック
    • version suffixの内容チェック: latest, upgrade, patch, < prefix, > prefix, semver
  3. modloadでビルドリストを構築する
  4. 必要なmoduleをロード: ... が含まれる時にextraな処理がある
  5. ロード結果の精査

ビルドリストの編集 modload.EditBuildList がキモっぽい。
他は runInstall の InstallPackages のパスでも見た。

// EditBuildList edits the global build list by first adding every module in add
// to the existing build list, then adjusting versions (and adding or removing
// requirements as needed) until every module in mustSelect is selected at the
// given version.
//
// (Note that the newly-added modules might not be selected in the resulting
// build list: they could be lower than existing requirements or conflict with
// versions in mustSelect.)
//
// If the versions listed in mustSelect are mutually incompatible (due to one of
// the listed modules requiring a higher version of another), EditBuildList
// returns a *ConstraintError and leaves the build list in its previous state.

modload.EditBuildList()modload.buildListをいじる。
アップグレードするモジュール(mvs.Upgrade())と
ダウングレードするモジュール(mvs.Downgrade())を決めて、
最終的なbuildListを作る。

buildList内に同じモジュールの異なるバージョンがあると
inconsistentということでエラーになる。
inconsistent時のエラーメッセージをわかりやすくするために
厚めの事後処理をしている。

mvs.Upgrade()mvs.Downgrade()の中身を軽く見たら、
work.InstallPackages()と、modloadの繋がり方を確認する。

パッケージmvsはMinimal Version Selectの意味。
なんかもうこれだけでソースはあんま追わなくてよい気になってきた。

mvs.Upgrade()はシンプル。
必須モジュール(upgrade)を既存モジュールリストに加える。
その際同名のモジュールがあったらよりバージョンの高い方を採用する。

mvs.Downgrade()はそれに比べるとちょっと大きい。
現時点で読むのはやめる。

ポイントを押さえれば大きさの割には読みやすくなるはず。

ちょっと気になったのはmodload.Targetがmvsにも引き渡され、
Upgrade()およびDowngrade()におけるモジュール一覧の初期値となってる。
ここが空かどうかがgo.modコンテキスト有無(≒no root)になってる予感。

NoRootについて気になったので調べる。

modload.RootNode = modload.NoRootwork.installOutsideModule()にあった。
なので前節の予感は間違ってた。

RootNodeにとれるのはRoot列挙型で値はAutoRoot, NoRoot, NeedRootのみ。
NoRootはmodload.Init()で使われ、modload.modRootを強制的に空にする。

ちなみにNoRootでなければmodRootには、CWDにgo.modがあることを確認して、CWDが設定される。
モジュールのサブディレクトリの場合は親に向かってgo.modを探す必要がありそうだけど…
今はそれは興味の対象ではないから詳しくは見ない。

値AutoRootは使ってない。デフォルト値ということだろうね。

NeedRootを参照しているのはNoRootの参照場所に近い。
modRootが決められなかった時にエラーにしている。


つまり今はmodRootが空の時に何が起こるかを知る必要がある。

$ grep -nr 'modRoot [!=]= ""' ./internal/modload/
./internal/modload/init.go:165: if modRoot != "" {
./internal/modload/init.go:175:         if modRoot == "" {
./internal/modload/init.go:216: if modRoot == "" {
./internal/modload/init.go:250: if modRoot != "" || cfg.ModulesEnabled {
./internal/modload/init.go:271: if modRoot := findModuleRoot(base.Cwd); modRoot == "" {
./internal/modload/init.go:292: return modRoot != "" || cfg.ModulesEnabled
./internal/modload/init.go:309: return modRoot != ""
./internal/modload/init.go:361: if modRoot == "" {
./internal/modload/init.go:607: if modRoot == "" {
./internal/modload/init.go:879: if modRoot == "" {
./internal/modload/load.go:406: if modRoot != "" && absDir == modRoot {
./internal/modload/load.go:416: if modRoot != "" && strings.HasPrefix(absDir, modRoot+string(filepath.Separator)) && !strings.Contains(absDir[len(modRoot):], "@") {

そんな多くない。まずはload.goから見ておくか。

load.goはresolveLocalPackage()で使ってる。

// resolveLocalPackage resolves a filesystem path to a package path.

modRootがある時にいくらかの判定を挟むためのもの。
逆に言うとないときはそれらの判定をすっ飛ばして
builtinやstandard runtimeとして解決するんだろう。
まぁそうだよね、という感じ。

init.goではmodRoot自身のセットアップと、別パッケージから状態をチェックする目的で使ってそう。

EnabledではmodRootがあるかcfg.ModulesEnabledでmoduleの有効性を判断してる。

LoadModFileではmodRootが無い時に
ダミーのmodload.Targetとmodload.buildListを仕立ててる。
たぶんこれがポイント

setDefaultBuildModではmodRootが無い時に
cfg.BuildMod = "readonly" にしてる。
恐らくgo.modを変更しないためのものかな。

と思ったらWriteGoMod()でもチェックして
書かないためにearly returnしてる。
cfg.BuildMod-modに対応するらしいんだけど…詳細は保留。

以上

ちょっと気になったのはmodload.Targetがmvsにも引き渡され、
Upgrade()およびDowngrade()におけるモジュール一覧の初期値となってる。
ここが空かどうかがgo.modコンテキスト有無(≒no root)になってる予感。

答え

go.modコンテキストの有無はmodload.modRootにあらわされ
空の時はコンテキストが空とされmodload.Targetにはダミーのものが入る。
空でないときはgo.modファイルが読み込まれ
modFileToBuildList()においてその内容に基づいてTargetが埋められる。

NoRootモードではmodfetch.GoMod()でgo.modを持ってきてる。
(場所はwork/build.goのinstallOutsideModule())

これはそのモジュール&バージョンの存在チェックと
ReplaceやExcludeなどを使ってないかの事前確認にとどまってそう。
ダミーとなっているmodload.Target情報の補足には使ってない。
そちらにはmodload.EditBuildList()mustSelectで渡している。

ここまでのまとめ:

go installwork.InstallPackages() が本体。
ここに至るまでにどのように環境をセットアップするかでgo installの動作モードが変わる。
動作モードは大まかにわけて以下の3つ。

  • GOPATHモード
  • moduleモード(go.mod有)
  • moduleモード(go.mod無)

うちGOPATHモードについてはまだしっかり理解してない。
moduleモードのgo.modの有無はMinimal Version Selectionにおける初期依存モジュールの違いに現れる。

go.modが無い場合は当然初期依存モジュールが空になり、ExcludeとReplaceが使えなくなる。
結果としてツールが依存するモジュール全てにおいて
ツールauthorが想定したとおりのバージョンが採用される。

一方でgo.modがある場合はそれらが変わってしまうことがある。
それを意図する場合もあるだろうが
GOBIN指定なしで共有ディレクトリにgo installしちゃうと
依存モジュールバージョンが異なるバイナリが入る可能性がある。
これはプロジェクトをまたいで同じツールを使う場合にあまり好ましいとは言えない。

なのでプロジェクトでgo.modでツールのバージョンを管理する際には、
GOBINをプロジェクト固有のものにしてgo install(without version suffix)するか
いっそ常にgo runでツールを実行したほうが良い。


あとはwork.InstallPackages()の中身をみて、
GOPATHモードとmoduleモードの違いを把握すればおおよそ網羅できそう。

作成者以外のコメントは許可されていません