[インデックス 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.go、src/cmd/go/http.go、src/cmd/go/vcs.goの3つのファイルに変更を加えています。
src/cmd/go/get.goの変更
最も重要な変更はget.goに集中しています。
-
downloadPaths関数の導入:runGet関数内で、これまではimportPaths(args)を直接呼び出していましたが、新たにdownloadPaths(args)を呼び出すように変更されました。downloadPathsは、引数リストをdownload関数に渡す前に準備する新しい関数です。- この関数は、ワイルドカード(
...)を含むパスを事前に展開しようとします。 build.IsLocalImportを使用してローカルインポートかどうかを判断し、matchPackagesInFS(ファイルシステム内のパッケージをマッチング)またはmatchPackages(インポートパスに基づいてパッケージをマッチング)を呼び出してワイルドカードを展開します。- 重要なのは、ワイルドカードが展開できない場合(例: まだダウンロードされていない新しいリポジトリの場合)でも、そのパスを結果リストに残すことです。これにより、
download関数がリポジトリを特定し、ダウンロードする機会が与えられます。
-
ダウンロードとインストールフェーズの分離と再評価:
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がエラーを出力する機会を与えるためです。
-
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におけるrunGetとdownloadPathsの変更
--- 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
}
コアとなるコードの解説
runGetとdownloadPathsの変更
以前のrunGetでは、最初にimportPaths(args)を呼び出して引数を処理していました。このimportPathsは、ワイルドカードを展開する際に、ローカルに存在するパッケージ情報に依存していました。そのため、まだダウンロードされていない新しいリポジトリに対してnew.code/...のようなパスが指定された場合、ワイルドカードが正しく展開されず、download関数に適切な引数が渡されないという問題がありました。
新しいrunGetでは、まずdownloadPaths(args)を呼び出すように変更されています。
downloadPaths関数は、この問題を解決するために導入されました。
importPathsNoDotExpansion(args): まず、引数からワイルドカードを展開せずにインポートパスを処理します。これは、ワイルドカードがまだ展開できない可能性があるためです。- ワイルドカードの事前展開試行: 各引数
aについて、strings.Contains(a, "...")でワイルドカードが含まれているかを確認します。 build.IsLocalImport(a): パスがローカルインポートであるか(例:./mypackage)を判断します。matchPackagesInFS(a)/matchPackages(a): ローカルインポートであればファイルシステムから、そうでなければインポートパスに基づいてワイルドカードを展開しようとします。- 展開できなかった場合の処理:
len(expand) > 0で展開が成功したかを確認します。もし展開できた場合は、その展開されたパスをoutに追加します。重要なのは、展開できなかった場合でも、元のワイルドカードを含むパスaをoutに追加することです。 これにより、download関数がそのパスを受け取り、リポジトリを特定してダウンロードを試みることができます。
このdownloadPathsの導入により、go getは、ワイルドカードを含む新しいコードパスが指定された場合でも、まずリポジトリをダウンロードし、その後でワイルドカードを適切に再評価して、関連するすべてのパッケージを取得できるようになりました。
ダウンロードが完了した後、runGetはpackageCacheをクリアし、再度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のようなパスは、repoRootForImportPathがexample.com/repo/...をリポジトリルートとして特定した場合に、このチェックによってエラーとなります。リポジトリルートは明確なものでなければならず、ワイルドカードを含むべきではないという設計思想に基づいています。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/4e18bfb9306e80fd16522bfb6a4a98c3f2b42c0d
- Go CL (Code Review): https://golang.org/cl/5796072
参考にした情報源リンク
- コミットメッセージと変更されたソースコード
- Go言語の公式ドキュメント(
go get、go/buildパッケージに関する一般的な知識) - Go言語のワイルドカード(
...)に関する一般的な情報 - GoのIssueトラッカー(#2909の具体的な内容は、公開されている情報からは特定できませんでしたが、コミットメッセージからその目的を推測しました。)