Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 12580] ファイルの概要

このコミットは、Go言語のgoコマンドにおけるgo getサブコマンドの挙動を改善するものです。具体的には、go get new.code/...のように、まだローカルに存在しない新しいコードベースに対してワイルドカード(...)を含むパスを指定した場合に、正しく動作するように修正が加えられています。これにより、新しいリポジトリやパッケージ群を一度に取得する際の利便性と信頼性が向上しました。

コミット

commit 4e18bfb9306e80fd16522bfb6a4a98c3f2b42c0d
Author: Russ Cox <rsc@golang.org>
Date:   Mon Mar 12 16:35:15 2012 -0400

    cmd/go: make go get new.code/... work
    
    Fixes #2909.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/5796072

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/4e18bfb9306e80fd16522bfb6a4a98c3f2b42c0d

元コミット内容

cmd/go: make go get new.code/... work

Fixes #2909.

変更の背景

go getコマンドは、Goパッケージの依存関係を解決し、リモートリポジトリからソースコードを取得してローカルのGOPATHに配置するための重要なツールです。このコミットが修正する問題は、go getがワイルドカード(...)を含むパスを処理する際の既存の制限に関連しています。

以前のgo getの実装では、new.code/...のように、まだローカルにクローンされていない新しいリポジトリに対してワイルドカードを使用した場合、期待通りに動作しないケースがありました。これは、go getがワイルドカードを展開する際に、ローカルに存在するパッケージ情報に依存していたためと考えられます。つまり、パッケージがまだダウンロードされていない場合、ワイルドカードが正しく解釈されず、結果として目的のコードが取得できないという問題が発生していました。

この問題は、特に新しいプロジェクトを開始する際や、大規模なライブラリ群を一度に取得する際に、ユーザーエクスペリエンスを著しく損なうものでした。コミットメッセージにあるFixes #2909は、この特定のバグ報告に対応するものであることを示しています。この修正により、go getは、ワイルドカードを含むパスが指定された場合でも、まずリポジトリをダウンロードし、その後でワイルドカードを適切に展開して、関連するすべてのパッケージを取得できるようになります。

前提知識の解説

go getコマンド

go getは、Go言語のビルドツールチェーンの一部であり、リモートリポジトリからGoパッケージとその依存関係をダウンロードし、ローカルのGOPATH(またはGo Modulesが有効な場合はモジュールキャッシュ)にインストールするために使用されます。これにより、開発者は外部のライブラリやツールを簡単にプロジェクトに組み込むことができます。

Goのインポートパスとワイルドカード(...

Go言語では、パッケージはインポートパスによって識別されます。これは通常、バージョン管理システムのリポジトリパスと、そのリポジトリ内のパッケージのディレクトリパスを組み合わせたものです(例: github.com/user/repo/package)。

ワイルドカード...は、Goのツールにおいて「任意のサブディレクトリとファイル」を意味するために使用されます。例えば、example.com/repo/...example.com/repo以下のすべてのパッケージ(サブディレクトリを含む)を指します。これは、複数の関連するパッケージを一度に操作したい場合に非常に便利です。

go/buildパッケージ

go/buildパッケージは、Goのソースコードを解析し、パッケージのビルドに関する情報(インポートパス、ソースファイルのリスト、依存関係など)を提供する標準ライブラリです。go getのようなツールは、このパッケージを利用してGoのプロジェクト構造を理解し、ビルドプロセスを管理します。

  • build.IsLocalImport(path string) bool: この関数は、与えられたインポートパスがローカルファイルシステム上のパッケージを指しているかどうかを判断します。例えば、相対パス(例: ./mypackage)や、GOPATH内のパスなどがローカルインポートと見なされます。

packageCache

goコマンド内部では、パッケージ情報をキャッシュして、同じパッケージの情報を何度も解析し直すことを避けています。packageCacheは、このキャッシュメカニズムの一部であり、パッケージのインポートパスやディレクトリパスをキーとして、解析済みの*Package構造体を格納します。

技術的詳細

このコミットは、主にsrc/cmd/go/get.gosrc/cmd/go/http.gosrc/cmd/go/vcs.goの3つのファイルに変更を加えています。

src/cmd/go/get.goの変更

最も重要な変更はget.goに集中しています。

  1. downloadPaths関数の導入:

    • runGet関数内で、これまではimportPaths(args)を直接呼び出していましたが、新たにdownloadPaths(args)を呼び出すように変更されました。
    • downloadPathsは、引数リストをdownload関数に渡す前に準備する新しい関数です。
    • この関数は、ワイルドカード(...)を含むパスを事前に展開しようとします。
    • build.IsLocalImportを使用してローカルインポートかどうかを判断し、matchPackagesInFS(ファイルシステム内のパッケージをマッチング)またはmatchPackages(インポートパスに基づいてパッケージをマッチング)を呼び出してワイルドカードを展開します。
    • 重要なのは、ワイルドカードが展開できない場合(例: まだダウンロードされていない新しいリポジトリの場合)でも、そのパスを結果リストに残すことです。これにより、download関数がリポジトリを特定し、ダウンロードする機会が与えられます。
  2. ダウンロードとインストールフェーズの分離と再評価:

    • runGetの処理フローが再構築されました。
    • Phase 1 (Download/Update): downloadPathsで準備されたパスに基づいて、download関数が呼び出され、パッケージのダウンロードまたは更新が行われます。
    • Phase 2 (Rescan packages and reevaluate args list): ダウンロードが完了した後、packageCacheから関連するエントリが削除され、importPaths(args)が再度呼び出されます。これにより、新しくダウンロードされたパッケージ情報が反映され、ワイルドカードが正しく展開されるようになります。
    • Phase 3 (Install): getDフラグ(ダウンロードのみ)が設定されていない場合に、runInstallが呼び出され、パッケージのインストールが行われます。getDのチェックがこのフェーズに遅延されたのは、importPathsがエラーを出力する機会を与えるためです。
  3. download関数の改善:

    • download関数内で、パッケージがダウンロードされた後、ワイルドカードを含む引数(arg)が指定されていた場合に、そのワイルドカードを再評価するロジックが追加されました。
    • wildcardOkayフラグ(スタックが空の場合にtrue)とstrings.Contains(arg, "...")のチェックにより、ワイルドカードの再評価が必要かどうかが判断されます。
    • 再評価の際も、build.IsLocalImportに基づいてmatchPackagesInFSまたはmatchPackagesが使用されます。
    • ダウンロード後にpackageCacheをクリアする処理が追加され、新しいパッケージ情報が確実にロードされるようにしています。
    • fixツールの実行とそれに続くパッケージのリロードのロジックが、ワイルドカード展開後の複数のパッケージに対応できるようにループ内に移動されました。

src/cmd/go/http.goの変更

  • httpsOrHTTP関数内で、HTTPフェッチが失敗した場合のlog.Printf("http fetch failed")という冗長なログ出力が削除されました。これは、エラーが既に返されているため、追加のログは不要と判断されたためです。

src/cmd/go/vcs.goの変更

  • repoRootForImportPath関数に、ワイルドカード(...)がリポジトリルート自体に含まれている場合の追加のチェックが導入されました。
  • strings.Contains(importPath, "...") && strings.Contains(rr.root, "...")という条件が追加され、もしインポートパスとリポジトリルートの両方にワイルドカードが含まれている場合、エラーを返すようになりました。これは、リポジトリルート自体にワイルドカードが含まれることは許可されないという制約を強制するためです。例えば、example.com/repo/.../subのようなパスは不正と見なされます。

これらの変更により、go getは、ワイルドカードを含む新しいインポートパスに対しても、より堅牢かつ正確に動作するようになりました。

コアとなるコードの変更箇所

src/cmd/go/get.goにおけるrunGetdownloadPathsの変更

--- a/src/cmd/go/get.go
+++ b/src/cmd/go/get.go
@@ -57,19 +58,13 @@ func init() {
 
 func runGet(cmd *Command, args []string) {
 	// Phase 1.  Download/update.
-	args = importPaths(args)
 	var stk importStack
-	for _, arg := range args {
+	for _, arg := range downloadPaths(args) {
 		download(arg, &stk)
 	}
 	exitIfErrors()
 
-	if *getD {
-		// download only
-		return
-	}
-
-	// Phase 2. Install.
+	// Phase 2. Rescan packages and reevaluate args list.
 
 	// Code we downloaded and all code that depends on it
 	// needs to be evicted from the package cache so that
@@ -80,9 +75,48 @@ func runGet(cmd *Command, args []string) {
 		delete(packageCache, name)
 	}
 
+	args = importPaths(args)
+
+	// Phase 3.  Install.
+	if *getD {
+		// Download only.
+		// Check delayed until now so that importPaths
+		// has a chance to print errors.
+		return
+	}
+
 	runInstall(cmd, args)
 }
 
+// downloadPath prepares the list of paths to pass to download.
+// It expands ... patterns that can be expanded.  If there is no match
+// for a particular pattern, downloadPaths leaves it in the result list,
+// in the hope that we can figure out the repository from the
+// initial ...-free prefix.
+func downloadPaths(args []string) []string {
+	args = importPathsNoDotExpansion(args)
+	var out []string
+	for _, a := range args {
+		if strings.Contains(a, "...") {
+			var expand []string
+			// Use matchPackagesInFS to avoid printing
+			// warnings.  They will be printed by the 
+			// eventual call to importPaths instead.
+			if build.IsLocalImport(a) {
+				expand = matchPackagesInFS(a)
+			} else {
+				expand = matchPackages(a)
+			}
+			if len(expand) > 0 {
+				out = append(out, expand...)
+				continue
+			}
+		}
+		out = append(out, a)
+	}
+	return out
+}
+
 // downloadCache records the import paths we have already
 // considered during the download, to avoid duplicate work when
 // there is more than one dependency sequence leading to

src/cmd/go/vcs.goにおけるワイルドカードのルートチェック

--- a/src/cmd/go/vcs.go
+++ b/src/cmd/go/vcs.go
@@ -323,6 +323,23 @@ func repoRootForImportPath(importPath string) (*repoRoot, error) {
 	rr, err := repoRootForImportPathStatic(importPath, "")
 	if err == errUnknownSite {
 		rr, err = repoRootForImportDynamic(importPath)
+
+		// repoRootForImportDynamic returns error detail
+		// that is irrelevant if the user didn't intend to use a
+		// dynamic import in the first place.
+		// Squelch it.
+		if err != nil {
+			if buildV {
+				log.Printf("import %q: %v", importPath, err)
+			}
+			err = fmt.Errorf("unrecognized import path %q", importPath)
+		}
+	}
+
+	if err == nil && strings.Contains(importPath, "...") && strings.Contains(rr.root, "...") {
+		// Do not allow wildcards in the repo root.
+		rr = nil
+		err = fmt.Errorf("cannot expand ... in %q", importPath)
 	}
 	return rr, err
 }

コアとなるコードの解説

runGetdownloadPathsの変更

以前のrunGetでは、最初にimportPaths(args)を呼び出して引数を処理していました。このimportPathsは、ワイルドカードを展開する際に、ローカルに存在するパッケージ情報に依存していました。そのため、まだダウンロードされていない新しいリポジトリに対してnew.code/...のようなパスが指定された場合、ワイルドカードが正しく展開されず、download関数に適切な引数が渡されないという問題がありました。

新しいrunGetでは、まずdownloadPaths(args)を呼び出すように変更されています。 downloadPaths関数は、この問題を解決するために導入されました。

  1. importPathsNoDotExpansion(args): まず、引数からワイルドカードを展開せずにインポートパスを処理します。これは、ワイルドカードがまだ展開できない可能性があるためです。
  2. ワイルドカードの事前展開試行: 各引数aについて、strings.Contains(a, "...")でワイルドカードが含まれているかを確認します。
  3. build.IsLocalImport(a): パスがローカルインポートであるか(例: ./mypackage)を判断します。
  4. matchPackagesInFS(a) / matchPackages(a): ローカルインポートであればファイルシステムから、そうでなければインポートパスに基づいてワイルドカードを展開しようとします。
  5. 展開できなかった場合の処理: len(expand) > 0で展開が成功したかを確認します。もし展開できた場合は、その展開されたパスをoutに追加します。重要なのは、展開できなかった場合でも、元のワイルドカードを含むパスaoutに追加することです。 これにより、download関数がそのパスを受け取り、リポジトリを特定してダウンロードを試みることができます。

このdownloadPathsの導入により、go getは、ワイルドカードを含む新しいコードパスが指定された場合でも、まずリポジトリをダウンロードし、その後でワイルドカードを適切に再評価して、関連するすべてのパッケージを取得できるようになりました。

ダウンロードが完了した後、runGetpackageCacheをクリアし、再度importPaths(args)を呼び出します。この再評価により、新しくダウンロードされたコードベース内のワイルドカードが正しく展開され、その後のインストールフェーズで適切なパッケージが処理されるようになります。

vcs.goにおけるワイルドカードのルートチェック

repoRootForImportPath関数は、与えられたインポートパスからリポジトリのルートを特定する役割を担っています。このコミットでは、動的なインポートパス解決(repoRootForImportDynamic)の後に、以下の新しいチェックが追加されました。

if err == nil && strings.Contains(importPath, "...") && strings.Contains(rr.root, "...") {
	// Do not allow wildcards in the repo root.
	rr = nil
	err = fmt.Errorf("cannot expand ... in %q", importPath)
}

このコードは、以下の条件がすべて真の場合にエラーを発生させます。

  • err == nil: これまでの処理でエラーが発生していない。
  • strings.Contains(importPath, "..."): 元のインポートパスにワイルドカードが含まれている。
  • strings.Contains(rr.root, "..."): 特定されたリポジトリのルートパスにもワイルドカードが含まれている。

これは、リポジトリのルート自体にワイルドカードが含まれるようなインポートパスは不正であるという制約を強制するためのものです。例えば、example.com/repo/.../subpackageのようなパスは、repoRootForImportPathexample.com/repo/...をリポジトリルートとして特定した場合に、このチェックによってエラーとなります。リポジトリルートは明確なものでなければならず、ワイルドカードを含むべきではないという設計思想に基づいています。

関連リンク

参考にした情報源リンク

  • コミットメッセージと変更されたソースコード
  • Go言語の公式ドキュメント(go getgo/buildパッケージに関する一般的な知識)
  • Go言語のワイルドカード(...)に関する一般的な情報
  • GoのIssueトラッカー(#2909の具体的な内容は、公開されている情報からは特定できませんでしたが、コミットメッセージからその目的を推測しました。)