Closed19

Dive into `go install`

MURAOKA TaroMURAOKA Taro

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

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

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

対象は go 1.16.5 のソース

MURAOKA TaroMURAOKA Taro

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

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

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

MURAOKA TaroMURAOKA Taro

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

MURAOKA TaroMURAOKA Taro

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

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

MURAOKA TaroMURAOKA Taro

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.
MURAOKA TaroMURAOKA Taro

いろいろの内容。

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

ビルドリストの編集 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.
MURAOKA TaroMURAOKA Taro

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

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

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

MURAOKA TaroMURAOKA Taro

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

MURAOKA TaroMURAOKA Taro

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

MURAOKA TaroMURAOKA Taro

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

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

MURAOKA TaroMURAOKA Taro

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

MURAOKA TaroMURAOKA Taro

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が空の時に何が起こるかを知る必要がある。

MURAOKA TaroMURAOKA Taro
$ 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から見ておくか。

MURAOKA TaroMURAOKA Taro

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

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

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

MURAOKA TaroMURAOKA Taro

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に対応するらしいんだけど…詳細は保留。

以上

MURAOKA TaroMURAOKA Taro

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

答え

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

MURAOKA TaroMURAOKA Taro

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

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

MURAOKA TaroMURAOKA Taro

ここまでのまとめ:

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モードの違いを把握すればおおよそ網羅できそう。

このスクラップは2022/04/04にクローズされました